mixdog 1.0.12 → 1.1.0

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 (256) hide show
  1. package/README.md +75 -181
  2. package/package.json +4 -2
  3. package/scripts/build-shipped-history.mjs +131 -0
  4. package/scripts/lib/run-node-tests.mjs +235 -13
  5. package/scripts/lib/test-failure-records.mjs +5 -0
  6. package/scripts/lib/test-failure-summary.mjs +9 -3
  7. package/scripts/lib/test-spawn-ledger.mjs +19 -0
  8. package/scripts/prune-embedding-runtime.mjs +9 -5
  9. package/scripts/test-direct.mjs +16 -1
  10. package/src/agents/advisor/AGENT.md +38 -0
  11. package/src/agents/advisor/agent.json +6 -0
  12. package/src/agents/reviewer/AGENT.md +5 -2
  13. package/src/agents/reviewer/agent.json +1 -1
  14. package/src/defaults/shipped-history.json +381 -0
  15. package/src/defaults/skills/goal-management/SKILL.md +11 -5
  16. package/src/defaults/skills/setup/SKILL.md +5 -8
  17. package/src/defaults/skills/setup/references/actions.md +61 -31
  18. package/src/defaults/skills/setup/references/surfaces.md +12 -10
  19. package/src/lib/rules-builder.cjs +1 -1
  20. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +3 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  22. package/src/runtime/agent/orchestrator/config-presets.d.mts +1 -0
  23. package/src/runtime/agent/orchestrator/config-presets.mjs +44 -0
  24. package/src/runtime/agent/orchestrator/config-providers-merge.mjs +9 -1
  25. package/src/runtime/agent/orchestrator/config.mjs +102 -21
  26. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +77 -26
  27. package/src/runtime/agent/orchestrator/providers/anthropic-api-request.mjs +44 -14
  28. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +2 -1
  29. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +11 -2
  30. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +2 -0
  31. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +3 -3
  32. package/src/runtime/agent/orchestrator/providers/custom-provider-config.mjs +388 -0
  33. package/src/runtime/agent/orchestrator/providers/custom-provider.mjs +92 -109
  34. package/src/runtime/agent/orchestrator/providers/custom-reasoning.mjs +107 -0
  35. package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +51 -9
  36. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +2 -1
  37. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +14 -9
  38. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-send.mjs +51 -11
  39. package/src/runtime/agent/orchestrator/providers/openai-compat-chat-stream.mjs +6 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-models.mjs +7 -5
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-options.mjs +12 -0
  42. package/src/runtime/agent/orchestrator/providers/openai-compat-responses.mjs +27 -7
  43. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +12 -2
  44. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +2 -1
  46. package/src/runtime/agent/orchestrator/providers/quota-reading.mjs +70 -0
  47. package/src/runtime/agent/orchestrator/providers/registry.mjs +38 -3
  48. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +19 -35
  49. package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +5 -0
  50. package/src/runtime/agent/orchestrator/runtime-core/goal-mutations.mjs +0 -17
  51. package/src/runtime/agent/orchestrator/runtime-core/goal-state.mjs +0 -3
  52. package/src/runtime/agent/orchestrator/runtime-core/goal-store.mjs +0 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/goal-text.mjs +1 -1
  54. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-defs.mjs +1 -1
  55. package/src/runtime/agent/orchestrator/runtime-core/goal-tool-exec.mjs +1 -2
  56. package/src/runtime/agent/orchestrator/runtime-core/goal-turns.mjs +18 -4
  57. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +7 -1
  58. package/src/runtime/agent/orchestrator/runtime-core/session-route-policy.mjs +16 -0
  59. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +2 -0
  60. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +7 -7
  62. package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +6 -0
  63. package/src/runtime/agent/orchestrator/session/loop/step-auto-effort.mjs +28 -3
  64. package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +24 -11
  65. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +2 -6
  66. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +63 -3
  67. package/src/runtime/agent/orchestrator/session/manager/session-tool-surface.mjs +4 -4
  68. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +1 -0
  69. package/src/runtime/agent/orchestrator/stall-policy.mjs +0 -1
  70. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +19 -20
  72. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -11
  73. package/src/runtime/agent/orchestrator/tools/builtin/file-version-test-helpers.mjs +11 -0
  74. package/src/runtime/agent/orchestrator/tools/builtin/file-version.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +8 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-task-output.mjs +25 -12
  77. package/src/runtime/agent/orchestrator/tools/builtin/raw-content-publication.mjs +17 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +136 -76
  79. package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +15 -17
  80. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -2
  81. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +11 -10
  82. package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -3
  83. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +30 -16
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +4 -11
  85. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +7 -3
  86. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-validation.mjs +16 -19
  87. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +13 -13
  88. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background.mjs +4 -1
  89. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +14 -4
  90. package/src/runtime/agent/orchestrator/tools/patch/apply-patch/codex-batch.mjs +58 -8
  91. package/src/runtime/agent/orchestrator/tools/patch/content-guard.mjs +47 -0
  92. package/src/runtime/agent/orchestrator/tools/patch/delete-quarantine.mjs +155 -0
  93. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-proofs.mjs +6 -2
  94. package/src/runtime/agent/orchestrator/tools/patch/dispatch/native-snapshots.mjs +6 -2
  95. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +19 -16
  96. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +13 -44
  97. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  98. package/src/runtime/agent/orchestrator/tools/patch/post-patch-excerpts.mjs +5 -2
  99. package/src/runtime/agent/orchestrator/tools/patch/replay-capture.mjs +47 -7
  100. package/src/runtime/agent/orchestrator/tools/patch/rollback-state.mjs +72 -8
  101. package/src/runtime/agent/orchestrator/tools/patch/section-coalesce.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/patch/sequence/run.mjs +9 -1
  103. package/src/runtime/agent/orchestrator/tools/patch/sequence/units.mjs +9 -1
  104. package/src/runtime/agent/orchestrator/tools/patch/sequence.mjs +6 -6
  105. package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +16 -4
  106. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -14
  107. package/src/runtime/agent/orchestrator/tools/patch/wave.mjs +42 -4
  108. package/src/runtime/agent/orchestrator/tools/shell-codepage.mjs +296 -0
  109. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +75 -24
  110. package/src/runtime/channels/lib/config.mjs +3 -8
  111. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +7 -3
  112. package/src/{tui → runtime/channels}/lib/voice-setup.mjs +38 -22
  113. package/src/runtime/channels/lib/webhook/request-handler.mjs +42 -8
  114. package/src/runtime/computer-bridge/actions.mjs +2 -0
  115. package/src/runtime/effort-judge/judge-client.mjs +12 -3
  116. package/src/runtime/effort-judge/model-manifest.json +7 -7
  117. package/src/runtime/effort-judge/previous-human-request.mjs +162 -0
  118. package/src/runtime/effort-judge/verified-context-input.mjs +10 -0
  119. package/src/runtime/memory/lib/embedding-provider.mjs +12 -0
  120. package/src/runtime/memory/lib/http-router/session-routes.mjs +2 -1
  121. package/src/runtime/memory/lib/memory-config-flags.mjs +3 -2
  122. package/src/runtime/memory/lib/memory-embed.mjs +5 -0
  123. package/src/runtime/shared/agent-route-config.mjs +50 -2
  124. package/src/runtime/shared/bounded-file-read.mjs +166 -0
  125. package/src/runtime/shared/builtin-grandfathering.mjs +37 -0
  126. package/src/runtime/shared/config.mjs +24 -4
  127. package/src/runtime/shared/defaults-separation.mjs +263 -0
  128. package/src/runtime/shared/llm/anthropic-effort.mjs +4 -0
  129. package/src/runtime/shared/llm/model-catalog.mjs +23 -0
  130. package/src/runtime/shared/llm/usage-ledger-rollup.mjs +105 -1
  131. package/src/runtime/shared/llm/usage-ledger-worker.mjs +2 -1
  132. package/src/runtime/shared/llm/usage-ledger.mjs +72 -5
  133. package/src/runtime/shared/onnx-runtime-support.mjs +6 -0
  134. package/src/runtime/shared/orchestration.mjs +14 -6
  135. package/src/runtime/shared/projects.mjs +65 -48
  136. package/src/runtime/shared/provider-accounts.mjs +47 -6
  137. package/src/runtime/shared/quota-resume-store.mjs +255 -0
  138. package/src/runtime/shared/shipped-definitions.mjs +275 -0
  139. package/src/runtime/shared/tool-card-model.golden.json +3 -3
  140. package/src/runtime/shared/tool-result-summary.mjs +86 -15
  141. package/src/runtime/shared/transcript-metadata.mjs +2 -0
  142. package/src/runtime/shared/turn-review-reply.mjs +2 -0
  143. package/src/runtime/shared/turn-snapshot.mjs +65 -6
  144. package/src/runtime/shared/turn-worktree-snapshot.mjs +202 -22
  145. package/src/session-runtime/boot/apis.mjs +19 -0
  146. package/src/session-runtime/boot/config.mjs +4 -0
  147. package/src/session-runtime/boot/facade.mjs +4 -1
  148. package/src/session-runtime/boot/infrastructure.mjs +21 -2
  149. package/src/session-runtime/boot/session.mjs +3 -0
  150. package/src/session-runtime/channel-config-api.mjs +5 -0
  151. package/src/session-runtime/config-lifecycle/config-writers.mjs +3 -0
  152. package/src/session-runtime/config-lifecycle.mjs +31 -6
  153. package/src/session-runtime/goal-facade-api.mjs +4 -0
  154. package/src/session-runtime/lifecycle/inheritance.mjs +28 -6
  155. package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +3 -0
  156. package/src/session-runtime/lifecycle/session-switch/resume.mjs +7 -1
  157. package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +6 -0
  158. package/src/session-runtime/lifecycle/teardown/session-close.mjs +14 -2
  159. package/src/session-runtime/media-api.mjs +32 -22
  160. package/src/session-runtime/model-recency.mjs +14 -0
  161. package/src/session-runtime/model-route/route-tuning.mjs +3 -5
  162. package/src/session-runtime/model-route/set-route.mjs +49 -21
  163. package/src/session-runtime/model-route-api.mjs +2 -1
  164. package/src/session-runtime/notification-bus.mjs +31 -6
  165. package/src/session-runtime/provider-auth/login.mjs +6 -0
  166. package/src/session-runtime/provider-models/catalog-load.mjs +3 -1
  167. package/src/session-runtime/provider-models/row-hydration.mjs +15 -14
  168. package/src/session-runtime/quick-model-rows/model-meta.mjs +26 -0
  169. package/src/session-runtime/quick-model-rows/provider-rows.mjs +40 -14
  170. package/src/session-runtime/resource-api.mjs +8 -1
  171. package/src/session-runtime/runtime-core.mjs +4 -0
  172. package/src/session-runtime/runtime-facade.mjs +20 -1
  173. package/src/session-runtime/runtime-review-api.mjs +12 -1
  174. package/src/session-runtime/services/agent-tool/helpers.mjs +4 -0
  175. package/src/session-runtime/services/agent-tool/index-write-queue.mjs +10 -15
  176. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +11 -3
  177. package/src/session-runtime/services/agent-tool/spawn-preset.mjs +2 -0
  178. package/src/session-runtime/services/channel-admin.mjs +87 -13
  179. package/src/session-runtime/services/provider-admin.mjs +256 -63
  180. package/src/session-runtime/services/seeds.mjs +73 -142
  181. package/src/session-runtime/services/usage-dashboard-model.mjs +29 -0
  182. package/src/session-runtime/services/usage-dashboard.mjs +7 -0
  183. package/src/session-runtime/session-address.mjs +125 -0
  184. package/src/session-runtime/session-create.mjs +57 -2
  185. package/src/session-runtime/settings-builtin-tools-api.mjs +8 -0
  186. package/src/session-runtime/settings-compaction-api.mjs +13 -0
  187. package/src/session-runtime/setup-tool/executor.mjs +92 -40
  188. package/src/session-runtime/setup-tool/extended-actions.mjs +64 -5
  189. package/src/session-runtime/setup-tool/settings-contract.mjs +44 -1
  190. package/src/session-runtime/setup-tool/tool-defs.mjs +12 -7
  191. package/src/session-runtime/tool-surface.mjs +16 -9
  192. package/src/session-runtime/turn/session-ops.mjs +3 -0
  193. package/src/session-runtime/turn/share-ops.mjs +4 -1
  194. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +6 -0
  195. package/src/session-runtime/usage-stats-api.mjs +15 -0
  196. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +31 -5
  197. package/src/session-runtime/workflow-agents-api/agent-route.mjs +4 -0
  198. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +27 -7
  199. package/src/standalone/daemon-boot-coordinator.mjs +10 -0
  200. package/src/standalone/daemon-drain-policy.mjs +9 -0
  201. package/src/standalone/daemon-stored-session-views.mjs +7 -8
  202. package/src/standalone/daemon.mjs +21 -5
  203. package/src/standalone/local-session-runtime.mjs +11 -0
  204. package/src/standalone/session-client.mjs +52 -7
  205. package/src/standalone/session-protocol.mjs +8 -0
  206. package/src/standalone/session-service/agent-tree/agent-child.mjs +2 -0
  207. package/src/standalone/session-service/entries/lifecycle.mjs +2 -0
  208. package/src/standalone/session-service/entries/session-loads.mjs +9 -0
  209. package/src/standalone/session-service/projection/revisions.mjs +28 -4
  210. package/src/standalone/session-service/projection.mjs +1 -0
  211. package/src/standalone/session-service/quota-resume.mjs +140 -0
  212. package/src/standalone/session-service/session-calls/session-action.mjs +2 -2
  213. package/src/standalone/session-service/session-calls.mjs +10 -2
  214. package/src/standalone/session-service/stored-reader.mjs +26 -1
  215. package/src/standalone/session-service.mjs +21 -0
  216. package/src/standalone/session-transport/routes.mjs +2 -0
  217. package/src/tui/app/doctor.mjs +7 -11
  218. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +3 -5
  219. package/src/tui/app/settings-picker/settings-rows.mjs +4 -6
  220. package/src/tui/app/settings-picker/settings-toggles/store-toggles.mjs +1 -1
  221. package/src/tui/app/slash-dispatch/route-commands.mjs +3 -2
  222. package/src/tui/components/ContextPanel.jsx +5 -1
  223. package/src/tui/components/Spinner.jsx +34 -319
  224. package/src/tui/components/SpinnerByline.jsx +256 -0
  225. package/src/tui/components/use-spinner-progress.mjs +113 -0
  226. package/src/tui/dist/index.mjs +15540 -14452
  227. package/src/tui/session/agent-job-feed/execution-delivery.mjs +13 -1
  228. package/src/tui/session/agent-job-feed/notification-router.mjs +3 -0
  229. package/src/tui/session/agent-job-feed/pending-resume.mjs +14 -1
  230. package/src/tui/session/agent-job-feed.mjs +4 -0
  231. package/src/tui/session/context-state.mjs +13 -1
  232. package/src/tui/session/initial-state.mjs +6 -0
  233. package/src/tui/session/quota-resume.mjs +263 -0
  234. package/src/tui/session/session-api/intake/abort.mjs +39 -35
  235. package/src/tui/session/session-api/intake/submission.mjs +13 -21
  236. package/src/tui/session/session-api/intake.mjs +3 -3
  237. package/src/tui/session/session-api/integrations.mjs +5 -1
  238. package/src/tui/session/session-api/lifecycle/dispose.mjs +1 -0
  239. package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -2
  240. package/src/tui/session/session-api/lifecycle/session-reset.mjs +4 -0
  241. package/src/tui/session/session-api/lifecycle.mjs +71 -2
  242. package/src/tui/session/session-api/media.mjs +2 -2
  243. package/src/tui/session/session-api/quota-resume.mjs +21 -0
  244. package/src/tui/session/session-api/routes.mjs +2 -0
  245. package/src/tui/session/session-api/settings.mjs +1 -0
  246. package/src/tui/session/session-api.mjs +2 -0
  247. package/src/tui/session/session-flow/auto-clear.mjs +14 -6
  248. package/src/tui/session/session-flow/drain.mjs +52 -5
  249. package/src/tui/session/session-flow/queue/take-entries.mjs +2 -3
  250. package/src/tui/session/session-flow/queue.mjs +3 -0
  251. package/src/tui/session/session-flow/steering.mjs +11 -0
  252. package/src/tui/session/session-flow.mjs +1 -0
  253. package/src/tui/session/tool-approval.mjs +7 -2
  254. package/src/tui/session/transcript-intake.mjs +1 -0
  255. package/src/tui/session/turn.mjs +40 -3
  256. package/src/vendor/statusline/bin/statusline-route.mjs +3 -2
