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.
Files changed (236) hide show
  1. package/package.json +1 -1
  2. package/scripts/check-voice-runtime-inputs.mjs +25 -0
  3. package/scripts/gate-local.mjs +12 -3
  4. package/scripts/latest-guard.mjs +21 -9
  5. package/scripts/lib/isolated-test-env.mjs +10 -5
  6. package/scripts/lib/run-node-tests.mjs +37 -37
  7. package/scripts/lib/scoped-temp.d.mts +16 -0
  8. package/scripts/lib/scoped-temp.mjs +123 -0
  9. package/scripts/lib/stale-temp.mjs +44 -0
  10. package/scripts/post-release-check.mjs +1 -6
  11. package/scripts/pptx-kit-index.mjs +17 -4
  12. package/scripts/suite-health.mjs +10 -1
  13. package/scripts/test-data-dir.mjs +10 -7
  14. package/src/app.mjs +4 -0
  15. package/src/defaults/shipped-history.json +5 -0
  16. package/src/defaults/skills/browser-use/SKILL.md +6 -4
  17. package/src/defaults/skills/computer-use/SKILL.md +4 -2
  18. package/src/lib/rules-builder.cjs +29 -12
  19. package/src/output-styles/common.md +2 -2
  20. package/src/output-styles/simple.md +1 -1
  21. package/src/rules/agent/AGENT.md +5 -4
  22. package/src/rules/lead/LEAD.md +10 -5
  23. package/src/rules/shared/10-tool-workflow.md +3 -1
  24. package/src/rules/shared/45-destructive.md +0 -2
  25. package/src/rules/shared/50-execution.md +2 -0
  26. package/src/rules/shared/70-delivery.md +7 -9
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -6
  28. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +2 -0
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +3 -1
  31. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +5 -2
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +7 -3
  35. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +8 -2
  36. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +6 -2
  37. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +2 -2
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +3 -1
  39. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +14 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +6 -0
  41. package/src/runtime/agent/orchestrator/providers/anthropic-turn-result.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/providers/command-code-oauth-login.mjs +1 -10
  43. package/src/runtime/agent/orchestrator/providers/cursor.mjs +3 -0
  44. package/src/runtime/agent/orchestrator/providers/devin-wire-transport.mjs +1 -1
  45. package/src/runtime/agent/orchestrator/providers/devin.mjs +3 -0
  46. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +210 -20
  47. package/src/runtime/agent/orchestrator/providers/lib/anthropic-prefix-trace.mjs +76 -11
  48. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +45 -6
  49. package/src/runtime/agent/orchestrator/providers/lib/first-output-window.mjs +58 -0
  50. package/src/runtime/agent/orchestrator/providers/lib/listen-on.mjs +14 -0
  51. package/src/runtime/agent/orchestrator/providers/mimo-oauth-login.mjs +1 -10
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +1 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/chunk-events.mjs +9 -2
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-state.mjs +7 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +14 -3
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +4 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +1 -0
  58. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +5 -3
  59. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +16 -3
  60. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +1 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/output-items.mjs +5 -2
  62. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events.mjs +13 -3
  63. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +13 -2
  64. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +81 -42
  65. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +8 -0
  66. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +136 -5
  67. package/src/runtime/agent/orchestrator/providers/openai-ws-stream/event-switch.mjs +20 -12
  68. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +7 -1
  69. package/src/runtime/agent/orchestrator/providers/openai-ws-watchdogs.mjs +48 -22
  70. package/src/runtime/agent/orchestrator/providers/registry.mjs +12 -0
  71. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +41 -13
  72. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -6
  73. package/src/runtime/agent/orchestrator/runtime-core/session-text.mjs +7 -3
  74. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +3 -1
  75. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +5 -2
  76. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +1 -3
  77. package/src/runtime/agent/orchestrator/session/cache-warm.mjs +8 -2
  78. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +4 -1
  79. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +1 -8
  80. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +23 -0
  81. package/src/runtime/agent/orchestrator/session/compact/observation-supersession.mjs +103 -23
  82. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +160 -36
  83. package/src/runtime/agent/orchestrator/session/context-utils.mjs +13 -1
  84. package/src/runtime/agent/orchestrator/session/conversation-import/claude.mjs +163 -22
  85. package/src/runtime/agent/orchestrator/session/conversation-import/codex.mjs +132 -17
  86. package/src/runtime/agent/orchestrator/session/conversation-import/convert.mjs +38 -7
  87. package/src/runtime/agent/orchestrator/session/conversation-import/imported-session.mjs +3 -0
  88. package/src/runtime/agent/orchestrator/session/conversation-import/turns.mjs +90 -22
  89. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +33 -3
  90. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +7 -2
  91. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -1
  92. package/src/runtime/agent/orchestrator/session/loop/provider-tool-exclusions.mjs +13 -5
  93. package/src/runtime/agent/orchestrator/session/loop/route-fallback.mjs +14 -0
  94. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +13 -1
  95. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +2 -2
  96. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +39 -21
  97. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -3
  98. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +3 -1
  99. package/src/runtime/agent/orchestrator/session/manager/owner-route-fallbacks.mjs +2 -2
  100. package/src/runtime/agent/orchestrator/session/manager/route-fallback-chain.mjs +2 -1
  101. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +27 -9
  102. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +61 -6
  103. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +19 -8
  104. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +12 -5
  105. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -3
  106. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-context.mjs +4 -0
  107. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +3 -0
  108. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +9 -0
  109. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +5 -1
  110. package/src/runtime/agent/orchestrator/session/prompt-cache-touch-store.mjs +30 -7
  111. package/src/runtime/agent/orchestrator/session/restart-prefix-parity.fixture.mjs +154 -0
  112. package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +7 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +4 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +15 -0
  115. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +37 -8
  116. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +9 -1
  117. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +61 -2
  118. package/src/runtime/agent/orchestrator/stall-policy.mjs +23 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/invocation-plan.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/spawn-env.mjs +4 -2
  121. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +6 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -20
  123. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +7 -1
  124. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +31 -5
  125. package/src/runtime/agent/orchestrator/tools/builtin/git-worktree-tool.mjs +22 -8
  126. package/src/runtime/agent/orchestrator/tools/builtin/lib/scoped-search-flight.mjs +9 -5
  127. package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +7 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +6 -3
  129. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +1 -1
  130. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +23 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +6 -1
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -8
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch/federation.mjs +3 -1
  134. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  136. package/src/runtime/agent/orchestrator/tools/lib/isolated-test-env.mjs +4 -3
  137. package/src/runtime/agent/orchestrator/tools/lib/shell-warm-standby.mjs +26 -4
  138. package/src/runtime/channels/lib/webhook/signature.mjs +4 -1
  139. package/src/runtime/channels/lib/whisper-port.mjs +10 -1
  140. package/src/runtime/computer-bridge/action-schema.mjs +2 -2
  141. package/src/runtime/computer-bridge/core-actions.mjs +2 -2
  142. package/src/runtime/computer-bridge/error-recovery.mjs +5 -4
  143. package/src/runtime/computer-bridge/linux-action-capabilities.mjs +24 -5
  144. package/src/runtime/computer-bridge/linux-readiness.d.mts +7 -2
  145. package/src/runtime/local-provider/server-process.mjs +2 -1
  146. package/src/runtime/media/adapters/gemini-video.mjs +4 -1
  147. package/src/runtime/media/adapters/xai-media.mjs +2 -1
  148. package/src/runtime/memory/lib/embedding-worker.mjs +6 -3
  149. package/src/runtime/memory/lib/memory-action-handlers/cycle-backfill-action.mjs +1 -7
  150. package/src/runtime/memory/lib/memory-chunk-quality.mjs +1 -6
  151. package/src/runtime/office/authoring/pdf-author-action.mjs +1 -6
  152. package/src/runtime/office/design/design-art-direction.mjs +29 -27
  153. package/src/runtime/office/pdf/pdf-writer.mjs +6 -1
  154. package/src/runtime/office/portable/portable-snapshot-docx.mjs +7 -1
  155. package/src/runtime/office/portable/portable-snapshot-shared.mjs +6 -1
  156. package/src/runtime/office/quality/assurance-rendered.mjs +9 -2
  157. package/src/runtime/office/quality/assurance-structure-docx.mjs +3 -1
  158. package/src/runtime/office/quality/design-review.mjs +1 -5
  159. package/src/runtime/office/quality/document-brief.mjs +1 -5
  160. package/src/runtime/office/quality/forced-break-pages.mjs +9 -6
  161. package/src/runtime/office/quality/quality-pipeline.mjs +1 -1
  162. package/src/runtime/shared/bot-owned-work.mjs +56 -0
  163. package/src/runtime/shared/defaults-separation.mjs +1 -4
  164. package/src/runtime/shared/fetch-safe-listen.d.mts +16 -0
  165. package/src/runtime/shared/fetch-safe-listen.mjs +50 -0
  166. package/src/runtime/shared/handoff-artifacts.d.mts +2 -1
  167. package/src/runtime/shared/handoff-artifacts.mjs +18 -8
  168. package/src/runtime/shared/idle-auto-clear-gate.mjs +29 -7
  169. package/src/runtime/shared/llm/cost.mjs +5 -1
  170. package/src/runtime/shared/loopback-listener.mjs +25 -2
  171. package/src/runtime/shared/model-route-chain.d.mts +7 -2
  172. package/src/runtime/shared/model-route-chain.mjs +22 -4
  173. package/src/runtime/shared/open-url.mjs +10 -0
  174. package/src/runtime/shared/session-activity-signal.mjs +14 -0
  175. package/src/runtime/shared/session-scratch.mjs +37 -0
  176. package/src/runtime/shared/tool-card-model/agent-surface.mjs +1 -3
  177. package/src/runtime/shared/tool-card-model/card-status.mjs +15 -2
  178. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  179. package/src/runtime/shared/tool-surface.mjs +3 -0
  180. package/src/runtime/shared/tool-work-units.mjs +2 -0
  181. package/src/runtime/shared/turn-snapshot-store.mjs +13 -6
  182. package/src/runtime/shared/worktrees/contract.mjs +4 -1
  183. package/src/runtime/shared/worktrees/git.mjs +15 -0
  184. package/src/runtime/shared/worktrees/manager.mjs +87 -38
  185. package/src/runtime/shared/worktrees/test-support.mjs +6 -10
  186. package/src/runtime/visualize-bridge/client.mjs +2 -0
  187. package/src/session-runtime/agent-home-delivery.mjs +41 -0
  188. package/src/session-runtime/agent-task-sessions.mjs +152 -63
  189. package/src/session-runtime/agent-tool-routing.mjs +8 -41
  190. package/src/session-runtime/boot/tools.mjs +5 -1
  191. package/src/session-runtime/bot-status.mjs +12 -6
  192. package/src/session-runtime/bot-target.mjs +38 -0
  193. package/src/session-runtime/channel-config-api.mjs +6 -1
  194. package/src/session-runtime/conversation-import/codex-titles.mjs +88 -0
  195. package/src/session-runtime/conversation-import/discovery.mjs +271 -25
  196. package/src/session-runtime/conversation-import/service.mjs +26 -5
  197. package/src/session-runtime/handoff-attachments.mjs +82 -31
  198. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +12 -13
  199. package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +1 -0
  200. package/src/session-runtime/internal-tool-executor/reaction-tool.mjs +4 -1
  201. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +8 -2
  202. package/src/session-runtime/lifecycle/cold-heir.mjs +11 -6
  203. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -1
  204. package/src/session-runtime/model-route/set-route.mjs +11 -2
  205. package/src/session-runtime/provider-catalog-cache.mjs +82 -36
  206. package/src/session-runtime/provider-models/catalog-load.mjs +23 -6
  207. package/src/session-runtime/provider-models.mjs +4 -2
  208. package/src/session-runtime/resource-recall-api.mjs +6 -1
  209. package/src/session-runtime/runtime-feature-gates.mjs +5 -1
  210. package/src/session-runtime/services/agent-tool/execute.mjs +14 -9
  211. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +20 -4
  212. package/src/session-runtime/services/agent-tool/model-select.mjs +48 -7
  213. package/src/session-runtime/services/agent-tool/render.mjs +71 -17
  214. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +6 -2
  215. package/src/session-runtime/services/agent-tool/tool-def.mjs +9 -9
  216. package/src/session-runtime/services/provider-admin.mjs +2 -1
  217. package/src/session-runtime/tool-policy/policy-stage.mjs +1 -0
  218. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +5 -1
  219. package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -4
  220. package/src/session-runtime/workflow-agents-api/onboarding.mjs +3 -3
  221. package/src/session-runtime/workspace/transitions.mjs +25 -0
  222. package/src/session-runtime/workspace/worktree-tool.mjs +26 -9
  223. package/src/standalone/daemon-session-control.mjs +18 -2
  224. package/src/standalone/session-service/agent-tree/agent-turns.mjs +16 -12
  225. package/src/standalone/session-service/session-calls/session-action.mjs +10 -3
  226. package/src/standalone/session-transport/client-registry/client-grace.mjs +12 -9
  227. package/src/tui/app/app-view/floating-panel.jsx +7 -4
  228. package/src/tui/app/model-picker/route-selection/fast-parameters.mjs +1 -3
  229. package/src/tui/app/model-picker/route-selection.mjs +5 -6
  230. package/src/tui/app/provider-setup/oauth-actions.mjs +38 -20
  231. package/src/tui/app/provider-setup/provider-items.mjs +8 -2
  232. package/src/tui/app/provider-setup-picker.mjs +9 -5
  233. package/src/tui/components/QuestionPanel.jsx +13 -4
  234. package/src/tui/dist/index.mjs +75 -28
  235. package/src/tui/session/session-flow/queue/take-entries.mjs +11 -0
  236. 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 (stage === 'requesting' && priorStage !== 'requesting') {
147
- entry.modelRequestStartedAt = now;
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.modelRequestStartedAt = null;
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
- const waitingForTransport = Boolean(modelRequestStartedAt && activeModelStage && !entry.lastTransportAt);
462
- const waitingForFirstSemantic = Boolean(modelRequestStartedAt && activeModelStage && !firstSemanticAt);
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
- : normalizeWorkflowRoute({
243
- provider: providerName,
244
- model: modelName,
245
- fallbacks: Object.hasOwn(opts, 'routeFallbacks') ? opts.routeFallbacks : presetObj?.fallbacks,
246
- })?.fallbacks;
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, isAgentOwner(session)) ? ['agent'] : []),
333
+ ...(delegationDisabled(session, ownerIsAgent) ? ['agent'] : []),
329
334
  ];
330
335
  const allowTools = honoredSchemaAllowlist(session?.schemaAllowedTools, {
331
- ownerIsAgent: isAgentOwner(session),
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, at] of Object.entries(stored?.warmUses || {})) {
47
- if (Number(at) > 0) warmUses.set(sessionId, Number(at));
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
- recordWarmUse(sessionId, at) {
109
- if (!sessionId || !(Number(at) > 0)) return;
110
- uses().set(String(sessionId), Number(at));
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;