mixdog 1.4.0 → 1.4.1
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/check-voice-runtime-inputs.mjs +25 -0
- package/scripts/gate-local.mjs +12 -3
- package/scripts/latest-guard.mjs +21 -9
- package/scripts/lib/isolated-test-env.mjs +10 -5
- package/scripts/lib/run-node-tests.mjs +37 -37
- package/scripts/lib/scoped-temp.d.mts +16 -0
- package/scripts/lib/scoped-temp.mjs +123 -0
- package/scripts/lib/stale-temp.mjs +44 -0
- package/scripts/post-release-check.mjs +1 -6
- package/scripts/pptx-kit-index.mjs +17 -4
- package/scripts/suite-health.mjs +10 -1
- package/scripts/test-data-dir.mjs +10 -7
- package/src/app.mjs +4 -0
- package/src/defaults/shipped-history.json +5 -0
- package/src/defaults/skills/browser-use/SKILL.md +6 -4
- package/src/defaults/skills/computer-use/SKILL.md +4 -2
- package/src/lib/rules-builder.cjs +29 -12
- package/src/output-styles/common.md +2 -2
- package/src/output-styles/simple.md +1 -1
- package/src/rules/agent/AGENT.md +5 -4
- package/src/rules/lead/LEAD.md +10 -5
- package/src/rules/shared/10-tool-workflow.md +3 -1
- package/src/rules/shared/45-destructive.md +0 -2
- package/src/rules/shared/50-execution.md +2 -0
- package/src/rules/shared/70-delivery.md +7 -9
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
- package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
- package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
- package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
- package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
- package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
- package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
- package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
- package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
- package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
- package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
- package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
- package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
- package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
- package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
- package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
- package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
- package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
- package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
- package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
- package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
- package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
- package/src/runtime/channels/lib/whisper-port.mjs +10 -1
- package/src/runtime/computer-bridge/action-schema.mjs +2 -2
- package/src/runtime/computer-bridge/core-actions.mjs +2 -2
- package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
- package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
- package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
- package/src/runtime/local-provider/server-process.mjs +2 -1
- package/src/runtime/media/adapters/gemini-video.mjs +4 -1
- package/src/runtime/media/adapters/xai-media.mjs +2 -1
- package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
- package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
- package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
- package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
- package/src/runtime/office/design/design-art-direction.mjs +29 -27
- package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
- package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
- package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
- package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
- package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
- package/src/runtime/office/quality/design-review.mjs +1 -5
- package/src/runtime/office/quality/document-brief.mjs +1 -5
- package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
- package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
- package/src/runtime/shared/bot-owned-work.mjs +56 -0
- package/src/runtime/shared/defaults-separation.mjs +1 -4
- package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
- package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
- package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
- package/src/runtime/shared/handoff-artifacts.mjs +18 -8
- package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
- package/src/runtime/shared/llm/cost.mjs +5 -1
- package/src/runtime/shared/loopback-listener.mjs +25 -2
- package/src/runtime/shared/model-route-chain.d.mts +7 -2
- package/src/runtime/shared/model-route-chain.mjs +22 -4
- package/src/runtime/shared/open-url.mjs +10 -0
- package/src/runtime/shared/session-activity-signal.mjs +14 -0
- package/src/runtime/shared/session-scratch.mjs +37 -0
- package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
- package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +3 -0
- package/src/runtime/shared/tool-work-units.mjs +2 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
- package/src/runtime/shared/worktrees/contract.mjs +4 -1
- package/src/runtime/shared/worktrees/git.mjs +15 -0
- package/src/runtime/shared/worktrees/manager.mjs +87 -38
- package/src/runtime/shared/worktrees/test-support.mjs +6 -10
- package/src/runtime/visualize-bridge/client.mjs +2 -0
- package/src/session-runtime/agent-home-delivery.mjs +41 -0
- package/src/session-runtime/agent-task-sessions.mjs +152 -63
- package/src/session-runtime/agent-tool-routing.mjs +8 -41
- package/src/session-runtime/boot/tools.mjs +5 -1
- package/src/session-runtime/bot-status.mjs +12 -6
- package/src/session-runtime/bot-target.mjs +38 -0
- package/src/session-runtime/channel-config-api.mjs +6 -1
- package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
- package/src/session-runtime/conversation-import/discovery.mjs +271 -25
- package/src/session-runtime/conversation-import/service.mjs +26 -5
- package/src/session-runtime/handoff-attachments.mjs +82 -31
- package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
- package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
- package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
- package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
- package/src/session-runtime/model-route/set-route.mjs +11 -2
- package/src/session-runtime/provider-catalog-cache.mjs +82 -36
- package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
- package/src/session-runtime/provider-models.mjs +4 -2
- package/src/session-runtime/resource-recall-api.mjs +6 -1
- package/src/session-runtime/runtime-feature-gates.mjs +5 -1
- package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
- package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
- package/src/session-runtime/services/agent-tool/render.mjs +71 -17
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
- package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
- package/src/session-runtime/services/provider-admin.mjs +2 -1
- package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
- package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
- package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
- package/src/session-runtime/workspace/transitions.mjs +25 -0
- package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
- package/src/standalone/daemon-session-control.mjs +18 -2
- package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
- package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
- package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
- package/src/tui/app/app-view/floating-panel.jsx +7 -4
- package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
- package/src/tui/app/model-picker/route-selection.mjs +5 -6
- package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
- package/src/tui/app/provider-setup/provider-items.mjs +8 -2
- package/src/tui/app/provider-setup-picker.mjs +9 -5
- package/src/tui/components/QuestionPanel.jsx +13 -4
- package/src/tui/dist/index.mjs +75 -28
- package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
- package/src/ui/statusline-agents.mjs +1 -3
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
// stage, lastStreamDeltaAt, lastTransportAt, firstSemanticAt,
|
|
18
18
|
// lastSemanticAt, lastReasoningAt, lastVisibleTextAt, lastToolProtocolAt,
|
|
19
19
|
// lastToolCall, lastError, updatedAt,
|
|
20
|
+
// modelRequestStartedAt, requestTransportAt, requestFirstSemanticAt,
|
|
21
|
+
// modelResponseAccepted, // the current logical model request (see
|
|
22
|
+
// // _setLogicalModelRequest); the fields above
|
|
23
|
+
// // stay turn-level history
|
|
20
24
|
// controller?: AbortController, // set while an ask is in flight
|
|
21
25
|
// generation?: number, // snapshot taken at ask start
|
|
22
26
|
// closed?: boolean, // flipped by closeSession()
|
|
@@ -131,6 +135,23 @@ function _startToolActivityHeartbeat(id) {
|
|
|
131
135
|
_toolActivityHeartbeats.set(id, timer);
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
// One logical model request owns one first-output allowance: its request
|
|
139
|
+
// clock and its own first transport / first semantic marks. It begins with the
|
|
140
|
+
// ask's first request, with the first request after the model's previous
|
|
141
|
+
// response was accepted (its tool calls ran — the request that carries the
|
|
142
|
+
// tool results), and with a request after a settled turn. Every other entry
|
|
143
|
+
// into `requesting` — a provider or loop retry, a route fallback, a no-tool
|
|
144
|
+
// recovery continuation — re-sends the same logical request and keeps all of
|
|
145
|
+
// it, so a retry never earns a fresh allowance. The turn-level marks
|
|
146
|
+
// (firstSemanticAt, lastTransportAt, …) are history and are not touched.
|
|
147
|
+
// `startedAt` null: no request yet (an ask starting).
|
|
148
|
+
function _setLogicalModelRequest(entry, startedAt) {
|
|
149
|
+
entry.modelRequestStartedAt = startedAt;
|
|
150
|
+
entry.requestTransportAt = null;
|
|
151
|
+
entry.requestFirstSemanticAt = null;
|
|
152
|
+
entry.modelResponseAccepted = false;
|
|
153
|
+
}
|
|
154
|
+
|
|
134
155
|
export function updateSessionStage(id, stage) {
|
|
135
156
|
if (stage === MODEL_THINKING_STAGE) {
|
|
136
157
|
markSessionRequestSent(id);
|
|
@@ -143,8 +164,11 @@ export function updateSessionStage(id, stage) {
|
|
|
143
164
|
entry.stage = stage;
|
|
144
165
|
// A new stage (a retried attempt included) has not sent its request yet.
|
|
145
166
|
entry.requestSentAt = null;
|
|
146
|
-
if (
|
|
147
|
-
|
|
167
|
+
if (
|
|
168
|
+
stage === 'requesting' &&
|
|
169
|
+
(!entry.modelRequestStartedAt || entry.modelResponseAccepted || TERMINAL_STAGES.has(priorStage))
|
|
170
|
+
) {
|
|
171
|
+
_setLogicalModelRequest(entry, now);
|
|
148
172
|
}
|
|
149
173
|
entry.lastProgressAt = now;
|
|
150
174
|
entry.updatedAt = now;
|
|
@@ -192,7 +216,7 @@ export function markSessionAskStart(id) {
|
|
|
192
216
|
// after provider admission (the scheduler emits stage=requesting).
|
|
193
217
|
const now = Date.now();
|
|
194
218
|
entry.askStartedAt = now;
|
|
195
|
-
entry
|
|
219
|
+
_setLogicalModelRequest(entry, null);
|
|
196
220
|
entry.lastProgressAt = now;
|
|
197
221
|
entry.updatedAt = now;
|
|
198
222
|
entry.taskWaits = 0;
|
|
@@ -229,13 +253,14 @@ export function markSessionTransportActivity(id) {
|
|
|
229
253
|
if (!entry || entry.closed || entry.controller?.signal?.aborted || TERMINAL_STAGES.has(entry.stage)) return;
|
|
230
254
|
const now = Date.now();
|
|
231
255
|
entry.lastTransportAt = now;
|
|
256
|
+
entry.requestTransportAt = now;
|
|
232
257
|
entry.updatedAt = now;
|
|
233
258
|
}
|
|
234
259
|
function _normalizeModelProgressKind(kind) {
|
|
235
260
|
let value = 'semantic';
|
|
236
261
|
if (typeof kind === 'string') value = kind;
|
|
237
262
|
else if (kind && typeof kind.kind === 'string') value = kind.kind;
|
|
238
|
-
if (value === 'transport' || value === 'reasoning' || value === 'text' || value === 'tool') {
|
|
263
|
+
if (value === 'transport' || value === 'keepalive' || value === 'reasoning' || value === 'text' || value === 'tool') {
|
|
239
264
|
return value;
|
|
240
265
|
}
|
|
241
266
|
return 'semantic';
|
|
@@ -257,12 +282,22 @@ export async function markSessionStreamDelta(id, kind = 'semantic') {
|
|
|
257
282
|
markSessionTransportActivity(id);
|
|
258
283
|
return;
|
|
259
284
|
}
|
|
285
|
+
if (progressKind === 'keepalive') {
|
|
286
|
+
markSessionTransportActivity(id);
|
|
287
|
+
// A keepalive before the request's first semantic event is covered by the
|
|
288
|
+
// first-output window and never counts as progress. lastProgressAt stays
|
|
289
|
+
// untouched: the watchdog caps how long keepalives may extend it.
|
|
290
|
+
if (entry.requestFirstSemanticAt) entry.lastKeepaliveAt = Date.now();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
260
293
|
_stopToolActivityHeartbeat(id);
|
|
261
294
|
const now = Date.now();
|
|
262
295
|
// Every semantic model event also proves transport health. The inverse is
|
|
263
296
|
// intentionally false: raw chunks/keepalives update only lastTransportAt.
|
|
264
297
|
entry.lastTransportAt = now;
|
|
298
|
+
entry.requestTransportAt = now;
|
|
265
299
|
if (!entry.firstSemanticAt) entry.firstSemanticAt = now;
|
|
300
|
+
if (!entry.requestFirstSemanticAt) entry.requestFirstSemanticAt = now;
|
|
266
301
|
entry.lastSemanticAt = now;
|
|
267
302
|
entry.lastSemanticKind = progressKind;
|
|
268
303
|
if (progressKind === 'reasoning') entry.lastReasoningAt = now;
|
|
@@ -296,7 +331,12 @@ export function markSessionToolCall(id, toolName) {
|
|
|
296
331
|
entry.toolOutputTailAt = null;
|
|
297
332
|
entry.toolStartedAt = Date.now();
|
|
298
333
|
entry.lastTransportAt = entry.toolStartedAt;
|
|
334
|
+
entry.requestTransportAt = entry.toolStartedAt;
|
|
299
335
|
if (!entry.firstSemanticAt) entry.firstSemanticAt = entry.toolStartedAt;
|
|
336
|
+
if (!entry.requestFirstSemanticAt) entry.requestFirstSemanticAt = entry.toolStartedAt;
|
|
337
|
+
// The response whose tool call runs now was accepted: the next model
|
|
338
|
+
// request carries its tool results and is a new logical request.
|
|
339
|
+
entry.modelResponseAccepted = true;
|
|
300
340
|
entry.lastSemanticAt = entry.toolStartedAt;
|
|
301
341
|
entry.lastSemanticKind = 'tool';
|
|
302
342
|
entry.lastToolProtocolAt = entry.toolStartedAt;
|
|
@@ -436,6 +476,18 @@ export function markSessionCancelled(id) {
|
|
|
436
476
|
entry.emptyFinalAt = null;
|
|
437
477
|
_unlinkParentAbortListener(entry);
|
|
438
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* The session's current logical model request: when it started and whether
|
|
481
|
+
* it has produced semantic output yet. A provider stream opened for it — a
|
|
482
|
+
* retry or a route fallback included — bounds its first-output phase by this
|
|
483
|
+
* request instead of starting a fresh one. Null without a request.
|
|
484
|
+
*/
|
|
485
|
+
export function getLogicalModelRequest(sessionId) {
|
|
486
|
+
const entry = sessionId ? _runtimeState.get(sessionId) : null;
|
|
487
|
+
if (!entry?.modelRequestStartedAt) return null;
|
|
488
|
+
return { startedAt: entry.modelRequestStartedAt, outputSeen: Boolean(entry.requestFirstSemanticAt) };
|
|
489
|
+
}
|
|
490
|
+
|
|
439
491
|
export function getSessionRuntime(id) {
|
|
440
492
|
return id ? _runtimeState.get(id) || null : null;
|
|
441
493
|
}
|
|
@@ -458,8 +510,10 @@ export function getSessionProgressSnapshot(sessionId) {
|
|
|
458
510
|
const firstActivityAt = firstSemanticAt;
|
|
459
511
|
const stage = entry.stage || 'idle';
|
|
460
512
|
const activeModelStage = stage === 'connecting' || stage === 'requesting' || stage === 'streaming';
|
|
461
|
-
|
|
462
|
-
|
|
513
|
+
// Judged per logical model request: output an earlier request of this turn
|
|
514
|
+
// produced never ends a later request's first-output phase.
|
|
515
|
+
const waitingForTransport = Boolean(modelRequestStartedAt && activeModelStage && !entry.requestTransportAt);
|
|
516
|
+
const waitingForFirstSemantic = Boolean(modelRequestStartedAt && activeModelStage && !entry.requestFirstSemanticAt);
|
|
463
517
|
return {
|
|
464
518
|
stage,
|
|
465
519
|
askStartedAt,
|
|
@@ -481,6 +535,7 @@ export function getSessionProgressSnapshot(sessionId) {
|
|
|
481
535
|
toolOutputTail: entry.toolOutputTail || null,
|
|
482
536
|
toolOutputTailAt: entry.toolOutputTailAt || 0,
|
|
483
537
|
lastProgressAt: entry.lastProgressAt || 0,
|
|
538
|
+
lastKeepaliveAt: entry.lastKeepaliveAt || 0,
|
|
484
539
|
updatedAt: entry.updatedAt || 0,
|
|
485
540
|
hasFirstActivity: Boolean(firstSemanticAt && (!askStartedAt || firstSemanticAt >= askStartedAt)),
|
|
486
541
|
hasFirstSemantic: Boolean(firstSemanticAt && (!askStartedAt || firstSemanticAt >= askStartedAt)),
|
|
@@ -42,11 +42,13 @@ import { ensureCodexWireSessionId, mintSessionId } from './session-id.mjs';
|
|
|
42
42
|
import { providerCacheKey } from './provider-cache-key.mjs';
|
|
43
43
|
import { buildSessionRecord, normalizeDesktopSessionMetadata, sessionOriginFields } from './session-record.mjs';
|
|
44
44
|
import { normalizeWorkflowRoute } from '../../runtime-core/workflow.mjs';
|
|
45
|
+
import { executionFallbacks } from '../../../../shared/model-route-chain.mjs';
|
|
45
46
|
import { ownerRouteFallbacks } from './owner-route-fallbacks.mjs';
|
|
46
47
|
import { applyPendingWorkspaceReroot, refreshSessionBp3Environment } from './prompt-utils.mjs';
|
|
47
48
|
import { isWorktreeBound } from '../../../../shared/worktrees/contract.mjs';
|
|
48
49
|
import { applyAgentToolExposure } from '../loop/tool-exposure-migration.mjs';
|
|
49
50
|
import { prewarmSessionAutoEffort } from './ask-turn-auto-effort.mjs';
|
|
51
|
+
import { cancelCacheWarm } from '../cache-warm.mjs';
|
|
50
52
|
|
|
51
53
|
// Durable turn-checkpoint recovery; re-exported so prior importers of this
|
|
52
54
|
// module stay unchanged.
|
|
@@ -239,11 +241,13 @@ export function createSession(createOpts) {
|
|
|
239
241
|
? Object.hasOwn(opts, 'routeFallbacks')
|
|
240
242
|
? opts.routeFallbacks
|
|
241
243
|
: ownerRouteFallbacks({ sessionMode, agentId: actingAgent, provider: providerName, model: modelName })
|
|
242
|
-
:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
: executionFallbacks(
|
|
245
|
+
normalizeWorkflowRoute({
|
|
246
|
+
provider: providerName,
|
|
247
|
+
model: modelName,
|
|
248
|
+
fallbacks: Object.hasOwn(opts, 'routeFallbacks') ? opts.routeFallbacks : presetObj?.fallbacks,
|
|
249
|
+
})
|
|
250
|
+
);
|
|
247
251
|
// Provider cache strategy — agentRuntime.resolveSync() above is a
|
|
248
252
|
// best-effort injection point (setAgentRuntime() has no live caller
|
|
249
253
|
// today, so that branch never fires); build it directly here so every
|
|
@@ -262,7 +266,7 @@ export function createSession(createOpts) {
|
|
|
262
266
|
: agentProfileRevision(surface.resolvedAgent, { mixy: sessionMode === AGENT_MODE });
|
|
263
267
|
// A bot thread lives on across releases: the rules it was built from.
|
|
264
268
|
const rulesRevision = agentProfile && sessionMode === AGENT_MODE ? promptRulesRevision() : '';
|
|
265
|
-
const prompt = composeSessionSystem(opts, { profile, providerName, modelName, surface, sessionMode });
|
|
269
|
+
const prompt = composeSessionSystem(opts, { profile, providerName, modelName, surface, sessionMode, sessionId: id });
|
|
266
270
|
const messages = seedSessionMessages(prompt, opts.files);
|
|
267
271
|
logSessionSurface(surface);
|
|
268
272
|
const contextMeta = resolveSessionContextMeta(provider, modelName, {
|
|
@@ -322,24 +326,27 @@ export function contextSeedForRouteUpdate(session, routeChanged, selectedContext
|
|
|
322
326
|
// ever replaced; custom-prompt or agent layouts without that block are left
|
|
323
327
|
// untouched, and a same-variant switch is a no-op.
|
|
324
328
|
export function _refreshSessionRuleVariantsForModel(session, previousModel, previousProvider = session?.provider) {
|
|
329
|
+
const ownerIsAgent = isAgentOwner(session);
|
|
325
330
|
const deny = [
|
|
326
331
|
...(Array.isArray(session?.disallowedTools) ? session.disallowedTools : []),
|
|
327
332
|
...(getHiddenAgent(session?.agent || null) ? ['Skill'] : []),
|
|
328
|
-
...(delegationDisabled(session,
|
|
333
|
+
...(delegationDisabled(session, ownerIsAgent) ? ['agent'] : []),
|
|
329
334
|
];
|
|
330
335
|
const allowTools = honoredSchemaAllowlist(session?.schemaAllowedTools, {
|
|
331
|
-
ownerIsAgent
|
|
336
|
+
ownerIsAgent,
|
|
332
337
|
agent: session?.agent || null,
|
|
333
338
|
});
|
|
334
339
|
const previousRules = _buildBaseRules({
|
|
335
340
|
omitTools: [...deny, unusedModelEditToolName(previousModel)],
|
|
336
341
|
allowTools,
|
|
342
|
+
ownerIsAgent,
|
|
337
343
|
provider: previousProvider,
|
|
338
344
|
model: previousModel,
|
|
339
345
|
});
|
|
340
346
|
const nextRules = _buildBaseRules({
|
|
341
347
|
omitTools: [...deny, unusedModelEditToolName(session?.model)],
|
|
342
348
|
allowTools,
|
|
349
|
+
ownerIsAgent,
|
|
343
350
|
provider: session?.provider,
|
|
344
351
|
model: session?.model,
|
|
345
352
|
});
|
|
@@ -436,6 +443,10 @@ function resetSessionForRouteChange(id, session, previousModel, previousProvider
|
|
|
436
443
|
delete session._providerPrefixGuardState;
|
|
437
444
|
// The new route resolves its own wire before its first request.
|
|
438
445
|
session.wireRoute = null;
|
|
446
|
+
// A warm chain replays the previous route's request: stop it. Its recorded
|
|
447
|
+
// warm use names that route, so it never counts as fresh cache for the new
|
|
448
|
+
// one (compact-policy.mjs shouldCompactForExpiredAgentCache).
|
|
449
|
+
cancelCacheWarm(id);
|
|
439
450
|
const prepared = _prepareResumeTools(session, session.preset || 'full');
|
|
440
451
|
session.tools = prepared.tools;
|
|
441
452
|
session.toolSpec = prepared.toolSpec;
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
describeShellToolsStartupState,
|
|
22
22
|
} from '../../tools/builtin/runtime-capabilities.mjs';
|
|
23
23
|
import { captureOriginalUserCwd } from '../../../../shared/user-cwd.mjs';
|
|
24
|
+
import { sessionScratchDir } from '../../../../shared/session-scratch.mjs';
|
|
24
25
|
import { delegationDisabled } from './session-tool-surface.mjs';
|
|
25
26
|
import { contextAckMessage } from '../compact/user-message-kind.mjs';
|
|
26
27
|
|
|
@@ -31,7 +32,9 @@ import { contextAckMessage } from '../compact/user-message-kind.mjs';
|
|
|
31
32
|
// it still get a location line via captureOriginalUserCwd(), which
|
|
32
33
|
// resolves explicit session signals first and never leaks the daemon's
|
|
33
34
|
// install root (user-cwd.mjs safe fallback chain).
|
|
34
|
-
function buildShellEnvironmentContext(opts, ownerIsAgent, toolsForRouting) {
|
|
35
|
+
function buildShellEnvironmentContext(opts, ownerIsAgent, toolsForRouting, sessionId = null) {
|
|
36
|
+
// Fixed per session id and data dir, so the line never changes across turns.
|
|
37
|
+
const scratchDir = sessionScratchDir(sessionId);
|
|
35
38
|
const sessionCwdLine = opts.cwd || captureOriginalUserCwd();
|
|
36
39
|
const wantsGitStartupLine = toolsForRouting.some((tool) => tool?.name === 'git');
|
|
37
40
|
let cwdContextLine = '';
|
|
@@ -45,6 +48,9 @@ function buildShellEnvironmentContext(opts, ownerIsAgent, toolsForRouting) {
|
|
|
45
48
|
return [
|
|
46
49
|
cwdContextLine,
|
|
47
50
|
`- Shell: ${shellName}. Use ${shellName} syntax unless the user specifies otherwise.`,
|
|
51
|
+
scratchDir
|
|
52
|
+
? `- Scratch: ${scratchDir} — this session's scratch directory; TEMP, TMP and TMPDIR in shell commands point here.`
|
|
53
|
+
: '',
|
|
48
54
|
// Which common tools the shell can run, measured where commands run
|
|
49
55
|
// (login shell on POSIX, this process's PATH on Windows); see
|
|
50
56
|
// runtime-capabilities.mjs. Recorded runs spent a round per session
|
|
@@ -84,7 +90,7 @@ export function buildWorkspacePromptContext(session, cwd) {
|
|
|
84
90
|
...(Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : []),
|
|
85
91
|
];
|
|
86
92
|
return {
|
|
87
|
-
environmentContext: buildShellEnvironmentContext({ cwd }, ownerIsAgent, tools),
|
|
93
|
+
environmentContext: buildShellEnvironmentContext({ cwd }, ownerIsAgent, tools, session?.id),
|
|
88
94
|
// Agent mode never carries project instruction files.
|
|
89
95
|
instructionFilesContext:
|
|
90
96
|
session?.skipInstructionFiles === true || sessionModeOf(session) === AGENT_MODE
|
|
@@ -143,7 +149,7 @@ export function sessionRolePromptFields(identity, { ownerIsAgent, toolFreeRole,
|
|
|
143
149
|
};
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
export function composeSessionSystem(opts, { profile, providerName, modelName, surface, sessionMode }) {
|
|
152
|
+
export function composeSessionSystem(opts, { profile, providerName, modelName, surface, sessionMode, sessionId }) {
|
|
147
153
|
const agentMode = sessionMode === AGENT_MODE;
|
|
148
154
|
const { ownerIsAgent, resolvedAgent, skills } = surface;
|
|
149
155
|
// A tool-free hidden role (a one-shot classifier or title writer) carries
|
|
@@ -156,6 +162,7 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
|
|
|
156
162
|
: _buildBaseRules({
|
|
157
163
|
omitTools: surface.ruleOmitTools,
|
|
158
164
|
allowTools: surface.schemaAllowedTools,
|
|
165
|
+
ownerIsAgent,
|
|
159
166
|
provider: providerName,
|
|
160
167
|
model: modelName,
|
|
161
168
|
});
|
|
@@ -177,12 +184,12 @@ export function composeSessionSystem(opts, { profile, providerName, modelName, s
|
|
|
177
184
|
const metaContext = leadOnly ? _buildLeadMetaContext({ includeOutputStyle: !agentMode }) : '';
|
|
178
185
|
// Lead-only response language; closes the environment block so no English
|
|
179
186
|
// system text (session/shell/git lines) follows it.
|
|
180
|
-
const languageContext = leadOnly ? _buildLeadLanguageContext() : '';
|
|
187
|
+
const languageContext = leadOnly ? _buildLeadLanguageContext({ includeOutputStyle: !agentMode }) : '';
|
|
181
188
|
// Cwd, shell and repository facts only serve tool calls; a tool-free role
|
|
182
189
|
// gets none, which also keeps its prompt identical across projects.
|
|
183
190
|
const shellEnvironmentContext = toolFreeRole
|
|
184
191
|
? ''
|
|
185
|
-
: buildShellEnvironmentContext(opts, ownerIsAgent, surface.toolsForRouting);
|
|
192
|
+
: buildShellEnvironmentContext(opts, ownerIsAgent, surface.toolsForRouting, sessionId);
|
|
186
193
|
// Persisted env tail: refreshSessionBp3Environment rebuilds the env block
|
|
187
194
|
// as [session, bp3EnvironmentContext], so the language block must
|
|
188
195
|
// already sit at the end of this string to stay last after a refresh.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { compareCodePoints } from '../../../../shared/code-point-order.mjs';
|
|
4
4
|
import { getMcpTools } from '../../mcp/client.mjs';
|
|
5
5
|
import { getInternalTools } from '../../internal-tools.mjs';
|
|
6
|
-
import { BUILTIN_TOOLS, bindShellKind } from '../../tools/builtin/builtin-tools.mjs';
|
|
6
|
+
import { BUILTIN_TOOLS, bindAgentOwner, bindShellKind } from '../../tools/builtin/builtin-tools.mjs';
|
|
7
7
|
import { resolveShellFor } from '../../tools/builtin/shell-runtime.mjs';
|
|
8
8
|
import { PATCH_TOOL_DEFS } from '../../tools/patch-tool-defs.mjs';
|
|
9
9
|
import { CODE_GRAPH_TOOL_DEFS } from '../../tools/code-graph-tool-defs.mjs';
|
|
@@ -51,7 +51,7 @@ const FILESYSTEM_TOOL_NAMES = new Set(['code_graph', 'find', 'glob', 'list', 'gr
|
|
|
51
51
|
const READONLY_FS_TOOL_NAMES = new Set(['code_graph', 'find', 'glob', 'list', 'grep', 'read']);
|
|
52
52
|
|
|
53
53
|
// Lead-only control tools: never part of a worker/sub-agent session surface.
|
|
54
|
-
export const AGENT_WORKER_DENIED_TOOLS = Object.freeze(new Set(['agent', 'ask_user', 'session']));
|
|
54
|
+
export const AGENT_WORKER_DENIED_TOOLS = Object.freeze(new Set(['agent', 'ask_user', 'handoff_user', 'session']));
|
|
55
55
|
|
|
56
56
|
export function finalizeSessionToolList(
|
|
57
57
|
tools,
|
|
@@ -70,7 +70,7 @@ export function finalizeSessionToolList(
|
|
|
70
70
|
}
|
|
71
71
|
if (ownerIsAgent) {
|
|
72
72
|
out = out.filter((t) => !AGENT_WORKER_DENIED_TOOLS.has(String(t?.name || '').toLowerCase()));
|
|
73
|
-
out = orderSessionTools(out);
|
|
73
|
+
out = orderSessionTools(out.map(bindAgentOwner));
|
|
74
74
|
}
|
|
75
75
|
return out;
|
|
76
76
|
}
|
|
@@ -93,6 +93,10 @@ export function captureTurnCheckpointContextState(session, messages, currentUser
|
|
|
93
93
|
),
|
|
94
94
|
...(kind === 'post_compact' ? { usageSnapshot: { ...usageSnapshot } } : {}),
|
|
95
95
|
compaction: compactCheckpointState(session.compaction),
|
|
96
|
+
// Diagnostic only: the prefix fingerprint of the turn's last settled
|
|
97
|
+
// request (anthropic-prefix-trace.mjs), so the first request after a
|
|
98
|
+
// crash is still traced against it.
|
|
99
|
+
...(session.providerPrefixBaseline ? { prefixBaseline: session.providerPrefixBaseline } : {}),
|
|
96
100
|
};
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -224,6 +224,9 @@ export function recoverTurnCheckpoint(session) {
|
|
|
224
224
|
// mark it unanchored so the first resumed send cannot compact from a gross
|
|
225
225
|
// whole-transcript estimate.
|
|
226
226
|
const contextStateRestored = restoreTurnCheckpointContextState(session, checkpoint);
|
|
227
|
+
// Diagnostic only (anthropic-prefix-trace.mjs): the turn's last settled
|
|
228
|
+
// request, whatever the context reading's alignment.
|
|
229
|
+
if (checkpoint.contextState?.prefixBaseline) session.providerPrefixBaseline = checkpoint.contextState.prefixBaseline;
|
|
227
230
|
delete session.activeTurnCheckpoint;
|
|
228
231
|
delete session.providerState;
|
|
229
232
|
delete session._providerPrefixGuardState;
|
|
@@ -110,6 +110,15 @@ function finalizeInterruptedTurn({
|
|
|
110
110
|
};
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
// Killed with a request on the wire (phase 'streaming' after a tool run):
|
|
114
|
+
// the provider received the transcript up to its last message, and may
|
|
115
|
+
// have cached it. Stamping that message keeps what this interruption
|
|
116
|
+
// appends (the marker, the next prompt) out of the tool_result it ends —
|
|
117
|
+
// see toAnthropicMessages. The stamp is meta only; no wire bytes change.
|
|
118
|
+
const sentTail = messages.length - 1;
|
|
119
|
+
if (phase === 'streaming' && (messages[sentTail]?.role === 'tool' || messages[sentTail]?.role === 'user')) {
|
|
120
|
+
messages[sentTail] = { ...messages[sentTail], meta: { ...messages[sentTail].meta, sentRequestTail: true } };
|
|
121
|
+
}
|
|
113
122
|
appendObservedTurnOutput(messages, {
|
|
114
123
|
partialAssistantContent,
|
|
115
124
|
partialReasoningContent,
|
|
@@ -92,7 +92,11 @@ function preSendCompactDecision(state, compactPolicy) {
|
|
|
92
92
|
messages,
|
|
93
93
|
sessionRef,
|
|
94
94
|
});
|
|
95
|
-
const cacheExpired = shouldCompactForExpiredAgentCache(sessionRef, state.opts
|
|
95
|
+
const cacheExpired = shouldCompactForExpiredAgentCache(sessionRef, state.opts, {
|
|
96
|
+
provider: sessionRef?.provider || state.provider?.name,
|
|
97
|
+
model: state.model,
|
|
98
|
+
messages,
|
|
99
|
+
});
|
|
96
100
|
const mediaPressure = shouldCompactForRequestMedia(messages, sessionRef?.provider);
|
|
97
101
|
const requireReduction = !reactivePending && !mediaPressure;
|
|
98
102
|
let shouldCompact =
|
|
@@ -26,6 +26,20 @@ function validTouch(value) {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
// A warm use as stored: `{ at, provider, model, accountId }` — the route and
|
|
30
|
+
// account whose cached prefix the warm read. A bare time (written before the
|
|
31
|
+
// identity was recorded) keeps its time and matches no route.
|
|
32
|
+
function warmUseRecord(value) {
|
|
33
|
+
const record = value && typeof value === 'object' ? value : { at: value };
|
|
34
|
+
if (!(Number(record.at) > 0)) return null;
|
|
35
|
+
return {
|
|
36
|
+
at: Number(record.at),
|
|
37
|
+
provider: String(record.provider || ''),
|
|
38
|
+
model: String(record.model || ''),
|
|
39
|
+
accountId: String(record.accountId || ''),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/** A touch store backed by `path` (default: the data dir's file). */
|
|
30
44
|
export function createPromptCacheTouchStore(
|
|
31
45
|
path = () => join(resolvePluginData(), PROMPT_CACHE_TOUCH_FILE),
|
|
@@ -43,8 +57,9 @@ export function createPromptCacheTouchStore(
|
|
|
43
57
|
for (const [sessionId, touch] of Object.entries(stored?.touches || {})) {
|
|
44
58
|
if (validTouch(touch)) touches.set(sessionId, { ...touch });
|
|
45
59
|
}
|
|
46
|
-
for (const [sessionId,
|
|
47
|
-
|
|
60
|
+
for (const [sessionId, use] of Object.entries(stored?.warmUses || {})) {
|
|
61
|
+
const record = warmUseRecord(use);
|
|
62
|
+
if (record) warmUses.set(sessionId, record);
|
|
48
63
|
}
|
|
49
64
|
return touches;
|
|
50
65
|
};
|
|
@@ -102,12 +117,20 @@ export function createPromptCacheTouchStore(
|
|
|
102
117
|
},
|
|
103
118
|
/** When a warm last used the session's cache successfully, or 0. */
|
|
104
119
|
lastWarmUse(sessionId) {
|
|
105
|
-
return uses().get(String(sessionId || '')) || 0;
|
|
120
|
+
return uses().get(String(sessionId || ''))?.at || 0;
|
|
121
|
+
},
|
|
122
|
+
/** The session's last successful warm use `{ at, provider, model,
|
|
123
|
+
* accountId }`, or null. */
|
|
124
|
+
warmUse(sessionId) {
|
|
125
|
+
const use = uses().get(String(sessionId || ''));
|
|
126
|
+
return use ? { ...use } : null;
|
|
106
127
|
},
|
|
107
|
-
/** A warm read the session's cache without writing at `at
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
128
|
+
/** A warm read the session's cache without writing at `at`, on the
|
|
129
|
+
* route and account `{ provider, model, accountId }`. */
|
|
130
|
+
recordWarmUse(sessionId, at, identity = {}) {
|
|
131
|
+
const record = warmUseRecord({ ...identity, at });
|
|
132
|
+
if (!sessionId || !record) return;
|
|
133
|
+
uses().set(String(sessionId), record);
|
|
111
134
|
scheduleFlush();
|
|
112
135
|
},
|
|
113
136
|
/** The session's own activity supersedes its warm use (a real request
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Child process of restart-prefix-parity.test.mjs: one phase of a scripted
|
|
2
|
+
// anthropic-oauth session in its own process, so a "restart" is a real new
|
|
3
|
+
// process reading the data dir the previous one left. The fake provider
|
|
4
|
+
// records the request body the real OAuth builder makes for every main-loop
|
|
5
|
+
// send (providers/anthropic-oauth.mjs buildRequestBody, after the same
|
|
6
|
+
// tool-pair sanitize its send path runs) and answers from a script.
|
|
7
|
+
//
|
|
8
|
+
// node restart-prefix-parity.fixture.mjs <phase> <dataDir> <outFile> [sessionId]
|
|
9
|
+
// phase idle two complete turns, then exit
|
|
10
|
+
// phase killtool one complete turn; the second turn's second request is
|
|
11
|
+
// answered with a tool call whose run settles the journal,
|
|
12
|
+
// prints READY and hangs until the parent kills the process
|
|
13
|
+
// phase killrequest the same, but it hangs in the second request itself
|
|
14
|
+
// (sent, never answered)
|
|
15
|
+
// phase resume one turn on the stored session (crash recovery included)
|
|
16
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const [phase, dataDir, outFile, resumeId] = process.argv.slice(2);
|
|
20
|
+
process.env.MIXDOG_DATA_DIR = dataDir;
|
|
21
|
+
process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
|
|
22
|
+
const workDir = join(dataDir, '..', 'work');
|
|
23
|
+
mkdirSync(workDir, { recursive: true });
|
|
24
|
+
// The response language defaults to the system locale, and only a Korean one
|
|
25
|
+
// gets Opus 5.5's turn-scoped tail reminder (tail-reminder-rules.mjs), so the
|
|
26
|
+
// language is pinned: every host sends the same requests. The resume phase
|
|
27
|
+
// reads what the first phase wrote.
|
|
28
|
+
if (phase !== 'resume') {
|
|
29
|
+
writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({ agent: { profile: { language: 'ko' } } }));
|
|
30
|
+
}
|
|
31
|
+
// One file per call: a repeated read is answered from the read cache, which
|
|
32
|
+
// never reaches the tool run the midturn phase hangs in.
|
|
33
|
+
for (const name of ['call_1', 'call_2', 'call_3']) writeFileSync(join(workDir, `${name}.txt`), `${name}\n`);
|
|
34
|
+
|
|
35
|
+
const { createSession } = await import('./manager/session-lifecycle.mjs');
|
|
36
|
+
const { askSession } = await import('./manager/ask-session.mjs');
|
|
37
|
+
const { settleTurnCheckpointWrites } = await import('./manager/turn-checkpoint.mjs');
|
|
38
|
+
const { sanitizeToolPairs } = await import('./context-utils.mjs');
|
|
39
|
+
const { _withRegisteredProviderForTestAsync } = await import('../providers/registry.mjs');
|
|
40
|
+
const { _buildRequestBodyForCacheSmoke } = await import('../providers/anthropic-oauth.mjs');
|
|
41
|
+
const { modelToolSchemaAllowlist } = await import('../runtime-core/tool-profile.mjs');
|
|
42
|
+
const { anthropicPrefixChanges, anthropicPrefixFingerprint, anthropicPrefixSessionId, recordAnthropicPrefix } =
|
|
43
|
+
await import('../providers/lib/anthropic-prefix-trace.mjs');
|
|
44
|
+
|
|
45
|
+
const MODEL = 'claude-opus-5-5';
|
|
46
|
+
const USAGE = { inputTokens: 10, outputTokens: 5, cachedTokens: 0, cacheWriteTokens: 0 };
|
|
47
|
+
const thinking = (id) => [{ type: 'thinking', thinking: `plan ${id}`, signature: `sig-${id}` }];
|
|
48
|
+
const toolStep = (id) => async () => ({
|
|
49
|
+
content: 'Reading.',
|
|
50
|
+
thinkingBlocks: thinking(id),
|
|
51
|
+
hasThinkingContent: true,
|
|
52
|
+
toolCalls: [{ id, name: 'read', arguments: { path: `${id}.txt` } }],
|
|
53
|
+
stopReason: 'tool_use',
|
|
54
|
+
usage: USAGE,
|
|
55
|
+
});
|
|
56
|
+
const textStep = (text) => async () => ({
|
|
57
|
+
content: text,
|
|
58
|
+
thinkingBlocks: thinking(text),
|
|
59
|
+
hasThinkingContent: true,
|
|
60
|
+
stopReason: 'end_turn',
|
|
61
|
+
usage: USAGE,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const bodies = [];
|
|
65
|
+
const changes = [];
|
|
66
|
+
let warmBody = null;
|
|
67
|
+
let script = [];
|
|
68
|
+
let sessionId = resumeId || null;
|
|
69
|
+
const provider = {
|
|
70
|
+
name: 'anthropic-oauth',
|
|
71
|
+
contextWindow: 200_000,
|
|
72
|
+
async send(messages, model, tools, opts) {
|
|
73
|
+
if (opts?.requestKind && opts.requestKind !== 'turn') return { content: 'Title', usage: USAGE };
|
|
74
|
+
const body = await _buildRequestBodyForCacheSmoke(sanitizeToolPairs(messages), model, tools || [], opts);
|
|
75
|
+
bodies.push(JSON.parse(JSON.stringify(body)));
|
|
76
|
+
// The diagnostic fingerprint the real send records, and what it reports
|
|
77
|
+
// against the previous request (the record's, after a restart).
|
|
78
|
+
recordAnthropicPrefix(anthropicPrefixSessionId(opts), anthropicPrefixFingerprint(body));
|
|
79
|
+
changes.push(anthropicPrefixChanges(anthropicPrefixSessionId(opts)));
|
|
80
|
+
const step = script.shift();
|
|
81
|
+
if (!step) throw new Error(`unscripted send #${bodies.length}`);
|
|
82
|
+
return step();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Where the interrupted turn stops: the checkpoint made durable, the recorded
|
|
87
|
+
// bodies handed over, and nothing ever finishes (the parent kills the process).
|
|
88
|
+
const hang = async () => {
|
|
89
|
+
await settleTurnCheckpointWrites(sessionId);
|
|
90
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
91
|
+
await settleTurnCheckpointWrites(sessionId);
|
|
92
|
+
writeFileSync(outFile, JSON.stringify({ sessionId, bodies, changes }));
|
|
93
|
+
process.stdout.write('READY\n');
|
|
94
|
+
return new Promise(() => {});
|
|
95
|
+
};
|
|
96
|
+
// killtool: in the second tool run of the turn (after its second request settled).
|
|
97
|
+
let toolRuns = 0;
|
|
98
|
+
const hangInSecondTool = async () => {
|
|
99
|
+
toolRuns += 1;
|
|
100
|
+
if (toolRuns >= 2) await hang();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await _withRegisteredProviderForTestAsync('anthropic-oauth', provider, async () => {
|
|
104
|
+
if (phase === 'resume') {
|
|
105
|
+
// The cache warm a restart re-arms (cache-warm.mjs rearmCacheWarmsAfterRestart)
|
|
106
|
+
// replays this request, built before the next turn touches the session.
|
|
107
|
+
const { lastSentMainRequest } = await import('./side-question.mjs');
|
|
108
|
+
const { cacheWarmSend } = await import('./cache-warm.mjs');
|
|
109
|
+
const request = lastSentMainRequest(sessionId);
|
|
110
|
+
if (request) {
|
|
111
|
+
const send = cacheWarmSend(request);
|
|
112
|
+
warmBody = await _buildRequestBodyForCacheSmoke(
|
|
113
|
+
sanitizeToolPairs(send.messages),
|
|
114
|
+
send.model,
|
|
115
|
+
send.tools || [],
|
|
116
|
+
send.opts
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
script = [textStep('resumed')];
|
|
120
|
+
await askSession(sessionId, 'third task', null, null, null, null, {});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const session = createSession({
|
|
124
|
+
provider: 'anthropic-oauth',
|
|
125
|
+
model: MODEL,
|
|
126
|
+
cwd: workDir,
|
|
127
|
+
skipSkills: true,
|
|
128
|
+
schemaAllowedTools: modelToolSchemaAllowlist('headless'),
|
|
129
|
+
workflow: { id: 'headless', delegatesAgents: false },
|
|
130
|
+
});
|
|
131
|
+
sessionId = session.id;
|
|
132
|
+
if (phase === 'idle') {
|
|
133
|
+
script = [toolStep('call_1'), textStep('done one'), toolStep('call_2'), textStep('done two')];
|
|
134
|
+
await askSession(sessionId, 'first task', null, null, null, null, {});
|
|
135
|
+
await askSession(sessionId, 'second task', null, null, null, null, {});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// killrequest: while the turn's second request is on the wire.
|
|
139
|
+
const killTool = phase === 'killtool';
|
|
140
|
+
script = [toolStep('call_1'), textStep('done one'), toolStep('call_2'), killTool ? toolStep('call_3') : hang];
|
|
141
|
+
await askSession(sessionId, 'first task', null, null, null, null, {});
|
|
142
|
+
await askSession(
|
|
143
|
+
sessionId,
|
|
144
|
+
'second task',
|
|
145
|
+
null,
|
|
146
|
+
null,
|
|
147
|
+
null,
|
|
148
|
+
null,
|
|
149
|
+
killTool ? { beforeToolExecution: hangInSecondTool } : {}
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
writeFileSync(outFile, JSON.stringify({ sessionId, bodies, changes, warmBody }));
|
|
154
|
+
process.exit(0);
|
|
@@ -36,6 +36,7 @@ import { _savePending, _clearDebounce, _releasePendingSlot } from './pending-sav
|
|
|
36
36
|
import { _runtimeLivenessVeto } from './liveness-veto.mjs';
|
|
37
37
|
import { _runSessionPurgeHooks } from './session-observers.mjs';
|
|
38
38
|
import { deleteCompactionArchive } from '../compaction-archive.mjs';
|
|
39
|
+
import { removeSessionScratchDir } from '../../../../shared/session-scratch.mjs';
|
|
39
40
|
|
|
40
41
|
/** Child-agent session ids linked to one visible parent. Candidates come from
|
|
41
42
|
* metadata only (the summary rows and the agent-worker links — no folder
|
|
@@ -154,6 +155,12 @@ export function deleteSession(id, options = {}) {
|
|
|
154
155
|
_runSessionPurgeHooks(id, { cwds: knownSessionCwds(id) });
|
|
155
156
|
// The display-only compaction archive has no meaning without its session.
|
|
156
157
|
deleteCompactionArchive(getStoreDir(), id);
|
|
158
|
+
// The session's scratch directory (shell TEMP/TMP/TMPDIR) goes with it.
|
|
159
|
+
try {
|
|
160
|
+
removeSessionScratchDir(id);
|
|
161
|
+
} catch {
|
|
162
|
+
/* a locked scratch file never fails the delete */
|
|
163
|
+
}
|
|
157
164
|
_clearLiveSession(id);
|
|
158
165
|
// The file is gone: every marker of the retired incarnation goes with it
|
|
159
166
|
// (save error + failed-snapshot evidence, split-brain drop flag, lifecycle
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
inheritedFromSessionId,
|
|
31
31
|
isStoredSessionId,
|
|
32
32
|
positiveNumber,
|
|
33
|
+
compactCountField,
|
|
33
34
|
realTurnCount,
|
|
34
35
|
workspaceSummary,
|
|
35
36
|
} from './store-summary-fields.mjs';
|
|
@@ -216,6 +217,9 @@ export function normalizedRow(row) {
|
|
|
216
217
|
// Absent from a row read off a session file: its assistant text is not
|
|
217
218
|
// decoded there, so the unread count falls back until the next save.
|
|
218
219
|
...(Array.isArray(row.incomingTail) ? { incomingTail: normalizedIncomingTail(row.incomingTail) } : {}),
|
|
220
|
+
// Absent from a row read off a session file (its compaction record is not
|
|
221
|
+
// decoded there) until the next save.
|
|
222
|
+
...compactCountField(row),
|
|
219
223
|
realTurnCount: realTurnCount(row),
|
|
220
224
|
title: cleanText(row.title, 100),
|
|
221
225
|
preview: cleanText(row.preview),
|
|
@@ -63,6 +63,21 @@ export function realTurnCount(value) {
|
|
|
63
63
|
return typeof count === 'number' && Number.isFinite(count) && count >= 0 ? Math.floor(count) : null;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** How many compactions a session has had (`compaction.compactCount`): each
|
|
67
|
+
* one trims the live messages and so restarts their numbering (the unread
|
|
68
|
+
* cursors count them). Null for a value without it, e.g. a row written
|
|
69
|
+
* before the field existed. */
|
|
70
|
+
export function compactCount(value) {
|
|
71
|
+
const count = value?.compactCount;
|
|
72
|
+
return typeof count === 'number' && Number.isSafeInteger(count) && count >= 0 ? count : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** `{ compactCount }` for a row that carries one, else nothing. */
|
|
76
|
+
export function compactCountField(value) {
|
|
77
|
+
const count = compactCount(value);
|
|
78
|
+
return count === null ? {} : { compactCount: count };
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
export function importKey(value) {
|
|
67
82
|
const key = typeof value?.importKey === 'string' ? value.importKey : value?.importOrigin?.key;
|
|
68
83
|
return typeof key === 'string' && key.trim() ? key : null;
|