mixdog 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/package.json +1 -1
  2. package/scripts/gate-local.mjs +19 -3
  3. package/scripts/model-catalog-audit.mjs +1 -15
  4. package/src/agents/reviewer/AGENT.md +3 -3
  5. package/src/agents/reviewer/agent.json +1 -1
  6. package/src/defaults/shipped-history.json +1 -0
  7. package/src/repl/slash-commands.mjs +16 -1
  8. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +29 -4
  9. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +10 -0
  10. package/src/runtime/agent/orchestrator/mcp/client-config.mjs +36 -7
  11. package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +49 -10
  12. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +40 -3
  13. package/src/runtime/agent/orchestrator/mcp/client.mjs +230 -96
  14. package/src/runtime/agent/orchestrator/mcp/oauth-callback.mjs +103 -0
  15. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +150 -0
  16. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +111 -0
  17. package/src/runtime/agent/orchestrator/mcp/oauth-probe.mjs +87 -0
  18. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +95 -0
  19. package/src/runtime/agent/orchestrator/mcp/oauth-request.mjs +122 -0
  20. package/src/runtime/agent/orchestrator/mcp/oauth-shared.mjs +9 -0
  21. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +229 -0
  22. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +188 -29
  23. package/src/runtime/agent/orchestrator/providers/anthropic-api-recovery.mjs +2 -1
  24. package/src/runtime/agent/orchestrator/providers/anthropic-api-transport.mjs +94 -31
  25. package/src/runtime/agent/orchestrator/providers/anthropic-error-contract.mjs +96 -0
  26. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +39 -18
  27. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens-recovery.mjs +55 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +25 -21
  29. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +3 -1
  30. package/src/runtime/agent/orchestrator/providers/anthropic-midstream-recovery.mjs +18 -4
  31. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +13 -1
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-midstream.mjs +10 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-recovery.mjs +23 -9
  35. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request/initial-status.mjs +46 -20
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-request.mjs +14 -9
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +73 -35
  38. package/src/runtime/agent/orchestrator/providers/anthropic-overload-fallback.mjs +69 -0
  39. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +4 -0
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +8 -1
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -7
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +30 -9
  43. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +30 -14
  45. package/src/runtime/agent/orchestrator/providers/antigravity-response.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/antigravity-retry-policy.mjs +93 -0
  47. package/src/runtime/agent/orchestrator/providers/antigravity-transport.mjs +82 -9
  48. package/src/runtime/agent/orchestrator/providers/command-code-oauth.mjs +3 -0
  49. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +38 -5
  50. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  51. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +97 -2
  52. package/src/runtime/agent/orchestrator/providers/cursor-wire-stream-bridge.mjs +102 -23
  53. package/src/runtime/agent/orchestrator/providers/cursor-wire-transport.mjs +103 -33
  54. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +29 -6
  55. package/src/runtime/agent/orchestrator/providers/cursor.mjs +18 -1
  56. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +77 -17
  57. package/src/runtime/agent/orchestrator/providers/gemini-cache-policy.mjs +1 -1
  58. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +29 -8
  59. package/src/runtime/agent/orchestrator/providers/gemini-error-contract.mjs +191 -0
  60. package/src/runtime/agent/orchestrator/providers/gemini-family.mjs +9 -0
  61. package/src/runtime/agent/orchestrator/providers/gemini-history-signatures.mjs +9 -4
  62. package/src/runtime/agent/orchestrator/providers/gemini-response.mjs +3 -1
  63. package/src/runtime/agent/orchestrator/providers/gemini-retry-policy.mjs +180 -0
  64. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +21 -82
  65. package/src/runtime/agent/orchestrator/providers/gemini-sdk-request.mjs +4 -1
  66. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +71 -12
  67. package/src/runtime/agent/orchestrator/providers/gemini-thinking.mjs +21 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +56 -23
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
  70. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +68 -19
  71. package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +3 -0
  72. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +2 -1
  73. package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +4 -9
  74. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +4 -2
  75. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream/stream-result.mjs +22 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +1 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-error-contract.mjs +50 -0
  78. package/src/runtime/agent/orchestrator/providers/openai-compat-response-normalization.mjs +23 -0
  79. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-events.mjs +16 -8
  80. package/src/runtime/agent/orchestrator/providers/openai-compat-responses-state.mjs +3 -2
  81. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +94 -13
  82. package/src/runtime/agent/orchestrator/providers/openai-compat-retry-profile.mjs +193 -0
  83. package/src/runtime/agent/orchestrator/providers/openai-compat-stream-common.mjs +66 -2
  84. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +2 -1
  85. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +6 -0
  86. package/src/runtime/agent/orchestrator/providers/openai-compat-xai-send.mjs +4 -2
  87. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -14
  88. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +17 -3
  89. package/src/runtime/agent/orchestrator/providers/openai-direct-request.mjs +42 -25
  90. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +274 -0
  91. package/src/runtime/agent/orchestrator/providers/openai-http-sse-request.mjs +72 -32
  92. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-events/terminal-frames.mjs +18 -11
  93. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-outcome.mjs +1 -2
  94. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-stamps.mjs +2 -2
  95. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state/response-state.mjs +2 -3
  96. package/src/runtime/agent/orchestrator/providers/openai-http-sse-response-state.mjs +2 -2
  97. package/src/runtime/agent/orchestrator/providers/openai-http-sse-watchdogs.mjs +1 -3
  98. package/src/runtime/agent/orchestrator/providers/openai-oauth-dispatch.mjs +34 -3
  99. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +5 -1
  100. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +22 -2
  101. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +5 -0
  102. package/src/runtime/agent/orchestrator/providers/openai-responses-input.mjs +5 -9
  103. package/src/runtime/agent/orchestrator/providers/openai-retry-policy.mjs +9 -0
  104. package/src/runtime/agent/orchestrator/providers/openai-ws-handshake.mjs +16 -8
  105. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/attempt-context.mjs +1 -2
  106. package/src/runtime/agent/orchestrator/providers/openai-ws-send-attempts/policy.mjs +25 -14
  107. package/src/runtime/agent/orchestrator/providers/openai-ws-terminal.mjs +18 -11
  108. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +31 -20
  109. package/src/runtime/agent/orchestrator/providers/provider-error-kind.mjs +581 -0
  110. package/src/runtime/agent/orchestrator/providers/retry-backoff.mjs +46 -7
  111. package/src/runtime/agent/orchestrator/providers/retry-classification.mjs +118 -219
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +2 -0
  113. package/src/runtime/agent/orchestrator/providers/retry-executor.mjs +148 -45
  114. package/src/runtime/agent/orchestrator/providers/retry-midstream.mjs +81 -16
  115. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +3 -0
  116. package/src/runtime/agent/orchestrator/runtime-core/goal-reminder.mjs +0 -18
  117. package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +57 -28
  118. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +19 -16
  119. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -97
  120. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +3 -1
  121. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +4 -0
  122. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +102 -18
  123. package/src/runtime/agent/orchestrator/session/loop/tool-phase.mjs +3 -1
  124. package/src/runtime/agent/orchestrator/session/manager/ask-queue.mjs +30 -2
  125. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +11 -2
  126. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +41 -21
  127. package/src/runtime/agent/orchestrator/session/manager/pending-message-entry.mjs +5 -10
  128. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +18 -7
  130. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +155 -29
  131. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +6 -3
  132. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +16 -0
  133. package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +22 -10
  134. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +5 -0
  136. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +3 -0
  137. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +20 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool/deadline-plan.mjs +12 -15
  140. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  141. package/src/runtime/memory/lib/pg/process.mjs +34 -24
  142. package/src/runtime/memory/lib/pg/supervisor.mjs +16 -13
  143. package/src/runtime/office/README.md +1 -1
  144. package/src/runtime/office/shared/values.mjs +0 -5
  145. package/src/runtime/shared/background-tasks.mjs +92 -14
  146. package/src/runtime/shared/err-text.mjs +25 -0
  147. package/src/runtime/shared/error-presentation.mjs +159 -19
  148. package/src/runtime/shared/llm/anthropic-betas.mjs +4 -3
  149. package/src/runtime/shared/llm/anthropic-effort.mjs +123 -134
  150. package/src/runtime/shared/llm/anthropic-thinking-contract.mjs +50 -14
  151. package/src/runtime/shared/llm/model-catalog.mjs +53 -40
  152. package/src/runtime/shared/llm/openai-daybreak.mjs +4 -16
  153. package/src/runtime/shared/llm/provider-catalog-registry.mjs +11 -0
  154. package/src/runtime/shared/llm/provider-model-identities.mjs +13 -4
  155. package/src/runtime/shared/llm/usage-rollup.mjs +0 -1
  156. package/src/runtime/shared/model-display.mjs +13 -0
  157. package/src/runtime/shared/quota-resume-store.mjs +11 -0
  158. package/src/runtime/shared/session-baseline-store.mjs +960 -0
  159. package/src/runtime/shared/statusline/statusline-format.mjs +4 -4
  160. package/src/runtime/shared/task-notification-envelope.mjs +2 -2
  161. package/src/runtime/shared/turn-changes.mjs +447 -219
  162. package/src/runtime/shared/turn-snapshot-store.mjs +182 -1
  163. package/src/runtime/shared/turn-snapshot.mjs +170 -48
  164. package/src/runtime/shared/turn-worktree-snapshot.mjs +135 -46
  165. package/src/runtime/shared/worktrees/contract.mjs +0 -24
  166. package/src/runtime/shared/worktrees/test-support.mjs +19 -4
  167. package/src/session-runtime/boot/apis.mjs +6 -0
  168. package/src/session-runtime/boot/infrastructure.mjs +6 -0
  169. package/src/session-runtime/lifecycle/session-catalog.mjs +2 -0
  170. package/src/session-runtime/lifecycle/teardown.mjs +1 -1
  171. package/src/session-runtime/mcp-glue/connect.mjs +29 -9
  172. package/src/session-runtime/mcp-glue/server-catalog.mjs +20 -3
  173. package/src/session-runtime/mcp-glue.mjs +3 -0
  174. package/src/session-runtime/mcp-oauth-login.mjs +174 -0
  175. package/src/session-runtime/model-route/model-switch-target.mjs +34 -0
  176. package/src/session-runtime/model-route/set-route.mjs +83 -15
  177. package/src/session-runtime/model-route/web-search-route.mjs +1 -1
  178. package/src/session-runtime/native-web-search/routes.mjs +22 -3
  179. package/src/session-runtime/provider-models/model-meta.mjs +12 -3
  180. package/src/session-runtime/quick-model-rows/web-search-rows.mjs +13 -8
  181. package/src/session-runtime/quick-web-search-models.mjs +13 -38
  182. package/src/session-runtime/resource-extension-sync.mjs +4 -0
  183. package/src/session-runtime/resource-mcp-api.mjs +98 -2
  184. package/src/session-runtime/runtime-review-api.mjs +13 -0
  185. package/src/session-runtime/services/agent-tool/job-task-reconcile.mjs +13 -1
  186. package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +7 -2
  187. package/src/session-runtime/services/agent-tool/send-flow.mjs +22 -4
  188. package/src/session-runtime/services/agent-tool/spawn-flow/job-start.mjs +2 -0
  189. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-run.mjs +22 -5
  190. package/src/session-runtime/services/agent-tool/worker-index/row-liveness.mjs +8 -1
  191. package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +2 -2
  192. package/src/session-runtime/services/agent-tool/worker-rows.mjs +17 -0
  193. package/src/session-runtime/settings-compaction-api.mjs +1 -0
  194. package/src/standalone/daemon-drain-policy.mjs +8 -0
  195. package/src/standalone/daemon-telemetry.mjs +55 -0
  196. package/src/standalone/daemon.mjs +3 -7
  197. package/src/standalone/session-protocol.mjs +7 -0
  198. package/src/standalone/session-service/agent-tree/agent-cancel.mjs +33 -2
  199. package/src/standalone/session-service/agent-tree/agent-turns.mjs +26 -10
  200. package/src/standalone/session-service/agent-tree.mjs +12 -0
  201. package/src/standalone/session-service/quota-resume.mjs +8 -0
  202. package/src/standalone/session-service/retention/busy.mjs +15 -1
  203. package/src/standalone/session-service-api.mjs +5 -0
  204. package/src/standalone/session-service.mjs +1 -0
  205. package/src/tui/app/app-view.jsx +1 -0
  206. package/src/tui/app/create-app-pickers.mjs +4 -0
  207. package/src/tui/app/extension-pickers/mcp-login.mjs +107 -0
  208. package/src/tui/app/extension-pickers/mcp-servers-picker.mjs +2 -1
  209. package/src/tui/app/extension-pickers.mjs +2 -1
  210. package/src/tui/app/model-options.mjs +11 -16
  211. package/src/tui/app/slash-commands.mjs +7 -2
  212. package/src/tui/app/slash-dispatch/panel-commands.mjs +16 -1
  213. package/src/tui/components/StatusLine.jsx +2 -0
  214. package/src/tui/components/status-line/local-line.mjs +5 -2
  215. package/src/tui/components/status-line/use-statusline-text.mjs +7 -1
  216. package/src/tui/dist/index.mjs +816 -43
  217. package/src/tui/session/quota-resume.mjs +46 -14
  218. package/src/tui/session/session-api/commands.mjs +33 -24
  219. package/src/tui/session/session-api/extensions.mjs +22 -0
  220. package/src/tui/session/session-api/integrations.mjs +7 -0
  221. package/src/tui/session/session-api/lifecycle.mjs +5 -1
  222. package/src/tui/session/session-api/routes.mjs +71 -7
  223. package/src/tui/session/session-api/settings.mjs +5 -14
  224. package/src/tui/session/session-flow/drain.mjs +7 -1
  225. package/src/tui/session/session-flow/failed-turn-rewind.mjs +10 -4
  226. package/src/tui/session/turn.mjs +38 -15
  227. package/src/tui/statusline-ansi-bridge.mjs +1 -1
  228. package/src/ui/statusline-agents.mjs +15 -4
  229. package/src/ui/statusline.mjs +7 -3
  230. package/src/ui/streaming-markdown-heal.mjs +34 -3
  231. package/src/vendor/statusline/src/gateway/claude-current.mjs +14 -4
  232. package/src/runtime/memory/lib/memory-session-merge.mjs +0 -32
  233. package/src/runtime/shared/llm/usage-session-history.mjs +0 -266
