create-walle 0.9.28 → 0.9.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/create-walle.js +166 -6
- package/package.json +1 -1
- package/template/bin/ctm-launch.sh +70 -18
- package/template/bin/dev.sh +18 -0
- package/template/bin/ensure-stable-node.js +11 -0
- package/template/bin/node-bin.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +214 -23
- package/template/claude-task-manager/db.js +884 -50
- package/template/claude-task-manager/docs/backfill-incremental-no-main-fallback.md +48 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +21 -0
- package/template/claude-task-manager/docs/conversation-log-redesign.html +587 -0
- package/template/claude-task-manager/docs/session-title-authority.md +8 -3
- package/template/claude-task-manager/lib/auth-rules.js +13 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +63 -0
- package/template/claude-task-manager/lib/codex-config-guard.js +124 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +93 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +5 -4
- package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +114 -0
- package/template/claude-task-manager/lib/db-owner-task-queue.js +67 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +5 -1
- package/template/claude-task-manager/lib/desktop-fork.js +81 -0
- package/template/claude-task-manager/lib/headless-term-service.js +251 -4
- package/template/claude-task-manager/lib/message-identity.js +115 -0
- package/template/claude-task-manager/lib/mirror-feed-guards.js +25 -0
- package/template/claude-task-manager/lib/mirror-feed-sanitize.js +45 -0
- package/template/claude-task-manager/lib/path-suggest.js +77 -0
- package/template/claude-task-manager/lib/prompt-index-inputs.js +136 -0
- package/template/claude-task-manager/lib/real-node.js +36 -4
- package/template/claude-task-manager/lib/restore-auto-resume-policy.js +67 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +20 -0
- package/template/claude-task-manager/lib/restore-terminal-dims.js +109 -0
- package/template/claude-task-manager/lib/resume-cwd.js +124 -3
- package/template/claude-task-manager/lib/runtime-approval-recorder.js +152 -0
- package/template/claude-task-manager/lib/runtime-context-truth.js +236 -0
- package/template/claude-task-manager/lib/runtime-contract.js +195 -0
- package/template/claude-task-manager/lib/runtime-history-builder.js +205 -0
- package/template/claude-task-manager/lib/runtime-hook-bus.js +98 -0
- package/template/claude-task-manager/lib/runtime-input-queue.js +114 -0
- package/template/claude-task-manager/lib/runtime-input-recorder.js +156 -0
- package/template/claude-task-manager/lib/runtime-lineage.js +189 -0
- package/template/claude-task-manager/lib/runtime-registry.js +263 -0
- package/template/claude-task-manager/lib/runtime-session-history.js +41 -0
- package/template/claude-task-manager/lib/scrollback-snapshot-policy.js +37 -0
- package/template/claude-task-manager/lib/server-phase-conditions.js +103 -0
- package/template/claude-task-manager/lib/session-content-backfill.js +55 -8
- package/template/claude-task-manager/lib/session-db-read-contract.js +67 -0
- package/template/claude-task-manager/lib/session-history.js +93 -5
- package/template/claude-task-manager/lib/session-host-manager.js +154 -2
- package/template/claude-task-manager/lib/session-messages-defer.js +50 -0
- package/template/claude-task-manager/lib/session-messages-page.js +13 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +48 -29
- package/template/claude-task-manager/lib/session-stream.js +80 -17
- package/template/claude-task-manager/lib/session-title-signals.js +54 -0
- package/template/claude-task-manager/lib/session-token-usage.js +13 -0
- package/template/claude-task-manager/lib/state-sync/cell-diff.js +41 -0
- package/template/claude-task-manager/lib/state-sync/frame-emitter.js +214 -0
- package/template/claude-task-manager/lib/state-sync/frame-rate.js +75 -0
- package/template/claude-task-manager/lib/state-sync/row-serializer.js +166 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +19 -3
- package/template/claude-task-manager/lib/transcript-ingest-chunker.js +41 -0
- package/template/claude-task-manager/lib/transcript-store.js +99 -7
- package/template/claude-task-manager/lib/wal-checkpoint-policy.js +40 -0
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +100 -9
- package/template/claude-task-manager/lib/worktree-output-binding.js +93 -0
- package/template/claude-task-manager/lib/write-coalescer.js +83 -0
- package/template/claude-task-manager/public/css/walle-session.css +4 -0
- package/template/claude-task-manager/public/css/walle.css +0 -66
- package/template/claude-task-manager/public/index.html +1707 -266
- package/template/claude-task-manager/public/js/feedback.js +8 -1
- package/template/claude-task-manager/public/js/message-renderer.js +72 -2
- package/template/claude-task-manager/public/js/session-phase.js +4 -0
- package/template/claude-task-manager/public/js/session-status-precedence.js +7 -173
- package/template/claude-task-manager/public/js/setup.js +46 -3
- package/template/claude-task-manager/public/js/state-sync-client.js +257 -0
- package/template/claude-task-manager/public/js/state-sync-predictor.js +41 -0
- package/template/claude-task-manager/public/js/stream-view.js +113 -9
- package/template/claude-task-manager/public/js/terminal-reconciler.js +24 -4
- package/template/claude-task-manager/public/js/walle-session.js +239 -19
- package/template/claude-task-manager/public/js/walle.js +32 -119
- package/template/claude-task-manager/queue-engine.js +140 -0
- package/template/claude-task-manager/server.js +2802 -416
- package/template/claude-task-manager/session-integrity.js +16 -1
- package/template/claude-task-manager/workers/db-owner-worker.js +23 -6
- package/template/claude-task-manager/workers/read-pool-worker.js +55 -1
- package/template/claude-task-manager/workers/session-host-pool-process.js +193 -0
- package/template/claude-task-manager/workers/session-host-process.js +47 -11
- package/template/claude-task-manager/workers/state-detectors/codex.js +33 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +191 -31
- package/template/wall-e/api-walle.js +97 -52
- package/template/wall-e/auth/flow-manager.js +78 -1
- package/template/wall-e/auth/provider-flows.js +56 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +138 -5
- package/template/wall-e/brain.js +175 -13
- package/template/wall-e/chat.js +46 -1
- package/template/wall-e/embeddings.js +70 -0
- package/template/wall-e/events/event-bus.js +11 -1
- package/template/wall-e/http/auth.js +3 -1
- package/template/wall-e/http/model-admin.js +22 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +36 -4
- package/template/wall-e/lib/diagnostics-flags.js +9 -0
- package/template/wall-e/lib/event-loop-monitor.js +84 -5
- package/template/wall-e/lib/mcp-scan-lifecycle.js +247 -0
- package/template/wall-e/lib/parent-brain-owner-client.js +109 -0
- package/template/wall-e/lib/runtime-process-inventory.js +114 -0
- package/template/wall-e/lib/runtime-worker-pool.js +214 -23
- package/template/wall-e/lib/scheduler-worker-jobs.js +49 -4
- package/template/wall-e/lib/scheduler.js +320 -35
- package/template/wall-e/lib/slack-identity.js +120 -0
- package/template/wall-e/lib/slack-permalink.js +107 -0
- package/template/wall-e/lib/slack-web.js +174 -0
- package/template/wall-e/lib/worker-thread-pool.js +55 -4
- package/template/wall-e/llm/claude-cli.js +21 -3
- package/template/wall-e/llm/cli-binary.js +90 -0
- package/template/wall-e/llm/codex-cli.js +113 -49
- package/template/wall-e/llm/default-fallback.js +10 -4
- package/template/wall-e/llm/mlx.js +46 -8
- package/template/wall-e/llm/model-catalog.js +129 -17
- package/template/wall-e/llm/provider-detector.js +112 -22
- package/template/wall-e/loops/backfill.js +32 -16
- package/template/wall-e/loops/ingest.js +50 -16
- package/template/wall-e/loops/tasks.js +521 -25
- package/template/wall-e/mcp-server.js +215 -6
- package/template/wall-e/memory/ctm-session-context.js +93 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +15 -23
- package/template/wall-e/skills/_bundled/gws-workspace/gws-router +237 -0
- package/template/wall-e/skills/_bundled/gws-workspace/setup.js +112 -1
- package/template/wall-e/skills/_bundled/mcp-scan/run.js +265 -41
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +434 -93
- package/template/wall-e/skills/internal-skill-registry.js +27 -5
- package/template/wall-e/skills/mcp-client.js +18 -3
- package/template/wall-e/skills/script-skill-runner.js +53 -5
- package/template/wall-e/skills/skill-planner.js +5 -26
- package/template/wall-e/training/real-trajectory-miner.js +24 -114
- package/template/wall-e/utils/dedup.js +165 -66
- package/template/wall-e/weather-runtime.js +12 -4
- package/template/wall-e/workers/brain-owner-worker.js +68 -0
- package/template/wall-e/workers/runtime-worker.js +4 -0
- package/template/website/index.html +3 -0
|
@@ -11,6 +11,13 @@ const { codexRolloutIdFromPath, readCodexRolloutMetadata } = require('./lib/sess
|
|
|
11
11
|
const { ensureTranscriptTables, healSelfLinkedTranscriptKeys } = require('./lib/transcript-store');
|
|
12
12
|
const { isProviderGeneratedUserContextText } = require('./lib/provider-user-context');
|
|
13
13
|
const { buildSessionImageRefs } = require('./lib/session-image-refs');
|
|
14
|
+
const {
|
|
15
|
+
normalizeRuntimeEvent,
|
|
16
|
+
normalizeTurnRecord,
|
|
17
|
+
normalizeInputEnvelope,
|
|
18
|
+
normalizeRouteSnapshot,
|
|
19
|
+
normalizeApprovalRecord,
|
|
20
|
+
} = require('./lib/runtime-contract');
|
|
14
21
|
const {
|
|
15
22
|
classifySqliteError,
|
|
16
23
|
ensureSqliteDriverReady,
|
|
@@ -785,6 +792,121 @@ function connectDb(dbPath) {
|
|
|
785
792
|
}
|
|
786
793
|
}
|
|
787
794
|
|
|
795
|
+
function runtimeKernelSchemaSql() {
|
|
796
|
+
return `
|
|
797
|
+
CREATE TABLE IF NOT EXISTS ctm_runtime_events (
|
|
798
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
799
|
+
event_id TEXT NOT NULL UNIQUE,
|
|
800
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
801
|
+
ctm_session_id TEXT NOT NULL,
|
|
802
|
+
agent_session_id TEXT DEFAULT '',
|
|
803
|
+
turn_id TEXT DEFAULT '',
|
|
804
|
+
adapter TEXT NOT NULL,
|
|
805
|
+
event_type TEXT NOT NULL,
|
|
806
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
807
|
+
created_at_ms INTEGER NOT NULL,
|
|
808
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
809
|
+
);
|
|
810
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_events_session
|
|
811
|
+
ON ctm_runtime_events(ctm_session_id, created_at_ms, id);
|
|
812
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_events_turn
|
|
813
|
+
ON ctm_runtime_events(ctm_session_id, turn_id, created_at_ms, id);
|
|
814
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_events_type
|
|
815
|
+
ON ctm_runtime_events(event_type, created_at_ms);
|
|
816
|
+
-- Serves "latest event of a type for a session" (latestContextTruth's per-session token/compact
|
|
817
|
+
-- lookup) as an O(1) index seek instead of fetching+parsing the oldest 1000 events per poll.
|
|
818
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_events_session_type
|
|
819
|
+
ON ctm_runtime_events(ctm_session_id, event_type, id);
|
|
820
|
+
|
|
821
|
+
CREATE TABLE IF NOT EXISTS ctm_runtime_turns (
|
|
822
|
+
ctm_session_id TEXT NOT NULL,
|
|
823
|
+
turn_id TEXT NOT NULL,
|
|
824
|
+
agent_session_id TEXT DEFAULT '',
|
|
825
|
+
parent_turn_id TEXT DEFAULT '',
|
|
826
|
+
user_input_event_id TEXT DEFAULT '',
|
|
827
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
828
|
+
opened_at_ms INTEGER NOT NULL,
|
|
829
|
+
closed_at_ms INTEGER DEFAULT 0,
|
|
830
|
+
model_route_json TEXT DEFAULT '{}',
|
|
831
|
+
compact_boundary_json TEXT DEFAULT '{}',
|
|
832
|
+
metadata_json TEXT DEFAULT '{}',
|
|
833
|
+
updated_at_ms INTEGER NOT NULL,
|
|
834
|
+
PRIMARY KEY (ctm_session_id, turn_id)
|
|
835
|
+
);
|
|
836
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_turns_session_status
|
|
837
|
+
ON ctm_runtime_turns(ctm_session_id, status, updated_at_ms DESC);
|
|
838
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_turns_agent
|
|
839
|
+
ON ctm_runtime_turns(agent_session_id, updated_at_ms DESC);
|
|
840
|
+
|
|
841
|
+
CREATE TABLE IF NOT EXISTS ctm_runtime_input_envelopes (
|
|
842
|
+
input_id TEXT PRIMARY KEY,
|
|
843
|
+
ctm_session_id TEXT NOT NULL,
|
|
844
|
+
agent_session_id TEXT DEFAULT '',
|
|
845
|
+
mode TEXT NOT NULL DEFAULT 'mailbox',
|
|
846
|
+
source TEXT NOT NULL DEFAULT 'runtime',
|
|
847
|
+
text TEXT NOT NULL DEFAULT '',
|
|
848
|
+
attachments_json TEXT NOT NULL DEFAULT '[]',
|
|
849
|
+
created_at_ms INTEGER NOT NULL,
|
|
850
|
+
accepted_at_ms INTEGER DEFAULT 0,
|
|
851
|
+
delivered_at_ms INTEGER DEFAULT 0,
|
|
852
|
+
result TEXT NOT NULL DEFAULT 'pending',
|
|
853
|
+
result_reason TEXT DEFAULT '',
|
|
854
|
+
metadata_json TEXT DEFAULT '{}',
|
|
855
|
+
updated_at_ms INTEGER NOT NULL
|
|
856
|
+
);
|
|
857
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_input_session_result
|
|
858
|
+
ON ctm_runtime_input_envelopes(ctm_session_id, result, created_at_ms);
|
|
859
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_input_agent
|
|
860
|
+
ON ctm_runtime_input_envelopes(agent_session_id, created_at_ms);
|
|
861
|
+
|
|
862
|
+
CREATE TABLE IF NOT EXISTS ctm_runtime_route_snapshots (
|
|
863
|
+
route_id TEXT PRIMARY KEY,
|
|
864
|
+
ctm_session_id TEXT NOT NULL,
|
|
865
|
+
agent_session_id TEXT DEFAULT '',
|
|
866
|
+
turn_id TEXT DEFAULT '',
|
|
867
|
+
requested_model TEXT DEFAULT '',
|
|
868
|
+
resolved_model TEXT DEFAULT '',
|
|
869
|
+
provider TEXT DEFAULT '',
|
|
870
|
+
connection_layer TEXT DEFAULT 'unknown',
|
|
871
|
+
route_source TEXT DEFAULT 'unknown',
|
|
872
|
+
supports_images INTEGER DEFAULT 0,
|
|
873
|
+
supports_tools INTEGER DEFAULT 0,
|
|
874
|
+
supports_mcp INTEGER DEFAULT 0,
|
|
875
|
+
created_at_ms INTEGER NOT NULL,
|
|
876
|
+
metadata_json TEXT DEFAULT '{}'
|
|
877
|
+
);
|
|
878
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_route_session
|
|
879
|
+
ON ctm_runtime_route_snapshots(ctm_session_id, created_at_ms DESC);
|
|
880
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_route_turn
|
|
881
|
+
ON ctm_runtime_route_snapshots(ctm_session_id, turn_id, created_at_ms DESC);
|
|
882
|
+
|
|
883
|
+
CREATE TABLE IF NOT EXISTS ctm_runtime_approvals (
|
|
884
|
+
approval_id TEXT PRIMARY KEY,
|
|
885
|
+
ctm_session_id TEXT NOT NULL,
|
|
886
|
+
agent_session_id TEXT DEFAULT '',
|
|
887
|
+
turn_id TEXT DEFAULT '',
|
|
888
|
+
source_event_id TEXT DEFAULT '',
|
|
889
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
890
|
+
prompt TEXT DEFAULT '',
|
|
891
|
+
tool_name TEXT DEFAULT '',
|
|
892
|
+
decision TEXT DEFAULT '',
|
|
893
|
+
decided_by TEXT DEFAULT '',
|
|
894
|
+
requested_at_ms INTEGER NOT NULL,
|
|
895
|
+
resolved_at_ms INTEGER DEFAULT 0,
|
|
896
|
+
metadata_json TEXT DEFAULT '{}',
|
|
897
|
+
updated_at_ms INTEGER NOT NULL
|
|
898
|
+
);
|
|
899
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_approvals_session_status
|
|
900
|
+
ON ctm_runtime_approvals(ctm_session_id, status, requested_at_ms DESC);
|
|
901
|
+
CREATE INDEX IF NOT EXISTS idx_ctm_runtime_approvals_source_event
|
|
902
|
+
ON ctm_runtime_approvals(source_event_id);
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function ensureRuntimeKernelTables(handle = getDb()) {
|
|
907
|
+
handle.exec(runtimeKernelSchemaSql());
|
|
908
|
+
}
|
|
909
|
+
|
|
788
910
|
function createTables() {
|
|
789
911
|
db.exec(`
|
|
790
912
|
-- Settings (key-value store)
|
|
@@ -1006,6 +1128,7 @@ function createTables() {
|
|
|
1006
1128
|
CREATE INDEX IF NOT EXISTS idx_runtime_tasks_session_status ON runtime_tasks(ctm_session_id, status, updated_at DESC);
|
|
1007
1129
|
CREATE INDEX IF NOT EXISTS idx_runtime_tasks_agent ON runtime_tasks(agent_session_id, updated_at DESC);
|
|
1008
1130
|
`);
|
|
1131
|
+
ensureRuntimeKernelTables(db);
|
|
1009
1132
|
}
|
|
1010
1133
|
|
|
1011
1134
|
function runMigrations() {
|
|
@@ -1047,6 +1170,7 @@ function runMigrations() {
|
|
|
1047
1170
|
CREATE INDEX IF NOT EXISTS idx_runtime_tasks_session_status ON runtime_tasks(ctm_session_id, status, updated_at DESC);
|
|
1048
1171
|
CREATE INDEX IF NOT EXISTS idx_runtime_tasks_agent ON runtime_tasks(agent_session_id, updated_at DESC);
|
|
1049
1172
|
`);
|
|
1173
|
+
ensureRuntimeKernelTables(getDb());
|
|
1050
1174
|
|
|
1051
1175
|
// Add pinned column to prompts if not exists
|
|
1052
1176
|
const cols = getDb().prepare("PRAGMA table_info(prompts)").all();
|
|
@@ -2385,6 +2509,34 @@ function migrateSchemaIfNeeded() {
|
|
|
2385
2509
|
// Non-fatal: dropping a dormant cache table; the app runs fine if it lingers.
|
|
2386
2510
|
}
|
|
2387
2511
|
}
|
|
2512
|
+
if (getSchemaVersion() < 11) {
|
|
2513
|
+
try {
|
|
2514
|
+
migrateToV11();
|
|
2515
|
+
} catch (e) {
|
|
2516
|
+
console.error('[db] Schema migration to v11 FAILED:', e.message);
|
|
2517
|
+
console.error('[db] Stack:', e.stack);
|
|
2518
|
+
// Non-fatal: the column only enables Claude Desktop → Code fork dedup; absent it,
|
|
2519
|
+
// getForkForDesktopUuid simply returns null and conversion stays available.
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* Schema v11: link a converted Claude Desktop conversation to its resumable Claude Code
|
|
2526
|
+
* fork. A read-only Desktop conversation can be snapshot-and-forked into a real Code session
|
|
2527
|
+
* (see lib/claude-desktop-sessions.js materializeForkTranscript); we record the originating
|
|
2528
|
+
* Desktop uuid on the fork's agent_sessions row so the same conversation is never offered for
|
|
2529
|
+
* conversion twice — the sidebar shows "Resume fork" instead. Idempotent via PRAGMA pre-check.
|
|
2530
|
+
*/
|
|
2531
|
+
function migrateToV11() {
|
|
2532
|
+
const d = getDb();
|
|
2533
|
+
const cols = d.prepare('PRAGMA table_info(agent_sessions)').all();
|
|
2534
|
+
if (!cols.find((c) => c.name === 'forked_from_desktop_uuid')) {
|
|
2535
|
+
d.prepare("ALTER TABLE agent_sessions ADD COLUMN forked_from_desktop_uuid TEXT DEFAULT ''").run();
|
|
2536
|
+
}
|
|
2537
|
+
d.exec("CREATE INDEX IF NOT EXISTS idx_agent_forked_desktop ON agent_sessions(forked_from_desktop_uuid) WHERE forked_from_desktop_uuid != ''");
|
|
2538
|
+
setSchemaVersion(11);
|
|
2539
|
+
console.log('[db] Schema migrated to v11 (Claude Desktop fork linkage)');
|
|
2388
2540
|
}
|
|
2389
2541
|
|
|
2390
2542
|
/**
|
|
@@ -3048,6 +3200,7 @@ function createV1Tables() {
|
|
|
3048
3200
|
parent_agent_session_id TEXT DEFAULT '',
|
|
3049
3201
|
agent_nickname TEXT DEFAULT '',
|
|
3050
3202
|
agent_role TEXT DEFAULT '',
|
|
3203
|
+
forked_from_desktop_uuid TEXT DEFAULT '',
|
|
3051
3204
|
created_at TEXT DEFAULT (datetime('now')),
|
|
3052
3205
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
3053
3206
|
FOREIGN KEY (ctm_session_id) REFERENCES ctm_sessions(id) ON DELETE CASCADE
|
|
@@ -3069,6 +3222,7 @@ function createV1Tables() {
|
|
|
3069
3222
|
d.exec('CREATE INDEX IF NOT EXISTS idx_agent_slug ON agent_sessions(slug)');
|
|
3070
3223
|
d.exec('CREATE INDEX IF NOT EXISTS idx_agent_parent_session ON agent_sessions(parent_agent_session_id)');
|
|
3071
3224
|
d.exec('CREATE INDEX IF NOT EXISTS idx_agent_thread_source ON agent_sessions(thread_source)');
|
|
3225
|
+
d.exec("CREATE INDEX IF NOT EXISTS idx_agent_forked_desktop ON agent_sessions(forked_from_desktop_uuid) WHERE forked_from_desktop_uuid != ''");
|
|
3072
3226
|
}
|
|
3073
3227
|
|
|
3074
3228
|
// --- Settings CRUD ---
|
|
@@ -3384,6 +3538,501 @@ function deleteRuntimeTasksForSession(ctmSessionId, { completedOnly = false } =
|
|
|
3384
3538
|
return result.changes || 0;
|
|
3385
3539
|
}
|
|
3386
3540
|
|
|
3541
|
+
function _parseJsonObject(value) {
|
|
3542
|
+
try {
|
|
3543
|
+
const parsed = JSON.parse(value || '{}');
|
|
3544
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
3545
|
+
} catch {
|
|
3546
|
+
return {};
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
function _parseJsonArray(value) {
|
|
3551
|
+
try {
|
|
3552
|
+
const parsed = JSON.parse(value || '[]');
|
|
3553
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3554
|
+
} catch {
|
|
3555
|
+
return [];
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
function _parseRuntimeEvent(row) {
|
|
3560
|
+
if (!row) return null;
|
|
3561
|
+
return {
|
|
3562
|
+
id: row.id,
|
|
3563
|
+
eventId: row.event_id,
|
|
3564
|
+
event_id: row.event_id,
|
|
3565
|
+
idempotencyKey: row.idempotency_key,
|
|
3566
|
+
idempotency_key: row.idempotency_key,
|
|
3567
|
+
ctmSessionId: row.ctm_session_id,
|
|
3568
|
+
ctm_session_id: row.ctm_session_id,
|
|
3569
|
+
agentSessionId: row.agent_session_id || '',
|
|
3570
|
+
agent_session_id: row.agent_session_id || '',
|
|
3571
|
+
turnId: row.turn_id || '',
|
|
3572
|
+
turn_id: row.turn_id || '',
|
|
3573
|
+
adapter: row.adapter || 'unknown',
|
|
3574
|
+
type: row.event_type || '',
|
|
3575
|
+
event_type: row.event_type || '',
|
|
3576
|
+
payload: _parseJsonObject(row.payload_json),
|
|
3577
|
+
payload_json: row.payload_json || '{}',
|
|
3578
|
+
createdAtMs: Number(row.created_at_ms || 0),
|
|
3579
|
+
created_at_ms: Number(row.created_at_ms || 0),
|
|
3580
|
+
created_at: row.created_at || '',
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
function appendRuntimeEvent(event = {}) {
|
|
3585
|
+
const normalized = normalizeRuntimeEvent(event);
|
|
3586
|
+
if (!normalized.ctmSessionId || !normalized.type) return null;
|
|
3587
|
+
getDb().prepare(`
|
|
3588
|
+
INSERT OR IGNORE INTO ctm_runtime_events (
|
|
3589
|
+
event_id, idempotency_key, ctm_session_id, agent_session_id, turn_id,
|
|
3590
|
+
adapter, event_type, payload_json, created_at_ms
|
|
3591
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3592
|
+
`).run(
|
|
3593
|
+
normalized.eventId,
|
|
3594
|
+
normalized.idempotencyKey,
|
|
3595
|
+
normalized.ctmSessionId,
|
|
3596
|
+
normalized.agentSessionId,
|
|
3597
|
+
normalized.turnId,
|
|
3598
|
+
normalized.adapter,
|
|
3599
|
+
normalized.type,
|
|
3600
|
+
_runtimeJsonObject(normalized.payload),
|
|
3601
|
+
normalized.createdAtMs
|
|
3602
|
+
);
|
|
3603
|
+
return _parseRuntimeEvent(getDb().prepare(`
|
|
3604
|
+
SELECT * FROM ctm_runtime_events
|
|
3605
|
+
WHERE idempotency_key = ?
|
|
3606
|
+
ORDER BY id ASC
|
|
3607
|
+
LIMIT 1
|
|
3608
|
+
`).get(normalized.idempotencyKey));
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
function listRuntimeEvents(ctmSessionId, { sinceId = 0, turnId = '', limit = 1000 } = {}) {
|
|
3612
|
+
const cleanSessionId = String(ctmSessionId || '').trim();
|
|
3613
|
+
if (!cleanSessionId) return [];
|
|
3614
|
+
const cap = Math.max(1, Math.min(Number(limit) || 1000, 5000));
|
|
3615
|
+
const minId = Math.max(0, Number(sinceId || 0) || 0);
|
|
3616
|
+
const cleanTurnId = String(turnId || '').trim();
|
|
3617
|
+
const rows = cleanTurnId
|
|
3618
|
+
? getDb().prepare(`
|
|
3619
|
+
SELECT * FROM ctm_runtime_events
|
|
3620
|
+
WHERE ctm_session_id = ? AND turn_id = ? AND id > ?
|
|
3621
|
+
ORDER BY id ASC
|
|
3622
|
+
LIMIT ?
|
|
3623
|
+
`).all(cleanSessionId, cleanTurnId, minId, cap)
|
|
3624
|
+
: getDb().prepare(`
|
|
3625
|
+
SELECT * FROM ctm_runtime_events
|
|
3626
|
+
WHERE ctm_session_id = ? AND id > ?
|
|
3627
|
+
ORDER BY id ASC
|
|
3628
|
+
LIMIT ?
|
|
3629
|
+
`).all(cleanSessionId, minId, cap);
|
|
3630
|
+
return rows.map(_parseRuntimeEvent).filter(Boolean);
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
// Latest event whose event_type is one of `eventTypes` for a session — the highest id wins.
|
|
3634
|
+
// Backs latestContextTruth, which previously fetched + JSON-parsed the oldest 1000 events per
|
|
3635
|
+
// session per poll just to find the latest token_usage + latest compact (a 3.98s/4min on-main cost
|
|
3636
|
+
// in the live CPU profile, and wrong once a session exceeds 1000 events). One indexed DESC LIMIT 1
|
|
3637
|
+
// seek per type (idx_ctm_runtime_events_session_type) → O(1), parses at most one row per type.
|
|
3638
|
+
function latestRuntimeEventOfTypes(ctmSessionId, eventTypes, { turnId = '' } = {}) {
|
|
3639
|
+
const cleanSessionId = String(ctmSessionId || '').trim();
|
|
3640
|
+
if (!cleanSessionId || !Array.isArray(eventTypes) || eventTypes.length === 0) return null;
|
|
3641
|
+
const cleanTurnId = String(turnId || '').trim();
|
|
3642
|
+
const d = getDb();
|
|
3643
|
+
let best = null;
|
|
3644
|
+
for (const t of eventTypes) {
|
|
3645
|
+
const et = String(t || '').trim();
|
|
3646
|
+
if (!et) continue;
|
|
3647
|
+
const row = cleanTurnId
|
|
3648
|
+
? d.prepare(
|
|
3649
|
+
'SELECT * FROM ctm_runtime_events WHERE ctm_session_id = ? AND turn_id = ? AND event_type = ? ORDER BY id DESC LIMIT 1'
|
|
3650
|
+
).get(cleanSessionId, cleanTurnId, et)
|
|
3651
|
+
: d.prepare(
|
|
3652
|
+
'SELECT * FROM ctm_runtime_events WHERE ctm_session_id = ? AND event_type = ? ORDER BY id DESC LIMIT 1'
|
|
3653
|
+
).get(cleanSessionId, et);
|
|
3654
|
+
if (row && (!best || Number(row.id) > Number(best.id))) best = row;
|
|
3655
|
+
}
|
|
3656
|
+
return best ? _parseRuntimeEvent(best) : null;
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
function _parseRuntimeTurn(row) {
|
|
3660
|
+
if (!row) return null;
|
|
3661
|
+
return {
|
|
3662
|
+
ctmSessionId: row.ctm_session_id,
|
|
3663
|
+
ctm_session_id: row.ctm_session_id,
|
|
3664
|
+
turnId: row.turn_id,
|
|
3665
|
+
turn_id: row.turn_id,
|
|
3666
|
+
agentSessionId: row.agent_session_id || '',
|
|
3667
|
+
agent_session_id: row.agent_session_id || '',
|
|
3668
|
+
parentTurnId: row.parent_turn_id || '',
|
|
3669
|
+
parent_turn_id: row.parent_turn_id || '',
|
|
3670
|
+
userInputEventId: row.user_input_event_id || '',
|
|
3671
|
+
user_input_event_id: row.user_input_event_id || '',
|
|
3672
|
+
status: row.status || 'open',
|
|
3673
|
+
openedAtMs: Number(row.opened_at_ms || 0),
|
|
3674
|
+
opened_at_ms: Number(row.opened_at_ms || 0),
|
|
3675
|
+
closedAtMs: Number(row.closed_at_ms || 0),
|
|
3676
|
+
closed_at_ms: Number(row.closed_at_ms || 0),
|
|
3677
|
+
modelRoute: _parseJsonObject(row.model_route_json),
|
|
3678
|
+
model_route_json: row.model_route_json || '{}',
|
|
3679
|
+
compactBoundary: _parseJsonObject(row.compact_boundary_json),
|
|
3680
|
+
compact_boundary_json: row.compact_boundary_json || '{}',
|
|
3681
|
+
metadata: _parseJsonObject(row.metadata_json),
|
|
3682
|
+
metadata_json: row.metadata_json || '{}',
|
|
3683
|
+
updatedAtMs: Number(row.updated_at_ms || 0),
|
|
3684
|
+
updated_at_ms: Number(row.updated_at_ms || 0),
|
|
3685
|
+
};
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3688
|
+
function upsertRuntimeTurn(turn = {}) {
|
|
3689
|
+
const normalized = normalizeTurnRecord(turn);
|
|
3690
|
+
if (!normalized.ctmSessionId || !normalized.turnId) return null;
|
|
3691
|
+
const now = Date.now();
|
|
3692
|
+
getDb().prepare(`
|
|
3693
|
+
INSERT INTO ctm_runtime_turns (
|
|
3694
|
+
ctm_session_id, turn_id, agent_session_id, parent_turn_id, user_input_event_id,
|
|
3695
|
+
status, opened_at_ms, closed_at_ms, model_route_json, compact_boundary_json,
|
|
3696
|
+
metadata_json, updated_at_ms
|
|
3697
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3698
|
+
ON CONFLICT(ctm_session_id, turn_id) DO UPDATE SET
|
|
3699
|
+
agent_session_id = excluded.agent_session_id,
|
|
3700
|
+
parent_turn_id = excluded.parent_turn_id,
|
|
3701
|
+
user_input_event_id = excluded.user_input_event_id,
|
|
3702
|
+
status = excluded.status,
|
|
3703
|
+
opened_at_ms = MIN(ctm_runtime_turns.opened_at_ms, excluded.opened_at_ms),
|
|
3704
|
+
closed_at_ms = excluded.closed_at_ms,
|
|
3705
|
+
model_route_json = CASE
|
|
3706
|
+
WHEN excluded.model_route_json = '{}' THEN ctm_runtime_turns.model_route_json
|
|
3707
|
+
ELSE excluded.model_route_json
|
|
3708
|
+
END,
|
|
3709
|
+
compact_boundary_json = CASE
|
|
3710
|
+
WHEN excluded.compact_boundary_json = '{}' THEN ctm_runtime_turns.compact_boundary_json
|
|
3711
|
+
ELSE excluded.compact_boundary_json
|
|
3712
|
+
END,
|
|
3713
|
+
metadata_json = CASE
|
|
3714
|
+
WHEN excluded.metadata_json = '{}' THEN ctm_runtime_turns.metadata_json
|
|
3715
|
+
ELSE excluded.metadata_json
|
|
3716
|
+
END,
|
|
3717
|
+
updated_at_ms = excluded.updated_at_ms
|
|
3718
|
+
`).run(
|
|
3719
|
+
normalized.ctmSessionId,
|
|
3720
|
+
normalized.turnId,
|
|
3721
|
+
normalized.agentSessionId,
|
|
3722
|
+
normalized.parentTurnId,
|
|
3723
|
+
normalized.userInputEventId,
|
|
3724
|
+
normalized.status,
|
|
3725
|
+
normalized.openedAtMs,
|
|
3726
|
+
normalized.closedAtMs,
|
|
3727
|
+
_runtimeJsonObject(normalized.modelRoute),
|
|
3728
|
+
_runtimeJsonObject(normalized.compactBoundary),
|
|
3729
|
+
_runtimeJsonObject(normalized.metadata),
|
|
3730
|
+
now
|
|
3731
|
+
);
|
|
3732
|
+
return getRuntimeTurn(normalized.ctmSessionId, normalized.turnId);
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function getRuntimeTurn(ctmSessionId, turnId) {
|
|
3736
|
+
const row = getDb().prepare('SELECT * FROM ctm_runtime_turns WHERE ctm_session_id = ? AND turn_id = ?')
|
|
3737
|
+
.get(String(ctmSessionId || '').trim(), String(turnId || '').trim());
|
|
3738
|
+
return _parseRuntimeTurn(row);
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
function listRuntimeTurns(ctmSessionId, { status = '', limit = 200 } = {}) {
|
|
3742
|
+
const cleanSessionId = String(ctmSessionId || '').trim();
|
|
3743
|
+
if (!cleanSessionId) return [];
|
|
3744
|
+
const cap = Math.max(1, Math.min(Number(limit) || 200, 1000));
|
|
3745
|
+
const cleanStatus = String(status || '').trim();
|
|
3746
|
+
const rows = cleanStatus
|
|
3747
|
+
? getDb().prepare('SELECT * FROM ctm_runtime_turns WHERE ctm_session_id = ? AND status = ? ORDER BY opened_at_ms ASC LIMIT ?').all(cleanSessionId, cleanStatus, cap)
|
|
3748
|
+
: getDb().prepare('SELECT * FROM ctm_runtime_turns WHERE ctm_session_id = ? ORDER BY opened_at_ms ASC LIMIT ?').all(cleanSessionId, cap);
|
|
3749
|
+
return rows.map(_parseRuntimeTurn).filter(Boolean);
|
|
3750
|
+
}
|
|
3751
|
+
|
|
3752
|
+
function _parseRuntimeInputEnvelope(row) {
|
|
3753
|
+
if (!row) return null;
|
|
3754
|
+
return {
|
|
3755
|
+
inputId: row.input_id,
|
|
3756
|
+
input_id: row.input_id,
|
|
3757
|
+
ctmSessionId: row.ctm_session_id,
|
|
3758
|
+
ctm_session_id: row.ctm_session_id,
|
|
3759
|
+
agentSessionId: row.agent_session_id || '',
|
|
3760
|
+
agent_session_id: row.agent_session_id || '',
|
|
3761
|
+
mode: row.mode || 'mailbox',
|
|
3762
|
+
source: row.source || 'runtime',
|
|
3763
|
+
text: row.text || '',
|
|
3764
|
+
attachments: _parseJsonArray(row.attachments_json),
|
|
3765
|
+
attachments_json: row.attachments_json || '[]',
|
|
3766
|
+
createdAtMs: Number(row.created_at_ms || 0),
|
|
3767
|
+
created_at_ms: Number(row.created_at_ms || 0),
|
|
3768
|
+
acceptedAtMs: Number(row.accepted_at_ms || 0),
|
|
3769
|
+
accepted_at_ms: Number(row.accepted_at_ms || 0),
|
|
3770
|
+
deliveredAtMs: Number(row.delivered_at_ms || 0),
|
|
3771
|
+
delivered_at_ms: Number(row.delivered_at_ms || 0),
|
|
3772
|
+
result: row.result || 'pending',
|
|
3773
|
+
resultReason: row.result_reason || '',
|
|
3774
|
+
result_reason: row.result_reason || '',
|
|
3775
|
+
metadata: _parseJsonObject(row.metadata_json),
|
|
3776
|
+
metadata_json: row.metadata_json || '{}',
|
|
3777
|
+
updatedAtMs: Number(row.updated_at_ms || 0),
|
|
3778
|
+
updated_at_ms: Number(row.updated_at_ms || 0),
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3782
|
+
function upsertRuntimeInputEnvelope(input = {}) {
|
|
3783
|
+
const normalized = normalizeInputEnvelope(input);
|
|
3784
|
+
if (!normalized.ctmSessionId || !normalized.inputId) return null;
|
|
3785
|
+
const now = Date.now();
|
|
3786
|
+
getDb().prepare(`
|
|
3787
|
+
INSERT INTO ctm_runtime_input_envelopes (
|
|
3788
|
+
input_id, ctm_session_id, agent_session_id, mode, source, text,
|
|
3789
|
+
attachments_json, created_at_ms, accepted_at_ms, delivered_at_ms,
|
|
3790
|
+
result, result_reason, metadata_json, updated_at_ms
|
|
3791
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3792
|
+
ON CONFLICT(input_id) DO UPDATE SET
|
|
3793
|
+
ctm_session_id = excluded.ctm_session_id,
|
|
3794
|
+
agent_session_id = excluded.agent_session_id,
|
|
3795
|
+
mode = excluded.mode,
|
|
3796
|
+
source = excluded.source,
|
|
3797
|
+
text = excluded.text,
|
|
3798
|
+
attachments_json = excluded.attachments_json,
|
|
3799
|
+
created_at_ms = MIN(ctm_runtime_input_envelopes.created_at_ms, excluded.created_at_ms),
|
|
3800
|
+
accepted_at_ms = excluded.accepted_at_ms,
|
|
3801
|
+
delivered_at_ms = excluded.delivered_at_ms,
|
|
3802
|
+
result = excluded.result,
|
|
3803
|
+
result_reason = excluded.result_reason,
|
|
3804
|
+
metadata_json = excluded.metadata_json,
|
|
3805
|
+
updated_at_ms = excluded.updated_at_ms
|
|
3806
|
+
`).run(
|
|
3807
|
+
normalized.inputId,
|
|
3808
|
+
normalized.ctmSessionId,
|
|
3809
|
+
normalized.agentSessionId,
|
|
3810
|
+
normalized.mode,
|
|
3811
|
+
normalized.source,
|
|
3812
|
+
normalized.text,
|
|
3813
|
+
_runtimeJsonArray(normalized.attachments),
|
|
3814
|
+
normalized.createdAtMs,
|
|
3815
|
+
normalized.acceptedAtMs,
|
|
3816
|
+
normalized.deliveredAtMs,
|
|
3817
|
+
normalized.result,
|
|
3818
|
+
normalized.resultReason,
|
|
3819
|
+
_runtimeJsonObject(normalized.metadata),
|
|
3820
|
+
now
|
|
3821
|
+
);
|
|
3822
|
+
return _parseRuntimeInputEnvelope(getDb().prepare('SELECT * FROM ctm_runtime_input_envelopes WHERE input_id = ?').get(normalized.inputId));
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
function updateRuntimeInputEnvelope(inputId, patch = {}) {
|
|
3826
|
+
const existing = _parseRuntimeInputEnvelope(getDb().prepare('SELECT * FROM ctm_runtime_input_envelopes WHERE input_id = ?').get(String(inputId || '').trim()));
|
|
3827
|
+
if (!existing) return null;
|
|
3828
|
+
return upsertRuntimeInputEnvelope({
|
|
3829
|
+
...existing,
|
|
3830
|
+
...patch,
|
|
3831
|
+
inputId: existing.inputId,
|
|
3832
|
+
ctmSessionId: patch.ctmSessionId || patch.ctm_session_id || existing.ctmSessionId,
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
function getRuntimeInputEnvelope(inputId) {
|
|
3837
|
+
const cleanInputId = String(inputId || '').trim();
|
|
3838
|
+
if (!cleanInputId) return null;
|
|
3839
|
+
return _parseRuntimeInputEnvelope(getDb().prepare('SELECT * FROM ctm_runtime_input_envelopes WHERE input_id = ?').get(cleanInputId));
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
function listRuntimeInputEnvelopes(ctmSessionId, { result = '', limit = 200 } = {}) {
|
|
3843
|
+
const cleanSessionId = String(ctmSessionId || '').trim();
|
|
3844
|
+
if (!cleanSessionId) return [];
|
|
3845
|
+
const cap = Math.max(1, Math.min(Number(limit) || 200, 1000));
|
|
3846
|
+
const cleanResult = String(result || '').trim();
|
|
3847
|
+
const rows = cleanResult
|
|
3848
|
+
? getDb().prepare('SELECT * FROM ctm_runtime_input_envelopes WHERE ctm_session_id = ? AND result = ? ORDER BY created_at_ms ASC LIMIT ?').all(cleanSessionId, cleanResult, cap)
|
|
3849
|
+
: getDb().prepare('SELECT * FROM ctm_runtime_input_envelopes WHERE ctm_session_id = ? ORDER BY created_at_ms ASC LIMIT ?').all(cleanSessionId, cap);
|
|
3850
|
+
return rows.map(_parseRuntimeInputEnvelope).filter(Boolean);
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
function _parseRuntimeRouteSnapshot(row) {
|
|
3854
|
+
if (!row) return null;
|
|
3855
|
+
return {
|
|
3856
|
+
routeId: row.route_id,
|
|
3857
|
+
route_id: row.route_id,
|
|
3858
|
+
ctmSessionId: row.ctm_session_id,
|
|
3859
|
+
ctm_session_id: row.ctm_session_id,
|
|
3860
|
+
agentSessionId: row.agent_session_id || '',
|
|
3861
|
+
agent_session_id: row.agent_session_id || '',
|
|
3862
|
+
turnId: row.turn_id || '',
|
|
3863
|
+
turn_id: row.turn_id || '',
|
|
3864
|
+
requestedModel: row.requested_model || '',
|
|
3865
|
+
requested_model: row.requested_model || '',
|
|
3866
|
+
resolvedModel: row.resolved_model || '',
|
|
3867
|
+
resolved_model: row.resolved_model || '',
|
|
3868
|
+
provider: row.provider || '',
|
|
3869
|
+
connectionLayer: row.connection_layer || 'unknown',
|
|
3870
|
+
connection_layer: row.connection_layer || 'unknown',
|
|
3871
|
+
routeSource: row.route_source || 'unknown',
|
|
3872
|
+
route_source: row.route_source || 'unknown',
|
|
3873
|
+
supportsImages: !!row.supports_images,
|
|
3874
|
+
supports_images: !!row.supports_images,
|
|
3875
|
+
supportsTools: !!row.supports_tools,
|
|
3876
|
+
supports_tools: !!row.supports_tools,
|
|
3877
|
+
supportsMcp: !!row.supports_mcp,
|
|
3878
|
+
supports_mcp: !!row.supports_mcp,
|
|
3879
|
+
createdAtMs: Number(row.created_at_ms || 0),
|
|
3880
|
+
created_at_ms: Number(row.created_at_ms || 0),
|
|
3881
|
+
metadata: _parseJsonObject(row.metadata_json),
|
|
3882
|
+
metadata_json: row.metadata_json || '{}',
|
|
3883
|
+
};
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
function upsertRuntimeRouteSnapshot(snapshot = {}) {
|
|
3887
|
+
const normalized = normalizeRouteSnapshot(snapshot);
|
|
3888
|
+
if (!normalized.ctmSessionId || !normalized.routeId) return null;
|
|
3889
|
+
getDb().prepare(`
|
|
3890
|
+
INSERT INTO ctm_runtime_route_snapshots (
|
|
3891
|
+
route_id, ctm_session_id, agent_session_id, turn_id, requested_model,
|
|
3892
|
+
resolved_model, provider, connection_layer, route_source, supports_images,
|
|
3893
|
+
supports_tools, supports_mcp, created_at_ms, metadata_json
|
|
3894
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3895
|
+
ON CONFLICT(route_id) DO UPDATE SET
|
|
3896
|
+
ctm_session_id = excluded.ctm_session_id,
|
|
3897
|
+
agent_session_id = excluded.agent_session_id,
|
|
3898
|
+
turn_id = excluded.turn_id,
|
|
3899
|
+
requested_model = excluded.requested_model,
|
|
3900
|
+
resolved_model = excluded.resolved_model,
|
|
3901
|
+
provider = excluded.provider,
|
|
3902
|
+
connection_layer = excluded.connection_layer,
|
|
3903
|
+
route_source = excluded.route_source,
|
|
3904
|
+
supports_images = excluded.supports_images,
|
|
3905
|
+
supports_tools = excluded.supports_tools,
|
|
3906
|
+
supports_mcp = excluded.supports_mcp,
|
|
3907
|
+
created_at_ms = excluded.created_at_ms,
|
|
3908
|
+
metadata_json = excluded.metadata_json
|
|
3909
|
+
`).run(
|
|
3910
|
+
normalized.routeId,
|
|
3911
|
+
normalized.ctmSessionId,
|
|
3912
|
+
normalized.agentSessionId,
|
|
3913
|
+
normalized.turnId,
|
|
3914
|
+
normalized.requestedModel,
|
|
3915
|
+
normalized.resolvedModel,
|
|
3916
|
+
normalized.provider,
|
|
3917
|
+
normalized.connectionLayer,
|
|
3918
|
+
normalized.routeSource,
|
|
3919
|
+
normalized.supportsImages ? 1 : 0,
|
|
3920
|
+
normalized.supportsTools ? 1 : 0,
|
|
3921
|
+
normalized.supportsMcp ? 1 : 0,
|
|
3922
|
+
normalized.createdAtMs,
|
|
3923
|
+
_runtimeJsonObject(normalized.metadata)
|
|
3924
|
+
);
|
|
3925
|
+
return _parseRuntimeRouteSnapshot(getDb().prepare('SELECT * FROM ctm_runtime_route_snapshots WHERE route_id = ?').get(normalized.routeId));
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
function getLatestRuntimeRouteSnapshot(ctmSessionId, { turnId = '' } = {}) {
|
|
3929
|
+
const cleanSessionId = String(ctmSessionId || '').trim();
|
|
3930
|
+
if (!cleanSessionId) return null;
|
|
3931
|
+
const cleanTurnId = String(turnId || '').trim();
|
|
3932
|
+
const row = cleanTurnId
|
|
3933
|
+
? getDb().prepare('SELECT * FROM ctm_runtime_route_snapshots WHERE ctm_session_id = ? AND turn_id = ? ORDER BY created_at_ms DESC LIMIT 1').get(cleanSessionId, cleanTurnId)
|
|
3934
|
+
: getDb().prepare('SELECT * FROM ctm_runtime_route_snapshots WHERE ctm_session_id = ? ORDER BY created_at_ms DESC LIMIT 1').get(cleanSessionId);
|
|
3935
|
+
return _parseRuntimeRouteSnapshot(row);
|
|
3936
|
+
}
|
|
3937
|
+
|
|
3938
|
+
function _parseRuntimeApproval(row) {
|
|
3939
|
+
if (!row) return null;
|
|
3940
|
+
return {
|
|
3941
|
+
approvalId: row.approval_id,
|
|
3942
|
+
approval_id: row.approval_id,
|
|
3943
|
+
ctmSessionId: row.ctm_session_id,
|
|
3944
|
+
ctm_session_id: row.ctm_session_id,
|
|
3945
|
+
agentSessionId: row.agent_session_id || '',
|
|
3946
|
+
agent_session_id: row.agent_session_id || '',
|
|
3947
|
+
turnId: row.turn_id || '',
|
|
3948
|
+
turn_id: row.turn_id || '',
|
|
3949
|
+
sourceEventId: row.source_event_id || '',
|
|
3950
|
+
source_event_id: row.source_event_id || '',
|
|
3951
|
+
status: row.status || 'pending',
|
|
3952
|
+
prompt: row.prompt || '',
|
|
3953
|
+
toolName: row.tool_name || '',
|
|
3954
|
+
tool_name: row.tool_name || '',
|
|
3955
|
+
decision: row.decision || '',
|
|
3956
|
+
decidedBy: row.decided_by || '',
|
|
3957
|
+
decided_by: row.decided_by || '',
|
|
3958
|
+
requestedAtMs: Number(row.requested_at_ms || 0),
|
|
3959
|
+
requested_at_ms: Number(row.requested_at_ms || 0),
|
|
3960
|
+
resolvedAtMs: Number(row.resolved_at_ms || 0),
|
|
3961
|
+
resolved_at_ms: Number(row.resolved_at_ms || 0),
|
|
3962
|
+
metadata: _parseJsonObject(row.metadata_json),
|
|
3963
|
+
metadata_json: row.metadata_json || '{}',
|
|
3964
|
+
updatedAtMs: Number(row.updated_at_ms || 0),
|
|
3965
|
+
updated_at_ms: Number(row.updated_at_ms || 0),
|
|
3966
|
+
};
|
|
3967
|
+
}
|
|
3968
|
+
|
|
3969
|
+
function upsertRuntimeApproval(approval = {}) {
|
|
3970
|
+
const normalized = normalizeApprovalRecord(approval);
|
|
3971
|
+
if (!normalized.ctmSessionId || !normalized.approvalId) return null;
|
|
3972
|
+
const now = Date.now();
|
|
3973
|
+
getDb().prepare(`
|
|
3974
|
+
INSERT INTO ctm_runtime_approvals (
|
|
3975
|
+
approval_id, ctm_session_id, agent_session_id, turn_id, source_event_id,
|
|
3976
|
+
status, prompt, tool_name, decision, decided_by, requested_at_ms,
|
|
3977
|
+
resolved_at_ms, metadata_json, updated_at_ms
|
|
3978
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
3979
|
+
ON CONFLICT(approval_id) DO UPDATE SET
|
|
3980
|
+
ctm_session_id = excluded.ctm_session_id,
|
|
3981
|
+
agent_session_id = excluded.agent_session_id,
|
|
3982
|
+
turn_id = excluded.turn_id,
|
|
3983
|
+
source_event_id = excluded.source_event_id,
|
|
3984
|
+
status = excluded.status,
|
|
3985
|
+
prompt = excluded.prompt,
|
|
3986
|
+
tool_name = excluded.tool_name,
|
|
3987
|
+
decision = excluded.decision,
|
|
3988
|
+
decided_by = excluded.decided_by,
|
|
3989
|
+
requested_at_ms = MIN(ctm_runtime_approvals.requested_at_ms, excluded.requested_at_ms),
|
|
3990
|
+
resolved_at_ms = excluded.resolved_at_ms,
|
|
3991
|
+
metadata_json = excluded.metadata_json,
|
|
3992
|
+
updated_at_ms = excluded.updated_at_ms
|
|
3993
|
+
`).run(
|
|
3994
|
+
normalized.approvalId,
|
|
3995
|
+
normalized.ctmSessionId,
|
|
3996
|
+
normalized.agentSessionId,
|
|
3997
|
+
normalized.turnId,
|
|
3998
|
+
normalized.sourceEventId,
|
|
3999
|
+
normalized.status,
|
|
4000
|
+
normalized.prompt,
|
|
4001
|
+
normalized.toolName,
|
|
4002
|
+
normalized.decision,
|
|
4003
|
+
normalized.decidedBy,
|
|
4004
|
+
normalized.requestedAtMs,
|
|
4005
|
+
normalized.resolvedAtMs,
|
|
4006
|
+
_runtimeJsonObject(normalized.metadata),
|
|
4007
|
+
now
|
|
4008
|
+
);
|
|
4009
|
+
return _parseRuntimeApproval(getDb().prepare('SELECT * FROM ctm_runtime_approvals WHERE approval_id = ?').get(normalized.approvalId));
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
function resolveRuntimeApproval(approvalId, { decision = '', decidedBy = '', status = 'approved', metadata = {} } = {}) {
|
|
4013
|
+
const existing = _parseRuntimeApproval(getDb().prepare('SELECT * FROM ctm_runtime_approvals WHERE approval_id = ?').get(String(approvalId || '').trim()));
|
|
4014
|
+
if (!existing) return null;
|
|
4015
|
+
return upsertRuntimeApproval({
|
|
4016
|
+
...existing,
|
|
4017
|
+
status,
|
|
4018
|
+
decision,
|
|
4019
|
+
decidedBy,
|
|
4020
|
+
resolvedAtMs: Date.now(),
|
|
4021
|
+
metadata: { ...(existing.metadata || {}), ...(metadata || {}) },
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
function listRuntimeApprovals(ctmSessionId, { status = '', limit = 100 } = {}) {
|
|
4026
|
+
const cleanSessionId = String(ctmSessionId || '').trim();
|
|
4027
|
+
if (!cleanSessionId) return [];
|
|
4028
|
+
const cap = Math.max(1, Math.min(Number(limit) || 100, 500));
|
|
4029
|
+
const cleanStatus = String(status || '').trim();
|
|
4030
|
+
const rows = cleanStatus
|
|
4031
|
+
? getDb().prepare('SELECT * FROM ctm_runtime_approvals WHERE ctm_session_id = ? AND status = ? ORDER BY requested_at_ms DESC LIMIT ?').all(cleanSessionId, cleanStatus, cap)
|
|
4032
|
+
: getDb().prepare('SELECT * FROM ctm_runtime_approvals WHERE ctm_session_id = ? ORDER BY requested_at_ms DESC LIMIT ?').all(cleanSessionId, cap);
|
|
4033
|
+
return rows.map(_parseRuntimeApproval).filter(Boolean);
|
|
4034
|
+
}
|
|
4035
|
+
|
|
3387
4036
|
// --- Domain auto-classification ---
|
|
3388
4037
|
const DOMAIN_RULES = [
|
|
3389
4038
|
{ domain: 'coding', patterns: /\b(code|function|class|method|variable|bug|test|refactor|implement|api|endpoint|import|export|module|compile|syntax|type(script)?|lint|format|regex|array|object|string|int|float|boolean|null|undefined|return|async|await|promise|callback|error handling|unit test|integration test|debug|stack trace|exception|try.catch|react|component|hook|useState|useEffect|vue|angular|svelte|html|css|dom|frontend|backend|database|sql|orm|schema|migration|git|commit|branch|merge|pr|pull.request|npm|pip|cargo|maven|gradle|python|javascript|java|rust|go|ruby|php|swift|kotlin)\b/i },
|
|
@@ -4373,6 +5022,14 @@ const _overCapImportLogged = new Set(); // session_id → already warned about a
|
|
|
4373
5022
|
// when the count grew past a delta (or the model changed); otherwise the upsert's COALESCE preserves
|
|
4374
5023
|
// the prior stored value — exact recompute still happens on the live path for Claude/Codex.
|
|
4375
5024
|
const _importTokenEstimateLen = new Map();
|
|
5025
|
+
// Blob retirement is the shipped default: stop writing the monolithic `messages` blob (store '[]')
|
|
5026
|
+
// because the faithful session_message_rows serve every read. Two safety conditions gate it:
|
|
5027
|
+
// - CTM_DUAL_WRITE_BLOB=1 → rollback hatch: keep dual-writing the real blob.
|
|
5028
|
+
// - CTM_SESSION_ROWS=0 → rows READ path is off, so nulling the blob would blind reads → keep it.
|
|
5029
|
+
// lib/session-content-backfill.js mirrors this predicate for the reclaim sweep (keep them in lockstep).
|
|
5030
|
+
function _blobRetirementActive() {
|
|
5031
|
+
return process.env.CTM_DUAL_WRITE_BLOB !== '1' && process.env.CTM_SESSION_ROWS !== '0';
|
|
5032
|
+
}
|
|
4376
5033
|
function importSessionConversation({
|
|
4377
5034
|
session_id, project_path, messages, user_msg_count, assistant_msg_count,
|
|
4378
5035
|
search_messages,
|
|
@@ -4386,6 +5043,10 @@ function importSessionConversation({
|
|
|
4386
5043
|
// render shape differs from `messages` (Wall-E persists review-shaped rows, not the raw transcript
|
|
4387
5044
|
// blob shape). The metadata upsert + blob still happen so the freshness gate + legacy reads work.
|
|
4388
5045
|
skipMessageRows,
|
|
5046
|
+
// When true, write the real blob even under blob retirement so the session renders from the blob
|
|
5047
|
+
// while content-rows-backfill fills rows across slices (chunked/cold-import route). Over-cap still
|
|
5048
|
+
// wins — a multi-GB stringify is never safe regardless of this flag.
|
|
5049
|
+
forceBlobWrite,
|
|
4389
5050
|
}) {
|
|
4390
5051
|
// Attribution: the whole import is one synchronous span — a JSON.stringify of
|
|
4391
5052
|
// the full message array (multi-MB for 2000+ prompt sessions) + an upsert + a
|
|
@@ -4404,16 +5065,19 @@ function importSessionConversation({
|
|
|
4404
5065
|
let _isOverParseCap = null;
|
|
4405
5066
|
try { _isOverParseCap = require('./lib/size-cap').isOverParseCap; } catch { /* optional */ }
|
|
4406
5067
|
const _overParseCap = typeof _isOverParseCap === 'function' ? _isOverParseCap(file_size) : false;
|
|
4407
|
-
// Phase 7 (blob retirement):
|
|
4408
|
-
//
|
|
4409
|
-
//
|
|
4410
|
-
//
|
|
4411
|
-
//
|
|
4412
|
-
//
|
|
4413
|
-
//
|
|
4414
|
-
//
|
|
4415
|
-
const _retireBlob =
|
|
4416
|
-
|
|
5068
|
+
// Phase 7 (blob retirement): reads are served from session_message_rows (the
|
|
5069
|
+
// faithful per-message store), so the monolithic `messages` blob is dead weight
|
|
5070
|
+
// and its full-array JSON.stringify on every append is the O(N²) write cost we
|
|
5071
|
+
// want gone. DEFAULT (shipped to everyone, incl. npx): retire the blob (store
|
|
5072
|
+
// '[]'). Rollback hatch: CTM_DUAL_WRITE_BLOB=1 keeps dual-writing the real blob.
|
|
5073
|
+
// SAFETY: only retire when the rows READ path is on (CTM_SESSION_ROWS != '0',
|
|
5074
|
+
// the default), otherwise nulling the blob would blind every read; and the row
|
|
5075
|
+
// write below restores the real blob if it fails, so a session is never empty.
|
|
5076
|
+
const _retireBlob = _blobRetirementActive();
|
|
5077
|
+
// forceBlobWrite (cold/large chunked-import route) writes the real blob even under retirement so
|
|
5078
|
+
// the session renders from blob while content-rows-backfill fills rows across slices. Over-cap
|
|
5079
|
+
// still wins (a multi-GB stringify is never safe).
|
|
5080
|
+
const _skipBlobWrite = _overParseCap || (_retireBlob && !forceBlobWrite);
|
|
4417
5081
|
if (_overParseCap && !_overCapImportLogged.has(session_id)) {
|
|
4418
5082
|
_overCapImportLogged.add(session_id);
|
|
4419
5083
|
console.warn(`[db] session_conversations import over parse cap (file_size=${file_size}) for ${String(session_id || '').slice(0, 8)} — storing empty blob, skipping full stringify/row-rewrite.`);
|
|
@@ -4435,7 +5099,8 @@ function importSessionConversation({
|
|
|
4435
5099
|
const _prevEst = _importTokenEstimateLen.get(session_id);
|
|
4436
5100
|
const _modelChanged = !_prevEst || _prevEst.model !== (model_id || '');
|
|
4437
5101
|
const _grewEnough = !_prevEst || _reestimateDelta === 0 || (_msgLen - _prevEst.len) >= _reestimateDelta;
|
|
4438
|
-
|
|
5102
|
+
const _chunkedRoute = !!forceBlobWrite && !!skipMessageRows && !_overParseCap;
|
|
5103
|
+
if (!_overParseCap && !_chunkedRoute && (_modelChanged || _grewEnough)) {
|
|
4439
5104
|
try {
|
|
4440
5105
|
const summary = require('./lib/session-token-usage').estimateFromMessages(messages || [], model_id || '');
|
|
4441
5106
|
if (summary && summary.total > 0) {
|
|
@@ -4490,6 +5155,14 @@ function importSessionConversation({
|
|
|
4490
5155
|
_tokTotal, _tokCtx, _tokWindow, _tokExact, _tokBreakdown
|
|
4491
5156
|
);
|
|
4492
5157
|
|
|
5158
|
+
// Chunked-import route: rows are written later by content-rows-backfill (in windows). Stamp the
|
|
5159
|
+
// render-gate HWM now so findUnbackfilledSessions (extracted_source_len>0 AND rows<len) adopts
|
|
5160
|
+
// this session; appendSessionMessageRowsChunk re-stamps the same value idempotently on completion.
|
|
5161
|
+
if (_chunkedRoute) {
|
|
5162
|
+
try { _setExtractedSourceLen(getDb(), session_id, _msgLen, _lastMessageAt); }
|
|
5163
|
+
catch (e) { console.error('[db] chunked-import source-len stamp failed:', e.message); }
|
|
5164
|
+
}
|
|
5165
|
+
|
|
4493
5166
|
// Keep message-level search/review surfaces in lockstep with the durable
|
|
4494
5167
|
// conversation cache. The older one-shot backfill path intentionally skips
|
|
4495
5168
|
// sessions that already have rows, which made long-running imported Codex
|
|
@@ -5280,7 +5953,27 @@ const _AI_REFINEMENT_FAILED_MAX_ROWS = Number(process.env.CTM_AI_REFINEMENT_FAIL
|
|
|
5280
5953
|
// (a few new failed rows/hour) never approaches the cap.
|
|
5281
5954
|
const _AI_REFINEMENT_PRUNE_MAX_PER_RUN = Number(process.env.CTM_AI_REFINEMENT_PRUNE_MAX_PER_RUN || 5000);
|
|
5282
5955
|
|
|
5283
|
-
|
|
5956
|
+
const _APPROVAL_OBS_INSERT_SQL = `
|
|
5957
|
+
INSERT INTO approval_observations (
|
|
5958
|
+
session_id,
|
|
5959
|
+
provider_id,
|
|
5960
|
+
source,
|
|
5961
|
+
raw_detected,
|
|
5962
|
+
gated,
|
|
5963
|
+
gate_reason,
|
|
5964
|
+
parse_status,
|
|
5965
|
+
policy_decision,
|
|
5966
|
+
decided_by,
|
|
5967
|
+
keystroke_status,
|
|
5968
|
+
screen_fingerprint,
|
|
5969
|
+
redacted_screen_tail,
|
|
5970
|
+
debug_lines
|
|
5971
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5972
|
+
`;
|
|
5973
|
+
|
|
5974
|
+
// Bind args for one approval_observations row. SHARED by the single-row insert and the batch
|
|
5975
|
+
// insert so per-row semantics (tail trimming, debug cap) can never diverge between the two paths.
|
|
5976
|
+
function _bindApprovalObservationRow({
|
|
5284
5977
|
sessionId,
|
|
5285
5978
|
providerId,
|
|
5286
5979
|
source,
|
|
@@ -5304,23 +5997,7 @@ function addApprovalObservation({
|
|
|
5304
5997
|
const _tailMax = _keepFullTail
|
|
5305
5998
|
? _APPROVAL_OBS_TAIL_MAX_GATED
|
|
5306
5999
|
: _APPROVAL_OBS_TAIL_MAX_UNGATED;
|
|
5307
|
-
|
|
5308
|
-
INSERT INTO approval_observations (
|
|
5309
|
-
session_id,
|
|
5310
|
-
provider_id,
|
|
5311
|
-
source,
|
|
5312
|
-
raw_detected,
|
|
5313
|
-
gated,
|
|
5314
|
-
gate_reason,
|
|
5315
|
-
parse_status,
|
|
5316
|
-
policy_decision,
|
|
5317
|
-
decided_by,
|
|
5318
|
-
keystroke_status,
|
|
5319
|
-
screen_fingerprint,
|
|
5320
|
-
redacted_screen_tail,
|
|
5321
|
-
debug_lines
|
|
5322
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
5323
|
-
`).run(
|
|
6000
|
+
return [
|
|
5324
6001
|
sessionId || '',
|
|
5325
6002
|
providerId || '',
|
|
5326
6003
|
source || '',
|
|
@@ -5334,10 +6011,36 @@ function addApprovalObservation({
|
|
|
5334
6011
|
screenFingerprint || '',
|
|
5335
6012
|
String(redactedScreenTail || '').slice(-_tailMax),
|
|
5336
6013
|
debug.slice(-2000),
|
|
5337
|
-
|
|
6014
|
+
];
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
function addApprovalObservation(observation = {}) {
|
|
6018
|
+
const result = getDb().prepare(_APPROVAL_OBS_INSERT_SQL).run(..._bindApprovalObservationRow(observation));
|
|
5338
6019
|
return result.lastInsertRowid;
|
|
5339
6020
|
}
|
|
5340
6021
|
|
|
6022
|
+
// Coalesced sink for the approval-observation write path: insert N observations in ONE
|
|
6023
|
+
// transaction (one write-lock acquire / one commit) with byte-identical per-row semantics to
|
|
6024
|
+
// addApprovalObservation. The single-threaded db-owner worker was flooded by one INSERT op per
|
|
6025
|
+
// event (~20k/day); batching collapses that to a few ops/flush-window. Falsy entries are skipped;
|
|
6026
|
+
// returns the number of rows inserted.
|
|
6027
|
+
function addApprovalObservationsBatch(entries) {
|
|
6028
|
+
if (!Array.isArray(entries) || entries.length === 0) return 0;
|
|
6029
|
+
const valid = entries.filter((e) => e && typeof e === 'object');
|
|
6030
|
+
if (valid.length === 0) return 0;
|
|
6031
|
+
const d = getDb();
|
|
6032
|
+
const stmt = d.prepare(_APPROVAL_OBS_INSERT_SQL);
|
|
6033
|
+
const insertAll = d.transaction((rows) => {
|
|
6034
|
+
let inserted = 0;
|
|
6035
|
+
for (const obs of rows) {
|
|
6036
|
+
stmt.run(..._bindApprovalObservationRow(obs));
|
|
6037
|
+
inserted++;
|
|
6038
|
+
}
|
|
6039
|
+
return inserted;
|
|
6040
|
+
});
|
|
6041
|
+
return insertAll(valid);
|
|
6042
|
+
}
|
|
6043
|
+
|
|
5341
6044
|
// Retention for approval_observations. The approver records ~20k observations/day, each
|
|
5342
6045
|
// historically carrying a ~4KB screen tail, with no pruning — the table had grown to
|
|
5343
6046
|
// 267MB. This caps it by age and by row count. Heavy DELETE → run on the db-owner worker
|
|
@@ -7259,6 +7962,9 @@ function replaceSessionMessages(sessionId, messages) {
|
|
|
7259
7962
|
// transcript timestamp is available, advance last_message_at without allowing
|
|
7260
7963
|
// partial rewrites or compacted sources to move activity backward.
|
|
7261
7964
|
function _setExtractedSourceLen(d, ctmSessionId, sourceLen, lastMessageAt = '') {
|
|
7965
|
+
// Any row write reaches here to advance the watermark — drop the cached rows-available verdict so
|
|
7966
|
+
// sessionContentRowsAvailable re-COUNTs once against the new state (covers same-watermark mutations).
|
|
7967
|
+
_invalidateRowsAvailCache(ctmSessionId);
|
|
7262
7968
|
try {
|
|
7263
7969
|
d.prepare('UPDATE session_conversations SET extracted_source_len = ? WHERE ctm_session_id = ?')
|
|
7264
7970
|
.run(Number(sourceLen) || 0, ctmSessionId);
|
|
@@ -7432,6 +8138,42 @@ function replaceSessionMessageRows(sessionId, messages) {
|
|
|
7432
8138
|
return normalized.length;
|
|
7433
8139
|
}
|
|
7434
8140
|
|
|
8141
|
+
// Incremental, resumable backfill of ONE session's rows in a bounded chunk. The background
|
|
8142
|
+
// migration calls this once per session per scheduler tick so a single giant session (16k+
|
|
8143
|
+
// messages) is never rewritten in one write-lock-holding transaction — it fills in index order
|
|
8144
|
+
// across ticks. The store is dense and keyed by message_index, so INSERT OR IGNORE makes this
|
|
8145
|
+
// idempotent: re-running resumes from the existing row count (passed as startIndex) and never
|
|
8146
|
+
// double-writes. extracted_source_len (the render gate's target) is only stamped when the final
|
|
8147
|
+
// chunk lands, so a half-filled session stays on the blob path (sessionContentRowsAvailable=false)
|
|
8148
|
+
// until it is 100% backfilled. Returns {written, nextIndex, complete}.
|
|
8149
|
+
function appendSessionMessageRowsChunk(sessionId, messages, startIndex = 0, maxCount = 0) {
|
|
8150
|
+
if (!sessionId || !_tableExists('session_message_rows')) {
|
|
8151
|
+
return { written: 0, nextIndex: Number(startIndex) || 0, complete: false };
|
|
8152
|
+
}
|
|
8153
|
+
const d = getDb();
|
|
8154
|
+
const normalized = Array.isArray(messages) ? messages : [];
|
|
8155
|
+
const start = Math.max(0, Math.min(normalized.length, Number(startIndex) || 0));
|
|
8156
|
+
const cap = Number(maxCount) > 0 ? Number(maxCount) : (normalized.length - start);
|
|
8157
|
+
const end = Math.min(normalized.length, start + Math.max(1, cap));
|
|
8158
|
+
const insertRow = d.prepare(
|
|
8159
|
+
'INSERT OR IGNORE INTO session_message_rows (ctm_session_id, message_index, role, text, timestamp, meta) VALUES (?, ?, ?, ?, ?, ?)'
|
|
8160
|
+
);
|
|
8161
|
+
const writeRow = (i) => {
|
|
8162
|
+
const m = normalized[i] || {};
|
|
8163
|
+
return insertRow.run(
|
|
8164
|
+
sessionId, i, String(m.role || ''),
|
|
8165
|
+
String(m.text != null ? m.text : (m.content != null ? m.content : '')),
|
|
8166
|
+
m.timestamp != null ? String(m.timestamp) : '',
|
|
8167
|
+
_messageRowMeta(m)
|
|
8168
|
+
);
|
|
8169
|
+
};
|
|
8170
|
+
let written = 0;
|
|
8171
|
+
d.transaction(() => { for (let i = start; i < end; i++) { if (writeRow(i).changes > 0) written++; } })();
|
|
8172
|
+
const complete = end >= normalized.length;
|
|
8173
|
+
if (complete) _setExtractedSourceLen(d, sessionId, normalized.length, _lastMessageAtFromMessages(normalized));
|
|
8174
|
+
return { written, nextIndex: end, complete };
|
|
8175
|
+
}
|
|
8176
|
+
|
|
7435
8177
|
// O(Δ) append of NEW messages after a verified-dense, unchanged-prefix base. Unlike
|
|
7436
8178
|
// replaceSessionMessageRows this NEVER loads or rewrites the base array, so it stays O(Δ) for an
|
|
7437
8179
|
// active multi-thousand-message session (that full-array load + re-process was the giant-transcript
|
|
@@ -7558,8 +8300,10 @@ function appendSessionConversation({
|
|
|
7558
8300
|
function getSessionMessagesArray(sessionId, { fallbackToBlob = true } = {}) {
|
|
7559
8301
|
const d = getDb();
|
|
7560
8302
|
if (_tableExists('session_message_rows')) {
|
|
7561
|
-
|
|
7562
|
-
|
|
8303
|
+
// Delegate the SELECT + row→object map to the shared lib so this row read is byte-identical
|
|
8304
|
+
// to the read-pool worker's off-thread getSessionMessagesArray op (parity by construction).
|
|
8305
|
+
const rows = require('./lib/session-messages-page').getMessagesArray(d, { sessionId });
|
|
8306
|
+
if (rows.length) return rows;
|
|
7563
8307
|
}
|
|
7564
8308
|
if (fallbackToBlob) {
|
|
7565
8309
|
try {
|
|
@@ -7573,16 +8317,40 @@ function getSessionMessagesArray(sessionId, { fallbackToBlob = true } = {}) {
|
|
|
7573
8317
|
// True only when the faithful rows can serve this session: present AND fully backfilled
|
|
7574
8318
|
// (row count covers the conversation's extracted length). A half-migrated session returns
|
|
7575
8319
|
// false → the caller uses the complete blob/JSONL path.
|
|
8320
|
+
// Watermark-keyed cache of the "rows fully cover the conversation" verdict. sessionContentRowsAvailable
|
|
8321
|
+
// is on MANY hot/periodic main-loop paths — most heavily the per-codex-session snapshot serialize
|
|
8322
|
+
// (_serializeSessionSnapshot → _codexRolloutHistoryActive → _codexRowLookupId), plus the
|
|
8323
|
+
// apiSessionMessages gate, attach, and exit — and each call ran an O(rows) COUNT(*) that cold-page
|
|
8324
|
+
// faults for seconds (the `(unknown)` cpu-low apiSessionMessages/snapshot freeze on the primary).
|
|
8325
|
+
// The gate is `cnt >= expected` where expected = extracted_source_len (the import high-water mark).
|
|
8326
|
+
// Rows only grow and `expected` advances only as import progresses, so a TRUE verdict stays true
|
|
8327
|
+
// while `expected` is unchanged. Cache ONLY true verdicts keyed on `expected`: a key match means
|
|
8328
|
+
// cnt was already ≥ this watermark and (rows-only-grow) still is — skip the COUNT(*). A false/
|
|
8329
|
+
// half-migrated verdict is NOT cached (it must be re-checked so a just-completed migration is
|
|
8330
|
+
// picked up). Row writes invalidate the entry (belt-and-suspenders for a same-watermark row
|
|
8331
|
+
// mutation). CTM_ROWS_AVAIL_CACHE=0 disables.
|
|
8332
|
+
const _rowsAvailCache = new Map(); // ctm_session_id -> expected (watermark at which it was TRUE)
|
|
8333
|
+
function _invalidateRowsAvailCache(sessionId) { _rowsAvailCache.delete(String(sessionId || '')); }
|
|
7576
8334
|
function sessionContentRowsAvailable(sessionId) {
|
|
7577
8335
|
try {
|
|
7578
8336
|
const d = getDb();
|
|
7579
8337
|
if (!_tableExists('session_message_rows')) return false;
|
|
7580
|
-
|
|
7581
|
-
|
|
8338
|
+
// Cheap watermark read first (single non-blob column); also lets us skip the COUNT entirely for
|
|
8339
|
+
// sessions with no HWM (always false under the old logic too).
|
|
7582
8340
|
const conv = d.prepare('SELECT extracted_source_len FROM session_conversations WHERE ctm_session_id = ?').get(sessionId);
|
|
7583
8341
|
const expected = conv ? Number(conv.extracted_source_len) : 0;
|
|
7584
|
-
if (!(expected > 0)
|
|
7585
|
-
|
|
8342
|
+
if (!(expected > 0)) { _invalidateRowsAvailCache(sessionId); return false; } // unknown HWM → blob path
|
|
8343
|
+
const cacheOn = process.env.CTM_ROWS_AVAIL_CACHE !== '0';
|
|
8344
|
+
if (cacheOn && _rowsAvailCache.get(sessionId) === expected) return true; // TRUE@expected still holds (rows only grow)
|
|
8345
|
+
const cnt = Number(d.prepare('SELECT COUNT(*) AS n FROM session_message_rows WHERE ctm_session_id = ?').get(sessionId).n);
|
|
8346
|
+
const available = cnt > 0 && cnt >= expected; // unknown HWM or half-migrated → blob path
|
|
8347
|
+
if (available && cacheOn) {
|
|
8348
|
+
_rowsAvailCache.set(sessionId, expected);
|
|
8349
|
+
if (_rowsAvailCache.size > 1024) { const k = _rowsAvailCache.keys().next().value; _rowsAvailCache.delete(k); }
|
|
8350
|
+
} else {
|
|
8351
|
+
_invalidateRowsAvailCache(sessionId);
|
|
8352
|
+
}
|
|
8353
|
+
return available;
|
|
7586
8354
|
} catch { return false; }
|
|
7587
8355
|
}
|
|
7588
8356
|
|
|
@@ -7704,8 +8472,15 @@ function runContentRowsBackfillSweep(options = {}) {
|
|
|
7704
8472
|
const limit = Math.max(1, Math.min(500, Number(o.limit) || 25));
|
|
7705
8473
|
const budgetMs = Math.max(0, Number(o.budgetMs ?? process.env.CTM_ROWS_BACKFILL_BUDGET_MS ?? 250));
|
|
7706
8474
|
const maxRows = Math.max(0, Number(o.maxRows ?? process.env.CTM_ROWS_BACKFILL_MAX_ROWS ?? 3000));
|
|
8475
|
+
// Per-session row cap: migrate a giant session in bounded slices across ticks (resumable) so one
|
|
8476
|
+
// session can't hold the write lock for its whole length. 0 = whole-session (legacy/tests).
|
|
8477
|
+
const maxRowsPerSession = Math.max(0, Number(o.maxRowsPerSession ?? process.env.CTM_ROWS_BACKFILL_MAX_ROWS_PER_SESSION ?? 2000));
|
|
7707
8478
|
const bf = require('./lib/session-content-backfill');
|
|
7708
|
-
return bf.runContentRowsBackfillSweep(getDb(), replaceSessionMessageRows, {
|
|
8479
|
+
return bf.runContentRowsBackfillSweep(getDb(), replaceSessionMessageRows, {
|
|
8480
|
+
limit, budgetMs, maxRows, maxRowsPerSession,
|
|
8481
|
+
appendChunk: appendSessionMessageRowsChunk,
|
|
8482
|
+
getRowCount: (id) => countSessionMessageRows(id),
|
|
8483
|
+
});
|
|
7709
8484
|
}
|
|
7710
8485
|
|
|
7711
8486
|
// Migration completeness for GET /api/ctm/session-rows-status (+ the cutover gate).
|
|
@@ -7733,9 +8508,28 @@ function _messageFreshnessStatsForId(id, { userOnly = false } = {}) {
|
|
|
7733
8508
|
const d = getDb();
|
|
7734
8509
|
const bindId = String(id);
|
|
7735
8510
|
const whereRole = userOnly ? " AND role = 'user'" : '';
|
|
7736
|
-
|
|
8511
|
+
// The all-message session_message_rows store is DENSE — every message is a row at its array index
|
|
8512
|
+
// (replaceSessionMessageRows / appendSessionMessageRowsChunk write message_index = i contiguously
|
|
8513
|
+
// from 0, an invariant the row write paths already rely on), so COUNT(*) === MAX(message_index)+1.
|
|
8514
|
+
// The per-poll COUNT(*) over a giant (14k-19k-row) session was a 5.25s/4min on-main cost in the
|
|
8515
|
+
// live CPU profile; the query already computes MAX (an O(1) index seek), so deriving rows from it
|
|
8516
|
+
// drops the only O(rows) term with NO schema or write-path change — strictly better than a
|
|
8517
|
+
// maintained counter (no cross-thread drift). Equivalent as a change-detector: maxIndex moves on
|
|
8518
|
+
// every append, and the store is append-only / full-replace (no mid-array deletes), so maxIndex+1
|
|
8519
|
+
// changes exactly when the message set does. User-only rows are SPARSE (no density) and the legacy
|
|
8520
|
+
// session_messages store isn't guaranteed dense, so both keep the exact COUNT(*).
|
|
8521
|
+
const countStats = (table) => d.prepare(
|
|
7737
8522
|
`SELECT COALESCE(MAX(message_index), -1) AS maxIndex, COUNT(*) AS rows FROM ${table} WHERE ctm_session_id = ?${whereRole}`
|
|
7738
8523
|
).get(bindId);
|
|
8524
|
+
const denseStats = (table) => {
|
|
8525
|
+
const r = d.prepare(
|
|
8526
|
+
`SELECT COALESCE(MAX(message_index), -1) AS maxIndex FROM ${table} WHERE ctm_session_id = ?`
|
|
8527
|
+
).get(bindId);
|
|
8528
|
+
const maxIndex = Number(r && r.maxIndex != null ? r.maxIndex : -1);
|
|
8529
|
+
return { maxIndex, rows: maxIndex + 1 };
|
|
8530
|
+
};
|
|
8531
|
+
const readStats = (table) =>
|
|
8532
|
+
(!userOnly && table === 'session_message_rows') ? denseStats(table) : countStats(table);
|
|
7739
8533
|
|
|
7740
8534
|
// The row store is the default read source, but migration and tests can leave
|
|
7741
8535
|
// some sessions present only in the legacy filtered index. Pick per session
|
|
@@ -8180,10 +8974,10 @@ function getLatestAgentSessionForCtm(ctmSessionId, options = {}) {
|
|
|
8180
8974
|
return null;
|
|
8181
8975
|
}
|
|
8182
8976
|
|
|
8183
|
-
function getSessionConversationSourceIds(id, options = {}) {
|
|
8977
|
+
function getSessionConversationSourceIds(id, options = {}, db) {
|
|
8184
8978
|
const cleanId = String(id || '').trim();
|
|
8185
8979
|
if (!cleanId) return [];
|
|
8186
|
-
const identity = getSessionIdentity(cleanId);
|
|
8980
|
+
const identity = getSessionIdentity(cleanId, db);
|
|
8187
8981
|
const ids = [];
|
|
8188
8982
|
const add = (value) => {
|
|
8189
8983
|
const s = String(value || '').trim();
|
|
@@ -8210,10 +9004,13 @@ function getSessionConversationSourceIds(id, options = {}) {
|
|
|
8210
9004
|
* lifecycle/restore table; orphan recovery can use it explicitly, but normal
|
|
8211
9005
|
* reads must not treat it as a second mapping source.
|
|
8212
9006
|
*/
|
|
8213
|
-
function getSessionIdentity(id) {
|
|
9007
|
+
function getSessionIdentity(id, db) {
|
|
8214
9008
|
const cleanId = String(id || '').trim();
|
|
8215
9009
|
if (!cleanId) return null;
|
|
8216
|
-
|
|
9010
|
+
// db-injectable: the read-pool worker passes its own read-only connection so the
|
|
9011
|
+
// prompt-index input resolution runs off the main loop (parity by construction —
|
|
9012
|
+
// same query bodies, different handle). Existing callers pass nothing → singleton.
|
|
9013
|
+
const d = db || getDb();
|
|
8217
9014
|
|
|
8218
9015
|
let ctm = d.prepare('SELECT * FROM ctm_sessions WHERE id = ?').get(cleanId) || null;
|
|
8219
9016
|
let ctmSessionId = ctm ? ctm.id : '';
|
|
@@ -8710,14 +9507,15 @@ function upsertAgentSessionIdentity(agentSessionId, data = {}) {
|
|
|
8710
9507
|
parent_agent_session_id: lineage.parent_agent_session_id,
|
|
8711
9508
|
agent_nickname: lineage.agent_nickname,
|
|
8712
9509
|
agent_role: lineage.agent_role,
|
|
9510
|
+
forked_from_desktop_uuid: data.forkedFromDesktopUuid || data.forked_from_desktop_uuid || '',
|
|
8713
9511
|
};
|
|
8714
9512
|
d.prepare(`
|
|
8715
9513
|
INSERT INTO agent_sessions (agent_session_id, ctm_session_id, provider, provider_resume_id, project_path, jsonl_path,
|
|
8716
9514
|
first_message, file_size, modified_at, hostname, model, git_branch, user_msg_count, slug,
|
|
8717
|
-
thread_source, parent_agent_session_id, agent_nickname, agent_role)
|
|
9515
|
+
thread_source, parent_agent_session_id, agent_nickname, agent_role, forked_from_desktop_uuid)
|
|
8718
9516
|
VALUES (@agent_session_id, @ctm_session_id, @provider, @provider_resume_id, @project_path, @jsonl_path,
|
|
8719
9517
|
@first_message, @file_size, @modified_at, @hostname, @model, @git_branch, @user_msg_count, @slug,
|
|
8720
|
-
@thread_source, @parent_agent_session_id, @agent_nickname, @agent_role)
|
|
9518
|
+
@thread_source, @parent_agent_session_id, @agent_nickname, @agent_role, @forked_from_desktop_uuid)
|
|
8721
9519
|
ON CONFLICT(agent_session_id) DO UPDATE SET
|
|
8722
9520
|
ctm_session_id = COALESCE(agent_sessions.ctm_session_id, excluded.ctm_session_id),
|
|
8723
9521
|
provider = COALESCE(NULLIF(excluded.provider, ''), agent_sessions.provider),
|
|
@@ -8736,10 +9534,40 @@ function upsertAgentSessionIdentity(agentSessionId, data = {}) {
|
|
|
8736
9534
|
parent_agent_session_id = COALESCE(NULLIF(excluded.parent_agent_session_id, ''), agent_sessions.parent_agent_session_id),
|
|
8737
9535
|
agent_nickname = COALESCE(NULLIF(excluded.agent_nickname, ''), agent_sessions.agent_nickname),
|
|
8738
9536
|
agent_role = COALESCE(NULLIF(excluded.agent_role, ''), agent_sessions.agent_role),
|
|
9537
|
+
forked_from_desktop_uuid = COALESCE(NULLIF(excluded.forked_from_desktop_uuid, ''), agent_sessions.forked_from_desktop_uuid),
|
|
8739
9538
|
updated_at = datetime('now')
|
|
8740
9539
|
`).run(params);
|
|
8741
9540
|
}
|
|
8742
9541
|
|
|
9542
|
+
/**
|
|
9543
|
+
* Look up the Claude Code fork that was created from a given Claude Desktop conversation.
|
|
9544
|
+
* Returns the fork's agent_sessions row ({ agent_session_id, jsonl_path, ctm_session_id })
|
|
9545
|
+
* or null when the conversation has never been converted. Callers verify the fork still
|
|
9546
|
+
* exists (row + jsonl file) before treating the conversation as already-converted; a deleted
|
|
9547
|
+
* fork should re-offer conversion. Anchored on the stable Desktop uuid so a re-scan of the
|
|
9548
|
+
* Desktop cache never mints a duplicate fork.
|
|
9549
|
+
*/
|
|
9550
|
+
function getForkForDesktopUuid(desktopUuid) {
|
|
9551
|
+
const id = String(desktopUuid || '').trim();
|
|
9552
|
+
if (!id) return null;
|
|
9553
|
+
return getDb().prepare(
|
|
9554
|
+
'SELECT agent_session_id, jsonl_path, ctm_session_id FROM agent_sessions WHERE forked_from_desktop_uuid = ? LIMIT 1'
|
|
9555
|
+
).get(id) || null;
|
|
9556
|
+
}
|
|
9557
|
+
|
|
9558
|
+
/**
|
|
9559
|
+
* List every Claude Code fork created from a Claude Desktop conversation, keyed by the
|
|
9560
|
+
* originating Desktop uuid. The client fetches this to decide, per Desktop conversation,
|
|
9561
|
+
* whether to offer "Convert" or "Resume fork". Callers verify the transcript still exists
|
|
9562
|
+
* before trusting the link (a deleted fork should re-offer conversion).
|
|
9563
|
+
*/
|
|
9564
|
+
function listDesktopForks() {
|
|
9565
|
+
return getDb().prepare(
|
|
9566
|
+
"SELECT forked_from_desktop_uuid AS desktopUuid, agent_session_id AS forkSessionId, jsonl_path AS jsonlPath, project_path AS projectPath " +
|
|
9567
|
+
"FROM agent_sessions WHERE forked_from_desktop_uuid != ''"
|
|
9568
|
+
).all();
|
|
9569
|
+
}
|
|
9570
|
+
|
|
8743
9571
|
function setSessionStar(id, starred) {
|
|
8744
9572
|
// Try ctm_sessions first
|
|
8745
9573
|
const result = getDb().prepare("UPDATE ctm_sessions SET starred = ?, updated_at = datetime('now') WHERE id = ?")
|
|
@@ -9558,8 +10386,8 @@ function deleteCtmSession(ctmSessionId) {
|
|
|
9558
10386
|
|
|
9559
10387
|
// Legacy compatibility: upsertSessionIndex is now a no-op (session_index dropped)
|
|
9560
10388
|
function upsertSessionIndex() {}
|
|
9561
|
-
function resolveSessionId(id) {
|
|
9562
|
-
const identity = getSessionIdentity(id);
|
|
10389
|
+
function resolveSessionId(id, db) {
|
|
10390
|
+
const identity = getSessionIdentity(id, db);
|
|
9563
10391
|
if (!identity) return null;
|
|
9564
10392
|
return {
|
|
9565
10393
|
ctm_session_id: identity.ctm_session_id,
|
|
@@ -9575,6 +10403,12 @@ module.exports = {
|
|
|
9575
10403
|
getWriteLockStats, resetWriteLockStats,
|
|
9576
10404
|
getStorageRisk,
|
|
9577
10405
|
getSqliteDriverStatus,
|
|
10406
|
+
ensureRuntimeKernelTables,
|
|
10407
|
+
appendRuntimeEvent, listRuntimeEvents, latestRuntimeEventOfTypes,
|
|
10408
|
+
upsertRuntimeTurn, getRuntimeTurn, listRuntimeTurns,
|
|
10409
|
+
upsertRuntimeInputEnvelope, updateRuntimeInputEnvelope, getRuntimeInputEnvelope, listRuntimeInputEnvelopes,
|
|
10410
|
+
upsertRuntimeRouteSnapshot, getLatestRuntimeRouteSnapshot,
|
|
10411
|
+
upsertRuntimeApproval, resolveRuntimeApproval, listRuntimeApprovals,
|
|
9578
10412
|
getSetting, getSettingsByPrefix, setSetting,
|
|
9579
10413
|
addSessionDiagnostic, flushSessionDiagnostics, listSessionDiagnostics,
|
|
9580
10414
|
upsertRuntimeTask, updateRuntimeTask, getRuntimeTask, listRuntimeTasks, deleteRuntimeTasksForSession,
|
|
@@ -9587,8 +10421,8 @@ module.exports = {
|
|
|
9587
10421
|
listPermissionLog, addPermissionLog,
|
|
9588
10422
|
getAlwaysAskTools, setAlwaysAsk,
|
|
9589
10423
|
importSessionConversation, appendSessionConversation, appendSessionMessageRows, listSessionConversations, getSessionConversation, getSessionConversationMeta, getSessionConversationMessages, updateSessionModel,
|
|
9590
|
-
replaceSessionMessageRows, getSessionMessagesArray, sessionContentRowsAvailable, getSessionMessagesPage, getSessionMessagesTurnPage, countSessionMessageRows,
|
|
9591
|
-
runContentRowsBackfillSweep, getSessionRowsStatus, retireLegacyStores, runVacuum,
|
|
10424
|
+
replaceSessionMessageRows, appendSessionMessageRowsChunk, getSessionMessagesArray, sessionContentRowsAvailable, getSessionMessagesPage, getSessionMessagesTurnPage, countSessionMessageRows,
|
|
10425
|
+
runContentRowsBackfillSweep, getSessionRowsStatus, retireLegacyStores, runVacuum, _blobRetirementActive,
|
|
9592
10426
|
updateSessionTokens, getSessionTokens,
|
|
9593
10427
|
pickDisplayTitle,
|
|
9594
10428
|
getSessionTitle, setSessionTitle, isSessionUserRenamed, getAllSessionTitles,
|
|
@@ -9601,7 +10435,7 @@ module.exports = {
|
|
|
9601
10435
|
listAutoApprovals, upsertAutoApproval, toggleAutoApproval, deleteAutoApproval, getEnabledAutoApprovals,
|
|
9602
10436
|
listApprovalRules, upsertApprovalRule, findApprovalRuleBySignature, toggleApprovalRule, deleteApprovalRule, incrementApprovalRuleMatch,
|
|
9603
10437
|
addApprovalDecision, listApprovalDecisions, resolveApprovalDecision, getPendingEscalations,
|
|
9604
|
-
addApprovalObservation, listApprovalObservations, pruneApprovalObservations, pruneApprovalAiRefinementRules,
|
|
10438
|
+
addApprovalObservation, addApprovalObservationsBatch, listApprovalObservations, pruneApprovalObservations, pruneApprovalAiRefinementRules,
|
|
9605
10439
|
getApprovalRescuePattern, saveApprovalRescuePattern, listApprovalRescuePatterns,
|
|
9606
10440
|
getApprovalAiRefinementRule, saveApprovalAiRefinementRule, listApprovalAiRefinementRules,
|
|
9607
10441
|
findActiveApprovalAiRefinementRules, saveApprovalAiRefinementWarning, listApprovalAiRefinementWarnings,
|
|
@@ -9639,7 +10473,7 @@ module.exports = {
|
|
|
9639
10473
|
setSessionTitleNew, setSessionTitleStatusNew, getSessionTitleNew, getSessionTitlesByIds, getSessionDisplayTitleInfo, isUserSessionTitleCandidate, promoteStartupTaskTitleIfUserNamed, repairSessionTitlesFromStartupTasks, getAllSessionsData,
|
|
9640
10474
|
repairCodexSessionMetadataFromConversations, repairCodexRolloutAgentIdentities, detachPromotedProviderChildSessions,
|
|
9641
10475
|
listProviderChildAgentOwnerMappings,
|
|
9642
|
-
getAgentSessions, getAgentSession, deleteCtmSession,
|
|
10476
|
+
getAgentSessions, getAgentSession, getForkForDesktopUuid, listDesktopForks, deleteCtmSession,
|
|
9643
10477
|
// Schema version
|
|
9644
10478
|
getSchemaVersion,
|
|
9645
10479
|
};
|