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
|
@@ -56,17 +56,22 @@ let _kiwi = null
|
|
|
56
56
|
let _log = () => {}
|
|
57
57
|
let _initMs = 0
|
|
58
58
|
let _idleTimer = null
|
|
59
|
+
const _stemCache = new Map()
|
|
60
|
+
const STEM_CACHE_LIMIT = 256
|
|
59
61
|
// Idle window before the analyzer is released. A cold rebuild costs ~1.5-1.8s
|
|
60
62
|
// and BLOCKS the recall path (buildFtsQuery awaits it for Hangul queries), so a
|
|
61
|
-
// 60s
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
63
|
+
// 60s expired between ordinary conversational turns and made nearly every
|
|
64
|
+
// interactive recall pay that rebuild. Two minutes covers a normal follow-up
|
|
65
|
+
// while returning the analyzer's native memory promptly once recall goes quiet.
|
|
66
|
+
// MIXDOG_KO_MORPH_IDLE_TIMEOUT_MS overrides;
|
|
65
67
|
// 0 disables the release entirely.
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
?
|
|
69
|
-
|
|
68
|
+
export function _resolveKoMorphIdleTimeout(value) {
|
|
69
|
+
const configured = Number(value)
|
|
70
|
+
return Number.isFinite(configured) && configured >= 0 ? configured : 120_000
|
|
71
|
+
}
|
|
72
|
+
const IDLE_TIMEOUT_MS = _resolveKoMorphIdleTimeout(
|
|
73
|
+
process.env.MIXDOG_KO_MORPH_IDLE_TIMEOUT_MS,
|
|
74
|
+
)
|
|
70
75
|
|
|
71
76
|
export function isReady() { return _state === 'ready' && _kiwi != null }
|
|
72
77
|
export function state() { return _state }
|
|
@@ -80,6 +85,7 @@ function releaseLoaded(reason = '') {
|
|
|
80
85
|
_state = 'idle'
|
|
81
86
|
_initPromise = null
|
|
82
87
|
_initMs = 0
|
|
88
|
+
_stemCache.clear()
|
|
83
89
|
try { current?.dispose?.() } catch {}
|
|
84
90
|
if (current && reason) _log(`[memory-service] kiwi morph ${reason} — analyzer released\n`)
|
|
85
91
|
}
|
|
@@ -200,13 +206,20 @@ function readModelFiles(dir) {
|
|
|
200
206
|
for (const f of REQUIRED_MODEL_FILES) {
|
|
201
207
|
modelFiles[f] = new Uint8Array(fs.readFileSync(path.join(dir, f)))
|
|
202
208
|
}
|
|
203
|
-
for (const f of OPTIONAL_MODEL_FILES) {
|
|
204
|
-
const p = path.join(dir, f)
|
|
205
|
-
if (fs.existsSync(p)) modelFiles[f] = new Uint8Array(fs.readFileSync(p))
|
|
206
|
-
}
|
|
207
209
|
return modelFiles
|
|
208
210
|
}
|
|
209
211
|
|
|
212
|
+
export function _kiwiBuildArgs(modelFiles) {
|
|
213
|
+
return {
|
|
214
|
+
modelFiles,
|
|
215
|
+
modelType: 'knlm',
|
|
216
|
+
loadDefaultDict: true,
|
|
217
|
+
loadMultiDict: false,
|
|
218
|
+
loadTypoDict: false,
|
|
219
|
+
typos: 'none',
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
210
223
|
// Lazy, idempotent, never-throwing init. Fire-and-forget at boot.
|
|
211
224
|
export async function init(dataDir, log = () => {}) {
|
|
212
225
|
_log = typeof log === 'function' ? log : (() => {})
|
|
@@ -232,9 +245,10 @@ export async function init(dataDir, log = () => {}) {
|
|
|
232
245
|
const dir = await downloadAndExtractModel(dataDir)
|
|
233
246
|
const modelFiles = readModelFiles(dir)
|
|
234
247
|
const builder = await KiwiBuilder.create(wasmPath)
|
|
235
|
-
//
|
|
236
|
-
//
|
|
237
|
-
|
|
248
|
+
// Recall needs content stems, not typo correction or the 12 MB polysemous
|
|
249
|
+
// proper-noun dictionary. Keep the KNLM and default dictionary for Korean
|
|
250
|
+
// accuracy while leaving every optional model out of WASM memory.
|
|
251
|
+
_kiwi = await builder.build(_kiwiBuildArgs(modelFiles))
|
|
238
252
|
_state = 'ready'
|
|
239
253
|
_initMs = Date.now() - t0
|
|
240
254
|
_log(`[memory-service] kiwi morph ready in ${_initMs}ms (model ${KIWI_MODEL_VERSION}, rss≈${Math.round(process.memoryUsage().rss / 1e6)}MB)\n`)
|
|
@@ -266,6 +280,15 @@ export function analyze(text) {
|
|
|
266
280
|
// Content-morpheme stem forms for a Korean phrase. null when not ready.
|
|
267
281
|
// Example: an inflected Korean noun/verb phrase is reduced to content stems.
|
|
268
282
|
export function stems(text) {
|
|
283
|
+
const source = String(text ?? '')
|
|
284
|
+
if (!source) return null
|
|
285
|
+
const cached = _stemCache.get(source)
|
|
286
|
+
if (cached) {
|
|
287
|
+
_stemCache.delete(source)
|
|
288
|
+
_stemCache.set(source, cached)
|
|
289
|
+
touchIdleTimer()
|
|
290
|
+
return cached
|
|
291
|
+
}
|
|
269
292
|
const tokens = analyze(text)
|
|
270
293
|
if (!tokens) return null
|
|
271
294
|
const out = []
|
|
@@ -274,6 +297,14 @@ export function stems(text) {
|
|
|
274
297
|
const form = String(t.str || '').trim()
|
|
275
298
|
if (form.length >= 1) out.push(form)
|
|
276
299
|
}
|
|
300
|
+
if (source.length <= 4_096) {
|
|
301
|
+
_stemCache.set(source, out)
|
|
302
|
+
while (_stemCache.size > STEM_CACHE_LIMIT) {
|
|
303
|
+
const oldest = _stemCache.keys().next().value
|
|
304
|
+
if (oldest === undefined) break
|
|
305
|
+
_stemCache.delete(oldest)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
277
308
|
return out
|
|
278
309
|
}
|
|
279
310
|
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
CYCLE2_ACTIVE_TARGET_CAP,
|
|
22
22
|
} from './memory-cycle.mjs'
|
|
23
23
|
import { getInFlightCycle1 } from './memory-cycle1.mjs'
|
|
24
|
+
import { isStoreFault } from './memory-cycle2-shared.mjs'
|
|
24
25
|
import { pruneOldEntries } from './memory-maintenance-store.mjs'
|
|
25
26
|
import { computeEntryScore } from './memory-score.mjs'
|
|
26
27
|
import { runFullBackfill } from './memory-ops-policy.mjs'
|
|
@@ -869,7 +870,19 @@ export function createMemoryActionHandlers({
|
|
|
869
870
|
}
|
|
870
871
|
acted.add(targetId)
|
|
871
872
|
filteredSources.forEach(s => acted.add(Number(s)))
|
|
872
|
-
|
|
873
|
+
// Same optimistic contract as the archived/update verbs above: the
|
|
874
|
+
// gate's verdict is only applied while target and sources still
|
|
875
|
+
// hold the state it judged.
|
|
876
|
+
const expectedBySource = {}
|
|
877
|
+
for (const sourceId of filteredSources.map(Number)) {
|
|
878
|
+
const sourceRow = rowById.get(sourceId)
|
|
879
|
+
if (sourceRow) expectedBySource[sourceId] = { status: sourceRow.status, summary: sourceRow.summary }
|
|
880
|
+
}
|
|
881
|
+
const targetRow = rowById.get(targetId)
|
|
882
|
+
const moved = await applyMerge(db, targetId, filteredSources, {
|
|
883
|
+
expectedBySource,
|
|
884
|
+
expectedTarget: targetRow ? { status: targetRow.status, summary: targetRow.summary } : null,
|
|
885
|
+
})
|
|
873
886
|
if (moved > 0) {
|
|
874
887
|
merged += moved
|
|
875
888
|
if (typeof act.element === 'string' || typeof act.summary === 'string') {
|
|
@@ -883,6 +896,10 @@ export function createMemoryActionHandlers({
|
|
|
883
896
|
}
|
|
884
897
|
}
|
|
885
898
|
} catch (err) {
|
|
899
|
+
// Same split as the cycle2 apply path: a store fault stops the run
|
|
900
|
+
// (the DB state is unknown), while a guard-rejected verdict stays an
|
|
901
|
+
// ordinary counted error.
|
|
902
|
+
if (isStoreFault(err)) throw err
|
|
886
903
|
log(`[retro_eval_active] action error (id=${act?.entry_id}): ${err.message}\n`)
|
|
887
904
|
errors += 1
|
|
888
905
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { __mixdogMemoryLog } from './memory-log.mjs';
|
|
2
|
+
import { createSemaphore } from './memory-cycle2-shared.mjs';
|
|
2
3
|
|
|
3
4
|
import { cleanMemoryText } from './memory.mjs'
|
|
4
5
|
import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
|
|
@@ -278,24 +279,6 @@ function logCycle1Throttled(key, message, intervalMs = 60_000) {
|
|
|
278
279
|
__mixdogMemoryLog(message)
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
// Tiny inline semaphore — bounds cycle1 window fan-out.
|
|
282
|
-
function createSemaphore(limit) {
|
|
283
|
-
const cap = Math.max(1, Number(limit) || 1)
|
|
284
|
-
let active = 0
|
|
285
|
-
const queue = []
|
|
286
|
-
const release = () => {
|
|
287
|
-
active -= 1
|
|
288
|
-
const next = queue.shift()
|
|
289
|
-
if (next) next()
|
|
290
|
-
}
|
|
291
|
-
return async (fn) => {
|
|
292
|
-
if (active >= cap) await new Promise(res => queue.push(res))
|
|
293
|
-
active += 1
|
|
294
|
-
try { return await fn() }
|
|
295
|
-
finally { release() }
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
282
|
export function packCycle1Windows(rowsBySession, packetSize = CYCLE1_PACKET_MAX_ROWS, maxPackets = CYCLE1_MAX_PACKETS) {
|
|
300
283
|
const size = Math.min(CYCLE1_PACKET_MAX_ROWS, Math.max(1, Number(packetSize) || CYCLE1_PACKET_MAX_ROWS))
|
|
301
284
|
const cap = Math.min(CYCLE1_MAX_PACKETS, Math.max(1, Number(maxPackets) || CYCLE1_MAX_PACKETS))
|
|
@@ -7,7 +7,7 @@ import { join } from 'path'
|
|
|
7
7
|
import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
|
|
8
8
|
import { callAgentDispatch } from './agent-ipc.mjs'
|
|
9
9
|
import { listCore } from './core-memory-store.mjs'
|
|
10
|
-
import { __mixdogMemoryLog, throwIfAborted, resourceDir } from './memory-cycle2-shared.mjs'
|
|
10
|
+
import { __mixdogMemoryLog, throwIfAborted, resourceDir, createSemaphore } from './memory-cycle2-shared.mjs'
|
|
11
11
|
|
|
12
12
|
export const CYCLE2_ACTIVE_TARGET_CAP = 100
|
|
13
13
|
const CYCLE2_PACKET_MATERIAL_CAP = 50
|
|
@@ -213,23 +213,6 @@ export function packUnifiedGatePackets(rows, candidates, options = {}) {
|
|
|
213
213
|
return { packets, deferredIds }
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
function createSemaphore(limit) {
|
|
217
|
-
const cap = Math.max(1, Number(limit) || 1)
|
|
218
|
-
let active = 0
|
|
219
|
-
const queue = []
|
|
220
|
-
const release = () => {
|
|
221
|
-
active -= 1
|
|
222
|
-
const next = queue.shift()
|
|
223
|
-
if (next) next()
|
|
224
|
-
}
|
|
225
|
-
return async (fn) => {
|
|
226
|
-
if (active >= cap) await new Promise(resolve => queue.push(resolve))
|
|
227
|
-
active += 1
|
|
228
|
-
try { return await fn() }
|
|
229
|
-
finally { release() }
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
216
|
function formatLineageCandidates(rows, candidates) {
|
|
234
217
|
const lines = []
|
|
235
218
|
rows.forEach((row, index) => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { deleteRootEmbedding, syncRootEmbedding } from './memory-embed.mjs'
|
|
5
5
|
import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
|
|
6
6
|
import { callAgentDispatch } from './agent-ipc.mjs'
|
|
7
|
-
import { __mixdogMemoryLog, throwIfAborted } from './memory-cycle2-shared.mjs'
|
|
7
|
+
import { __mixdogMemoryLog, throwIfAborted, markStoreFault } from './memory-cycle2-shared.mjs'
|
|
8
8
|
|
|
9
9
|
const TIER1_THRESHOLD = 0.78
|
|
10
10
|
const TIER2_LOW = 0.65
|
|
@@ -268,74 +268,122 @@ export async function applyMerge(db, targetId, sourceIds, options = {}) {
|
|
|
268
268
|
const signal = options?.signal
|
|
269
269
|
throwIfAborted(signal)
|
|
270
270
|
if (!Number.isFinite(targetId)) return 0
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
271
|
+
// Optimistic guards keyed by source id: { status, summary } as observed when
|
|
272
|
+
// the merge verdict was produced (same contract as applyUpdate's `expected`).
|
|
273
|
+
const expectedBySource = options?.expectedBySource ?? null
|
|
274
|
+
const readExpected = (sid) => (
|
|
275
|
+
typeof expectedBySource?.get === 'function' ? expectedBySource.get(sid) : expectedBySource?.[sid]
|
|
276
|
+
) ?? null
|
|
277
|
+
// Snapshot of the merge TARGET as the verdict saw it. A merge is a statement
|
|
278
|
+
// about both rows, so the target has to be validated exactly like a source.
|
|
279
|
+
const expectedTarget = options?.expectedTarget ?? null
|
|
278
280
|
let moved = 0
|
|
279
281
|
for (const src of sourceIds) {
|
|
280
282
|
throwIfAborted(signal)
|
|
281
283
|
const sid = Number(src)
|
|
282
284
|
if (!Number.isFinite(sid) || sid === targetId) continue
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)
|
|
287
|
-
throwIfAborted(signal)
|
|
288
|
-
const srcRow = srcRes.rows[0]
|
|
289
|
-
if (!srcRow) continue
|
|
290
|
-
if (target.project_id !== srcRow.project_id) {
|
|
291
|
-
__mixdogMemoryLog(
|
|
292
|
-
`[cycle2] merge rejected: cross-pool (target=${targetId} project_id=${target.project_id ?? 'COMMON'} src=${sid} project_id=${srcRow.project_id ?? 'COMMON'})\n`,
|
|
293
|
-
)
|
|
294
|
-
continue
|
|
295
|
-
}
|
|
296
|
-
// Floor guard: archiving an active source row shrinks the active pool, so
|
|
297
|
-
// it must reserve from the shared floor budget. Over-budget merges are
|
|
298
|
-
// skipped (source stays active); pending sources never reserve.
|
|
299
|
-
const srcActive = srcRow.status === 'active'
|
|
300
|
-
if (srcActive && options.floorGuard && !options.floorGuard.reserve()) {
|
|
301
|
-
__mixdogMemoryLog(`[cycle2] merge source archive skipped (floor guard): target=${targetId} src=${sid}\n`)
|
|
302
|
-
continue
|
|
303
|
-
}
|
|
304
|
-
let archivedPrevStatus = null
|
|
285
|
+
let reserved = false
|
|
286
|
+
let archived = false
|
|
287
|
+
let skip = null
|
|
305
288
|
try {
|
|
306
289
|
// Archive is the guarded mutation unit: the reservation is only kept if
|
|
307
290
|
// this transaction commits. Embedding cleanup is deliberately OUTSIDE
|
|
308
291
|
// this try — a post-commit cleanup failure must NOT refund the budget,
|
|
309
292
|
// since the active row is already archived.
|
|
310
293
|
await db.transaction(async (tx) => {
|
|
294
|
+
// Target AND source are read inside the transaction and locked in id
|
|
295
|
+
// order (deterministic ordering, so two concurrent merges cannot
|
|
296
|
+
// deadlock). Reading them outside meant the verdict was applied to
|
|
297
|
+
// state that could already have changed content or status.
|
|
298
|
+
const { rows } = await tx.query(
|
|
299
|
+
`SELECT id, project_id, status, summary FROM entries
|
|
300
|
+
WHERE id = ANY($1::bigint[]) AND is_root = 1
|
|
301
|
+
ORDER BY id FOR UPDATE`,
|
|
302
|
+
[[targetId, sid]],
|
|
303
|
+
)
|
|
304
|
+
const target = rows.find((r) => Number(r.id) === Number(targetId))
|
|
305
|
+
const srcRow = rows.find((r) => Number(r.id) === sid)
|
|
306
|
+
if (!target) { skip = `target ${targetId} is not a root`; return }
|
|
307
|
+
if (!srcRow) { skip = `source ${sid} is not a root`; return }
|
|
308
|
+
if (target.project_id !== srcRow.project_id) {
|
|
309
|
+
skip = `cross-pool (target=${targetId} project_id=${target.project_id ?? 'COMMON'} src=${sid} project_id=${srcRow.project_id ?? 'COMMON'})`
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
// Target guards: folding a live source into a target that has since been
|
|
313
|
+
// archived (or rewritten) destroys the source without a surviving
|
|
314
|
+
// consolidated row.
|
|
315
|
+
if (target.status === 'archived') { skip = `target ${targetId} is archived`; return }
|
|
316
|
+
if (expectedTarget && typeof expectedTarget.status === 'string' && target.status !== expectedTarget.status) {
|
|
317
|
+
skip = `target ${targetId} status moved (${expectedTarget.status} → ${target.status})`
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
if (expectedTarget && typeof expectedTarget.summary === 'string' && target.summary !== expectedTarget.summary) {
|
|
321
|
+
skip = `target ${targetId} content changed since the verdict`
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
// Status guard: never re-archive an already archived source.
|
|
325
|
+
if (srcRow.status === 'archived') { skip = `source ${sid} already archived`; return }
|
|
326
|
+
// Content guard: the source must still say what the verdict judged.
|
|
327
|
+
const expected = readExpected(sid)
|
|
328
|
+
if (expected && typeof expected.status === 'string' && srcRow.status !== expected.status) {
|
|
329
|
+
skip = `source ${sid} status moved (${expected.status} → ${srcRow.status})`
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
if (expected && typeof expected.summary === 'string' && srcRow.summary !== expected.summary) {
|
|
333
|
+
skip = `source ${sid} content changed since the verdict`
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
// Floor guard: archiving an active source row shrinks the active pool,
|
|
337
|
+
// so it must reserve from the shared floor budget. Over-budget merges
|
|
338
|
+
// are skipped (source stays active); pending sources never reserve.
|
|
339
|
+
if (srcRow.status === 'active' && options.floorGuard) {
|
|
340
|
+
if (!options.floorGuard.reserve()) {
|
|
341
|
+
skip = `floor guard: target=${targetId} src=${sid}`
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
reserved = true
|
|
345
|
+
}
|
|
311
346
|
await tx.query(
|
|
312
347
|
`UPDATE entries SET chunk_root = $1, project_id = $2 WHERE chunk_root = $3 AND id != $4 AND is_root = 0`,
|
|
313
348
|
[targetId, target.project_id, sid, sid],
|
|
314
349
|
)
|
|
315
|
-
// Capture prev status in the same statement so a stale snapshot (the
|
|
316
|
-
// row was already archived by a concurrent write) can refund below.
|
|
317
350
|
const ar = await tx.query(
|
|
318
|
-
`
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
RETURNING pre.prev AS prev_status`,
|
|
322
|
-
[sid],
|
|
351
|
+
`UPDATE entries SET status = 'archived'
|
|
352
|
+
WHERE id = $1 AND is_root = 1 AND status IS NOT DISTINCT FROM $2::entry_status`,
|
|
353
|
+
[sid, srcRow.status],
|
|
323
354
|
)
|
|
324
|
-
|
|
355
|
+
archived = Number(ar.rowCount ?? ar.affectedRows ?? 0) > 0
|
|
325
356
|
})
|
|
326
357
|
} catch (err) {
|
|
327
|
-
|
|
358
|
+
// Store fault: the transaction rolled back, or its COMMIT outcome is
|
|
359
|
+
// unknown. The reservation is deliberately NOT refunded — the source may
|
|
360
|
+
// in fact be archived, so returning the budget would let a later archive
|
|
361
|
+
// spend it twice and breach the active floor. Marked so callers can tell
|
|
362
|
+
// it apart from the guard rejections above, which return normally.
|
|
363
|
+
if (reserved && options.floorGuard) {
|
|
364
|
+
__mixdogMemoryLog(`[cycle2] merge floor reservation withheld after store fault: target=${targetId} src=${sid}\n`)
|
|
365
|
+
}
|
|
328
366
|
__mixdogMemoryLog(`[cycle2] merge failed (target=${targetId} src=${sid}): ${err.message}\n`)
|
|
367
|
+
throw markStoreFault(err)
|
|
368
|
+
}
|
|
369
|
+
if (skip) __mixdogMemoryLog(`[cycle2] merge source skipped: ${skip}\n`)
|
|
370
|
+
if (!archived) {
|
|
371
|
+
if (reserved && options.floorGuard) options.floorGuard.refund()
|
|
329
372
|
continue
|
|
330
373
|
}
|
|
331
|
-
// Archive committed. If the source was no longer active at UPDATE time,
|
|
332
|
-
// the reservation did not cover a real active→archived transition — refund.
|
|
333
|
-
if (srcActive && options.floorGuard && archivedPrevStatus !== 'active') options.floorGuard.refund()
|
|
334
374
|
// Archive committed — best-effort embedding cleanup only. The next abort
|
|
335
375
|
// checkpoint is before the next source.
|
|
336
376
|
moved += 1
|
|
337
|
-
try {
|
|
338
|
-
|
|
377
|
+
try {
|
|
378
|
+
await deleteRootEmbedding(db, sid)
|
|
379
|
+
} catch (err) {
|
|
380
|
+
// The cleanup is a SECOND transaction, so its failure is a store fault of
|
|
381
|
+
// exactly the same kind — including the commit-ambiguous case. Absorbing
|
|
382
|
+
// it here kept archiving later sources against a store whose state is
|
|
383
|
+
// already unknown, which is the invariant the archive path enforces.
|
|
384
|
+
__mixdogMemoryLog(`[cycle2] merge embedding cleanup failed (target=${targetId} src=${sid}): ${err.message}\n`)
|
|
385
|
+
throw markStoreFault(err)
|
|
386
|
+
}
|
|
339
387
|
}
|
|
340
388
|
return moved
|
|
341
389
|
}
|
|
@@ -432,7 +480,12 @@ export async function runPhaseMerge(db, options = {}) {
|
|
|
432
480
|
const loser = keeper.id === a.id ? b : a
|
|
433
481
|
// phase_merge only pairs active rows, so the loser archive is an active
|
|
434
482
|
// demotion — thread the floor budget through applyMerge.
|
|
435
|
-
const moved = await applyMerge(db, keeper.id, [loser.id], {
|
|
483
|
+
const moved = await applyMerge(db, keeper.id, [loser.id], {
|
|
484
|
+
signal,
|
|
485
|
+
floorGuard,
|
|
486
|
+
expectedBySource: { [loser.id]: { status: loser.status, summary: loser.summary } },
|
|
487
|
+
expectedTarget: { status: keeper.status, summary: keeper.summary },
|
|
488
|
+
})
|
|
436
489
|
if (moved > 0) {
|
|
437
490
|
merged += moved
|
|
438
491
|
mergedIds.add(loser.id)
|
|
@@ -11,6 +11,77 @@ export function throwIfAborted(signal) {
|
|
|
11
11
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Tiny inline semaphore — bounds cycle fan-out (cycle1 windows, cycle2 gate
|
|
15
|
+
// packets). One implementation so the two concurrency caps cannot drift.
|
|
16
|
+
export function createSemaphore(limit) {
|
|
17
|
+
const cap = Math.max(1, Number(limit) || 1)
|
|
18
|
+
let active = 0
|
|
19
|
+
const queue = []
|
|
20
|
+
const release = () => {
|
|
21
|
+
active -= 1
|
|
22
|
+
const next = queue.shift()
|
|
23
|
+
if (next) next()
|
|
24
|
+
}
|
|
25
|
+
return async (fn) => {
|
|
26
|
+
if (active >= cap) await new Promise(resolve => queue.push(resolve))
|
|
27
|
+
active += 1
|
|
28
|
+
try { return await fn() }
|
|
29
|
+
finally { release() }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Two error classes travel through the cycle2 apply paths and must never be
|
|
34
|
+
// confused:
|
|
35
|
+
// * STORE FAULT — the database itself failed the write (transaction rolled
|
|
36
|
+
// back, or its COMMIT outcome is unknown). Nothing about the verdict was
|
|
37
|
+
// wrong, and the store's state is no longer known, so the run stops.
|
|
38
|
+
// * verdict rejection — a guard refused the mutation (stale snapshot, status
|
|
39
|
+
// moved, content changed, floor budget exhausted). The store is healthy;
|
|
40
|
+
// these return normally and are counted as ordinary rejections/errors.
|
|
41
|
+
// Only writers raise store faults, via markStoreFault; callers branch on
|
|
42
|
+
// isStoreFault before absorbing an error into a per-action counter.
|
|
43
|
+
// Classification is TYPE-based, never text-based: the writer raises a dedicated
|
|
44
|
+
// error class, and `err instanceof MemoryStoreFault` is the decision. An
|
|
45
|
+
// explicit own field (`isMemoryStoreFault` + `code`) corroborates it for the
|
|
46
|
+
// cross-realm case where two copies of this module exist. Nothing about the
|
|
47
|
+
// error's message or name participates — a foreign provider/DB error is never
|
|
48
|
+
// reclassified because of how its text happens to read, and a store fault keeps
|
|
49
|
+
// the original message verbatim so logs stay honest.
|
|
50
|
+
//
|
|
51
|
+
// Boundaries this signal actually crosses: none that serialize. The writer
|
|
52
|
+
// (applyMerge) throws and the deciders (the cycle2 apply loop and
|
|
53
|
+
// retro_eval_active) catch it in the same process, same module registry, same
|
|
54
|
+
// tick. runCycle2 converts it at its own boundary into a plain
|
|
55
|
+
// { ok: false, error: <string>, storeFault: <boolean> } result, so the explicit
|
|
56
|
+
// boolean — not a re-parsed Error — is what any IPC/worker hop carries onward.
|
|
57
|
+
export const MEMORY_STORE_FAULT_CODE = 'MEMORY_STORE_FAULT'
|
|
58
|
+
|
|
59
|
+
export class MemoryStoreFault extends Error {
|
|
60
|
+
constructor(message, options) {
|
|
61
|
+
super(message, options)
|
|
62
|
+
this.name = 'MemoryStoreFault'
|
|
63
|
+
this.code = MEMORY_STORE_FAULT_CODE
|
|
64
|
+
this.isMemoryStoreFault = true
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function markStoreFault(err) {
|
|
69
|
+
if (isStoreFault(err)) return err
|
|
70
|
+
const cause = err instanceof Error ? err : new Error(String(err))
|
|
71
|
+
// The original is never mutated (frozen/sealed store errors are ordinary):
|
|
72
|
+
// it is carried as `cause`, with its message copied verbatim — unprefixed —
|
|
73
|
+
// and its stack preserved so the failing statement stays visible.
|
|
74
|
+
const fault = new MemoryStoreFault(cause.message || String(cause), { cause })
|
|
75
|
+
if (typeof cause.stack === 'string') fault.stack = cause.stack
|
|
76
|
+
return fault
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isStoreFault(err) {
|
|
80
|
+
if (!err || typeof err !== 'object') return false
|
|
81
|
+
if (err instanceof MemoryStoreFault) return true
|
|
82
|
+
return err.isMemoryStoreFault === true && err.code === MEMORY_STORE_FAULT_CODE
|
|
83
|
+
}
|
|
84
|
+
|
|
14
85
|
export function resourceDir() {
|
|
15
86
|
return process.env.MIXDOG_ROOT
|
|
16
87
|
? join(process.env.MIXDOG_ROOT, 'src')
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
MEMORY_STORE_FAULT_CODE,
|
|
6
|
+
MemoryStoreFault,
|
|
7
|
+
isStoreFault,
|
|
8
|
+
markStoreFault,
|
|
9
|
+
} from './memory-cycle2-shared.mjs'
|
|
10
|
+
|
|
11
|
+
test('a foreign error is never classified by its text', () => {
|
|
12
|
+
// The exact former marker text, now owned by nobody: a provider/DB error that
|
|
13
|
+
// happens to read this way must stay an ordinary, recoverable action error.
|
|
14
|
+
const lookalikeMessage = new Error('MemoryStoreFault: connection reset by peer')
|
|
15
|
+
assert.equal(isStoreFault(lookalikeMessage), false)
|
|
16
|
+
|
|
17
|
+
const lookalikeName = new Error('connection reset by peer')
|
|
18
|
+
lookalikeName.name = 'MemoryStoreFault'
|
|
19
|
+
assert.equal(isStoreFault(lookalikeName), false)
|
|
20
|
+
|
|
21
|
+
const lookalikeBoth = new Error('MemoryStoreFault: rollback')
|
|
22
|
+
lookalikeBoth.name = 'MemoryStoreFault:Error'
|
|
23
|
+
assert.equal(isStoreFault(lookalikeBoth), false)
|
|
24
|
+
|
|
25
|
+
// A half-claimed field is not a claim either.
|
|
26
|
+
const halfClaim = new Error('nope')
|
|
27
|
+
halfClaim.isMemoryStoreFault = true
|
|
28
|
+
assert.equal(isStoreFault(halfClaim), false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('markStoreFault produces a typed fault and preserves the original', () => {
|
|
32
|
+
const original = new Error('rollback')
|
|
33
|
+
const fault = markStoreFault(original)
|
|
34
|
+
assert.ok(fault instanceof MemoryStoreFault)
|
|
35
|
+
assert.equal(isStoreFault(fault), true)
|
|
36
|
+
assert.equal(fault.code, MEMORY_STORE_FAULT_CODE)
|
|
37
|
+
// Message stays verbatim — it is not a classification channel.
|
|
38
|
+
assert.equal(fault.message, 'rollback')
|
|
39
|
+
assert.equal(fault.cause, original)
|
|
40
|
+
assert.equal(original.message, 'rollback')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('frozen, sealed and non-Error inputs still yield a typed fault', () => {
|
|
44
|
+
const frozen = Object.freeze(new Error('commit ambiguous'))
|
|
45
|
+
const fromFrozen = markStoreFault(frozen)
|
|
46
|
+
assert.equal(isStoreFault(fromFrozen), true)
|
|
47
|
+
assert.equal(fromFrozen.cause, frozen)
|
|
48
|
+
assert.equal(frozen.message, 'commit ambiguous')
|
|
49
|
+
|
|
50
|
+
const sealed = Object.seal(new Error('sealed write'))
|
|
51
|
+
assert.equal(isStoreFault(markStoreFault(sealed)), true)
|
|
52
|
+
|
|
53
|
+
const fromString = markStoreFault('boom')
|
|
54
|
+
assert.equal(isStoreFault(fromString), true)
|
|
55
|
+
assert.equal(fromString.message, 'boom')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('marking is idempotent and non-faults stay unclassified', () => {
|
|
59
|
+
const fault = markStoreFault(new Error('rollback'))
|
|
60
|
+
assert.equal(markStoreFault(fault), fault)
|
|
61
|
+
|
|
62
|
+
assert.equal(isStoreFault(new Error('guard mismatch')), false)
|
|
63
|
+
assert.equal(isStoreFault(null), false)
|
|
64
|
+
assert.equal(isStoreFault(undefined), false)
|
|
65
|
+
assert.equal(isStoreFault('MemoryStoreFault: not an error'), false)
|
|
66
|
+
assert.equal(isStoreFault({}), false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('an explicitly tagged cross-realm fault is recognised without instanceof', () => {
|
|
70
|
+
// Second copy of the module (different realm/registry): instanceof fails, the
|
|
71
|
+
// explicit field pair still classifies.
|
|
72
|
+
const crossRealm = Object.assign(new Error('rollback'), {
|
|
73
|
+
isMemoryStoreFault: true,
|
|
74
|
+
code: MEMORY_STORE_FAULT_CODE,
|
|
75
|
+
})
|
|
76
|
+
assert.equal(crossRealm instanceof MemoryStoreFault, false)
|
|
77
|
+
assert.equal(isStoreFault(crossRealm), true)
|
|
78
|
+
})
|
|
@@ -9,7 +9,7 @@ import { flushEmbeddingDirty } from './memory-embed.mjs'
|
|
|
9
9
|
import { refreshHotActive } from './memory.mjs'
|
|
10
10
|
import { backfillCoreEmbeddings, nominateCoreCandidates } from './core-memory-store.mjs'
|
|
11
11
|
import { markCycleRequest, consumeCycleRequests, resolveCoalesceMaxDrains, scheduleCoalescedCycleRetry, makeCycleRequestSignature, resolveCoalesceMaxRetries } from './memory-cycle-requests.mjs'
|
|
12
|
-
import { __mixdogMemoryLog, throwIfAborted } from './memory-cycle2-shared.mjs'
|
|
12
|
+
import { __mixdogMemoryLog, throwIfAborted, isStoreFault } from './memory-cycle2-shared.mjs'
|
|
13
13
|
import {
|
|
14
14
|
applyBatchStatusVerdicts, clampPendingPromotions, blockTransientPromotions, applySimpleStatus, applyUpdate, applyLineage, applyMerge, runPhaseMerge,
|
|
15
15
|
} from './memory-cycle2-mutations.mjs'
|
|
@@ -142,6 +142,16 @@ export async function runCycle2(db, config = {}, options = {}, dataDir = null) {
|
|
|
142
142
|
try {
|
|
143
143
|
result = await _runCycle2Impl(db, config, options, dataDir)
|
|
144
144
|
} catch (err) {
|
|
145
|
+
// A marked store fault means the database just failed (or ambiguously
|
|
146
|
+
// failed) a write: do not push another write — the coalesced-request
|
|
147
|
+
// ledger is a meta-table INSERT/UPDATE — onto it. Go straight to the
|
|
148
|
+
// { ok: false } boundary. The in-process retry timer is left intact so
|
|
149
|
+
// recovery still happens on a later, independent run.
|
|
150
|
+
if (isStoreFault(err)) {
|
|
151
|
+
__mixdogMemoryLog('[cycle2] store fault — skipping coalesced request mark\n')
|
|
152
|
+
if (coalescedRetry && retryAttempt < maxRetries) scheduleRetry()
|
|
153
|
+
throw err
|
|
154
|
+
}
|
|
145
155
|
if (coalescedRetry) {
|
|
146
156
|
await markCycleRequest(db, 'cycle2', 'retry-error', requestSignature)
|
|
147
157
|
if (retryAttempt < maxRetries) scheduleRetry()
|
|
@@ -162,6 +172,12 @@ export async function runCycle2(db, config = {}, options = {}, dataDir = null) {
|
|
|
162
172
|
const next = await _runCycle2Impl(db, config, options, dataDir)
|
|
163
173
|
result = mergeCycle2Results(result, next)
|
|
164
174
|
} catch (err) {
|
|
175
|
+
// Same rule on the drain pass: no ledger write after a store fault.
|
|
176
|
+
if (isStoreFault(err)) {
|
|
177
|
+
__mixdogMemoryLog('[cycle2] store fault — skipping coalesced drain mark\n')
|
|
178
|
+
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
179
|
+
throw err
|
|
180
|
+
}
|
|
165
181
|
await markCycleRequest(db, 'cycle2', 'drain-error', requestSignature)
|
|
166
182
|
if (!coalescedRetry || retryAttempt < maxRetries) scheduleRetry()
|
|
167
183
|
throw err
|
|
@@ -178,7 +194,10 @@ export async function runCycle2(db, config = {}, options = {}, dataDir = null) {
|
|
|
178
194
|
return okResult
|
|
179
195
|
} catch (e) {
|
|
180
196
|
if (signal?.aborted) throw signal.reason ?? e
|
|
181
|
-
|
|
197
|
+
// The store-fault outcome leaves this boundary as an explicit boolean, so
|
|
198
|
+
// no consumer downstream (IPC, worker, log pipeline) ever has to re-derive
|
|
199
|
+
// it from the error text.
|
|
200
|
+
return { ok: false, error: e.message, storeFault: isStoreFault(e), ...partial }
|
|
182
201
|
} finally {
|
|
183
202
|
let releaseErr = null
|
|
184
203
|
try {
|
|
@@ -540,7 +559,16 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
540
559
|
)
|
|
541
560
|
continue
|
|
542
561
|
}
|
|
543
|
-
|
|
562
|
+
// Pass the snapshot each verdict was computed against so applyMerge
|
|
563
|
+
// can refuse to archive a source whose status/content moved on.
|
|
564
|
+
const expectedBySource = {}
|
|
565
|
+
for (const sourceId of sourceIds.map(Number)) {
|
|
566
|
+
const sourceRow = rowsById.get(sourceId)
|
|
567
|
+
if (sourceRow) expectedBySource[sourceId] = { status: sourceRow.status, summary: sourceRow.summary }
|
|
568
|
+
}
|
|
569
|
+
const targetRow = rowsById.get(Number(targetId))
|
|
570
|
+
const expectedTarget = targetRow ? { status: targetRow.status, summary: targetRow.summary } : null
|
|
571
|
+
const moved = await applyMerge(db, targetId, sourceIds, { signal, floorGuard, expectedBySource, expectedTarget })
|
|
544
572
|
throwIfAborted(signal)
|
|
545
573
|
if (moved > 0) {
|
|
546
574
|
stats.merged += moved
|
|
@@ -561,6 +589,12 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
561
589
|
if (accepted) reviewedIds.push(id)
|
|
562
590
|
} catch (err) {
|
|
563
591
|
if (signal?.aborted) throw signal.reason ?? err
|
|
592
|
+
// A store fault leaves the database state unknown and the floor
|
|
593
|
+
// reservation permanently spent, so the cycle stops here exactly like
|
|
594
|
+
// phase_merge does (runCycle2 reports it as { ok: false }). Verdict
|
|
595
|
+
// rejections never land here — a guard mismatch or skipped merge
|
|
596
|
+
// returns normally and is counted as merge_rejected / error_count.
|
|
597
|
+
if (isStoreFault(err)) throw err
|
|
564
598
|
__mixdogMemoryLog(`[cycle2] action error (id=${id}): ${err.message}\n`)
|
|
565
599
|
}
|
|
566
600
|
}
|
|
@@ -527,7 +527,7 @@ async function _runCycle3Impl(db, config, dataDir, options = {}) {
|
|
|
527
527
|
const scope = core.project_id ? String(core.project_id) : 'common'
|
|
528
528
|
let queryVector = null
|
|
529
529
|
try {
|
|
530
|
-
queryVector = await embedText(queryText)
|
|
530
|
+
queryVector = await embedText(queryText, { inputType: 'query' })
|
|
531
531
|
} catch (err) {
|
|
532
532
|
if (signal?.aborted) throw signal.reason ?? err
|
|
533
533
|
__mixdogMemoryLog(`[cycle3] embedding failed for core id=${core.id}: ${err.message}\n`)
|