@@ -2,16 +2,14 @@ import { normalizeOrchestrationMode } from '../../../shared/orchestration.mjs';
2
2
 
3
3
  const BATCHING = {
4
4
  focused: `Lead executes the main scope directly. Delegate only substantial,
5
- self-contained supporting work. Batch closely related tasks into one
6
- coherent assignment; do not split them merely to increase parallelism.`,
7
- balanced: `Delegate independent work by coherent feature or module. Batch related
8
- tasks within the same change scope into one assignment rather than
9
- splitting every file or step.
10
- If the plan has only one scope, Lead executes it directly.`,
11
- swarm: `Delegate every substantial independent scope to its own agent. Treat
12
- disjoint file or module sets as independent and merge scopes only on a true
13
- output dependency. Prefer parallel scopes over sequential slices in one agent.
14
- If the plan has only one scope, Lead executes it directly.`,
5
+ self-contained supporting work, one agent per role; batch related tasks
6
+ rather than splitting them for parallelism.`,
7
+ balanced: `Delegate independent work by coherent feature or module, one agent per
8
+ scope. Batch related tasks within the same change scope rather than
9
+ splitting every file or step.`,
10
+ swarm: `Delegate every independent scope to its own agent. Treat disjoint file or
11
+ module sets as independent and merge scopes only on a true output
12
+ dependency. Prefer parallel scopes over sequential slices in one agent.`,
15
13
  };
16
14
 
17
15
  const SOLO = `# Orchestration Mode: Solo
@@ -27,6 +25,12 @@ export function orchestrationInstructions(value) {
27
25
 
28
26
  ${BATCHING[mode]}
29
27
 
28
+ The mode sets how work splits; an agent's description sets its role and when
29
+ it runs. Splitting applies to every role alike — implementation, review,
30
+ investigation, writing — by file, module, commit range or risk area.
31
+ If the plan has only one implementation scope, Lead executes it directly; a
32
+ role tied to a point in the work, such as the final check, still runs then.
33
+
30
34
  Delegation is for work whose size justifies a brief. When the brief would have
31
35
  to spell out the edits themselves — a few localized changes whose sites Lead
32
36
  already holds — Lead makes them directly in every mode; the number of files or
@@ -34,10 +38,9 @@ scopes does not change this.
34
38
 
35
39
  Dispatch all ready independent scopes in one turn. Only a scope that requires
36
40
  another's output waits. Brief each agent as the Lead rules describe.
37
- Use only available agents; Lead handles work without a suitable agent.
38
-
39
- If the reviewer is disabled or unavailable, Lead alone reviews the result
40
- against the approved scope and performs the required verification. Do not
41
- change settings, enable the reviewer, or delegate review to a substitute agent.
42
- Do not skip review or describe Lead's own review as independent review.`;
41
+ Use only available agents; when none fits a role, Lead does that work itself
42
+ and never changes settings to enable one. If the reviewer is disabled or
43
+ unavailable, Lead alone reviews the result against the approved scope and
44
+ performs the required verification, without a substitute agent; never skip
45
+ review or describe Lead's own review as independent.`;
43
46
  }
@@ -1,17 +1,5 @@
1
1
  // Plugin/project MCP server discovery + normalization, and skill-file counting.
2
- import {
3
- chmodSync,
4
- closeSync,
5
- existsSync,
6
- fsyncSync,
7
- openSync,
8
- readFileSync,
9
- readdirSync,
10
- renameSync,
11
- statSync,
12
- unlinkSync,
13
- writeFileSync,
14
- } from 'node:fs';
2
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
15
3
  import { join, relative, sep } from 'node:path';
16
4
  import { clean } from './session-text.mjs';
17
5
  import {
@@ -54,90 +42,6 @@ export function mergeMcpServerConfig(existing, next) {
54
42
  return { ...merged, ...next };
55
43
  }
56
44
 
57
- function readProjectMcpDocument(cwd, allowMissing = false) {
58
- const path = join(cwd || '.', '.mcp.json');
59
- if (!existsSync(path)) {
60
- if (allowMissing) return { path, raw: { mcpServers: {} }, usesWrapper: true };
61
- throw new Error(`MCP config file not found: ${path}`);
62
- }
63
- let raw;
64
- try {
65
- raw = JSON.parse(readFileSync(path, 'utf8'));
66
- } catch (error) {
67
- throw new Error(`cannot read ${path}: ${error?.message || String(error)}`);
68
- }
69
- if (!isPlainObject(raw)) throw new Error(`unexpected .mcp.json shape at ${path}`);
70
- if (Object.hasOwn(raw, 'mcpServers') && !isPlainObject(raw.mcpServers)) {
71
- throw new Error(`unexpected .mcp.json shape at ${path}`);
72
- }
73
- const usesWrapper = isPlainObject(raw.mcpServers);
74
- return { path, raw, usesWrapper };
75
- }
76
-
77
- function writeProjectMcpDocument(path, raw) {
78
- let mode = 0o644;
79
- try {
80
- mode = statSync(path).mode;
81
- } catch {
82
- /* new file */
83
- }
84
- const tempPath = `${path}.tmp-${process.pid}-${Date.now()}`;
85
- let fd = null;
86
- try {
87
- fd = openSync(tempPath, 'w', mode);
88
- writeFileSync(fd, `${JSON.stringify(raw, null, 2)}\n`, 'utf8');
89
- fsyncSync(fd);
90
- closeSync(fd);
91
- fd = null;
92
- chmodSync(tempPath, mode);
93
- renameSync(tempPath, path);
94
- } catch (error) {
95
- if (fd !== null) {
96
- try {
97
- closeSync(fd);
98
- } catch {
99
- /* best effort */
100
- }
101
- }
102
- try {
103
- unlinkSync(tempPath);
104
- } catch {
105
- /* best effort */
106
- }
107
- throw error;
108
- }
109
- }
110
-
111
- // The stored key for `key`: an exact own key first, else one that trims to it.
112
- function mcpEntryKey(map, key) {
113
- return Object.hasOwn(map, key) ? key : Object.keys(map).find((candidate) => String(candidate || '').trim() === key);
114
- }
115
-
116
- export function readProjectMcpServerConfig(cwd, name) {
117
- const { raw, usesWrapper } = readProjectMcpDocument(cwd);
118
- const map = usesWrapper ? raw.mcpServers : raw;
119
- const entryKey = mcpEntryKey(map, String(name || '').trim());
120
- return entryKey && isPlainObject(map[entryKey]) ? { name: entryKey, config: { ...map[entryKey] } } : null;
121
- }
122
-
123
- export function saveProjectMcpServer(cwd, { originalName = '', name, config }) {
124
- const { path, raw, usesWrapper } = readProjectMcpDocument(cwd, !originalName);
125
- const map = usesWrapper ? raw.mcpServers : raw;
126
- const target = String(name || '').trim();
127
- const original = String(originalName || '').trim();
128
- if (!target) throw new Error('MCP server name is required');
129
- const entryKey = original ? mcpEntryKey(map, original) : null;
130
- if (original && !entryKey) throw new Error(`MCP server not defined in ${path}: ${original}`);
131
- if (target !== entryKey && Object.hasOwn(map, target)) {
132
- throw new Error(`MCP server already exists in ${path}: ${target}`);
133
- }
134
- const existing = entryKey ? map[entryKey] : {};
135
- if (entryKey && entryKey !== target) delete map[entryKey];
136
- map[target] = mergeMcpServerConfig(existing, config);
137
- writeProjectMcpDocument(path, raw);
138
- return { name: target, source: 'project', path, config: { ...map[target] } };
139
- }
140
-
141
45
  // Manifest + contained-path resolution live in the shared module so skill
142
46
  // discovery and this status surface read the same roots.
143
47
  export { pluginManifest, resolveContainedPluginPath };
@@ -11,7 +11,7 @@ import { createHash } from 'node:crypto';
11
11
 
12
12
  /** Bumped whenever the table layout or a codec changes: the DB is then
13
13
  * dropped and backfilled again from the JSON catalog. */
14
- export const SCHEMA_VERSION = 3;
14
+ export const SCHEMA_VERSION = 4;
15
15
  /** PRAGMA application_id ('MXSC'): a file without it is not this catalog. */
16
16
  export const APPLICATION_ID = 0x4d585343;
17
17
 
@@ -57,6 +57,8 @@ const FIELDS = [
57
57
  ['detachedReason', 'detached_reason', 'loose'],
58
58
  // importOrigin.key of an imported conversation (schema 3).
59
59
  ['importKey', 'import_key', 'loose'],
60
+ // Source session of an inherited (forked) session (schema 4).
61
+ ['inheritedFromSessionId', 'inherited_from_session_id', 'loose'],
60
62
  ];
61
63
 
62
64
  const COLUMN_TYPES = { text: 'TEXT', real: 'REAL', int: 'INTEGER', bool: 'INTEGER', json: 'TEXT', optjson: 'TEXT' };
@@ -8,6 +8,7 @@ import { resolveLiveToolCwd } from './tool-exec.mjs';
8
8
  import { notifySkillsApplied, prepareExplicitSkills, recordAppliedSkills } from '../explicit-skills.mjs';
9
9
  import { addUsage, usageDeltaEvent } from './usage.mjs';
10
10
  import { prepareProviderRequest } from './request-boundary.mjs';
11
+ import { applyStepEffort } from './step-auto-effort.mjs';
11
12
  import { projectProviderRequest } from './request-projection.mjs';
12
13
  import { traceProviderSend, traceOutputTruncation } from './diagnostics.mjs';
13
14
  import { createEagerDispatcher } from '../eager-dispatch.mjs';
@@ -132,6 +133,9 @@ export async function sendProviderRequest(state, round) {
132
133
  const sendStartedAt = Date.now();
133
134
  const preSendMs = sendStartedAt - round.iterT0;
134
135
  const toolResumeMs = state.lastToolBatchEndedAt ? sendStartedAt - state.lastToolBatchEndedAt : null;
136
+ // The step judgment ran beside opening the iteration; wait for it only up to
137
+ // its grace, then it rides on this request or on the next.
138
+ await applyStepEffort(state);
135
139
  // Every attempt is built from the live transcript, so a compaction between
136
140
  // attempts reaches the provider; a pending image strip only filters it.
137
141
  const messageSource = state.imageStrip
@@ -8,12 +8,13 @@
8
8
  * later request rebuilds the same history and the prompt cache keeps hitting.
9
9
  * A judge that is missing, slow or failing leaves the step at the effort in
10
10
  * force (the turn's or the previous step's).
11
- * A custom route (snapshot mode 'custom') has no mid-conversation update: the
12
- * judged effort, snapped onto the model's levels, becomes the effort of the
13
- * turn's following requests.
11
+ * A custom route snaps the judged effort onto the model's levels. Without a
12
+ * mid-conversation update (snapshot mode 'custom') it becomes the effort of
13
+ * the turn's following requests; a custom GPT-6+ Responses route (mode
14
+ * 'responses') also marks it as the update.
14
15
  */
15
16
  import { resolveAutoEffort } from '../../providers/auto-effort.mjs';
16
- import { resolveCustomReasoning } from '../../providers/custom-reasoning.mjs';
17
+ import { isCustomProviderName, resolveCustomReasoning } from '../../providers/custom-reasoning.mjs';
17
18
  import { autoEffortModel, stepEffortConfiguration } from '../../providers/effort-configuration.mjs';
18
19
  import { effortOptionsFor } from '../../runtime-core/effort.mjs';
19
20
  import { judgeTurn, recordEffortDecision } from '../../../../effort-judge/judge-client.mjs';
@@ -35,12 +36,58 @@ const argText = (args) =>
35
36
  .slice(0, 600)
36
37
  .join('');
37
38
 
39
+ // A judgment runs beside the next iteration instead of in front of it. Its
40
+ // record lives on the loop state (`state.stepEffort`), so it can only ever
41
+ // reach the turn that started it; its promise settles the record and touches
42
+ // nothing else, so an aborted loop is never mutated afterwards. The result
43
+ // lands on the batch's last tool result in one of two places:
44
+ // - resolved before the next request is projected: on that tool result, so
45
+ // the request carries it (applyStepEffort);
46
+ // - resolved later: the request already went out with the earlier effort and
47
+ // its history is never rewritten (that would break the prompt cache), so
48
+ // the result marks the last tool result of the next batch, and the
49
+ // following request carries it (markStepEffort).
50
+ function applyResult(state, target, result) {
51
+ if (result.level) state.opts.effort = result.level;
52
+ if (result.marked) target.meta = { ...target.meta, effortConfiguration: result.marked };
53
+ }
54
+
55
+ // How long a request waits for its step judgment (measured judge p90 ≈ 284 ms).
56
+ export const STEP_EFFORT_GRACE_MS = 300;
57
+
58
+ /**
59
+ * Before a provider request is projected: wait up to the grace for the pending
60
+ * judgment and apply it if it resolved. A slower one leaves this request at
61
+ * the effort in force and lands on the following request.
62
+ */
63
+ export async function applyStepEffort(state) {
64
+ const pending = state.stepEffort;
65
+ if (!pending || pending.sent) return;
66
+ if (!pending.settled && !state.signal?.aborted) {
67
+ let timer;
68
+ await Promise.race([
69
+ pending.promise,
70
+ new Promise((resolve) => {
71
+ timer = setTimeout(resolve, STEP_EFFORT_GRACE_MS);
72
+ }),
73
+ ]);
74
+ clearTimeout(timer);
75
+ // Another caller may have handled it while this one waited.
76
+ if (pending.sent) return;
77
+ }
78
+ pending.sent = true;
79
+ if (!pending.result || state.signal?.aborted || !state.messages.includes(pending.target)) return;
80
+ applyResult(state, pending.target, pending.result);
81
+ pending.applied = true;
82
+ }
83
+
38
84
  /**
39
- * Judge the step after this batch and mark its last tool result.
85
+ * Start judging the step after this batch (not awaited). The result applies to
86
+ * the next request if it is ready by then, else to the one after.
40
87
  * `state.opts.stepAutoEffort` ({ request, base }) is set only when auto effort
41
88
  * is on for this turn and the installed judge was trained on steps.
42
89
  */
43
- export async function markStepEffort(state, assistantTurnMsg) {
90
+ export function markStepEffort(state, assistantTurnMsg) {
44
91
  const auto = state.opts?.stepAutoEffort;
45
92
  const snapshot = state.opts?.effortConfiguration;
46
93
  if (!auto || !snapshot || state.signal?.aborted) return;
@@ -55,11 +102,27 @@ export async function markStepEffort(state, assistantTurnMsg) {
55
102
  }
56
103
  }
57
104
  if (!last) return;
105
+ const previous = state.stepEffort;
106
+ if (previous) {
107
+ // It resolved after its request went out: this batch carries it. One still
108
+ // running is superseded: the newest step's judgment queues behind it and
109
+ // any judgment still waiting in between is skipped (latest wins), so the
110
+ // judge never has more than one running and one queued.
111
+ if (previous.settled) {
112
+ if (previous.result && !previous.applied) applyResult(state, last, previous.result);
113
+ } else {
114
+ previous.superseded = true;
115
+ }
116
+ }
58
117
  state.autoEffortSteps = (state.autoEffortSteps || 0) + 1;
59
118
  const step = state.autoEffortSteps;
60
119
  const provider = state.sessionRef?.provider;
61
120
  const model = state.model;
62
- const judged = await judgeTurn({
121
+ const pending = { target: last, sent: false, settled: false, applied: false, superseded: false, result: null };
122
+ const behind = previous && !previous.settled ? previous.promise : Promise.resolve();
123
+ state.stepEffort = pending;
124
+ const context = { auto, snapshot, step, provider, model, providerClient: state.provider, sessionId: state.sessionId };
125
+ const input = {
63
126
  step: {
64
127
  request: auto.request,
65
128
  step,
@@ -70,11 +133,26 @@ export async function markStepEffort(state, assistantTurnMsg) {
70
133
  result: stepResultWindow(textOf(results.get(call.id)?.content)),
71
134
  })),
72
135
  },
73
- });
136
+ };
137
+ pending.promise = behind
138
+ .then(() => (pending.superseded ? null : judgeTurn(input)))
139
+ .then((judged) => (judged ? stepResult(context, judged) : null))
140
+ // A failed judgment leaves the step at the effort in force.
141
+ .catch(() => null)
142
+ .then((result) => {
143
+ // Only this record changes here; the loop reads it at its next boundary.
144
+ pending.result = result;
145
+ pending.settled = true;
146
+ });
147
+ }
148
+
149
+ // What a resolved judgment sets: `{ marked, level }` (the tool result's effort
150
+ // configuration, and the per-request effort of a custom route), or null.
151
+ function stepResult({ auto, snapshot, step, provider, model, providerClient, sessionId }, judged) {
74
152
  // `atStep` numbers the tool-result step; `step` in a resolved decision is the ladder move.
75
153
  const record = {
76
154
  at: new Date().toISOString(),
77
- sessionId: state.sessionId,
155
+ sessionId,
78
156
  provider,
79
157
  model,
80
158
  mode: 'on',
@@ -82,9 +160,9 @@ export async function markStepEffort(state, assistantTurnMsg) {
82
160
  };
83
161
  if (!judged.probs) {
84
162
  recordEffortDecision({ ...record, skipped: judged.skipped });
85
- return;
163
+ return null;
86
164
  }
87
- const modelMeta = autoEffortModel(provider, state.provider, model);
165
+ const modelMeta = autoEffortModel(provider, providerClient, model);
88
166
  const resolved = resolveAutoEffort({
89
167
  base: auto.base,
90
168
  options: effortOptionsFor(provider, modelMeta),
@@ -96,19 +174,25 @@ export async function markStepEffort(state, assistantTurnMsg) {
96
174
  ms: judged.ms,
97
175
  probs: judged.probs.map((value) => Number(value.toFixed(3))),
98
176
  });
99
- if (snapshot.mode === 'custom') {
177
+ if (isCustomProviderName(provider)) {
100
178
  const level =
101
179
  resolved &&
102
180
  resolveCustomReasoning({
103
- protocol: state.provider?.customConfig?.protocol,
181
+ protocol: providerClient?.customConfig?.protocol,
104
182
  model: modelMeta,
105
183
  effort: resolved.effort,
106
184
  }).level;
107
- if (!level) return;
108
- state.opts.effort = level;
109
- last.meta = { ...last.meta, effortConfiguration: { ...snapshot, effort: level } };
110
- return;
185
+ if (!level) return null;
186
+ // The per-request effort also carries the step on an update route,
187
+ // where a rejected update replays the request with it.
188
+ return {
189
+ level,
190
+ marked:
191
+ snapshot.mode === 'custom'
192
+ ? { ...snapshot, effort: level }
193
+ : stepEffortConfiguration(snapshot, provider, model, level),
194
+ };
111
195
  }
112
196
  const marked = resolved && stepEffortConfiguration(snapshot, provider, model, resolved.effort);
113
- if (marked) last.meta = { ...last.meta, effortConfiguration: marked };
197
+ return marked ? { level: null, marked } : null;
114
198
  }
@@ -70,7 +70,9 @@ export async function runToolPhase(state, round, sent, onToolCall) {
70
70
  // carries a runtime notice for the model, delivered after its results.
71
71
  if (response.recoveryNotice) messages.push(response.recoveryNotice);
72
72
  const toolsEndedAt = Date.now();
73
- await markStepEffort(state, assistantTurnMsg);
73
+ // Starts the step judgment; the next request uses it if it has resolved by
74
+ // then (send-phase.mjs applyStepEffort), else the one after.
75
+ markStepEffort(state, assistantTurnMsg);
74
76
  try {
75
77
  opts.onToolPhaseCompleted?.({
76
78
  iteration: round.nextIteration,
@@ -54,6 +54,12 @@ export function createAskPromptQueue({ sessionId, promptSource }) {
54
54
  // after each turn — keeps queued `agent type=send` messages in order.
55
55
  const tail = [];
56
56
  let initialPromptSource = initialPromptSourceFor(promptSource);
57
+ // Queue entries of the input nextTurn handed out that no turn owns yet.
58
+ // Once a turn starts, its failure path acks or releases them instead.
59
+ let unstartedEntries = [];
60
+ const release = (entries) => {
61
+ if (entries?.length > 0) releasePendingMessages(sessionId, entries);
62
+ };
57
63
 
58
64
  /** The next turn's input — prompt, caller context, consumed queue entries
59
65
  * and provenance — or null when nothing model-visible is left to run. */
@@ -83,7 +89,26 @@ export function createAskPromptQueue({ sessionId, promptSource }) {
83
89
  tail.unshift(...groups.slice(1).map(tailTurnFor));
84
90
  input = queuedTurnInput(tailTurnFor(first));
85
91
  }
86
- return hasModelVisiblePromptContent(input.prompt) ? input : null;
92
+ if (!hasModelVisiblePromptContent(input.prompt)) {
93
+ release(input.pendingEntries);
94
+ return null;
95
+ }
96
+ unstartedEntries = input.pendingEntries;
97
+ return input;
98
+ };
99
+
100
+ /** The turn for the last nextTurn input started: it now owns its entries. */
101
+ const markStarted = () => {
102
+ unstartedEntries = [];
103
+ };
104
+
105
+ /** Hands every drained entry no turn started back to the pending queue —
106
+ * the waiting follow-up turns and an input whose turn never opened.
107
+ * Called once when the ask unwinds; each entry is released once. */
108
+ const releaseUnstarted = () => {
109
+ const entries = [...unstartedEntries, ...tail.splice(0).flatMap((turn) => turn.entries || [])];
110
+ unstartedEntries = [];
111
+ release(entries);
87
112
  };
88
113
 
89
114
  /** Queues drained pending-message entries as follow-up turns; true when
@@ -101,5 +126,8 @@ export function createAskPromptQueue({ sessionId, promptSource }) {
101
126
  return true;
102
127
  };
103
128
 
104
- return { nextTurn, pushDrained };
129
+ /** True while drained follow-up turns are still waiting to run. */
130
+ const hasQueuedTurns = () => tail.length > 0;
131
+
132
+ return { nextTurn, pushDrained, hasQueuedTurns, markStarted, releaseUnstarted };
105
133
  }
@@ -178,6 +178,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
178
178
  ...claimed,
179
179
  ...armTurnDurability({ sessionId, ...claimed, pendingEntries: input.pendingEntries }),
180
180
  };
181
+ // From here the turn's own unwind (ask-turn-failure) acks or releases
182
+ // its entries; the queue keeps owning only the not-yet-started ones.
183
+ queue.markStarted();
181
184
  const turn = await runAskTurn({
182
185
  sessionId,
183
186
  opened,
@@ -193,9 +196,12 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
193
196
  // that arrived while this turn was in flight runs next, in order, as a
194
197
  // follow-up user turn. The mutex is still held, so a send racing this
195
198
  // drain either landed before (picked up here) or enqueues for the next
196
- // loop. When the queue is empty we return the latest turn's result.
199
+ // loop. Follow-ups an idle ask already drained wait in the queue's
200
+ // tail and run before anything new. When both are empty we return the
201
+ // latest turn's result.
197
202
  const drained = turn.drained.length > 0 ? turn.drained : drainPendingMessages(sessionId);
198
- if (queue.pushDrained(drained)) {
203
+ queue.pushDrained(drained);
204
+ if (queue.hasQueuedTurns()) {
199
205
  // Carry the just-committed in-memory session into the follow-up
200
206
  // turn so the queued tail sees the preceding assistant/tool
201
207
  // context. loadSession() would return this same live snapshot
@@ -216,6 +222,9 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
216
222
  return result;
217
223
  }
218
224
  } finally {
225
+ // A failed/cancelled turn ends the ask: the follow-up turns drained
226
+ // behind it never ran, so their entries go back to the pending queue.
227
+ queue.releaseUnstarted();
219
228
  // A thrown setup/provider path must never leave a delayed checkpoint
220
229
  // writer alive after the turn lock is released.
221
230
  // Clear the controller only if it's still ours (closeSession may have
@@ -27,16 +27,12 @@ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, tr
27
27
  // Register the live session object for synchronous close snapshots.
28
28
  runtime.session = session;
29
29
  if (!provider) throw new Error(`Provider "${session.provider}" not available`);
30
- const autoEffort = await resolveTurnAutoEffort({ sessionId, session, provider, input });
31
- const turnEffort = autoEffort?.applied ? autoEffort.effort : session.effort || null;
32
- const effortConfiguration = prepareTurnEffortConfiguration(
33
- session,
34
- provider,
35
- autoEffort?.applied ? autoEffort.effort : undefined
36
- );
37
- // Auto effort also judges every tool-result step of the turn when the
38
- // installed judge was trained on steps (session/loop/step-auto-effort.mjs).
39
- const stepAutoEffort = turnStepAutoEffort(autoEffort, effortConfiguration);
30
+ // The turn-level judgment runs while the rest of the turn is prepared; it
31
+ // reads the session as the turn found it (synchronously, on this call).
32
+ // The first request still carries its decision: it is awaited below,
33
+ // together with the user turn, before the effort configuration is stamped.
34
+ const autoEffortPending = resolveTurnAutoEffort({ sessionId, session, provider, input });
35
+ autoEffortPending.catch(() => {});
40
36
  applyTurnContextMeta(session, provider);
41
37
  const effectiveCwd = cwdOverride || session.cwd;
42
38
  // A failed-turn retry resubmits the prompt the failure left unanswered;
@@ -46,25 +42,49 @@ export async function prepareAskTurn({ sessionId, opened, input, cwdOverride, tr
46
42
  if (session.sessionStartMetaInjected !== true && !hasUserConversationMessage(session.messages)) {
47
43
  refreshSessionBp3Environment(session, effectiveCwd);
48
44
  }
45
+ // The prewarm only opens the transport (effort never shapes the socket), so
46
+ // it starts now instead of after the judgment.
49
47
  const startupProviderPrewarm = startProviderPrewarm({
50
48
  sessionId,
51
49
  session,
52
50
  provider,
53
- turnEffort,
54
- effortConfiguration,
51
+ turnEffort: session.effort || null,
52
+ effortConfiguration: null,
55
53
  codexTurnId,
56
54
  startedAtMs: startedAt,
57
55
  });
58
- const userTurn = await buildUserTurn({
56
+ const [autoEffort, userTurn] = await Promise.all([
57
+ autoEffortPending,
58
+ buildUserTurn({
59
+ session,
60
+ prompt: input.prompt,
61
+ context: input.context,
62
+ explicitPrefetch: input.explicitPrefetch,
63
+ turnSignal,
64
+ transcriptMeta,
65
+ turnPromptSource: input.promptSource,
66
+ effortConfiguration: null,
67
+ }),
68
+ ]);
69
+ const turnEffort = autoEffort?.applied ? autoEffort.effort : session.effort || null;
70
+ const effortConfiguration = prepareTurnEffortConfiguration(
59
71
  session,
60
- prompt: input.prompt,
61
- context: input.context,
62
- explicitPrefetch: input.explicitPrefetch,
63
- turnSignal,
64
- transcriptMeta,
65
- turnPromptSource: input.promptSource,
66
- effortConfiguration,
67
- });
72
+ provider,
73
+ autoEffort?.applied ? autoEffort.effort : undefined
74
+ );
75
+ // Auto effort also judges every tool-result step of the turn when the
76
+ // installed judge was trained on steps (session/loop/step-auto-effort.mjs).
77
+ const stepAutoEffort = turnStepAutoEffort(autoEffort, effortConfiguration);
78
+ if (effortConfiguration) {
79
+ // Stamp the turn's effort on the user message (same meta order as when it
80
+ // was built with it: before the runtime-context provenance).
81
+ const { runtimeUserContext, ...meta } = userTurn.userTurnMessage.meta || {};
82
+ userTurn.userTurnMessage.meta = {
83
+ ...meta,
84
+ effortConfiguration,
85
+ ...(runtimeUserContext ? { runtimeUserContext } : {}),
86
+ };
87
+ }
68
88
  const { historyMessages, outgoing } = userTurn;
69
89
  turn.userTurnContent = userTurn.userTurnMessage.content;
70
90
  turn.outgoing = outgoing;
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
2
  import { mergeNormalizedContentEntries } from '../loop/steering.mjs';
3
3
  import { isDeliveredCompletion, logDuplicateSkip } from './delivered-completions.mjs';
4
- import { isInternalRuntimeNotificationText, promptContentText } from './prompt-utils.mjs';
4
+ import { promptContentText } from './prompt-utils.mjs';
5
5
  import { parseTaskNotification, taskNotificationId } from '../../../../shared/task-notification-envelope.mjs';
6
6
 
7
7
  const STALE_USER_INJECTION_TTL_MS = 30 * 60 * 1000;
@@ -295,15 +295,10 @@ function normalizePersistedEntryBase(entry) {
295
295
  }
296
296
 
297
297
  export function modelVisiblePendingMessages(messages) {
298
- return (Array.isArray(messages) ? messages : [])
299
- .map(pendingMessageQueueEntry)
300
- .filter(Boolean)
301
- .filter(
302
- (message) =>
303
- !isInternalRuntimeNotificationText(
304
- message && typeof message === 'object' && Object.hasOwn(message, 'content') ? message.content : message
305
- )
306
- );
298
+ // No content filter: isInternalRuntimeNotificationText is a display-only
299
+ // predicate. Every canonical completion envelope matches it, and a drained
300
+ // entry the model never sees is never claimed/acked/released.
301
+ return (Array.isArray(messages) ? messages : []).map(pendingMessageQueueEntry).filter(Boolean);
307
302
  }
308
303
 
309
304
  export function _mergePendingMessageEntries(entries) {
@@ -216,7 +216,7 @@ function releaseSessionReadCaches(id) {
216
216
  /* ignore */
217
217
  }
218
218
  try {
219
- forgetSessionSaveBaseline(id);
219
+ forgetSessionSaveBaseline(id, { releaseWriteControl: true });
220
220
  } catch {
221
221
  /* ignore */
222
222
  }
@@ -1,6 +1,6 @@
1
1
  import { sanitizeToolPairs } from '../context-utils.mjs';
2
2
  import { collectAssistantToolCallIds as assistantToolCallIds } from '../context-tool-pairs.mjs';
3
- import { isInternalRuntimeNotificationText, promptContentText } from './prompt-utils.mjs';
3
+ import { promptContentText } from './prompt-utils.mjs';
4
4
  import { filterModelVisibleSessionMessages } from './message-sanitize.mjs';
5
5
  import { journalDelta } from './turn-interruption-journal.mjs';
6
6
 
@@ -72,8 +72,11 @@ function finalizeInterruptedTurn({
72
72
  const index = provisionalUserTurnIndex(messages, currentUserContent);
73
73
  return index >= 0 && index < messages.length - 1;
74
74
  })();
75
- const preserveResponse =
76
- responseStarted && !isInternalRuntimeNotificationText(currentUserContent) && (producedOutput || !userCancelled);
75
+ // A turn woken by a task notification is preserved by the same rule as a
76
+ // user prompt: once it produced output its notification is consumed (the
77
+ // host commits it and never requeues it), so rewinding it would erase the
78
+ // turn and its side effects from history.
79
+ const preserveResponse = responseStarted && (producedOutput || !userCancelled);
77
80
  if (!preserveResponse) {
78
81
  if (!userCancelled) {
79
82
  return {
@@ -198,8 +201,11 @@ export function createTurnInterruptionTracker() {
198
201
  // copies of the same first sentence). Reference agents never
199
202
  // commit a replayed attempt's discarded stream at all; the
200
203
  // tombstone exists only for the window between a retraction and
201
- // its replacement.
202
- if (state.tombstonedAssistantContent) {
204
+ // its replacement. Whitespace is not a replacement: it is not output
205
+ // (finalizeInterruptedTurn trims, and the host commits the prompt only
206
+ // on non-blank text), so until real text arrives the retracted bytes
207
+ // remain the only output the user saw.
208
+ if (state.tombstonedAssistantContent && value.trim()) {
203
209
  state.tombstonedAssistantContent = '';
204
210
  state.tombEpoch += 1;
205
211
  }
@@ -212,8 +218,13 @@ export function createTurnInterruptionTracker() {
212
218
  // REPLACE, never accumulate: consecutive retractions belong to
213
219
  // successive attempts at the SAME answer, so keeping the earlier
214
220
  // tombstone alongside the newer one is what multiplied the visible
215
- // text across retries.
216
- state.tombstonedAssistantContent = state.partialAssistantContent.slice(cutAt);
221
+ // text across retries. A whitespace-only retraction is not a newer
222
+ // attempt at the answer (recordTextDelta never let it supersede the
223
+ // draft), so it never replaces a meaningful tombstone.
224
+ const retracted = state.partialAssistantContent.slice(cutAt);
225
+ if (retracted.trim() || !state.tombstonedAssistantContent.trim()) {
226
+ state.tombstonedAssistantContent = retracted;
227
+ }
217
228
  state.partialAssistantContent = state.partialAssistantContent.slice(0, cutAt);
218
229
  state.textEpoch += 1;
219
230
  state.tombEpoch += 1;