mixdog 0.9.0 → 0.9.2
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 +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -105,31 +105,50 @@ const _checkedConnect = checkedConnect
|
|
|
105
105
|
// native PG db shim
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
107
107
|
|
|
108
|
-
function makeCompatDb(pgPool, schema) {
|
|
108
|
+
function makeCompatDb(pgPool, schema, dataDir) {
|
|
109
109
|
const db = {
|
|
110
110
|
// query: use pool directly for single-statement queries
|
|
111
111
|
query: async (sql, params) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
return await withPgRetry(dataDir, schema, async (pool) => {
|
|
113
|
+
const client = await _checkedConnect(pool, schema)
|
|
114
|
+
try {
|
|
115
|
+
return await client.query(sql, params)
|
|
116
|
+
} finally {
|
|
117
|
+
client.release()
|
|
118
|
+
}
|
|
119
|
+
})
|
|
118
120
|
},
|
|
119
121
|
|
|
120
122
|
// exec: multi-statement SQL (semicolon-separated); single client for session state
|
|
121
123
|
exec: async (sql) => {
|
|
122
|
-
|
|
124
|
+
// Not retried: exec runs arbitrary multi-statement SQL where a partial
|
|
125
|
+
// failure mid-sequence is unobservable from here — replaying the whole
|
|
126
|
+
// string after recovery risks double-applying already-committed
|
|
127
|
+
// statements. On ECONNREFUSED, trigger recovery in the background (so
|
|
128
|
+
// the NEXT call gets a fresh pool) and propagate the original error.
|
|
129
|
+
const pool = instances.get(`${resolve(dataDir)}|${schema}`)?.pool ?? pgPool
|
|
123
130
|
try {
|
|
124
|
-
await
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
const client = await _checkedConnect(pool, schema)
|
|
132
|
+
try {
|
|
133
|
+
await client.query(sql)
|
|
134
|
+
} finally {
|
|
135
|
+
client.release()
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err?.code === 'ECONNREFUSED') _recoverPgConnection(dataDir, schema).catch(() => {})
|
|
139
|
+
throw err
|
|
127
140
|
}
|
|
128
141
|
},
|
|
129
142
|
|
|
130
143
|
// transaction: check out one client, BEGIN, run callback(tx), COMMIT or ROLLBACK
|
|
131
144
|
transaction: async (fn) => {
|
|
132
|
-
|
|
145
|
+
// Not retried — same rationale as exec(): a COMMIT that fails with
|
|
146
|
+
// ECONNREFUSED leaves the transaction's applied/rolled-back state
|
|
147
|
+
// unknown to this process; blindly replaying fn() could double-apply
|
|
148
|
+
// side effects. Recover in the background for the next caller and
|
|
149
|
+
// propagate the original error immediately.
|
|
150
|
+
const pool = instances.get(`${resolve(dataDir)}|${schema}`)?.pool ?? pgPool
|
|
151
|
+
const client = await _checkedConnect(pool, schema)
|
|
133
152
|
try {
|
|
134
153
|
await client.query('BEGIN')
|
|
135
154
|
const tx = {
|
|
@@ -141,6 +160,7 @@ function makeCompatDb(pgPool, schema) {
|
|
|
141
160
|
return result
|
|
142
161
|
} catch (err) {
|
|
143
162
|
try { await client.query('ROLLBACK') } catch {}
|
|
163
|
+
if (err?.code === 'ECONNREFUSED') _recoverPgConnection(dataDir, schema).catch(() => {})
|
|
144
164
|
throw err
|
|
145
165
|
} finally {
|
|
146
166
|
client.release()
|
|
@@ -150,12 +170,116 @@ function makeCompatDb(pgPool, schema) {
|
|
|
150
170
|
// close: drain pool
|
|
151
171
|
close: () => pgPool.end(),
|
|
152
172
|
|
|
153
|
-
// Internal access for callers that need raw pool
|
|
154
|
-
|
|
173
|
+
// Internal access for callers that need raw pool. Getter (not a fixed
|
|
174
|
+
// field) so callers that hold onto `db` across a recovery cycle (e.g.
|
|
175
|
+
// trace-store's checkedConnect/pool.connect() call sites) transparently
|
|
176
|
+
// see the refreshed pool after withPgRetry swaps the cached instance —
|
|
177
|
+
// otherwise db._pool would keep pointing at the dead pre-recovery pool.
|
|
178
|
+
get _pool() {
|
|
179
|
+
return instances.get(`${resolve(dataDir)}|${schema}`)?.pool ?? pgPool
|
|
180
|
+
},
|
|
155
181
|
}
|
|
156
182
|
return db
|
|
157
183
|
}
|
|
158
184
|
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Self-healing: pool query fails with ECONNREFUSED (target pg_port down while
|
|
187
|
+
// the server process itself stays alive) → discard the stale pool via
|
|
188
|
+
// closePgInstance and re-run ensurePgInstance so supervisor-pg restarts/
|
|
189
|
+
// re-attaches PG, then retry the failed operation exactly once against the
|
|
190
|
+
// fresh pool.
|
|
191
|
+
//
|
|
192
|
+
// Guards against runaway restart loops:
|
|
193
|
+
// - _recoverInFlight: at most one recovery coroutine per dataDir at a time;
|
|
194
|
+
// concurrent callers await the same promise instead of racing restarts.
|
|
195
|
+
// - _lastRecoverAt / RECOVER_COOLDOWN_MS: after a recovery attempt (success
|
|
196
|
+
// or failure) further ECONNREFUSED hits within the cooldown window
|
|
197
|
+
// surface the original error instead of re-triggering PG restart.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
const RECOVER_COOLDOWN_MS = 15_000
|
|
201
|
+
const _lastRecoverAt = new Map() // dataDirKey → epoch ms of last recovery attempt
|
|
202
|
+
const _recoverInFlight = new Map() // dataDirKey → Promise<boolean>
|
|
203
|
+
|
|
204
|
+
function _instanceKeysForDataDir(dataDirKey) {
|
|
205
|
+
const prefix = `${dataDirKey}|`
|
|
206
|
+
return Array.from(instances.keys()).filter(k => k.startsWith(prefix))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function _recoverPgConnection(dataDir, schema) {
|
|
210
|
+
const dataDirKey = resolve(dataDir)
|
|
211
|
+
// In-flight check FIRST: a concurrent recovery already running for this
|
|
212
|
+
// dataDir must be awaited by every caller (even ones arriving inside the
|
|
213
|
+
// cooldown window that starts once that recovery begins) — otherwise a
|
|
214
|
+
// caller that lands between the in-flight recovery's start and its cooldown
|
|
215
|
+
// stamp would see neither in-flight nor cooldown and could trigger a second
|
|
216
|
+
// redundant restart cycle.
|
|
217
|
+
if (_recoverInFlight.has(dataDirKey)) return _recoverInFlight.get(dataDirKey)
|
|
218
|
+
const now = Date.now()
|
|
219
|
+
const last = _lastRecoverAt.get(dataDirKey) || 0
|
|
220
|
+
if (now - last < RECOVER_COOLDOWN_MS) {
|
|
221
|
+
// Cooldown active — do not hammer PG restart on every failing query.
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const p = (async () => {
|
|
226
|
+
_lastRecoverAt.set(dataDirKey, Date.now())
|
|
227
|
+
__mixdogMemoryLog(`[pg-adapter] ECONNREFUSED on pool query — recovering PG for dataDir=${dataDirKey}\n`)
|
|
228
|
+
// memory + trace schemas share one PG cluster/port; discard every cached
|
|
229
|
+
// pool for this dataDir so a dead-port pool is never reused after restart.
|
|
230
|
+
// Track every schema whose pool was actually closed here so ALL of them
|
|
231
|
+
// get re-ensured below (not just the schema of the caller that happened
|
|
232
|
+
// to trigger this recovery) — otherwise a sibling schema's cached `db`
|
|
233
|
+
// handle is left pointing at an ended pool (its `_pool` getter falls back
|
|
234
|
+
// to the stale `pgPool` closure var) and its next query throws a
|
|
235
|
+
// "Cannot use a pool after calling end" TypeError instead of recovering.
|
|
236
|
+
const closedSchemas = new Set()
|
|
237
|
+
for (const key of _instanceKeysForDataDir(dataDirKey)) {
|
|
238
|
+
const sch = key.slice(dataDirKey.length + 1)
|
|
239
|
+
try { await closePgInstance(dataDir, { schema: sch }) } catch {}
|
|
240
|
+
closedSchemas.add(sch)
|
|
241
|
+
}
|
|
242
|
+
closedSchemas.add(schema) // the triggering schema, even if it had no cached instance yet
|
|
243
|
+
try {
|
|
244
|
+
for (const sch of closedSchemas) {
|
|
245
|
+
await ensurePgInstance(dataDir, { schema: sch })
|
|
246
|
+
}
|
|
247
|
+
__mixdogMemoryLog(`[pg-adapter] PG reconnect recovery complete for dataDir=${dataDirKey}\n`)
|
|
248
|
+
return true
|
|
249
|
+
} catch (e) {
|
|
250
|
+
__mixdogMemoryLog(`[pg-adapter] PG reconnect recovery failed for dataDir=${dataDirKey}: ${e?.message || e}\n`)
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
})()
|
|
254
|
+
_recoverInFlight.set(dataDirKey, p)
|
|
255
|
+
try {
|
|
256
|
+
return await p
|
|
257
|
+
} finally {
|
|
258
|
+
_recoverInFlight.delete(dataDirKey)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Run `fn(pool)` against the current pool for (dataDir, schema); on
|
|
264
|
+
* ECONNREFUSED, recover PG (see _recoverPgConnection) and retry once against
|
|
265
|
+
* the refreshed pool. Non-ECONNREFUSED errors and cooldown/in-flight misses
|
|
266
|
+
* propagate the original error unchanged.
|
|
267
|
+
*/
|
|
268
|
+
async function withPgRetry(dataDir, schema, fn) {
|
|
269
|
+
const key = `${resolve(dataDir)}|${schema}`
|
|
270
|
+
const pool0 = instances.get(key)?.pool
|
|
271
|
+
try {
|
|
272
|
+
return await fn(pool0)
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (err?.code !== 'ECONNREFUSED') throw err
|
|
275
|
+
const recovered = await _recoverPgConnection(dataDir, schema)
|
|
276
|
+
if (!recovered) throw err
|
|
277
|
+
const pool1 = instances.get(key)?.pool
|
|
278
|
+
if (!pool1) throw err
|
|
279
|
+
return await fn(pool1)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
159
283
|
// ---------------------------------------------------------------------------
|
|
160
284
|
// Instance bootstrap — extensions + schemas (idempotent)
|
|
161
285
|
// ---------------------------------------------------------------------------
|
|
@@ -299,7 +423,7 @@ export async function ensurePgInstance(dataDir, opts = {}) {
|
|
|
299
423
|
await bootstrapInstance(pgPool, resolve(dataDir))
|
|
300
424
|
|
|
301
425
|
// 5. Build the compat db shim.
|
|
302
|
-
const db = makeCompatDb(pgPool, schema)
|
|
426
|
+
const db = makeCompatDb(pgPool, schema, dataDir)
|
|
303
427
|
|
|
304
428
|
const result = { db, pool: pgPool, host, port, runtimeDir, pgdataDir }
|
|
305
429
|
instances.set(key, result)
|
|
@@ -48,6 +48,15 @@ function platformKey() {
|
|
|
48
48
|
return `${os}-${process.arch}`
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function platformKeyCandidates() {
|
|
52
|
+
const primary = platformKey()
|
|
53
|
+
const candidates = [primary]
|
|
54
|
+
if (process.platform === 'win32' && process.arch === 'arm64') {
|
|
55
|
+
candidates.push('win32-x64')
|
|
56
|
+
}
|
|
57
|
+
return candidates
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
// Fail-closed asset validation. A selected manifest asset is usable only if it
|
|
52
61
|
// is not explicitly marked unsupported AND carries a real downloadable payload:
|
|
53
62
|
// non-empty url, a well-formed 64-hex sha256, and a positive integer size.
|
|
@@ -365,25 +374,35 @@ export async function ensureRuntime(dataDir) {
|
|
|
365
374
|
gcRuntimeDir(runtimeBaseDir, readActiveVersion(runtimeBaseDir))
|
|
366
375
|
|
|
367
376
|
const manifest = await loadManifest(key)
|
|
368
|
-
const pkey
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
.join(', ') || '(none)'
|
|
379
|
-
throw new Error(
|
|
380
|
-
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
381
|
-
`no runtime asset for this platform/arch in the manifest. ` +
|
|
382
|
-
`Supported: ${supported}. ` +
|
|
383
|
-
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
384
|
-
)
|
|
377
|
+
const pkey = platformKey()
|
|
378
|
+
let selectedKey = null
|
|
379
|
+
let asset = null
|
|
380
|
+
for (const candidateKey of platformKeyCandidates()) {
|
|
381
|
+
const candidateAsset = manifest.assets?.[candidateKey]
|
|
382
|
+
if (isUsableAsset(candidateAsset)) {
|
|
383
|
+
selectedKey = candidateKey
|
|
384
|
+
asset = candidateAsset
|
|
385
|
+
break
|
|
386
|
+
}
|
|
385
387
|
}
|
|
386
|
-
if (!
|
|
388
|
+
if (!asset) {
|
|
389
|
+
const primaryAsset = manifest.assets?.[pkey]
|
|
390
|
+
if (!primaryAsset) {
|
|
391
|
+
// Platform/arch absent from the manifest entirely (e.g. an exotic arch).
|
|
392
|
+
// The memory PG runtime cannot start here; fail with a single clear,
|
|
393
|
+
// actionable message. The memory worker's init().catch reports this as
|
|
394
|
+
// degraded and the rest of mixdog (agent, tools) keeps working without
|
|
395
|
+
// memory.
|
|
396
|
+
const supported = Object.keys(manifest.assets || {})
|
|
397
|
+
.filter((k) => isUsableAsset(manifest.assets[k]))
|
|
398
|
+
.join(', ') || '(none)'
|
|
399
|
+
throw new Error(
|
|
400
|
+
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
401
|
+
`no runtime asset for this platform/arch in the manifest. ` +
|
|
402
|
+
`Supported: ${supported}. ` +
|
|
403
|
+
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
404
|
+
)
|
|
405
|
+
}
|
|
387
406
|
// Platform/arch present but explicitly marked unsupported or carrying a
|
|
388
407
|
// placeholder/TBD payload (e.g. linux-arm64). Same graceful-degrade path.
|
|
389
408
|
throw new Error(
|
|
@@ -393,6 +412,12 @@ export async function ensureRuntime(dataDir) {
|
|
|
393
412
|
)
|
|
394
413
|
}
|
|
395
414
|
|
|
415
|
+
if (selectedKey !== pkey) {
|
|
416
|
+
__mixdogMemoryLog(
|
|
417
|
+
'[runtime-fetcher] win32-arm64 has no native runtime; using win32-x64 under emulation\n'
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
396
421
|
const { url, sha256, size } = asset
|
|
397
422
|
const version = `pg${manifest.pg?.major}.${manifest.pg?.minor}+pgvector-${manifest.pgvector?.version}`
|
|
398
423
|
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import crypto from 'node:crypto'
|
|
2
|
+
import { isInternalRuntimeNotificationText } from '../../shared/tool-execution-contract.mjs'
|
|
2
3
|
|
|
3
4
|
// Side-effect-free helpers for ingest_session (recall fast-track hydration).
|
|
4
5
|
// Extracted from memory/index.mjs so the pure logic (stable identity, sensitive
|
|
5
6
|
// redaction, role/content shaping) can be unit-tested without importing the
|
|
6
7
|
// MCP server entrypoint and its heavy boot-time side effects.
|
|
7
8
|
|
|
8
|
-
// Roles we persist from an in-memory session transcript
|
|
9
|
-
//
|
|
10
|
-
|
|
9
|
+
// Roles we persist from an in-memory session transcript (conversation only).
|
|
10
|
+
// Map provider/runtime spellings onto canonical roles; only user/assistant are
|
|
11
|
+
// kept so recall-fasttrack memory does not duplicate protected system prefix.
|
|
12
|
+
const INGEST_SESSION_ROLES = new Set(['user', 'assistant'])
|
|
11
13
|
|
|
12
14
|
export function normalizeIngestRole(role) {
|
|
13
15
|
const raw = String(role || '').trim().toLowerCase()
|
|
14
16
|
if (!raw) return null
|
|
15
|
-
|
|
16
|
-
if (raw === '
|
|
17
|
-
if (raw === '
|
|
18
|
-
|
|
17
|
+
let canonical = raw
|
|
18
|
+
if (raw === 'tool_result' || raw === 'function' || raw === 'tool-result') canonical = 'tool'
|
|
19
|
+
else if (raw === 'human') canonical = 'user'
|
|
20
|
+
else if (raw === 'ai' || raw === 'model') canonical = 'assistant'
|
|
21
|
+
return INGEST_SESSION_ROLES.has(canonical) ? canonical : null
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
// Extract the first textual content block from a message content field.
|
|
@@ -192,3 +195,109 @@ export function sessionMessageContent(m) {
|
|
|
192
195
|
}
|
|
193
196
|
return parts.join('\n')
|
|
194
197
|
}
|
|
198
|
+
|
|
199
|
+
// ── Pure-conversation ingest shaping ──────────────────────────────────────
|
|
200
|
+
//
|
|
201
|
+
// ingest_session persists ONLY real conversation (human prompts + model reply
|
|
202
|
+
// prose). sessionMessageContent (above) is the structured-handoff shaper that
|
|
203
|
+
// inlines tool_call / tool_result traces and is consumed by smoke tests; it is
|
|
204
|
+
// intentionally LEFT UNCHANGED. The functions below are ingest-only and strip
|
|
205
|
+
// everything that is mechanical/synthetic so memory episode rows are pure
|
|
206
|
+
// conversation with zero loss of genuine human/model text.
|
|
207
|
+
|
|
208
|
+
// Mirror of compact.mjs SUMMARY_PREFIX (the compaction handoff anchor). Copied
|
|
209
|
+
// locally rather than imported because compact.mjs lives under
|
|
210
|
+
// agent/orchestrator/session and pulls in heavy context/offload modules —
|
|
211
|
+
// importing it into the memory layer would create a layering dependency (memory
|
|
212
|
+
// → orchestrator) and risk a boot-time cycle. Keep this string byte-identical
|
|
213
|
+
// to compact.mjs:9; if that anchor changes, update this copy.
|
|
214
|
+
const SUMMARY_PREFIX_INGEST = 'A previous model worked on this task and produced the compacted handoff summary below. Build on the work already done and avoid duplicating it; treat the summary as authoritative context for continuing the task. You also retain the preserved recent turns that follow.'
|
|
215
|
+
|
|
216
|
+
// Anchored strip of the deterministic user-turn prefix envelopes that
|
|
217
|
+
// manager.mjs prepends to the SINGLE real user message (manager.mjs:3166-3201
|
|
218
|
+
// via prefixUserTurnContent / prefixSessionStartContent / buildSessionStartBlock).
|
|
219
|
+
// Zero-loss design: every rule is anchored to the START of the message and only
|
|
220
|
+
// removes the EXACT shapes manager.mjs produces. A `# Task` / `# Session` etc.
|
|
221
|
+
// appearing mid-message in the human's own text is never touched. When in
|
|
222
|
+
// doubt the rules UNDER-strip (leave content) rather than delete human text.
|
|
223
|
+
function stripUserTurnPrefixEnvelopes(text) {
|
|
224
|
+
let out = String(text ?? '')
|
|
225
|
+
// 1) Leading `# Session` block (buildSessionStartBlock: `# Session\nCwd: ...
|
|
226
|
+
// \nModel: ...\nWorkflow: ...`, joined by prefixSessionStartContent with a
|
|
227
|
+
// trailing `\n\n`). FIELD-ANCHORED: only strip when the line(s) right after
|
|
228
|
+
// `# Session\n` are EXACTLY the fixed fields buildSessionStartBlock emits
|
|
229
|
+
// (`Cwd: `, `Model: `, `Workflow: `, each on its own line) before the
|
|
230
|
+
// blank-line terminator. A human doc that merely STARTS with a `# Session`
|
|
231
|
+
// heading followed by free prose (e.g. `프로젝트 회의록입니다`) does NOT match
|
|
232
|
+
// — its next line is not a `Cwd:/Model:/Workflow:` field — so it is
|
|
233
|
+
// preserved verbatim (zero-loss). Anchored ^.
|
|
234
|
+
out = out.replace(/^# Session\n(?:(?:Cwd|Model|Workflow): [^\n]*\n)+(?:\n|$)/, '')
|
|
235
|
+
// 2) Leading `# Additional context\n<body>\n\n` (manager.mjs:3168). Anchored
|
|
236
|
+
// to start; body runs up to the next `# ` section boundary or end. The
|
|
237
|
+
// `\n\n` separator manager.mjs emits is included.
|
|
238
|
+
out = out.replace(/^# Additional context\n[\s\S]*?(?=\n# |$)/, '')
|
|
239
|
+
out = out.replace(/^\n+/, '')
|
|
240
|
+
// 3) Leading `# Prefetch\n<body>\n\n` (manager.mjs:3171). Same anchoring.
|
|
241
|
+
out = out.replace(/^# Prefetch\n[\s\S]*?(?=\n# |$)/, '')
|
|
242
|
+
out = out.replace(/^\n+/, '')
|
|
243
|
+
// 4) Leading `# Task\n` marker (prefixUserTurnContent: `${contextBlock}# Task\n${content}`).
|
|
244
|
+
// Remove ONLY the marker line; everything after it is the human prompt and
|
|
245
|
+
// is preserved verbatim. Anchored ^ so a `# Task` in the human's own prose
|
|
246
|
+
// (after real text precedes it) is never removed.
|
|
247
|
+
out = out.replace(/^# Task\n/, '')
|
|
248
|
+
return out
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Ingest-only message shaper: returns ONLY the human/model prose text, NEVER
|
|
252
|
+
// inlining tool_call / tool_result traces. For user messages, the deterministic
|
|
253
|
+
// manager.mjs prefix envelopes are stripped so only the human's actual prompt
|
|
254
|
+
// remains. The <system-reminder> block is left in place here because
|
|
255
|
+
// cleanMemoryText already removes it downstream (text-utils.cjs:28).
|
|
256
|
+
export function sessionMessageContentForIngest(m) {
|
|
257
|
+
const base = firstTextContent(m?.content)
|
|
258
|
+
if (!base) return ''
|
|
259
|
+
if (normalizeIngestRole(m?.role) === 'user') {
|
|
260
|
+
return stripUserTurnPrefixEnvelopes(base)
|
|
261
|
+
}
|
|
262
|
+
return base
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Row-exclusion predicate for ingest_session. Synthetic / non-conversation
|
|
266
|
+
// rows (reference-files injections, compaction summaries, protected-context
|
|
267
|
+
// `.` acks, internal runtime nudges) are dropped ENTIRELY — they are noise,
|
|
268
|
+
// not conversation. Mirrors the predicates in manager.mjs / compact.mjs but is
|
|
269
|
+
// reimplemented locally to avoid a memory→orchestrator layering dependency.
|
|
270
|
+
export function shouldExcludeIngestMessage(m) {
|
|
271
|
+
if (!m || typeof m !== 'object') return true
|
|
272
|
+
const role = normalizeIngestRole(m?.role)
|
|
273
|
+
const raw = firstTextContent(m?.content)
|
|
274
|
+
// `Reference files:` synthetic user rows (manager.mjs isReferenceFilesMessage).
|
|
275
|
+
if (role === 'user' && typeof raw === 'string' && /^Reference files:\s*/i.test(raw.trimStart())) {
|
|
276
|
+
return true
|
|
277
|
+
}
|
|
278
|
+
// Attachment-only placeholder rows (e.g. Discord backend discord.mjs:724
|
|
279
|
+
// `"(attachment)"` fallback when a message carries no text, only files).
|
|
280
|
+
// Carries zero retrievable content — excluded so recall isn't polluted
|
|
281
|
+
// with bare "(attachment)" memory rows.
|
|
282
|
+
if (role === 'user' && typeof raw === 'string' && raw.trim() === '(attachment)') {
|
|
283
|
+
return true
|
|
284
|
+
}
|
|
285
|
+
// Compaction summary user rows (compact.mjs isSummaryMessage / SUMMARY_PREFIX).
|
|
286
|
+
if (role === 'user' && typeof raw === 'string' && raw.startsWith(SUMMARY_PREFIX_INGEST)) {
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
// Protected-context `.` ack assistant rows (compact.mjs isProtectedContextAckMessage):
|
|
290
|
+
// a bare `.` with no tool calls. cleanMemoryText leaves a lone `.` non-empty
|
|
291
|
+
// (no \p{L}\p{N}), so it would otherwise survive the empty-skip — exclude it.
|
|
292
|
+
if (role === 'assistant' && typeof raw === 'string' && raw.trim() === '.' && !Array.isArray(m?.toolCalls)) {
|
|
293
|
+
return true
|
|
294
|
+
}
|
|
295
|
+
// Internal runtime nudge `[mixdog-runtime] ...` user rows (loop.mjs:2014-2020)
|
|
296
|
+
// and other internal runtime notifications (tool-execution-contract).
|
|
297
|
+
if (role === 'user') {
|
|
298
|
+
const text = typeof raw === 'string' ? raw : ''
|
|
299
|
+
if (/^\[mixdog-runtime\]/.test(text.trimStart())) return true
|
|
300
|
+
if (isInternalRuntimeNotificationText(text)) return true
|
|
301
|
+
}
|
|
302
|
+
return false
|
|
303
|
+
}
|
|
@@ -56,7 +56,7 @@ async function init(client) {
|
|
|
56
56
|
session_id TEXT,
|
|
57
57
|
iteration INTEGER,
|
|
58
58
|
kind TEXT NOT NULL,
|
|
59
|
-
|
|
59
|
+
agent TEXT,
|
|
60
60
|
model TEXT,
|
|
61
61
|
tool_name TEXT,
|
|
62
62
|
tool_ms INTEGER,
|
|
@@ -103,12 +103,18 @@ async function init(client) {
|
|
|
103
103
|
END $$
|
|
104
104
|
`)
|
|
105
105
|
|
|
106
|
+
// Drift repair MUST run before index creation: an old cluster whose
|
|
107
|
+
// trace_events predates the `agent` column would otherwise die right below
|
|
108
|
+
// at idx_trace_agent_ts (CREATE INDEX references the missing column) before
|
|
109
|
+
// initAgentTables() ever gets a chance to repair it. Reviewer High fix.
|
|
110
|
+
await migrateSchemaDrift(client)
|
|
111
|
+
|
|
106
112
|
// BRIN on ts — ~1000× smaller than btree for append-only timeseries; ideal
|
|
107
113
|
// for time-window range scans where rows arrive in roughly ts order.
|
|
108
114
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_ts_brin ON trace_events USING BRIN (ts) WITH (pages_per_range = 32)`)
|
|
109
115
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_kind_ts ON trace_events(kind, ts DESC)`)
|
|
110
116
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_session ON trace_events(session_id, ts)`)
|
|
111
|
-
await client.query(`CREATE INDEX IF NOT EXISTS
|
|
117
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_agent_ts ON trace_events(agent, ts DESC)`)
|
|
112
118
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_model_ts ON trace_events(model, ts DESC)`)
|
|
113
119
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_trace_tool ON trace_events(tool_name) WHERE kind = 'tool'`)
|
|
114
120
|
// Span-tree and cross-schema recall↔trace correlation — partial indexes so
|
|
@@ -122,13 +128,45 @@ async function init(client) {
|
|
|
122
128
|
await ensureCurrentAndNextMonthPartitions(client)
|
|
123
129
|
}
|
|
124
130
|
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Schema-drift repair — ALTER TABLE ... ADD COLUMN IF NOT EXISTS
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Observed failure: pg.log repeatedly logs
|
|
135
|
+
// column "agent" of relation "trace_events" does not exist
|
|
136
|
+
// and the equivalent for agent_sessions — an older cluster/pgdata created
|
|
137
|
+
// before the `agent` column was added to these two tables, so every INSERT/
|
|
138
|
+
// UPSERT touching it fails forever until manually patched. CREATE TABLE IF
|
|
139
|
+
// NOT EXISTS above is a no-op once the table exists, so it never repairs an
|
|
140
|
+
// already-created table with a stale column set. Run these idempotent
|
|
141
|
+
// ADD COLUMN migrations on every boot (init() and initAgentTables()) so
|
|
142
|
+
// drifted clusters self-heal without a manual ALTER.
|
|
143
|
+
async function migrateSchemaDrift(client) {
|
|
144
|
+
// Bounded lock wait: nullable ADD COLUMN is metadata-only once the ACCESS
|
|
145
|
+
// EXCLUSIVE lock is held, but acquiring that lock can queue behind live
|
|
146
|
+
// readers/writers indefinitely and wedge boot (reviewer Medium). 5s is
|
|
147
|
+
// generous for a metadata change; on timeout we leave the drift in place
|
|
148
|
+
// (inserts keep failing as before — no worse) instead of hanging startup.
|
|
149
|
+
await client.query(`SET LOCAL lock_timeout = '5s'`).catch(() => {})
|
|
150
|
+
try {
|
|
151
|
+
await client.query(`ALTER TABLE IF EXISTS trace_events ADD COLUMN IF NOT EXISTS agent TEXT`)
|
|
152
|
+
await client.query(`ALTER TABLE IF EXISTS agent_sessions ADD COLUMN IF NOT EXISTS agent TEXT`)
|
|
153
|
+
} finally {
|
|
154
|
+
await client.query(`SET LOCAL lock_timeout = DEFAULT`).catch(() => {})
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
125
158
|
// ---------------------------------------------------------------------------
|
|
126
159
|
// Agent-specific analytic tables (added post-init via initAgentTables)
|
|
127
160
|
// ---------------------------------------------------------------------------
|
|
128
161
|
|
|
129
|
-
// Called once per openTraceDatabase boot
|
|
130
|
-
//
|
|
162
|
+
// Called once per openTraceDatabase boot, inside the BOOTSTRAP advisory-lock
|
|
163
|
+
// scope (see openTraceDatabase) so concurrent first-boot processes don't race
|
|
164
|
+
// this DDL. All statements remain IF NOT EXISTS as a second line of defense.
|
|
131
165
|
export async function initAgentTables(client) {
|
|
166
|
+
// Repair schema drift on already-created tables before anything else
|
|
167
|
+
// (trace_events already exists at this point via init(); agent_sessions
|
|
168
|
+
// is created just below in this same function).
|
|
169
|
+
await migrateSchemaDrift(client)
|
|
132
170
|
// ── agent_calls: one row per tool invocation ─────────────────────────────
|
|
133
171
|
await client.query(`
|
|
134
172
|
CREATE TABLE IF NOT EXISTS agent_calls (
|
|
@@ -175,7 +213,7 @@ export async function initAgentTables(client) {
|
|
|
175
213
|
await client.query(`
|
|
176
214
|
CREATE TABLE IF NOT EXISTS agent_sessions (
|
|
177
215
|
session_id TEXT PRIMARY KEY,
|
|
178
|
-
|
|
216
|
+
agent TEXT,
|
|
179
217
|
model TEXT,
|
|
180
218
|
started_at TIMESTAMPTZ,
|
|
181
219
|
last_seen_at TIMESTAMPTZ,
|
|
@@ -186,6 +224,10 @@ export async function initAgentTables(client) {
|
|
|
186
224
|
total_output_tokens BIGINT NOT NULL DEFAULT 0
|
|
187
225
|
)
|
|
188
226
|
`)
|
|
227
|
+
|
|
228
|
+
// Repair drift again post-creation for agent_sessions (belt-and-suspenders;
|
|
229
|
+
// no-op when the table was freshly created above with the column present).
|
|
230
|
+
await migrateSchemaDrift(client)
|
|
189
231
|
}
|
|
190
232
|
|
|
191
233
|
// ---------------------------------------------------------------------------
|
|
@@ -289,14 +331,14 @@ export async function insertAgentCalls(db, events) {
|
|
|
289
331
|
// Upsert session summaries — accumulate from tool+llm rows in this batch
|
|
290
332
|
const sessionMap = new Map()
|
|
291
333
|
for (const r of toolRows) {
|
|
292
|
-
const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts,
|
|
334
|
+
const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts, agent: null, model: null }
|
|
293
335
|
s.tool_calls += 1
|
|
294
336
|
if (r.iteration != null && r.iteration > s.max_iteration) s.max_iteration = r.iteration
|
|
295
337
|
if (r.ts < s.ts0) s.ts0 = r.ts; if (r.ts > s.ts1) s.ts1 = r.ts
|
|
296
338
|
sessionMap.set(r.session_id, s)
|
|
297
339
|
}
|
|
298
340
|
for (const r of llmRows) {
|
|
299
|
-
const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts,
|
|
341
|
+
const s = sessionMap.get(r.session_id) ?? { tool_calls: 0, llm_calls: 0, max_iteration: 0, total_input: 0n, total_output: 0n, ts0: r.ts, ts1: r.ts, agent: null, model: null }
|
|
300
342
|
s.llm_calls += 1
|
|
301
343
|
s.total_input += BigInt(r.input_tokens ?? 0)
|
|
302
344
|
s.total_output += BigInt(r.output_tokens ?? 0)
|
|
@@ -305,13 +347,13 @@ export async function insertAgentCalls(db, events) {
|
|
|
305
347
|
if (r.ts < s.ts0) s.ts0 = r.ts; if (r.ts > s.ts1) s.ts1 = r.ts
|
|
306
348
|
sessionMap.set(r.session_id, s)
|
|
307
349
|
}
|
|
308
|
-
// Also pick up
|
|
350
|
+
// Also pick up agent from preset_assign events in the same batch
|
|
309
351
|
for (const ev of events) {
|
|
310
|
-
if (ev.kind === 'preset_assign' && ev.
|
|
352
|
+
if (ev.kind === 'preset_assign' && ev.agent) {
|
|
311
353
|
const sid = ev.session_id ?? ev.sessionId ?? null
|
|
312
354
|
if (!sid) continue
|
|
313
355
|
const s = sessionMap.get(sid)
|
|
314
|
-
if (s) s.
|
|
356
|
+
if (s) s.agent = ev.agent
|
|
315
357
|
}
|
|
316
358
|
}
|
|
317
359
|
// Fix 5 — upsert sessions for preset_assign-only batches (no tool/llm rows yet)
|
|
@@ -326,32 +368,32 @@ export async function insertAgentCalls(db, events) {
|
|
|
326
368
|
tool_calls: 0, llm_calls: 0, max_iteration: 0,
|
|
327
369
|
total_input: 0n, total_output: 0n,
|
|
328
370
|
ts0: tsIso, ts1: tsIso,
|
|
329
|
-
|
|
371
|
+
agent: ev.agent ?? null, model: ev.model ?? null,
|
|
330
372
|
})
|
|
331
373
|
}
|
|
332
374
|
|
|
333
375
|
// Coalesce agent_sessions upserts: batch all sessions in one unnest INSERT.
|
|
334
376
|
// Also within the same transaction.
|
|
335
377
|
if (sessionMap.size > 0) {
|
|
336
|
-
const sids = [],
|
|
378
|
+
const sids = [], agents = [], models = [], ts0s = [], ts1s = [],
|
|
337
379
|
tcalls = [], lcalls = [], maxiters = [], tinputs = [], toutputs = []
|
|
338
380
|
for (const [sid, s] of sessionMap) {
|
|
339
|
-
sids.push(sid);
|
|
381
|
+
sids.push(sid); agents.push(s.agent); models.push(s.model)
|
|
340
382
|
ts0s.push(s.ts0); ts1s.push(s.ts1)
|
|
341
383
|
tcalls.push(s.tool_calls); lcalls.push(s.llm_calls)
|
|
342
384
|
maxiters.push(s.max_iteration)
|
|
343
385
|
tinputs.push(String(s.total_input)); toutputs.push(String(s.total_output))
|
|
344
386
|
}
|
|
345
387
|
await client.query(`
|
|
346
|
-
INSERT INTO agent_sessions (session_id,
|
|
347
|
-
SELECT u.session_id, u.
|
|
388
|
+
INSERT INTO agent_sessions (session_id, agent, model, started_at, last_seen_at, tool_calls, llm_calls, max_iteration, total_input_tokens, total_output_tokens)
|
|
389
|
+
SELECT u.session_id, u.agent, u.model,
|
|
348
390
|
u.started_at::timestamptz, u.last_seen_at::timestamptz,
|
|
349
391
|
u.tool_calls::int, u.llm_calls::int, u.max_iteration::int,
|
|
350
392
|
u.total_input_tokens::bigint, u.total_output_tokens::bigint
|
|
351
393
|
FROM unnest($1::text[],$2::text[],$3::text[],$4::text[],$5::text[],$6::int[],$7::int[],$8::int[],$9::text[],$10::text[])
|
|
352
|
-
AS u(session_id,
|
|
394
|
+
AS u(session_id,agent,model,started_at,last_seen_at,tool_calls,llm_calls,max_iteration,total_input_tokens,total_output_tokens)
|
|
353
395
|
ON CONFLICT (session_id) DO UPDATE SET
|
|
354
|
-
|
|
396
|
+
agent = COALESCE(EXCLUDED.agent, agent_sessions.agent),
|
|
355
397
|
model = COALESCE(EXCLUDED.model, agent_sessions.model),
|
|
356
398
|
started_at = LEAST(agent_sessions.started_at, EXCLUDED.started_at),
|
|
357
399
|
last_seen_at = GREATEST(agent_sessions.last_seen_at, EXCLUDED.last_seen_at),
|
|
@@ -360,7 +402,7 @@ export async function insertAgentCalls(db, events) {
|
|
|
360
402
|
max_iteration = GREATEST(agent_sessions.max_iteration, EXCLUDED.max_iteration),
|
|
361
403
|
total_input_tokens = agent_sessions.total_input_tokens + EXCLUDED.total_input_tokens,
|
|
362
404
|
total_output_tokens = agent_sessions.total_output_tokens + EXCLUDED.total_output_tokens
|
|
363
|
-
`, [sids,
|
|
405
|
+
`, [sids, agents, models, ts0s, ts1s, tcalls, lcalls, maxiters, tinputs, toutputs])
|
|
364
406
|
}
|
|
365
407
|
|
|
366
408
|
await client.query('COMMIT')
|
|
@@ -513,14 +555,19 @@ export async function openTraceDatabase(dataDir) {
|
|
|
513
555
|
// are pre-created for this boot.
|
|
514
556
|
await ensureCurrentAndNextMonthPartitions(client)
|
|
515
557
|
}
|
|
558
|
+
// Agent-specific analytic tables — moved inside the advisory-lock
|
|
559
|
+
// scope (was previously run after client.release() with no lock,
|
|
560
|
+
// letting concurrent first-boot processes race the agent_calls/
|
|
561
|
+
// agent_llm/agent_sessions CREATE TABLE + migrateSchemaDrift DDL).
|
|
562
|
+
// Reuses the same lock-holding client/session; still idempotent
|
|
563
|
+
// (IF NOT EXISTS everywhere) but no longer racy across processes.
|
|
564
|
+
await initAgentTables(client)
|
|
516
565
|
} finally {
|
|
517
566
|
await client.query(`SELECT pg_advisory_unlock(${BOOTSTRAP_LOCK_KEY})`)
|
|
518
567
|
}
|
|
519
568
|
} finally {
|
|
520
569
|
client.release()
|
|
521
570
|
}
|
|
522
|
-
// Agent-specific analytic tables — idempotent, no advisory lock needed.
|
|
523
|
-
await initAgentTables(db)
|
|
524
571
|
|
|
525
572
|
dbs.set(key, db)
|
|
526
573
|
|
|
@@ -557,7 +604,7 @@ export async function openTraceDatabase(dataDir) {
|
|
|
557
604
|
// ---------------------------------------------------------------------------
|
|
558
605
|
|
|
559
606
|
const TRACE_COLS = [
|
|
560
|
-
'ts', 'session_id', 'iteration', 'kind', '
|
|
607
|
+
'ts', 'session_id', 'iteration', 'kind', 'agent', 'model',
|
|
561
608
|
'tool_name', 'tool_ms', 'input_tokens', 'output_tokens',
|
|
562
609
|
'cached_tokens', 'cache_write_tokens', 'duration_ms',
|
|
563
610
|
'error_message', 'payload', 'parent_span_id', 'entry_id',
|
|
@@ -755,7 +802,7 @@ export async function insertTraceEvents(db, events) {
|
|
|
755
802
|
ev.session_id ?? null,
|
|
756
803
|
ev.iteration != null ? Number(ev.iteration) : null,
|
|
757
804
|
String(ev.kind ?? 'unknown'),
|
|
758
|
-
ev.
|
|
805
|
+
ev.agent ?? null,
|
|
759
806
|
ev.model ?? null,
|
|
760
807
|
ev.tool_name ?? null,
|
|
761
808
|
ev.tool_ms != null ? Number(ev.tool_ms) : null,
|