@loicngr/kobo 1.11.6 → 1.11.7

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 (77) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/server/routes/workspaces.js +66 -68
  3. package/dist/server/services/agent/engines/codex/engine.js +42 -7
  4. package/dist/server/services/agent/orchestrator.js +205 -54
  5. package/dist/server/services/agent/session-controller.js +20 -4
  6. package/dist/server/services/settings-service.js +58 -2
  7. package/dist/shared/whip-shortcut.js +41 -0
  8. package/package.json +1 -1
  9. package/src/client/dist/pwa/assets/ActivityFeed-CdjS_Ay5.js +8 -0
  10. package/src/client/dist/pwa/assets/{ChangelogPage-oLAJW1vx.js → ChangelogPage-BTU5fnD6.js} +1 -1
  11. package/src/client/dist/pwa/assets/{ClosePopup-BiMd0EFR.js → ClosePopup-BAuFTRhx.js} +1 -1
  12. package/src/client/dist/pwa/assets/{CreatePage-D7R9aFEL.js → CreatePage-CsEecOVF.js} +2 -2
  13. package/src/client/dist/pwa/assets/{DiffViewer-CS1roZQD.js → DiffViewer-Dq8vqrxA.js} +3 -3
  14. package/src/client/dist/pwa/assets/{HealthPage-cP6NvQlI.js → HealthPage-sbDeyEHP.js} +1 -1
  15. package/src/client/dist/pwa/assets/MainLayout-DFwMZKt9.js +37 -0
  16. package/src/client/dist/pwa/assets/{MainLayout-CYT-Kkia.css → MainLayout-DlS2wxkK.css} +1 -1
  17. package/src/client/dist/pwa/assets/{QBtnToggle-5Ypsr5PI.js → QBtnToggle-dJ_m8WhV.js} +1 -1
  18. package/src/client/dist/pwa/assets/{QChip-CIwjmeox.js → QChip-Bo3J1D2W.js} +1 -1
  19. package/src/client/dist/pwa/assets/QSelect-OaS8rLcb.js +36 -0
  20. package/src/client/dist/pwa/assets/QTooltip-j1MBzbTI.js +1 -0
  21. package/src/client/dist/pwa/assets/{SearchPage-DnUu9LCd.js → SearchPage-BmwtbAot.js} +1 -1
  22. package/src/client/dist/pwa/assets/SettingsPage-COvUlfw5.js +16 -0
  23. package/src/client/dist/pwa/assets/SettingsPage-DyZUZK5m.css +1 -0
  24. package/src/client/dist/pwa/assets/{ThinkingItem-Dsh0S-4R.js → ThinkingItem-6MaIe4ws.js} +1 -1
  25. package/src/client/dist/pwa/assets/{TouchPan-BK8skWbU.js → TouchPan-BIp_9Cq6.js} +1 -1
  26. package/src/client/dist/pwa/assets/{WorkspacePage-BE5DwWlF.css → WorkspacePage-Bgxg_Rua.css} +1 -1
  27. package/src/client/dist/pwa/assets/WorkspacePage-DLtgENoR.js +4 -0
  28. package/src/client/dist/pwa/assets/{build-path-tree-Dt3aRTqV.js → build-path-tree-Du1muMLG.js} +1 -1
  29. package/src/client/dist/pwa/assets/{cssMode-C2-EJrmK.js → cssMode-DJXtqmV0.js} +1 -1
  30. package/src/client/dist/pwa/assets/documents-Br5SnzNo.js +1 -0
  31. package/src/client/dist/pwa/assets/{editor.api2-CkPvAZB5.js → editor.api2-CZ8VVoEU.js} +1 -1
  32. package/src/client/dist/pwa/assets/{editor.main-B-8Qv0oB.js → editor.main-DevrSvQT.js} +2 -2
  33. package/src/client/dist/pwa/assets/{expand-template-Cnj1_kF0.js → expand-template-CL8E3tTa.js} +1 -1
  34. package/src/client/dist/pwa/assets/{formatters-BGHblyDL.js → formatters-BfN5-DOd.js} +1 -1
  35. package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
  36. package/src/client/dist/pwa/assets/{freemarker2-VFyuTCM8.js → freemarker2-C2qa_OD-.js} +1 -1
  37. package/src/client/dist/pwa/assets/{handlebars-g2m5ALjb.js → handlebars-rmnqsHUd.js} +1 -1
  38. package/src/client/dist/pwa/assets/{html-DxnmOMw6.js → html-BGmv8Xd1.js} +1 -1
  39. package/src/client/dist/pwa/assets/{htmlMode-Df0wbbtZ.js → htmlMode-BAm18C8p.js} +1 -1
  40. package/src/client/dist/pwa/assets/i18n-JdLLVp05.js +1 -0
  41. package/src/client/dist/pwa/assets/{index-CXciTur8.js → index-BbjyEh9i.js} +9 -9
  42. package/src/client/dist/pwa/assets/{javascript-Df_-1Pv_.js → javascript-CHZDr7Bk.js} +1 -1
  43. package/src/client/dist/pwa/assets/{jsonMode-BJFi57QF.js → jsonMode-B6gDhGXQ.js} +1 -1
  44. package/src/client/dist/pwa/assets/{kobo-commands-HJVSrhFj.js → kobo-commands-BR2agQLr.js} +1 -1
  45. package/src/client/dist/pwa/assets/{layout-DLVS_2RL.js → layout-D2SIWUB9.js} +1 -1
  46. package/src/client/dist/pwa/assets/{liquid-Dy9wgTn4.js → liquid-oF7cqWzJ.js} +1 -1
  47. package/src/client/dist/pwa/assets/{lspLanguageFeatures-DcaGTZUU.js → lspLanguageFeatures-B8EE1Ucr.js} +1 -1
  48. package/src/client/dist/pwa/assets/{mdx-B1nH83dR.js → mdx-DXdcJzaj.js} +1 -1
  49. package/src/client/dist/pwa/assets/{monaco.contribution--RPgZPOx.js → monaco.contribution-CAMlPG7z.js} +2 -2
  50. package/src/client/dist/pwa/assets/network-auth-BnLlhIx6.js +1 -0
  51. package/src/client/dist/pwa/assets/{pending-diff-open-BS9BktRr.js → pending-diff-open-BMRVHZZL.js} +1 -1
  52. package/src/client/dist/pwa/assets/{permissionModes-ClcSQKmQ.js → permissionModes-CfAFkWP3.js} +1 -1
  53. package/src/client/dist/pwa/assets/{project-color-SXRt-W03.js → project-color-DO7Am583.js} +1 -1
  54. package/src/client/dist/pwa/assets/{python-CJMnM2o7.js → python-DKQcYTdf.js} +1 -1
  55. package/src/client/dist/pwa/assets/{razor-BUhyGYdm.js → razor-2a-_ORi3.js} +1 -1
  56. package/src/client/dist/pwa/assets/{render-chat-markdown-BH1Z10ar.js → render-chat-markdown-ESsukc9f.js} +1 -1
  57. package/src/client/dist/pwa/assets/{tsMode-Cps5WrVq.js → tsMode-DB3IvOW4.js} +1 -1
  58. package/src/client/dist/pwa/assets/{typescript-DHc8Mn2c.js → typescript-Dc5EP7Bs.js} +1 -1
  59. package/src/client/dist/pwa/assets/{use-onboarding-BEN9LPyX.js → use-onboarding-x0pF_4eq.js} +1 -1
  60. package/src/client/dist/pwa/assets/{use-quasar-CgjwhVVN.js → use-quasar-CBDv7_fL.js} +1 -1
  61. package/src/client/dist/pwa/assets/{workers-B_i0s5nK.js → workers-DO53K6md.js} +1 -1
  62. package/src/client/dist/pwa/assets/{xml-B4oQ8zNG.js → xml-D-yJwZeB.js} +1 -1
  63. package/src/client/dist/pwa/assets/{yaml-CUpQdB7b.js → yaml--VhNMxaM.js} +1 -1
  64. package/src/client/dist/pwa/index.html +1 -1
  65. package/src/client/dist/pwa/sounds/mais-laisse-moi-dormir-zebi.mp3 +0 -0
  66. package/src/client/dist/pwa/sounds/yaaa.mp3 +0 -0
  67. package/src/client/dist/pwa/sw.js +1 -1
  68. package/src/client/dist/pwa/assets/ActivityFeed-BM5l2qIA.js +0 -8
  69. package/src/client/dist/pwa/assets/MainLayout-BS27uyox.js +0 -37
  70. package/src/client/dist/pwa/assets/QSelect-BYNOOfHf.js +0 -36
  71. package/src/client/dist/pwa/assets/QTooltip-A4le4l3f.js +0 -1
  72. package/src/client/dist/pwa/assets/SettingsPage-BPplhEXC.js +0 -16
  73. package/src/client/dist/pwa/assets/SettingsPage-Os-6b-6H.css +0 -1
  74. package/src/client/dist/pwa/assets/WorkspacePage-YM_Qujn2.js +0 -4
  75. package/src/client/dist/pwa/assets/documents-BFIwenTv.js +0 -1
  76. package/src/client/dist/pwa/assets/i18n-Cf8eFhZ8.js +0 -1
  77. package/src/client/dist/pwa/assets/network-auth-DYx2-JTO.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 1.11.7
