mixdog 0.9.93 → 0.9.95
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/NOTICE.md +72 -0
- package/package.json +16 -6
- package/scripts/lib/isolated-root-cleanup.mjs +19 -0
- package/scripts/run-suite.mjs +100 -0
- package/src/lib/rules-builder.cjs +4 -3
- package/src/output-styles/detailed.md +13 -15
- package/src/output-styles/extreme-minimal.md +7 -11
- package/src/output-styles/minimal.md +4 -7
- package/src/output-styles/simple.md +11 -13
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-brief.md +4 -5
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +28 -29
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +23 -7
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +29 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +6 -2
- package/src/runtime/agent/orchestrator/mcp/client.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +97 -7
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +11 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +4 -6
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +8 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +7 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +14 -16
- package/src/runtime/agent/orchestrator/providers/openai-ws-headers.mjs +5 -4
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +10 -10
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/registry.mjs +51 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +15 -15
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +12 -32
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +10 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +0 -14
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +9 -8
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +40 -13
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +8 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +5 -9
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +103 -92
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/stall-policy.mjs +2 -3
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +25 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +546 -23
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +8 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +35 -5
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +98 -0
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +7 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +104 -14
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +47 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +9 -2
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +57 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +151 -18
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +109 -13
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +10 -10
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +4 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/channels/backends/discord.mjs +5 -14
- package/src/runtime/channels/backends/telegram.mjs +0 -5
- package/src/runtime/channels/lib/inbound-handler.mjs +0 -1
- package/src/runtime/channels/lib/output-forwarder.mjs +24 -5
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/worker-main.mjs +1 -1
- package/src/runtime/media/renditions.mjs +21 -2
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/memory/tool-defs.mjs +6 -6
- package/src/runtime/shared/atomic-file.mjs +53 -0
- package/src/runtime/shared/background-tasks.mjs +16 -6
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/resource-admission.mjs +40 -1
- package/src/runtime/shared/task-notification-envelope.mjs +11 -2
- package/src/runtime/shared/tool-card-model.mjs +6 -2
- package/src/runtime/shared/tool-status.mjs +10 -1
- package/src/runtime/shared/tool-surface.mjs +5 -0
- package/src/runtime/shared/turn-snapshot.mjs +385 -21
- package/src/runtime/shared/turn-worktree-snapshot.mjs +540 -0
- package/src/session-runtime/context-status.mjs +9 -3
- package/src/session-runtime/lifecycle-api.mjs +35 -5
- package/src/session-runtime/mcp-glue.mjs +11 -6
- package/src/session-runtime/provider-models.mjs +94 -43
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/session-runtime/runtime-core.mjs +46 -8
- package/src/session-runtime/runtime-tunables.mjs +4 -0
- package/src/session-runtime/self-update.mjs +33 -4
- package/src/session-runtime/session-lifecycle.mjs +2 -0
- package/src/session-runtime/session-text.mjs +2 -1
- package/src/session-runtime/session-turn-api.mjs +106 -22
- package/src/session-runtime/workflow.mjs +11 -4
- package/src/standalone/agent-tool.mjs +4 -4
- package/src/standalone/backend-daemon.mjs +570 -0
- package/src/standalone/channel-daemon-transport.mjs +141 -1
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/engine-daemon-client.mjs +894 -0
- package/src/standalone/engine-daemon-local-bridge.mjs +20 -0
- package/src/standalone/engine-daemon-protocol.mjs +33 -0
- package/src/standalone/engine-daemon-service.mjs +864 -0
- package/src/standalone/engine-daemon-transport.mjs +603 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +62 -47
- package/src/tui/app/app-format.mjs +4 -2
- package/src/tui/app/app-view.jsx +5 -0
- package/src/tui/app/channel-pickers.mjs +7 -6
- package/src/tui/app/core-memory-picker.mjs +4 -4
- package/src/tui/app/extension-pickers.mjs +20 -18
- package/src/tui/app/maintenance-pickers.mjs +27 -27
- package/src/tui/app/onboarding-steps.mjs +23 -18
- package/src/tui/app/prompt-submit.mjs +13 -2
- package/src/tui/app/route-pickers.mjs +13 -8
- package/src/tui/app/settings-picker.mjs +55 -58
- package/src/tui/app/slash-dispatch.mjs +32 -28
- package/src/tui/app/transcript-window.mjs +19 -0
- package/src/tui/app/usage-context-panels.mjs +22 -7
- package/src/tui/app/use-mouse-input.mjs +25 -3
- package/src/tui/app/use-prompt-queue-history.mjs +31 -16
- package/src/tui/app/use-transcript-scroll.mjs +30 -8
- package/src/tui/app/use-transcript-window.mjs +22 -1
- package/src/tui/app/use-welcome-prompt-hint.mjs +2 -2
- package/src/tui/components/PromptInput.jsx +10 -0
- package/src/tui/components/Spinner.jsx +89 -86
- package/src/tui/components/TextEntryPanel.jsx +14 -0
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +1537 -9718
- package/src/tui/engine/agent-job-feed.mjs +2 -2
- package/src/tui/engine/live-share.mjs +97 -4
- package/src/tui/engine/session-api-ext.mjs +23 -1
- package/src/tui/engine/session-api.mjs +88 -41
- package/src/tui/engine/session-flow.mjs +43 -4
- package/src/tui/engine/tool-card-results.mjs +6 -0
- package/src/tui/engine/turn.mjs +84 -4
- package/src/tui/engine-local-session.mjs +1108 -0
- package/src/tui/engine.mjs +16 -1057
- package/src/tui/index.jsx +47 -2
- package/src/tui/markdown/stream-fence.mjs +1 -1
- package/src/tui/spinner-meta.mjs +80 -0
- package/src/tui/spinner-verbs.mjs +35 -0
- package/src/ui/statusline-segments.mjs +43 -10
- package/src/ui/statusline.mjs +10 -1
- package/scripts/tmp-cdp-errors.mjs +0 -41
- package/scripts/tmp-cdp-inspect.mjs +0 -41
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +0 -176
- package/src/standalone/channel-daemon.mjs +0 -226
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { join, resolve as _pathResolve, isAbsolute as _pathIsAbs, normalize as _pathNorm } from 'node:path';
|
|
6
6
|
import { writeJsonAtomicSync } from '../../../../shared/atomic-file.mjs';
|
|
7
7
|
import { _normalizeCacheKey } from './util.mjs';
|
|
8
|
+
import { GREP_AUTO_CONTEXT_LINES } from '../../tools/builtin/path-utils.mjs';
|
|
8
9
|
|
|
9
10
|
const MAX_PER_SESSION = 100;
|
|
10
11
|
|
|
@@ -43,6 +44,22 @@ function _firstArg(args, names) {
|
|
|
43
44
|
return undefined;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
const _GREP_CONTEXT_GROUPS = [
|
|
48
|
+
['-A', ['-A', 'A', 'after', 'after_context', 'afterContext', '--after-context', 'after-context', 'afterLines', 'after_lines']],
|
|
49
|
+
['-B', ['-B', 'B', 'before', 'before_context', 'beforeContext', '--before-context', 'before-context', 'beforeLines', 'before_lines']],
|
|
50
|
+
['context', ['context', '-C', 'C', 'context_lines', 'contextLines', '--context', 'contextN', 'around', 'surrounding']],
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function _canonicalizeGrepContextArgs(args) {
|
|
54
|
+
for (const [canonical, aliases] of _GREP_CONTEXT_GROUPS) {
|
|
55
|
+
const value = _firstArg(args, aliases);
|
|
56
|
+
for (const alias of aliases) delete args[alias];
|
|
57
|
+
if (value === undefined) continue;
|
|
58
|
+
const numeric = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
59
|
+
args[canonical] = Number.isFinite(numeric) && Number.isInteger(numeric) ? numeric : value;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
46
63
|
function _canonicalToolArgs(toolName, args) {
|
|
47
64
|
if (!args || typeof args !== 'object') return args;
|
|
48
65
|
const next = { ...args };
|
|
@@ -59,12 +76,35 @@ function _canonicalToolArgs(toolName, args) {
|
|
|
59
76
|
const alias = _firstArg(next, ['root', 'directory', 'dir']);
|
|
60
77
|
if (alias !== undefined) next.path = alias;
|
|
61
78
|
}
|
|
79
|
+
_canonicalizeGrepContextArgs(next);
|
|
62
80
|
if ((next.output_mode === undefined || next.output_mode === null || next.output_mode === '') && typeof next.mode === 'string') {
|
|
63
81
|
const mode = next.mode.trim();
|
|
64
|
-
if (['files_with_matches', 'content', 'count'].includes(mode)) next.output_mode = mode;
|
|
82
|
+
if (['files_with_matches', 'content', 'content_with_context', 'count'].includes(mode)) next.output_mode = mode;
|
|
65
83
|
}
|
|
66
84
|
for (const k of ['query', 'regex', 'regexp', 'needle', 'search', 'literal', 'file_pattern', 'filePattern', 'include', 'includes', 'files', 'root', 'directory', 'dir']) delete next[k];
|
|
67
|
-
|
|
85
|
+
delete next.mode;
|
|
86
|
+
|
|
87
|
+
// Canonicalize by execution semantics, not caller spelling:
|
|
88
|
+
// omitted/content_with_context => content + automatic 25-line context;
|
|
89
|
+
// context:0 => bare content; context flags are ignored in count/files.
|
|
90
|
+
const requestedMode = typeof next.output_mode === 'string' ? next.output_mode.trim() : '';
|
|
91
|
+
if (requestedMode === 'files_with_matches' || requestedMode === 'count') {
|
|
92
|
+
next.output_mode = requestedMode;
|
|
93
|
+
delete next['-A'];
|
|
94
|
+
delete next['-B'];
|
|
95
|
+
delete next.context;
|
|
96
|
+
} else {
|
|
97
|
+
const hasExplicitContext = ['-A', '-B', 'context']
|
|
98
|
+
.some((key) => Object.prototype.hasOwnProperty.call(next, key));
|
|
99
|
+
next.output_mode = 'content';
|
|
100
|
+
if ((requestedMode === '' || requestedMode === 'content_with_context') && !hasExplicitContext) {
|
|
101
|
+
next.context = GREP_AUTO_CONTEXT_LINES;
|
|
102
|
+
}
|
|
103
|
+
if (next.context === 0 && !Object.prototype.hasOwnProperty.call(next, '-A')
|
|
104
|
+
&& !Object.prototype.hasOwnProperty.call(next, '-B')) {
|
|
105
|
+
delete next.context;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
68
108
|
} else if (toolName === 'glob') {
|
|
69
109
|
if (next.pattern === undefined || next.pattern === null || next.pattern === '') {
|
|
70
110
|
const alias = _firstArg(next, ['glob', 'file_pattern', 'filePattern', 'name', 'include', 'includes', 'files']);
|
|
@@ -2,7 +2,9 @@ import { isAgentOwner } from '../agent-owner.mjs';
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_COMPACTION_BUFFER_TOKENS = 0;
|
|
4
4
|
export const DEFAULT_COMPACTION_BUFFER_RATIO = 0.1;
|
|
5
|
-
|
|
5
|
+
// Main/user sessions default to full-window trigger (buffer 0): RFT compact
|
|
6
|
+
// runs at the boundary. Agent sessions keep DEFAULT_COMPACTION_BUFFER_RATIO.
|
|
7
|
+
export const DEFAULT_MAIN_COMPACTION_BUFFER_RATIO = 0;
|
|
6
8
|
export const MAX_COMPACTION_BUFFER_RATIO = 0.25;
|
|
7
9
|
const MAX_BUFFER_INPUT_RATIO = 0.999_999;
|
|
8
10
|
export const DEFAULT_COMPACTION_KEEP_TOKENS = 8_000;
|
|
@@ -16,9 +18,14 @@ function envTokenInt(name) {
|
|
|
16
18
|
return positiveTokenInt(process.env[name]);
|
|
17
19
|
}
|
|
18
20
|
export function normalizeCompactionBufferRatio(value, fallback = DEFAULT_COMPACTION_BUFFER_RATIO) {
|
|
21
|
+
// Explicit 0 is a valid buffer (main full-window trigger). Only nullish /
|
|
22
|
+
// empty / non-finite / negative values fall back. Do not treat Number(null)
|
|
23
|
+
// as zero here — null must mean "unset".
|
|
24
|
+
if (value == null || value === '') return fallback;
|
|
19
25
|
const n = Number(value);
|
|
20
|
-
if (Number.isFinite(n)
|
|
21
|
-
return
|
|
26
|
+
if (!Number.isFinite(n) || n < 0) return fallback;
|
|
27
|
+
if (n === 0) return 0;
|
|
28
|
+
return n > 1 ? n / 100 : n;
|
|
22
29
|
}
|
|
23
30
|
export function resolveBufferRatioCandidate(percentInputs = [], ratioInputs = []) {
|
|
24
31
|
for (const raw of percentInputs) {
|
|
@@ -160,11 +160,10 @@ export function providerTokenCalibration(provider) {
|
|
|
160
160
|
if (p.startsWith('gemini') || p.startsWith('google')) return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_GEMINI') ?? 1.15;
|
|
161
161
|
return calibrationEnv('MIXDOG_TOKEN_CALIBRATION_DEFAULT') ?? 1.0;
|
|
162
162
|
}
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
// the 2000x2000 resize ceiling (5333 tokens).
|
|
163
|
+
// Images/documents count a flat 2000 tokens when dimensions are unknown —
|
|
164
|
+
// the conservative constant also used by the micro-compaction path. Known
|
|
165
|
+
// dimensions may only RAISE the allowance via Anthropic's real vision
|
|
166
|
+
// formula (w*h/750), capped at the 2000x2000 resize ceiling (5333 tokens).
|
|
168
167
|
export const IMAGE_VISUAL_TOKEN_ALLOWANCE = 2_000;
|
|
169
168
|
const IMAGE_MAX_TOKEN_ALLOWANCE = 5_333;
|
|
170
169
|
|
|
@@ -249,8 +248,8 @@ function legacyEstimateTokens(s) {
|
|
|
249
248
|
// for those runs without penalizing ordinary spaced prose.
|
|
250
249
|
let denseAsciiFloor = 0;
|
|
251
250
|
for (const match of s.matchAll(/[\x21-\x7e]{16,}/g)) {
|
|
252
|
-
//
|
|
253
|
-
//
|
|
251
|
+
// Dense JSON/JSONL prices at chars/2; 0.5/char keeps that ratio for
|
|
252
|
+
// long unmerged printable runs.
|
|
254
253
|
denseAsciiFloor += match[0].length * 0.5;
|
|
255
254
|
}
|
|
256
255
|
const encodedWords = s.match(/\b(?=[A-Za-z0-9]{8,}\b)(?=[A-Za-z0-9]*[A-Za-z])(?=[A-Za-z0-9]*\d)[A-Za-z0-9]+\b/g) || [];
|
|
@@ -272,7 +271,7 @@ function legacyEstimateTokens(s) {
|
|
|
272
271
|
&& (jsonLikeLines >= Math.ceil(lines.length / 2) || structural / nonWhitespace >= 0.12)) {
|
|
273
272
|
// JSONL, compact tables and generated line protocols can consist
|
|
274
273
|
// entirely of short runs while still tokenizing like minified data.
|
|
275
|
-
// chars/2 on the dense payload chars (
|
|
274
|
+
// chars/2 on the dense payload chars (dense-JSON pricing).
|
|
276
275
|
denseAsciiFloor = Math.max(denseAsciiFloor, (nonWhitespace * 0.5) + ((s.length - nonWhitespace) * 0.25));
|
|
277
276
|
}
|
|
278
277
|
const asciiFloor = s.length / 4; // never below the legacy chars/4 lower bound
|
|
@@ -355,8 +354,8 @@ function messageEstimateText(m) {
|
|
|
355
354
|
function imageDescriptorAllowance(descriptor) {
|
|
356
355
|
if (descriptor.width && descriptor.height) {
|
|
357
356
|
// Anthropic vision cost: tokens = (width * height) / 750, with images
|
|
358
|
-
// resized down to at most 2000x2000 (5333 tokens)
|
|
359
|
-
//
|
|
357
|
+
// resized down to at most 2000x2000 (5333 tokens). Caller-supplied
|
|
358
|
+
// dimensions may RAISE the
|
|
360
359
|
// allowance above the unknown-image floor but never lower it (the
|
|
361
360
|
// provider normalizer may not preserve caller metadata).
|
|
362
361
|
const formula = Math.ceil((descriptor.width * descriptor.height) / 750);
|
|
@@ -365,7 +364,7 @@ function imageDescriptorAllowance(descriptor) {
|
|
|
365
364
|
Math.max(IMAGE_VISUAL_TOKEN_ALLOWANCE, formula),
|
|
366
365
|
);
|
|
367
366
|
}
|
|
368
|
-
// Unknown-size images: flat conservative allowance
|
|
367
|
+
// Unknown-size images: flat conservative allowance.
|
|
369
368
|
return IMAGE_VISUAL_TOKEN_ALLOWANCE;
|
|
370
369
|
}
|
|
371
370
|
function messageImageDescriptors(m) {
|
|
@@ -100,6 +100,7 @@ export function createEagerDispatcher({
|
|
|
100
100
|
if (_dedupEligible) _eagerInFlightSigs.set(_sig, call.id);
|
|
101
101
|
entry.promise = (async () => {
|
|
102
102
|
try {
|
|
103
|
+
await opts.beforeToolExecution?.();
|
|
103
104
|
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 }) };
|
|
104
105
|
} catch (error) {
|
|
105
106
|
return { ok: false, error };
|
|
@@ -112,8 +112,8 @@ export function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
|
112
112
|
if (!boundaryTokens) return null;
|
|
113
113
|
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
114
114
|
// Shared session-compaction policy (context-utils): agent semantic keeps the
|
|
115
|
-
// default early-trigger buffer (90%); main/user
|
|
116
|
-
//
|
|
115
|
+
// default early-trigger buffer (90%); main/user default to full-window
|
|
116
|
+
// trigger (buffer 0 / 100%), still overridable via mainBuffer*;
|
|
117
117
|
// a truly-explicit sub-boundary limit wins. explicitAutoCompactTokenLimit
|
|
118
118
|
// is the sanitized (null when legacy full-window) value so telemetry never
|
|
119
119
|
// re-persists a boundary-collapsing limit.
|
|
@@ -216,7 +216,7 @@ function providerPressureTokens(sessionRef, usage) {
|
|
|
216
216
|
/**
|
|
217
217
|
* Align an authoritative provider usage snapshot to the message prefix it
|
|
218
218
|
* covers. Later pressure checks add estimates only for messages after this
|
|
219
|
-
* baseline
|
|
219
|
+
* baseline: actual-usage-plus-growth accounting.
|
|
220
220
|
*/
|
|
221
221
|
export function recordProviderContextBaseline(sessionRef, messages, usage, {
|
|
222
222
|
boundary = 'complete',
|
|
@@ -38,20 +38,6 @@ export function isEditProgressTool(name, isEager) {
|
|
|
38
38
|
return !NON_PROGRESS_TOOLS.has(bare);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
// Step 1 — level-2 escalation steering. N = cumulative level-1 fires.
|
|
42
|
-
// `readOnlyRole` swaps the edit-oriented directive for a report-oriented one:
|
|
43
|
-
// read-permission sessions (reviewer-style) cannot apply_patch, so telling
|
|
44
|
-
// them to edit is self-contradictory and pushes premature termination.
|
|
45
|
-
const LEVEL2_DIMINISHING_RETURNS = ' Further reads add cost, not evidence.';
|
|
46
|
-
|
|
47
|
-
export function level2SteerMessage(n, readOnlyRole = false, level2Fires = 1) {
|
|
48
|
-
const tail = Number(level2Fires) >= 2 ? LEVEL2_DIMINISHING_RETURNS : '';
|
|
49
|
-
if (readOnlyRole) {
|
|
50
|
-
return `<system-reminder>\nEnough evidence gathered — report findings now; name any gaps.${tail}\n</system-reminder>`;
|
|
51
|
-
}
|
|
52
|
-
return `<system-reminder>\nStop exploring — apply the edit you know, or report blocked with what's missing.${tail}\n</system-reminder>`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
41
|
// Step 2 — cross-turn dedup stub. `stuck` appends the escalation tail at the
|
|
56
42
|
// 5th+ dedup stub in the session.
|
|
57
43
|
export function crossTurnDedupStub(name, firstIteration, stuck) {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Turn stop hook.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// Before breaking, the turn runs its stop hooks; a hook may block ONCE
|
|
5
|
-
// returning continuation fragments, which are recorded as a prompt message
|
|
6
|
-
// before sampling resumes (
|
|
7
|
-
//
|
|
8
|
-
// completion strikes, no lexical inspection of
|
|
3
|
+
// Termination semantics: a sampling request that needs no follow-up ends the
|
|
4
|
+
// turn. Before breaking, the turn runs its stop hooks; a hook may block ONCE
|
|
5
|
+
// by returning continuation fragments, which are recorded as a prompt message
|
|
6
|
+
// before sampling resumes (the hook stays armed for the rest of the turn and
|
|
7
|
+
// is never cleared inside it). Nothing else can force another turn: no
|
|
8
|
+
// mandatory terminal tool, no completion strikes, no lexical inspection of
|
|
9
|
+
// the assistant text.
|
|
9
10
|
//
|
|
10
11
|
// The single hook implemented here preserves the original failed-apply_patch
|
|
11
12
|
// protection: while a real tool failure is unresolved, the first terminal
|
|
@@ -41,7 +42,7 @@ export function toolFailureContinuationPrompt(failedTool) {
|
|
|
41
42
|
* success clears it.
|
|
42
43
|
* - takeContinuationPrompt(): returns the continuation prompt the FIRST time
|
|
43
44
|
* a terminal assistant message arrives while a failure is unresolved, then
|
|
44
|
-
* stays silent for the rest of the turn
|
|
45
|
+
* stays silent for the rest of the turn.
|
|
45
46
|
*/
|
|
46
47
|
export function createToolFailureStopHook() {
|
|
47
48
|
let unresolvedFailure = false;
|
|
@@ -672,11 +672,14 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
672
672
|
},
|
|
673
673
|
onToolResult: _trackToolResult,
|
|
674
674
|
onToolApproval: typeof askOpts?.onToolApproval === 'function' ? askOpts.onToolApproval : undefined,
|
|
675
|
+
beforeToolExecution: typeof askOpts?.beforeToolExecution === 'function'
|
|
676
|
+
? askOpts.beforeToolExecution
|
|
677
|
+
: undefined,
|
|
675
678
|
onCompactEvent: (event) => {
|
|
676
679
|
_scheduleTurnCheckpoint(true);
|
|
677
680
|
try { askOpts?.onCompactEvent?.(event); } catch {}
|
|
678
681
|
},
|
|
679
|
-
//
|
|
682
|
+
// Mid-chain queued prompt/notification
|
|
680
683
|
// drain is owned by agentLoop at provider-continuation
|
|
681
684
|
// boundaries (after a tool batch, before the next send).
|
|
682
685
|
// The post-loop _pendingTail drain below still handles
|
|
@@ -692,9 +695,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
|
|
|
692
695
|
// Manager/pending-messages entries carry no
|
|
693
696
|
// mode/priority/slash metadata, so they stay OUT of the
|
|
694
697
|
// mid-chain (post-tool-batch) drain — that would bypass
|
|
695
|
-
//
|
|
696
|
-
//
|
|
697
|
-
//
|
|
698
|
+
// the queued-command filters. At the TERMINAL boundary
|
|
699
|
+
// they are exactly pending input: an `agent type=send`
|
|
700
|
+
// queued while
|
|
698
701
|
// the terminal sample was in flight must be folded into
|
|
699
702
|
// THIS turn before any stop hook runs, instead of losing
|
|
700
703
|
// its slot to a synthetic continuation prompt. The mutex
|
|
@@ -210,7 +210,7 @@ export function resolveSessionContextMeta(provider, model, seed = {}) {
|
|
|
210
210
|
}
|
|
211
211
|
export function compactTriggerForSession(session, boundaryTokens) {
|
|
212
212
|
// Delegates to the shared session-compaction policy (context-utils):
|
|
213
|
-
// agent semantic -> 90% (default buffer), main/user ->
|
|
213
|
+
// agent semantic -> 90% (default buffer), main/user -> 100% (buffer 0),
|
|
214
214
|
// truly-explicit sub-boundary limit wins.
|
|
215
215
|
return resolveSessionCompactPolicy(session, boundaryTokens).triggerTokens;
|
|
216
216
|
}
|
|
@@ -150,7 +150,7 @@ export function _runCleanupCycle() {
|
|
|
150
150
|
// Drain every settled runtime entry on each pass, not just the one or two
|
|
151
151
|
// sessions whose on-disk idle TTL happened to expire in this interval.
|
|
152
152
|
_sweepTerminalSessionRuntimes();
|
|
153
|
-
sweepOrphanedPendingMessages();
|
|
153
|
+
await sweepOrphanedPendingMessages();
|
|
154
154
|
await sweepIdleSessions({ includeTombstones: true });
|
|
155
155
|
// Reclaim same-process session snapshots whose state is durable on disk
|
|
156
156
|
// (memory-leak guard: _liveSessions used to grow for process lifetime).
|
|
@@ -4,7 +4,7 @@ import { join } from 'path';
|
|
|
4
4
|
import { readFileSync, statSync } from 'fs';
|
|
5
5
|
import { createHash, randomBytes } from 'crypto';
|
|
6
6
|
import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
7
|
-
import {
|
|
7
|
+
import { updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
|
|
8
8
|
import { promptContentText, isInternalRuntimeNotificationText } from './prompt-utils.mjs';
|
|
9
9
|
import { loadSession, readSessionLifecycleStateFromDisk, saveSessionAsync } from '../store.mjs';
|
|
10
10
|
import { isDeliveredCompletion, logDuplicateSkip } from './delivered-completions.mjs';
|
|
@@ -942,11 +942,11 @@ function shouldEvictPendingSession(sessionId, ttlMs, entryTouchedAt, now = Date.
|
|
|
942
942
|
return entryTouch > 0 && (now - entryTouch) > PENDING_ORPHAN_GRACE_MS;
|
|
943
943
|
}
|
|
944
944
|
|
|
945
|
-
export function sweepOrphanedPendingMessages({ ttlMs = PENDING_ORPHAN_TTL_MS } = {}) {
|
|
945
|
+
export async function sweepOrphanedPendingMessages({ ttlMs = PENDING_ORPHAN_TTL_MS } = {}) {
|
|
946
946
|
const now = Date.now();
|
|
947
947
|
const removed = [];
|
|
948
948
|
try {
|
|
949
|
-
|
|
949
|
+
await updateJsonAtomic(pendingMessagesPath(), (raw) => {
|
|
950
950
|
const next = normalizePendingStore(raw);
|
|
951
951
|
const ids = Object.keys(next.sessions);
|
|
952
952
|
if (ids.length === 0) return undefined;
|
|
@@ -988,9 +988,9 @@ function modelVisiblePendingMessages(messages) {
|
|
|
988
988
|
}
|
|
989
989
|
|
|
990
990
|
export function _mergePendingMessageEntries(entries) {
|
|
991
|
-
//
|
|
992
|
-
//
|
|
993
|
-
//
|
|
991
|
+
// Delivery priority: user input enqueues at 'next', task notifications at
|
|
992
|
+
// 'later', and dequeue always serves 'next' first. Our single merged turn
|
|
993
|
+
// message is the
|
|
994
994
|
// analogue of that dequeue order: genuine user/steering entries are
|
|
995
995
|
// merged BEFORE deferred completion notifications so queued user input
|
|
996
996
|
// is never buried under system notification text. FIFO is preserved
|
|
@@ -1074,7 +1074,16 @@ export function enqueueRemotePendingMessage(sessionId, message) {
|
|
|
1074
1074
|
// spool lock (see persistPendingMessages).
|
|
1075
1075
|
runPendingTestHook('enqueue:beforePublish', { sessionId });
|
|
1076
1076
|
if (pendingLifecycleInvalidated(sessionId, token)) return 0;
|
|
1077
|
-
|
|
1077
|
+
// Preserve the viewer's submission id end-to-end. Minting a fresh id here
|
|
1078
|
+
// made pipe-delivered + spool-fallback twins look like two messages
|
|
1079
|
+
// (owner accepted the pipe copy, then re-took the spool copy).
|
|
1080
|
+
const preservedId = pendingMessageId(normalized) || pendingMessageId(message);
|
|
1081
|
+
const remoteEntry = stampLifecycleToken({
|
|
1082
|
+
...normalized,
|
|
1083
|
+
id: (typeof preservedId === 'string' && preservedId.trim())
|
|
1084
|
+
? preservedId.trim()
|
|
1085
|
+
: newPendingMessageId(),
|
|
1086
|
+
}, token);
|
|
1078
1087
|
return persistPendingMessages(sessionId, [remoteEntry]);
|
|
1079
1088
|
}
|
|
1080
1089
|
|
|
@@ -1104,7 +1113,7 @@ function _rememberForeignSpoolScan(sessionId, mtime) {
|
|
|
1104
1113
|
* internal-notification entries are left untouched for the normal
|
|
1105
1114
|
* askSession hydrate path.
|
|
1106
1115
|
*/
|
|
1107
|
-
export function drainForeignUserInjections(sessionId) {
|
|
1116
|
+
export async function drainForeignUserInjections(sessionId) {
|
|
1108
1117
|
if (!isValidPendingSessionId(sessionId)) return [];
|
|
1109
1118
|
// DELIVERY path, not cleanup: this takes rows out of the spool and hands
|
|
1110
1119
|
// them to a live turn. A tombstoned (or otherwise invalidated) session may
|
|
@@ -1147,7 +1156,13 @@ export function drainForeignUserInjections(sessionId) {
|
|
|
1147
1156
|
// spool immediately pollable again.
|
|
1148
1157
|
let lifecycleDecided = false;
|
|
1149
1158
|
try {
|
|
1150
|
-
|
|
1159
|
+
// ASYNC spool lock. The sync variant parked the owning process's event
|
|
1160
|
+
// loop in Atomics.wait for the whole timeout: in the daemon (PTYs,
|
|
1161
|
+
// engines, IPC and SSE fan-out share one loop) every poll that lost the
|
|
1162
|
+
// race froze the entire app, and it could never win against an async
|
|
1163
|
+
// holder in the SAME process. Waiting off the loop also lets the
|
|
1164
|
+
// in-process lock queue serialize sibling sessions for free.
|
|
1165
|
+
await updateJsonAtomic(pendingMessagesPath(), (raw) => {
|
|
1151
1166
|
// Same authority re-read INSIDE the spool lock: a close/detach
|
|
1152
1167
|
// landing in this window must neither deliver to the old owner nor
|
|
1153
1168
|
// remove the reopened owner's rows.
|
|
@@ -1172,8 +1187,10 @@ export function drainForeignUserInjections(sessionId) {
|
|
|
1172
1187
|
// late-delivery header instead of being silently removed
|
|
1173
1188
|
// (user report: remote sends silently discarded around owner
|
|
1174
1189
|
// restarts).
|
|
1175
|
-
if (foreignUser && isStaleUserInjection(entry))
|
|
1176
|
-
|
|
1190
|
+
if (foreignUser && isStaleUserInjection(entry)) {
|
|
1191
|
+
taken.push({ text: lateDeliveryText(text, entry), id });
|
|
1192
|
+
}
|
|
1193
|
+
else if (foreignUser) taken.push({ text, id });
|
|
1177
1194
|
else kept.push(entry);
|
|
1178
1195
|
}
|
|
1179
1196
|
if (taken.length === 0) return undefined;
|
|
@@ -1184,8 +1201,18 @@ export function drainForeignUserInjections(sessionId) {
|
|
|
1184
1201
|
}
|
|
1185
1202
|
next.updatedAt = Date.now();
|
|
1186
1203
|
return next;
|
|
1187
|
-
}, {
|
|
1204
|
+
}, {
|
|
1205
|
+
compact: true,
|
|
1206
|
+
lock: true,
|
|
1207
|
+
mode: PENDING_MESSAGES_MODE,
|
|
1208
|
+
fsync: false,
|
|
1209
|
+
// Delivery polling must never wait on an async spool writer in the
|
|
1210
|
+
// same daemon process: waiting synchronously prevents that writer
|
|
1211
|
+
// from ever reaching its finally/release path.
|
|
1212
|
+
timeoutMs: 0,
|
|
1213
|
+
});
|
|
1188
1214
|
} catch (err) {
|
|
1215
|
+
if (err?.code === 'ELOCKCONTENDED') return [];
|
|
1189
1216
|
try { process.stderr.write(`[session] foreign-injection drain failed sessionId=${sessionId}: ${err?.message || err}\n`); } catch {}
|
|
1190
1217
|
return [];
|
|
1191
1218
|
}
|
|
@@ -1422,5 +1449,5 @@ setImmediate(() => {
|
|
|
1422
1449
|
// the cross-process lock against the lead's writes for zero
|
|
1423
1450
|
// benefit — the lead already sweeps.
|
|
1424
1451
|
if (process.env.MIXDOG_WORKER_MODE === '1') return;
|
|
1425
|
-
|
|
1452
|
+
void sweepOrphanedPendingMessages().catch(() => {});
|
|
1426
1453
|
});
|
|
@@ -12,6 +12,7 @@ import { _dropPendingMessageState } from './pending-messages.mjs';
|
|
|
12
12
|
import { _stopToolActivityHeartbeat, _getRuntimeEntry, _clearSessionRuntime } from './runtime-liveness.mjs';
|
|
13
13
|
import { _closeBashSessionLazy } from './runtime-loaders.mjs';
|
|
14
14
|
import { clearTurnCheckpoint } from './turn-checkpoint.mjs';
|
|
15
|
+
import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Close a session. Plants a `closed=true` tombstone on disk with a bumped
|
|
@@ -137,6 +138,7 @@ export function closeSession(id, reason = 'manual', opts = {}) {
|
|
|
137
138
|
// Drop session-scoped read dedup cache so the Map doesn't accumulate
|
|
138
139
|
// entries across mcp-server lifetime.
|
|
139
140
|
try { clearReadDedupSession(id); } catch { /* ignore */ }
|
|
141
|
+
try { releaseReadSnapshotScope(id); } catch { /* ignore */ }
|
|
140
142
|
// Drop offload sidecars + module-level counter for this session so a
|
|
141
143
|
// long-running mcp-server doesn't leak disk (tool-results/<id>/*.txt)
|
|
142
144
|
// or Map entries across session lifetime. Fire-and-forget — close path
|
|
@@ -226,6 +228,7 @@ export function unloadSessionRuntime(id, reason = 'runtime-unload') {
|
|
|
226
228
|
try { globalThis.__mixdogCloseProviderConnectionsForSession?.(id, `runtime-unload:${reason}`); } catch { /* ignore */ }
|
|
227
229
|
// Pure caches — a resumed turn rebuilds them from the transcript/disk.
|
|
228
230
|
try { clearReadDedupSession(id); } catch { /* ignore */ }
|
|
231
|
+
try { releaseReadSnapshotScope(id); } catch { /* ignore */ }
|
|
229
232
|
_clearSessionRuntime(id);
|
|
230
233
|
if (process.env.MIXDOG_DEBUG_SESSION_LOG) {
|
|
231
234
|
try { process.stderr.write(`[agent-unload] session=${id} reason=${reason} shells=${allBashIds.length}\n`); } catch { /* best-effort */ }
|
|
@@ -169,7 +169,14 @@ export function createSession(opts) {
|
|
|
169
169
|
const provider = getProvider(providerName);
|
|
170
170
|
if (!provider)
|
|
171
171
|
throw new Error(`Provider "${providerName}" not found or not enabled`);
|
|
172
|
-
const
|
|
172
|
+
const requestedId = String(opts.id || '').trim();
|
|
173
|
+
if (requestedId && !/^[A-Za-z0-9_-]+$/.test(requestedId)) {
|
|
174
|
+
throw new Error('createSession: id is invalid');
|
|
175
|
+
}
|
|
176
|
+
// The daemon may reserve the durable address before provider/session
|
|
177
|
+
// materialization. Supplying that reservation here keeps the address
|
|
178
|
+
// stable across intake -> queued turn -> provider execution.
|
|
179
|
+
const id = requestedId || mintSessionId();
|
|
173
180
|
// Provider cache strategy — agentRuntime.resolveSync() above is a
|
|
174
181
|
// best-effort injection point (setAgentRuntime() has no live caller
|
|
175
182
|
// today, so that branch never fires); build it directly here so every
|
|
@@ -7,10 +7,10 @@ import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
|
|
|
7
7
|
|
|
8
8
|
const INTERRUPT_MESSAGE = '[Request interrupted by user]';
|
|
9
9
|
const INTERRUPT_MESSAGE_FOR_TOOL_USE = '[Request interrupted by user for tool use]';
|
|
10
|
-
const STREAMING_INTERRUPTED_TOOL_RESULT = 'Interrupted by user';
|
|
11
10
|
const PROCESS_RESTART_INTERRUPT_MESSAGE = '[Request interrupted by process restart]';
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Short tool_result body for any unfinished call closed by
|
|
12
|
+
// cancel/crash. UI maps this (and legacy long reject bodies) to Cancelled.
|
|
13
|
+
const INTERRUPTED_TOOL_RESULT = 'Cancelled';
|
|
14
14
|
|
|
15
15
|
// Abort reasons that represent an EXPLICIT user cancellation of this turn.
|
|
16
16
|
// Only these rewind a not-yet-answered user turn out of history — the TUI/
|
|
@@ -136,11 +136,7 @@ function finalizeInterruptedTurn({
|
|
|
136
136
|
const executionStarted = phase === 'tools' || observed?.eagerStarted === true;
|
|
137
137
|
messages.push({
|
|
138
138
|
role: 'tool',
|
|
139
|
-
content:
|
|
140
|
-
? PROCESS_RESTART_TOOL_RESULT
|
|
141
|
-
: (executionStarted
|
|
142
|
-
? TOOL_USE_REJECT_RESULT
|
|
143
|
-
: STREAMING_INTERRUPTED_TOOL_RESULT),
|
|
139
|
+
content: INTERRUPTED_TOOL_RESULT,
|
|
144
140
|
toolCallId: callId,
|
|
145
141
|
toolKind: 'error',
|
|
146
142
|
});
|
|
@@ -148,7 +144,7 @@ function finalizeInterruptedTurn({
|
|
|
148
144
|
}
|
|
149
145
|
|
|
150
146
|
const pairedMessages = sanitizeToolPairs(messages);
|
|
151
|
-
//
|
|
147
|
+
// The synthetic marker is omitted when a queued user submission
|
|
152
148
|
// interrupted the active request; that queued message is the boundary.
|
|
153
149
|
if (abortReason !== 'interrupt' && abortReason !== 'provider-error') {
|
|
154
150
|
pairedMessages.push({
|
|
@@ -36,7 +36,7 @@ function normalizedIncompleteUsage(raw) {
|
|
|
36
36
|
};
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// Loop-level transport replay
|
|
39
|
+
// Loop-level transport replay. The provider
|
|
40
40
|
// layer already retries transient failures with a ~15s total envelope
|
|
41
41
|
// (PROVIDER_RETRY_BACKOFF_MS); a real network blip (router/VPN flap — the
|
|
42
42
|
// 2026-08-02 17:37 incident also dropped the Discord gateway) outlasts it and
|
|
@@ -192,8 +192,8 @@ export async function sendWithRecovery(ctx) {
|
|
|
192
192
|
// have been mid-sentence, and there is no completion signal that
|
|
193
193
|
// says the work is done. Promoting it to a normal terminal response
|
|
194
194
|
// silently reported truncated/aborted turns as finished. Keep it an
|
|
195
|
-
// explicit failure (
|
|
196
|
-
//
|
|
195
|
+
// explicit failure (no completion signal means no completed turn,
|
|
196
|
+
// and the partial is persisted): the already-streamed text
|
|
197
197
|
// is preserved by the interruption/error persistence path
|
|
198
198
|
// (turn-interruption.mjs records the deltas and commits them as the
|
|
199
199
|
// partial assistant message alongside the error), so nothing visible
|