@wjarka/cezarion 0.14.8 → 0.14.9-dev.829

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 (207) hide show
  1. package/README.md +76 -2
  2. package/dist/application-update/bin-recovery.d.ts +5 -0
  3. package/dist/application-update/bin-recovery.js +96 -0
  4. package/dist/application-update/bin-recovery.js.map +1 -0
  5. package/dist/application-update/discovery.d.ts +22 -0
  6. package/dist/application-update/discovery.js +91 -0
  7. package/dist/application-update/discovery.js.map +1 -0
  8. package/dist/application-update/helper.d.ts +57 -0
  9. package/dist/application-update/helper.js +374 -0
  10. package/dist/application-update/helper.js.map +1 -0
  11. package/dist/application-update/launcher.d.ts +8 -0
  12. package/dist/application-update/launcher.js +53 -0
  13. package/dist/application-update/launcher.js.map +1 -0
  14. package/dist/application-update/lock.d.ts +4 -0
  15. package/dist/application-update/lock.js +70 -0
  16. package/dist/application-update/lock.js.map +1 -0
  17. package/dist/application-update/npm-process.d.ts +17 -0
  18. package/dist/application-update/npm-process.js +133 -0
  19. package/dist/application-update/npm-process.js.map +1 -0
  20. package/dist/application-update/service.d.ts +69 -0
  21. package/dist/application-update/service.js +456 -0
  22. package/dist/application-update/service.js.map +1 -0
  23. package/dist/artifacts/cli.d.ts +1 -0
  24. package/dist/artifacts/cli.js +32 -0
  25. package/dist/artifacts/cli.js.map +1 -0
  26. package/dist/artifacts/lifecycle.d.ts +4 -0
  27. package/dist/artifacts/lifecycle.js +42 -0
  28. package/dist/artifacts/lifecycle.js.map +1 -0
  29. package/dist/artifacts/resolve.d.ts +18 -0
  30. package/dist/artifacts/resolve.js +102 -0
  31. package/dist/artifacts/resolve.js.map +1 -0
  32. package/dist/artifacts/store.d.ts +8 -0
  33. package/dist/artifacts/store.js +323 -0
  34. package/dist/artifacts/store.js.map +1 -0
  35. package/dist/ci-wait/mcp.d.ts +3 -0
  36. package/dist/ci-wait/mcp.js +14 -3
  37. package/dist/ci-wait/mcp.js.map +1 -1
  38. package/dist/contract/application-update.d.ts +32 -0
  39. package/dist/contract/artifacts.d.ts +83 -0
  40. package/dist/contract/ask-schema.d.ts +32 -0
  41. package/dist/contract/ask.d.ts +2 -32
  42. package/dist/contract/conversations.d.ts +165 -0
  43. package/dist/contract/delegation.d.ts +39 -0
  44. package/dist/contract/events.d.ts +49 -0
  45. package/dist/contract/health.d.ts +12 -0
  46. package/dist/contract/index.d.ts +2 -0
  47. package/dist/contract/index.js +1374 -1247
  48. package/dist/contract/runs.d.ts +143 -0
  49. package/dist/contract/skills.d.ts +28 -0
  50. package/dist/core/agent-runner.d.ts +33 -4
  51. package/dist/core/agent-runner.js +5 -0
  52. package/dist/core/agent-runner.js.map +1 -1
  53. package/dist/core/claude-cli-runner.d.ts +6 -12
  54. package/dist/core/claude-cli-runner.js +89 -19
  55. package/dist/core/claude-cli-runner.js.map +1 -1
  56. package/dist/core/codex-app-server-runner.d.ts +2 -1
  57. package/dist/core/codex-app-server-runner.js +327 -92
  58. package/dist/core/codex-app-server-runner.js.map +1 -1
  59. package/dist/core/codex-app-server-transport.d.ts +13 -1
  60. package/dist/core/codex-app-server-transport.js +58 -17
  61. package/dist/core/codex-app-server-transport.js.map +1 -1
  62. package/dist/core/cursor-acp-runner.d.ts +4 -1
  63. package/dist/core/cursor-acp-runner.js +3 -0
  64. package/dist/core/cursor-acp-runner.js.map +1 -1
  65. package/dist/core/cursor-provider-error.js +3 -1
  66. package/dist/core/cursor-provider-error.js.map +1 -1
  67. package/dist/core/input-submissions.d.ts +14 -0
  68. package/dist/core/input-submissions.js +26 -0
  69. package/dist/core/input-submissions.js.map +1 -0
  70. package/dist/core/opencode-server-runner.d.ts +4 -1
  71. package/dist/core/opencode-server-runner.js +144 -21
  72. package/dist/core/opencode-server-runner.js.map +1 -1
  73. package/dist/core/pi-runner.d.ts +2 -1
  74. package/dist/core/pi-runner.js +72 -7
  75. package/dist/core/pi-runner.js.map +1 -1
  76. package/dist/delegation/cli.js +102 -6
  77. package/dist/delegation/cli.js.map +1 -1
  78. package/dist/delegation/conversations.js +13 -5
  79. package/dist/delegation/conversations.js.map +1 -1
  80. package/dist/delegation/input.d.ts +2 -1
  81. package/dist/delegation/input.js +6 -1
  82. package/dist/delegation/input.js.map +1 -1
  83. package/dist/delegation/provision.js +5 -3
  84. package/dist/delegation/provision.js.map +1 -1
  85. package/dist/delegation/questions.d.ts +19 -0
  86. package/dist/delegation/questions.js +45 -0
  87. package/dist/delegation/questions.js.map +1 -0
  88. package/dist/delegation/results.d.ts +1 -1
  89. package/dist/delegation/results.js +23 -13
  90. package/dist/delegation/results.js.map +1 -1
  91. package/dist/delegation/routes.d.ts +142 -15
  92. package/dist/delegation/routes.js +4 -1
  93. package/dist/delegation/routes.js.map +1 -1
  94. package/dist/delegation/service.d.ts +9 -5
  95. package/dist/delegation/service.js +106 -7
  96. package/dist/delegation/service.js.map +1 -1
  97. package/dist/delegation/transport.d.ts +138 -11
  98. package/dist/index.js +72 -26
  99. package/dist/index.js.map +1 -1
  100. package/dist/open-url.d.ts +2 -0
  101. package/dist/open-url.js +20 -0
  102. package/dist/open-url.js.map +1 -0
  103. package/dist/release/snapshot.d.ts +3 -3
  104. package/dist/release/snapshot.js +6 -6
  105. package/dist/release/snapshot.js.map +1 -1
  106. package/dist/runs/client-request.d.ts +11 -0
  107. package/dist/runs/client-request.js +26 -0
  108. package/dist/runs/client-request.js.map +1 -0
  109. package/dist/runs/delegation-state.d.ts +17 -0
  110. package/dist/runs/event-history.js +6 -0
  111. package/dist/runs/event-history.js.map +1 -1
  112. package/dist/runs/store.d.ts +78 -6
  113. package/dist/runs/store.js +163 -1
  114. package/dist/runs/store.js.map +1 -1
  115. package/dist/server/server.d.ts +3761 -1648
  116. package/dist/server/server.js +170 -4
  117. package/dist/server/server.js.map +1 -1
  118. package/dist/task-cli/cli.d.ts +38 -0
  119. package/dist/task-cli/cli.js +463 -0
  120. package/dist/task-cli/cli.js.map +1 -0
  121. package/dist/task-cli/discovery.d.ts +22 -0
  122. package/dist/task-cli/discovery.js +95 -0
  123. package/dist/task-cli/discovery.js.map +1 -0
  124. package/dist/task-cli/http.d.ts +43 -0
  125. package/dist/task-cli/http.js +101 -0
  126. package/dist/task-cli/http.js.map +1 -0
  127. package/dist/task-cli/projections.d.ts +40 -0
  128. package/dist/task-cli/projections.js +45 -0
  129. package/dist/task-cli/projections.js.map +1 -0
  130. package/dist/task-cli/watch.d.ts +78 -0
  131. package/dist/task-cli/watch.js +336 -0
  132. package/dist/task-cli/watch.js.map +1 -0
  133. package/dist/workflows/run.d.ts +93 -0
  134. package/dist/workflows/run.js +816 -81
  135. package/dist/workflows/run.js.map +1 -1
  136. package/dist/workspace/projects-cli.js +1 -1
  137. package/package.json +3 -3
  138. package/scripts/mock-claude.mjs +38 -2
  139. package/scripts/mock-codex-app-server.mjs +98 -1
  140. package/scripts/mock-cursor-acp.mjs +3 -0
  141. package/scripts/mock-opencode-serve.mjs +69 -2
  142. package/scripts/mock-pi-rpc.mjs +63 -2
  143. package/web/dist/assets/agent-accounts-JPfZPgKn.js +1 -0
  144. package/web/dist/assets/centered-state-HwCqQkH3.js +43 -0
  145. package/web/dist/assets/collapsible-CTymFdL7.js +1 -0
  146. package/web/dist/assets/commit-list-BZpYGQl2.js +1 -0
  147. package/web/dist/assets/compare-variants-9VkybaQ7.js +1 -0
  148. package/web/dist/assets/{diff-tRE59UNb2.js → diff-CZcD4D1r2.js} +2 -2
  149. package/web/dist/assets/diff-controls-CpcHG3Mm.js +1 -0
  150. package/web/dist/assets/{diff-stat-Cgnl_hYF.js → diff-stat-Cn7rzy1H.js} +1 -1
  151. package/web/dist/assets/{diff-view-DN1ort7Q.js → diff-view-C46nHiP2.js} +2 -2
  152. package/web/dist/assets/dropdown-menu-_YAzCaoQ.js +1 -0
  153. package/web/dist/assets/git-toolbar-BvKDz6uS.js +1 -0
  154. package/web/dist/assets/github-CgjJCGrD.js +1 -0
  155. package/web/dist/assets/{image-preview-C2JGcBlC.js → image-preview-B618kDBH.js} +1 -1
  156. package/web/dist/assets/index-BJtve0Pm.js +7 -0
  157. package/web/dist/assets/index-D0WAgmTQ.css +1 -0
  158. package/web/dist/assets/markdown-CFWZnvMh.js +2 -0
  159. package/web/dist/assets/pill-QfwkuR50.js +1 -0
  160. package/web/dist/assets/project-router-CkAA39N5.js +1 -0
  161. package/web/dist/assets/prompt-templates-Cfna_S0T.js +15 -0
  162. package/web/dist/assets/repo-git-DgzgJ0cP.js +1 -0
  163. package/web/dist/assets/{run-diff-CyG0Nciv.js → run-diff-YbCEQn5I.js} +2 -2
  164. package/web/dist/assets/run-header-DMgIbr1d.js +1 -0
  165. package/web/dist/assets/skills-BW79kUTd.js +1 -0
  166. package/web/dist/assets/{tab-link-BWNp3mLY.js → tab-link-MIHvczEm.js} +1 -1
  167. package/web/dist/assets/task-changes-C96ekt94.js +1 -0
  168. package/web/dist/assets/task-commits-DBfcMZyG.js +1 -0
  169. package/web/dist/assets/task-files-BKpH8dKQ.js +2 -0
  170. package/web/dist/assets/task-thread-Bw_nQuTx.css +1 -0
  171. package/web/dist/assets/task-thread-Dnq7Ek6W.js +9 -0
  172. package/web/dist/assets/textarea-D2vAVgYW.js +1 -0
  173. package/web/dist/assets/thread-loading-NW8Mthco.js +1 -0
  174. package/web/dist/assets/token-metrics-D188e6_0.js +1 -0
  175. package/web/dist/assets/utils-DKheTxGD.js +64 -0
  176. package/web/dist/assets/workflows-BFY1hgp3.js +11 -0
  177. package/web/dist/assets/{zoomable-image-Z3JDnm0b.js → zoomable-image-YW3Qdrr_.js} +1 -1
  178. package/web/dist/index.html +14 -14
  179. package/web/dist/assets/agent-accounts-BZm_2WAv.js +0 -1
  180. package/web/dist/assets/alert-dialog-B422RI-k.js +0 -43
  181. package/web/dist/assets/collapsible-Cy_Fmkrd.js +0 -1
  182. package/web/dist/assets/commit-list-Dcnk7TnI.js +0 -1
  183. package/web/dist/assets/compare-variants-Ciqwvd_f.js +0 -1
  184. package/web/dist/assets/diff-controls-BGrXGi7O.js +0 -1
  185. package/web/dist/assets/dropdown-menu-dwDGT8MM.js +0 -1
  186. package/web/dist/assets/git-toolbar-UiHnGvE0.js +0 -1
  187. package/web/dist/assets/github-D-MrkbJ7.js +0 -1
  188. package/web/dist/assets/ibm-plex-mono-regular-BF3vfe7_.ttf +0 -0
  189. package/web/dist/assets/index-BszfYvBq.js +0 -7
  190. package/web/dist/assets/index-CWh4pKP3.css +0 -1
  191. package/web/dist/assets/markdown-C_IcKVQd.js +0 -2
  192. package/web/dist/assets/pill-BsbO6G7y.js +0 -1
  193. package/web/dist/assets/project-router-DbLkll25.js +0 -1
  194. package/web/dist/assets/prompt-templates-BlTGJB5W.js +0 -15
  195. package/web/dist/assets/repo-git-Bm3s6Why.js +0 -1
  196. package/web/dist/assets/run-header-IFyIGCey.js +0 -1
  197. package/web/dist/assets/skills-C-C4DVFY.js +0 -1
  198. package/web/dist/assets/task-changes-D2PHtWUW.js +0 -1
  199. package/web/dist/assets/task-commits-N5u9U0_R.js +0 -1
  200. package/web/dist/assets/task-files-BS7c7CFm.js +0 -2
  201. package/web/dist/assets/task-thread-BQFdz1dJ.css +0 -1
  202. package/web/dist/assets/task-thread-DGM2dZV-.js +0 -9
  203. package/web/dist/assets/textarea-FjaU370i.js +0 -1
  204. package/web/dist/assets/thread-loading-77sJ8Yu_.js +0 -1
  205. package/web/dist/assets/token-metrics-Clxh03bE.js +0 -1
  206. package/web/dist/assets/utils-PYYcpCFZ.js +0 -64
  207. package/web/dist/assets/workflows-Czl0ZfXH.js +0 -11