@@ -22,7 +22,15 @@ function ackModelVisible(event) {
22
22
 
23
23
  // The `execution-ui` delivery: one transcript card per execution card key,
24
24
  // then the model-visible completion body enqueued once per execution.
25
- export function createExecutionDelivery({ dedup, pendingResume, enqueue, nextId, pushResponse, statusRefresh }) {
25
+ export function createExecutionDelivery({
26
+ dedup,
27
+ pendingResume,
28
+ enqueue,
29
+ nextId,
30
+ pushResponse,
31
+ statusRefresh,
32
+ getAdmission = () => 'open',
33
+ }) {
26
34
  function pushCard({ event, text, parsed, delivery, executionId, status, terminal }) {
27
35
  const cardKey = executionCardKey(event, text, parsed);
28
36
  const firstDelivery = !cardKey || !dedup.hasNotificationKey(cardKey);
@@ -65,6 +73,10 @@ export function createExecutionDelivery({ dedup, pendingResume, enqueue, nextId,
65
73
  function enqueueCompletion({ event, text, delivery, executionId, notificationKey }) {
66
74
  const resumeBody = String(delivery.modelContent || '').trim();
67
75
  if (!resumeBody) return;
76
+ // Validated at consumption (not only at the bus): a closing/closed owner
77
+ // keeps the card for UI/history but never queues a model-visible twin, and
78
+ // does not ack it as model-delivered.
79
+ if (getAdmission() !== 'open' || event?.meta?.owner_closed === true) return;
68
80
  const completionKey = executionResumeKey(resumeBody, executionId);
69
81
  if (pendingResume.isDiscarded(completionKey) || isDeliveredCompletion({ executionId, text: resumeBody })) {
70
82
  ackModelVisible(event);
@@ -13,6 +13,7 @@ export function createNotificationRouter({
13
13
  statusRefresh,
14
14
  executionDelivery,
15
15
  modelNotification,
16
+ getAdmission = () => 'open',
16
17
  }) {
17
18
  let uiOpenSeq = 0;
18
19
  let setupChangeSeq = 0;
@@ -68,6 +69,8 @@ export function createNotificationRouter({
68
69
  });
69
70
  }
70
71
  statusRefresh.refresh(parsed);
72
+ // A closing/closed owner enqueues no model work from a notification.
73
+ if (getAdmission() !== 'open' || event?.meta?.owner_closed === true) return false;
71
74
  return modelNotification.enqueueNotification({ event, text, delivery, notificationKey });
72
75
  };
73
76
  }
@@ -25,6 +25,7 @@ export function createPendingResume({
25
25
  now,
26
26
  tombstoneTtlMs,
27
27
  tombstoneLimit,
28
+ getAdmission = () => 'open',
28
29
  }) {
29
30
  let deferred = false;
30
31
  // Completion keys explicitly abandoned by Esc. Tombstones are per-feed
@@ -88,8 +89,20 @@ export function createPendingResume({
88
89
  function kick(body = '', completionKey = '') {
89
90
  const key = executionResumeKey(body, completionKey);
90
91
  if (body && isDiscarded(key)) return;
92
+ // Admission fence: a closed session never resumes from a completion; a
93
+ // closing one defers (a failed close flushes the deferred kick).
94
+ const admission = getAdmission();
95
+ if (admission === 'terminal') {
96
+ bodies.length = 0;
97
+ deferred = false;
98
+ return;
99
+ }
91
100
  if (body) bodies.push({ body, key });
92
101
  if (getDisposed()) return;
102
+ if (admission === 'closing') {
103
+ deferred = true;
104
+ return;
105
+ }
93
106
  if (getState().busy) {
94
107
  deferred = true;
95
108
  return;
@@ -104,7 +117,7 @@ export function createPendingResume({
104
117
  }
105
118
 
106
119
  function flushDeferred() {
107
- if (!deferred || getDisposed() || getState().busy) return;
120
+ if (!deferred || getDisposed() || getState().busy || getAdmission() !== 'open') return;
108
121
  kick();
109
122
  }
110
123
 
@@ -40,6 +40,7 @@ export function createAgentJobFeed({
40
40
  now = () => Date.now(),
41
41
  executionResumeTombstoneTtlMs = 30_000,
42
42
  executionResumeTombstoneLimit = 128,
43
+ getAdmission = () => 'open',
43
44
  }) {
44
45
  const dedup = createExecutionDedup({ displayedNotificationKeys: displayedExecutionNotificationKeys });
45
46
  const pendingResume = createPendingResume({
@@ -51,6 +52,7 @@ export function createAgentJobFeed({
51
52
  now,
52
53
  tombstoneTtlMs: executionResumeTombstoneTtlMs,
53
54
  tombstoneLimit: executionResumeTombstoneLimit,
55
+ getAdmission,
54
56
  });
55
57
  const card = createAgentJobCard({ getState, itemIndexById, patchItem });
56
58
  const statusRefresh = createAgentStatusRefresh({ set, getDisposed, agentStatusState });
@@ -61,6 +63,7 @@ export function createAgentJobFeed({
61
63
  nextId,
62
64
  pushResponse: pushAsyncAgentResponse || pushUserOrSyntheticItem,
63
65
  statusRefresh,
66
+ getAdmission,
64
67
  });
65
68
  const modelNotification = createModelNotificationEnqueue({
66
69
  chain: createNotificationEnqueueChain({ pushNotice }),
@@ -76,6 +79,7 @@ export function createAgentJobFeed({
76
79
  statusRefresh,
77
80
  executionDelivery,
78
81
  modelNotification,
82
+ getAdmission,
79
83
  });
80
84
 
81
85
  function subscribeRuntimeNotifications() {
@@ -8,7 +8,14 @@
8
8
  import { contextMeasurementStats } from '../../runtime/shared/context-measurement.mjs';
9
9
  import { goalStateSnapshot } from '../../runtime/agent/orchestrator/runtime-core/goal-state.mjs';
10
10
 
11
- export function createContextState({ runtime, getState, updateState, getPendingSessionReset, getVisibleGoal }) {
11
+ export function createContextState({
12
+ runtime,
13
+ getState,
14
+ updateState,
15
+ getPendingSessionReset,
16
+ getVisibleGoal,
17
+ getQuotaResume = null,
18
+ }) {
12
19
  const autoClearState = () =>
13
20
  runtime.getAutoClear?.() ||
14
21
  runtime.autoClear || {
@@ -46,6 +53,8 @@ export function createContextState({ runtime, getState, updateState, getPendingS
46
53
  clientHostPid: runtime.clientHostPid || null,
47
54
  model: runtime.model,
48
55
  provider: runtime.provider,
56
+ sessionProvider: runtime.sessionProvider ?? null,
57
+ sessionModel: runtime.sessionModel ?? null,
49
58
  effort: runtime.effort,
50
59
  effortOptions: runtime.effortOptions,
51
60
  fast: runtime.fast,
@@ -69,6 +78,9 @@ export function createContextState({ runtime, getState, updateState, getPendingS
69
78
  // 안 보이던 골이 생성되었다 바로 사라짐).
70
79
  // Clock-anchored, so an unchanged running Goal reads equal on every pulse.
71
80
  goal: goalStateSnapshot(typeof getVisibleGoal === 'function' ? getVisibleGoal() : runtime.goalStatus?.() || null),
81
+ // The daemon-queued quota resume of this session id (identity-stable while
82
+ // unchanged), so a resume or session switch republishes it with the route.
83
+ quotaResume: typeof getQuotaResume === 'function' ? getQuotaResume() : null,
72
84
  });
73
85
 
74
86
  const routeState = () => {
@@ -23,6 +23,12 @@ export function createInitialSessionState({ runtime, runtimeCwd, baseRouteState
23
23
  queued: [],
24
24
  thinking: null,
25
25
  toolApproval: null,
26
+ // How the last approval ended and from which device, so every device can
27
+ // say who answered ({ id, approved, device, at }).
28
+ toolApprovalResult: null,
29
+ // A prompt reclaimed by a DIFFERENT device than the one that sent it:
30
+ // { id, ids, text, device, at } — only the sender's composer applies it.
31
+ promptRestore: null,
26
32
  lastTurn: null,
27
33
  stats: createSessionStats(),
28
34
  // Incremental derivations published by the session runtime so App does not scan all
@@ -0,0 +1,263 @@
1
+ /**
2
+ * quota-resume.mjs — one Lead session's side of the quota auto-resume: it
3
+ * queues a resume when a turn stops on exhausted provider quota, cancels it on
4
+ * the inputs that supersede it, owns the one "retry failed turn" operation,
5
+ * and runs the resume when the daemon scheduler fires it.
6
+ *
7
+ * The durable queue itself lives in the daemon-wide store
8
+ * (runtime/shared/quota-resume-store.mjs); this lane never keeps a timer, so an
9
+ * evicted session waits exactly like a live one.
10
+ */
11
+ import {
12
+ cancelQuotaResumeEntry,
13
+ forgetQuotaSuspendedGoal,
14
+ noteQuotaSuspendedGoal,
15
+ publicQuotaResume,
16
+ quotaAutoResumeEnabled,
17
+ quotaResumeEntry,
18
+ quotaSuspendedGoal,
19
+ scheduleQuotaResumeEntry,
20
+ subscribeQuotaResume,
21
+ } from '../../runtime/shared/quota-resume-store.mjs';
22
+ import { createSubmissionIntake } from './session-api/intake/submission.mjs';
23
+
24
+ export const RETRY_CONTINUE_INSTRUCTION = 'Continue from where you left off.';
25
+
26
+ // Rows that frame a turn rather than carry its output (desktop transcript-rows).
27
+ const TURN_CHROME_KINDS = new Set(['user', 'turndone', 'statusdone', 'notice']);
28
+
29
+ function stableItemKey(item, index) {
30
+ if (item?.id !== undefined && item?.id !== null) return String(item.id);
31
+ return `${item?.kind || 'item'}:${item?.text || item?.label || item?.status || ''}:${index}`;
32
+ }
33
+
34
+ /** Turn identity per transcript row — the same keys the desktop derives. */
35
+ export function transcriptTurnKeys(items) {
36
+ const transcript = Array.isArray(items) ? items : [];
37
+ const keys = [];
38
+ let activeTurn = null;
39
+ for (let index = 0; index < transcript.length; index += 1) {
40
+ const item = transcript[index];
41
+ if (item?.kind === 'user') activeTurn = `turn:${stableItemKey(item, index)}`;
42
+ if (!activeTurn) activeTurn = `segment:${stableItemKey(item, index)}`;
43
+ keys.push(activeTurn);
44
+ if (item?.kind === 'turndone') activeTurn = null;
45
+ }
46
+ return keys;
47
+ }
48
+
49
+ /** The failed turndone that ended the transcript on exhausted provider quota,
50
+ * when no later turn has begun: { turnKey, quota: { provider, model, resetAt|null } }. */
51
+ export function latestQuotaFailure(items) {
52
+ const transcript = Array.isArray(items) ? items : [];
53
+ for (let index = transcript.length - 1; index >= 0; index -= 1) {
54
+ const item = transcript[index];
55
+ if (item?.kind === 'user') return null;
56
+ if (item?.kind !== 'turndone') continue;
57
+ const quota = item.quota;
58
+ if (String(item.status || '').toLowerCase() !== 'failed' || !quota || typeof quota.provider !== 'string') {
59
+ return null;
60
+ }
61
+ const turnKey = transcriptTurnKeys(transcript)[index] || '';
62
+ if (!turnKey) return null;
63
+ return {
64
+ turnKey,
65
+ quota: {
66
+ provider: quota.provider,
67
+ model: typeof quota.model === 'string' ? quota.model : '',
68
+ resetAt: typeof quota.resetAt === 'number' && Number.isFinite(quota.resetAt) ? quota.resetAt : null,
69
+ },
70
+ };
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /** What retrying the turn submits: its prompt when it produced no output, the
76
+ * continuation instruction when it did, null when the prompt carried no text
77
+ * (a media/attachment-only prompt cannot be replayed). */
78
+ export function failedTurnRetryPrompt(items, turnKey) {
79
+ const transcript = Array.isArray(items) ? items : [];
80
+ const keys = transcriptTurnKeys(transcript);
81
+ let prompt = '';
82
+ let sampled = false;
83
+ let hasAttachments = false;
84
+ for (let index = 0; index < transcript.length; index += 1) {
85
+ if (keys[index] !== turnKey) continue;
86
+ const item = transcript[index];
87
+ if (item?.kind === 'user' && !prompt) {
88
+ prompt = String(item.text ?? '').trim();
89
+ hasAttachments = Array.isArray(item.images) && item.images.length > 0;
90
+ }
91
+ if (!TURN_CHROME_KINDS.has(String(item?.kind || ''))) sampled = true;
92
+ }
93
+ if (!prompt) return null;
94
+ // Transcript rows keep only attachment metadata, so a text-only replay
95
+ // would drop the attachments and the rewind would delete the original row.
96
+ if (hasAttachments && !sampled) return null;
97
+ return sampled ? RETRY_CONTINUE_INSTRUCTION : prompt;
98
+ }
99
+
100
+ export function createQuotaResumeLane(bag) {
101
+ const { runtime, flags, getState, set, pending } = bag;
102
+ const currentId = () => String(getState().sessionId || '');
103
+ let intake = null;
104
+ const submitAsync = (...args) => {
105
+ intake ??= createSubmissionIntake(bag);
106
+ return intake.submitAsync(...args);
107
+ };
108
+
109
+ const publishQuotaResume = () => {
110
+ const next = publicQuotaResume(quotaResumeEntry(currentId()));
111
+ if ((getState().quotaResume ?? null) !== next) set({ quotaResume: next });
112
+ };
113
+ const unsubscribe = subscribeQuotaResume((sessionId) => {
114
+ if (flags.disposed) return;
115
+ if (sessionId === null || sessionId === currentId()) publishQuotaResume();
116
+ });
117
+
118
+ const requireAddressed = (sessionId) => {
119
+ if (!sessionId || currentId() !== sessionId) throw new Error('session is not the active session');
120
+ };
121
+
122
+ // The session's own route (the selected route only before it has one).
123
+ const sessionRoute = () => ({
124
+ routeProvider: String(runtime.sessionProvider ?? runtime.provider ?? ''),
125
+ routeModel: String(runtime.sessionModel ?? runtime.model ?? ''),
126
+ });
127
+ const queueResume = (sessionId, failure) =>
128
+ scheduleQuotaResumeEntry({ sessionId, turnKey: failure.turnKey, ...failure.quota, ...sessionRoute() });
129
+
130
+ // A failed turn is settling between its failed turndone (where the resume is
131
+ // queued) and its Goal settlement (where the suspended Goal is recorded).
132
+ // Every superseding cancel bumps the epoch, so a cancel that lands inside
133
+ // that window also keeps the settlement from recording the Goal.
134
+ let settlement = null;
135
+ let cancelEpoch = 0;
136
+
137
+ const cancelFor = (sessionId) => {
138
+ cancelEpoch += 1;
139
+ cancelQuotaResumeEntry(sessionId);
140
+ forgetQuotaSuspendedGoal(sessionId);
141
+ };
142
+
143
+ /** The one retry operation. `automatic` is the scheduler's resume: it
144
+ * enqueues directly, because the hours it waited must not trigger the idle
145
+ * auto-clear a user submission starts. */
146
+ function retryFailedTurn(turnKey, { automatic = false } = {}) {
147
+ const prompt = failedTurnRetryPrompt(getState().items, turnKey);
148
+ if (!prompt) return false;
149
+ if (automatic) {
150
+ return bag.enqueue(prompt, { mode: 'prompt', id: bag.nextId(), displayText: prompt, retryFailedTurn: true });
151
+ }
152
+ return submitAsync(prompt, { retryFailedTurn: true });
153
+ }
154
+
155
+ return {
156
+ publishQuotaResume,
157
+ disposeQuotaResume: unsubscribe,
158
+
159
+ /** Turn close, synchronously with the failed turndone: with a known reset
160
+ * and the setting on, queues the resume, so the same publication that
161
+ * shows the failure carries `quotaResume`. */
162
+ beginQuotaSettlement() {
163
+ const id = currentId();
164
+ const failure = latestQuotaFailure(getState().items);
165
+ settlement = null;
166
+ if (!id || !failure) return null;
167
+ settlement = { sessionId: id, turnKey: failure.turnKey, epoch: cancelEpoch };
168
+ if (failure.quota.resetAt === null || !quotaAutoResumeEnabled()) return null;
169
+ return queueResume(id, failure);
170
+ },
171
+
172
+ /** After the Goal settled: records the Goal this failure suspended, unless
173
+ * a superseding cancel landed since the turn closed. */
174
+ finishQuotaSettlement({ suspendedGoalId = '' } = {}) {
175
+ const current = settlement;
176
+ settlement = null;
177
+ if (!current || current.epoch !== cancelEpoch || !suspendedGoalId) return false;
178
+ return noteQuotaSuspendedGoal(current.sessionId, current.turnKey, suspendedGoalId);
179
+ },
180
+
181
+ /** New user input supersedes the stopped turn. */
182
+ cancelQuotaResumeOnUserInput() {
183
+ const id = currentId();
184
+ if (id) cancelFor(id);
185
+ },
186
+
187
+ /** Explicit request to auto-resume the latest quota-failed turn. */
188
+ scheduleQuotaResume(sessionId) {
189
+ requireAddressed(sessionId);
190
+ const failure = latestQuotaFailure(getState().items);
191
+ if (!failure || failure.quota.resetAt === null) {
192
+ throw new Error('latest turn did not fail on exhausted provider quota with a known reset time');
193
+ }
194
+ queueResume(sessionId, failure);
195
+ return true;
196
+ },
197
+
198
+ cancelQuotaResume(sessionId) {
199
+ requireAddressed(sessionId);
200
+ cancelQuotaResumeEntry(sessionId);
201
+ return true;
202
+ },
203
+
204
+ /** A route change or a session clear/delete supersedes the wait. */
205
+ supersedeQuotaResume(sessionId) {
206
+ if (sessionId) cancelFor(String(sessionId));
207
+ },
208
+
209
+ async retryFailedTurn(sessionId, turnKey) {
210
+ requireAddressed(sessionId);
211
+ return (await retryFailedTurn(String(turnKey || ''))) === true;
212
+ },
213
+
214
+ /** Scheduler fire for the entry of `generation`, after a usable reading.
215
+ * Synchronous from the generation check to the admitted retry, so no
216
+ * cancellation can land between them. Returns the outcome. */
217
+ fireQuotaResume({ generation } = {}) {
218
+ const id = currentId();
219
+ const entry = quotaResumeEntry(id);
220
+ if (!id || !entry || entry.generation !== generation) return 'stale';
221
+ const state = getState();
222
+ if (
223
+ flags.disposed ||
224
+ settlement !== null ||
225
+ (flags.admission || 'open') !== 'open' ||
226
+ state.busy ||
227
+ state.commandBusy ||
228
+ flags.autoClearRunning ||
229
+ flags.draining ||
230
+ pending.length > 0
231
+ ) {
232
+ return 'busy';
233
+ }
234
+ const failure = latestQuotaFailure(state.items);
235
+ if (!failure || failure.turnKey !== entry.turnKey) {
236
+ cancelQuotaResumeEntry(id, { generation });
237
+ return 'superseded';
238
+ }
239
+ const route = sessionRoute();
240
+ if (route.routeProvider !== entry.routeProvider || route.routeModel !== entry.routeModel) {
241
+ cancelQuotaResumeEntry(id, { generation });
242
+ return 'route-changed';
243
+ }
244
+ const goalId = quotaSuspendedGoal(id, entry.turnKey);
245
+ // A user turn replays its prompt; an automatic Goal turn has no user row
246
+ // and resumes as its Goal's next continuation.
247
+ const userTurn = entry.turnKey.startsWith('turn:');
248
+ if (userTurn ? !failedTurnRetryPrompt(state.items, entry.turnKey) : !goalId) {
249
+ cancelQuotaResumeEntry(id, { generation });
250
+ return 'unreplayable';
251
+ }
252
+ if (!cancelQuotaResumeEntry(id, { generation })) return 'stale';
253
+ forgetQuotaSuspendedGoal(id);
254
+ // Queued on the Goal's mutation chain ahead of the retry turn's own
255
+ // start, so that turn opens on the revived Goal; the retry is busy
256
+ // before the revived Goal's continuation check runs, so it queues none.
257
+ // Without a retry the revived Goal's continuation is the one turn.
258
+ if (goalId) void Promise.resolve(runtime.reactivateUsageLimitedGoal?.(goalId)).catch(() => {});
259
+ if (!userTurn) return 'fired';
260
+ return retryFailedTurn(entry.turnKey, { automatic: true }) !== false ? 'fired' : 'rejected';
261
+ },
262
+ };
263
+ }
@@ -1,7 +1,6 @@
1
- // Abort: Esc while idle hands a queued or still-accepting submission back to
2
- // the draft; Esc during a turn cancels it and, unless a steering prompt is
3
- // already queued, reclaims the in-flight prompt, its history slot and its
4
- // requeue entries.
1
+ // Abort: Esc while idle hands a queued submission back to the draft; Esc
2
+ // during a turn cancels it and, unless a steering prompt is already queued,
3
+ // reclaims the in-flight prompt, its history slot and its requeue entries.
5
4
  import { hydratePastedAttachments, hydrateRestorableFileParts } from '../../../../runtime/attachments/store.mjs';
6
5
  import { abortGoalTurn } from '../../goal-turn-state.mjs';
7
6
  import { promptHistoryWithout } from '../../prompt-history.mjs';
@@ -15,16 +14,15 @@ function hydrateTolerantly(pastedImages, pastedTexts, content = null) {
15
14
  };
16
15
  const hydrated = hydratePastedAttachments(pastedImages, pastedTexts, { onUnreadable });
17
16
  const files = hydrateRestorableFileParts(content, { onUnreadable });
17
+ const verb = unreadable === 1 ? ' was' : 's were';
18
18
  return {
19
19
  ...hydrated,
20
20
  ...(files.length ? { content: files } : {}),
21
- notice: unreadable
22
- ? `${unreadable} attachment${unreadable === 1 ? ' was' : 's were'} no longer available and dropped.`
23
- : '',
21
+ notice: unreadable ? `${unreadable} attachment${verb} no longer available and dropped.` : '',
24
22
  };
25
23
  }
26
24
 
27
- export function createAbortAction(bag, { acceptingSubmissions }) {
25
+ export function createAbortAction(bag) {
28
26
  const {
29
27
  runtime,
30
28
  flags,
@@ -39,31 +37,12 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
39
37
  discardExecutionPendingResume,
40
38
  } = bag;
41
39
 
42
- // Esc while idle: hand a queued (or still-accepting) submission back to the
43
- // draft instead of interrupting anything.
40
+ // Esc while idle: hand a queued submission back to the draft instead of
41
+ // interrupting anything.
44
42
  const reclaimIdleSubmission = (submissionId) => {
45
43
  if (!submissionId) return false;
46
44
  const restored = restoreQueued('', submissionId);
47
- if (!restored || Number(restored.count) < 1) {
48
- const intake = acceptingSubmissions.get(submissionId);
49
- if (!intake) return false;
50
- // Hydrate before cancelling: nothing is lost if an attachment is gone.
51
- const attachments = hydrateTolerantly(
52
- intake.queueOptions.pastedImages,
53
- intake.queueOptions.pastedTexts,
54
- intake.text
55
- );
56
- intake.cancelled = true;
57
- return {
58
- aborted: false,
59
- restoreText: String(intake.queueOptions.displayText || '').trim(),
60
- pastedImages: attachments.pastedImages,
61
- pastedTexts: attachments.pastedTexts,
62
- ...(attachments.content ? { content: attachments.content } : {}),
63
- ...(attachments.notice ? { notice: attachments.notice } : {}),
64
- restoredSubmissionIds: [submissionId],
65
- };
66
- }
45
+ if (!restored || Number(restored.count) < 1) return false;
67
46
  return {
68
47
  aborted: false,
69
48
  restoreText: restored.text,
@@ -77,10 +56,21 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
77
56
 
78
57
  // Pull the interrupted prompt back out of the transcript and history and
79
58
  // put its unsent requeue entries at the front of the queue.
80
- const reclaimInFlightPrompt = (restoreState, { restoreText, requeueEntries }) => {
59
+ const reclaimInFlightPrompt = (restoreState, { restoreText, requeueEntries, handoff = false }) => {
81
60
  restoreState.reclaimed = true;
82
61
  const idSet = new Set((restoreState.submittedIds || []).filter((id) => id != null));
83
62
  const patch = { spinner: null, thinking: null, lastTurn: null };
63
+ // Another device cancelled this turn: the prompt travels to the device that
64
+ // sent it through the state, never through the canceller's reply.
65
+ if (handoff && restoreText) {
66
+ patch.promptRestore = {
67
+ id: `restore-${Date.now()}-${[...idSet].join(',')}`,
68
+ ids: [...idSet].map(String),
69
+ text: restoreText,
70
+ device: restoreState.device,
71
+ at: Date.now(),
72
+ };
73
+ }
84
74
  if (restoreText) patch.promptHistoryList = promptHistoryWithout(getState().promptHistoryList, restoreText);
85
75
  if (idSet.size > 0) {
86
76
  const items = getState().items.filter((item) => !idSet.has(item?.id));
@@ -127,7 +117,18 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
127
117
  // interrupting should just cancel the running turn and let the steering
128
118
  // prompt run next, NOT resurrect the in-flight prompt back into the draft.
129
119
  const hasPendingSteering = pending.some((entry) => isQueuedEntryEditable(entry));
130
- const canRestore = options?.restorePrompt !== false && restoreState?.restorable && !hasPendingSteering;
120
+ // A different device than the one that sent the prompt is cancelling. Only
121
+ // plain text can be handed over through the state; a prompt carrying
122
+ // attachments stays in the transcript instead of landing in the wrong
123
+ // composer.
124
+ const canceller = typeof options?.device === 'string' ? options.device : '';
125
+ const crossDevice = Boolean(canceller && restoreState?.device && canceller !== restoreState.device);
126
+ const hasAttachments = Boolean(restoreState?.pastedImages || restoreState?.pastedTexts || restoreState?.content);
127
+ const canRestore =
128
+ options?.restorePrompt !== false &&
129
+ restoreState?.restorable &&
130
+ !hasPendingSteering &&
131
+ !(crossDevice && hasAttachments);
131
132
  const restoreText = canRestore ? restoreState.text : '';
132
133
  const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
133
134
  const restorePastedTexts = canRestore && restoreState?.pastedTexts ? restoreState.pastedTexts : null;
@@ -151,7 +152,7 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
151
152
  discardExecutionPendingResume?.(restoreState.discardExecutionPendingResumeKeys);
152
153
  }
153
154
  if ((restoreText || requeueEntries.length > 0) && aborted !== false) {
154
- reclaimInFlightPrompt(restoreState, { restoreText, requeueEntries });
155
+ reclaimInFlightPrompt(restoreState, { restoreText, requeueEntries, handoff: crossDevice });
155
156
  }
156
157
  restoreState.restorable = false;
157
158
  restoreState.requeueEntries = [];
@@ -163,16 +164,19 @@ export function createAbortAction(bag, { acceptingSubmissions }) {
163
164
  restorePastedTexts,
164
165
  canRestore ? restoreState?.content : null
165
166
  );
167
+ // The canceller never receives another device's prompt.
168
+ const handedOver = crossDevice && Boolean(restoreText) && aborted !== false;
166
169
  return {
167
170
  aborted,
168
- restoreText,
171
+ restoreText: handedOver ? '' : restoreText,
169
172
  ...(restored.content ? { content: restored.content } : {}),
170
173
  ...(restored.notice ? { notice: restored.notice } : {}),
171
174
  pastedImages: restored.pastedImages,
172
175
  discardPastedImages,
173
176
  pastedTexts: restored.pastedTexts,
174
177
  discardPastedTexts,
175
- restoredSubmissionIds: restoreText ? (restoreState?.submittedIds || []).map(String).filter(Boolean) : [],
178
+ restoredSubmissionIds:
179
+ restoreText && !handedOver ? (restoreState?.submittedIds || []).map(String).filter(Boolean) : [],
176
180
  };
177
181
  };
178
182
 
@@ -1,14 +1,10 @@
1
- // Prompt submission: mint the intake, decide between queueing now and queueing
2
- // after idle auto-clear, and keep every still-accepting submission addressable
3
- // so Esc can reclaim it before enqueue.
1
+ // Prompt submission: mint the intake and queue it at once, starting idle
2
+ // auto-clear first when the session is idle. Queued prompts are reclaimed by
3
+ // Esc through restoreQueued.
4
4
  import { promptDisplayText } from '../../queue-helpers.mjs';
5
5
 
6
6
  export function createSubmissionIntake(bag) {
7
7
  const { runtime, nextId, flags, getState, enqueue, autoClearBeforeSubmit } = bag;
8
- // submitAsync may be awaiting auto-clear while the renderer already owns an
9
- // optimistic user row. Keep that intake addressable so Esc can reclaim it
10
- // before enqueue()/busy publication without racing a delayed snapshot.
11
- const acceptingSubmissions = new Map();
12
8
 
13
9
  const submission = (text, options = {}) => {
14
10
  const displayText = promptDisplayText(text, options);
@@ -25,20 +21,17 @@ export function createSubmissionIntake(bag) {
25
21
  displayText,
26
22
  },
27
23
  };
28
- acceptingSubmissions.set(intake.queueOptions.id, intake);
29
24
  return intake;
30
25
  };
31
26
 
32
27
  const enqueueSubmission = (intake) => {
33
- const submissionId = String(intake?.queueOptions?.id || '').trim();
34
- if (submissionId) acceptingSubmissions.delete(submissionId);
35
- if (intake?.cancelled === true) return false;
36
28
  const accepted = enqueue(intake.text, intake.queueOptions);
37
29
  // User input wakes a passive `task wait` without cancelling either the
38
30
  // turn or the background task. The returned running snapshot creates the
39
31
  // normal post-tool boundary, where this queued prompt is injected.
40
32
  if (accepted !== false) {
41
33
  if ((intake.queueOptions.mode || 'prompt') === 'prompt') {
34
+ bag.cancelQuotaResumeOnUserInput?.();
42
35
  bag.cancelQueuedGoalContinuations?.();
43
36
  bag.archiveCompletedGoalOnUserInput?.();
44
37
  }
@@ -58,18 +51,17 @@ export function createSubmissionIntake(bag) {
58
51
  if (flags.autoClearRunning || getState().commandBusy || getState().busy) {
59
52
  return enqueueSubmission(intake) !== false;
60
53
  }
61
- // If autoClearBeforeSubmit rejects (e.g. compaction timeout throws), the
62
- // prompt must still be queued — swallow the rejection so enqueue always
63
- // runs and the submit is never silently lost.
64
- // A synchronous throw is the same failure as a rejection.
65
- let autoClear;
54
+ // Start idle auto-clear first so commandBusy is raised synchronously, then
55
+ // queue right away: drain stays blocked until the clear settles, so the
56
+ // prompt still runs against the cleared conversation, and a later submit
57
+ // can never be queued ahead of this one. A rejection or synchronous throw
58
+ // must never lose the prompt.
66
59
  try {
67
- autoClear = Promise.resolve(autoClearBeforeSubmit());
60
+ void Promise.resolve(autoClearBeforeSubmit()).catch(() => {});
68
61
  } catch {
69
- autoClear = Promise.resolve();
62
+ /* the prompt is queued below regardless */
70
63
  }
71
- void autoClear.catch(() => {}).then(() => enqueueSubmission(intake));
72
- return true;
64
+ return enqueueSubmission(intake) !== false;
73
65
  };
74
66
 
75
67
  const submitAsync = async (text, options = {}) => {
@@ -107,5 +99,5 @@ export function createSubmissionIntake(bag) {
107
99
  return await settled;
108
100
  };
109
101
 
110
- return { acceptingSubmissions, submit, submitAsync, submitAndWait };
102
+ return { submit, submitAsync, submitAndWait };
111
103
  }
@@ -2,7 +2,7 @@
2
2
  * intake.mjs — the session object's prompt intake and interruption surface:
3
3
  * state access/subscription and the reserved-session handshake here, with
4
4
  * submit / submitAsync / submitAndWait, abort (which reclaims an in-flight or
5
- * still-accepting submission back into the draft) and message rewind composed
5
+ * queued submission back into the draft) and message rewind composed
6
6
  * from ./intake/.
7
7
  */
8
8
  import { createAbortAction } from './intake/abort.mjs';
@@ -24,7 +24,7 @@ export function createSessionIntakeApi(bag) {
24
24
  restoreQueued,
25
25
  prioritizeQueued,
26
26
  } = bag;
27
- const { acceptingSubmissions, submit, submitAsync, submitAndWait } = createSubmissionIntake(bag);
27
+ const { submit, submitAsync, submitAndWait } = createSubmissionIntake(bag);
28
28
 
29
29
  return {
30
30
  getState: () => getPublishedState(),
@@ -49,6 +49,6 @@ export function createSessionIntakeApi(bag) {
49
49
  restoreQueued,
50
50
  prioritizeQueued,
51
51
  ...createRewindAction(bag),
52
- ...createAbortAction(bag, { acceptingSubmissions }),
52
+ ...createAbortAction(bag),
53
53
  };
54
54
  }