8
+
9
+ - feat: add opt-in interactive whip (#23)
10
+
7
11
  ## 1.11.6
8
12
 
9
13
  - fix(git): continue paused operations
@@ -51,6 +51,14 @@ const VALID_AGENT_PERMISSION_MODES = ['plan', 'bypass', 'strict', 'interactive']
51
51
  function isAgentPermissionMode(value) {
52
52
  return typeof value === 'string' && VALID_AGENT_PERMISSION_MODES.includes(value);
53
53
  }
54
+ async function deliverAgentPrompt(workspace, workingDir, prompt) {
55
+ const delivery = await agentManager.sendMessageForFallback(workspace.id, prompt);
56
+ if (delivery.status === 'sent')
57
+ return { agentSessionId: delivery.sessionId };
58
+ const agent = agentManager.startAgent(workspace.id, workingDir, prompt, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
59
+ workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
60
+ return { agentSessionId: agent.agentSessionId };
61
+ }
54
62
  /**
55
63
  * Resolve the unified permission mode for a new workspace.
56
64
  *
@@ -3138,24 +3146,16 @@ app.post('/:id/git/commit-with-agent', migrationGuard, async (c) => {
3138
3146
  5. Do NOT push the branch.
3139
3147
 
3140
3148
  When finished, report the commit SHA, its message, the files included, and the checks you ran.`;
3141
- const session = workspaceService.getActiveSession(workspace.id);
3142
- wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, session?.id ?? undefined);
3143
3149
  wakeupService.cancel(workspace.id, 'user-message');
3144
3150
  let messageSent = false;
3145
3151
  try {
3146
- agentManager.sendMessage(workspace.id, prompt);
3152
+ const { agentSessionId } = await deliverAgentPrompt(workspace, workspace.worktreePath, prompt);
3147
3153
  messageSent = true;
3154
+ wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, agentSessionId);
3148
3155
  }
3149
- catch {
3150
- try {
3151
- agentManager.startAgent(workspace.id, workspace.worktreePath, prompt, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3152
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3153
- messageSent = true;
3154
- }
3155
- catch (resumeErr) {
3156
- const message = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3157
- return c.json({ error: `Unable to ask the agent to commit: ${message}` }, 409);
3158
- }
3156
+ catch (deliveryErr) {
3157
+ const message = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
3158
+ return c.json({ error: `Unable to ask the agent to commit: ${message}` }, 409);
3159
3159
  }
3160
3160
  return c.json({ ok: true, messageSent });
3161
3161
  }
@@ -3229,27 +3229,18 @@ ${fileList}
3229
3229
  6. Report the summary: which files you touched, the key decisions you made, and the final test result.${cherryPickNote}
3230
3230
 
3231
3231
  Start now.`;
3232
- // Persist the prompt in the chat feed so the user sees what was dispatched.
3233
- const session = workspaceService.getActiveSession(workspace.id);
3234
- wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, session?.id ?? undefined);
3235
3232
  // Cancel any pending wakeup: the user is driving this turn, the
3236
3233
  // scheduler should not also wake the agent a few minutes later.
3237
3234
  wakeupService.cancel(workspace.id, 'user-message');
3238
3235
  let messageSent = false;
3239
3236
  try {
3240
- agentManager.sendMessage(workspace.id, prompt);
3237
+ const { agentSessionId } = await deliverAgentPrompt(workspace, worktreePath, prompt);
3241
3238
  messageSent = true;
3239
+ wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, agentSessionId);
3242
3240
  }
3243
- catch {
3244
- try {
3245
- agentManager.startAgent(workspace.id, worktreePath, prompt, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3246
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3247
- messageSent = true;
3248
- }
3249
- catch (resumeErr) {
3250
- const resumeMsg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3251
- console.warn(`[workspaces] resolve-with-agent: agent resume failed: ${resumeMsg}`);
3252
- }
3241
+ catch (deliveryErr) {
3242
+ const deliveryMessage = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
3243
+ console.warn(`[workspaces] resolve-with-agent: agent resume failed: ${deliveryMessage}`);
3253
3244
  }
3254
3245
  return c.json({ ok: true, operation, files, messageSent });
3255
3246
  }
@@ -3524,29 +3515,18 @@ app.post('/:id/open-pr', async (c) => {
3524
3515
  diffStats,
3525
3516
  tasks,
3526
3517
  });
3527
- // Emit user:message into the chat feed
3528
- const session = workspaceService.getActiveSession(workspace.id);
3529
- wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, session?.id ?? undefined);
3530
3518
  // Cancel any pending wakeup: the user is driving this turn.
3531
3519
  wakeupService.cancel(workspace.id, 'user-message');
3532
3520
  // Send to the running agent, or resume the agent with the PR prompt
3533
3521
  let messageSent = false;
3534
3522
  try {
3535
- agentManager.sendMessage(workspace.id, rendered);
3523
+ const { agentSessionId } = await deliverAgentPrompt(workspace, workspace.worktreePath, rendered);
3536
3524
  messageSent = true;
3525
+ wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, agentSessionId);
3537
3526
  }
3538
- catch {
3539
- // Agent not running — resume it with the PR prompt
3540
- try {
3541
- const worktreePathForResume = workspace.worktreePath;
3542
- agentManager.startAgent(workspace.id, worktreePathForResume, rendered, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3543
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3544
- messageSent = true;
3545
- }
3546
- catch (resumeErr) {
3547
- const resumeMsg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3548
- console.warn(`[workspaces] open-pr: PR created but agent resume failed: ${resumeMsg}`);
3549
- }
3527
+ catch (deliveryErr) {
3528
+ const deliveryMessage = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
3529
+ console.warn(`[workspaces] open-pr: PR created but agent resume failed: ${deliveryMessage}`);
3550
3530
  }
3551
3531
  return c.json({ ok: true, prNumber, prUrl, messageSent });
3552
3532
  }
@@ -3636,20 +3616,13 @@ app.post('/:id/start-review', async (c) => {
3636
3616
  const session = workspaceService.getActiveSession(workspace.id);
3637
3617
  emitSessionId = session?.id;
3638
3618
  try {
3639
- agentManager.sendMessage(workspace.id, rendered);
3619
+ const delivery = await deliverAgentPrompt(workspace, worktreePath, rendered);
3620
+ emitSessionId = delivery.agentSessionId;
3640
3621
  messageSent = true;
3641
3622
  }
3642
- catch {
3643
- try {
3644
- const agent = agentManager.startAgent(workspace.id, worktreePath, rendered, workspace.model, true /* resume */, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3645
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3646
- emitSessionId = agent?.agentSessionId ?? emitSessionId;
3647
- messageSent = true;
3648
- }
3649
- catch (resumeErr) {
3650
- const msg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3651
- return c.json({ error: `Failed to dispatch review prompt: ${msg}` }, 500);
3652
- }
3623
+ catch (deliveryErr) {
3624
+ const message = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
3625
+ return c.json({ error: `Failed to dispatch review prompt: ${message}` }, 500);
3653
3626
  }
3654
3627
  }
