mixdog 0.9.96 → 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/README.md +0 -17
- package/package.json +8 -7
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- 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/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- 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/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- 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 +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- 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/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 +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -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/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- 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 +6 -5
- 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 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +17 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- 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/prewarm.mjs +1 -1
- 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 +52 -28
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +8 -4
- 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} +170 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +8 -8
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +154 -127
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- 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 +7 -7
- 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 -17
- package/src/tui/{engine → session}/session-api.mjs +7 -60
- package/src/tui/{engine → session}/session-flow.mjs +1 -1
- 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 +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
- package/src/tui/session.mjs +22 -0
- 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-protocol.mjs +0 -32
- 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
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
// Eager tool-dispatch controller, extracted from agent-loop.mjs. Owns the
|
|
2
2
|
// per-turn pending promise map, the intra-turn in-flight signature set, and
|
|
3
|
-
// the mutation epoch. Every valid call
|
|
4
|
-
// streaming
|
|
5
|
-
//
|
|
3
|
+
// the mutation epoch. Every valid call dispatches while the provider is still
|
|
4
|
+
// streaming. Calls execute in parallel except that shell after apply_patch
|
|
5
|
+
// waits for every earlier patch in the turn; results are collected later in
|
|
6
|
+
// call order.
|
|
6
7
|
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
7
8
|
import { isInvalidToolArgsMarker } from '../providers/openai-compat-stream.mjs';
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
_intraTurnSig,
|
|
11
|
+
_isMutationTool,
|
|
12
|
+
_isReadTool,
|
|
13
|
+
_isScopedCacheableTool,
|
|
14
|
+
_isShellTool,
|
|
15
|
+
_stripMcpPrefix,
|
|
16
|
+
} from './loop/tool-classify.mjs';
|
|
9
17
|
import { tryReadCached, tryScopedToolCached } from './read-dedup.mjs';
|
|
10
18
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
11
19
|
import { executeTool } from './loop/tool-exec.mjs';
|
|
@@ -18,6 +26,10 @@ export function createEagerDispatcher({
|
|
|
18
26
|
executeToolFn = executeTool,
|
|
19
27
|
}) {
|
|
20
28
|
const pending = new Map();
|
|
29
|
+
// Cumulative settlement barrier for patches already emitted in this
|
|
30
|
+
// assistant turn. Patches remain path-parallel with each other; only a
|
|
31
|
+
// later shell waits, so validation cannot observe pre-patch files.
|
|
32
|
+
let patchBarrier = Promise.resolve();
|
|
21
33
|
// Streaming-time intra-turn dedup. When the LLM emits two
|
|
22
34
|
// tool_use blocks with identical (name, args) signatures in
|
|
23
35
|
// sequence, the provider's onToolCall fires for both BEFORE
|
|
@@ -90,9 +102,11 @@ export function createEagerDispatcher({
|
|
|
90
102
|
// this call there, never start it eagerly here.
|
|
91
103
|
if (preDispatchDenyForSession(sessionRef, call, toolKind) !== null) return null;
|
|
92
104
|
const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: epoch.mutation };
|
|
105
|
+
const precedingPatches = _isShellTool(call.name) ? patchBarrier : null;
|
|
93
106
|
if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
|
|
94
107
|
entry.promise = (async () => {
|
|
95
108
|
try {
|
|
109
|
+
if (precedingPatches) await precedingPatches;
|
|
96
110
|
await opts.beforeToolExecution?.();
|
|
97
111
|
return { ok: true, value: await executeToolFn(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal, notifyFn: opts.notifyFn, toolApprovalHook: opts.onToolApproval, iteration: getNextIteration(), deferShellCwdCommit: true }) };
|
|
98
112
|
} catch (error) {
|
|
@@ -152,6 +166,10 @@ export function createEagerDispatcher({
|
|
|
152
166
|
return settled;
|
|
153
167
|
});
|
|
154
168
|
pending.set(call.id, entry);
|
|
169
|
+
if (_isMutationTool(call.name)) {
|
|
170
|
+
const currentPatch = entry.promise;
|
|
171
|
+
patchBarrier = Promise.allSettled([patchBarrier, currentPatch]).then(() => undefined);
|
|
172
|
+
}
|
|
155
173
|
return entry;
|
|
156
174
|
};
|
|
157
175
|
const startEagerRun = (calls, startIndex, dupSet) => {
|
|
@@ -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)
|
|
@@ -40,9 +40,10 @@ export function isEagerDispatchable(name, tools) {
|
|
|
40
40
|
return set.has(name);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Full
|
|
44
|
-
//
|
|
45
|
-
//
|
|
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.
|
|
46
47
|
// isEagerDispatchable above remains the READ-ONLY classifier used for dedup
|
|
47
48
|
// eligibility, steering, and post-mutation result invalidation.
|
|
48
49
|
export function isParallelDispatchable(name) {
|
|
@@ -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();
|
|
@@ -766,6 +778,7 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
766
778
|
session.toolApprovalHook = priorToolApprovalHook;
|
|
767
779
|
}
|
|
768
780
|
}
|
|
781
|
+
throwIfAborted(turnSignal);
|
|
769
782
|
// Post-loop validation: if closeSession() landed while we were awaiting,
|
|
770
783
|
// drop the save so the tombstone on disk isn't overwritten.
|
|
771
784
|
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
@@ -900,7 +913,10 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
900
913
|
// Unified total-prompt field. Anthropic = input+cache_read+cache_write
|
|
901
914
|
// (additive); OpenAI OAuth/API/Gemini = input_tokens already includes the
|
|
902
915
|
// cached portion (inclusive), so the fallback must not double-count.
|
|
903
|
-
const { isInclusiveProvider, computeCostUsd } = await
|
|
916
|
+
const { isInclusiveProvider, computeCostUsd } = await runAbortable(
|
|
917
|
+
turnSignal,
|
|
918
|
+
() => import('../../../../shared/llm/cost.mjs'),
|
|
919
|
+
);
|
|
904
920
|
const inclusive = isInclusiveProvider(session.provider);
|
|
905
921
|
const promptTokens = typeof result.usage.promptTokens === 'number'
|
|
906
922
|
? result.usage.promptTokens
|
|
@@ -1042,15 +1058,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1042
1058
|
}
|
|
1043
1059
|
try {
|
|
1044
1060
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1045
|
-
|
|
1046
|
-
|
|
1061
|
+
const cleanup = finalized.responsePreserved
|
|
1062
|
+
? finalizePendingMessageDelivery(
|
|
1047
1063
|
activeSession, _turnPendingEntries, durableSave,
|
|
1048
1064
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1049
|
-
)
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
1065
|
+
)
|
|
1066
|
+
: durableSave;
|
|
1067
|
+
const cleanupResult = await settleAskCleanup(cleanup);
|
|
1068
|
+
if (cleanupResult.settled) clearTurnCheckpoint(sessionId, _turnCheckpointToken);
|
|
1054
1069
|
} catch { /* cancellation cleanup is best-effort */ }
|
|
1055
1070
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
1056
1071
|
} else releasePendingMessages(sessionId, _turnPendingEntries);
|
|
@@ -1088,35 +1103,34 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1088
1103
|
else releasePendingMessages(sessionId, _turnPendingEntries);
|
|
1089
1104
|
try {
|
|
1090
1105
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1091
|
-
|
|
1092
|
-
|
|
1106
|
+
const cleanup = finalized.responsePreserved
|
|
1107
|
+
? finalizePendingMessageDelivery(
|
|
1093
1108
|
activeSession, _turnPendingEntries, durableSave,
|
|
1094
1109
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1095
|
-
)
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}
|
|
1099
|
-
_errorStateDurable = true;
|
|
1110
|
+
)
|
|
1111
|
+
: durableSave;
|
|
1112
|
+
_errorStateDurable = (await settleAskCleanup(cleanup)).settled;
|
|
1100
1113
|
} catch { /* provider-failure history persistence is best-effort */ }
|
|
1101
1114
|
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
1102
1115
|
if (currentRuntime) currentRuntime.session = activeSession;
|
|
1103
1116
|
} else {
|
|
1104
|
-
const
|
|
1117
|
+
const compactPersist = await settleAskCleanup(persistCompactedOutgoingAfterAskFailure({
|
|
1105
1118
|
sessionId,
|
|
1106
1119
|
activeSession,
|
|
1107
1120
|
askGeneration,
|
|
1108
1121
|
turnOutgoing: _turnOutgoing,
|
|
1109
1122
|
error: err,
|
|
1110
|
-
});
|
|
1123
|
+
}));
|
|
1124
|
+
const promptPersisted = compactPersist.settled && compactPersist.value === true;
|
|
1111
1125
|
if (promptPersisted) {
|
|
1112
1126
|
recordPendingMessageDelivery(activeSession, _turnPendingEntries);
|
|
1113
1127
|
try {
|
|
1114
1128
|
const durableSave = saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
1115
|
-
|
|
1129
|
+
const cleanup = finalizePendingMessageDelivery(
|
|
1116
1130
|
activeSession, _turnPendingEntries, durableSave,
|
|
1117
1131
|
() => saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1118
1132
|
);
|
|
1119
|
-
_errorStateDurable =
|
|
1133
|
+
_errorStateDurable = (await settleAskCleanup(cleanup)).settled;
|
|
1120
1134
|
} catch {}
|
|
1121
1135
|
} else {
|
|
1122
1136
|
releasePendingMessages(sessionId, _turnPendingEntries);
|
|
@@ -1124,8 +1138,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
1124
1138
|
}
|
|
1125
1139
|
if (!_errorStateDurable && activeSession) {
|
|
1126
1140
|
try {
|
|
1127
|
-
await
|
|
1128
|
-
|
|
1141
|
+
_errorStateDurable = (await settleAskCleanup(
|
|
1142
|
+
saveSessionAsync(activeSession, { expectedGeneration: askGeneration }),
|
|
1143
|
+
)).settled;
|
|
1129
1144
|
} catch { /* retain checkpoint when canonical persistence fails */ }
|
|
1130
1145
|
}
|
|
1131
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
|
|
@@ -6,8 +6,10 @@ import { executeInternalTool } from '../../internal-tools.mjs';
|
|
|
6
6
|
import { classifyResultKind } from '../result-classification.mjs';
|
|
7
7
|
import { tryPrefetchCached, setPrefetchCached } from '../read-dedup.mjs';
|
|
8
8
|
import { _executeCodeGraphToolLazy } from './runtime-loaders.mjs';
|
|
9
|
+
import { runAbortable, throwIfAborted } from '../../../../shared/abort-race.mjs';
|
|
9
10
|
|
|
10
|
-
export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
11
|
+
export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch, signal = null) {
|
|
12
|
+
throwIfAborted(signal);
|
|
11
13
|
if (!explicitPrefetch || typeof explicitPrefetch !== 'object') return null;
|
|
12
14
|
if (!isAgentOwner(session)) return null;
|
|
13
15
|
const parts = [];
|
|
@@ -52,7 +54,10 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
52
54
|
totalEntries.push(...files);
|
|
53
55
|
// R20: per-file prefetch cache (cross-dispatch, process-local).
|
|
54
56
|
// Try each file from cache first; batch misses into one disk read.
|
|
55
|
-
const { resolve: _pfResolve, isAbsolute: _pfIsAbs, normalize: _pfNorm } = await
|
|
57
|
+
const { resolve: _pfResolve, isAbsolute: _pfIsAbs, normalize: _pfNorm } = await runAbortable(
|
|
58
|
+
signal,
|
|
59
|
+
() => import('path'),
|
|
60
|
+
);
|
|
56
61
|
const _pfCwd = session.cwd || null;
|
|
57
62
|
function _pfAbsPath(f) {
|
|
58
63
|
const abs = _pfIsAbs(f) ? f : _pfResolve(_pfCwd || process.cwd(), f);
|
|
@@ -79,7 +84,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
79
84
|
if (missFiles.length > 0) {
|
|
80
85
|
// Read each miss file individually so we can cache per-file.
|
|
81
86
|
// The files list is small (typically 2-5), so N awaits is fine.
|
|
82
|
-
await Promise.all(missFiles.map(async (f) => {
|
|
87
|
+
await runAbortable(signal, () => Promise.all(missFiles.map(async (f) => {
|
|
83
88
|
const opts = _readOptsByFile.get(f) || {};
|
|
84
89
|
const readArgs = { path: f };
|
|
85
90
|
if (opts.mode === 'full') {
|
|
@@ -95,7 +100,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
95
100
|
if (out !== null) {
|
|
96
101
|
missResults[f] = String(out);
|
|
97
102
|
}
|
|
98
|
-
}));
|
|
103
|
+
})));
|
|
99
104
|
// Cache successful miss results.
|
|
100
105
|
for (const { file, abs } of fileMisses) {
|
|
101
106
|
const content = missResults[file];
|
|
@@ -157,7 +162,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
157
162
|
return { symbol, out: null };
|
|
158
163
|
});
|
|
159
164
|
});
|
|
160
|
-
const callerResults = await Promise.allSettled(callerTasks);
|
|
165
|
+
const callerResults = await runAbortable(signal, () => Promise.allSettled(callerTasks));
|
|
161
166
|
for (const r of callerResults) {
|
|
162
167
|
const { symbol, out } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
|
|
163
168
|
if (out && classifyResultKind(String(out)) !== 'error') {
|
|
@@ -182,7 +187,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
182
187
|
return { symbol, out: null };
|
|
183
188
|
});
|
|
184
189
|
});
|
|
185
|
-
const refResults = await Promise.allSettled(refTasks);
|
|
190
|
+
const refResults = await runAbortable(signal, () => Promise.allSettled(refTasks));
|
|
186
191
|
for (const r of refResults) {
|
|
187
192
|
const { symbol, out } = r.status === 'fulfilled' ? r.value : { symbol: '?', out: null };
|
|
188
193
|
if (out && classifyResultKind(String(out)) !== 'error') {
|
|
@@ -192,6 +197,7 @@ export async function _tryBridgeExplicitPrefetch(session, explicitPrefetch) {
|
|
|
192
197
|
}
|
|
193
198
|
}
|
|
194
199
|
}
|
|
200
|
+
throwIfAborted(signal);
|
|
195
201
|
if (session && typeof session === 'object' && (callers.length > 0 || references.length > 0)) {
|
|
196
202
|
if (!session.prefetchStats) session.prefetchStats = { files: 0, cached: 0, miss: 0, failed: 0, callers: 0, references: 0 };
|
|
197
203
|
session.prefetchStats.callers = (session.prefetchStats.callers || 0) + callers.length;
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
// Per-session mutex extracted verbatim from manager.mjs. Queues concurrent
|
|
3
3
|
// askSession calls (and their drained pending-message tail turns) to prevent
|
|
4
4
|
// message loss / interleaving.
|
|
5
|
+
import { abortReason } from '../../../../shared/abort-race.mjs';
|
|
6
|
+
|
|
5
7
|
const _sessionLocks = new Map();
|
|
6
|
-
export function acquireSessionLock(sessionId) {
|
|
8
|
+
export function acquireSessionLock(sessionId, signal = null) {
|
|
7
9
|
let entry = _sessionLocks.get(sessionId);
|
|
8
10
|
if (!entry) {
|
|
9
11
|
entry = { promise: Promise.resolve(), count: 0 };
|
|
@@ -15,9 +17,43 @@ export function acquireSessionLock(sessionId) {
|
|
|
15
17
|
entry.promise = new Promise(r => { release = r; });
|
|
16
18
|
// Self-heal: if the previous holder rejected, swallow so subsequent
|
|
17
19
|
// queued waiters don't propagate that rejection and brick the lock chain.
|
|
18
|
-
|
|
20
|
+
const previousReady = prev.catch(() => {});
|
|
21
|
+
let slotReleased = false;
|
|
22
|
+
const releaseSlot = () => {
|
|
23
|
+
if (slotReleased) return;
|
|
24
|
+
slotReleased = true;
|
|
19
25
|
entry.count--;
|
|
20
26
|
if (entry.count === 0) _sessionLocks.delete(sessionId);
|
|
21
27
|
release();
|
|
28
|
+
};
|
|
29
|
+
const unlock = () => releaseSlot();
|
|
30
|
+
if (!signal) return previousReady.then(() => unlock);
|
|
31
|
+
|
|
32
|
+
// A cancelled waiter must reject immediately without opening a hole in the
|
|
33
|
+
// mutex. Its reserved slot is skipped only after the previous holder exits,
|
|
34
|
+
// so later waiters still cannot overlap the live owner.
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
let settled = false;
|
|
37
|
+
const removeAbort = () => {
|
|
38
|
+
try { signal.removeEventListener('abort', onAbort); } catch {}
|
|
39
|
+
};
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
if (settled) return;
|
|
42
|
+
settled = true;
|
|
43
|
+
removeAbort();
|
|
44
|
+
void previousReady.then(releaseSlot);
|
|
45
|
+
reject(abortReason(signal, `Session "${sessionId}" lock wait aborted`));
|
|
46
|
+
};
|
|
47
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
48
|
+
if (signal.aborted) {
|
|
49
|
+
onAbort();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
previousReady.then(() => {
|
|
53
|
+
if (settled) return;
|
|
54
|
+
settled = true;
|
|
55
|
+
removeAbort();
|
|
56
|
+
resolve(unlock);
|
|
57
|
+
});
|
|
22
58
|
});
|
|
23
59
|
}
|
|
@@ -121,7 +121,11 @@ export function _queueSessionSummaryUpsert(session, version = null) {
|
|
|
121
121
|
*/
|
|
122
122
|
export function _queueSessionSummaryUpsertRow(row, version = null) {
|
|
123
123
|
if (!row) return;
|
|
124
|
-
|
|
124
|
+
const fileProbe = probePath(join(getStoreDir(), `${row.id}.json`));
|
|
125
|
+
const durableRow = fileProbe.state === PROBE_PRESENT
|
|
126
|
+
? { ...row, storageMtimeMs: fileProbe.mtimeMs, storageSize: fileProbe.size }
|
|
127
|
+
: row;
|
|
128
|
+
_setCachedBaseSummary(durableRow);
|
|
125
129
|
if (version === null || (_summaryCacheVersions.get(row.id) || 0) === version) {
|
|
126
130
|
_summaryCacheUpserts.delete(row.id);
|
|
127
131
|
if (_summaryCacheLatestRows.get(row.id)?.version === version) {
|
|
@@ -130,7 +134,7 @@ export function _queueSessionSummaryUpsertRow(row, version = null) {
|
|
|
130
134
|
_summaryCacheRemovals.delete(row.id);
|
|
131
135
|
_invalidateSummaryRowsView();
|
|
132
136
|
}
|
|
133
|
-
_upsertSessionSummaryRow(
|
|
137
|
+
_upsertSessionSummaryRow(durableRow);
|
|
134
138
|
}
|
|
135
139
|
|
|
136
140
|
export function _queueSessionSummaryRemoval(id) {
|
|
@@ -220,7 +224,10 @@ export function _scanStoredSessionSummaryRows() {
|
|
|
220
224
|
else markInvalid(f);
|
|
221
225
|
continue;
|
|
222
226
|
}
|
|
223
|
-
const
|
|
227
|
+
const summary = session ? _sessionSummary(session) : null;
|
|
228
|
+
const row = summary
|
|
229
|
+
? { ...summary, storageMtimeMs: fileProbe.mtimeMs, storageSize: fileProbe.size }
|
|
230
|
+
: null;
|
|
224
231
|
_summaryScanCache.set(f, { mtimeMs: fileProbe.mtimeMs, size: fileProbe.size, row });
|
|
225
232
|
changed = true;
|
|
226
233
|
if (row) rows.push(row);
|
|
@@ -154,6 +154,8 @@ export function _sessionSummary(session) {
|
|
|
154
154
|
preview: messageProjection.preview,
|
|
155
155
|
generation: typeof session.generation === 'number' ? session.generation : 0,
|
|
156
156
|
implicitBashSessionId: session.implicitBashSessionId || null,
|
|
157
|
+
storageMtimeMs: _positiveNumber(session.storageMtimeMs, 0),
|
|
158
|
+
storageSize: _positiveNumber(session.storageSize, 0),
|
|
157
159
|
// Lifecycle provenance for catalog filters: resume-machinery scratch
|
|
158
160
|
// forks (detachedReason='cli-resume') must never surface in Recent.
|
|
159
161
|
detachedReason: session.detachedReason || null,
|
|
@@ -190,6 +192,8 @@ function _normalizeSummaryRow(row) {
|
|
|
190
192
|
preview: _cleanPreview(row.preview || ''),
|
|
191
193
|
generation: typeof row.generation === 'number' ? row.generation : 0,
|
|
192
194
|
implicitBashSessionId: row.implicitBashSessionId || null,
|
|
195
|
+
storageMtimeMs: _positiveNumber(row.storageMtimeMs, 0),
|
|
196
|
+
storageSize: _positiveNumber(row.storageSize, 0),
|
|
193
197
|
detachedReason: row.detachedReason || null,
|
|
194
198
|
};
|
|
195
199
|
}
|