mixdog 0.9.95 → 0.9.97
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/README.md +0 -17
- package/package.json +16 -12
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +21 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +104 -41
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +36 -10
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +21 -29
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +7 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +45 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +71 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +116 -26
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +12 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +83 -33
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +42 -45
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +27 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +142 -26
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +47 -19
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +94 -20
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +12 -11
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +63 -9
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +34 -34
- package/src/runtime/channels/lib/inbound-handler.mjs +8 -8
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +76 -248
- package/src/runtime/memory/lib/agent-ipc.mjs +146 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +13 -34
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +9 -6
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +139 -72
- package/src/runtime/shared/config.mjs +120 -47
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +150 -100
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -27
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +47 -6
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +63 -39
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +45 -17
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +96 -24
- package/src/session-runtime/settings-api.mjs +12 -24
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/channel-admin.mjs +53 -64
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +33 -19
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +127 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +270 -154
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/session-client.mjs +1134 -0
- package/src/standalone/session-protocol.mjs +192 -0
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +481 -224
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +390 -121
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +34 -21
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +3 -4
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +48 -43
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +31 -33
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +10 -3
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +1031 -1676
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +12 -13
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +38 -4
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -20
- package/src/tui/{engine → session}/session-api.mjs +110 -93
- package/src/tui/{engine → session}/session-flow.mjs +8 -2
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +8 -6
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +13 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +38 -176
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +103 -103
- package/src/tui/session.mjs +22 -0
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-client.mjs +0 -894
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
- package/src/standalone/engine-daemon-protocol.mjs +0 -33
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
} from './tool-helpers.mjs';
|
|
24
24
|
import { isOnDeferredToolSurface, prepareDeferredToolCallThrough } from './deferred-call-through.mjs';
|
|
25
25
|
import { preDispatchDenyForSession, routeWebFetchCall } from './pre-dispatch-deny.mjs';
|
|
26
|
+
import { runWithToolExecutionOwner } from '../../../../shared/tool-execution-owner.mjs';
|
|
26
27
|
|
|
27
28
|
let codeGraphRuntimePromise = null;
|
|
28
29
|
async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
|
|
@@ -32,6 +33,11 @@ async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options
|
|
|
32
33
|
return mod.executeCodeGraphTool(name, args, cwd, signal, options);
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
export function resolveLiveToolCwd(cwd, sessionRef) {
|
|
37
|
+
const liveCwd = typeof sessionRef?.cwd === 'string' ? sessionRef.cwd : '';
|
|
38
|
+
return liveCwd || cwd;
|
|
39
|
+
}
|
|
40
|
+
|
|
35
41
|
export function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
|
|
36
42
|
if (executeOpts.scopedCacheOutcome) {
|
|
37
43
|
if (sessionRef && toolCallId) {
|
|
@@ -51,7 +57,16 @@ export function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, cal
|
|
|
51
57
|
return outcome;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
export
|
|
60
|
+
export function executeTool(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
|
|
61
|
+
return runWithToolExecutionOwner(callerSessionId, () =>
|
|
62
|
+
executeToolOwned(name, args, cwd, callerSessionId, sessionRef, executeOpts));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
|
|
66
|
+
// cwd is captured when the turn starts. The deferred cwd tool updates
|
|
67
|
+
// sessionRef.cwd in place, so every later tool call must re-read that live
|
|
68
|
+
// value instead of continuing to use the stale turn snapshot.
|
|
69
|
+
cwd = resolveLiveToolCwd(cwd, sessionRef);
|
|
55
70
|
const scopedCacheOutcome = _scopedCacheOutcomeForCall(
|
|
56
71
|
sessionRef,
|
|
57
72
|
executeOpts.toolCallId,
|
|
@@ -174,7 +189,10 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
|
|
|
174
189
|
&& mcpToolHasField(name, 'cwd')
|
|
175
190
|
&& (args == null || args.cwd == null);
|
|
176
191
|
const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
|
|
177
|
-
return executeMcpTool(name, finalArgs
|
|
192
|
+
return executeMcpTool(name, finalArgs, {
|
|
193
|
+
signal: executeOpts.signal || null,
|
|
194
|
+
ownerKey: callerSessionId,
|
|
195
|
+
});
|
|
178
196
|
}
|
|
179
197
|
if (name === 'code_graph') {
|
|
180
198
|
// cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
|
|
@@ -227,36 +245,10 @@ export async function executeTool(name, args, cwd, callerSessionId, sessionRef,
|
|
|
227
245
|
}
|
|
228
246
|
if (name === 'apply_patch') {
|
|
229
247
|
const patchArgs = typeof args === 'string' ? { patch: args } : { ...(args || {}) };
|
|
230
|
-
|
|
231
|
-
// one-shot shell path ONLY after the patch applies cleanly; a failed
|
|
232
|
-
// patch skips it. Patch text and shell output return as ONE tool
|
|
233
|
-
// result, so patch+verify costs a single call. Runtime-only knob:
|
|
234
|
-
// stripped before executePatchTool sees the args.
|
|
235
|
-
const postShell = typeof patchArgs.post_shell === 'string' && patchArgs.post_shell.trim()
|
|
236
|
-
? patchArgs.post_shell.trim()
|
|
237
|
-
: null;
|
|
238
|
-
delete patchArgs.post_shell;
|
|
239
|
-
const patchResult = await executePatchTool(name, patchArgs, cwd, {
|
|
248
|
+
return executePatchTool(name, patchArgs, cwd, {
|
|
240
249
|
sessionId: callerSessionId,
|
|
241
250
|
toolCallId: executeOpts.toolCallId || null,
|
|
242
251
|
});
|
|
243
|
-
if (!postShell) return patchResult;
|
|
244
|
-
const patchNorm = normalizeToolEnvelope(patchResult);
|
|
245
|
-
const patchText = typeof patchNorm.result === 'string' ? patchNorm.result : String(patchNorm.result ?? '');
|
|
246
|
-
// Text-based failure detection only: legacy string returns normalize
|
|
247
|
-
// to explicitSuccess:false even on success, so that flag is unusable here.
|
|
248
|
-
const patchFailed = /^Error[\s:[]/.test(patchText.trimStart());
|
|
249
|
-
if (patchFailed) {
|
|
250
|
-
return `${patchText}\n--- post_shell skipped: patch failed ---`;
|
|
251
|
-
}
|
|
252
|
-
const shellRes = await executeBuiltinTool('shell', { command: postShell }, cwd, completionToolOpts);
|
|
253
|
-
const shellNorm = normalizeToolEnvelope(shellRes);
|
|
254
|
-
const shellText = typeof shellNorm.result === 'string' ? shellNorm.result : String(shellNorm.result ?? '');
|
|
255
|
-
const shellFailed = /^Error[\s:[]/.test(shellText.trimStart());
|
|
256
|
-
const header = shellFailed
|
|
257
|
-
? '--- post_shell FAILED (patch is applied; fix and re-verify) ---'
|
|
258
|
-
: '--- post_shell ---';
|
|
259
|
-
return `${patchText}\n\n${header}\n${shellText}`;
|
|
260
252
|
}
|
|
261
253
|
if (isBuiltinTool(name)) {
|
|
262
254
|
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
isSkillDisabled,
|
|
13
13
|
} from '../../context/collect.mjs';
|
|
14
14
|
import { isAgentOwner } from '../../agent-owner.mjs';
|
|
15
|
-
import { _isMutationTool } from './tool-classify.mjs';
|
|
16
15
|
|
|
17
16
|
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
18
17
|
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
@@ -41,16 +40,14 @@ export function isEagerDispatchable(name, tools) {
|
|
|
41
40
|
return set.has(name);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
// Full
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// classifier used for dedup eligibility, steering, and post-mutation result
|
|
51
|
-
// invalidation.
|
|
43
|
+
// Full dispatch policy: EVERY valid tool call in an assistant turn is queued
|
|
44
|
+
// immediately. Mutations rely on their own conflict scope (apply_patch uses
|
|
45
|
+
// per-path locks); the eager dispatcher adds only the apply_patch -> shell
|
|
46
|
+
// execution barrier required for same-turn validation.
|
|
47
|
+
// isEagerDispatchable above remains the READ-ONLY classifier used for dedup
|
|
48
|
+
// eligibility, steering, and post-mutation result invalidation.
|
|
52
49
|
export function isParallelDispatchable(name) {
|
|
53
|
-
return typeof name === 'string' && name.length > 0
|
|
50
|
+
return typeof name === 'string' && name.length > 0;
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
// Read-only is necessary but not sufficient for result deduplication. Loader
|
|
@@ -67,6 +67,17 @@ import {
|
|
|
67
67
|
import { _getAgentLoop } from './runtime-loaders.mjs';
|
|
68
68
|
import { getAgentRuntimeSync } from './agent-runtime-singleton.mjs';
|
|
69
69
|
import { recordProviderContextBaseline } from '../loop/compact-policy.mjs';
|
|
70
|
+
import { runAbortable, settleWithin, throwIfAborted } from '../../../../shared/abort-race.mjs';
|
|
71
|
+
|
|
72
|
+
export const DEFAULT_ASK_CLEANUP_SETTLE_MS = 2_000;
|
|
73
|
+
|
|
74
|
+
export async function settleAskCleanup(promise, { timeoutMs } = {}) {
|
|
75
|
+
const configured = Number(process.env.MIXDOG_ASK_CLEANUP_SETTLE_MS);
|
|
76
|
+
const budget = Number.isFinite(Number(timeoutMs)) && Number(timeoutMs) > 0
|
|
77
|
+
? Number(timeoutMs)
|
|
78
|
+
: (Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_ASK_CLEANUP_SETTLE_MS);
|
|
79
|
+
return await settleWithin(promise, budget);
|
|
80
|
+
}
|
|
70
81
|
|
|
71
82
|
export function persistedAssistantTranscriptMetadata(value, fallbackAt = Date.now()) {
|
|
72
83
|
if (!value || typeof value !== 'object') return null;
|
|
@@ -196,7 +207,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
196
207
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
197
208
|
process.stderr.write(`[agent-trace] t0-ask-start sessionHash=${createHash('sha256').update(String(sessionId)).digest('hex').slice(0, 8)} role=? iteration=0 promptSrc=${_promptSrc} prefetchFiles=${_prefetchFiles} callers=${_prefetchCallers} references=${_prefetchRefs}\n`);
|
|
198
209
|
}
|
|
199
|
-
const unlock = await acquireSessionLock(sessionId);
|
|
210
|
+
const unlock = await acquireSessionLock(sessionId, askOpts?.signal);
|
|
200
211
|
// Start crash-spool hydration without delaying the user turn. A completed
|
|
201
212
|
// background hydration joins the id-deduped memory drain below.
|
|
202
213
|
const takeoverHydration = hydratePendingMessages(sessionId);
|
|
@@ -309,9 +320,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
309
320
|
// computation would run detached. Capture the linked signal, install the
|
|
310
321
|
// fresh controller, then re-cascade it — aborting the new controller
|
|
311
322
|
// immediately when the parent already fired, or re-arming the listener.
|
|
312
|
-
const _linkedParentSignal = runtime.parentAbortLink?.signal;
|
|
323
|
+
const _linkedParentSignal = askOpts?.signal || runtime.parentAbortLink?.signal;
|
|
313
324
|
// Fresh controller per ask — the previous ask's controller may have aborted.
|
|
314
325
|
runtime.controller = createAbortController();
|
|
326
|
+
const turnSignal = runtime.controller.signal;
|
|
315
327
|
runtime.generation = askGeneration;
|
|
316
328
|
runtime.closed = false;
|
|
317
329
|
runtime.session = preSession;
|
|
@@ -469,7 +481,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
469
481
|
// even to the real task. Single-turn structure with a labelled
|
|
470
482
|
// `# Task` block forces the model to treat the brief as the work
|
|
471
483
|
// unit, not as another piece of context to ack.
|
|
472
|
-
const explicitPrefetchResult = await _tryBridgeExplicitPrefetch(session, explicitPrefetch);
|
|
484
|
+
const explicitPrefetchResult = await _tryBridgeExplicitPrefetch(session, explicitPrefetch, turnSignal);
|
|
473
485
|
let _contextBlock = '';
|
|
474
486
|
if (context) {
|
|
475
487
|
_contextBlock += `# Additional context\n${_capCtx(context)}\n\n`;
|
|
@@ -574,7 +586,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
574
586
|
},
|
|
575
587
|
});
|
|
576
588
|
} catch { /* trace must never break the ask path */ }
|
|
577
|
-
const agentLoop = await _getAgentLoop();
|
|
589
|
+
const agentLoop = await runAbortable(turnSignal, () => _getAgentLoop());
|
|
578
590
|
const _trackTextDelta = (chunk) => {
|
|
579
591
|
_turnInterruption.recordTextDelta(chunk);
|
|
580
592
|
_scheduleTurnCheckpoint();
|
|
@@ -645,10 +657,15 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
645
657
|
onProviderSendStarted: () => {
|
|
646
658
|
_turnInterruption.markProviderSendStarted();
|
|
647
659
|
_scheduleTurnCheckpoint(true);
|
|
660
|
+
try { askOpts?.onProviderSendStarted?.(); } catch {}
|
|
648
661
|
},
|
|
649
662
|
onToolPhaseStarted: () => {
|
|
650
663
|
_turnInterruption.markToolPhaseStarted();
|
|
651
664
|
_scheduleTurnCheckpoint(true);
|
|
665
|
+
try { askOpts?.onToolPhaseStarted?.(); } catch {}
|
|
666
|
+
},
|
|
667
|
+
onToolPhaseCompleted: (detail) => {
|
|
668
|
+
try { askOpts?.onToolPhaseCompleted?.(detail); } catch {}
|
|
652
669
|
},
|
|
653
670
|
onUsageDelta: (d) => {
|
|
654
671
|
persistIterationMetrics(d).catch(() => {});
|
|
@@ -761,6 +778,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
761
778
|
session.toolApprovalHook = priorToolApprovalHook;
|
|
762
779
|
}
|
|
763
780
|
}
|
|
781
|
+
throwIfAborted(turnSignal);
|
|
764
782
|
// Post-loop validation: if closeSession() landed while we were awaiting,
|
|
765
783
|
// drop the save so the tombstone on disk isn't overwritten.
|
|
766
784
|
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
@@ -895,7 +913,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
895
913
|
// Unified total-prompt field. Anthropic = input+cache_read+cache_write
|
|
896
914
|
// (additive); OpenAI OAuth/API/Gemini = input_tokens already includes the
|
|
897
915
|
// cached portion (inclusive), so the fallback must not double-count.
|
|
898
|
-
const { isInclusiveProvider, computeCostUsd } = await
|
|
916
|
+
const { isInclusiveProvider, computeCostUsd } = await runAbortable(
|
|
917
|
+
turnSignal,
|
|
918
|
+
() => import('../../../../shared/llm/cost.mjs'),
|
|
919
|
+
);
|
|
899
920
|
const inclusive = isInclusiveProvider(session.provider);
|
|
900
921
|
const promptTokens = typeof result.usage.promptTokens === 'number'
|
|
901
922
|
? result.usage.promptTokens
|
|
@@ -1037,15 +1058,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1037
1058
|
}
|
|
1038
1059
|
try {
|
|
1039
1060
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1040
|
-
|
|
1041
|
-
|
|
1061
|
+
const cleanup = finalized.responsePreserved
|
|
1062
|
+
? finalizePendingMessageDelivery(
|
|
1042
1063
|
activeSession, _turnPendingEntries, durableSave,
|
|
1043
1064
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1044
|
-
)
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
1065
|
+
)
|
|
1066
|
+
: durableSave;
|
|
1067
|
+
const cleanupResult = await settleAskCleanup(cleanup);
|
|
1068
|
+
if (cleanupResult.settled) clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
1049
1069
|
} catch { /* cancellation cleanup is best-effort */ }
|
|
1050
1070
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
1051
1071
|
} else releasePendingMessages(sessionId, _turnPendingEntries);
|
|
@@ -1083,35 +1103,34 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1083
1103
|
else releasePendingMessages(sessionId, _turnPendingEntries);
|
|
1084
1104
|
try {
|
|
1085
1105
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1086
|
-
|
|
1087
|
-
|
|
1106
|
+
const cleanup = finalized.responsePreserved
|
|
1107
|
+
? finalizePendingMessageDelivery(
|
|
1088
1108
|
activeSession, _turnPendingEntries, durableSave,
|
|
1089
1109
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1090
|
-
)
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
_errorStateDurable = true;
|
|
1110
|
+
)
|
|
1111
|
+
: durableSave;
|
|
1112
|
+
_errorStateDurable = (await settleAskCleanup(cleanup)).settled;
|
|
1095
1113
|
} catch { /* provider-failure history persistence is best-effort */ }
|
|
1096
1114
|
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
1097
1115
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
1098
1116
|
} else {
|
|
1099
|
-
const
|
|
1117
|
+
const compactPersist = await settleAskCleanup(persistCompactedOutgoingAfterAskFailure({
|
|
1100
1118
|
sessionId,
|
|
1101
1119
|
activeSession,
|
|
1102
1120
|
askGeneration,
|
|
1103
1121
|
turnOutgoing: _turnOutgoing,
|
|
1104
1122
|
error: err,
|
|
1105
|
-
});
|
|
1123
|
+
}));
|
|
1124
|
+
const promptPersisted = compactPersist.settled && compactPersist.value === true;
|
|
1106
1125
|
if (promptPersisted) {
|
|
1107
1126
|
recordPendingMessageDelivery(activeSession, _turnPendingEntries);
|
|
1108
1127
|
try {
|
|
1109
1128
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1110
|
-
|
|
1129
|
+
const cleanup = finalizePendingMessageDelivery(
|
|
1111
1130
|
activeSession, _turnPendingEntries, durableSave,
|
|
1112
1131
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1113
1132
|
);
|
|
1114
|
-
_errorStateDurable =
|
|
1133
|
+
_errorStateDurable = (await settleAskCleanup(cleanup)).settled;
|
|
1115
1134
|
} catch {}
|
|
1116
1135
|
} else {
|
|
1117
1136
|
releasePendingMessages(sessionId, _turnPendingEntries);
|
|
@@ -1119,8 +1138,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1119
1138
|
}
|
|
1120
1139
|
if (!_errorStateDurable && activeSession) {
|
|
1121
1140
|
try {
|
|
1122
|
-
await
|
|
1123
|
-
|
|
1141
|
+
_errorStateDurable = (await settleAskCleanup(
|
|
1142
|
+
saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1143
|
+
)).settled;
|
|
1124
1144
|
} catch { /* retain checkpoint when canonical persistence fails */ }
|
|
1125
1145
|
}
|
|
1126
1146
|
if (_errorStateDurable) clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
@@ -77,8 +77,7 @@ function addCompactUsageToSession(session, usage) {
|
|
|
77
77
|
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
78
78
|
session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
|
|
79
79
|
}
|
|
80
|
-
// A
|
|
81
|
-
// and retry once (memory-runtime-proxy.mjs) — that must NEVER wedge the
|
|
80
|
+
// A stalled memory call must NEVER wedge the
|
|
82
81
|
// compact//clear path. Bound every recall-fasttrack memory call with a short
|
|
83
82
|
// local timeout: on timeout we abort (best-effort cancel via a chained signal)
|
|
84
83
|
// and treat it exactly like an RPC failure, so compaction proceeds WITHOUT
|
|
@@ -93,7 +92,7 @@ function recallMemoryTimeoutMs(session) {
|
|
|
93
92
|
// misconfigured tiny value can't turn the bound into a busy no-wait.
|
|
94
93
|
return Math.max(250, configured || RECALL_MEMORY_CALL_TIMEOUT_MS);
|
|
95
94
|
}
|
|
96
|
-
// Cold-start allowance (clear/manual path only): a booting memory
|
|
95
|
+
// Cold-start allowance (clear/manual path only): a booting memory runtime can
|
|
97
96
|
// miss the tight first bound (waitForPort + first-RPC warmup ~2-10s). On a
|
|
98
97
|
// timeout we retry ONCE with a longer bound before honoring the bail-to-
|
|
99
98
|
// semantic contract, so a rebooting runtime succeeds instead of instantly
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Steering / pending-message queue with sync buffered + atomic-file persistence.
|
|
2
2
|
// Extracted verbatim from manager.mjs (behavior-preserving).
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { readFileSync
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { stat } from 'fs/promises';
|
|
5
6
|
import { createHash, randomBytes } from 'crypto';
|
|
6
7
|
import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
7
8
|
import { updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
|
|
@@ -20,8 +21,8 @@ const PENDING_ORPHAN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
20
21
|
const PENDING_ORPHAN_GRACE_MS = 60 * 60 * 1000;
|
|
21
22
|
// Replay window for genuine user/steering entries. A cross-surface submit is
|
|
22
23
|
// meant for a LIVE owner; entries that predate this process and exceeded the
|
|
23
|
-
// window are still DELIVERED (
|
|
24
|
-
//
|
|
24
|
+
// window are still DELIVERED (queued user input is never silently
|
|
25
|
+
// discarded), but annotated with an
|
|
25
26
|
// explicit late-delivery header so a session resumed hours later reads them
|
|
26
27
|
// as clearly-late input instead of a surprise self-injection.
|
|
27
28
|
const STALE_USER_INJECTION_TTL_MS = 30 * 60 * 1000;
|
|
@@ -44,7 +45,7 @@ function isStaleUserInjection(entry, now = Date.now()) {
|
|
|
44
45
|
return (now - enqueuedAt) > STALE_USER_INJECTION_TTL_MS;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
//
|
|
48
|
+
// Never silently discard queued user input. A stale entry is
|
|
48
49
|
// delivered with this explicit age-annotated header so neither the user nor
|
|
49
50
|
// the model mistakes it for fresh input.
|
|
50
51
|
function lateDeliveryText(text, entry, now = Date.now()) {
|
|
@@ -823,7 +824,7 @@ export function hydratePendingMessages(sessionId, options = {}) {
|
|
|
823
824
|
if (!id || inDelivery.has(id) || acked.has(id)) return false;
|
|
824
825
|
return true;
|
|
825
826
|
});
|
|
826
|
-
//
|
|
827
|
+
// Stale genuine user/steering entries DELIVER with a
|
|
827
828
|
// late-delivery header instead of being silently dropped (the old
|
|
828
829
|
// behavior discarded them; user report: remote/steering sends
|
|
829
830
|
// silently ignored around owner restarts). Completion entries
|
|
@@ -1092,8 +1093,14 @@ export function enqueueRemotePendingMessage(sessionId, message) {
|
|
|
1092
1093
|
// several sessions (desktop tabs, TUI + engine hosts) and a single shared
|
|
1093
1094
|
// counter let the first drain of a tick swallow the mtime bump for every
|
|
1094
1095
|
// other session, stranding their foreign submits until the next spool write.
|
|
1095
|
-
const FOREIGN_SPOOL_SCAN_LIMIT =
|
|
1096
|
+
const FOREIGN_SPOOL_SCAN_LIMIT = Math.max(
|
|
1097
|
+
128,
|
|
1098
|
+
Number(process.env.MIXDOG_FOREIGN_SPOOL_SCAN_LIMIT) || 512,
|
|
1099
|
+
);
|
|
1096
1100
|
const _foreignSpoolScanMtimes = new Map();
|
|
1101
|
+
const _foreignDrainRequests = new Map();
|
|
1102
|
+
let _foreignDrainScheduled = false;
|
|
1103
|
+
let _foreignDrainRunning = false;
|
|
1097
1104
|
|
|
1098
1105
|
function _rememberForeignSpoolScan(sessionId, mtime) {
|
|
1099
1106
|
_foreignSpoolScanMtimes.delete(sessionId);
|
|
@@ -1105,28 +1112,7 @@ function _rememberForeignSpoolScan(sessionId, mtime) {
|
|
|
1105
1112
|
}
|
|
1106
1113
|
}
|
|
1107
1114
|
|
|
1108
|
-
|
|
1109
|
-
* Owner-side drain of FOREIGN user injections for a session this process
|
|
1110
|
-
* owns: atomically removes (and returns the text of) genuine user/steering
|
|
1111
|
-
* entries that were persisted by ANOTHER process — entries known locally
|
|
1112
|
-
* (own steering buffers, hydrated, in-delivery, acked) and completion/
|
|
1113
|
-
* internal-notification entries are left untouched for the normal
|
|
1114
|
-
* askSession hydrate path.
|
|
1115
|
-
*/
|
|
1116
|
-
export async function drainForeignUserInjections(sessionId) {
|
|
1117
|
-
if (!isValidPendingSessionId(sessionId)) return [];
|
|
1118
|
-
// DELIVERY path, not cleanup: this takes rows out of the spool and hands
|
|
1119
|
-
// them to a live turn. A tombstoned (or otherwise invalidated) session may
|
|
1120
|
-
// neither receive nor lose input, so the tombstone-TOLERANT epoch check
|
|
1121
|
-
// used by the explicit cleanup mutations (ack / clear) is wrong here — the
|
|
1122
|
-
// token is refused outright when it was captured from a closed session.
|
|
1123
|
-
// Checked BEFORE the mtime memo so a later reopen still sees this spool
|
|
1124
|
-
// state as unscanned.
|
|
1125
|
-
const epochToken = currentPendingLifecycleToken(sessionId);
|
|
1126
|
-
if (pendingLifecycleInvalidated(sessionId)) return [];
|
|
1127
|
-
let mtime = 0;
|
|
1128
|
-
try { mtime = statSync(pendingMessagesPath()).mtimeMs || 0; } catch { return []; }
|
|
1129
|
-
if (_foreignSpoolScanMtimes.get(sessionId) === mtime) return [];
|
|
1115
|
+
function _foreignLocalIds(sessionId) {
|
|
1130
1116
|
const localIds = new Set();
|
|
1131
1117
|
for (const map of [_sessionPendingMessages, _pendingPersistBuffers, _hydratedPendingMessages]) {
|
|
1132
1118
|
for (const entry of map.get(sessionId) || []) {
|
|
@@ -1146,59 +1132,86 @@ export async function drainForeignUserInjections(sessionId) {
|
|
|
1146
1132
|
if (typeof id === 'string' && id) localIds.add(id);
|
|
1147
1133
|
}
|
|
1148
1134
|
} catch { /* ledger unavailable — in-memory sets still guard */ }
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1135
|
+
return localIds;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function _settleForeignDrain(request, value) {
|
|
1139
|
+
for (const resolve of request.waiters) {
|
|
1140
|
+
try { resolve(value); } catch { /* a consumer cannot break the batch */ }
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function _scheduleForeignDrainBatch() {
|
|
1145
|
+
if (_foreignDrainScheduled || _foreignDrainRunning || _foreignDrainRequests.size === 0) return;
|
|
1146
|
+
_foreignDrainScheduled = true;
|
|
1147
|
+
setImmediate(() => {
|
|
1148
|
+
_foreignDrainScheduled = false;
|
|
1149
|
+
void _flushForeignDrainBatch();
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
async function _flushForeignDrainBatch() {
|
|
1154
|
+
if (_foreignDrainRunning) return;
|
|
1155
|
+
_foreignDrainRunning = true;
|
|
1156
|
+
const batch = [..._foreignDrainRequests.values()];
|
|
1157
|
+
_foreignDrainRequests.clear();
|
|
1158
1158
|
try {
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1159
|
+
let mtime = 0;
|
|
1160
|
+
try { mtime = (await stat(pendingMessagesPath())).mtimeMs || 0; }
|
|
1161
|
+
catch {
|
|
1162
|
+
for (const request of batch) _settleForeignDrain(request, []);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
const candidates = batch.filter((request) =>
|
|
1166
|
+
_foreignSpoolScanMtimes.get(request.sessionId) !== mtime
|
|
1167
|
+
&& !pendingLifecycleInvalidated(request.sessionId, request.epochToken));
|
|
1168
|
+
for (const request of batch) {
|
|
1169
|
+
if (!candidates.includes(request)) _settleForeignDrain(request, []);
|
|
1170
|
+
}
|
|
1171
|
+
if (candidates.length === 0) return;
|
|
1172
|
+
for (const request of candidates) {
|
|
1173
|
+
request.localIds = _foreignLocalIds(request.sessionId);
|
|
1174
|
+
request.taken = [];
|
|
1175
|
+
request.lifecycleDecided = false;
|
|
1176
|
+
}
|
|
1165
1177
|
await updateJsonAtomic(pendingMessagesPath(), (raw) => {
|
|
1166
|
-
// Same authority re-read INSIDE the spool lock: a close/detach
|
|
1167
|
-
// landing in this window must neither deliver to the old owner nor
|
|
1168
|
-
// remove the reopened owner's rows.
|
|
1169
|
-
runPendingTestHook('foreignDrain:beforeCommit', { sessionId });
|
|
1170
|
-
if (pendingLifecycleInvalidated(sessionId, epochToken)) return undefined;
|
|
1171
|
-
// Authoritative from here on: whatever this pass decides (take,
|
|
1172
|
-
// drop-stale, or "nothing foreign here") is a real answer for THIS
|
|
1173
|
-
// spool state, so the memo may skip re-scanning it.
|
|
1174
|
-
lifecycleDecided = true;
|
|
1175
1178
|
const next = normalizePendingStore(raw);
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1179
|
+
let changed = false;
|
|
1180
|
+
for (const request of candidates) {
|
|
1181
|
+
const { sessionId, epochToken, localIds, taken } = request;
|
|
1182
|
+
// Same authority re-read INSIDE the spool lock: a close/detach
|
|
1183
|
+
// landing in this window must neither deliver to the old owner
|
|
1184
|
+
// nor remove the reopened owner's rows.
|
|
1185
|
+
runPendingTestHook('foreignDrain:beforeCommit', { sessionId });
|
|
1186
|
+
if (pendingLifecycleInvalidated(sessionId, epochToken)) continue;
|
|
1187
|
+
request.lifecycleDecided = true;
|
|
1188
|
+
const q = Array.isArray(next.sessions[sessionId]) ? next.sessions[sessionId] : [];
|
|
1189
|
+
if (q.length === 0) continue;
|
|
1190
|
+
const kept = [];
|
|
1191
|
+
for (const entry of q) {
|
|
1192
|
+
const id = pendingMessageId(entry);
|
|
1193
|
+
const text = pendingMessageText(entry);
|
|
1194
|
+
const foreignUser = id && !localIds.has(id)
|
|
1195
|
+
&& !isCompletionNotificationEntry(entry)
|
|
1196
|
+
&& !isLegacyUnmarkedCompletionNotification(text)
|
|
1197
|
+
&& text && !isInternalRuntimeNotificationText(text);
|
|
1198
|
+
if (foreignUser && isStaleUserInjection(entry)) {
|
|
1199
|
+
taken.push({ text: lateDeliveryText(text, entry), id });
|
|
1200
|
+
} else if (foreignUser) {
|
|
1201
|
+
taken.push({ text, id });
|
|
1202
|
+
} else {
|
|
1203
|
+
kept.push(entry);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (taken.length === 0) continue;
|
|
1207
|
+
changed = true;
|
|
1208
|
+
if (kept.length > 0) next.sessions[sessionId] = kept;
|
|
1209
|
+
else {
|
|
1210
|
+
delete next.sessions[sessionId];
|
|
1211
|
+
if (next.sessionTouchedAt) delete next.sessionTouchedAt[sessionId];
|
|
1192
1212
|
}
|
|
1193
|
-
else if (foreignUser) taken.push({ text, id });
|
|
1194
|
-
else kept.push(entry);
|
|
1195
|
-
}
|
|
1196
|
-
if (taken.length === 0) return undefined;
|
|
1197
|
-
if (kept.length > 0) next.sessions[sessionId] = kept;
|
|
1198
|
-
else {
|
|
1199
|
-
delete next.sessions[sessionId];
|
|
1200
|
-
if (next.sessionTouchedAt) delete next.sessionTouchedAt[sessionId];
|
|
1201
1213
|
}
|
|
1214
|
+
if (!changed) return undefined;
|
|
1202
1215
|
next.updatedAt = Date.now();
|
|
1203
1216
|
return next;
|
|
1204
1217
|
}, {
|
|
@@ -1206,18 +1219,63 @@ export async function drainForeignUserInjections(sessionId) {
|
|
|
1206
1219
|
lock: true,
|
|
1207
1220
|
mode: PENDING_MESSAGES_MODE,
|
|
1208
1221
|
fsync: false,
|
|
1209
|
-
// Delivery polling must never wait
|
|
1210
|
-
//
|
|
1211
|
-
// from ever reaching its finally/release path.
|
|
1222
|
+
// Delivery polling must never wait behind a spool writer. A later
|
|
1223
|
+
// fs.watch/poll tick retries unchanged requests.
|
|
1212
1224
|
timeoutMs: 0,
|
|
1213
1225
|
});
|
|
1226
|
+
for (const request of candidates) {
|
|
1227
|
+
if (request.lifecycleDecided) _rememberForeignSpoolScan(request.sessionId, mtime);
|
|
1228
|
+
_settleForeignDrain(request, request.taken);
|
|
1229
|
+
}
|
|
1214
1230
|
} catch (err) {
|
|
1215
|
-
if (err?.code
|
|
1216
|
-
|
|
1217
|
-
|
|
1231
|
+
if (err?.code !== 'ELOCKCONTENDED') {
|
|
1232
|
+
try { process.stderr.write(`[session] foreign-injection drain failed: ${err?.message || err}\n`); } catch {}
|
|
1233
|
+
}
|
|
1234
|
+
for (const request of batch) {
|
|
1235
|
+
if (request.waiters.length > 0) _settleForeignDrain(request, []);
|
|
1236
|
+
}
|
|
1237
|
+
} finally {
|
|
1238
|
+
_foreignDrainRunning = false;
|
|
1239
|
+
_scheduleForeignDrainBatch();
|
|
1218
1240
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/**
|
|
1244
|
+
* Owner-side drain of FOREIGN user injections for a session this process
|
|
1245
|
+
* owns: atomically removes (and returns the text of) genuine user/steering
|
|
1246
|
+
* entries that were persisted by ANOTHER process — entries known locally
|
|
1247
|
+
* (own steering buffers, hydrated, in-delivery, acked) and completion/
|
|
1248
|
+
* internal-notification entries are left untouched for the normal
|
|
1249
|
+
* askSession hydrate path.
|
|
1250
|
+
*/
|
|
1251
|
+
export async function drainForeignUserInjections(sessionId) {
|
|
1252
|
+
if (!isValidPendingSessionId(sessionId)) return [];
|
|
1253
|
+
// DELIVERY path, not cleanup: this takes rows out of the spool and hands
|
|
1254
|
+
// them to a live turn. A tombstoned (or otherwise invalidated) session may
|
|
1255
|
+
// neither receive nor lose input, so the tombstone-TOLERANT epoch check
|
|
1256
|
+
// used by the explicit cleanup mutations (ack / clear) is wrong here — the
|
|
1257
|
+
// token is refused outright when it was captured from a closed session.
|
|
1258
|
+
// Checked BEFORE the mtime memo so a later reopen still sees this spool
|
|
1259
|
+
// state as unscanned.
|
|
1260
|
+
const epochToken = currentPendingLifecycleToken(sessionId);
|
|
1261
|
+
if (pendingLifecycleInvalidated(sessionId)) return [];
|
|
1262
|
+
return new Promise((resolve) => {
|
|
1263
|
+
const existing = _foreignDrainRequests.get(sessionId);
|
|
1264
|
+
if (existing && existing.epochToken === epochToken) {
|
|
1265
|
+
existing.waiters.push(resolve);
|
|
1266
|
+
} else {
|
|
1267
|
+
if (existing) _settleForeignDrain(existing, []);
|
|
1268
|
+
_foreignDrainRequests.set(sessionId, {
|
|
1269
|
+
sessionId,
|
|
1270
|
+
epochToken,
|
|
1271
|
+
waiters: [resolve],
|
|
1272
|
+
localIds: null,
|
|
1273
|
+
taken: [],
|
|
1274
|
+
lifecycleDecided: false,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
_scheduleForeignDrainBatch();
|
|
1278
|
+
});
|
|
1221
1279
|
}
|
|
1222
1280
|
|
|
1223
1281
|
export function drainPendingMessages(sessionId) {
|