mixdog 0.9.147 → 0.9.149
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 +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { resetEmbeddingColumnsForModel } from './memory.mjs'
|
|
5
|
+
|
|
6
|
+
function mockDb({ entriesDims = 640, coreDims = 640, identityMatches = true } = {}) {
|
|
7
|
+
const execs = []
|
|
8
|
+
const queries = []
|
|
9
|
+
return {
|
|
10
|
+
execs,
|
|
11
|
+
queries,
|
|
12
|
+
async exec(sql) {
|
|
13
|
+
execs.push(sql)
|
|
14
|
+
},
|
|
15
|
+
async query(sql, params = []) {
|
|
16
|
+
queries.push({ sql, params })
|
|
17
|
+
if (sql.includes('FROM pg_attribute')) {
|
|
18
|
+
return { rows: [{ atttypmod: params[0] === 'entries' ? entriesDims : coreDims }] }
|
|
19
|
+
}
|
|
20
|
+
if (sql.includes('SELECT value = $2::jsonb')) {
|
|
21
|
+
return { rows: identityMatches ? [{ matches: true }] : [] }
|
|
22
|
+
}
|
|
23
|
+
return { rows: [] }
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const identity = { model: 'onnx-community/harrier-oss-v1-270m-ONNX', dtype: 'q4' }
|
|
29
|
+
|
|
30
|
+
test('matching embedding dimensions and identity keep stored vectors', async () => {
|
|
31
|
+
const db = mockDb()
|
|
32
|
+
assert.equal(await resetEmbeddingColumnsForModel(db, 640, identity), false)
|
|
33
|
+
assert.deepEqual(db.execs, [])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('a model identity change invalidates vectors even at the same dimensions', async () => {
|
|
37
|
+
const db = mockDb({ identityMatches: false })
|
|
38
|
+
assert.equal(await resetEmbeddingColumnsForModel(db, 640, identity), true)
|
|
39
|
+
assert.ok(db.execs.some((sql) => sql.includes('UPDATE entries SET embedding = NULL')))
|
|
40
|
+
assert.ok(db.execs.some((sql) => sql.includes('UPDATE core_entries SET embedding = NULL')))
|
|
41
|
+
assert.ok(db.execs.some((sql) => sql.includes('DROP TABLE IF EXISTS memory.embedding_cache')))
|
|
42
|
+
assert.ok(db.queries.some(({ params }) => params[0] === 'embedding.current_model'))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('a dimension change rebuilds both halfvec columns', async () => {
|
|
46
|
+
const db = mockDb({ entriesDims: 384, coreDims: 384 })
|
|
47
|
+
assert.equal(await resetEmbeddingColumnsForModel(db, 640, identity), true)
|
|
48
|
+
assert.ok(db.execs.some((sql) => sql.includes('ALTER TABLE entries ALTER COLUMN embedding TYPE halfvec(640)')))
|
|
49
|
+
assert.ok(db.execs.some((sql) => sql.includes('ALTER TABLE core_entries ALTER COLUMN embedding TYPE halfvec(640)')))
|
|
50
|
+
})
|
|
@@ -46,7 +46,6 @@ export async function searchRelevantHybrid(db, query, options = {}) {
|
|
|
46
46
|
// page still needs a broad pool so the final decision can outrank them.
|
|
47
47
|
const candidateWindow = Math.max(240, limit * 8)
|
|
48
48
|
const includeMembers = Boolean(options.includeMembers)
|
|
49
|
-
const writeBackMemberHits = options.writeBackMemberHits !== false
|
|
50
49
|
const rootOnly = options.rootOnly === true
|
|
51
50
|
// Pre-filter knobs. Without them, FTS/vec rank the whole tree and a
|
|
52
51
|
// post-filter time window can wipe the result set.
|
|
@@ -423,8 +422,11 @@ LEFT JOIN exact x ON x.id = c.id`
|
|
|
423
422
|
if (r.exact_rank != null) exactCount++
|
|
424
423
|
}
|
|
425
424
|
} catch (err) {
|
|
425
|
+
// A failing hybrid CTE is a DB/schema fault. Returning [] rendered it to the
|
|
426
|
+
// caller as "no memory found", so a broken index or a migration gap looked
|
|
427
|
+
// like an empty store instead of an error.
|
|
426
428
|
__mixdogMemoryLog(`[recall] hybrid CTE failed: ${err.message}\n`)
|
|
427
|
-
|
|
429
|
+
throw err
|
|
428
430
|
}
|
|
429
431
|
|
|
430
432
|
if (rawRows.length === 0) return []
|
|
@@ -739,27 +741,10 @@ LEFT JOIN exact x ON x.id = c.id`
|
|
|
739
741
|
if (rootIdsForReturn.length >= limit) break
|
|
740
742
|
}
|
|
741
743
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
for (const rootId of memberHitRootIds) {
|
|
747
|
-
const r = rootIdsForReturn.find(x => x.root.id === rootId)?.root ?? byId.get(rootId)
|
|
748
|
-
if (r) validRootIds.push(Number(rootId))
|
|
749
|
-
}
|
|
750
|
-
if (validRootIds.length > 0) {
|
|
751
|
-
try {
|
|
752
|
-
const { rowCount } = await db.query(
|
|
753
|
-
`UPDATE entries SET last_seen_at = $1::bigint WHERE id = ANY($2::bigint[]) AND is_root = 1
|
|
754
|
-
AND (last_seen_at IS NULL OR last_seen_at < $1::bigint - 3600000)`,
|
|
755
|
-
[String(nowMs), validRootIds],
|
|
756
|
-
)
|
|
757
|
-
writeBackCount = rowCount ?? validRootIds.length
|
|
758
|
-
} catch (err) {
|
|
759
|
-
__mixdogMemoryLog(`[recall] writeback batch failed (count=${validRootIds.length}): ${err.message}\n`)
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
}
|
|
744
|
+
// Recall is a read. The member-hit write-back that used to run here bumped
|
|
745
|
+
// last_seen_at, which feeds the stored score/freshness ranking — so merely
|
|
746
|
+
// searching reordered later results. Removed outright: every caller already
|
|
747
|
+
// passed writeBackMemberHits: false.
|
|
763
748
|
|
|
764
749
|
// ── Final fetch: full row for each root by id = ANY(bigint[]) ────────────
|
|
765
750
|
const topIds = rootIdsForReturn.map(x => Number(x.root.id))
|
|
@@ -929,7 +914,7 @@ LEFT JOIN exact x ON x.id = c.id`
|
|
|
929
914
|
}
|
|
930
915
|
|
|
931
916
|
__mixdogMemoryLog(
|
|
932
|
-
`[recall] dense=${denseCount} sparse=${sparseCount} trgm=${trgmCount} exact=${exactCount} semantic_only_dropped=${semanticOnlyDropped} weak_text_dropped=${weakTextDropped} merged=${results.length}
|
|
917
|
+
`[recall] dense=${denseCount} sparse=${sparseCount} trgm=${trgmCount} exact=${exactCount} semantic_only_dropped=${semanticOnlyDropped} weak_text_dropped=${weakTextDropped} merged=${results.length}\n`,
|
|
933
918
|
)
|
|
934
919
|
|
|
935
920
|
return results
|
|
@@ -18,7 +18,7 @@ export const VALID_CATEGORY = new Set([
|
|
|
18
18
|
'rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue',
|
|
19
19
|
])
|
|
20
20
|
|
|
21
|
-
export async function init(db, dims) {
|
|
21
|
+
export async function init(db, dims, embeddingIdentity = null) {
|
|
22
22
|
const dimCount = Number(dims)
|
|
23
23
|
if (!Number.isInteger(dimCount) || dimCount <= 0) {
|
|
24
24
|
throw new Error(`init: dims must be a positive integer, got ${dims}`)
|
|
@@ -266,6 +266,13 @@ export async function init(db, dims) {
|
|
|
266
266
|
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
|
|
267
267
|
['embedding.current_dims', JSON.stringify(dimCount)],
|
|
268
268
|
)
|
|
269
|
+
if (embeddingIdentity != null) {
|
|
270
|
+
await db.query(
|
|
271
|
+
`INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
|
|
272
|
+
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
|
|
273
|
+
['embedding.current_model', JSON.stringify(embeddingIdentity)],
|
|
274
|
+
)
|
|
275
|
+
}
|
|
269
276
|
await db.query(
|
|
270
277
|
`INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
|
|
271
278
|
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
|
|
@@ -343,15 +350,24 @@ export async function refreshHotActive(db) {
|
|
|
343
350
|
}
|
|
344
351
|
}
|
|
345
352
|
|
|
346
|
-
async function
|
|
353
|
+
export async function resetEmbeddingColumnsForModel(db, dimCount, embeddingIdentity = null) {
|
|
347
354
|
const entriesDims = await getEmbeddingColumnDims(db, 'entries')
|
|
348
355
|
const coreDims = await getEmbeddingColumnDims(db, 'core_entries')
|
|
349
|
-
const
|
|
350
|
-
|
|
356
|
+
const normalizedIdentity = embeddingIdentity == null ? null : JSON.stringify(embeddingIdentity)
|
|
357
|
+
let identityChanged = false
|
|
358
|
+
if (normalizedIdentity != null) {
|
|
359
|
+
const identity = await db.query(
|
|
360
|
+
`SELECT value = $2::jsonb AS matches FROM meta WHERE key = $1`,
|
|
361
|
+
['embedding.current_model', normalizedIdentity],
|
|
362
|
+
)
|
|
363
|
+
identityChanged = identity.rows.length === 0 || identity.rows[0].matches !== true
|
|
364
|
+
}
|
|
365
|
+
const needsEntriesReset = entriesDims != null && (entriesDims !== dimCount || identityChanged)
|
|
366
|
+
const needsCoreReset = coreDims != null && (coreDims !== dimCount || identityChanged)
|
|
351
367
|
if (!needsEntriesReset && !needsCoreReset) return false
|
|
352
368
|
|
|
353
369
|
__mixdogMemoryLog(
|
|
354
|
-
`[memory] embedding
|
|
370
|
+
`[memory] embedding model changed; resetting vectors for halfvec(${dimCount}) ` +
|
|
355
371
|
`(entries=${entriesDims ?? 'missing'}, core_entries=${coreDims ?? 'missing'})\n`,
|
|
356
372
|
)
|
|
357
373
|
|
|
@@ -360,11 +376,19 @@ async function resetEmbeddingColumnsForDims(db, dimCount) {
|
|
|
360
376
|
await db.exec(`DROP INDEX IF EXISTS core_entries_embedding_hnsw`)
|
|
361
377
|
|
|
362
378
|
if (needsEntriesReset) {
|
|
363
|
-
|
|
379
|
+
if (entriesDims !== dimCount) {
|
|
380
|
+
await db.exec(`ALTER TABLE entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
|
|
381
|
+
} else {
|
|
382
|
+
await db.exec(`UPDATE entries SET embedding = NULL WHERE embedding IS NOT NULL`)
|
|
383
|
+
}
|
|
364
384
|
await db.exec(`UPDATE entries SET summary_hash = NULL WHERE summary_hash IS NOT NULL`)
|
|
365
385
|
}
|
|
366
386
|
if (needsCoreReset) {
|
|
367
|
-
|
|
387
|
+
if (coreDims !== dimCount) {
|
|
388
|
+
await db.exec(`ALTER TABLE core_entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
|
|
389
|
+
} else {
|
|
390
|
+
await db.exec(`UPDATE core_entries SET embedding = NULL WHERE embedding IS NOT NULL`)
|
|
391
|
+
}
|
|
368
392
|
}
|
|
369
393
|
|
|
370
394
|
await db.exec(`DROP TABLE IF EXISTS memory.embedding_cache`)
|
|
@@ -373,6 +397,13 @@ async function resetEmbeddingColumnsForDims(db, dimCount) {
|
|
|
373
397
|
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
|
|
374
398
|
['embedding.current_dims', JSON.stringify(dimCount)],
|
|
375
399
|
)
|
|
400
|
+
if (normalizedIdentity != null) {
|
|
401
|
+
await db.query(
|
|
402
|
+
`INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
|
|
403
|
+
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
|
|
404
|
+
['embedding.current_model', normalizedIdentity],
|
|
405
|
+
)
|
|
406
|
+
}
|
|
376
407
|
return true
|
|
377
408
|
}
|
|
378
409
|
|
|
@@ -430,7 +461,7 @@ async function _migrateRecallIndexesIfStale(db) {
|
|
|
430
461
|
}
|
|
431
462
|
}
|
|
432
463
|
|
|
433
|
-
export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
464
|
+
export async function ensureCurrentSchemaExtensions(db, dims, embeddingIdentity = null) {
|
|
434
465
|
// One-time cleanup: attachment-only placeholder rows ('(attachment)' user
|
|
435
466
|
// content, e.g. Discord provider discord.mjs:724) predate the
|
|
436
467
|
// shouldExcludeIngestMessage() ingest-time filter (session-ingest.mjs).
|
|
@@ -607,11 +638,18 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
|
607
638
|
['embedding.current_dims', JSON.stringify(dims)],
|
|
608
639
|
)
|
|
609
640
|
}
|
|
641
|
+
if (embeddingIdentity != null) {
|
|
642
|
+
await db.query(
|
|
643
|
+
`INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
|
|
644
|
+
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
|
|
645
|
+
['embedding.current_model', JSON.stringify(embeddingIdentity)],
|
|
646
|
+
)
|
|
647
|
+
}
|
|
610
648
|
|
|
611
649
|
await ensureHotActiveSearchObjects(db)
|
|
612
650
|
}
|
|
613
651
|
|
|
614
|
-
export async function openDatabase(dataDir, dims) {
|
|
652
|
+
export async function openDatabase(dataDir, dims, embeddingIdentity = null) {
|
|
615
653
|
const key = resolve(dataDir)
|
|
616
654
|
|
|
617
655
|
// Fast path — already resolved.
|
|
@@ -632,14 +670,14 @@ export async function openDatabase(dataDir, dims) {
|
|
|
632
670
|
// race skips the redundant DDL instead of re-running init().
|
|
633
671
|
await withSchemaBootstrapLock(pool, async () => {
|
|
634
672
|
if (!(await isBootstrapComplete(db))) {
|
|
635
|
-
await init(db, dims)
|
|
673
|
+
await init(db, dims, embeddingIdentity)
|
|
636
674
|
}
|
|
637
675
|
})
|
|
638
676
|
}
|
|
639
677
|
if (await isBootstrapComplete(db)) {
|
|
640
|
-
await
|
|
678
|
+
await resetEmbeddingColumnsForModel(db, Number(dims), embeddingIdentity)
|
|
641
679
|
}
|
|
642
|
-
await ensureCurrentSchemaExtensions(db, Number(dims))
|
|
680
|
+
await ensureCurrentSchemaExtensions(db, Number(dims), embeddingIdentity)
|
|
643
681
|
await validateEmbeddingDims(db, Number(dims))
|
|
644
682
|
|
|
645
683
|
dbs.set(key, db)
|
|
@@ -14,6 +14,7 @@ import { join } from 'path'
|
|
|
14
14
|
import { spawn, spawnSync } from 'child_process'
|
|
15
15
|
import { createConnection } from 'net'
|
|
16
16
|
import { createServer } from 'net'
|
|
17
|
+
import { isPidAlive } from '../../../shared/pid-liveness.mjs'
|
|
17
18
|
|
|
18
19
|
// ---------------------------------------------------------------------------
|
|
19
20
|
// Helpers
|
|
@@ -53,16 +54,6 @@ function isTcpPortFree(port) {
|
|
|
53
54
|
const PG_PORT_MIN = 55432
|
|
54
55
|
const PG_PORT_MAX = 55632
|
|
55
56
|
|
|
56
|
-
function isPidAlive(pid) {
|
|
57
|
-
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
58
|
-
try {
|
|
59
|
-
process.kill(pid, 0)
|
|
60
|
-
return true
|
|
61
|
-
} catch (err) {
|
|
62
|
-
return err?.code === 'EPERM'
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
57
|
function readPostmasterInfo(pgdataDir) {
|
|
67
58
|
try {
|
|
68
59
|
const lines = readFileSync(join(pgdataDir, 'postmaster.pid'), 'utf8').split('\n')
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
writeServiceAdvert as _writePgServiceAdvert,
|
|
30
30
|
} from '../../../shared/service-discovery.mjs';
|
|
31
31
|
import { withFileLockSync } from '../../../shared/atomic-file.mjs';
|
|
32
|
+
import { isPidAlive } from '../../../shared/pid-liveness.mjs';
|
|
32
33
|
import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from '../../../shared/runtime-root.mjs';
|
|
33
34
|
|
|
34
35
|
// ── pg-process interface (Track A) ───────────────────────────────────────────
|
|
@@ -350,15 +351,6 @@ function readPostmasterInfo(pgdata) {
|
|
|
350
351
|
} catch { return { pid: null, port: null }; }
|
|
351
352
|
}
|
|
352
353
|
|
|
353
|
-
function isPidAlive(pid) {
|
|
354
|
-
try { process.kill(pid, 0); return true; }
|
|
355
|
-
catch (e) {
|
|
356
|
-
// EPERM: process exists but we lack permission → alive.
|
|
357
|
-
// ESRCH: no such process → dead.
|
|
358
|
-
return e.code === 'EPERM';
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
354
|
/**
|
|
363
355
|
* Best-effort process-name check: confirms the pid's comm name contains 'postgres'.
|
|
364
356
|
* Falls back to true (alive) when the name cannot be determined.
|
|
@@ -432,7 +432,7 @@ export function createQueryHandlers({
|
|
|
432
432
|
// Race against the same deadline as the fan-out itself: a stuck
|
|
433
433
|
// embedding worker would previously park here indefinitely because
|
|
434
434
|
// the timer hadn't been started yet from the fan-out's perspective.
|
|
435
|
-
await Promise.race([embedTexts(queries), deadlineRace])
|
|
435
|
+
await Promise.race([embedTexts(queries, { inputType: 'query' }), deadlineRace])
|
|
436
436
|
} else if (embeddingWarmupCanStart()) {
|
|
437
437
|
void warmupEmbeddingProvider().catch((err) => {
|
|
438
438
|
log(`[memory-service] embedding warmup after cold fan-out skipped dense search: ${err?.message || err}\n`)
|
|
@@ -569,7 +569,7 @@ export function createQueryHandlers({
|
|
|
569
569
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
570
570
|
let queryVector = null
|
|
571
571
|
if (isEmbeddingModelReady()) {
|
|
572
|
-
queryVector = await embedText(retrievalQuery, { priority: true })
|
|
572
|
+
queryVector = await embedText(retrievalQuery, { priority: true, inputType: 'query' })
|
|
573
573
|
} else if (embeddingWarmupCanStart()) {
|
|
574
574
|
// Never turn a cold model into an 8-second foreground lock. Start the
|
|
575
575
|
// isolated worker and return lexical results now; the next recall gets
|
|
@@ -612,7 +612,6 @@ export function createQueryHandlers({
|
|
|
612
612
|
projectScope,
|
|
613
613
|
category,
|
|
614
614
|
excludeStatuses,
|
|
615
|
-
writeBackMemberHits: false,
|
|
616
615
|
latestByConcept: latestIntent && args.period == null,
|
|
617
616
|
// useHotActive was set to true here so default (no-period) calls
|
|
618
617
|
// routed through the mv_hot_active materialized view — a narrow
|
|
@@ -648,7 +647,6 @@ export function createQueryHandlers({
|
|
|
648
647
|
...searchOptions,
|
|
649
648
|
includeMembers: false,
|
|
650
649
|
rootOnly: true,
|
|
651
|
-
writeBackMemberHits: false,
|
|
652
650
|
})
|
|
653
651
|
: Promise.resolve([]),
|
|
654
652
|
])
|
|
@@ -334,12 +334,12 @@ export function createSessionIngestRuntime({
|
|
|
334
334
|
currentSnapshot.push({ key: snapshotKey, occurrence })
|
|
335
335
|
if (i >= start) {
|
|
336
336
|
considered += 1
|
|
337
|
-
prepared.push({ m, role, content, occurrence, index: i })
|
|
337
|
+
prepared.push({ m, role, content, occurrence, index: i, untimestamped })
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
ordinalState.snapshot = currentSnapshot
|
|
341
341
|
ordinalState.seeded = true
|
|
342
|
-
for (const { m, role, content, occurrence, index } of prepared) {
|
|
342
|
+
for (const { m, role, content, occurrence, index, untimestamped } of prepared) {
|
|
343
343
|
const fallbackTs = Date.now() - (messages.length - index)
|
|
344
344
|
const rawTimestamp = m.ts ?? m.timestamp
|
|
345
345
|
const timeSource = untimestamped ? 'collected' : 'recorded'
|
|
@@ -286,6 +286,18 @@ export function cancelBackgroundTasks(options = {}) {
|
|
|
286
286
|
return { cancelled };
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
export function acknowledgeBackgroundTaskCompletion(taskId, options = {}) {
|
|
290
|
+
const task = getBackgroundTask(taskId, options);
|
|
291
|
+
if (!task || !TERMINAL_STATUSES.has(task.status)) return null;
|
|
292
|
+
// A terminal task read is an explicit consumer ACK. It wins over a racing
|
|
293
|
+
// async notify settlement and prevents any later reconcile from re-firing
|
|
294
|
+
// the same completion.
|
|
295
|
+
task.completionAcknowledged = true;
|
|
296
|
+
task.notified = true;
|
|
297
|
+
task.notifiedWithBody = true;
|
|
298
|
+
return task;
|
|
299
|
+
}
|
|
300
|
+
|
|
289
301
|
function resultTextForTask(task) {
|
|
290
302
|
if (task.resultText) return task.resultText;
|
|
291
303
|
if (task.result !== undefined) {
|
|
@@ -327,6 +339,7 @@ export function completeBackgroundTask(taskId, {
|
|
|
327
339
|
if (resultText != null) task.resultText = compactText(resultText);
|
|
328
340
|
if (error != null) task.error = presentErrorText(error, { surface: task.surface });
|
|
329
341
|
if (resultType) task.resultType = resultType;
|
|
342
|
+
if (instruction) task.notificationInstruction = compactText(instruction, 1_000);
|
|
330
343
|
if (notify) {
|
|
331
344
|
task.meta = sanitizeTaskMeta(task.meta);
|
|
332
345
|
notifyTaskCompletion(task, instruction);
|
|
@@ -337,6 +350,7 @@ export function completeBackgroundTask(taskId, {
|
|
|
337
350
|
export function notifyTaskCompletion(task, instruction) {
|
|
338
351
|
if (!task) return false;
|
|
339
352
|
if (!TERMINAL_STATUSES.has(task.status)) return false;
|
|
353
|
+
if (task.completionAcknowledged === true) return false;
|
|
340
354
|
// `notified` tracks whether *any* completion notification was sent (e.g. an
|
|
341
355
|
// early header-only preview from the agent surface). A later call that can
|
|
342
356
|
// finally include the result body must still fire once, otherwise the owner
|
|
@@ -364,6 +378,7 @@ export function notifyTaskCompletion(task, instruction) {
|
|
|
364
378
|
// marks, preserving exact-once.
|
|
365
379
|
onSettled: (delivered) => {
|
|
366
380
|
if (delivered) return;
|
|
381
|
+
if (task.completionAcknowledged === true) return;
|
|
367
382
|
task.notified = false;
|
|
368
383
|
task.notifiedWithBody = false;
|
|
369
384
|
},
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
cleanupBackgroundTasks,
|
|
9
|
+
completeBackgroundTask,
|
|
10
|
+
getBackgroundTask,
|
|
11
|
+
registerBackgroundTask,
|
|
12
|
+
renderBackgroundTask,
|
|
13
|
+
} from './background-tasks.mjs';
|
|
14
|
+
import { modelVisibleToolCompletionMessage } from './tool-execution-contract.mjs';
|
|
15
|
+
import { executeTaskTool } from '../agent/orchestrator/tools/builtin/task-tool.mjs';
|
|
16
|
+
import {
|
|
17
|
+
_dropPendingMessageState,
|
|
18
|
+
_settlePendingMessageWrites,
|
|
19
|
+
drainPendingMessages,
|
|
20
|
+
enqueuePendingMessage,
|
|
21
|
+
markCompletionEntry,
|
|
22
|
+
} from '../agent/orchestrator/session/manager/pending-messages.mjs';
|
|
23
|
+
import { _clearDeliveredCompletions } from '../agent/orchestrator/session/manager/delivered-completions.mjs';
|
|
24
|
+
|
|
25
|
+
test('terminal task read ACKs queued and racing completion notifications', async () => {
|
|
26
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-task-read-ack-'));
|
|
27
|
+
const previousDataDir = process.env.MIXDOG_DATA_DIR;
|
|
28
|
+
const sessionId = `sess_task_read_ack_${process.pid}`;
|
|
29
|
+
const taskId = `task_read_ack_${process.pid}`;
|
|
30
|
+
process.env.MIXDOG_DATA_DIR = root;
|
|
31
|
+
try {
|
|
32
|
+
const task = registerBackgroundTask({
|
|
33
|
+
taskId,
|
|
34
|
+
surface: 'shell',
|
|
35
|
+
operation: 'shell',
|
|
36
|
+
context: { callerSessionId: sessionId },
|
|
37
|
+
});
|
|
38
|
+
const instruction = `Async shell task ${taskId} (completed, exit 0) finished.`;
|
|
39
|
+
completeBackgroundTask(taskId, {
|
|
40
|
+
status: 'completed',
|
|
41
|
+
resultText: 'command output',
|
|
42
|
+
resultType: 'shell_task_result',
|
|
43
|
+
instruction,
|
|
44
|
+
notify: false,
|
|
45
|
+
});
|
|
46
|
+
const completionText = renderBackgroundTask(task, { includeResult: true });
|
|
47
|
+
const visible = modelVisibleToolCompletionMessage(completionText, {
|
|
48
|
+
type: 'shell_task_result',
|
|
49
|
+
execution_surface: 'shell',
|
|
50
|
+
execution_id: taskId,
|
|
51
|
+
status: 'completed',
|
|
52
|
+
instruction,
|
|
53
|
+
});
|
|
54
|
+
const entry = markCompletionEntry(visible, { executionId: taskId });
|
|
55
|
+
assert.ok(enqueuePendingMessage(sessionId, entry) > 0);
|
|
56
|
+
await _settlePendingMessageWrites();
|
|
57
|
+
|
|
58
|
+
assert.match(
|
|
59
|
+
await executeTaskTool({ action: 'read', task_id: taskId }, { sessionId }),
|
|
60
|
+
/status: completed/,
|
|
61
|
+
);
|
|
62
|
+
assert.equal(getBackgroundTask(taskId).completionAcknowledged, true);
|
|
63
|
+
|
|
64
|
+
// Covers both orderings: already queued before the read, and a fallback
|
|
65
|
+
// enqueue that races in after the read ACK.
|
|
66
|
+
assert.ok(enqueuePendingMessage(sessionId, entry) > 0);
|
|
67
|
+
assert.deepEqual(drainPendingMessages(sessionId), []);
|
|
68
|
+
await _settlePendingMessageWrites();
|
|
69
|
+
} finally {
|
|
70
|
+
_dropPendingMessageState(sessionId);
|
|
71
|
+
await _settlePendingMessageWrites().catch(() => {});
|
|
72
|
+
cleanupBackgroundTasks({ force: true });
|
|
73
|
+
_clearDeliveredCompletions();
|
|
74
|
+
if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
|
|
75
|
+
else process.env.MIXDOG_DATA_DIR = previousDataDir;
|
|
76
|
+
rmSync(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { isPidAlive } from './pid-liveness.mjs';
|
|
5
6
|
import { detachedSpawnOpts } from './spawn-flags.mjs';
|
|
6
7
|
|
|
7
8
|
function positiveInt(value) {
|
|
@@ -169,15 +170,6 @@ function sendBrokerMessage(message) {
|
|
|
169
170
|
}
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
function targetPidAlive(pid) {
|
|
173
|
-
try {
|
|
174
|
-
process.kill(pid, 0);
|
|
175
|
-
return true;
|
|
176
|
-
} catch (error) {
|
|
177
|
-
return error?.code === 'EPERM';
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
173
|
function stopBrokerTargetSweepIfIdle() {
|
|
182
174
|
if (brokerTargets.size > 0 || !brokerTargetSweepTimer) return;
|
|
183
175
|
clearInterval(brokerTargetSweepTimer);
|
|
@@ -198,7 +190,7 @@ function ensureBrokerTargetSweep() {
|
|
|
198
190
|
if (brokerTargetSweepTimer) return;
|
|
199
191
|
brokerTargetSweepTimer = setInterval(() => {
|
|
200
192
|
for (const [id, target] of brokerTargets) {
|
|
201
|
-
if (!
|
|
193
|
+
if (!isPidAlive(target.childPid)) removeBrokerTarget(id);
|
|
202
194
|
}
|
|
203
195
|
stopBrokerTargetSweepIfIdle();
|
|
204
196
|
}, 250);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Canonical process-liveness probe.
|
|
2
|
+
//
|
|
3
|
+
// Discovery files, singleton claims, transports, spawn guardians and the pg
|
|
4
|
+
// supervisor each used to carry their own byte-identical copy of this pair.
|
|
5
|
+
// The rule they all agreed on: a pid is only usable when it is a positive
|
|
6
|
+
// integer, and `process.kill(pid, 0)` failing with EPERM means the process
|
|
7
|
+
// EXISTS but belongs to another user (alive); ESRCH — and any other failure —
|
|
8
|
+
// means it is not addressable.
|
|
9
|
+
//
|
|
10
|
+
// Callers that must fail SAFE on an unknown error (treat anything but ESRCH as
|
|
11
|
+
// alive) keep their own probe on purpose; this is the fail-closed variant.
|
|
12
|
+
|
|
13
|
+
export function parsePid(value) {
|
|
14
|
+
const pid = Number(value);
|
|
15
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isPidAlive(value) {
|
|
19
|
+
const pid = parsePid(value);
|
|
20
|
+
if (!pid) return false;
|
|
21
|
+
try {
|
|
22
|
+
process.kill(pid, 0);
|
|
23
|
+
return true;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return error?.code === 'EPERM';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from 'node:fs';
|
|
7
7
|
import { tmpdir } from 'node:os';
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
9
|
+
import { isPidAlive } from './pid-liveness.mjs';
|
|
9
10
|
|
|
10
11
|
let cachedDefaultRoot = '';
|
|
11
12
|
|
|
@@ -15,17 +16,6 @@ function currentUid() {
|
|
|
15
16
|
return Number.isInteger(uid) && uid >= 0 ? uid : null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
function isPidAlive(value) {
|
|
19
|
-
const pid = Number(value);
|
|
20
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
21
|
-
try {
|
|
22
|
-
process.kill(pid, 0);
|
|
23
|
-
return true;
|
|
24
|
-
} catch (error) {
|
|
25
|
-
return error?.code === 'EPERM';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
19
|
function safeLiveLegacyRoot(root, uid) {
|
|
30
20
|
try {
|
|
31
21
|
const directory = lstatSync(root);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { readFileSync, unlinkSync } from 'node:fs'
|
|
14
14
|
import { join } from 'node:path'
|
|
15
15
|
import { writeJsonAtomicSync } from './atomic-file.mjs'
|
|
16
|
+
import { isPidAlive } from './pid-liveness.mjs'
|
|
16
17
|
import { ensurePrivateRuntimeRoot, resolveRuntimeRoot } from './runtime-root.mjs'
|
|
17
18
|
|
|
18
19
|
function discoveryRoot() {
|
|
@@ -23,23 +24,6 @@ export function discoveryPath(service) {
|
|
|
23
24
|
return join(discoveryRoot(), `${service}.json`)
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
function parsePid(value) {
|
|
27
|
-
const pid = Number(value)
|
|
28
|
-
return Number.isInteger(pid) && pid > 0 ? pid : null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function isPidAlive(value) {
|
|
32
|
-
const pid = parsePid(value)
|
|
33
|
-
if (!pid) return false
|
|
34
|
-
try {
|
|
35
|
-
process.kill(pid, 0)
|
|
36
|
-
return true
|
|
37
|
-
} catch (e) {
|
|
38
|
-
// EPERM: process exists but we lack permission → alive. ESRCH → dead.
|
|
39
|
-
return e?.code === 'EPERM'
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
27
|
// Per-process "unreachable port" distrust set: service → { port, updatedAt,
|
|
44
28
|
// pid, ts } a consumer WITHOUT its own health probe proved dead this process
|
|
45
29
|
// (connect-failure). pid-liveness alone cannot catch a recycled owner pid: the
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
|
|
4
|
-
export function isPidAlive(pid) {
|
|
5
|
-
const n = Number(pid);
|
|
6
|
-
if (!Number.isInteger(n) || n <= 0) return false;
|
|
7
|
-
try {
|
|
8
|
-
process.kill(n, 0);
|
|
9
|
-
return true;
|
|
10
|
-
} catch (error) {
|
|
11
|
-
return error?.code === 'EPERM';
|
|
12
|
-
}
|
|
13
|
-
}
|
|
3
|
+
import { isPidAlive } from './pid-liveness.mjs';
|
|
14
4
|
|
|
15
5
|
function readJson(path) {
|
|
16
6
|
try {
|
|
@@ -36,6 +36,8 @@ import { dirname, basename, join } from 'node:path';
|
|
|
36
36
|
import { fileURLToPath } from 'node:url';
|
|
37
37
|
import { spawn } from 'node:child_process';
|
|
38
38
|
import { resolvePluginData } from './plugin-paths.mjs';
|
|
39
|
+
// Same probe every discovery/lock reader uses; `process.pid` is trivially alive.
|
|
40
|
+
import { isPidAlive as pidAlive } from './pid-liveness.mjs';
|
|
39
41
|
import { detachedSpawnOpts, hiddenSpawnOpts } from './spawn-flags.mjs';
|
|
40
42
|
import { renameWithRetrySync } from './atomic-file.mjs';
|
|
41
43
|
import {
|
|
@@ -67,18 +69,6 @@ function rmDir(dir) {
|
|
|
67
69
|
try { rmSync(dir, { recursive: true, force: true }); } catch { /* best-effort */ }
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
function pidAlive(pid) {
|
|
71
|
-
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
72
|
-
if (pid === process.pid) return true;
|
|
73
|
-
try {
|
|
74
|
-
process.kill(pid, 0);
|
|
75
|
-
return true;
|
|
76
|
-
} catch (err) {
|
|
77
|
-
// EPERM = exists but not signalable → alive; ESRCH = gone.
|
|
78
|
-
return err?.code === 'EPERM';
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
72
|
function sleepSync(ms) {
|
|
83
73
|
const dur = Math.max(1, Number(ms) || 1);
|
|
84
74
|
try {
|
|
@@ -11,7 +11,11 @@ export function normalizeToolTerminalStatus(value) {
|
|
|
11
11
|
if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
|
|
12
12
|
if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
|
|
13
13
|
if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
|
|
14
|
-
|
|
14
|
+
// `cancel-unconfirmed` is task control's honest answer when a cancel was
|
|
15
|
+
// delivered but the process exit could not be confirmed. It is terminal for
|
|
16
|
+
// the tool call (the detail lives in the body), so it renders as cancelled
|
|
17
|
+
// rather than falling through to '' and leaving the surface with no status.
|
|
18
|
+
if (/^(cancelled|canceled|cancel|cancel[-_ ]unconfirmed|cancel[-_ ]pending|cancelling|canceling)$/.test(raw)) return 'cancelled';
|
|
15
19
|
if (/^(denied|deny|refused|rejected)$/.test(raw)) return 'denied';
|
|
16
20
|
return '';
|
|
17
21
|
}
|