From d6a8db5c6cf85f77b46bb7500572448a02952134 Mon Sep 17 00:00:00 2001 From: kimi1 Date: Thu, 17 Sep 2026 19:35:04 +0800 Subject: [PATCH] =?UTF-8?q?quilt=20kernel=20P6:=20tidepool=20=E2=80=94=20t?= =?UTF-8?q?he=20helper-thread=20memory=20ocean=20joins=20the=20ledger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port of the tidepool v1 bench contract (/tmp/tidepool/README.md is the spec; the README wins wherever bench implementations diverged). - src/tidepool/ocean.ts — markdown ocean + recall index; weighted token scoring (helper/question x2, response/meta x1), tie to most recent, clamp 3..5, deterministic (replay-reproducible, no embeddings) - src/tidepool/stall.ts — threadLengthMonitor as a pure function; absence is the information (P2 negative-ledger pattern) - src/tidepool/projection.ts — projectHelperThread BIND-for-BIND (L1 law: channel + helper endpoints bound before the thread links them); projectQuietThread mirrors .refused; replayThreadIndexFromWal folds WAL-only rows; readWalRowsByMutation/readWalRowsSince for API read-back and tip continuation (tolerant of SqliteD1 sync-all and D1 async-all) - src/tidepool/index.ts — six public entry points; every write is a DUAL-WRITE (ocean markdown + quilt WAL); WAL failures caught and counted (local counter, forward-compatible with PR #5 ops counters) - helper_threads + helper_logs migration 0015 (drizzle-generated with snapshot + journal) - 11 tests: chain verifies with hermit's own verifyChain, L1 link order, single-mutation channel sweep, recall scoring/clamp/determinism, quiet detection + newlyQuietThreads crossing-only, drain negative ledger (helper_threads untouched), WAL-only replay parity, markdown reload parity, tip continuation, WAL-failure posture (memory survives, failure counted), oath audit (append-only, timestamps) Refs #48 --- drizzle/0015_flimsy_tenebrous.sql | 28 + drizzle/meta/0015_snapshot.json | 2307 +++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/db/schema.ts | 44 + src/tidepool/index.ts | 127 ++ src/tidepool/ocean.ts | 210 +++ src/tidepool/projection.ts | 172 +++ src/tidepool/stall.ts | 72 + tests/quiltKernelTidepool.test.ts | 355 +++++ 9 files changed, 3322 insertions(+) create mode 100644 drizzle/0015_flimsy_tenebrous.sql create mode 100644 drizzle/meta/0015_snapshot.json create mode 100644 src/tidepool/index.ts create mode 100644 src/tidepool/ocean.ts create mode 100644 src/tidepool/projection.ts create mode 100644 src/tidepool/stall.ts create mode 100644 tests/quiltKernelTidepool.test.ts diff --git a/drizzle/0015_flimsy_tenebrous.sql b/drizzle/0015_flimsy_tenebrous.sql new file mode 100644 index 0000000..fa406a9 --- /dev/null +++ b/drizzle/0015_flimsy_tenebrous.sql @@ -0,0 +1,28 @@ +CREATE TABLE `helper_logs` ( + `guild_id` text NOT NULL, + `channel_id` text NOT NULL, + `thread_id` text PRIMARY KEY NOT NULL, + `messages` text NOT NULL, + `created_at` text NOT NULL, + `updated_at` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `helper_threads` ( + `id` text PRIMARY KEY NOT NULL, + `guild_id` text NOT NULL, + `channel_id` text NOT NULL, + `user_id` text NOT NULL, + `helper_key` text NOT NULL, + `helper_name` text NOT NULL, + `question_text` text NOT NULL, + `response_text` text NOT NULL, + `thinking_level` text NOT NULL, + `response_length` integer NOT NULL, + `created_at` text NOT NULL, + `author_tag` text NOT NULL, + `author_username` text NOT NULL, + `last_message_id` text +); +--> statement-breakpoint +CREATE INDEX `helper_threads_channel_idx` ON `helper_threads` (`channel_id`);--> statement-breakpoint +CREATE INDEX `helper_threads_guild_idx` ON `helper_threads` (`guild_id`); \ No newline at end of file diff --git a/drizzle/meta/0015_snapshot.json b/drizzle/meta/0015_snapshot.json new file mode 100644 index 0000000..14c2b83 --- /dev/null +++ b/drizzle/meta/0015_snapshot.json @@ -0,0 +1,2307 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "09cc2a7e-5038-47c2-9e53-8c55c22dc2c6", + "prevId": "0079cbb5-f850-4f68-8704-7f5314ef2c48", + "tables": { + "action_cooldown_events": { + "name": "action_cooldown_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "interaction_id": { + "name": "interaction_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action_kind": { + "name": "action_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild_id": { + "name": "guild_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor_expires_at": { + "name": "actor_expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_expires_at": { + "name": "target_expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_expires_at": { + "name": "channel_expires_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "action_cooldown_events_interaction_id_unique": { + "name": "action_cooldown_events_interaction_id_unique", + "columns": [ + "interaction_id" + ], + "isUnique": true + }, + "idx_action_cooldowns_actor": { + "name": "idx_action_cooldowns_actor", + "columns": [ + "guild_id", + "actor_id", + "actor_expires_at" + ], + "isUnique": false + }, + "idx_action_cooldowns_target": { + "name": "idx_action_cooldowns_target", + "columns": [ + "guild_id", + "target_id", + "target_expires_at" + ], + "isUnique": false + }, + "idx_action_cooldowns_channel": { + "name": "idx_action_cooldowns_channel", + "columns": [ + "guild_id", + "channel_id", + "channel_expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "claim_requests": { + "name": "claim_requests", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "guild_id": { + "name": "guild_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'submitted'" + }, + "github_username": { + "name": "github_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "merged_pr_count": { + "name": "merged_pr_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "review_message_id": { + "name": "review_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "review_thread_id": { + "name": "review_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "decided_at": { + "name": "decided_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "decided_by_id": { + "name": "decided_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "decision_reason": { + "name": "decision_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "idx_claim_requests_guild_user": { + "name": "idx_claim_requests_guild_user", + "columns": [ + "guild_id", + "user_id" + ], + "isUnique": true + }, + "idx_claim_requests_user_id": { + "name": "idx_claim_requests_user_id", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "idx_claim_requests_status": { + "name": "idx_claim_requests_status", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "clawhub_content_rights_cases": { + "name": "clawhub_content_rights_cases", + "columns": { + "case_id": { + "name": "case_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "form_submission_id": { + "name": "form_submission_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'submitted'" + }, + "requester_name": { + "name": "requester_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization": { + "name": "organization", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "clawhub_urls": { + "name": "clawhub_urls", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "explanation": { + "name": "explanation", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "clawhub_content_rights_cases_form_submission_id_unique": { + "name": "clawhub_content_rights_cases_form_submission_id_unique", + "columns": [ + "form_submission_id" + ], + "isUnique": true + }, + "idx_clawhub_content_rights_cases_status": { + "name": "idx_clawhub_content_rights_cases_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_clawhub_content_rights_cases_email": { + "name": "idx_clawhub_content_rights_cases_email", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "clawhub_content_rights_events": { + "name": "clawhub_content_rights_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "case_id": { + "name": "case_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "idx_clawhub_content_rights_events_case_id": { + "name": "idx_clawhub_content_rights_events_case_id", + "columns": [ + "case_id" + ], + "isUnique": false + }, + "idx_clawhub_content_rights_events_event_type": { + "name": "idx_clawhub_content_rights_events_event_type", + "columns": [ + "event_type" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "clawhub_content_rights_files": { + "name": "clawhub_content_rights_files", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "case_id": { + "name": "case_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "clawhub_content_rights_files_object_key_unique": { + "name": "clawhub_content_rights_files_object_key_unique", + "columns": [ + "object_key" + ], + "isUnique": true + }, + "idx_clawhub_content_rights_files_case_id": { + "name": "idx_clawhub_content_rights_files_case_id", + "columns": [ + "case_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "form_submissions": { + "name": "form_submissions", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'submitted'" + }, + "auth_provider": { + "name": "auth_provider", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "applicant_id": { + "name": "applicant_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "applicant_username": { + "name": "applicant_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "payload": { + "name": "payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "review_channel_id": { + "name": "review_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "review_message_id": { + "name": "review_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "review_thread_id": { + "name": "review_thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "decided_at": { + "name": "decided_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "decided_by_id": { + "name": "decided_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "decision_reason": { + "name": "decision_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "action_result": { + "name": "action_result", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "idx_form_submissions_form_id": { + "name": "idx_form_submissions_form_id", + "columns": [ + "form_id" + ], + "isUnique": false + }, + "idx_form_submissions_status": { + "name": "idx_form_submissions_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_form_submissions_applicant_id": { + "name": "idx_form_submissions_applicant_id", + "columns": [ + "applicant_id" + ], + "isUnique": false + }, + "idx_form_submissions_review_message_id": { + "name": "idx_form_submissions_review_message_id", + "columns": [ + "review_message_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "helper_events": { + "name": "helper_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'helper_command'" + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "event_time": { + "name": "event_time", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoked_by_id": { + "name": "invoked_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoked_by_username": { + "name": "invoked_by_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoked_by_global_name": { + "name": "invoked_by_global_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "idx_helper_events_event_time": { + "name": "idx_helper_events_event_time", + "columns": [ + "event_time" + ], + "isUnique": false + }, + "idx_helper_events_command": { + "name": "idx_helper_events_command", + "columns": [ + "command" + ], + "isUnique": false + }, + "idx_helper_events_thread_id": { + "name": "idx_helper_events_thread_id", + "columns": [ + "thread_id" + ], + "isUnique": false + }, + "idx_helper_events_invoked_by_id": { + "name": "idx_helper_events_invoked_by_id", + "columns": [ + "invoked_by_id" + ], + "isUnique": false + }, + "idx_helper_events_event_type": { + "name": "idx_helper_events_event_type", + "columns": [ + "event_type" + ], + "isUnique": false + }, + "idx_helper_events_thread_time": { + "name": "idx_helper_events_thread_time", + "columns": [ + "thread_id", + "event_time" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "helper_logs": { + "name": "helper_logs", + "columns": { + "guild_id": { + "name": "guild_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "messages": { + "name": "messages", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "helper_threads": { + "name": "helper_threads", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "guild_id": { + "name": "guild_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "helper_key": { + "name": "helper_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "helper_name": { + "name": "helper_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_text": { + "name": "question_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_text": { + "name": "response_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "thinking_level": { + "name": "thinking_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "response_length": { + "name": "response_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_tag": { + "name": "author_tag", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_username": { + "name": "author_username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_message_id": { + "name": "last_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "helper_threads_channel_idx": { + "name": "helper_threads_channel_idx", + "columns": [ + "channel_id" + ], + "isUnique": false + }, + "helper_threads_guild_idx": { + "name": "helper_threads_guild_idx", + "columns": [ + "guild_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "keyValue": { + "name": "keyValue", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updatedAt": { + "name": "updatedAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "lobster_encounters": { + "name": "lobster_encounters", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "interaction_id": { + "name": "interaction_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cooldown_event_id": { + "name": "cooldown_event_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild_id": { + "name": "guild_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_is_bot": { + "name": "target_is_bot", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "taxonomy_snapshot_id": { + "name": "taxonomy_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "species_aphia_id": { + "name": "species_aphia_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "species_accepted_name": { + "name": "species_accepted_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "species_display_name": { + "name": "species_display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "species_family": { + "name": "species_family", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scene_id": { + "name": "scene_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_url": { + "name": "asset_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "asset_checksum": { + "name": "asset_checksum", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headline": { + "name": "headline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "narrative": { + "name": "narrative", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metrics_json": { + "name": "metrics_json", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accessibility_description": { + "name": "accessibility_description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "publication_status": { + "name": "publication_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "publication_failure": { + "name": "publication_failure", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "publication_failed_at": { + "name": "publication_failed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "message_bound_at": { + "name": "message_bound_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_status": { + "name": "response_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "response_type": { + "name": "response_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_actor_id": { + "name": "response_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "responded_at": { + "name": "responded_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "response_result_json": { + "name": "response_result_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_actor_id": { + "name": "counter_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_target_id": { + "name": "counter_target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_scene_id": { + "name": "counter_scene_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_asset_url": { + "name": "counter_asset_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_asset_checksum": { + "name": "counter_asset_checksum", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_headline": { + "name": "counter_headline", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_narrative": { + "name": "counter_narrative", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_metrics_json": { + "name": "counter_metrics_json", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_accessibility_description": { + "name": "counter_accessibility_description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "lobster_encounters_interaction_id_unique": { + "name": "lobster_encounters_interaction_id_unique", + "columns": [ + "interaction_id" + ], + "isUnique": true + }, + "lobster_encounters_cooldown_event_id_unique": { + "name": "lobster_encounters_cooldown_event_id_unique", + "columns": [ + "cooldown_event_id" + ], + "isUnique": true + }, + "idx_lobster_encounters_message": { + "name": "idx_lobster_encounters_message", + "columns": [ + "guild_id", + "channel_id", + "message_id" + ], + "isUnique": false + }, + "idx_lobster_encounters_species": { + "name": "idx_lobster_encounters_species", + "columns": [ + "species_aphia_id" + ], + "isUnique": false + }, + "idx_lobster_encounters_publication": { + "name": "idx_lobster_encounters_publication", + "columns": [ + "publication_status" + ], + "isUnique": false + }, + "idx_lobster_encounters_response": { + "name": "idx_lobster_encounters_response", + "columns": [ + "response_status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "nomination_approvals": { + "name": "nomination_approvals", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "nomination_id": { + "name": "nomination_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approver_id": { + "name": "approver_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "vote_choice": { + "name": "vote_choice", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'approve'" + }, + "mutation_id": { + "name": "mutation_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "idx_nomination_approvals_nomination_approver": { + "name": "idx_nomination_approvals_nomination_approver", + "columns": [ + "nomination_id", + "approver_id" + ], + "isUnique": true + }, + "idx_nomination_approvals_nomination_id": { + "name": "idx_nomination_approvals_nomination_id", + "columns": [ + "nomination_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "nominations": { + "name": "nominations", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "guild_id": { + "name": "guild_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nominee_id": { + "name": "nominee_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nominator_id": { + "name": "nominator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'No reason provided.'" + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_role_id": { + "name": "target_role_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "required_approvals": { + "name": "required_approvals", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'submitted'" + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "completed_at": { + "name": "completed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "desired_card_revision": { + "name": "desired_card_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "synced_card_revision": { + "name": "synced_card_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "card_sync_started_at": { + "name": "card_sync_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "card_sync_failure_count": { + "name": "card_sync_failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "grant_started_at": { + "name": "grant_started_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grant_failure_count": { + "name": "grant_failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "idx_nominations_active_unique": { + "name": "idx_nominations_active_unique", + "columns": [ + "guild_id", + "nominee_id", + "target_role_id" + ], + "isUnique": true, + "where": "\"nominations\".\"status\" in ('submitted', 'granting')" + }, + "idx_nominations_status": { + "name": "idx_nominations_status", + "columns": [ + "status" + ], + "isUnique": false + }, + "idx_nominations_card_sync": { + "name": "idx_nominations_card_sync", + "columns": [ + "desired_card_revision", + "synced_card_revision" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "quilt_wal": { + "name": "quilt_wal", + "columns": { + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "mutation_id": { + "name": "mutation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ts": { + "name": "ts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cell": { + "name": "cell", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "op": { + "name": "op", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "prev_hash": { + "name": "prev_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "quilt_wal_mutation_idx": { + "name": "quilt_wal_mutation_idx", + "columns": [ + "mutation_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "quilt_wal_lock": { + "name": "quilt_wal_lock", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "holder": { + "name": "holder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "acquired_at": { + "name": "acquired_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reddit_moderation_contexts": { + "name": "reddit_moderation_contexts", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "subreddit": { + "name": "subreddit", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'moderated'" + }, + "unaction": { + "name": "unaction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'reviewed'" + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "moderator": { + "name": "moderator", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned_at": { + "name": "banned_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "idx_reddit_moderation_contexts_subreddit_username": { + "name": "idx_reddit_moderation_contexts_subreddit_username", + "columns": [ + "subreddit", + "username" + ], + "isUnique": true + }, + "idx_reddit_moderation_contexts_username": { + "name": "idx_reddit_moderation_contexts_username", + "columns": [ + "username" + ], + "isUnique": false + }, + "idx_reddit_moderation_contexts_action": { + "name": "idx_reddit_moderation_contexts_action", + "columns": [ + "action" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "slap_events": { + "name": "slap_events", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "interaction_id": { + "name": "interaction_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "guild_id": { + "name": "guild_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_is_bot": { + "name": "target_is_bot", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "fish_slug": { + "name": "fish_slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fish_name": { + "name": "fish_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rarity": { + "name": "rarity", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "headline": { + "name": "headline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "narrative": { + "name": "narrative", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impact": { + "name": "impact", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "dignity_remaining": { + "name": "dignity_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fish_condition": { + "name": "fish_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter_actor_id": { + "name": "counter_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_target_id": { + "name": "counter_target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_fish_slug": { + "name": "counter_fish_slug", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_fish_name": { + "name": "counter_fish_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_rarity": { + "name": "counter_rarity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_outcome": { + "name": "counter_outcome", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_headline": { + "name": "counter_headline", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_narrative": { + "name": "counter_narrative", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_impact": { + "name": "counter_impact", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_dignity_remaining": { + "name": "counter_dignity_remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_fish_condition": { + "name": "counter_fish_condition", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "counter_image_url": { + "name": "counter_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "countered_at": { + "name": "countered_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "appealed_by_id": { + "name": "appealed_by_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "appeal_ruling": { + "name": "appeal_ruling", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "appealed_at": { + "name": "appealed_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "updated_at": { + "name": "updated_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + } + }, + "indexes": { + "slap_events_interaction_id_unique": { + "name": "slap_events_interaction_id_unique", + "columns": [ + "interaction_id" + ], + "isUnique": true + }, + "idx_slap_events_actor_cooldown": { + "name": "idx_slap_events_actor_cooldown", + "columns": [ + "guild_id", + "actor_id", + "created_at" + ], + "isUnique": false + }, + "idx_slap_events_target_cooldown": { + "name": "idx_slap_events_target_cooldown", + "columns": [ + "guild_id", + "target_id", + "created_at" + ], + "isUnique": false + }, + "idx_slap_events_channel_cooldown": { + "name": "idx_slap_events_channel_cooldown", + "columns": [ + "guild_id", + "channel_id", + "created_at" + ], + "isUnique": false + }, + "idx_slap_events_message": { + "name": "idx_slap_events_message", + "columns": [ + "guild_id", + "channel_id", + "message_id" + ], + "isUnique": false + }, + "idx_slap_events_outcome": { + "name": "idx_slap_events_outcome", + "columns": [ + "outcome" + ], + "isUnique": false + }, + "idx_slap_events_rarity": { + "name": "idx_slap_events_rarity", + "columns": [ + "rarity" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tracked_threads": { + "name": "tracked_threads", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_checked": { + "name": "last_checked", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved": { + "name": "solved", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "warning_level": { + "name": "warning_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "closed": { + "name": "closed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_message_count": { + "name": "last_message_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "received_at": { + "name": "received_at", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))" + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tracked_threads_thread_id_unique": { + "name": "tracked_threads_thread_id_unique", + "columns": [ + "thread_id" + ], + "isUnique": true + }, + "idx_tracked_threads_solved": { + "name": "idx_tracked_threads_solved", + "columns": [ + "solved" + ], + "isUnique": false + }, + "idx_tracked_threads_last_checked": { + "name": "idx_tracked_threads_last_checked", + "columns": [ + "last_checked" + ], + "isUnique": false + }, + "idx_tracked_threads_received_at": { + "name": "idx_tracked_threads_received_at", + "columns": [ + "received_at" + ], + "isUnique": false + }, + "idx_tracked_threads_closed": { + "name": "idx_tracked_threads_closed", + "columns": [ + "closed" + ], + "isUnique": false + }, + "idx_tracked_threads_warning_level": { + "name": "idx_tracked_threads_warning_level", + "columns": [ + "warning_level" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 165923f..8444011 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -106,6 +106,13 @@ "when": 1789640536915, "tag": "0014_wandering_owl", "breakpoints": true + }, + { + "idx": 15, + "version": "6", + "when": 1789643883055, + "tag": "0015_flimsy_tenebrous", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 5fbf930..9dbdf78 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -500,3 +500,47 @@ export const quiltWalLock = sqliteTable("quilt_wal_lock", { holder: text().notNull(), acquiredAt: text("acquired_at").notNull() }) + +// tidepool v1: helper-thread memory ocean. Per the contract the WAL is the +// source of truth — these tables are the materialized view for live reads; +// replay from WAL rebuilds the recall index bit-for-bit. +export const helperThreads = sqliteTable( + "helper_threads", + { + id: text().primaryKey(), // discord thread id + guildId: text("guild_id").notNull(), + channelId: text("channel_id").notNull(), + userId: text("user_id").notNull(), + helperKey: text("helper_key").notNull(), + helperName: text("helper_name").notNull(), + questionText: text("question_text").notNull(), + responseText: text("response_text").notNull(), + thinkingLevel: text("thinking_level").notNull(), + responseLength: integer("response_length").notNull(), + createdAt: text("created_at").notNull(), + authorTag: text("author_tag").notNull(), + authorUsername: text("author_username").notNull(), + lastMessageId: text("last_message_id") + }, + (table) => [ + index("helper_threads_channel_idx").on(table.channelId), + index("helper_threads_guild_idx").on(table.guildId) + ] +) +export type HelperThread = typeof helperThreads.$inferSelect +export type NewHelperThread = typeof helperThreads.$inferInsert + +// Raw message log per thread (JSON string), append-only. +export const helperLogs = sqliteTable( + "helper_logs", + { + guildId: text("guild_id").notNull(), + channelId: text("channel_id").notNull(), + threadId: text("thread_id").notNull().primaryKey(), + messages: text().notNull(), // JSON string, append-only + createdAt: text("created_at").notNull(), + updatedAt: text("updated_at").notNull() + } +) +export type HelperLog = typeof helperLogs.$inferSelect +export type NewHelperLog = typeof helperLogs.$inferInsert diff --git a/src/tidepool/index.ts b/src/tidepool/index.ts new file mode 100644 index 0000000..e79ab78 --- /dev/null +++ b/src/tidepool/index.ts @@ -0,0 +1,127 @@ +// index.ts — the hermit-facing tidepool v1 API. +// +// Every write is a DUAL-WRITE: the ocean markdown is the human-readable +// memory; the quilt WAL is the replayable ledger (contract: /tmp/tidepool +// README). WAL failures are caught and counted, never thrown into the +// helper path — the P1/P2 risk posture. When PR #5 (ops counters) lands, +// the local counter below wires into recordWalFailure("tidepool_projection"). + +import { commitProjection, type WalClient } from "../quilt/commit.js" +import type { WalRow } from "../quilt/projection.js" +import { + TidepoolOcean, + type HelperThreadSummary, + type OceanEntry +} from "./ocean.js" +import { + projectHelperThread, + projectQuietThread, + readWalRowsByMutation +} from "./projection.js" +import type { QuietThread } from "./stall.js" + +// Local failure counter (forward-compatible with src/quilt/ops.ts). +let walFailureCount = 0 +export const getTidepoolWalFailures = (): number => walFailureCount +export const resetTidepoolWalFailures = (): void => { + walFailureCount = 0 +} + +/** + * Remember one helper-thread summary: append to the ocean AND project + * BIND-for-BIND into the quilt WAL (one mutation). Returns the WAL rows + * read back from the ledger — the WAL is the source of truth. + */ +export const rememberHelperThread = async ( + ocean: TidepoolOcean, + walClient: WalClient, + thread: HelperThreadSummary, + ctx: { mutationId: string; ts: string } +): Promise<{ section: string; entry: OceanEntry; walRows: WalRow[] }> => { + const { section, entry } = await ocean.remember(thread) + let walRows: WalRow[] = [] + try { + await commitProjection(walClient, ctx, (kernel) => { + projectHelperThread(kernel, thread, ctx.ts) + }) + walRows = await readWalRowsByMutation( + walClient as unknown as Parameters[0], + ctx.mutationId + ) + } catch (error) { + walFailureCount += 1 + console.warn("quilt wal tidepool projection failed", error) + } + return { section, entry, walRows } +} + +/** + * Channel sweep: remember N threads in ONE projection → ONE mutation_id, + * ONE WAL batch (the hermit commit-batch shape). + */ +export const rememberChannelThreads = async ( + ocean: TidepoolOcean, + walClient: WalClient, + threads: HelperThreadSummary[], + ctx: { mutationId: string; ts: string } +): Promise<{ remembered: Array<{ section: string; entry: OceanEntry }>; walRows: WalRow[] }> => { + const remembered = await ocean.rememberAll(threads) + let walRows: WalRow[] = [] + try { + await commitProjection(walClient, ctx, (kernel) => { + for (const thread of threads) { + projectHelperThread(kernel, thread, ctx.ts) + } + }) + walRows = await readWalRowsByMutation( + walClient as unknown as Parameters[0], + ctx.mutationId + ) + } catch (error) { + walFailureCount += 1 + console.warn("quilt wal tidepool projection failed", error) + } + return { remembered, walRows } +} + +/** Top 3–5 relevant thread memories. Deterministic, replay-reproducible. */ +export const recallHelperContext = ( + ocean: TidepoolOcean, + query: string, + { limit = 5 }: { limit?: number } = {} +): Promise => ocean.recall(query, { limit }) + +/** The oath log: append-only, timestamped, immutable. */ +export const rememberBotAction = ( + ocean: TidepoolOcean, + action: string +): Promise<{ ts: string; action: string }> => ocean.botAction(action) + +/** + * threadLengthMonitor's negative ledger: drain detected quiet threads into + * the WAL. Absence is the information (P2 `.refused` pattern) — no + * helper_threads row is touched. + */ +export const drainQuietThreads = async ( + walClient: WalClient, + quietList: QuietThread[], + ctx: { mutationId: string; ts: string } +): Promise<{ walRows: WalRow[] }> => { + if (quietList.length === 0) return { walRows: [] } + let walRows: WalRow[] = [] + try { + await commitProjection(walClient, ctx, (kernel) => { + for (const quiet of quietList) { + projectQuietThread(kernel, quiet, ctx.ts) + } + }) + walRows = await readWalRowsByMutation( + walClient as unknown as Parameters[0], + ctx.mutationId + ) + } catch (error) { + walFailureCount += 1 + console.warn("quilt wal tidepool projection failed", error) + } + return { walRows } +} diff --git a/src/tidepool/ocean.ts b/src/tidepool/ocean.ts new file mode 100644 index 0000000..46803f4 --- /dev/null +++ b/src/tidepool/ocean.ts @@ -0,0 +1,210 @@ +// ocean.ts — the tidepool ocean: a markdown memory file with a recall index. +// +// Port of the tidepool v1 bench contract (/tmp/tidepool/README.md): helper +// thread summaries are stored as `## thread:` sections; recall is +// deterministic token-overlap scoring so WAL replay reproduces it +// bit-for-bit (no embeddings — hermit has no vector dependency). +// +// rememberBotAction oaths (enforced by API shape, audited by stats()): +// 1. NEVER modify messages — the ocean exposes no edit/delete API. +// 2. ALWAYS append-only — re-remembering appends a NEW section; the old +// one stays (a memory of a memory is itself a memory). +// 3. ALWAYS include timestamps — every section carries rememberedAt, +// every bot-action line carries an ISO ts. + +import { appendFile, readFile } from "node:fs/promises" + +export type OceanEntry = { + id: string + guildId: string + channelId: string + userId: string + helperKey: string + helperName: string + questionText: string + responseText: string + thinkingLevel: string + responseLength: number + createdAt: string + authorTag: string + authorUsername: string + lastMessageId: string + rememberedAt: string +} + +export type HelperThreadSummary = Omit + +const SECTION_RE = /^## thread:([^\n]+)$/gm + +const tokenize = (text: string): string[] => + String(text ?? "") + .toLowerCase() + .split(/[^a-z0-9_]+/) + .filter((t) => t.length >= 2) + +export class TidepoolOcean { + readonly path: string | null + private readonly now: () => string + markdown: string + /** threadId -> recall entry (latest memory wins) */ + index = new Map() + private actionCount = 0 + + constructor({ + path = null, + now = () => new Date().toISOString() + }: { path?: string | null; now?: () => string } = {}) { + this.path = path + this.now = now + this.markdown = + "# Tidepool Ocean\n\n\n" + } + + // The ONLY write path — oath-checked append. + private async append(text: string): Promise { + this.markdown += text + if (this.path) { + await appendFile(this.path, text, "utf8") + } + } + + /** Remember one thread summary: new section + index upsert. */ + async remember(thread: HelperThreadSummary): Promise<{ section: string; entry: OceanEntry }> { + const ts = this.now() + const entry: OceanEntry = { ...thread, rememberedAt: ts } + const section = + `## thread:${thread.id}\n` + + `rememberedAt: ${ts}\n` + + `helper: ${thread.helperName} (${thread.helperKey})\n` + + `channel: ${thread.channelId} guild: ${thread.guildId}\n` + + `author: ${thread.authorUsername} (${thread.authorTag})\n` + + `thinking: ${thread.thinkingLevel} responseChars: ${thread.responseLength}\n` + + `question: ${thread.questionText}\n` + + `response: ${thread.responseText}\n` + + `lastMessage: ${thread.lastMessageId}\n\n` + await this.append(section) + this.index.set(thread.id, entry) + return { section, entry } + } + + /** Channel sweep shape: remember several summaries (one WAL mutation upstream). */ + async rememberAll( + threads: HelperThreadSummary[] + ): Promise> { + const out: Array<{ section: string; entry: OceanEntry }> = [] + for (const thread of threads) { + out.push(await this.remember(thread)) + } + return out + } + + /** + * Top `limit` (default 5, clamped 3..5) relevant entries. + * helperName/question matches weigh 2, response/meta matches weigh 1; + * ties break to the most recently remembered. Deterministic. + */ + async recall(query: string, { limit = 5 }: { limit?: number } = {}): Promise { + const clamped = Math.max(3, Math.min(5, limit)) + const tokens = tokenize(query) + if (tokens.length === 0) return [] + const scored: Array<{ entry: OceanEntry; score: number }> = [] + for (const entry of this.index.values()) { + const weighted = { + q: tokenize(`${entry.helperName} ${entry.questionText}`), + r: tokenize( + `${entry.responseText} ${entry.helperKey} ${entry.authorUsername} ${entry.thinkingLevel}` + ) + } + let score = 0 + for (const token of tokens) { + if (weighted.q.includes(token)) score += 2 + if (weighted.r.includes(token)) score += 1 + } + if (score > 0) scored.push({ entry, score }) + } + scored.sort( + (a, b) => + b.score - a.score || + String(b.entry.rememberedAt).localeCompare(String(a.entry.rememberedAt)) + ) + return scored.slice(0, clamped).map((s) => s.entry) + } + + /** Oath log: append-only, timestamped, immutable. */ + async botAction(action: string): Promise<{ ts: string; action: string }> { + const ts = this.now() + const line = `- ${ts} ${String(action).replace(/[\n\r]+/g, " ")}\n` + if (this.actionCount === 0) { + await this.append("\n## bot-actions\n") + } + await this.append(line) + this.actionCount += 1 + return { ts, action: line.trim() } + } + + /** Oath audit: every thread section carries rememberedAt. */ + stats(): { + threadSections: number + indexEntries: number + botActionLines: number + oathViolations: number + bytes: number + } { + const sections = [...this.markdown.matchAll(SECTION_RE)] + let oathViolations = 0 + for (const match of sections) { + const rest = this.markdown.slice(match.index) + const end = rest.indexOf("\n## ", 4) + const body = end === -1 ? rest : rest.slice(0, end) + if (!/^rememberedAt: \S+/m.test(body)) oathViolations += 1 + } + return { + threadSections: sections.length, + indexEntries: this.index.size, + botActionLines: this.actionCount, + oathViolations, + bytes: Buffer.byteLength(this.markdown) + } + } + + /** Load an existing ocean file back (latest section per thread wins). */ + async load(): Promise { + if (!this.path) throw new Error("ocean has no path") + this.markdown = await readFile(this.path, "utf8") + this.index.clear() + this.actionCount = 0 + const sections = [...this.markdown.matchAll(SECTION_RE)] + for (const match of sections) { + const rest = this.markdown.slice(match.index) + const end = rest.indexOf("\n## ", 4) + const body = (end === -1 ? rest : rest.slice(0, end)).split("\n") + const id = match[1].trim() + const get = (prefix: string): string => { + const line = body.find((l) => l.startsWith(prefix)) + return line ? line.slice(prefix.length).trim() : "" + } + const helper = get("helper: ").match(/^(.*) \((.*)\)$/) + const channel = get("channel: ").match(/^(.*) guild: (.*)$/) + const author = get("author: ").match(/^(.*) \((.*)\)$/) + const thinking = get("thinking: ").match(/^(.*) responseChars: (.*)$/) + this.index.set(id, { + id, + rememberedAt: get("rememberedAt: "), + helperName: helper?.[1] ?? "", + helperKey: helper?.[2] ?? "", + channelId: channel?.[1] ?? "", + guildId: channel?.[2] ?? "", + authorUsername: author?.[1] ?? "", + authorTag: author?.[2] ?? "", + thinkingLevel: thinking?.[1] ?? "", + responseLength: Number(thinking?.[2] ?? 0), + questionText: get("question: "), + responseText: get("response: "), + lastMessageId: get("lastMessage: "), + userId: "", + createdAt: "" + }) + } + return this + } +} diff --git a/src/tidepool/projection.ts b/src/tidepool/projection.ts new file mode 100644 index 0000000..00c9281 --- /dev/null +++ b/src/tidepool/projection.ts @@ -0,0 +1,172 @@ +// projection.ts — tidepool's quilt WAL projection. +// +// The tidepool-specific projectors follow the hermit P1/P2 patterns: +// projectHelperThread mirrors projectNominationVote / projectLobsterEncounter +// (BIND-for-BIND, link endpoints must exist — L1 law); projectQuietThread +// mirrors the P2 `.refused` negative ledger — a quiet thread is an absence, +// and the absence is the information. +// +// Import, never duplicate: hash-chain primitives live in src/quilt/*. + +import type { QuiltKernel } from "../quilt/reference-kernel.mjs" +import type { WalRow } from "../quilt/projection.js" +import type { HelperThreadSummary, OceanEntry } from "./ocean.js" +import type { QuietThread } from "./stall.js" + +export type OceanIndexEntry = OceanEntry & { + quiet?: { + silentPolls: number + lastCount: number + since: string + ts: string + } | null +} + +/** + * Helper-thread memory projected BIND-for-BIND. The thread is a cell, + * every field is a cell, the thread LINKs to its channel and helper + * (endpoints bound first — L1 law), and the rememberedAt bind carries + * {ts} meta provenance. One call = one mutation's worth of kernel events. + */ +export const projectHelperThread = ( + kernel: QuiltKernel, + thread: HelperThreadSummary, + ts: string +): void => { + const base = `thread.${thread.id}` + const channelCell = `channel.${thread.channelId}` + kernel.bind(channelCell, { kind: "channel", guildId: thread.guildId }) + const helperCell = `helper.${thread.helperKey}` + kernel.bind(helperCell, { kind: "helper", helperName: thread.helperName }) + + kernel.bind(base, { + kind: "helper-thread", + id: thread.id, + guildId: thread.guildId, + channelId: thread.channelId, + userId: thread.userId, + helperKey: thread.helperKey, + helperName: thread.helperName, + createdAt: thread.createdAt + }) + kernel.bind(`${base}.question`, thread.questionText) + kernel.bind(`${base}.response`, thread.responseText) + kernel.bind(`${base}.meta`, { + thinkingLevel: thread.thinkingLevel, + responseLength: thread.responseLength, + authorTag: thread.authorTag, + authorUsername: thread.authorUsername + }) + kernel.bind(`${base}.lastMessage`, thread.lastMessageId) + kernel.bind(`${base}.rememberedAt`, ts, { ts }) + kernel.link(base, channelCell, "lives-in") + kernel.link(base, helperCell, "served-by") +} + +/** + * The negative ledger for the ocean: a thread whose message count stopped + * growing. Mirrors hermit's P2 `.refused` cells — live D1 (helper_threads) + * has no row for this; the WAL sees what the live table cannot. + */ +export const projectQuietThread = ( + kernel: QuiltKernel, + quiet: QuietThread, + ts: string +): void => { + kernel.bind(`thread.${quiet.threadId}.quiet`, { + silentPolls: quiet.silentPolls, + lastCount: quiet.lastCount, + since: quiet.since, + ts + }) +} + +/** + * Replay the recall index from WAL rows ALONE. Only BINDs carry state; the + * latest bind of each cell wins, so re-remembered threads refresh and + * `.quiet` state survives alongside. (Pattern: replayNominationFromWal / + * replayEncounterFromWal.) Pass rows read with readWalRowsSince for tip + * continuation — replay(rows) is a pure fold. + */ +export const replayThreadIndexFromWal = ( + rows: WalRow[] +): Map => { + const cells = new Map() + for (const row of rows) { + if (row.op !== "bind") continue + cells.set(row.cell, row.value === null ? null : JSON.parse(row.value)) + } + const index = new Map() + for (const [name, value] of cells) { + if (!name.startsWith("thread.")) continue + const rest = name.slice("thread.".length) + const dot = rest.indexOf(".") + const threadId = dot === -1 ? rest : rest.slice(0, dot) + if (!index.has(threadId)) { + index.set(threadId, { id: threadId } as OceanIndexEntry) + } + const entry = index.get(threadId)! + if (dot === -1) { + Object.assign(entry, (value ?? {}) as object) + } else { + const field = rest.slice(dot + 1) + if (field === "question") entry.questionText = value as string + else if (field === "response") entry.responseText = value as string + else if (field === "meta") Object.assign(entry, (value ?? {}) as object) + else if (field === "lastMessage") entry.lastMessageId = value as string + else if (field === "rememberedAt") entry.rememberedAt = value as string + else if (field === "quiet") entry.quiet = value as OceanIndexEntry["quiet"] + } + } + return index +} + +// ---- WAL read-back ------------------------------------------------------- +// +// The public API returns the rows it actually wrote. The honest source is +// the WAL itself (the WAL is the source of truth), so we read back by +// mutation / by tip. The client adapter tolerates both SqliteD1Database +// (sync .all() -> T[]) and Cloudflare D1 (async .all() -> {results}). + +type BoundStatement = { + all: () => unknown +} +export type WalReadClient = { + prepare: (query: string) => { bind: (...values: unknown[]) => BoundStatement } +} + +const unwrap = (out: unknown): Array> => { + if (Array.isArray(out)) return out as Array> + const results = (out as { results?: Array> } | null) + ?.results + return Array.isArray(results) ? results : [] +} + +export const readWalRowsByMutation = async ( + client: WalReadClient, + mutationId: string +): Promise => { + const stmt = client + .prepare( + `select seq, mutation_id, ts, cell, op, value, prev_hash, hash + from quilt_wal where mutation_id = ? order by seq` + ) + .bind(mutationId) + const out = await Promise.resolve(stmt.all()) + return unwrap(out) as unknown as WalRow[] +} + +/** Tip continuation: rows strictly after `sinceSeq`, in chain order. */ +export const readWalRowsSince = async ( + client: WalReadClient, + sinceSeq: number +): Promise => { + const stmt = client + .prepare( + `select seq, mutation_id, ts, cell, op, value, prev_hash, hash + from quilt_wal where seq > ? order by seq` + ) + .bind(sinceSeq) + const out = await Promise.resolve(stmt.all()) + return unwrap(out) as unknown as WalRow[] +} diff --git a/src/tidepool/stall.ts b/src/tidepool/stall.ts new file mode 100644 index 0000000..e3f4020 --- /dev/null +++ b/src/tidepool/stall.ts @@ -0,0 +1,72 @@ +// stall.ts — threadLengthMonitor as a PURE function. +// +// The recalled design had an edge function polling thread lengths every 30 +// minutes. The decision core is pure: given a length history, a thread +// whose message count stops growing across N consecutive polls becomes a +// "quiet thread" memory. The absence is the information — hermit's P2 +// negative-ledger pattern applied to helper threads. + +export type ThreadLengthPoll = { + threadId: string + ts: string + messageCount: number +} + +export type QuietThread = { + threadId: string + silentPolls: number + lastCount: number + since: string + ts: string +} + +/** + * History in, quiet list out — no I/O, no clock (timestamps come from + * the samples). `window` = consecutive flat polls required (default 3). + * `since` = ts of the last growth, `ts` = ts of the newest sample. + */ +export const detectQuietThreads = ( + polls: ThreadLengthPoll[], + { window = 3 }: { window?: number } = {} +): QuietThread[] => { + const byThread = new Map() + for (const poll of polls) { + if (!byThread.has(poll.threadId)) byThread.set(poll.threadId, []) + byThread.get(poll.threadId)!.push(poll) + } + const quiet: QuietThread[] = [] + for (const [threadId, samples] of byThread) { + samples.sort((a, b) => String(a.ts).localeCompare(String(b.ts))) + let flat = 0 + let since = samples[0]?.ts ?? "" + for (let i = 1; i < samples.length; i += 1) { + if (samples[i].messageCount > samples[i - 1].messageCount) { + flat = 0 + since = samples[i].ts + } else { + flat += 1 + } + } + if (flat >= window - 1 && samples.length >= window) { + quiet.push({ + threadId, + silentPolls: flat + 1, + lastCount: samples[samples.length - 1].messageCount, + since, + ts: samples[samples.length - 1].ts + }) + } + } + return quiet +} + +/** + * Crossing-only delta for a 30-minute cron: only threads that JUST crossed + * the threshold on the most recent poll. Threads already quiet stay in the + * ocean — silence is remembered once. (Bench extra; pure composition.) + */ +export const newlyQuietThreads = ( + polls: ThreadLengthPoll[], + { window = 3 }: { window?: number } = {} +): QuietThread[] => + detectQuietThreads(polls, { window }).filter((q) => q.silentPolls === window) diff --git a/tests/quiltKernelTidepool.test.ts b/tests/quiltKernelTidepool.test.ts new file mode 100644 index 0000000..26be0b4 --- /dev/null +++ b/tests/quiltKernelTidepool.test.ts @@ -0,0 +1,355 @@ +// quiltKernelTidepool.test.ts — the helper-thread memory ocean joins the +// ledger. Contract: /tmp/tidepool/README.md (the bench is the spec; the +// README wins wherever implementations diverged). +import { Database } from "bun:sqlite" +import { afterEach, describe, expect, it } from "bun:test" +import { mkdtempSync, rmSync } from "node:fs" +import { tmpdir } from "node:os" +import { readdirSync, readFileSync } from "node:fs" +import { verifyChain } from "../src/quilt/projection.js" +import { TidepoolOcean, type HelperThreadSummary } from "../src/tidepool/ocean.js" +import { + detectQuietThreads, + newlyQuietThreads, + type ThreadLengthPoll +} from "../src/tidepool/stall.js" +import { + replayThreadIndexFromWal, + readWalRowsSince +} from "../src/tidepool/projection.js" +import { + drainQuietThreads, + getTidepoolWalFailures, + recallHelperContext, + rememberBotAction, + rememberChannelThreads, + rememberHelperThread, + resetTidepoolWalFailures +} from "../src/tidepool/index.js" +import { SqliteD1Database } from "./helpers/sqliteD1.js" + +const migrationPaths = readdirSync("drizzle") + .filter((file) => + /0002_.*\.sql|000[4-9]_.*\.sql|001[01345]_.*\.sql/.test(file) + ) + .sort() + +const applyMigrations = (database: Database) => { + for (const path of migrationPaths) { + const migration = readFileSync(`drizzle/${path}`, "utf8") + for (const statement of migration.split("--> statement-breakpoint")) { + const trimmed = statement.trim() + if (trimmed) { + database.run(trimmed) + } + } + } +} + +const createHarness = () => { + const owner = new SqliteD1Database() + applyMigrations(owner.database) + return { owner } +} + +const TS = "2026-09-17T00:00:00.000Z" +const thread = (over: Partial = {}): HelperThreadSummary => ({ + id: "thread-1", + guildId: "guild-1", + channelId: "channel-1", + userId: "user-1", + helperKey: "kimi", + helperName: "Kimi", + questionText: "how does the quilt hash chain work", + responseText: "fnv1a over prev seq cell op value ts mutation", + thinkingLevel: "high", + responseLength: 48, + createdAt: TS, + authorTag: "casey#1", + authorUsername: "casey", + lastMessageId: "msg-1", + ...over +}) + +let tmpDirs: string[] = [] +afterEach(() => { + for (const dir of tmpDirs) rmSync(dir, { recursive: true, force: true }) + tmpDirs = [] + resetTidepoolWalFailures() +}) + +describe("tidepool — ocean + ledger", () => { + it("remember dual-writes: ocean section + WAL chain, read-back verified", async () => { + const { owner } = createHarness() + const ocean = new TidepoolOcean({ now: () => TS }) + const t = thread() + const { section, entry, walRows } = await rememberHelperThread( + ocean, owner as never, t, { mutationId: "remember:1", ts: TS } + ) + expect(section).toContain(`## thread:${t.id}`) + expect(entry.rememberedAt).toBe(TS) + expect(walRows.length).toBeGreaterThan(0) + // chain verifies against hermit's own verifier + expect(verifyChain(walRows).ok).toBe(true) + // L1 law: channel + helper endpoints bound before the thread links them + expect(walRows[0].cell).toBe(`channel.${t.channelId}`) + expect(walRows[1].cell).toBe(`helper.${t.helperKey}`) + const cells = walRows.map((r) => r.cell) + expect(cells).toContain(`thread.${t.id}.question`) + expect(cells).toContain(`thread.${t.id}.response`) + expect(cells).toContain(`thread.${t.id}.rememberedAt`) + owner.close() + }) + + it("channel sweep: one mutation_id, one batch, all threads", async () => { + const { owner } = createHarness() + const ocean = new TidepoolOcean() + const threads = [ + thread({ id: "a", questionText: "q-a" }), + thread({ id: "b", questionText: "q-b" }), + thread({ id: "c", questionText: "q-c" }) + ] + const { remembered, walRows } = await rememberChannelThreads( + ocean, owner as never, threads, { mutationId: "remember:sweep1", ts: TS } + ) + expect(remembered).toHaveLength(3) + expect(new Set(walRows.map((r) => r.mutation_id)).size).toBe(1) + expect(verifyChain(walRows).ok).toBe(true) + expect(walRows.filter((r) => r.op === "link")).toHaveLength(6) + owner.close() + }) + + it("recall: weighted scoring, tie to most recent, clamp 3..5, deterministic", async () => { + let call = 0 + const stamps = ["2026-09-17T00:00:00.000Z", "2026-09-17T00:01:00.000Z", "2026-09-17T00:02:00.000Z"] + const ocean = new TidepoolOcean({ now: () => stamps[call++] ?? TS }) + await ocean.remember( + thread({ id: "q1", questionText: "quilt hash chain question" }) + ) + await ocean.remember( + thread({ + id: "r1", + questionText: "something else entirely", + responseText: "the quilt hash chain answer" + }) + ) + await ocean.remember( + thread({ id: "old", questionText: "quilt hash chain", helperName: "Old" }) + ) + // question match weighs 2, response match weighs 1; tie breaks to most recent + const top = await recallHelperContext(ocean, "quilt hash chain", { limit: 3 }) + expect(top.map((e) => e.id)).toEqual(["old", "q1", "r1"]) + // determinism — same query, same order + const again = await recallHelperContext(ocean, "quilt hash chain", { limit: 3 }) + expect(again.map((e) => e.id)).toEqual(top.map((e) => e.id)) + // clamp: limit 99 → 5, limit 1 → 3 + expect((await recallHelperContext(ocean, "quilt", { limit: 99 })).length).toBeLessThanOrEqual(5) + // no-match query returns nothing + expect(await recallHelperContext(ocean, "zzz-no-such-token")).toEqual([]) + }) + + it("quiet detection: pure, windowed, growth resets, unsorted input", () => { + const polls: ThreadLengthPoll[] = [ + { threadId: "t1", ts: "2026-09-17T03:00:00Z", messageCount: 7 }, + { threadId: "t1", ts: "2026-09-17T02:00:00Z", messageCount: 7 }, + { threadId: "t1", ts: "2026-09-17T00:00:00Z", messageCount: 5 }, + { threadId: "t1", ts: "2026-09-17T01:00:00Z", messageCount: 7 }, + { threadId: "t2", ts: "2026-09-17T00:00:00Z", messageCount: 1 }, + { threadId: "t2", ts: "2026-09-17T01:00:00Z", messageCount: 2 } + ] + const quiet = detectQuietThreads(polls) + expect(quiet).toHaveLength(1) + expect(quiet[0].threadId).toBe("t1") + expect(quiet[0].silentPolls).toBe(3) + expect(quiet[0].lastCount).toBe(7) + expect(quiet[0].since).toBe("2026-09-17T01:00:00Z") + // custom window + expect(detectQuietThreads(polls, { window: 4 })).toHaveLength(0) + }) + + it("newlyQuietThreads: crossing-only — silence is remembered once", () => { + const history: ThreadLengthPoll[] = [ + { threadId: "fresh", ts: "2026-09-17T00:00:00Z", messageCount: 3 }, + { threadId: "fresh", ts: "2026-09-17T01:00:00Z", messageCount: 3 }, + { threadId: "fresh", ts: "2026-09-17T02:00:00Z", messageCount: 3 }, + { threadId: "long", ts: "2026-09-17T00:00:00Z", messageCount: 9 }, + { threadId: "long", ts: "2026-09-17T01:00:00Z", messageCount: 9 }, + { threadId: "long", ts: "2026-09-17T02:00:00Z", messageCount: 9 }, + { threadId: "long", ts: "2026-09-17T03:00:00Z", messageCount: 9 }, + { threadId: "long", ts: "2026-09-17T04:00:00Z", messageCount: 9 } + ] + const events = newlyQuietThreads(history) + expect(events.map((e) => e.threadId)).toEqual(["fresh"]) + }) + + it("drain: .quiet cells land in the WAL; helper_threads untouched", async () => { + const { owner } = createHarness() + await rememberHelperThread( + new TidepoolOcean(), owner as never, thread(), + { mutationId: "remember:1", ts: TS } + ) + const { walRows } = await drainQuietThreads( + owner as never, + [ + { + threadId: "thread-1", + silentPolls: 4, + lastCount: 7, + since: "2026-09-17T01:00:00Z", + ts: "2026-09-17T04:00:00Z" + } + ], + { mutationId: "stall-monitor:1", ts: "2026-09-17T04:00:00Z" } + ) + expect(walRows).toHaveLength(1) + expect(walRows[0].cell).toBe("thread.thread-1.quiet") + expect(JSON.parse(walRows[0].value!)).toEqual({ + silentPolls: 4, + lastCount: 7, + since: "2026-09-17T01:00:00Z", + ts: "2026-09-17T04:00:00Z" + }) + // negative ledger: the live table has no row for an absence + const rows = owner.database + .query("select count(*) as n from helper_threads") + .get() as { n: number } + expect(rows.n).toBe(0) + owner.close() + }) + + it("replay from WAL alone rebuilds the index; quiet survives alongside", async () => { + const { owner } = createHarness() + const ocean = new TidepoolOcean() + await rememberHelperThread( + ocean, owner as never, thread(), + { mutationId: "remember:1", ts: TS } + ) + await drainQuietThreads( + owner as never, + [ + { + threadId: "thread-1", + silentPolls: 3, + lastCount: 7, + since: TS, + ts: "2026-09-17T03:00:00Z" + } + ], + { mutationId: "stall-monitor:1", ts: "2026-09-17T03:00:00Z" } + ) + const all = owner.database + .query( + `select seq, mutation_id, ts, cell, op, value, prev_hash, hash + from quilt_wal order by seq` + ) + .all() as never[] + const replayed = replayThreadIndexFromWal(all) + expect(replayed.size).toBe(1) + const entry = replayed.get("thread-1")! + expect(entry.questionText).toBe(thread().questionText) + expect(entry.helperName).toBe("Kimi") + expect(entry.userId).toBe("user-1") // WAL replay keeps what markdown drops + expect(entry.quiet?.silentPolls).toBe(3) + owner.close() + }) + + it("restart parity: markdown load and WAL replay agree on recall fields", async () => { + const dir = mkdtempSync(`${tmpdir()}/tidepool-`) + tmpDirs.push(dir) + const path = `${dir}/ocean.md` + const { owner } = createHarness() + const ocean = new TidepoolOcean({ path, now: () => TS }) + await rememberHelperThread( + ocean, owner as never, thread({ id: "t-parity" }), + { mutationId: "remember:1", ts: TS } + ) + // byte-identical restart: markdown file reloaded + const reloaded = await new TidepoolOcean({ path, now: () => TS }).load() + expect(reloaded.stats().oathViolations).toBe(0) + expect(reloaded.index.get("t-parity")?.questionText).toBe( + thread({ id: "t-parity" }).questionText + ) + // WAL replay agrees on every recall-relevant field + const all = owner.database + .query( + `select seq, mutation_id, ts, cell, op, value, prev_hash, hash + from quilt_wal order by seq` + ) + .all() as never[] + const replayed = replayThreadIndexFromWal(all).get("t-parity")! + const loaded = reloaded.index.get("t-parity")! + for (const field of [ + "questionText", + "responseText", + "helperName", + "helperKey", + "channelId", + "guildId", + "rememberedAt" + ] as const) { + expect(loaded[field]).toBe(replayed[field]) + } + owner.close() + }) + + it("tip continuation: rows after a seq replay without duplication", async () => { + const { owner } = createHarness() + const ocean = new TidepoolOcean() + await rememberHelperThread( + ocean, owner as never, thread({ id: "t1" }), + { mutationId: "remember:1", ts: TS } + ) + const tip = ( + owner.database.query("select max(seq) as tip from quilt_wal").get() as { + tip: number + } + ).tip + await drainQuietThreads( + owner as never, + [ + { + threadId: "t1", + silentPolls: 3, + lastCount: 7, + since: TS, + ts: "2026-09-17T03:00:00Z" + } + ], + { mutationId: "stall-monitor:1", ts: "2026-09-17T03:00:00Z" } + ) + const fresh = await readWalRowsSince(owner as never, tip) + expect(fresh).toHaveLength(1) + expect(fresh[0].cell).toBe("thread.t1.quiet") + owner.close() + }) + + it("WAL failure posture: the memory survives, the failure is counted", async () => { + const { owner } = createHarness() + owner.database.run(`drop table quilt_wal`) + const ocean = new TidepoolOcean() + const { section, entry, walRows } = await rememberHelperThread( + ocean, owner as never, thread(), + { mutationId: "remember:1", ts: TS } + ) + expect(section).toContain("## thread:thread-1") + expect(entry.helperName).toBe("Kimi") + expect(walRows).toEqual([]) + expect(getTidepoolWalFailures()).toBe(1) + owner.close() + }) + + it("oaths: append-only double-remember, timestamp audit, bot-action log", async () => { + const ocean = new TidepoolOcean({ now: () => TS }) + await ocean.remember(thread({ questionText: "first version" })) + await ocean.remember(thread({ questionText: "second version" })) + const stats = ocean.stats() + expect(stats.threadSections).toBe(2) // memory of a memory stays + expect(stats.indexEntries).toBe(1) // latest wins for recall + expect(ocean.index.get("thread-1")?.questionText).toBe("second version") + expect(stats.oathViolations).toBe(0) + const { ts, action } = await rememberBotAction(ocean, "swept channel-1") + expect(ts).toBe(TS) + expect(action).toContain(TS) + expect(ocean.markdown).toContain("## bot-actions") + }) +})