mixdog 0.9.52 → 0.9.53

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.
Files changed (111) hide show
  1. package/package.json +1 -1
  2. package/scripts/bench-run.mjs +2 -2
  3. package/scripts/compact-pressure-test.mjs +104 -0
  4. package/scripts/desktop-session-bridge-test.mjs +704 -0
  5. package/scripts/freevar-smoke.mjs +7 -4
  6. package/scripts/lifecycle-api-test.mjs +65 -4
  7. package/scripts/max-output-recovery-test.mjs +31 -0
  8. package/scripts/memory-core-input-test.mjs +10 -0
  9. package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
  10. package/scripts/openai-ws-early-settle-test.mjs +40 -0
  11. package/scripts/parent-abort-link-test.mjs +24 -0
  12. package/scripts/process-lifecycle-test.mjs +80 -22
  13. package/scripts/provider-contract-test.mjs +257 -0
  14. package/scripts/provider-toolcall-test.mjs +172 -10
  15. package/scripts/session-orphan-sweep-test.mjs +27 -1
  16. package/src/lib/keychain-cjs.cjs +36 -23
  17. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
  18. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
  19. package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
  20. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
  21. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
  22. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
  23. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
  24. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
  25. package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
  26. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
  27. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
  28. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
  29. package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
  30. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
  32. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
  33. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
  35. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
  37. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
  38. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
  39. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
  40. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
  41. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
  42. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
  43. package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
  44. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
  46. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
  47. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
  48. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
  50. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
  51. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
  52. package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
  53. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
  54. package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
  55. package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
  56. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
  57. package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
  59. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
  62. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
  63. package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
  64. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
  65. package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
  66. package/src/runtime/channels/backends/discord.mjs +6 -6
  67. package/src/runtime/channels/lib/config.mjs +15 -2
  68. package/src/runtime/channels/lib/memory-client.mjs +20 -3
  69. package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
  70. package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
  71. package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
  72. package/src/runtime/channels/lib/worker-main.mjs +16 -5
  73. package/src/runtime/memory/index.mjs +24 -198
  74. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
  75. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
  76. package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
  77. package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
  78. package/src/runtime/memory/lib/query-handlers.mjs +2 -122
  79. package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
  80. package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
  81. package/src/runtime/search/lib/http-fetch.mjs +1 -1
  82. package/src/runtime/shared/config.mjs +58 -13
  83. package/src/runtime/shared/llm/cost.mjs +14 -4
  84. package/src/runtime/shared/memory-snapshot.mjs +236 -0
  85. package/src/runtime/shared/process-lifecycle.mjs +92 -19
  86. package/src/runtime/shared/process-shutdown.mjs +6 -0
  87. package/src/runtime/shared/resource-admission.mjs +7 -2
  88. package/src/session-runtime/channel-config-api.mjs +7 -7
  89. package/src/session-runtime/config-lifecycle.mjs +20 -17
  90. package/src/session-runtime/cwd-plugins.mjs +9 -7
  91. package/src/session-runtime/env.mjs +1 -2
  92. package/src/session-runtime/hitch-profile.mjs +45 -0
  93. package/src/session-runtime/lifecycle-api.mjs +36 -9
  94. package/src/session-runtime/mcp-glue.mjs +6 -11
  95. package/src/session-runtime/provider-init-key.mjs +17 -0
  96. package/src/session-runtime/runtime-core.mjs +44 -103
  97. package/src/session-runtime/runtime-paths.mjs +20 -0
  98. package/src/session-runtime/runtime-tool-routing.mjs +55 -0
  99. package/src/session-runtime/tool-catalog-data.mjs +51 -0
  100. package/src/session-runtime/tool-catalog.mjs +15 -89
  101. package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
  102. package/src/standalone/agent-tool/worker-rows.mjs +93 -0
  103. package/src/standalone/agent-tool.mjs +12 -162
  104. package/src/standalone/channel-admin.mjs +29 -0
  105. package/src/tui/App.jsx +11 -5
  106. package/src/tui/dist/index.mjs +202 -65
  107. package/src/tui/engine/session-api-ext.mjs +37 -4
  108. package/src/tui/index.jsx +1 -0
  109. package/src/tui/lib/voice-setup.mjs +5 -5
  110. package/scripts/_devtools-stub.mjs +0 -1
  111. package/src/runtime/lib/keychain-cjs.cjs +0 -288