@@ -1,12 +1,20 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { parseEffort } from '../contract/index.js';
2
3
  import { isSignalTerminationExit, prependSystemPrompt, trackChildExit } from './agent-runner.js';
3
4
  import { AUTO_END_DELAY_MS, DEFAULT_RUN_TIMEOUT_MS, KILL_GRACE_MS, } from './runner-runtime.js';
4
5
  import { parseAskRequest } from './ask.js';
5
6
  import { readNdjson } from './ndjson.js';
6
7
  import { V1TextCoalescer } from './v1-text-coalescer.js';
8
+ import { InputSubmissions } from './input-submissions.js';
7
9
  import { codexTurnOutcome } from './codex-turn-outcome.js';
8
- import { CodexAppServerRpc, codexSpawnError, endCodexAppServer, resolveCodexExecutable, spawnCodexAppServer, waitForCodexAppServerExit, } from './codex-app-server-transport.js';
10
+ import { CodexAppServerRpc, CodexRpcResponseError, codexSpawnError, endCodexAppServer, resolveCodexExecutable, spawnCodexAppServer, waitForCodexAppServerExit, } from './codex-app-server-transport.js';
9
11
  import { codexSessionStarted, createCodexUiState, mapCodexNotification, } from './codex-ui-mapper.js';