3655
3628
  // Emit AFTER dispatch so the user:message lands in the correct session id —
@@ -3713,18 +3686,12 @@ app.post('/:id/start-ci-fix', migrationGuard, async (c) => {
3713
3686
  const session = workspaceService.getActiveSession(workspace.id);
3714
3687
  let emitSessionId = session?.id;
3715
3688
  try {
3716
- agentManager.sendMessage(workspace.id, rendered);
3689
+ const delivery = await deliverAgentPrompt(workspace, workspace.worktreePath, rendered);
3690
+ emitSessionId = delivery.agentSessionId;
3717
3691
  }
3718
- catch {
3719
- try {
3720
- const agent = agentManager.startAgent(workspace.id, workspace.worktreePath, rendered, workspace.model, true /* resume */, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3721
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3722
- emitSessionId = agent?.agentSessionId ?? emitSessionId;
3723
- }
3724
- catch (resumeErr) {
3725
- const msg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3726
- return c.json({ error: `Failed to dispatch CI-fix prompt: ${msg}` }, 500);
3727
- }
3692
+ catch (deliveryErr) {
3693
+ const message = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
3694
+ return c.json({ error: `Failed to dispatch CI-fix prompt: ${message}` }, 500);
3728
3695
  }
3729
3696
  wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, emitSessionId);
3730
3697
  return c.json({ ok: true, failedChecksCount: failedChecks.length });
@@ -3836,18 +3803,49 @@ app.post('/:id/stop', migrationGuard, (c) => {
3836
3803
  }
3837
3804
  });
3838
3805
  // POST /api/workspaces/:id/interrupt — soft-interrupt agent (SIGINT, like Escape in Claude Code)
3839
- app.post('/:id/interrupt', migrationGuard, (c) => {
3806
+ app.post('/:id/interrupt', migrationGuard, async (c) => {
3840
3807
  try {
3841
3808
  const id = c.req.param('id');
3842
3809
  const workspace = workspaceService.getWorkspace(id);
3843
3810
  if (!workspace) {
3844
3811
  return c.json({ error: `Workspace '${id}' not found` }, 404);
3845
3812
  }
3846
- agentManager.interruptAgent(id);
3813
+ const rawBody = await c.req.text();
3814
+ let body = {};
3815
+ if (rawBody.trim().length > 0) {
3816
+ let parsed;
3817
+ try {
3818
+ parsed = JSON.parse(rawBody);
3819
+ }
3820
+ catch {
3821
+ return c.json({ error: 'Request body must be valid JSON' }, 400);
3822
+ }
3823
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
3824
+ return c.json({ error: 'Request body must be a JSON object' }, 400);
3825
+ }
3826
+ body = parsed;
3827
+ }
3828
+ if (body.expectedSessionId !== undefined &&
3829
+ (typeof body.expectedSessionId !== 'string' || body.expectedSessionId.trim().length === 0)) {
3830
+ return c.json({ error: 'expectedSessionId must be a non-empty string when provided' }, 400);
3831
+ }
3832
+ if (body.disableAutoLoop !== undefined && typeof body.disableAutoLoop !== 'boolean') {
3833
+ return c.json({ error: 'disableAutoLoop must be a boolean when provided' }, 400);
3834
+ }
3835
+ const options = {};
3836
+ if (body.expectedSessionId !== undefined)
3837
+ options.expectedSessionId = body.expectedSessionId;
3838
+ if (body.disableAutoLoop !== undefined)
3839
+ options.disableAutoLoop = body.disableAutoLoop;
3840
+ agentManager.interruptAgent(id, options);
3847
3841
  return c.json({ status: 'interrupted' });
3848
3842
  }
3849
3843
  catch (err) {
3850
3844
  const message = err instanceof Error ? err.message : String(err);
3845
+ if (err instanceof agentManager.InterruptAgentError) {
3846
+ const status = err.code === 'interrupt_failed' ? 500 : 409;
3847
+ return c.json({ error: message, code: err.code }, status);
3848
+ }
3851
3849
  return c.json({ error: message }, 500);
3852
3850
  }
3853
3851
  });
@@ -50,6 +50,14 @@ export function createCodexEngine() {
50
50
  let activeTurnId;
51
51
  let steerChain = Promise.resolve();
52
52
  let gracefulInterruptPromise;
53
+ let readySettled = false;
54
+ let resolveReady;
55
+ let rejectReady;
56
+ const readyPromise = new Promise((resolve, reject) => {
57
+ resolveReady = resolve;
58
+ rejectReady = reject;
59
+ });
60
+ void readyPromise.catch(() => { });
53
61
  const emitDirect = (ev) => {
54
62
  try {
55
63
  onEvent(ev);
@@ -60,7 +68,26 @@ export function createCodexEngine() {
60
68
  };
61
69
  const streamingBatcher = createStreamingBatcher(emitDirect);
62
70
  const safeEmit = (ev) => streamingBatcher.push(ev);
71
+ let rejectChildFailure;
72
+ const childFailurePromise = new Promise((_resolve, reject) => {
73
+ rejectChildFailure = reject;
74
+ });
75
+ void childFailurePromise.catch(() => { });
63
76
  const child = spawnAppServer({ cwd: options.workingDir, env: options.env, signal: abortController.signal });
77
+ child.on('error', (error) => {
78
+ const isExpectedAbort = abortController.signal.aborted && (error.code === 'ABORT_ERR' || error.name === 'AbortError');
79
+ if (isExpectedAbort)
80
+ return;
81
+ console.error('[codex] child process error:', error);
82
+ rejectChildFailure(error);
83
+ });
84
+ child.once('exit', (code, signal) => {
85
+ if (!iteratorRunning || abortController.signal.aborted)
86
+ return;
87
+ const detail = code === null ? `signal ${signal ?? 'unknown'}` : `code ${code}`;
88
+ rejectChildFailure(new Error(`Codex app-server exited unexpectedly with ${detail}`));
89
+ });
90
+ const waitForChild = (operation) => Promise.race([operation, childFailurePromise]);
64
91
  if (child.stderr) {
65
92
  child.stderr.setEncoding('utf8');
66
93
  child.stderr.on('data', (chunk) => {
@@ -87,6 +114,7 @@ export function createCodexEngine() {
87
114
  rejectTurnDone(new CodexTurnTimeoutError());
88
115
  },
89
116
  });
117
+ void turnDonePromise.catch(() => { });
90
118
  abortController.signal.addEventListener('abort', () => {
91
119
  const err = new Error('AbortError');
92
120
  err.name = 'AbortError';
@@ -199,9 +227,9 @@ export function createCodexEngine() {
199
227
  const iteratorPromise = (async () => {
200
228
  iteratorRunning = true;
201
229
  try {
202
- await client.connect();
230
+ await waitForChild(client.connect());
203
231
  if (isResume && options.resumeFromEngineSessionId) {
204
- await client.resumeThread({
232
+ await waitForChild(client.resumeThread({
205
233
  threadId: options.resumeFromEngineSessionId,
206
234
  cwd: options.workingDir,
207
235
  persistExtendedHistory: false,
@@ -212,24 +240,26 @@ export function createCodexEngine() {
212
240
  ? { modelReasoningEffort: threadParams.modelReasoningEffort }
213
241
  : {}),
214
242
  ...(threadParams.config != null ? { config: threadParams.config } : {}),
215
- });
243
+ }));
216
244
  }
217
245
  else {
218
- const startResp = await client.startThread(threadParams);
246
+ const startResp = await waitForChild(client.startThread(threadParams));
219
247
  discoveredSessionId = startResp.thread.id;
220
248
  }
221
249
  for (const ev of emitSessionStarted(discoveredSessionId, mapperState))
222
250
  safeEmit(ev);
223
251
  // collaborationMode is sticky server-side — always send it explicitly,
224
252
  // never omit (would leave a Bypass turn stuck in a previous Plan mode).
225
- const initialTurn = await client.startTurn({
253
+ const initialTurn = await waitForChild(client.startTurn({
226
254
  threadId: discoveredSessionId,
227
255
  input,
228
256
  collaborationMode,
229
- });
257
+ }));
230
258
  activeTurnId = initialTurn.turnId;
231
259
  turnLiveness.start();
232
- await turnDonePromise;
260
+ readySettled = true;
261
+ resolveReady();
262
+ await waitForChild(turnDonePromise);
233
263
  turnLiveness.stop();
234
264
  const reason = mapperState.sawErrorResult
235
265
  ? 'error'
@@ -246,6 +276,10 @@ export function createCodexEngine() {
246
276
  turnLiveness.stop();
247
277
  const error = err;
248
278
  const message = error.message ?? String(err);
279
+ if (!readySettled) {
280
+ readySettled = true;
281
+ rejectReady(error);
282
+ }
249
283
  const isAbort = userInterrupted || error.name === 'AbortError' || abortController.signal.aborted;
250
284
  const isResumeAttempt = options.resumeFromEngineSessionId !== undefined;
251
285
  if (isAbort) {
@@ -292,6 +326,7 @@ export function createCodexEngine() {
292
326
  },
293
327
  sendMessage(text) {
294
328
  const steer = async () => {
329
+ await readyPromise;
295
330
  if (!discoveredSessionId || !activeTurnId) {
296
331
  throw new Error('Codex session is not ready to receive a message');
297
332
  }