@@ -341,10 +341,6 @@ function patchActiveInstance(fields, opts = {}) {
341
341
 
342
342
  // ── postmaster.pid helpers ───────────────────────────────────────────────────
343
343
 
344
- function readPostmasterPid(pgdata) {
345
- return readPostmasterInfo(pgdata).pid;
346
- }
347
-
348
344
  function readPostmasterInfo(pgdata) {
349
345
  try {
350
346
  const raw = readFileSync(join(pgdata, 'postmaster.pid'), 'utf8');
@@ -24,8 +24,8 @@ import { searchRelevantHybrid } from './memory-recall-store.mjs'
24
24
  import { fetchEntriesByIdsScoped } from './memory-recall-id-patch.mjs'
25
25
  import { retrieveEntries } from './memory-retrievers.mjs'
26
26
  import { buildPromotedExclusionClauses } from './memory-recall-scope-filter.mjs'
27
- import { cleanMemoryText } from './memory.mjs'
28
27
  import { insertTraceEvents } from './trace-store.mjs'
28
+ import { createQueryMaintenanceHandlers } from './query-maintenance-handlers.mjs'
29
29
  import {
30
30
  embedText,
31
31
  embedTexts,
@@ -45,6 +45,7 @@ export function createQueryHandlers({
45
45
  // instance (one memory runtime = one factory) so the 10s de-dup window
46
46
  // behaves exactly as it did when it was a top-level `let` in index.mjs.
47
47
  let _embeddingColdRecallLogAt = 0
48
+ const { dumpSessionRootChunks, entryStats } = createQueryMaintenanceHandlers({ getDb })
48
49
 
49
50
  // Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
50
51
  // chunk_root IS NULL) are inserted immediately by ingestTranscriptFile before
@@ -866,127 +867,6 @@ export function createQueryHandlers({
866
867
  return { text: recallCapPrefix + renderSessionGroupedLines(sliced, { currentSessionId: _currentSessionHint, recencyOrder: sort === 'date' }) }
867
868
  }
868
869
 
869
- async function dumpSessionRootChunks(args = {}) {
870
- const db = getDb()
871
- const sessionId = String(args.sessionId || args.session_id || '').trim()
872
- if (!sessionId) return { text: '(no current session)', rows: [], chunks: [], isError: true }
873
- const includeRaw = args.includeRaw !== false
874
- const limit = Math.max(1, Math.min(1000, Number(args.limit) || 1000))
875
- const rootRows = (await db.query(`
876
- SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
877
- element, category, summary, status, score, last_seen_at, project_id
878
- FROM entries
879
- WHERE session_id = $1 AND is_root = 1
880
- ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
881
- LIMIT $2
882
- `, [sessionId, limit])).rows
883
- const roots = rootRows.map((r) => ({ ...r, members: [] }))
884
- const rootIds = roots.map((r) => Number(r.id)).filter((id) => Number.isFinite(id))
885
- const memberRows = rootIds.length > 0
886
- ? (await db.query(`
887
- SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
888
- FROM entries
889
- WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
890
- ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
891
- `, [rootIds])).rows
892
- : []
893
- const byRoot = new Map(roots.map((r) => [Number(r.id), r]))
894
- for (const m of memberRows) {
895
- const root = byRoot.get(Number(m.chunk_root))
896
- if (root) root.members.push(m)
897
- }
898
- let rawRows = []
899
- if (includeRaw) {
900
- rawRows = (await db.query(`
901
- SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
902
- FROM entries
903
- WHERE session_id = $1
904
- AND is_root = 0
905
- AND (chunk_root IS NULL OR chunk_root = id)
906
- ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
907
- LIMIT $2
908
- `, [sessionId, limit])).rows
909
- }
910
- const chunks = []
911
- for (const root of roots) {
912
- const memberText = root.members
913
- .map((m) => `${m.role === 'assistant' ? 'assistant' : m.role === 'user' ? 'user' : m.role}: ${cleanMemoryText(String(m.content ?? ''))}`)
914
- .filter(Boolean)
915
- .join('\n')
916
- const summary = [root.element, root.summary].map((v) => String(v || '').trim()).filter(Boolean).join(' — ')
917
- chunks.push({
918
- id: Number(root.id),
919
- kind: 'root',
920
- ts: Number(root.ts) || 0,
921
- sourceTurn: root.source_turn ?? null,
922
- category: root.category || null,
923
- summary,
924
- text: memberText || cleanMemoryText(String(root.content ?? '')),
925
- members: root.members,
926
- })
927
- }
928
- for (const raw of rawRows) {
929
- chunks.push({
930
- id: Number(raw.id),
931
- kind: 'raw',
932
- chunkRoot: raw.chunk_root ?? null,
933
- ts: Number(raw.ts) || 0,
934
- sourceTurn: raw.source_turn ?? null,
935
- category: null,
936
- summary: '',
937
- text: `${raw.role === 'assistant' ? 'assistant' : raw.role === 'user' ? 'user' : raw.role}: ${cleanMemoryText(String(raw.content ?? ''))}`,
938
- members: [],
939
- })
940
- }
941
- chunks.sort((a, b) => {
942
- const at = Number.isFinite(Number(a.sourceTurn)) ? Number(a.sourceTurn) : 2147483647
943
- const bt = Number.isFinite(Number(b.sourceTurn)) ? Number(b.sourceTurn) : 2147483647
944
- return (at - bt) || ((a.ts || 0) - (b.ts || 0)) || ((a.id || 0) - (b.id || 0))
945
- })
946
- const text = chunks.length
947
- ? chunks.map((chunk, idx) => {
948
- const label = chunk.kind === 'root'
949
- ? `# chunk ${idx + 1} root=${chunk.id}${chunk.category ? ` category=${chunk.category}` : ''}`
950
- : `${chunk.chunkRoot == null ? '# raw_pending' : '# raw_terminal'} ${idx + 1} id=${chunk.id}`
951
- const summary = chunk.summary ? `summary: ${chunk.summary}\n` : ''
952
- return `${label}\n${summary}${chunk.text}`.trim()
953
- }).join('\n\n')
954
- : '(no results)'
955
- return { text, rows: [...roots, ...rawRows], chunks }
956
- }
957
-
958
- async function entryStats() {
959
- const db = getDb()
960
- return await db.transaction(async (tx) => {
961
- const total = (await tx.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
962
- const roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1`)).rows[0].c
963
- const active_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active'`)).rows[0].c
964
- const archived_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'archived'`)).rows[0].c
965
- const unchunked_leaves = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL`)).rows[0].c
966
- const cycle2_pending_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'pending'`)).rows[0].c
967
- const core_entries = (await tx.query(`SELECT COUNT(*) c FROM core_entries`)).rows[0].c
968
- const core_embed_null = (await tx.query(`SELECT COUNT(*) c FROM core_entries WHERE embedding IS NULL`)).rows[0].c
969
- const active_core_summaries = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' AND core_summary IS NOT NULL`)).rows[0].c
970
- const active_core_summary_missing = (await tx.query(`
971
- SELECT COUNT(*) c
972
- FROM entries
973
- WHERE is_root = 1
974
- AND status = 'active'
975
- AND (core_summary IS NULL OR btrim(core_summary) = '')
976
- `)).rows[0].c
977
- const byStatus = (await tx.query(`SELECT status, COUNT(*) c FROM entries WHERE is_root = 1 GROUP BY status`)).rows
978
- const byCategory = (await tx.query(`SELECT category, COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' GROUP BY category ORDER BY c DESC`)).rows
979
- const mvRows = (await tx.query(`SELECT relispopulated FROM pg_class WHERE relname = 'mv_hot_active' LIMIT 1`)).rows
980
- const mv_hot_active_populated = mvRows.length ? Boolean(mvRows[0].relispopulated) : null
981
- return {
982
- total, roots, active_roots, archived_roots, unchunked_leaves, cycle2_pending_roots,
983
- core_entries, core_embed_null, active_core_summaries, active_core_summary_missing,
984
- mv_hot_active_populated,
985
- byStatus, byCategory,
986
- }
987
- })
988
- }
989
-
990
870
  return {
991
871
  readRawRowsInWindow,
992
872
  recallSessionRows,
@@ -0,0 +1,126 @@
1
+ import { cleanMemoryText } from './memory.mjs'
2
+
3
+ export function createQueryMaintenanceHandlers({ getDb }) {
4
+ async function dumpSessionRootChunks(args = {}) {
5
+ const db = getDb()
6
+ const sessionId = String(args.sessionId || args.session_id || '').trim()
7
+ if (!sessionId) return { text: '(no current session)', rows: [], chunks: [], isError: true }
8
+ const includeRaw = args.includeRaw !== false
9
+ const limit = Math.max(1, Math.min(1000, Number(args.limit) || 1000))
10
+ const rootRows = (await db.query(`
11
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
12
+ element, category, summary, status, score, last_seen_at, project_id
13
+ FROM entries
14
+ WHERE session_id = $1 AND is_root = 1
15
+ ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
16
+ LIMIT $2
17
+ `, [sessionId, limit])).rows
18
+ const roots = rootRows.map((r) => ({ ...r, members: [] }))
19
+ const rootIds = roots.map((r) => Number(r.id)).filter((id) => Number.isFinite(id))
20
+ const memberRows = rootIds.length > 0
21
+ ? (await db.query(`
22
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
23
+ FROM entries
24
+ WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
25
+ ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
26
+ `, [rootIds])).rows
27
+ : []
28
+ const byRoot = new Map(roots.map((r) => [Number(r.id), r]))
29
+ for (const m of memberRows) {
30
+ const root = byRoot.get(Number(m.chunk_root))
31
+ if (root) root.members.push(m)
32
+ }
33
+ let rawRows = []
34
+ if (includeRaw) {
35
+ rawRows = (await db.query(`
36
+ SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
37
+ FROM entries
38
+ WHERE session_id = $1
39
+ AND is_root = 0
40
+ AND (chunk_root IS NULL OR chunk_root = id)
41
+ ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
42
+ LIMIT $2
43
+ `, [sessionId, limit])).rows
44
+ }
45
+ const chunks = []
46
+ for (const root of roots) {
47
+ const memberText = root.members
48
+ .map((m) => `${m.role === 'assistant' ? 'assistant' : m.role === 'user' ? 'user' : m.role}: ${cleanMemoryText(String(m.content ?? ''))}`)
49
+ .filter(Boolean)
50
+ .join('\n')
51
+ const summary = [root.element, root.summary].map((v) => String(v || '').trim()).filter(Boolean).join(' — ')
52
+ chunks.push({
53
+ id: Number(root.id),
54
+ kind: 'root',
55
+ ts: Number(root.ts) || 0,
56
+ sourceTurn: root.source_turn ?? null,
57
+ category: root.category || null,
58
+ summary,
59
+ text: memberText || cleanMemoryText(String(root.content ?? '')),
60
+ members: root.members,
61
+ })
62
+ }
63
+ for (const raw of rawRows) {
64
+ chunks.push({
65
+ id: Number(raw.id),
66
+ kind: 'raw',
67
+ chunkRoot: raw.chunk_root ?? null,
68
+ ts: Number(raw.ts) || 0,
69
+ sourceTurn: raw.source_turn ?? null,
70
+ category: null,
71
+ summary: '',
72
+ text: `${raw.role === 'assistant' ? 'assistant' : raw.role === 'user' ? 'user' : raw.role}: ${cleanMemoryText(String(raw.content ?? ''))}`,
73
+ members: [],
74
+ })
75
+ }
76
+ chunks.sort((a, b) => {
77
+ const at = Number.isFinite(Number(a.sourceTurn)) ? Number(a.sourceTurn) : 2147483647
78
+ const bt = Number.isFinite(Number(b.sourceTurn)) ? Number(b.sourceTurn) : 2147483647
79
+ return (at - bt) || ((a.ts || 0) - (b.ts || 0)) || ((a.id || 0) - (b.id || 0))
80
+ })
81
+ const text = chunks.length
82
+ ? chunks.map((chunk, idx) => {
83
+ const label = chunk.kind === 'root'
84
+ ? `# chunk ${idx + 1} root=${chunk.id}${chunk.category ? ` category=${chunk.category}` : ''}`
85
+ : `${chunk.chunkRoot == null ? '# raw_pending' : '# raw_terminal'} ${idx + 1} id=${chunk.id}`
86
+ const summary = chunk.summary ? `summary: ${chunk.summary}\n` : ''
87
+ return `${label}\n${summary}${chunk.text}`.trim()
88
+ }).join('\n\n')
89
+ : '(no results)'
90
+ return { text, rows: [...roots, ...rawRows], chunks }
91
+ }
92
+
93
+ async function entryStats() {
94
+ const db = getDb()
95
+ return await db.transaction(async (tx) => {
96
+ const total = (await tx.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
97
+ const roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1`)).rows[0].c
98
+ const active_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active'`)).rows[0].c
99
+ const archived_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'archived'`)).rows[0].c
100
+ const unchunked_leaves = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL`)).rows[0].c
101
+ const cycle2_pending_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'pending'`)).rows[0].c
102
+ const core_entries = (await tx.query(`SELECT COUNT(*) c FROM core_entries`)).rows[0].c
103
+ const core_embed_null = (await tx.query(`SELECT COUNT(*) c FROM core_entries WHERE embedding IS NULL`)).rows[0].c
104
+ const active_core_summaries = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' AND core_summary IS NOT NULL`)).rows[0].c
105
+ const active_core_summary_missing = (await tx.query(`
106
+ SELECT COUNT(*) c
107
+ FROM entries
108
+ WHERE is_root = 1
109
+ AND status = 'active'
110
+ AND (core_summary IS NULL OR btrim(core_summary) = '')
111
+ `)).rows[0].c
112
+ const byStatus = (await tx.query(`SELECT status, COUNT(*) c FROM entries WHERE is_root = 1 GROUP BY status`)).rows
113
+ const byCategory = (await tx.query(`SELECT category, COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' GROUP BY category ORDER BY c DESC`)).rows
114
+ const mvRows = (await tx.query(`SELECT relispopulated FROM pg_class WHERE relname = 'mv_hot_active' LIMIT 1`)).rows
115
+ const mv_hot_active_populated = mvRows.length ? Boolean(mvRows[0].relispopulated) : null
116
+ return {
117
+ total, roots, active_roots, archived_roots, unchunked_leaves, cycle2_pending_roots,
118
+ core_entries, core_embed_null, active_core_summaries, active_core_summary_missing,
119
+ mv_hot_active_populated,
120
+ byStatus, byCategory,
121
+ }
122
+ })
123
+ }
124
+
125
+ return { dumpSessionRootChunks, entryStats }
126
+ }
@@ -0,0 +1,57 @@
1
+ export function createToolCallHandler({ handleSearch, handleMemoryAction }) {
2
+ async function handleToolCall(name, args, signal) {
3
+ try {
4
+ if (name === 'search_memories') {
5
+ const result = await handleSearch(args || {}, signal)
6
+ return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
7
+ }
8
+ if (name === 'recall') {
9
+ // recall is aiWrapped in the unified build; in standalone mode map it to
10
+ // search_memories so the advertised tool name actually works. Forward
11
+ // every advertised arg so id/limit/offset/sort/includeArchived/
12
+ // includeMembers/includeRaw reach handleSearch instead of being dropped.
13
+ const a = args || {}
14
+ const hasQuery = Array.isArray(a.query)
15
+ ? a.query.some((value) => String(value || '').trim())
16
+ : String(a.query ?? '').trim() !== ''
17
+ const recallIds = hasQuery
18
+ ? []
19
+ : (Array.isArray(a.id) ? a.id : [a.id])
20
+ .map((value) => Number(value))
21
+ .filter((value) => Number.isInteger(value) && value > 0)
22
+ const searchArgs = {
23
+ ...(a.query !== undefined ? { query: a.query } : {}),
24
+ ...(recallIds.length > 0 ? { ids: recallIds } : {}),
25
+ ...(a.period ? { period: a.period } : {}),
26
+ ...(a.limit !== undefined ? { limit: a.limit } : {}),
27
+ ...(a.offset !== undefined ? { offset: a.offset } : {}),
28
+ ...(a.sort !== undefined ? { sort: a.sort } : {}),
29
+ ...(a.category !== undefined ? { category: a.category } : {}),
30
+ ...(a.includeArchived !== undefined ? { includeArchived: a.includeArchived } : {}),
31
+ ...(a.includeMembers !== undefined ? { includeMembers: a.includeMembers } : {}),
32
+ ...(a.includeRaw !== undefined ? { includeRaw: a.includeRaw } : {}),
33
+ ...(a.cwd ? { cwd: a.cwd } : {}),
34
+ ...(a.projectScope ? { projectScope: a.projectScope } : {}),
35
+ ...(a.sessionId ? { sessionId: a.sessionId } : {}),
36
+ ...(a.session_id ? { session_id: a.session_id } : {}),
37
+ ...(a.currentSession !== undefined ? { currentSession: a.currentSession } : {}),
38
+ // Hint only — never a filter. Marks the caller's own session as
39
+ // "(current)" in the multi-session grouped browse output.
40
+ ...(a.currentSessionId ? { currentSessionId: a.currentSessionId } : {}),
41
+ }
42
+ const result = await handleSearch(searchArgs, signal)
43
+ return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
44
+ }
45
+ if (name === 'memory') {
46
+ const result = await handleMemoryAction(args || {}, signal)
47
+ return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
48
+ }
49
+ return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true }
50
+ } catch (err) {
51
+ const msg = err instanceof Error ? err.message : String(err)
52
+ return { content: [{ type: 'text', text: `${name} failed: ${msg}` }], isError: true }
53
+ }
54
+ }
55
+
56
+ return handleToolCall
57
+ }
@@ -7,7 +7,7 @@ import {
7
7
  pinnedFetch,
8
8
  } from './ssrf-guard.mjs'