12
+ // One startup budget, including handshake ACKs and the first main-thread turn.
13
+ // This is not a model-turn or human-question lifetime limit.
14
+ const STARTUP_TIMEOUT_MS = 60_000;
15
+ // After real process exit, allow buffered output to drain before closing pipes
16
+ // a descendant may have inherited. This timer never proves termination.
17
+ const EXIT_DRAIN_MS = 250;
10
18
  /**
11
19
  * `AgentRunner` over `codex app-server` — the same JSONL transport the VS Code
12
20
  * extension and desktop app use (JSON-RPC 2.0, newline-delimited, over
@@ -50,6 +58,12 @@ export const CODEX_SPEC_SUPPORT = {
50
58
  export class CodexAppServerRunner {
51
59
  backend = 'codex';
52
60
  specSupport = CODEX_SPEC_SUPPORT;
61
+ inputDelivery = {
62
+ mode: 'steer', consumption: 'observable',
63
+ // The userMessage item marks the input entering the thread's history (probe 2026-09-24:
64
+ // 0.8 s and 26.7 s after the steer, both before the tool ended); the next model call reads it.
65
+ via: 'turn/steer with clientUserMessageId; item/started userMessage clientId once in the thread history',
66
+ };
53
67
  bin;
54
68
  timeoutMs;
55
69
  lastSession = null;
@@ -82,6 +96,17 @@ class CodexSession {
82
96
  threadId;
83
97
  activeTurnId;
84
98
  agentInputReady = false;
99
+ /** Accepted agent input not yet seen as a consumed userMessage item (#505). */
100
+ submissions = new InputSubmissions();
101
+ inFlightSubmissionId;
102
+ refusedBeforeStartup = false;
103
+ humanPromptsPending = 0;
104
+ refusedForHuman = false;
105
+ /** Submissions that opened a turn via turn/start rather than steering one. */
106
+ turnStartSubmissions = new Set();
107
+ /** Recent main-thread turn outcomes (true = completed without error), for a late turn/start response. */
108
+ completedTurns = new Map();
109
+ turnCompletions = 0;
85
110
  agentSubmissionPending = false;
