mixdog 1.2.1 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/gate-local.mjs +19 -3
- package/scripts/model-catalog-audit.mjs +1 -15
- package/src/agents/reviewer/AGENT.md +3 -3
- package/src/agents/reviewer/agent.json +1 -1
- package/src/defaults/shipped-history.json +1 -0
- package/src/repl/slash-commands.mjs +16 -1
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
- package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
- package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
- package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
- package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
- package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
- package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
- package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
- package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
- package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
- package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
- package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
- package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
- package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
- package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
- package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
- package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
- package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
- package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
- package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
- package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
- package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
- package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
- package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
- package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
- package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/memory/lib/pg/process.mjs +34 -24
- package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
- package/src/runtime/office/README.md +1 -1
- package/src/runtime/office/shared/values.mjs +0 -5
- package/src/runtime/shared/background-tasks.mjs +92 -14
- package/src/runtime/shared/err-text.mjs +25 -0
- package/src/runtime/shared/error-presentation.mjs +159 -19
- package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
- package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
- package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
- package/src/runtime/shared/llm/model-catalog.mjs +53 -40
- package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
- package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
- package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
- package/src/runtime/shared/model-display.mjs +13 -0
- package/src/runtime/shared/quota-resume-store.mjs +11 -0
- package/src/runtime/shared/session-baseline-store.mjs +960 -0
- package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
- package/src/runtime/shared/task-notification-envelope.mjs +2 -2
- package/src/runtime/shared/turn-changes.mjs +447 -219
- package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
- package/src/runtime/shared/turn-snapshot.mjs +170 -48
- package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
- package/src/runtime/shared/worktrees/contract.mjs +0 -24
- package/src/runtime/shared/worktrees/test-support.mjs +19 -4
- package/src/session-runtime/boot/apis.mjs +6 -0
- package/src/session-runtime/boot/infrastructure.mjs +6 -0
- package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
- package/src/session-runtime/lifecycle/teardown.mjs +1 -1
- package/src/session-runtime/mcp-glue/connect.mjs +29 -9
- package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
- package/src/session-runtime/mcp-glue.mjs +3 -0
- package/src/session-runtime/mcp-oauth-login.mjs +174 -0
- package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
- package/src/session-runtime/model-route/set-route.mjs +83 -15
- package/src/session-runtime/model-route/web-search-route.mjs +1 -1
- package/src/session-runtime/native-web-search/routes.mjs +22 -3
- package/src/session-runtime/provider-models/model-meta.mjs +12 -3
- package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
- package/src/session-runtime/quick-web-search-models.mjs +13 -38
- package/src/session-runtime/resource-extension-sync.mjs +4 -0
- package/src/session-runtime/resource-mcp-api.mjs +98 -2
- package/src/session-runtime/runtime-review-api.mjs +13 -0
- package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
- package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
- package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
- package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
- package/src/session-runtime/settings-compaction-api.mjs +1 -0
- package/src/standalone/daemon-drain-policy.mjs +8 -0
- package/src/standalone/daemon-telemetry.mjs +55 -0
- package/src/standalone/daemon.mjs +3 -7
- package/src/standalone/session-protocol.mjs +7 -0
- package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
- package/src/standalone/session-service/agent-tree.mjs +12 -0
- package/src/standalone/session-service/quota-resume.mjs +8 -0
- package/src/standalone/session-service/retention/busy.mjs +15 -1
- package/src/standalone/session-service-api.mjs +5 -0
- package/src/standalone/session-service.mjs +1 -0
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/create-app-pickers.mjs +4 -0
- package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
- package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
- package/src/tui/app/extension-pickers.mjs +2 -1
- package/src/tui/app/model-options.mjs +11 -16
- package/src/tui/app/slash-commands.mjs +7 -2
- package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
- package/src/tui/components/StatusLine.jsx +2 -0
- package/src/tui/components/status-line/local-line.mjs +5 -2
- package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
- package/src/tui/dist/index.mjs +816 -43
- package/src/tui/session/quota-resume.mjs +46 -14
- package/src/tui/session/session-api/commands.mjs +33 -24
- package/src/tui/session/session-api/extensions.mjs +22 -0
- package/src/tui/session/session-api/integrations.mjs +7 -0
- package/src/tui/session/session-api/lifecycle.mjs +5 -1
- package/src/tui/session/session-api/routes.mjs +71 -7
- package/src/tui/session/session-api/settings.mjs +5 -14
- package/src/tui/session/session-flow/drain.mjs +7 -1
- package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
- package/src/tui/session/turn.mjs +38 -15
- package/src/tui/statusline-ansi-bridge.mjs +1 -1
- package/src/ui/statusline-agents.mjs +15 -4
- package/src/ui/statusline.mjs +7 -3
- package/src/ui/streaming-markdown-heal.mjs +34 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
- package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
- package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
isRetryableWireErrorEvent,
|
|
13
13
|
isRetryableStreamErrorEvent,
|
|
14
14
|
isProviderRecoveryExhausted,
|
|
15
|
+
isProviderRecoveryTerminal,
|
|
15
16
|
isServerUnavailable,
|
|
16
17
|
isStreamCut,
|
|
17
18
|
isToolInputCut,
|
|
@@ -20,7 +21,9 @@ import {
|
|
|
20
21
|
resetStallRetryBudget,
|
|
21
22
|
resolveStallRetryBudget,
|
|
22
23
|
retryAfterMsFromError,
|
|
24
|
+
sleepWithAbort,
|
|
23
25
|
} from '../providers/retry-classifier.mjs';
|
|
26
|
+
import { DETERMINISTIC_KINDS, providerErrorKind } from '../providers/provider-error-kind.mjs';
|
|
24
27
|
import {
|
|
25
28
|
persistsConfirmedImageRejection,
|
|
26
29
|
promptHasInlineImages,
|
|
@@ -34,7 +37,6 @@ import {
|
|
|
34
37
|
isThinkingReplayRejection,
|
|
35
38
|
repairThinkingReplayInPlace,
|
|
36
39
|
} from './thinking-replay-recovery.mjs';
|
|
37
|
-
import { setTimeout as sleepMs } from 'node:timers/promises';
|
|
38
40
|
import { readStreamOutcome } from '../providers/lib/stream-outcome.mjs';
|
|
39
41
|
import { cloneProviderReplay } from '../providers/lib/provider-replay.mjs';
|
|
40
42
|
import { resolveWorkerCompactPolicy } from './loop/compact-policy.mjs';
|
|
@@ -126,10 +128,10 @@ const RECOVERY_RETRY_BACKOFF_MS = retryLadderFromEnv(
|
|
|
126
128
|
[15_000, 30_000, 60_000, 60_000, 60_000]
|
|
127
129
|
);
|
|
128
130
|
|
|
129
|
-
// A server-advised wait (Retry-After) replaces the ladder step
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
131
|
+
// A server-advised wait (Retry-After / retryAfterMs) replaces the ladder step
|
|
132
|
+
// and is honored in full, never shortened. The ladder length
|
|
133
|
+
// still bounds how many replays run, the wait stays cancellable, and the
|
|
134
|
+
// reconnecting stage reports its remaining delay (retryAt).
|
|
133
135
|
|
|
134
136
|
// Jittering every backoff by 0.9–1.1 means parallel workers that lose the
|
|
135
137
|
// same uplink do not resume in lockstep; our provider layers already jitter
|
|
@@ -138,14 +140,42 @@ const TRANSPORT_RETRY_JITTER_RATIO = 0.1;
|
|
|
138
140
|
|
|
139
141
|
/** The ladder this failure class is entitled to: an unreachable network, a
|
|
140
142
|
* provider still unavailable after its own retries, or an ordinary fault. */
|
|
141
|
-
function retryLadderFor(error) {
|
|
143
|
+
function retryLadderFor(error, state = null) {
|
|
142
144
|
if (isConnectFailure(error)) return CONNECTION_RETRY_BACKOFF_MS;
|
|
143
|
-
if (isProviderRecoveryExhausted(error) && isServerUnavailable(error))
|
|
145
|
+
if (isProviderRecoveryExhausted(error) && isServerUnavailable(error) && recoveryCyclesEnabled(state)) {
|
|
146
|
+
return RECOVERY_RETRY_BACKOFF_MS;
|
|
147
|
+
}
|
|
144
148
|
return TRANSPORT_RETRY_BACKOFF_MS;
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
// retryPolicy.recoveryCycles:false — a provider that already ran its own full
|
|
152
|
+
// ladder surfaces its spent failure instead of parking on
|
|
153
|
+
// the 15/30/60/60/60s recovery cycles.
|
|
154
|
+
function recoveryCyclesEnabled(state) {
|
|
155
|
+
return loopRetryPolicy(state).recoveryCycles !== false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Per-provider loop retry policy, read off the provider object
|
|
159
|
+
// (`provider.retryPolicy`), default = the bounded ladders:
|
|
160
|
+
// unboundedConnectionRetry — a connect failure keeps waiting on the
|
|
161
|
+
// 5/10/20/40/60s… ladder (last step repeating) until the network returns;
|
|
162
|
+
// still cancellable, with "waiting for network" progress.
|
|
163
|
+
// contentFilterRetryable — a content_filter refusal is replayed
|
|
164
|
+
// under the transport budget with a contextual guidance notice.
|
|
165
|
+
// inBandInvalidRequestRetryable — an in-band stream error whose only
|
|
166
|
+
// deterministic evidence is its invalid_request envelope is replayed.
|
|
167
|
+
function loopRetryPolicy(state) {
|
|
168
|
+
const policy = state?.provider?.retryPolicy;
|
|
169
|
+
return policy && typeof policy === 'object' ? policy : {};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function waitsForNetwork(error, state) {
|
|
173
|
+
return loopRetryPolicy(state).unboundedConnectionRetry === true && isConnectFailure(error);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function retryBudgetFor(error, state = null) {
|
|
177
|
+
if (waitsForNetwork(error, state)) return Number.POSITIVE_INFINITY;
|
|
178
|
+
return retryLadderFor(error, state).length;
|
|
149
179
|
}
|
|
150
180
|
|
|
151
181
|
// A provider ladder spent on a connect failure has only shown that the network
|
|
@@ -153,15 +183,19 @@ function retryBudgetFor(error) {
|
|
|
153
183
|
// unavailable parks on the recovery cycles, but only while nothing has reached
|
|
154
184
|
// the user — a replay would otherwise re-show output or re-run a tool. Every
|
|
155
185
|
// other exhausted failure surfaces as-is.
|
|
156
|
-
|
|
186
|
+
// A provider VERDICT that recovery is over (an advice-gated denial, the
|
|
187
|
+
// repeated-overload stop) always ends it: the loop never overrides it.
|
|
188
|
+
function providerRecoveryEnded(error, outcome, state = null) {
|
|
189
|
+
if (isProviderRecoveryTerminal(error)) return true;
|
|
157
190
|
if (!isProviderRecoveryExhausted(error) || isConnectFailure(error)) return false;
|
|
191
|
+
if (!recoveryCyclesEnabled(state)) return true;
|
|
158
192
|
return !(outcome.replaySafe === true && isServerUnavailable(error));
|
|
159
193
|
}
|
|
160
194
|
|
|
161
|
-
function transportRetryWaitMs(attemptIndex, error) {
|
|
195
|
+
function transportRetryWaitMs(attemptIndex, error, state = null) {
|
|
162
196
|
const advisedMs = retryAfterMsFromError(error);
|
|
163
|
-
if (advisedMs != null) return
|
|
164
|
-
const ladder = retryLadderFor(error);
|
|
197
|
+
if (advisedMs != null) return advisedMs;
|
|
198
|
+
const ladder = retryLadderFor(error, state);
|
|
165
199
|
const base = ladder[Math.min(Math.max(attemptIndex, 0), ladder.length - 1)];
|
|
166
200
|
return jitterDelayMs(base, TRANSPORT_RETRY_JITTER_RATIO);
|
|
167
201
|
}
|
|
@@ -258,6 +292,46 @@ function traceLoop({ sessionId, nextIteration }, kind, fields) {
|
|
|
258
292
|
// 5s/15s wait rendered as a frozen turn with no explanation, so the stage
|
|
259
293
|
// change surfaces "Reconnecting... n/max" instead of leaving the user
|
|
260
294
|
// staring at a stalled stream.
|
|
295
|
+
// Unbounded connect-failure wait (retryPolicy.unboundedConnectionRetry): no
|
|
296
|
+
// "n/max" — the turn waits until the network is back, counting down each step.
|
|
297
|
+
function emitWaitingForNetwork(opts, { attempt, waitMs, classifier, error }) {
|
|
298
|
+
if (typeof opts?.onStageChange !== 'function') return;
|
|
299
|
+
try {
|
|
300
|
+
const wait = Math.max(0, Number(waitMs) || 0);
|
|
301
|
+
opts.onStageChange('reconnecting', {
|
|
302
|
+
attempt,
|
|
303
|
+
max: null,
|
|
304
|
+
waitMs: wait,
|
|
305
|
+
retryAt: Date.now() + wait,
|
|
306
|
+
reason: 'waiting for network',
|
|
307
|
+
waitingForNetwork: true,
|
|
308
|
+
classifier: classifier || error?.code || null,
|
|
309
|
+
message: `Reconnecting… waiting for network · attempt ${attempt}${wait > 0 ? ` in ${Math.ceil(wait / 1000)}s` : ''}`,
|
|
310
|
+
});
|
|
311
|
+
} catch {
|
|
312
|
+
/* display-only */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// content_filter on a path that opted in (retryPolicy.contentFilterRetryable,
|
|
317
|
+
// or the provider boundary stamping `contentFilterRetryable` on the error).
|
|
318
|
+
// A provider-terminal verdict outranks the opt-in.
|
|
319
|
+
function isReplayableContentFilter(sendErr, state) {
|
|
320
|
+
if (isProviderRecoveryTerminal(sendErr)) return false;
|
|
321
|
+
const optedIn = loopRetryPolicy(state).contentFilterRetryable === true || sendErr?.contentFilterRetryable === true;
|
|
322
|
+
return optedIn && providerErrorKind(sendErr).code === 'content_filter';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// The guidance travels as a contextual runtime fragment: the
|
|
326
|
+
// [mixdog-runtime] prefix keeps it out of the visible transcript.
|
|
327
|
+
const CONTENT_FILTER_NOTICE =
|
|
328
|
+
'[mixdog-runtime] The previous response was stopped by the provider content filter before it completed. ' +
|
|
329
|
+
'Continue the task, but rephrase or avoid the content that likely triggered the filter; do not repeat it verbatim.';
|
|
330
|
+
|
|
331
|
+
function contentFilterNotice() {
|
|
332
|
+
return { role: 'user', content: CONTENT_FILTER_NOTICE, meta: { source: 'content-filter-recovery' } };
|
|
333
|
+
}
|
|
334
|
+
|
|
261
335
|
function emitLoopReconnectProgress(opts, { attempt, maxAttempts, waitMs, classifier, error }) {
|
|
262
336
|
emitProviderRetryStage(opts?.onStageChange, {
|
|
263
337
|
attempt,
|
|
@@ -353,10 +427,22 @@ async function recoverFromSendError(sendErr, state) {
|
|
|
353
427
|
}
|
|
354
428
|
// Clean transient transport failure with zero exposure: replay the send
|
|
355
429
|
// after a bounded wait instead of failing the turn.
|
|
430
|
+
if (isReplayableContentFilter(sendErr, state)) {
|
|
431
|
+
if (transportRetriesUsed < TRANSPORT_RETRY_MAX && (outcome.replaySafe === true || (await retract()))) {
|
|
432
|
+
return scheduleTransportReplay(sendErr, state, {
|
|
433
|
+
budget: TRANSPORT_RETRY_MAX,
|
|
434
|
+
note: `[loop] content filter stopped the response (${loopTag(state)}); replaying with guidance`,
|
|
435
|
+
trace: { kind: 'content_filter_retry' },
|
|
436
|
+
classifier: 'content_filter',
|
|
437
|
+
notice: contentFilterNotice(),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
throw sendErr;
|
|
441
|
+
}
|
|
356
442
|
if (
|
|
357
|
-
transportRetriesUsed < retryBudgetFor(sendErr) &&
|
|
358
|
-
!providerRecoveryEnded(sendErr, outcome) &&
|
|
359
|
-
(await transportReplayPermitted(sendErr, outcome, retract))
|
|
443
|
+
transportRetriesUsed < retryBudgetFor(sendErr, state) &&
|
|
444
|
+
!providerRecoveryEnded(sendErr, outcome, state) &&
|
|
445
|
+
(await transportReplayPermitted(sendErr, outcome, retract, state))
|
|
360
446
|
) {
|
|
361
447
|
return retryTransport(sendErr, state);
|
|
362
448
|
}
|
|
@@ -373,7 +459,7 @@ async function recoverFromSendError(sendErr, state) {
|
|
|
373
459
|
) {
|
|
374
460
|
return repairThinkingReplay(recoveryMessages, state);
|
|
375
461
|
}
|
|
376
|
-
//
|
|
462
|
+
// Image-strip retry: drop inline images and replay once. Safe
|
|
377
463
|
// only when no tool was dispatched. Text exposure must be retracted first;
|
|
378
464
|
// reasoning-only high-effort streams can retry without a text reset.
|
|
379
465
|
if (
|
|
@@ -464,7 +550,7 @@ function isExposedNoToolStall(sendErr, outcome) {
|
|
|
464
550
|
// 'transient' and never fired).
|
|
465
551
|
async function recoverExposedStall(sendErr, outcome, state, retract) {
|
|
466
552
|
const { transportRetriesUsed = 0 } = state;
|
|
467
|
-
const exposedRetryBudget = retryBudgetFor(sendErr);
|
|
553
|
+
const exposedRetryBudget = retryBudgetFor(sendErr, state);
|
|
468
554
|
const len = sendErr.partialContent.length;
|
|
469
555
|
if (transportRetriesUsed < exposedRetryBudget && !isProviderRecoveryExhausted(sendErr) && (await retract())) {
|
|
470
556
|
return scheduleTransportReplay(sendErr, state, {
|
|
@@ -491,12 +577,17 @@ async function recoverExposedStall(sendErr, outcome, state, retract) {
|
|
|
491
577
|
async function scheduleTransportReplay(sendErr, state, { budget, note, trace, classifier, notice = null }) {
|
|
492
578
|
const { opts, signal, transportRetriesUsed = 0 } = state;
|
|
493
579
|
const attempt = transportRetriesUsed + 1;
|
|
494
|
-
const waitMs = transportRetryWaitMs(transportRetriesUsed, sendErr);
|
|
580
|
+
const waitMs = transportRetryWaitMs(transportRetriesUsed, sendErr, state);
|
|
495
581
|
logLoop(`${note}; transport retry ${attempt}/${budget} after ${waitMs}ms`);
|
|
496
582
|
const { kind, ...fields } = trace;
|
|
497
583
|
traceLoop(state, kind, { attempt, waitMs, ...fields });
|
|
498
|
-
|
|
499
|
-
|
|
584
|
+
if (Number.isFinite(budget)) {
|
|
585
|
+
emitLoopReconnectProgress(opts, { attempt, maxAttempts: budget, waitMs, classifier, error: sendErr });
|
|
586
|
+
} else {
|
|
587
|
+
emitWaitingForNetwork(opts, { attempt, waitMs, classifier, error: sendErr });
|
|
588
|
+
}
|
|
589
|
+
// Chunked, abortable sleep: honored advice may exceed one timer's range.
|
|
590
|
+
await sleepWithAbort(waitMs, signal, undefined, 'transport retry wait aborted');
|
|
500
591
|
return beginFreshTransportAttempt(opts, budget, notice);
|
|
501
592
|
}
|
|
502
593
|
|
|
@@ -542,7 +633,7 @@ function continueFromShownText(sendErr, state, relayWitness) {
|
|
|
542
633
|
// budget still bounds it.
|
|
543
634
|
async function recoverToolInputCut(sendErr, outcome, state, retract) {
|
|
544
635
|
const { transportRetriesUsed = 0 } = state;
|
|
545
|
-
const budget = retryBudgetFor(sendErr);
|
|
636
|
+
const budget = retryBudgetFor(sendErr, state);
|
|
546
637
|
if (transportRetriesUsed < budget && (outcome.replaySafe === true || (await retract()))) {
|
|
547
638
|
return scheduleTransportReplay(sendErr, state, {
|
|
548
639
|
budget,
|
|
@@ -650,20 +741,55 @@ function recoverPartialToolCalls(sendErr, state, toolCalls, outcome) {
|
|
|
650
741
|
// reasoning/text exposure makes classifyError() intentionally permanent,
|
|
651
742
|
// non-terminal close, or a dropped connection (undici `terminated`, socket
|
|
652
743
|
// reset) — then retract the exposed text and replay.
|
|
653
|
-
async function transportReplayPermitted(sendErr, outcome, retract) {
|
|
654
|
-
if (
|
|
655
|
-
|
|
744
|
+
async function transportReplayPermitted(sendErr, outcome, retract, state) {
|
|
745
|
+
if (!isSemanticallyRetryable(sendErr, state)) return false;
|
|
746
|
+
// Existing replay-safety gate: an unexposed failure replays outright only
|
|
747
|
+
// when it is typed-transient (or an opted-in in-band invalid_request); a
|
|
748
|
+
// named symptom replays after retraction.
|
|
749
|
+
if (
|
|
750
|
+
outcome.replaySafe === true &&
|
|
751
|
+
(classifyError(sendErr) === 'transient' || inBandInvalidRequestReplay(sendErr, state))
|
|
752
|
+
) {
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
return await retract();
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// The ONE semantic verdict on whether this failure is worth re-issuing,
|
|
759
|
+
// independent of what was exposed (the replay-safety gate decides that
|
|
760
|
+
// afterwards): never a deterministic refusal (quota, plan, billing, policy,
|
|
761
|
+
// auth, permission, context or size) — server advice never makes one
|
|
762
|
+
// retryable — and otherwise a named transient/transport symptom.
|
|
763
|
+
function inBandInvalidRequestReplay(sendErr, state) {
|
|
764
|
+
return (
|
|
765
|
+
loopRetryPolicy(state).inBandInvalidRequestRetryable === true &&
|
|
766
|
+
providerErrorKind(sendErr).kind === 'invalid_request' &&
|
|
767
|
+
isRetryableStreamErrorEvent(sendErr)
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function isSemanticallyRetryable(sendErr, state) {
|
|
772
|
+
if (isProviderRecoveryTerminal(sendErr)) return false;
|
|
773
|
+
// Opt-in (retryPolicy.inBandInvalidRequestRetryable): a provider whose
|
|
774
|
+
// in-band stream errors carry an invalid_request envelope for server faults
|
|
775
|
+
// replays one with no other deterministic evidence; for every other
|
|
776
|
+
// provider it is a final refusal.
|
|
777
|
+
const { kind } = providerErrorKind(sendErr);
|
|
778
|
+
if (DETERMINISTIC_KINDS.has(kind) && !inBandInvalidRequestReplay(sendErr, state)) {
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
return (
|
|
656
782
|
classifyError(sendErr) === 'transient' ||
|
|
657
783
|
isStreamCut(sendErr) ||
|
|
658
784
|
isRetryableWireErrorEvent(sendErr) ||
|
|
659
785
|
isRetryableStreamErrorEvent(sendErr) ||
|
|
660
|
-
isCursorTransientTransportError(sendErr)
|
|
661
|
-
|
|
786
|
+
isCursorTransientTransportError(sendErr)
|
|
787
|
+
);
|
|
662
788
|
}
|
|
663
789
|
|
|
664
790
|
function retryTransport(sendErr, state) {
|
|
665
791
|
return scheduleTransportReplay(sendErr, state, {
|
|
666
|
-
budget: retryBudgetFor(sendErr),
|
|
792
|
+
budget: retryBudgetFor(sendErr, state),
|
|
667
793
|
note:
|
|
668
794
|
`[loop] transient send failure with no dispatched tools (${loopTag(state)} code=${sendErr?.code ?? 'n/a'} ` +
|
|
669
795
|
`wsCloseCode=${sendErr?.wsCloseCode ?? 'n/a'} httpStatus=${sendErr?.httpStatus ?? sendErr?.status ?? 'n/a'})`,
|
|
@@ -700,7 +826,7 @@ function imageStripEligible(
|
|
|
700
826
|
hasImages: promptHasInlineImages(recoveryMessages),
|
|
701
827
|
alreadyStripped: imageStripUsed === true,
|
|
702
828
|
}) ||
|
|
703
|
-
// Grok
|
|
829
|
+
// Grok only: a bare stream error on a Grok
|
|
704
830
|
// upload may be the image body being rejected. Other providers' transient
|
|
705
831
|
// stream errors are not evidence about images and use normal transport retry.
|
|
706
832
|
(isGrok &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MessageChannel, Worker } from 'node:worker_threads';
|
|
2
|
-
import { guardedSaveOptions as _guardedSaveOptions } from './write-guards.mjs';
|
|
2
|
+
import { guardedSaveOptions as _guardedSaveOptions, releaseSessionWriteControl } from './write-guards.mjs';
|
|
3
3
|
import { _ensureLifecycleFields } from './serialize.mjs';
|
|
4
4
|
import {
|
|
5
5
|
_attemptEvidence,
|
|
@@ -63,9 +63,12 @@ export { _hasSessionSaveBaseline } from './save-delta.mjs';
|
|
|
63
63
|
* the writes, so a write already posted for `id` still lands first; the next
|
|
64
64
|
* save for `id`, if any, simply goes out as a full snapshot.
|
|
65
65
|
*/
|
|
66
|
-
export function forgetSessionSaveBaseline(id) {
|
|
66
|
+
export function forgetSessionSaveBaseline(id, { releaseWriteControl = false } = {}) {
|
|
67
67
|
if (!id) return false;
|
|
68
68
|
const had = _dropDeltaBaseline(id);
|
|
69
|
+
// Only when the session itself is gone (close/teardown/delete): a mid-life
|
|
70
|
+
// forget (compaction) must keep the landed-epoch fence for in-flight writes.
|
|
71
|
+
if (releaseWriteControl) releaseSessionWriteControl(String(id));
|
|
69
72
|
if (_saveWorker) {
|
|
70
73
|
try {
|
|
71
74
|
_saveWorker.postMessage({ __forgetBase: String(id) });
|
|
@@ -854,7 +857,7 @@ export function purgeSessionSaveBookkeeping(id) {
|
|
|
854
857
|
}
|
|
855
858
|
}
|
|
856
859
|
// Also frees the worker-side base: a deleted id never saves again.
|
|
857
|
-
forgetSessionSaveBaseline(id);
|
|
860
|
+
forgetSessionSaveBaseline(id, { releaseWriteControl: true });
|
|
858
861
|
return purged;
|
|
859
862
|
}
|
|
860
863
|
|
|
@@ -49,6 +49,22 @@ export function guardedSaveOptions(id, opts, epoch = null) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Drop the id's control buffer once the session is gone (close/forget/delete)
|
|
54
|
+
* so the map does not grow by one SharedArrayBuffer per session ever saved.
|
|
55
|
+
* Kept while a commit holds the lock: that write still serialises on this
|
|
56
|
+
* buffer. A later save for the same id simply allocates a fresh control.
|
|
57
|
+
*/
|
|
58
|
+
export function releaseSessionWriteControl(id) {
|
|
59
|
+
const control = _writeControls.get(id);
|
|
60
|
+
if (!control || Atomics.load(control, 1) !== 0) return false;
|
|
61
|
+
return _writeControls.delete(id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function _hasSessionWriteControl(id) {
|
|
65
|
+
return _writeControls.has(id);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
export function cancelSessionWrites(id) {
|
|
53
69
|
Atomics.add(writeControl(id), 0, 1);
|
|
54
70
|
}
|
|
@@ -106,13 +106,25 @@ function agentStatusValues(row) {
|
|
|
106
106
|
.filter((status, index, values) => Boolean(status) && values.indexOf(status) === index);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/** The worker's lifecycle. `status` decides; `stage` is only the runtime phase
|
|
110
|
+
* copied in by the progress sweep, so mid-work it may read idle/done/error
|
|
111
|
+
* beside a worker that is still running. Only a row with no status falls
|
|
112
|
+
* back to its stage. */
|
|
113
|
+
function workerLifecycle(row) {
|
|
114
|
+
return cleanValue(row?.status) || cleanValue(row?.stage);
|
|
115
|
+
}
|
|
116
|
+
|
|
109
117
|
function activeAgentWorker(row) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
)
|
|
118
|
+
// A cancel in either field ends the work (bare `cancelling` is still work).
|
|
119
|
+
if (
|
|
120
|
+
agentStatusValues(row).some(
|
|
121
|
+
(status) => CANCELLED_AGENT_STATUS.test(status) || CANCEL_UNCONFIRMED_AGENT_STATUS.test(status)
|
|
122
|
+
)
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const lifecycle = workerLifecycle(row);
|
|
127
|
+
return Boolean(lifecycle) && !DEAD_AGENT_STATUS.test(lifecycle) && LIVING_AGENT_STATUS.test(lifecycle);
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
function stampMs(value) {
|
|
@@ -287,7 +299,7 @@ const withinPoolWindow = (at, now) => at > 0 && now - at <= AGENT_POOL_HEARTBEAT
|
|
|
287
299
|
* a working status whose own stamp is still inside the pool window. */
|
|
288
300
|
function poolRowWorking(row, heartbeatMtimes, now) {
|
|
289
301
|
if (withinPoolWindow(heartbeatMtimes.get(cleanValue(row?.sessionId)) || 0, now)) return true;
|
|
290
|
-
if (!WORKING_AGENT_STATUS.test(
|
|
302
|
+
if (!WORKING_AGENT_STATUS.test(workerLifecycle(row))) return false;
|
|
291
303
|
return withinPoolWindow(Date.parse(cleanValue(row?.updatedAt)) || 0, now);
|
|
292
304
|
}
|
|
293
305
|
|
|
@@ -307,7 +319,7 @@ function projectChildWorkerRow(row, { now, heartbeatMtimes }) {
|
|
|
307
319
|
const session = readWorkerSessionHeader(sessionId);
|
|
308
320
|
const declaredStatus = cleanValue(row.status) || 'running';
|
|
309
321
|
const declaredStage = cleanValue(row.stage || row.status) || 'running';
|
|
310
|
-
const declaredWorking = WORKING_AGENT_STATUS.test(
|
|
322
|
+
const declaredWorking = WORKING_AGENT_STATUS.test(workerLifecycle(row) || 'running');
|
|
311
323
|
const heartbeatFresh = withinPoolWindow(heartbeatMtimes.get(sessionId) || 0, now);
|
|
312
324
|
const recentlyUpdated = withinPoolWindow(Date.parse(cleanValue(row.updatedAt)) || 0, now);
|
|
313
325
|
const runtimePid = positiveNumber(row.runtimePid, 0);
|
|
@@ -414,7 +426,7 @@ function promoteHeartbeatSidecar(bySessionId, sessionId, heartbeatAt) {
|
|
|
414
426
|
bySessionId.set(sessionId, sidecarWorkerRow(current, session, { ...identity, status: cancel.status }));
|
|
415
427
|
return;
|
|
416
428
|
}
|
|
417
|
-
const currentStatus =
|
|
429
|
+
const currentStatus = workerLifecycle(current);
|
|
418
430
|
if (currentStatus && !WORKING_AGENT_STATUS.test(currentStatus)) return;
|
|
419
431
|
// The sidecar is the live lease. Durable child sessions are intentionally
|
|
420
432
|
// detached/closed while their external owner runs.
|
|
@@ -452,7 +464,7 @@ function projectLeadWorkerRow(row, { now, heartbeatMtimes }) {
|
|
|
452
464
|
const heartbeatAt = heartbeatMtimes.get(sessionId) || 0;
|
|
453
465
|
const heartbeatFresh = withinPoolWindow(heartbeatAt, now);
|
|
454
466
|
const recentlyUpdated = withinPoolWindow(Date.parse(cleanValue(row.updatedAt)) || 0, now);
|
|
455
|
-
const declaredStatus =
|
|
467
|
+
const declaredStatus = workerLifecycle(row) || 'idle';
|
|
456
468
|
const working = WORKING_AGENT_STATUS.test(declaredStatus) && (heartbeatFresh || recentlyUpdated);
|
|
457
469
|
const reapAt = Date.parse(cleanValue(row.reapAt)) || 0;
|
|
458
470
|
if (!heartbeatFresh && reapAt > 0 && now >= reapAt) return null;
|
|
@@ -21,6 +21,7 @@ import { dataDir } from './store-summary-locations.mjs';
|
|
|
21
21
|
import {
|
|
22
22
|
desktopSession,
|
|
23
23
|
importKey,
|
|
24
|
+
inheritedFromSessionId,
|
|
24
25
|
isStoredSessionId,
|
|
25
26
|
positiveNumber,
|
|
26
27
|
workspaceSummary,
|
|
@@ -188,6 +189,7 @@ export function normalizedRow(row, heartbeatAt = 0) {
|
|
|
188
189
|
storageSize: positiveNumber(row.storageSize, 0),
|
|
189
190
|
detachedReason: row.detachedReason || null,
|
|
190
191
|
importKey: importKey(row),
|
|
192
|
+
inheritedFromSessionId: inheritedFromSessionId(row),
|
|
191
193
|
};
|
|
192
194
|
}
|
|
193
195
|
|
|
@@ -50,6 +50,11 @@ export function workspaceSummary(value) {
|
|
|
50
50
|
|
|
51
51
|
/** Dedupe key of an imported conversation: a session's `importOrigin.key`,
|
|
52
52
|
* or a summary row's projected `importKey`; null for ordinary sessions. */
|
|
53
|
+
export function inheritedFromSessionId(value) {
|
|
54
|
+
const id = typeof value?.inheritedFromSessionId === 'string' ? value.inheritedFromSessionId.trim() : '';
|
|
55
|
+
return id || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
export function importKey(value) {
|
|
54
59
|
const key = typeof value?.importKey === 'string' ? value.importKey : value?.importOrigin?.key;
|
|
55
60
|
return typeof key === 'string' && key.trim() ? key : null;
|
|
@@ -20,6 +20,7 @@ import { cleanSessionPreview, isSessionPreviewNoise, sessionMessageText } from '
|
|
|
20
20
|
import { sessionVisibility } from './store-summary-visibility.mjs';
|
|
21
21
|
import {
|
|
22
22
|
importKey,
|
|
23
|
+
inheritedFromSessionId,
|
|
23
24
|
isStoredSessionId,
|
|
24
25
|
positiveNumber as _positiveNumber,
|
|
25
26
|
workspaceSummary,
|
|
@@ -209,6 +210,7 @@ export function _sessionSummary(session) {
|
|
|
209
210
|
// Imported-conversation identity, so "already imported" is answered
|
|
210
211
|
// from summaries without loading sessions.
|
|
211
212
|
importKey: importKey(session),
|
|
213
|
+
inheritedFromSessionId: inheritedFromSessionId(session),
|
|
212
214
|
};
|
|
213
215
|
}
|
|
214
216
|
|
|
@@ -258,6 +260,7 @@ function _normalizeSummaryRow(row) {
|
|
|
258
260
|
storageSize: _positiveNumber(row.storageSize, 0),
|
|
259
261
|
detachedReason: row.detachedReason || null,
|
|
260
262
|
importKey: importKey(row),
|
|
263
|
+
inheritedFromSessionId: inheritedFromSessionId(row),
|
|
261
264
|
};
|
|
262
265
|
}
|
|
263
266
|
|
|
@@ -286,7 +286,9 @@ function mergeRestoredToolItems(items) {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
function restoredMessageItemUpperBound(message) {
|
|
289
|
-
const boundaries =
|
|
289
|
+
const boundaries =
|
|
290
|
+
(Array.isArray(message?.meta?.sessionInheritances) ? message.meta.sessionInheritances.length : 0) +
|
|
291
|
+
(Array.isArray(message?.meta?.modelSwitches) ? message.meta.modelSwitches.length : 0);
|
|
290
292
|
if (message?.role === 'user') return 1 + boundaries;
|
|
291
293
|
if (message?.role !== 'assistant') return boundaries;
|
|
292
294
|
const calls = restoredMessageToolCalls(message);
|
|
@@ -377,6 +379,19 @@ function restoredUserTranscriptItems(message, nextId) {
|
|
|
377
379
|
return [restoredNotificationToolItem(synthetic, text, message, nextId, /^(failed|error|killed|cancelled)$/i)];
|
|
378
380
|
}
|
|
379
381
|
|
|
382
|
+
/** The transcript row of an in-place model switch (live and restored). */
|
|
383
|
+
export function modelSwitchItem(id, boundary) {
|
|
384
|
+
return {
|
|
385
|
+
kind: 'statusdone',
|
|
386
|
+
id,
|
|
387
|
+
status: 'model-switched',
|
|
388
|
+
label: 'Model switched',
|
|
389
|
+
provider: boundary.provider,
|
|
390
|
+
modelId: boundary.modelId,
|
|
391
|
+
at: boundary.at,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
380
395
|
function restoreTranscriptRange(messages, start, sessionId) {
|
|
381
396
|
const items = [];
|
|
382
397
|
const pendingToolCalls = new Map();
|
|
@@ -408,6 +423,10 @@ function restoreTranscriptRange(messages, start, sessionId) {
|
|
|
408
423
|
at: boundary.at,
|
|
409
424
|
});
|
|
410
425
|
}
|
|
426
|
+
for (const boundary of Array.isArray(message?.meta?.modelSwitches) ? message.meta.modelSwitches : []) {
|
|
427
|
+
if (!boundary?.provider || !boundary?.modelId) continue;
|
|
428
|
+
items.push(modelSwitchItem(restoredId(), boundary));
|
|
429
|
+
}
|
|
411
430
|
}
|
|
412
431
|
return mergeRestoredToolItems(items);
|
|
413
432
|
}
|
|
@@ -786,9 +786,9 @@ function guardShell(a) {
|
|
|
786
786
|
}
|
|
787
787
|
// Over-max is an unambiguous "wait as long as allowed"; clamp like the other
|
|
788
788
|
// saturating integer args instead of failing the call.
|
|
789
|
-
if (hasOwn(a, 'wait_ms') && Number.isInteger(a.wait_ms) && a.wait_ms >
|
|
789
|
+
if (hasOwn(a, 'wait_ms') && Number.isInteger(a.wait_ms) && a.wait_ms > 30000) a.wait_ms = 30000;
|
|
790
790
|
if (hasOwn(a, 'wait_ms') && (!Number.isInteger(a.wait_ms) || a.wait_ms < 1000)) {
|
|
791
|
-
return `Error: shell arg "wait_ms" must be an integer from
|
|
791
|
+
return `Error: shell arg "wait_ms" must be an integer from 10000 to 30000, e.g. wait_ms:20000 (got ${JSON.stringify(a.wait_ms)})`;
|
|
792
792
|
}
|
|
793
793
|
if (!hasOwn(a, 'command')) {
|
|
794
794
|
return 'Error: shell requires "command"';
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// Commands start in the foreground. Only work still running after the
|
|
2
|
-
//
|
|
2
|
+
// 10 s coordination budget is promoted to a tracked background task.
|
|
3
3
|
// Short commands therefore complete in the original tool turn, while longer
|
|
4
4
|
// work returns partial output plus task_id and finishes by notification.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
export const
|
|
5
|
+
// A per-call wait_ms may stretch the window for a command whose result should
|
|
6
|
+
// return in the same call, never past 30 s, so the conversation is never held
|
|
7
|
+
// open long; anything longer finishes by notification.
|
|
8
|
+
export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 10_000;
|
|
9
|
+
export const MAX_SHELL_WAIT_MS = 30_000;
|
|
10
10
|
|
|
11
11
|
// JS timers (setTimeout) and PS WaitForExit(ms) are 32-bit: a delay above
|
|
12
12
|
// 2^31-1 wraps to a tiny/negative value and fires immediately. Clamp the
|
|
@@ -17,17 +17,14 @@ const TIMER_MAX_MS = 2_147_483_647;
|
|
|
17
17
|
|
|
18
18
|
// Main-agent blocking budget. A timeout is the command's total deadline, not
|
|
19
19
|
// permission to hold the conversation open for that whole duration: after
|
|
20
|
-
//
|
|
20
|
+
// 10 s a still-running command becomes a tracked background task and
|
|
21
21
|
// completion is pushed to the owner. MIXDOG_SHELL_AUTO_BACKGROUND_MS
|
|
22
22
|
// overrides; an explicit 0 disables. Gated on backgroundOnTimeout so
|
|
23
23
|
// disabled background tasks remain foreground.
|
|
24
|
-
// A per-call wait_ms extends that default for the call, capped by
|
|
25
|
-
// foreground maximum; it never shortens it. A
|
|
26
|
-
// command that would have returned its result in
|
|
27
|
-
// a whole follow-up request on `task wait`.
|
|
28
|
-
// 3062 shell calls): 329 passed a wait_ms under the default (53% of one model
|
|
29
|
-
// family's calls), 256 of those were promoted, and 108 of the 192 with a
|
|
30
|
-
// recorded duration had finished inside the default window (10 s then).
|
|
24
|
+
// A per-call wait_ms extends that default for the call, capped by
|
|
25
|
+
// MAX_SHELL_WAIT_MS and the foreground maximum; it never shortens it. A
|
|
26
|
+
// shorter window promotes a command that would have returned its result in
|
|
27
|
+
// the starting call and spends a whole follow-up request on `task wait`.
|
|
31
28
|
function autoBackgroundBudget(backgroundOnTimeout, timeout, waitMs, maxForegroundMs) {
|
|
32
29
|
const raw = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
|
|
33
30
|
const parsed = Number(raw);
|
|
@@ -35,7 +32,7 @@ function autoBackgroundBudget(backgroundOnTimeout, timeout, waitMs, maxForegroun
|
|
|
35
32
|
raw != null && String(raw).trim() !== '' && Number.isFinite(parsed) && parsed >= 0
|
|
36
33
|
? Math.floor(parsed)
|
|
37
34
|
: DEFAULT_SHELL_AUTO_BACKGROUND_MS;
|
|
38
|
-
if (waitMs > 0) defaultMs = Math.max(defaultMs, Math.min(waitMs, maxForegroundMs));
|
|
35
|
+
if (waitMs > 0) defaultMs = Math.max(defaultMs, Math.min(waitMs, MAX_SHELL_WAIT_MS, maxForegroundMs));
|
|
39
36
|
if (!backgroundOnTimeout || defaultMs <= 0) return 0;
|
|
40
37
|
return timeout > 0 ? Math.min(defaultMs, timeout) : defaultMs;
|
|
41
38
|
}
|
|
@@ -146,7 +146,7 @@ export const BUILTIN_TOOLS = [
|
|
|
146
146
|
openWorldHint: true,
|
|
147
147
|
compressible: true,
|
|
148
148
|
},
|
|
149
|
-
description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a
|
|
149
|
+
description: `Run programs, builds, tests and computation. Never use shell commands or scripts for work covered by dedicated tools: cat/head/tail→read, ls→list, find→glob, filename lookup→find, grep/rg→grep, code structure→code_graph, file edits/writes (sed/awk/redirection)→edit/apply_patch, Git→git when that tool is on the surface. Tool names are not shell commands. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'After a 10s foreground window (not a timeout; omit wait_ms by default, set it up to 30s only for a command you expect to finish in 10–30s, e.g. one test file), unfinished work continues under task_id; use task wait, not read polling. If no execution slot frees within the admission wait, the command is queued as a background task with a task_id. Dev servers/watchers: pass timeout_ms or stop them with task cancel when done.'}`,
|
|
150
150
|
inputSchema: {
|
|
151
151
|
type: 'object',
|
|
152
152
|
properties: {
|
|
@@ -155,14 +155,14 @@ export const BUILTIN_TOOLS = [
|
|
|
155
155
|
type: 'integer',
|
|
156
156
|
minimum: 0,
|
|
157
157
|
description:
|
|
158
|
-
'Hard kill deadline in ms (omit or 0 = none), separate from the
|
|
158
|
+
'Hard kill deadline in ms (omit or 0 = none), separate from the 10s foreground window; set one for throwaway probes so a slow check fails fast.',
|
|
159
159
|
},
|
|
160
160
|
wait_ms: {
|
|
161
161
|
type: 'integer',
|
|
162
|
-
minimum:
|
|
163
|
-
maximum:
|
|
162
|
+
minimum: 10000,
|
|
163
|
+
maximum: 30000,
|
|
164
164
|
description:
|
|
165
|
-
'Foreground wait in ms before unfinished work continues as a background task (default
|
|
165
|
+
'Foreground wait in ms before unfinished work continues as a background task (default 10000). Omit it by default. Set it only when you expect the command to finish within 10–30s (e.g. one test file or a small build step); never for work expected to take longer, which finishes by notification.',
|
|
166
166
|
},
|
|
167
167
|
},
|
|
168
168
|
required: ['command'],
|