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
|
@@ -126,8 +126,8 @@ function _isMaxOutputIncompleteReason(reason) {
|
|
|
126
126
|
return /^(?:max_output_tokens|max_tokens|length|output_token_limit)$/i.test(String(reason || '').trim());
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// Wire-level `end_turn` on a terminal Responses frame
|
|
130
|
-
//
|
|
129
|
+
// Wire-level `end_turn` on a terminal Responses frame: an optional boolean on
|
|
130
|
+
// the completed response.
|
|
131
131
|
// Optional by contract: only a real boolean normalizes; a missing/non-boolean
|
|
132
132
|
// field stays undefined so absence is never collapsed into false.
|
|
133
133
|
export function _endTurnFromEvent(event) {
|
|
@@ -178,9 +178,9 @@ function _buildOpenAIHttpFallbackHeaders({ auth, cacheKey }) {
|
|
|
178
178
|
};
|
|
179
179
|
if (cacheKey) {
|
|
180
180
|
const sid = String(cacheKey);
|
|
181
|
-
//
|
|
182
|
-
// `session-id`/`thread-id`
|
|
183
|
-
//
|
|
181
|
+
// Backend-native anchors (see openai-ws-pool _buildHandshakeHeaders):
|
|
182
|
+
// the hyphenated `session-id`/`thread-id` pair; legacy underscore
|
|
183
|
+
// `session_id` kept for backward compat.
|
|
184
184
|
headers.session_id = sid;
|
|
185
185
|
headers['session-id'] = sid;
|
|
186
186
|
headers['thread-id'] = sid;
|
|
@@ -244,9 +244,8 @@ export async function sendViaHttpSse({
|
|
|
244
244
|
const responsesUrl = auth?.type === 'openai-direct'
|
|
245
245
|
? OPENAI_DIRECT_RESPONSES_URL
|
|
246
246
|
: CODEX_RESPONSES_URL;
|
|
247
|
-
// Request-body zstd
|
|
248
|
-
//
|
|
249
|
-
// OpenAI provider — the server decompresses Content-Encoding: zstd).
|
|
247
|
+
// Request-body zstd: compression is enabled for the codex backend on the
|
|
248
|
+
// OpenAI provider — the server decompresses Content-Encoding: zstd.
|
|
250
249
|
// openai-direct is excluded: only the codex backend is verified. Env
|
|
251
250
|
// kill-switch plus a process-wide latch flipped on the first 400 seen on
|
|
252
251
|
// a compressed request, which then replays that attempt uncompressed.
|
|
@@ -88,7 +88,7 @@ globalThis.__mixdogOpenaiWsRuntimeLoaded = true;
|
|
|
88
88
|
// by connect/handshake and pre-output stream failures.
|
|
89
89
|
const MIDSTREAM_WS_TRANSIENT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.transientCloseRetries;
|
|
90
90
|
const MIDSTREAM_DEFAULT_RETRY_LIMIT = MIDSTREAM_RETRY_POLICY.ws.defaultRetries;
|
|
91
|
-
//
|
|
91
|
+
// The reference client uses a 200ms base, factor 2, and symmetric ±10%
|
|
92
92
|
// jitter for each of its five stream retries.
|
|
93
93
|
const MIDSTREAM_BACKOFF_MS = Object.freeze([200, 400, 800, 1600, 3200]);
|
|
94
94
|
const CODEX_RETRY_JITTER_RATIO = 0.1;
|
|
@@ -773,7 +773,7 @@ export async function sendViaWebSocket({
|
|
|
773
773
|
let result;
|
|
774
774
|
const streamTimeouts = null;
|
|
775
775
|
try {
|
|
776
|
-
//
|
|
776
|
+
// Prewarm gate: only when the session
|
|
777
777
|
// has no prior request state. A reused pooled socket with a live
|
|
778
778
|
// chain must go straight to the real request.
|
|
779
779
|
if (warmupBody && typeof warmupBody === 'object' && !completedWarmup
|
|
@@ -203,7 +203,7 @@ export function toOpenAIResponsesTool(t) {
|
|
|
203
203
|
|
|
204
204
|
export const _convertMessagesToResponsesInputForTest = convertMessagesToResponsesInput;
|
|
205
205
|
|
|
206
|
-
//
|
|
206
|
+
// The reference client only attaches the
|
|
207
207
|
// reasoning object when model_info.supports_reasoning_summaries; models
|
|
208
208
|
// without summary support get NO reasoning field at all. Mirror that via the
|
|
209
209
|
// cached codex catalog; unknown models default to true (gpt-5 family all
|
|
@@ -218,7 +218,7 @@ function _codexModelSupportsReasoningSummaries(id) {
|
|
|
218
218
|
return true;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
//
|
|
221
|
+
// Effort normalization: `ultra` collapses to
|
|
222
222
|
// `max` on the wire — the openai-oauth backend does not accept `ultra`. Every
|
|
223
223
|
// other effort passes through unchanged; empty/unknown falls back to medium.
|
|
224
224
|
export function _normalizeReasoningEffort(effort) {
|
|
@@ -251,12 +251,12 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
251
251
|
const value = String(item || '').trim();
|
|
252
252
|
if (value && !include.includes(value)) include.push(value);
|
|
253
253
|
}
|
|
254
|
-
// Field order MIRRORS
|
|
254
|
+
// Field order MIRRORS the reference request struct:
|
|
255
255
|
// model, instructions, input, tools, tool_choice, parallel_tool_calls,
|
|
256
256
|
// reasoning, store, stream, include, service_tier, prompt_cache_key, text.
|
|
257
257
|
// JSON serialization order is load-bearing for the server prompt cache
|
|
258
|
-
// (exact-prefix match): matching
|
|
259
|
-
// the same cache-routing shape
|
|
258
|
+
// (exact-prefix match): matching that byte layout keeps our requests on
|
|
259
|
+
// the same cache-routing shape the backend warms. tools/service_tier/
|
|
260
260
|
// prompt_cache_key are appended below in the same relative order.
|
|
261
261
|
const body = {
|
|
262
262
|
model,
|
|
@@ -264,17 +264,15 @@ export function buildRequestBody(messages, model, tools, sendOpts) {
|
|
|
264
264
|
input,
|
|
265
265
|
tool_choice: opts.toolChoice || 'auto',
|
|
266
266
|
parallel_tool_calls: true,
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// with NO summary field on gpt-5.5, regardless of what the repo's
|
|
277
|
-
// build_reasoning() suggests. Match the observed bytes.
|
|
267
|
+
// The reference client sends { effort, summary } — summary defaults
|
|
268
|
+
// to "auto" (lowercase on the wire). Matching this keeps our
|
|
269
|
+
// reasoning object byte-identical so the server prompt-cache prefix
|
|
270
|
+
// hash lines up. `ultra` is normalized to `max` on the wire too; the
|
|
271
|
+
// openai-oauth backend does not accept `ultra` as a wire value, so
|
|
272
|
+
// mirror that mapping here.
|
|
273
|
+
// WIRE-VERIFIED (40 response.create captures, 2026-07-03): the wire
|
|
274
|
+
// carries reasoning as {"effort":"..."} with NO summary field on
|
|
275
|
+
// gpt-5.5. Match the observed bytes.
|
|
278
276
|
reasoning: { effort: _normalizeReasoningEffort(opts.effort) },
|
|
279
277
|
store: process.env.MIXDOG_OAI_STORE === 'true' ? true : false,
|
|
280
278
|
stream: true,
|
|
@@ -41,8 +41,8 @@ export function _envOn(name) {
|
|
|
41
41
|
// (MIXDOG_OAI_CODEX_WIRE_PARITY=1 + ws-delta + underscore session_id) exactly
|
|
42
42
|
// as-is. These add EXTRA parity dimensions for backend fingerprint probes.
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
//
|
|
44
|
+
// The reference client sends dashed RFC-4122 UUIDs as session-id/thread-id;
|
|
45
|
+
// we key those dashed handshake headers off the underscore cacheKey by
|
|
46
46
|
// default. Opt in with MIXDOG_OAI_CODEX_WIRE_PARITY_UUID_IDS to reshape ONLY
|
|
47
47
|
// the dashed pair (session-id/thread-id/x-client-request-id) into codex's UUID
|
|
48
48
|
// format. The value is derived deterministically from the id so it stays
|
|
@@ -108,11 +108,12 @@ export function _codexBetaFeatures() {
|
|
|
108
108
|
return out.join(',');
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
// --- Opt-in raw WS capture for byte-diff
|
|
111
|
+
// --- Opt-in raw WS capture for wire byte-diff -------------------------------
|
|
112
112
|
// Enabled ONLY when MIXDOG_OAI_WS_DUMP_DIR names a directory. Persists the
|
|
113
113
|
// (redacted) handshake header metadata and the exact serialized
|
|
114
114
|
// response.create frame bytes so our wire format can be byte-diffed against
|
|
115
|
-
//
|
|
115
|
+
// the reference client. Secrets (Authorization / Cookie / account-id /
|
|
116
|
+
// routing tokens) are
|
|
116
117
|
// hashed, never written in clear. When the env is unset both helpers are
|
|
117
118
|
// no-ops, so there is no default behavior change.
|
|
118
119
|
const _WS_DUMP_SECRET_RE = /^(authorization|proxy-authorization|cookie|set-cookie|chatgpt-account-id|x-codex-turn-state|session_id|session-id|thread-id|x-codex-parent-thread-id|x-client-request-id|x-session-affinity)$/i;
|
|
@@ -110,16 +110,16 @@ function _selectIdleEntry(entries, compatibility) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// --- Cache-route probe state (2026-07-04 hunt) -----------------------------
|
|
113
|
-
// CF cookie stickiness (
|
|
113
|
+
// CF cookie stickiness (the reference client persists
|
|
114
114
|
// __cf_bm/_cfuvid across HTTP clients; our WS handshakes never echo them, so
|
|
115
115
|
// Cloudflare may re-shard every fresh socket). Jar is per-process, keyed by
|
|
116
116
|
// auth account. Env knobs (A/B):
|
|
117
117
|
// MIXDOG_OAI_CF_COOKIES=1 capture Set-Cookie from the 101 upgrade and
|
|
118
118
|
// send Cookie on subsequent handshakes
|
|
119
119
|
// MIXDOG_OAI_SESSION_AFFINITY=1 send x-session-affinity: <cacheKey>
|
|
120
|
-
// (
|
|
121
|
-
// MIXDOG_OAI_WS_URL_SESSION=0 drop the ?session_id= URL query (
|
|
122
|
-
//
|
|
120
|
+
// (a known cache-affinity hint)
|
|
121
|
+
// MIXDOG_OAI_WS_URL_SESSION=0 drop the ?session_id= URL query (reference
|
|
122
|
+
// clients all use the bare WS URL)
|
|
123
123
|
function _getPoolArr(poolKey) {
|
|
124
124
|
if (!poolKey) return null;
|
|
125
125
|
let arr = _wsPool.get(poolKey);
|
|
@@ -340,15 +340,15 @@ function _buildHandshakeHeaders({ auth, sessionToken, turnState, cacheKey: _cach
|
|
|
340
340
|
'x-codex-beta-features': _codexBetaFeatures(),
|
|
341
341
|
};
|
|
342
342
|
const isOpenAiOauth = auth.type !== 'xai' && auth.type !== 'openai-direct';
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
343
|
+
// The reference client sends only the dashed session-id/thread-id pair,
|
|
344
|
+
// but OUR backend measurements disagree with pure
|
|
345
|
+
// wire parity here: 2026-04-19 probes showed the OAuth backend dedupes
|
|
346
346
|
// its in-memory prefix state by the underscore session_id handshake
|
|
347
347
|
// header, and the only 0.0%-miss full-frame rounds (R7/R8, R15 regressed
|
|
348
348
|
// to 13% after this header was dropped) all had it present. Send both.
|
|
349
349
|
// The underscore session_id is the backend prefix-dedupe key (2026-04-19
|
|
350
|
-
// probes; R15 regressed to 13% miss when it was dropped).
|
|
351
|
-
//
|
|
350
|
+
// probes; R15 regressed to 13% miss when it was dropped). Strict wire
|
|
351
|
+
// parity sends ONLY the dashed pair, but dropping this
|
|
352
352
|
// header is a KNOWN cache-unsafe change — so the general parity flag no
|
|
353
353
|
// longer silently drops it. Keep it unless an operator EXPLICITLY opts
|
|
354
354
|
// into the codex-exact dashed-only wire via
|
|
@@ -432,7 +432,7 @@ function _openSocket({ auth, sessionToken, turnState, externalSignal, cacheKey,
|
|
|
432
432
|
if (process.env.MIXDOG_DEBUG_AGENT) {
|
|
433
433
|
process.stderr.write(`[agent-trace] ws-open-start url=${baseUrl} tokenHash=${createHash('sha256').update(String(sessionToken)).digest('hex').slice(0, 8)} ts=${_wsOpenStart}\n`);
|
|
434
434
|
}
|
|
435
|
-
// Bare WS URL by default (
|
|
435
|
+
// Bare WS URL by default (reference-client parity). Interleaved A/B
|
|
436
436
|
// (2026-07-04, ivA/ivB, 24 sessions each, alternating rounds to cancel
|
|
437
437
|
// server-time noise): dropping the ?session_id= query improved it1
|
|
438
438
|
// warmup-prefix hits 15/24 -> 22/24 and it2 full hits 11 -> 15 (miss
|
|
@@ -126,9 +126,8 @@ function _writeWsLifecycleTrace(lifecycle) {
|
|
|
126
126
|
process.stderr.write(`[ws-trace] t=${new Date().toISOString()} lifecycle=${lifecycle}\n`);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
// Wire-level `end_turn` on a terminal Responses frame
|
|
130
|
-
//
|
|
131
|
-
// consumed in core/src/session/turn.rs:2299 as "false ⇒ needs follow-up").
|
|
129
|
+
// Wire-level `end_turn` on a terminal Responses frame: an optional boolean on
|
|
130
|
+
// the completed response, where "false ⇒ needs follow-up".
|
|
132
131
|
// The field is optional: only a real boolean is normalized; anything else —
|
|
133
132
|
// including a missing field — stays undefined so absence is preserved and no
|
|
134
133
|
// caller can mistake "server said nothing" for "server said true/false".
|
|
@@ -35,6 +35,12 @@ let _initChain = Promise.resolve();
|
|
|
35
35
|
let _inFlightPromise = null;
|
|
36
36
|
let _inFlightSig = null;
|
|
37
37
|
let _lastAppliedSig = null;
|
|
38
|
+
// Provider instances are process-global inside the backend daemon. Catalog
|
|
39
|
+
// readers use this revision to share one raw model snapshot while still
|
|
40
|
+
// rebuilding their cheap route/config projection after auth/config changes.
|
|
41
|
+
let _providerCatalogRevision = 0;
|
|
42
|
+
let _startupCatalogRefreshPromise = null;
|
|
43
|
+
let _catalogRefreshPromise = null;
|
|
38
44
|
|
|
39
45
|
// Deterministic structural signature of a provider config. Recursively sorts
|
|
40
46
|
// object keys so signature equality reflects config-value equality regardless
|
|
@@ -141,7 +147,12 @@ export async function initProviders(config, { signal = null } = {}) {
|
|
|
141
147
|
}
|
|
142
148
|
};
|
|
143
149
|
const tracked = next.then(
|
|
144
|
-
(v) => {
|
|
150
|
+
(v) => {
|
|
151
|
+
if (_lastAppliedSig !== sig) _providerCatalogRevision += 1;
|
|
152
|
+
_lastAppliedSig = sig;
|
|
153
|
+
settle();
|
|
154
|
+
return v;
|
|
155
|
+
},
|
|
145
156
|
(err) => { settle(); throw err; },
|
|
146
157
|
);
|
|
147
158
|
_inFlightSig = sig;
|
|
@@ -236,6 +247,7 @@ export function getProvider(name) {
|
|
|
236
247
|
if (!Ctor) return undefined;
|
|
237
248
|
const inst = wrapProviderAdmission(new Ctor({}), name);
|
|
238
249
|
providers.set(name, inst);
|
|
250
|
+
_providerCatalogRevision += 1;
|
|
239
251
|
return inst;
|
|
240
252
|
}
|
|
241
253
|
if (name === 'openai-oauth' && hasOpenAIOAuthCredentials()) {
|
|
@@ -243,6 +255,7 @@ export function getProvider(name) {
|
|
|
243
255
|
if (!Ctor) return undefined;
|
|
244
256
|
const inst = wrapProviderAdmission(new Ctor({}), name);
|
|
245
257
|
providers.set(name, inst);
|
|
258
|
+
_providerCatalogRevision += 1;
|
|
246
259
|
return inst;
|
|
247
260
|
}
|
|
248
261
|
if (name === 'grok-oauth' && hasGrokOAuthCredentials()) {
|
|
@@ -250,6 +263,7 @@ export function getProvider(name) {
|
|
|
250
263
|
if (!Ctor) return undefined;
|
|
251
264
|
const inst = wrapProviderAdmission(new Ctor({}), name);
|
|
252
265
|
providers.set(name, inst);
|
|
266
|
+
_providerCatalogRevision += 1;
|
|
253
267
|
return inst;
|
|
254
268
|
}
|
|
255
269
|
return undefined;
|
|
@@ -287,6 +301,9 @@ export function getAllProviders() {
|
|
|
287
301
|
// stale entries across re-init (initProviders rebuilds the map in place).
|
|
288
302
|
return new Map(providers);
|
|
289
303
|
}
|
|
304
|
+
export function providerCatalogRevision() {
|
|
305
|
+
return _providerCatalogRevision;
|
|
306
|
+
}
|
|
290
307
|
// Narrow synchronous test seam for the lazy-OAuth boundary. It models a
|
|
291
308
|
// constructor whose module is already loaded while guaranteeing every touched
|
|
292
309
|
// registry entry is restored, even when the assertion callback throws.
|
|
@@ -311,6 +328,20 @@ export function _withLoadedProviderCtorForTest(name, Ctor, fn) {
|
|
|
311
328
|
else signatures.delete(name);
|
|
312
329
|
}
|
|
313
330
|
}
|
|
331
|
+
// Companion seam for registry walks that iterate live INSTANCES (what
|
|
332
|
+
// initProviders leaves behind) rather than constructors — the startup catalog
|
|
333
|
+
// refresh is one. Restores the prior entry even when the callback throws.
|
|
334
|
+
export function _withRegisteredProviderForTest(name, instance, fn) {
|
|
335
|
+
const hadProvider = providers.has(name);
|
|
336
|
+
const priorProvider = providers.get(name);
|
|
337
|
+
providers.set(name, instance);
|
|
338
|
+
try {
|
|
339
|
+
return fn();
|
|
340
|
+
} finally {
|
|
341
|
+
if (hadProvider) providers.set(name, priorProvider);
|
|
342
|
+
else providers.delete(name);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
314
345
|
// Background catalog warm-up. Each provider's listModels() either hits its
|
|
315
346
|
// own cached model list (no-op) or fires a single HTTP refresh. Called from
|
|
316
347
|
// agent.init() after providers are registered so the first agent dispatch call
|
|
@@ -336,7 +367,9 @@ function warmupCatalogs() {
|
|
|
336
367
|
}
|
|
337
368
|
}
|
|
338
369
|
|
|
339
|
-
// Force-refresh each provider's /models catalog
|
|
370
|
+
// Force-refresh each provider's /models catalog ONCE per backend-daemon
|
|
371
|
+
// lifetime. Every session runtime joins this process-global promise and then
|
|
372
|
+
// reads the same provider-instance caches. Unlike
|
|
340
373
|
// warmupCatalogs (which calls listModels() and so respects the 24h provider
|
|
341
374
|
// TTL → no-op when the cache is fresh), this bypasses the TTL via
|
|
342
375
|
// _refreshModelCache so a model released since the last refresh is picked up
|
|
@@ -345,6 +378,7 @@ function warmupCatalogs() {
|
|
|
345
378
|
// context metadata stays on its own 24h TTL. Fire-and-forget: never awaited,
|
|
346
379
|
// per-provider failures logged to stderr like warmupCatalogs.
|
|
347
380
|
export function refreshProviderCatalogsOnStartup() {
|
|
381
|
+
if (_startupCatalogRefreshPromise) return _startupCatalogRefreshPromise;
|
|
348
382
|
const pending = [];
|
|
349
383
|
for (const [name, provider] of providers) {
|
|
350
384
|
const refreshFn = typeof provider?._refreshModelCache === 'function'
|
|
@@ -361,7 +395,11 @@ export function refreshProviderCatalogsOnStartup() {
|
|
|
361
395
|
// Returns a completion promise so callers can invalidate stale model
|
|
362
396
|
// caches once the fresh catalogs land. Still fire-and-forget: unawaited
|
|
363
397
|
// callers keep the previous nonblocking startup behavior.
|
|
364
|
-
|
|
398
|
+
_startupCatalogRefreshPromise = Promise.allSettled(pending).then((results) => {
|
|
399
|
+
_providerCatalogRevision += 1;
|
|
400
|
+
return results;
|
|
401
|
+
});
|
|
402
|
+
return _startupCatalogRefreshPromise;
|
|
365
403
|
}
|
|
366
404
|
|
|
367
405
|
// Force-refresh provider catalogs after an operator changes model/provider
|
|
@@ -369,6 +407,7 @@ export function refreshProviderCatalogsOnStartup() {
|
|
|
369
407
|
// the shared LiteLLM metadata cache first so context/pricing metadata follows
|
|
370
408
|
// newly released models without waiting for the next process restart.
|
|
371
409
|
export function refreshCatalogs() {
|
|
410
|
+
if (_catalogRefreshPromise) return _catalogRefreshPromise;
|
|
372
411
|
const pending = [];
|
|
373
412
|
const metadataReady = Promise.resolve()
|
|
374
413
|
.then(() => refreshMetadataCatalog())
|
|
@@ -391,5 +430,13 @@ export function refreshCatalogs() {
|
|
|
391
430
|
}));
|
|
392
431
|
}
|
|
393
432
|
// Completion promise: lets callers drop stale model caches after refresh.
|
|
394
|
-
|
|
433
|
+
_catalogRefreshPromise = Promise.allSettled([metadataReady, ...pending])
|
|
434
|
+
.then((results) => {
|
|
435
|
+
_providerCatalogRevision += 1;
|
|
436
|
+
return results;
|
|
437
|
+
})
|
|
438
|
+
.finally(() => {
|
|
439
|
+
_catalogRefreshPromise = null;
|
|
440
|
+
});
|
|
441
|
+
return _catalogRefreshPromise;
|
|
395
442
|
}
|
|
@@ -252,7 +252,7 @@ function isRetryable(err) {
|
|
|
252
252
|
return classifyError(err) === 'transient'
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
/**
|
|
255
|
+
/** Anthropic request budget: 10 retries (11 attempts).
|
|
256
256
|
* CLAUDE_CODE_MAX_RETRIES is intentionally read per request for reload/tests.
|
|
257
257
|
* The upper bound prevents an accidental unbounded retry loop. */
|
|
258
258
|
export function anthropicMaxAttempts() {
|
|
@@ -262,16 +262,16 @@ export function anthropicMaxAttempts() {
|
|
|
262
262
|
return retries + 1
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
//
|
|
265
|
+
// Anthropic retry defaults: 500ms exponential backoff,
|
|
266
266
|
// capped at 32s, with positive-only jitter up to 25% of the base delay.
|
|
267
|
-
// The leading duplicate accounts for
|
|
267
|
+
// The leading duplicate accounts for the sleep-before-attempt index:
|
|
268
268
|
// retry attempt 2 reads index 1.
|
|
269
269
|
export const ANTHROPIC_RETRY_BACKOFF_MS = Object.freeze([
|
|
270
270
|
500, 500, 1000, 2000, 4000, 8000, 16000, 32000, 32000, 32000, 32000,
|
|
271
271
|
])
|
|
272
272
|
export const ANTHROPIC_RETRY_JITTER_RATIO = 0.25
|
|
273
273
|
|
|
274
|
-
//
|
|
274
|
+
// The Anthropic SDK client defaults API_TIMEOUT_MS to ten minutes.
|
|
275
275
|
// Read per request, like CLAUDE_CODE_MAX_RETRIES, so env reload/tests work.
|
|
276
276
|
export function anthropicRequestTimeoutMs() {
|
|
277
277
|
const parsed = Number.parseInt(process.env.API_TIMEOUT_MS || '', 10)
|
|
@@ -318,10 +318,10 @@ export function jitterDelayMs(ms, ratio = PROVIDER_RETRY_JITTER_RATIO, mode = 's
|
|
|
318
318
|
// Mid-stream 'stream_stalled' recoveries retry in place, which is right for a
|
|
319
319
|
// one-off blip but lets a chronically dying stream burn a whole task budget
|
|
320
320
|
// slowly (observed live: one send stretched 149s→298s→556s across stall
|
|
321
|
-
// retries before the agent deadline killed the task).
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
321
|
+
// retries before the agent deadline killed the task). A stalling stream is
|
|
322
|
+
// bounded instead of retried forever: a request is capped at ~300s wall
|
|
323
|
+
// clock (API_TIMEOUT_MS) and a stream dies after one 300s silent gap
|
|
324
|
+
// (stream idle timeout). This guard is the equivalent for our in-place
|
|
325
325
|
// recovery: the clock starts at the FIRST stall of a send, and stall-classified
|
|
326
326
|
// retries are allowed only inside that window; past it the stall error
|
|
327
327
|
// surfaces so loop-level transport retry issues a FRESH request. Healthy
|
|
@@ -357,7 +357,7 @@ export function createStallRetryBudget(budgetMs = STREAM_STALL_RETRY_BUDGET_MS,
|
|
|
357
357
|
// branched on a hardcoded provider name.
|
|
358
358
|
|
|
359
359
|
// F) Retry-budget profiles as DATA. The numbers live ONLY here now.
|
|
360
|
-
// ws.*Retries (5) — one
|
|
360
|
+
// ws.*Retries (5) — one Responses stream retry budget.
|
|
361
361
|
// sse.defaultRetries (3) — anthropic single-shot SSE mid-stream budget.
|
|
362
362
|
export const MIDSTREAM_RETRY_POLICY = {
|
|
363
363
|
ws: { transientCloseRetries: 5, defaultRetries: 5, backoff: [250, 1000, 2000, 4000, 5000] },
|
|
@@ -490,7 +490,7 @@ function _classifyMidstreamWs(err, state, attemptIndex, policy) {
|
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
// Explicit `response.failed` error codes/types that describe a transport-level
|
|
493
|
-
// interruption (
|
|
493
|
+
// interruption (these are retryable); every other code is terminal.
|
|
494
494
|
const RESPONSE_FAILED_CODE_CLASSIFIERS = new Map([
|
|
495
495
|
['stream_disconnected', 'response_failed_disconnected'],
|
|
496
496
|
['network_error', 'response_failed_network'],
|
|
@@ -550,7 +550,7 @@ export function shouldFallbackTransport(err, { signal, enabled = true } = {}) {
|
|
|
550
550
|
if (signal?.aborted) return false
|
|
551
551
|
// Transport fallback re-issues the request on another transport: it is a
|
|
552
552
|
// replay, so the exposure deny applies. Eligibility itself stays typed
|
|
553
|
-
// (status / errno / classifier)
|
|
553
|
+
// (status / errno / classifier) for the WS→HTTPS switch.
|
|
554
554
|
if (readStreamOutcome(err).replaySafe !== true) return false
|
|
555
555
|
const status = Number(err?.httpStatus || err?.status || 0)
|
|
556
556
|
// 401 is auth recovery, never transport fallback. 426 is the explicit
|
|
@@ -660,7 +660,7 @@ function _sleepChunkWithAbort(ms, signal, sleepFn, abortMessage) {
|
|
|
660
660
|
// E) Handshake classifier (moved here from openai-oauth-ws). Default-deny:
|
|
661
661
|
// anything not recognized as transient returns null. HTTP 401 is reserved
|
|
662
662
|
// for auth recovery and 426 for immediate HTTPS fallback. The OpenAI OAuth
|
|
663
|
-
// caller opts out of 429 retries (
|
|
663
|
+
// caller opts out of 429 retries (retry429:false); all other callers
|
|
664
664
|
// retain the historical retryable UnexpectedStatus policy.
|
|
665
665
|
export function classifyHandshakeError(err, { retry429 = true } = {}) {
|
|
666
666
|
if (!err) return null
|
|
@@ -764,8 +764,8 @@ export async function withRetry(fn, opts = {}) {
|
|
|
764
764
|
// an eligible failure is actually retried remains the typed question
|
|
765
765
|
// resolved by classifyError()/status below.
|
|
766
766
|
if (readStreamOutcome(caught).replaySafe !== true) throw caught
|
|
767
|
-
//
|
|
768
|
-
//
|
|
767
|
+
// x-should-retry:false is an explicit server veto on retrying and is
|
|
768
|
+
// honored as-is.
|
|
769
769
|
// Keep this ahead of status defaults, including the request-local 429 path.
|
|
770
770
|
const shouldRetryHeader = _headerValue(
|
|
771
771
|
caught?.headers || caught?.response?.headers || caught?.data?.responseHeaders,
|
|
@@ -786,7 +786,7 @@ export async function withRetry(fn, opts = {}) {
|
|
|
786
786
|
}
|
|
787
787
|
continue
|
|
788
788
|
}
|
|
789
|
-
//
|
|
789
|
+
// The optional model fallback fires on the third 529. This
|
|
790
790
|
// remains opt-in: providers pass fallbackModel only when the caller set
|
|
791
791
|
// one. The hard progress veto above must run first so fallback can never
|
|
792
792
|
// replay partial thinking/tool output.
|
|
@@ -91,7 +91,6 @@ import {
|
|
|
91
91
|
isOutputLimitStopReason,
|
|
92
92
|
providerContinuationSignal,
|
|
93
93
|
} from './loop/termination.mjs';
|
|
94
|
-
import { createSteeringLadder } from './loop/steering-ladder.mjs';
|
|
95
94
|
import { runPreSendCompactPass } from './pre-send-compact.mjs';
|
|
96
95
|
import { createEagerDispatcher } from './eager-dispatch.mjs';
|
|
97
96
|
import { sendWithRecovery } from './send-with-recovery.mjs';
|
|
@@ -238,7 +237,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
238
237
|
if (compactSettledToolCallBodies(messages) && !opts.cacheBreakIntent) {
|
|
239
238
|
opts.cacheBreakIntent = 'deferred_body_compaction';
|
|
240
239
|
}
|
|
241
|
-
// ----
|
|
240
|
+
// ---- Turn stop hook ----------------------------------------------------
|
|
242
241
|
// A no-tool assistant message is TERMINAL. Only a structured provider
|
|
243
242
|
// follow-up signal (end_turn=false / pause_turn), pending input, tool
|
|
244
243
|
// calls/results, or a stop hook that blocks with a continuation prompt keep
|
|
@@ -280,8 +279,8 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
280
279
|
}
|
|
281
280
|
// Tag steering-origin user messages so provider lowering keeps them
|
|
282
281
|
// distinct from preceding tool results. Keep each queued command as
|
|
283
|
-
// its own user turn
|
|
284
|
-
//
|
|
282
|
+
// its own user turn instead of collapsing priority/mode buckets
|
|
283
|
+
// together.
|
|
285
284
|
messages.push({
|
|
286
285
|
role: 'user',
|
|
287
286
|
content: merged.content,
|
|
@@ -366,9 +365,10 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
366
365
|
};
|
|
367
366
|
const maxLoopIterations = resolveSessionMaxLoopIterations(sessionRef);
|
|
368
367
|
// ---- Completion-first loop guards (worker runaway prevention) ----
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
//
|
|
368
|
+
// Behavior-steering hints (missed-parallelism, all-read-only, read-only
|
|
369
|
+
// shell, level-2 "stop exploring") were removed: they nudged tool shape
|
|
370
|
+
// instead of protecting resources. Only the staged iteration warnings, the
|
|
371
|
+
// hard cap, and the cross-turn dedup stub remain.
|
|
372
372
|
// _editCount counts any executed tool call whose def lacks readOnlyHint
|
|
373
373
|
// (i.e. edit/progress: apply_patch, bash, MCP writes, skills, ...).
|
|
374
374
|
let _editCount = 0;
|
|
@@ -406,7 +406,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
406
406
|
// Loop-level transport replays consumed this ask (see send-with-recovery
|
|
407
407
|
// TRANSPORT_RETRY_MAX): bounded per turn, reset only with a fresh ask.
|
|
408
408
|
let _transportRetriesUsed = 0;
|
|
409
|
-
//
|
|
409
|
+
// Queued prompt/task notifications are attached after a
|
|
410
410
|
// tool batch, before the continuation provider send. Normal batches drain
|
|
411
411
|
// up to 'next'; a Sleep-like tool grants a 'later' flush.
|
|
412
412
|
let _toolBatchJustCompleted = false;
|
|
@@ -415,20 +415,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
415
415
|
const name = String(call?.name || call?.toolName || call?.function?.name || '').toLowerCase();
|
|
416
416
|
return name === 'sleep' || name.endsWith('/sleep') || name.endsWith('.sleep');
|
|
417
417
|
};
|
|
418
|
-
// Completion-first steering ladder controller. Owns the (cumulative) level-1
|
|
419
|
-
// fire count, the all-read-only / serial-single / same-file-grep streaks,
|
|
420
|
-
// and the level-2 latch. Threaded via live getters so it reads the loop's
|
|
421
|
-
// current `iterations` / `_editCount` on every call (no stale snapshots).
|
|
422
|
-
const _steeringLadder = createSteeringLadder({
|
|
423
|
-
sessionId,
|
|
424
|
-
sessionAgent,
|
|
425
|
-
tools,
|
|
426
|
-
getIterations: () => iterations,
|
|
427
|
-
getEditCount: () => _editCount,
|
|
428
|
-
readOnlyRole: String(sessionRef?.permission || sessionRef?.toolPermission || '') === 'read',
|
|
429
|
-
pushUserMessage: (msg) => messages.push(msg),
|
|
430
|
-
pushSystemReminder: (text) => messages.push({ role: 'user', content: `<system-reminder>\n${text}\n</system-reminder>`, meta: 'hook' }),
|
|
431
|
-
});
|
|
432
418
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
433
419
|
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
434
420
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
@@ -503,8 +489,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
503
489
|
} catch { /* best-effort */ }
|
|
504
490
|
}
|
|
505
491
|
// Drain queued steering/prompts BEFORE the pre-send compact check, but
|
|
506
|
-
// only immediately after a tool batch has completed
|
|
507
|
-
// Claude Code's query.ts queued_command attachment drain: queued entries
|
|
492
|
+
// only immediately after a tool batch has completed: queued entries
|
|
508
493
|
// are attached after tool results are appended and before the recursive
|
|
509
494
|
// continuation, not on arbitrary non-tool continuations (empty nudges,
|
|
510
495
|
// iteration-cap final text turns, etc.).
|
|
@@ -792,11 +777,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
792
777
|
// tool-call-blocked vs contract-required oscillation.
|
|
793
778
|
if (!response.toolCalls?.length) {
|
|
794
779
|
// No tool calls. Decide between final-answer accept vs nudge.
|
|
795
|
-
// Reviewer fix: a zero-tool turn (final-pre-send steering drain or
|
|
796
|
-
// contract nudge `continue`) must not bridge the all-read-only
|
|
797
|
-
// streak across non-tool turns — that would fire level-2 early on
|
|
798
|
-
// a worker that paused to synthesize text mid-run.
|
|
799
|
-
_steeringLadder.resetAllReadOnlyStreak();
|
|
800
780
|
// - has content + non-hidden role → valid final, break.
|
|
801
781
|
// - empty content + hidden role → contract allows text-only
|
|
802
782
|
// terminal turn, break.
|
|
@@ -915,7 +895,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
915
895
|
messages.push({ role: 'user', content: nudgeMsg });
|
|
916
896
|
continue;
|
|
917
897
|
}
|
|
918
|
-
//
|
|
898
|
+
// Pending-input rule: queued user input is
|
|
919
899
|
// folded into needs_follow_up and evaluated BEFORE the stop hooks,
|
|
920
900
|
// so real steering always wins over a synthetic continuation prompt.
|
|
921
901
|
// Commit the terminal text first (beforeAppend), then resume.
|
|
@@ -926,7 +906,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
926
906
|
_emptyNudgeStreak = 0;
|
|
927
907
|
continue;
|
|
928
908
|
}
|
|
929
|
-
//
|
|
909
|
+
// This no-tool message ends the turn
|
|
930
910
|
// unless a stop hook blocks it. The unresolved-tool-failure hook may
|
|
931
911
|
// block exactly once — commit the assistant text, record the
|
|
932
912
|
// structural continuation prompt, resume sampling. Skipped on the
|
|
@@ -1064,7 +1044,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1064
1044
|
pending: eager.pending, epoch: eager.epoch, startEagerRun: eager.startEagerRun,
|
|
1065
1045
|
crossTurnCalls: _crossTurnCalls, crossTurnCap: _CROSS_TURN_CAP,
|
|
1066
1046
|
dedupStubTotal: _dedupStubTotal, editCount: _editCount,
|
|
1067
|
-
sessionAgent,
|
|
1047
|
+
sessionAgent,
|
|
1068
1048
|
pushToolResultMessage, throwIfAborted,
|
|
1069
1049
|
repeatFailLimit: REPEAT_FAIL_LIMIT,
|
|
1070
1050
|
}));
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import { _normalizeAbs, _statTuple, _statEqual } from './util.mjs';
|
|
4
4
|
import { clearScopedToolsForSession, clearScopedCounters } from './scoped-cache.mjs';
|
|
5
5
|
import { clearPostEditMarks } from './post-edit-marks.mjs';
|
|
6
|
+
import { registerSessionPurgeHook } from '../store.mjs';
|
|
7
|
+
import { releaseReadSnapshotScope } from '../../tools/builtin/snapshot-store.mjs';
|
|
6
8
|
|
|
7
9
|
const MAX_PER_SESSION = 100;
|
|
8
10
|
|
|
@@ -260,6 +262,11 @@ export function clearReadDedupSession(sessionId) {
|
|
|
260
262
|
clearScopedCounters(sessionId);
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
registerSessionPurgeHook((sessionId) => {
|
|
266
|
+
clearReadDedupSession(sessionId);
|
|
267
|
+
releaseReadSnapshotScope(sessionId, { deletePersisted: true, persist: false });
|
|
268
|
+
});
|
|
269
|
+
|
|
263
270
|
/**
|
|
264
271
|
* Extract the set of touched filesystem paths from a unified-diff patch text.
|
|
265
272
|
* Handles git-style `--- a/<path>` / `+++ b/<path>` headers and `/dev/null` markers.
|