9
9
 
10
- const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
10
+ const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../../../../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
11
11
 
12
12
  export function withTimeout(controller, timeoutMs) {
13
13
  return setTimeout(() => controller.abort(), timeoutMs)
@@ -27,7 +27,13 @@ export {
27
27
  }
28
28
 
29
29
  const _require = createRequire(import.meta.url)
30
- const { getSecret: _getSecret, setSecret: _setSecret, deleteSecret: _deleteSecret, hasSecret: _hasSecret } = _require('../../lib/keychain-cjs.cjs')
30
+ const {
31
+ getSecret: _getSecret,
32
+ setSecret: _setSecret,
33
+ deleteSecret: _deleteSecret,
34
+ hasSecret: _hasSecret,
35
+ invalidateSecretCache: _invalidateSecretCache,
36
+ } = _require('../../lib/keychain-cjs.cjs')
31
37
 
32
38
  const DATA_DIR = resolvePluginData()
33
39
 
@@ -338,25 +344,60 @@ function withConfigLockAsync(fn) {
338
344
  return withFileLock(`${CONFIG_PATH}.lock`, fn, { secret: true })
339
345
  }
340
346
 
341
- export async function updateConfigAsync(updater) {
342
- let saved = null
343
- await withConfigLockAsync(async () => {
344
- const current = stripGeneratedMarker(readAllForRmW()) || {}
345
- const next = typeof updater === 'function' ? updater({ ...current }) : updater
346
- if (!isPlainObject(next)) throw new Error('[config] updateConfigAsync updater must return an object')
347
- saved = stripGeneratedMarker(next) || {}
348
- await writeAllAsync(saved)
347
+ // Process-wide registry at the lock-owning layer: every public async RMW path
348
+ // registers before lock acquisition and unregisters on both fulfillment and
349
+ // rejection. Lifecycle teardown can therefore drain direct callers too, not
350
+ // just its own debounce queues.
351
+ const _pendingConfigWrites = new Set()
352
+
353
+ function trackConfigWrite(work) {
354
+ let promise
355
+ try {
356
+ promise = Promise.resolve(work())
357
+ } catch (error) {
358
+ promise = Promise.reject(error)
359
+ }
360
+ _pendingConfigWrites.add(promise)
361
+ const remove = () => { _pendingConfigWrites.delete(promise) }
362
+ promise.then(remove, remove)
363
+ return promise
364
+ }
365
+
366
+ export async function pendingConfigWrites() {
367
+ while (_pendingConfigWrites.size > 0) {
368
+ await Promise.allSettled([..._pendingConfigWrites])
369
+ }
370
+ }
371
+
372
+ export function updateConfigAsync(updater) {
373
+ return trackConfigWrite(async () => {
374
+ let saved = null
375
+ await withConfigLockAsync(async () => {
376
+ const current = stripGeneratedMarker(readAllForRmW()) || {}
377
+ const next = typeof updater === 'function' ? updater({ ...current }) : updater
378
+ if (!isPlainObject(next)) throw new Error('[config] updateConfigAsync updater must return an object')
379
+ saved = stripGeneratedMarker(next) || {}
380
+ await writeAllAsync(saved)
381
+ })
382
+ return saved
349
383
  })
350
- return saved
351
384
  }
352
385
 
353
- export async function updateSectionAsync(section, updater) {
354
- await withConfigLockAsync(async () => {
386
+ export function writeSectionAsync(section, data) {
387
+ return trackConfigWrite(() => withConfigLockAsync(async () => {
388
+ const all = readAllForRmW()
389
+ all[section] = stripGeneratedMarker(data)
390
+ await writeAllAsync(all)
391
+ }))
392
+ }
393
+
394
+ export function updateSectionAsync(section, updater) {
395
+ return trackConfigWrite(() => withConfigLockAsync(async () => {
355
396
  const all = readAllForRmW()
356
397
  const current = stripGeneratedMarker(all[section] || {})
357
398
  all[section] = stripGeneratedMarker(typeof updater === 'function' ? updater(current) : updater)
358
399
  await writeAllAsync(all)
359
- })
400
+ }))
360
401
  }
361
402
 
362
403
  // ── Capabilities (B2 central path policy) ───────────────────────────
@@ -510,6 +551,10 @@ export function deleteSecret(account) {
510
551
  _deleteSecret(account)
511
552
  }
512
553
 
554
+ export function invalidateSecretReadCache(account) {
555
+ _invalidateSecretCache(account)
556
+ }
557
+
513
558
  /**
514
559
  * Whether a secret is stored in the OS keychain for `account` (ignores env).
515
560
  * Lets the setup UI show "Set" WITHOUT ever sending the secret value to the
@@ -31,7 +31,14 @@ export function isInclusiveProvider(provider) {
31
31
  // usage rows — without it, cached tokens would be double-billed in the
32
32
  // cost fallback and prompt totals.
33
33
  return p.includes('openai') || p.includes('codex') || p.includes('gemini') || p.includes('google') || p.includes('xai') || p.includes('grok')
34
- || p.includes('deepseek') || p.includes('ollama') || p.includes('lmstudio') || p.includes('groq') || p.includes('openrouter');
34
+ || p.includes('deepseek') || p.includes('ollama') || p.includes('lmstudio') || p.includes('groq') || p.includes('openrouter')
35
+ || p.includes('opencode-go');
36
+ }
37
+
38
+ export function billableInputTokensForProvider(provider, inputTokens, cacheReadTokens = 0, cacheWriteTokens = 0) {
39
+ const input = Number(inputTokens) || 0;
40
+ if (!isInclusiveProvider(provider)) return input;
41
+ return Math.max(input - (Number(cacheReadTokens) || 0) - (Number(cacheWriteTokens) || 0), 0);
35
42
  }
36
43
 
37
44
  /**
@@ -51,9 +58,12 @@ export function computeCostUsd(args) {
51
58
  const outputTokens = args.outputTokens || 0;
52
59
  const cacheReadTokens = args.cacheReadTokens || 0;
53
60
  const cacheWriteTokens = args.cacheWriteTokens || 0;
54
- const billableInput = isInclusiveProvider(args.provider)
55
- ? Math.max(inputTokens - cacheReadTokens - cacheWriteTokens, 0)
56
- : inputTokens;
61
+ const billableInput = billableInputTokensForProvider(
62
+ args.provider,
63
+ inputTokens,
64
+ cacheReadTokens,
65
+ cacheWriteTokens,
66
+ );
57
67
  const parts = [
58
68
  billableInput * (meta.inputCostPerM || 0),
59
69
  outputTokens * (meta.outputCostPerM || 0),