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.
- package/package.json +1 -1
- package/scripts/bench-run.mjs +2 -2
- package/scripts/compact-pressure-test.mjs +104 -0
- package/scripts/desktop-session-bridge-test.mjs +704 -0
- package/scripts/freevar-smoke.mjs +7 -4
- package/scripts/lifecycle-api-test.mjs +65 -4
- package/scripts/max-output-recovery-test.mjs +31 -0
- package/scripts/memory-core-input-test.mjs +10 -0
- package/scripts/openai-oauth-ws-1006-retry-test.mjs +63 -3
- package/scripts/openai-ws-early-settle-test.mjs +40 -0
- package/scripts/parent-abort-link-test.mjs +24 -0
- package/scripts/process-lifecycle-test.mjs +80 -22
- package/scripts/provider-contract-test.mjs +257 -0
- package/scripts/provider-toolcall-test.mjs +172 -10
- package/scripts/session-orphan-sweep-test.mjs +27 -1
- package/src/lib/keychain-cjs.cjs +36 -23
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -13
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +7 -8
- package/src/runtime/agent/orchestrator/agent-trace.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +7 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -300
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +2 -4
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +18 -266
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +15 -130
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +5 -115
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +224 -0
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/lib/gemini-model-catalog.mjs +119 -0
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +109 -0
- package/src/runtime/agent/orchestrator/providers/lib/openai-tool-args.mjs +70 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +14 -71
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +47 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +1 -7
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +72 -77
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -20
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +30 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +37 -28
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +5 -7
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +170 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +20 -223
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +11 -17
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +21 -5
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +3 -58
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +24 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +57 -16
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +62 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +305 -127
- package/src/runtime/agent/orchestrator/tools/builtin/lib/list-helpers.mjs +46 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-grep-chunks.mjs +173 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/search-input-helpers.mjs +117 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-insights.mjs +199 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-spawn-helpers.mjs +107 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +1 -40
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +19 -277
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +22 -298
- package/src/runtime/agent/orchestrator/tools/lib/shell-spawn-retry.mjs +67 -0
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -71
- package/src/runtime/channels/backends/discord-gateway.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +6 -6
- package/src/runtime/channels/lib/config.mjs +15 -2
- package/src/runtime/channels/lib/memory-client.mjs +20 -3
- package/src/runtime/channels/lib/owned-runtime.mjs +19 -12
- package/src/runtime/channels/lib/status-snapshot.mjs +9 -0
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -5
- package/src/runtime/channels/lib/worker-main.mjs +16 -5
- package/src/runtime/memory/index.mjs +24 -198
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -53
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +115 -0
- package/src/runtime/memory/lib/memory-port-advertiser.mjs +105 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +0 -4
- package/src/runtime/memory/lib/query-handlers.mjs +2 -122
- package/src/runtime/memory/lib/query-maintenance-handlers.mjs +126 -0
- package/src/runtime/memory/lib/tool-call-handler.mjs +57 -0
- package/src/runtime/search/lib/http-fetch.mjs +1 -1
- package/src/runtime/shared/config.mjs +58 -13
- package/src/runtime/shared/llm/cost.mjs +14 -4
- package/src/runtime/shared/memory-snapshot.mjs +236 -0
- package/src/runtime/shared/process-lifecycle.mjs +92 -19
- package/src/runtime/shared/process-shutdown.mjs +6 -0
- package/src/runtime/shared/resource-admission.mjs +7 -2
- package/src/session-runtime/channel-config-api.mjs +7 -7
- package/src/session-runtime/config-lifecycle.mjs +20 -17
- package/src/session-runtime/cwd-plugins.mjs +9 -7
- package/src/session-runtime/env.mjs +1 -2
- package/src/session-runtime/hitch-profile.mjs +45 -0
- package/src/session-runtime/lifecycle-api.mjs +36 -9
- package/src/session-runtime/mcp-glue.mjs +6 -11
- package/src/session-runtime/provider-init-key.mjs +17 -0
- package/src/session-runtime/runtime-core.mjs +44 -103
- package/src/session-runtime/runtime-paths.mjs +20 -0
- package/src/session-runtime/runtime-tool-routing.mjs +55 -0
- package/src/session-runtime/tool-catalog-data.mjs +51 -0
- package/src/session-runtime/tool-catalog.mjs +15 -89
- package/src/standalone/agent-tool/spawn-preset.mjs +73 -0
- package/src/standalone/agent-tool/worker-rows.mjs +93 -0
- package/src/standalone/agent-tool.mjs +12 -162
- package/src/standalone/channel-admin.mjs +29 -0
- package/src/tui/App.jsx +11 -5
- package/src/tui/dist/index.mjs +202 -65
- package/src/tui/engine/session-api-ext.mjs +37 -4
- package/src/tui/index.jsx +1 -0
- package/src/tui/lib/voice-setup.mjs +5 -5
- package/scripts/_devtools-stub.mjs +0 -1
- 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('
|
|
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 {
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
354
|
-
|
|
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 =
|
|
55
|
-
|
|
56
|
-
|
|
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),
|