86
111
  turnBoundaryVersion = 0;
87
112
  pendingUserInput;
@@ -97,6 +122,14 @@ class CodexSession {
97
122
  });
98
123
  tokensUsed = 0;
99
124
  ready;
125
+ startupPhase = 'initialize';
126
+ openingAcknowledged = false;
127
+ firstTurnStarted = false;
128
+ startupComplete = false;
129
+ startupTimer;
130
+ stopKillTimer;
131
+ hardStopStarted = false;
132
+ failure;
100
133
  autoEndTimer;
101
134
  eofTermTimer;
102
135
  eofKillTimer;
@@ -121,98 +154,130 @@ class CodexSession {
121
154
  this.opts = opts;
122
155
  try {
123
156
  this.child = spawnCodexAppServer(bin, spec.cwd, spec.env);
124
- this.rpc = new CodexAppServerRpc(this.child);
157
+ this.rpc = new CodexAppServerRpc(this.child, (error) => {
158
+ if (!this.hardStopStarted && !this.hasExited())
159
+ this.fail(error);
160
+ });
125
161
  }
126
162
  catch (err) {
127
163
  throw codexSpawnError(err, bin);
128
164
  }
129
- this.hasExited = trackChildExit(this.child);
165
+ const hasExited = trackChildExit(this.child);
166
+ this.hasExited = hasExited;
167
+ // Observe termination BEFORE bootstrap. Pending RPCs cannot own the only
168
+ // path to discovering that their process has already gone (#493).
169
+ const exited = waitForCodexAppServerExit(this.child);
170
+ let drainTimer;
130
171
  this.child.on('error', (err) => {
131
- this.spawnFailed = codexSpawnError(err, bin);
172
+ if (this.child.pid === undefined) {
173
+ this.spawnFailed = codexSpawnError(err, bin);
174
+ this.closeInput(this.spawnFailed.message);
175
+ this.child.stdout.destroy();
176
+ }
177
+ else if (this.stdinOpen) {
178
+ this.fail(new Error(`codex app-server process error while waiting for ${this.startupPhase}`));
179
+ }
180
+ });
181
+ this.child.once('exit', (code, signal) => {
182
+ if (this.stdinOpen && !this.startupComplete) {
183
+ this.failure ??= new Error(`Codex startup exited (${signal ?? code ?? 'unknown'}) while waiting for ${this.startupPhase}`);
184
+ }
185
+ // Do not discard final buffered text on a healthy exit. Revoke RPCs
186
+ // immediately, then bound pipe draining ONLY after physical termination.
187
+ this.rpc.close('codex app-server exited');
188
+ drainTimer = setTimeout(() => this.child.stdout.destroy(), EXIT_DRAIN_MS);
189
+ drainTimer.unref?.();
190
+ if (this.stopKillTimer)
191
+ clearTimeout(this.stopKillTimer);
132
192
  });
133
193
  const stderrChunks = [];
134
194
  this.child.stderr.setEncoding('utf8');
135
195
  this.child.stderr.on('data', (chunk) => stderrChunks.push(chunk));
136
- // Optional wall-clock kill switch (disabled for interactive sessions).
196
+ // Whole-session timeouts remain optional; startup is always bounded.
137
197
  const limitMs = spec.timeoutMs ?? timeoutMs;
138
- let killTimer;
139
198
  let deadline;
140
199
  if (limitMs > 0) {
141
200
  deadline = setTimeout(() => {
142
201
  this.timedOut = true;
143
202
  this.interrupt();
144
- this.child.stdout.destroy();
145
- killTimer = setTimeout(() => {
146
- if (!this.hasExited()) {
147
- this.terminatedByCezar = true;
148
- this.child.kill('SIGKILL');
149
- }
150
- }, KILL_GRACE_MS);
151
- killTimer.unref?.();
152
203
  }, limitMs);
153
204
  deadline.unref?.();
154
205
  }
155
- // Handshake → thread → first turn. Kicked off concurrently with the read
156
- // loop below (which resolves the request() promises this awaits).
206
+ this.startupTimer = setTimeout(() => {
207
+ this.fail(new Error(`Codex startup timed out after 60s while waiting for ${this.startupPhase}`), 'timed out after 60s');
208
+ }, STARTUP_TIMEOUT_MS);
209
+ this.startupTimer.unref?.();
157
210
  this.ready = this.bootstrap();
158
- this.result = (async () => {
159
- let sessionError;
211
+ const reader = (async () => {
160
212
  try {
161
- const readLoop = async () => {
162
- for await (const line of readNdjson(this.child.stdout)) {
163
- if (this.timedOut)
164
- break;
165
- let msg;
166
- try {
167
- msg = JSON.parse(line);
168
- }
169
- catch {
170
- continue; // not JSON-RPC — skip
171
- }
172
- // A sub-agent child thread's turn lifecycle must reach neither channel (#600):
173
- // v1 would emit a bogus `turn-end`, and the v2 mapper — which carries no thread
174
- // identity — would record the child turn as the parent's, clearing its turn-scoped
175
- // plan/reasoning state and resetting the current turn id. Child ITEM events still
176
- // flow, so nested sub-agent activity keeps rendering.
177
- if (this.isForeignTurnLifecycle(msg))
178
- continue;
179
- this.emitUi((state) => mapCodexNotification(msg, state));
180
- this.dispatch(msg);
213
+ for await (const line of readNdjson(this.child.stdout)) {
214
+ let msg;
215
+ try {
216
+ msg = JSON.parse(line);
181
217
  }
182
- };
183
- // Start consuming stdout before awaiting bootstrap: JSON-RPC responses
184
- // read here settle the initialize/thread/turn requests. Owning both
185
- // promises makes every bootstrap rejection part of session.result.
186
- await Promise.all([this.ready, readLoop()]);
218
+ catch {
219
+ continue;
220
+ }
221
+ // Explicit shutdown suppresses late lifecycle, not passive output.
222
+ // Natural exit can overtake the final turn frame: retain its outcome
223
+ // without restoring ACK/ask/input authority on the dead connection.
224
+ const finalOutcome = this.stdinOpen && hasExited() &&
225
+ (msg.method === 'turn/completed' || msg.method === 'turn/failed');
226
+ if (!this.open && (msg.id !== undefined || (!PASSIVE_OUTPUT_METHODS.has(msg.method ?? '') && !finalOutcome)))
227
+ continue;
228
+ // Child turn lifecycle must reach neither channel (#600).
229
+ if (this.isForeignTurnLifecycle(msg))
230
+ continue;
231
+ this.emitUi((state) => mapCodexNotification(msg, state));
232
+ this.dispatch(msg);
233
+ }
234
+ if (this.stdinOpen && !hasExited()) {
235
+ if (!this.startupComplete)
236
+ this.fail(new Error(`Codex startup stdout closed while waiting for ${this.startupPhase}`));
237
+ else
238
+ this.end();
239
+ }
187
240
  }
188
- catch (err) {
189
- if (!this.timedOut) {
190
- sessionError = err;
191
- // Bootstrap failed before a usable turn exists. Closing stdin lets
192
- // app-server exit normally; end() also owns the TERM/KILL watchdog
193
- // if a broken child ignores EOF.
194
- this.end();
241
+ catch {
242
+ if (this.stdinOpen && !hasExited()) {
243
+ this.fail(new Error(`codex app-server output failed while waiting for ${this.startupPhase}`));
244
+ }
245
+ }
246
+ })();
247
+ this.result = (async () => {
248
+ let exitCode;
249
+ try {
250
+ try {
251
+ await Promise.all([this.ready, reader]);
195
252
  }
253
+ catch (err) {
254
+ if (this.stdinOpen && !hasExited())
255
+ this.fail(err instanceof Error ? err : new Error(String(err)));
256
+ }
257
+ // RPC cancellation is not a receipt for process termination. Keep the
258
+ // TERM→KILL watchdog alive until this independently owned promise settles.
259
+ exitCode = await exited;
260
+ await reader;
196
261
  }
197
262
  finally {
198
263
  if (deadline)
199
264
  clearTimeout(deadline);
200
- if (killTimer)
201
- clearTimeout(killTimer);
202
- if (this.autoEndTimer)
203
- clearTimeout(this.autoEndTimer);
204
- this.stdinOpen = false;
265
+ if (drainTimer)
266
+ clearTimeout(drainTimer);
267
+ if (this.stopKillTimer)
268
+ clearTimeout(this.stopKillTimer);
269
+ if (this.eofTermTimer)
270
+ clearTimeout(this.eofTermTimer);
271
+ if (this.eofKillTimer)
272
+ clearTimeout(this.eofKillTimer);
273
+ this.closeInput('codex app-server exited');
274
+ this.child.stdin.destroy();
275
+ this.child.stderr.destroy();
205
276
  }
206
- const exitCode = await waitForCodexAppServerExit(this.child);
207
- if (this.eofTermTimer)
208
- clearTimeout(this.eofTermTimer);
209
- if (this.eofKillTimer)
210
- clearTimeout(this.eofKillTimer);
211
- this.rpc.rejectPending();
212
277
  if (this.spawnFailed)
213
278
  throw this.spawnFailed;
214
- if (sessionError)
215
- throw sessionError;
279
+ if (this.failure)
280
+ throw this.failure;
216
281
  // Timeout/interrupt can end the read loop mid-item — recover buffered prose.
217
282
  this.textCoalescer.flush();
218
283
  const text = this.textChunks.join('\n').trim();
@@ -250,26 +315,45 @@ class CodexSession {
250
315
  })();
251
316
  }
252
317
  get open() {
253
- return this.stdinOpen;
318
+ return this.stdinOpen && this.rpc.open && !this.hasExited();
254
319
  }
255
320
  get pid() {
256
321
  return this.child.pid;
257
322
  }
258
- sendAgentMessage(content) {
259
- if (!this.stdinOpen || !this.agentInputReady || this.pendingUserInput || this.agentSubmissionPending)
323
+ sendAgentMessage(content, inputIds = []) {
324
+ // #505: a running turn is steered with turn/steer; only an unanswered native ask
325
+ // or an in-flight submission refuses.
326
+ // Until the thread and its first turn exist there is nothing to steer or start;
327
+ // a refusal here earns one readiness hint when startup completes.
328
+ if (this.open && !this.startupComplete) {
329
+ this.refusedBeforeStartup = true;
330
+ return false;
331
+ }
332
+ // A human prompt still on its way to the wire goes first (#505 local review).
333
+ if (this.open && this.humanPromptsPending > 0) {
334
+ this.refusedForHuman = true;
335
+ return false;
336
+ }
337
+ if (!this.open || this.pendingUserInput || this.agentSubmissionPending)
260
338
  return false;
261
339
  this.agentInputReady = false;
262
340
  this.agentSubmissionPending = true;
263
341
  if (this.autoEndTimer)
264
342
  clearTimeout(this.autoEndTimer);
265
343
  this.autoEndTimer = undefined;
344
+ const submissionId = randomUUID();
345
+ this.submissions.accept(submissionId, inputIds, '');
346
+ this.inFlightSubmissionId = submissionId;
266
347
  // Reserve synchronously; only the matching RPC result acknowledges delivery.
267
- return this.startOrSteerTurn(textOf(content)).catch((err) => {
348
+ return this.startOrSteerTurn(textOf(content), submissionId).catch((err) => {
349
+ this.submissions.consume(submissionId); // never accepted: the caller keeps ownership
268
350
  if (this.stdinOpen)
269
351
  this.emit({ type: 'error', message: `codex: agent input failed: ${String(err)}` });
270
352
  throw err;
271
353
  }).finally(() => {
272
354
  this.agentSubmissionPending = false;
355
+ if (this.inFlightSubmissionId === submissionId)
356
+ this.inFlightSubmissionId = undefined;
273
357
  if (this.stdinOpen && this.agentInputReady && !this.pendingUserInput) {
274
358
  this.opts.onAgentInputReady?.();
275
359
  this.scheduleAutoEnd();
@@ -277,7 +361,7 @@ class CodexSession {
277
361
  });
278
362
  }
279
363
  scheduleAutoEnd() {
280
- if (!this.opts.autoEndAfterFirstTurn || !this.stdinOpen || this.autoEndTimer || this.agentSubmissionPending)
364
+ if (!this.opts.autoEndAfterFirstTurn || !this.open || this.autoEndTimer || this.agentSubmissionPending)
281
365
  return;
282
366
  this.autoEndTimer = setTimeout(() => {
283
367
  this.autoEndTimer = undefined;
@@ -288,7 +372,7 @@ class CodexSession {
288
372
  }
289
373
  sendMessage(content) {
290
374
  this.agentInputReady = false;
291
- if (!this.stdinOpen)
375
+ if (!this.open)
292
376
  return false;
293
377
  if (this.autoEndTimer) {
294
378
  clearTimeout(this.autoEndTimer);
@@ -301,14 +385,26 @@ class CodexSession {
301
385
  const pending = this.pendingUserInput;
302
386
  this.pendingUserInput = undefined;
303
387
  this.rpc.respond({ id: pending.rpcId, result: { answers: userInputAnswers(pending.questions, text) } });
304
- return true;
388
+ return this.open;
305
389
  }
306
- // Wait for the thread to exist, then steer the live turn or start a new one.
390
+ // Wait for the thread to exist, then steer the live turn or start a new one. Agent input
391
+ // waits until this prompt's request settles, so it can never overtake it.
392
+ this.humanPromptsPending += 1;
307
393
  void this.ready
308
394
  .then(() => this.startOrSteerTurn(text))
309
395
  .catch((err) => {
310
- const message = err instanceof Error ? err.message : String(err);
311
- this.emit({ type: 'note', message: `codex: turn failed: ${message}` });
396
+ if (this.stdinOpen) {
397
+ const message = err instanceof Error ? err.message : String(err);
398
+ this.emit({ type: 'note', message: `codex: turn failed: ${message}` });
399
+ }
400
+ })
401
+ .finally(() => {
402
+ this.humanPromptsPending -= 1;
403
+ if (this.humanPromptsPending === 0 && this.refusedForHuman) {
404
+ this.refusedForHuman = false;
405
+ if (this.open && !this.pendingUserInput && !this.agentSubmissionPending)
406
+ this.opts.onAgentInputReady?.();
407
+ }
312
408
  });
313
409
  return true;
314
410
  }
@@ -316,8 +412,7 @@ class CodexSession {
316
412
  end() {
317
413
  if (!this.stdinOpen)
318
414
  return;
319
- this.rejectPendingUserInput('session ended');
320
- this.stdinOpen = false;
415
+ this.closeInput('session ended');
321
416
  try {
322
417
  endCodexAppServer(this.child, (term, kill) => {
323
418
  this.eofTermTimer = term;
@@ -331,20 +426,79 @@ class CodexSession {
331
426
  }
332
427
  }
333
428
  interrupt() {
334
- this.stdinOpen = false;
335
- this.rejectPendingUserInput('turn interrupted');
336
- // Best-effort graceful cancel of the in-flight turn, then hard stop.
337
- if (this.threadId && this.activeTurnId) {
429
+ if (this.hardStopStarted || this.hasExited())
430
+ return;
431
+ this.hardStopStarted = true;
432
+ if (this.eofTermTimer)
433
+ clearTimeout(this.eofTermTimer);
434
+ if (this.eofKillTimer)
435
+ clearTimeout(this.eofKillTimer);
436
+ // Native cancellation is best effort; never wait for its ACK to stop.
437
+ if (this.stdinOpen && this.threadId && this.activeTurnId) {
338
438
  void this.rpc.request('turn/interrupt', { threadId: this.threadId, turnId: this.activeTurnId }).catch(() => undefined);
339
439
  }
340
- if (!this.hasExited()) {
341
- this.terminatedByCezar = true;
342
- this.child.kill('SIGTERM');
440
+ this.closeInput('codex session stopped');
441
+ this.terminatedByCezar = true;
442
+ this.child.kill('SIGTERM');
443
+ this.stopKillTimer = setTimeout(() => {
444
+ if (!this.hasExited())
445
+ this.child.kill('SIGKILL');
446
+ }, KILL_GRACE_MS);
447
+ this.stopKillTimer.unref?.();
448
+ }
449
+ closeInput(reason) {
450
+ this.stdinOpen = false;
451
+ this.rejectPendingUserInput(reason);
452
+ this.agentInputReady = false;
453
+ if (this.startupTimer)
454
+ clearTimeout(this.startupTimer);
455
+ if (this.autoEndTimer)
456
+ clearTimeout(this.autoEndTimer);
457
+ this.rpc.close(reason);
458
+ }
459
+ fail(error, diagnostic = 'failed') {
460
+ if (!this.stdinOpen)
461
+ return;
462
+ this.failure ??= error;
463
+ // Stage-only diagnostics never include prompt/config/env or raw stderr.
464
+ const message = this.startupComplete ? 'Codex app-server failed; closing app-server'
465
+ : `Codex startup ${diagnostic} while waiting for ${this.startupPhase}; closing app-server`;
466
+ try {
467
+ this.emit({ type: 'note', message });
343
468
  }
469
+ catch { /* A failing diagnostic sink cannot release a still-live process. */ }
470
+ this.interrupt();
471
+ }
472
+ assertOpen() {
473
+ if (!this.open)
474
+ throw new Error('codex session closed');
475
+ }
476
+ startupWaitingFor(phase) {
477
+ this.assertOpen();
478
+ this.startupPhase = phase;
479
+ this.emit({ type: 'note', message: `Codex startup: waiting for ${phase}` });
480
+ }
481
+ checkStartupComplete() {
482
+ if (!this.openingAcknowledged || !this.firstTurnStarted || !this.stdinOpen)
483
+ return;
484
+ if (this.startupComplete)
485
+ return;
486
+ this.startupComplete = true;
487
+ if (this.startupTimer)
488
+ clearTimeout(this.startupTimer);
489
+ // Input refused during startup can now steer the opening turn (#505).
490
+ if (this.refusedBeforeStartup)
491
+ queueMicrotask(() => {
492
+ this.refusedBeforeStartup = false;
493
+ if (this.open && !this.pendingUserInput && !this.agentSubmissionPending)
494
+ this.opts.onAgentInputReady?.();
495
+ });
344
496
  }
345
497
  // ---- protocol -----------------------------------------------------------
346
498
  async bootstrap() {
499
+ this.startupWaitingFor('initialize');
347
500
  await this.rpc.initialize();
501
+ this.assertOpen();
348
502
  const overrides = {
349
503
  model: this.spec.model,
350
504
  cwd: this.spec.cwd,
@@ -360,11 +514,15 @@ class CodexSession {
360
514
  } } : {}),
361
515
  };
362
516
  if (this.spec.resume && this.spec.sessionId) {
517
+ this.startupWaitingFor('thread/resume');
363
518
  await this.rpc.request('thread/resume', { threadId: this.spec.sessionId, ...clean(overrides) });
519
+ this.assertOpen();
364
520
  this.threadId = this.spec.sessionId;
365
521
  }
366
522
  else {
523
+ this.startupWaitingFor('thread/start');
367
524
  const res = await this.rpc.request('thread/start', clean(overrides));
525
+ this.assertOpen();
368
526
  this.threadId = threadIdOf(res) ?? this.spec.sessionId;
369
527
  }
370
528
  if (this.threadId) {
@@ -378,31 +536,73 @@ class CodexSession {
378
536
  // has no dedicated app-server field, so it rides along as a leading block
379
537
  // of the opening message.
380
538
  const first = prependSystemPrompt(this.spec.systemPrompt, this.spec.userPrompt);
539
+ this.startupWaitingFor('turn/start');
381
540
  await this.startOrSteerTurn(first);
541
+ this.assertOpen();
542
+ this.openingAcknowledged = true;
543
+ this.checkStartupComplete();
544
+ if (!this.startupComplete)
545
+ this.startupWaitingFor('first main-thread turn');
382
546
  }
383
- async startOrSteerTurn(text) {
547
+ async startOrSteerTurn(text, clientUserMessageId) {
548
+ this.assertOpen();
384
549
  if (!this.threadId)
385
- return;
550
+ throw new Error('codex app-server did not return a thread id');
386
551
  this.agentInputReady = false;
387
552
  const input = [{ type: 'text', text, text_elements: [] }];
553
+ // Echoed as the consumed userMessage item's `clientId` (#505).
554
+ const ids = clientUserMessageId ? { clientUserMessageId } : {};
388
555
  if (this.activeTurnId) {
389
- await this.rpc.request('turn/steer', {
390
- threadId: this.threadId,
391
- input,
392
- expectedTurnId: this.activeTurnId,
393
- });
394
- return;
556
+ const boundary = this.turnBoundaryVersion;
557
+ try {
558
+ await this.rpc.request('turn/steer', {
559
+ threadId: this.threadId,
560
+ input,
561
+ expectedTurnId: this.activeTurnId,
562
+ ...ids,
563
+ });
564
+ // Accepted, but the turn completed before the model read it: nothing will read
565
+ // it now, so start a turn with it under the same client id (#505).
566
+ const stranded = clientUserMessageId !== undefined && this.turnBoundaryVersion !== boundary &&
567
+ !this.activeTurnId && this.submissions.has(clientUserMessageId);
568
+ if (!stranded)
569
+ return;
570
+ }
571
+ catch (err) {
572
+ // Only a definitive refusal falls back: the server answered with an error AND
573
+ // the turn ended meanwhile. A timeout or closed transport is ambiguous — the
574
+ // steer may have landed — so it rejects without a retry (#505).
575
+ if (!(err instanceof CodexRpcResponseError) || this.turnBoundaryVersion === boundary || this.activeTurnId)
576
+ throw err;
577
+ }
395
578
  }
396
579
  // Ask the app-server for reasoning summaries; without this the model runs
397
580
  // with its default (no summary), so the reasoning thread stays empty even
398
581
  // though the mapper and UI can render it. The override persists for this
399
582
  // turn and every subsequent turn, so seeding it on turn/start is enough.
400
583
  const boundaryVersion = this.turnBoundaryVersion;
584
+ const completionsBefore = this.turnCompletions;
401
585
  const res = await this.rpc.request('turn/start', {
402
586
  threadId: this.threadId,
403
587
  input,
588
+ ...ids,
404
589
  ...codexTurnStartExtras(this.spec),
405
590
  });
591
+ if (clientUserMessageId) {
592
+ // This turn's own input: its completion proves the model processed it (#505). A response
593
+ // can arrive after that turn already completed; settle it from the recorded outcome.
594
+ const startedTurn = turnIdOf(res);
595
+ const outcome = startedTurn && this.turnCompletions > completionsBefore ? this.completedTurns.get(startedTurn) : undefined;
596
+ if (outcome === undefined)
597
+ this.turnStartSubmissions.add(clientUserMessageId);
598
+ else {
599
+ const settled = this.submissions.consume(clientUserMessageId);
600
+ if (settled.length && outcome)
601
+ this.opts.onAgentInputConsumed?.(settled);
602
+ else if (settled.length)
603
+ this.emit({ type: 'input-unconsumed', inputIds: settled });
604
+ }
605
+ }
406
606
  if (this.turnBoundaryVersion === boundaryVersion)
407
607
  this.activeTurnId = turnIdOf(res) ?? this.activeTurnId;
408
608
  }
@@ -459,6 +659,10 @@ class CodexSession {
459
659
  this.turnBoundaryVersion += 1;
460
660
  if (this.isForeignThreadTurn(params))
461
661
  break; // sub-agent child thread — not our turn (#600)
662
+ if (this.threadId && turnIdOf(params)) {
663
+ this.firstTurnStarted = true;
664
+ this.checkStartupComplete();
665
+ }
462
666
  this.activeTurnId = turnIdOf(params) ?? this.activeTurnId;
463
667
  break;
464
668
  }
@@ -475,6 +679,13 @@ class CodexSession {
475
679
  case 'item/started': {
476
680
  const item = params.item ?? {};
477
681
  const type = stringField(item, 'type');
682
+ if (type === 'userMessage' && !this.isForeignThreadTurn(params)) {
683
+ // The model received this input now; `clientId` names our submission (#505).
684
+ const clientId = stringField(item, 'clientId');
685
+ const ids = clientId ? this.submissions.consume(clientId) : [];
686
+ if (ids.length)
687
+ this.opts.onAgentInputConsumed?.(ids);
688
+ }
478
689
  // Only tool-like items become tool events; message/reasoning stream as text.
479
690
  if (type && !NON_TOOL_ITEMS.has(type)) {
480
691
  const id = stringField(item, 'id') ?? `item-${this.rpc.allocateId()}`;
@@ -523,11 +734,29 @@ class CodexSession {
523
734
  // partial prose before the turn boundary (run.ts reads markers there).
524
735
  this.textCoalescer.flush();
525
736
  const outcome = codexTurnOutcome(method, params);
737
+ const completedId = turnIdOf(params);
738
+ if (completedId) {
739
+ this.completedTurns.set(completedId, outcome.error === undefined);
740
+ if (this.completedTurns.size > 32)
741
+ this.completedTurns.delete(this.completedTurns.keys().next().value);
742
+ }
743
+ this.turnCompletions += 1;
526
744
  if (outcome.error !== undefined && !this.terminatedByCezar) {
527
745
  this.emit({ type: 'error', message: outcome.error });
528
746
  }
529
- this.agentInputReady = true;
530
- this.emit({ type: 'turn-end' });
747
+ this.agentInputReady = this.open;
748
+ // A completed turn processed its own input; a failed one may never have reached the
749
+ // model, so its input stays pending and is reported unconsumed below.
750
+ const started = outcome.error === undefined ? [...this.turnStartSubmissions].flatMap(id => this.submissions.consume(id)) : [];
751
+ this.turnStartSubmissions.clear();
752
+ if (started.length)
753
+ this.opts.onAgentInputConsumed?.(started);
754
+ // An in-flight steer is owned by its RPC outcome, not by this boundary.
755
+ const inFlight = this.inFlightSubmissionId ? this.submissions.consume(this.inFlightSubmissionId) : [];
756
+ const unconsumedInputIds = this.submissions.takeUnconsumed();
757
+ if (this.inFlightSubmissionId && inFlight.length)
758
+ this.submissions.accept(this.inFlightSubmissionId, inFlight, '');
759
+ this.emit(unconsumedInputIds.length ? { type: 'turn-end', unconsumedInputIds } : { type: 'turn-end' });
531
760
  this.scheduleAutoEnd();
532
761
  break;
533
762
  }
@@ -601,6 +830,12 @@ const NON_TOOL_ITEMS = new Set(['agentMessage', 'userMessage', 'reasoning', 'pla
601
830
  /** Turn-lifecycle notification methods — the only frames whose child-thread copies must be
602
831
  * dropped so a sub-agent turn can't be mistaken for the parent's (#600). */
603
832
  const TURN_LIFECYCLE_METHODS = new Set(['turn/started', 'turn/completed', 'turn/failed']);
833
+ /** Notifications that only collect output; never dispatch control after close. */
834
+ const PASSIVE_OUTPUT_METHODS = new Set([
835
+ 'item/started', 'item/updated', 'item/completed', 'item/agentMessage/delta',
836
+ 'item/reasoning/textDelta', 'item/reasoning/summaryDelta', 'item/reasoning/summaryTextDelta',
837
+ 'item/commandExecution/outputDelta', 'thread/tokenUsage/updated', 'turn/plan/updated',
838
+ ]);
604
839
  const REASONING_SUMMARIES = new Set(['auto', 'concise', 'detailed', 'none']);
605
840
  /**
606
841
  * The reasoning-summary override sent on `turn/start` (TurnStartParams.summary).