@loicngr/kobo 1.11.5 → 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 (79) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/server/routes/workspaces.js +83 -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/server/utils/git-ops.js +14 -0
  8. package/dist/shared/whip-shortcut.js +41 -0
  9. package/package.json +1 -1
  10. package/src/client/dist/pwa/assets/ActivityFeed-CdjS_Ay5.js +8 -0
  11. package/src/client/dist/pwa/assets/{ChangelogPage-BaSsYQ5d.js → ChangelogPage-BTU5fnD6.js} +1 -1
  12. package/src/client/dist/pwa/assets/{ClosePopup-C7qMeVHU.js → ClosePopup-BAuFTRhx.js} +1 -1
  13. package/src/client/dist/pwa/assets/{CreatePage-BYYyZ_Ff.js → CreatePage-CsEecOVF.js} +2 -2
  14. package/src/client/dist/pwa/assets/{DiffViewer-CCtHwGu6.js → DiffViewer-Dq8vqrxA.js} +3 -3
  15. package/src/client/dist/pwa/assets/{HealthPage-DIMw_XqU.js → HealthPage-sbDeyEHP.js} +1 -1
  16. package/src/client/dist/pwa/assets/MainLayout-DFwMZKt9.js +37 -0
  17. package/src/client/dist/pwa/assets/{MainLayout-BNYFr-Y6.css → MainLayout-DlS2wxkK.css} +1 -1
  18. package/src/client/dist/pwa/assets/{QBtnToggle-Di2dp1Lt.js → QBtnToggle-dJ_m8WhV.js} +1 -1
  19. package/src/client/dist/pwa/assets/{QChip-Y5_nvR_g.js → QChip-Bo3J1D2W.js} +1 -1
  20. package/src/client/dist/pwa/assets/QSelect-OaS8rLcb.js +36 -0
  21. package/src/client/dist/pwa/assets/QTooltip-j1MBzbTI.js +1 -0
  22. package/src/client/dist/pwa/assets/{SearchPage-DxfA-RpF.js → SearchPage-BmwtbAot.js} +1 -1
  23. package/src/client/dist/pwa/assets/SettingsPage-COvUlfw5.js +16 -0
  24. package/src/client/dist/pwa/assets/SettingsPage-DyZUZK5m.css +1 -0
  25. package/src/client/dist/pwa/assets/{ThinkingItem-CcLcHo1b.js → ThinkingItem-6MaIe4ws.js} +1 -1
  26. package/src/client/dist/pwa/assets/{TouchPan-uoWtAAB4.js → TouchPan-BIp_9Cq6.js} +1 -1
  27. package/src/client/dist/pwa/assets/{WorkspacePage-BE5DwWlF.css → WorkspacePage-Bgxg_Rua.css} +1 -1
  28. package/src/client/dist/pwa/assets/WorkspacePage-DLtgENoR.js +4 -0
  29. package/src/client/dist/pwa/assets/{build-path-tree-D1fWaedJ.js → build-path-tree-Du1muMLG.js} +1 -1
  30. package/src/client/dist/pwa/assets/{cssMode-BLTlRoqI.js → cssMode-DJXtqmV0.js} +1 -1
  31. package/src/client/dist/pwa/assets/documents-Br5SnzNo.js +1 -0
  32. package/src/client/dist/pwa/assets/{editor.api2-C1zql2a3.js → editor.api2-CZ8VVoEU.js} +1 -1
  33. package/src/client/dist/pwa/assets/{editor.main-BM_AJ8GC.js → editor.main-DevrSvQT.js} +2 -2
  34. package/src/client/dist/pwa/assets/{expand-template-CdOnyk7u.js → expand-template-CL8E3tTa.js} +1 -1
  35. package/src/client/dist/pwa/assets/{formatters-D_RkuRmV.js → formatters-BfN5-DOd.js} +1 -1
  36. package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
  37. package/src/client/dist/pwa/assets/{freemarker2-B2Am5RCx.js → freemarker2-C2qa_OD-.js} +1 -1
  38. package/src/client/dist/pwa/assets/{handlebars-jtMsrPGt.js → handlebars-rmnqsHUd.js} +1 -1
  39. package/src/client/dist/pwa/assets/{html-XkaC_Vpq.js → html-BGmv8Xd1.js} +1 -1
  40. package/src/client/dist/pwa/assets/{htmlMode-BUuUfuL6.js → htmlMode-BAm18C8p.js} +1 -1
  41. package/src/client/dist/pwa/assets/i18n-JdLLVp05.js +1 -0
  42. package/src/client/dist/pwa/assets/index-BbjyEh9i.js +84 -0
  43. package/src/client/dist/pwa/assets/{javascript-DFkvrBQi.js → javascript-CHZDr7Bk.js} +1 -1
  44. package/src/client/dist/pwa/assets/{jsonMode-DG-AnaCb.js → jsonMode-B6gDhGXQ.js} +1 -1
  45. package/src/client/dist/pwa/assets/{kobo-commands-lJHYm--K.js → kobo-commands-BR2agQLr.js} +1 -1
  46. package/src/client/dist/pwa/assets/{layout-WGOsD1fA.js → layout-D2SIWUB9.js} +1 -1
  47. package/src/client/dist/pwa/assets/{liquid-Caxw51Te.js → liquid-oF7cqWzJ.js} +1 -1
  48. package/src/client/dist/pwa/assets/{lspLanguageFeatures-DJQ1N4bv.js → lspLanguageFeatures-B8EE1Ucr.js} +1 -1
  49. package/src/client/dist/pwa/assets/{mdx-D0tPH0_M.js → mdx-DXdcJzaj.js} +1 -1
  50. package/src/client/dist/pwa/assets/{monaco.contribution-D2e4yDW_.js → monaco.contribution-CAMlPG7z.js} +2 -2
  51. package/src/client/dist/pwa/assets/network-auth-BnLlhIx6.js +1 -0
  52. package/src/client/dist/pwa/assets/{pending-diff-open-DmfQOYj8.js → pending-diff-open-BMRVHZZL.js} +1 -1
  53. package/src/client/dist/pwa/assets/{permissionModes-ByPJHncQ.js → permissionModes-CfAFkWP3.js} +1 -1
  54. package/src/client/dist/pwa/assets/{project-color-z6Lk4hj8.js → project-color-DO7Am583.js} +1 -1
  55. package/src/client/dist/pwa/assets/{python-DN4jsz0q.js → python-DKQcYTdf.js} +1 -1
  56. package/src/client/dist/pwa/assets/{razor-CmVIxbMc.js → razor-2a-_ORi3.js} +1 -1
  57. package/src/client/dist/pwa/assets/{render-chat-markdown-DOgKdkvV.js → render-chat-markdown-ESsukc9f.js} +1 -1
  58. package/src/client/dist/pwa/assets/{tsMode-O2eMFtPN.js → tsMode-DB3IvOW4.js} +1 -1
  59. package/src/client/dist/pwa/assets/{typescript-r7SGGrpF.js → typescript-Dc5EP7Bs.js} +1 -1
  60. package/src/client/dist/pwa/assets/{use-onboarding-5eX5X0EJ.js → use-onboarding-x0pF_4eq.js} +1 -1
  61. package/src/client/dist/pwa/assets/{use-quasar-gNcVRGYS.js → use-quasar-CBDv7_fL.js} +1 -1
  62. package/src/client/dist/pwa/assets/{workers-DZ00UUkn.js → workers-DO53K6md.js} +1 -1
  63. package/src/client/dist/pwa/assets/{xml-CALFQGQ2.js → xml-D-yJwZeB.js} +1 -1
  64. package/src/client/dist/pwa/assets/{yaml-DNnrEGwW.js → yaml--VhNMxaM.js} +1 -1
  65. package/src/client/dist/pwa/index.html +1 -1
  66. package/src/client/dist/pwa/sounds/mais-laisse-moi-dormir-zebi.mp3 +0 -0
  67. package/src/client/dist/pwa/sounds/yaaa.mp3 +0 -0
  68. package/src/client/dist/pwa/sw.js +1 -1
  69. package/src/client/dist/pwa/assets/ActivityFeed-DB4rs2R_.js +0 -8
  70. package/src/client/dist/pwa/assets/MainLayout-DUcc_q9G.js +0 -37
  71. package/src/client/dist/pwa/assets/QSelect-q-_4A_Q0.js +0 -36
  72. package/src/client/dist/pwa/assets/QTooltip-Dl9hEvDw.js +0 -1
  73. package/src/client/dist/pwa/assets/SettingsPage-BAFLf2LW.js +0 -16
  74. package/src/client/dist/pwa/assets/SettingsPage-Os-6b-6H.css +0 -1
  75. package/src/client/dist/pwa/assets/WorkspacePage-DEj_FbHT.js +0 -4
  76. package/src/client/dist/pwa/assets/documents-BjIvI6HK.js +0 -1
  77. package/src/client/dist/pwa/assets/i18n-B9ComK7B.js +0 -1
  78. package/src/client/dist/pwa/assets/index-D741YDZP.js +0 -84
  79. package/src/client/dist/pwa/assets/network-auth-DuE1Pd1j.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ 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
+
11
+ ## 1.11.6
12
+
13
+ - fix(git): continue paused operations
14
+
7
15
  ## 1.11.5
8
16
 
9
17
  - fix(pwa): stabilize notifications and chat layout
@@ -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
  *
@@ -3082,6 +3090,23 @@ app.post('/:id/git/abort', (c) => {
3082
3090
  return c.json({ error: message }, 500);
3083
3091
  }
3084
3092
  });
3093
+ /** Continue an in-progress merge, rebase or cherry-pick after resolution. */
3094
+ app.post('/:id/git/continue', (c) => {
3095
+ try {
3096
+ const id = c.req.param('id');
3097
+ const workspace = workspaceService.getWorkspace(id);
3098
+ if (!workspace)
3099
+ return c.json({ error: `Workspace '${id}' not found` }, 404);
3100
+ const continued = gitOps.continueOngoingGitOperation(workspace.worktreePath);
3101
+ if (!continued)
3102
+ return c.json({ error: 'No Git operation is in progress' }, 409);
3103
+ return c.json({ success: true, continued });
3104
+ }
3105
+ catch (err) {
3106
+ const message = err instanceof Error ? err.message : String(err);
3107
+ return c.json({ error: message }, 500);
3108
+ }
3109
+ });
3085
3110
  /** Stage + commit all working-tree changes (recovery action for a dirty rebase/merge). */
3086
3111
  app.post('/:id/git/commit-all', async (c) => {
3087
3112
  try {
@@ -3121,24 +3146,16 @@ app.post('/:id/git/commit-with-agent', migrationGuard, async (c) => {
3121
3146
  5. Do NOT push the branch.
3122
3147
 
3123
3148
  When finished, report the commit SHA, its message, the files included, and the checks you ran.`;
3124
- const session = workspaceService.getActiveSession(workspace.id);
3125
- wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, session?.id ?? undefined);
3126
3149
  wakeupService.cancel(workspace.id, 'user-message');
3127
3150
  let messageSent = false;
3128
3151
  try {
3129
- agentManager.sendMessage(workspace.id, prompt);
3152
+ const { agentSessionId } = await deliverAgentPrompt(workspace, workspace.worktreePath, prompt);
3130
3153
  messageSent = true;
3154
+ wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, agentSessionId);
3131
3155
  }
3132
- catch {
3133
- try {
3134
- agentManager.startAgent(workspace.id, workspace.worktreePath, prompt, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3135
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3136
- messageSent = true;
3137
- }
3138
- catch (resumeErr) {
3139
- const message = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3140
- return c.json({ error: `Unable to ask the agent to commit: ${message}` }, 409);
3141
- }
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);
3142
3159
  }
3143
3160
  return c.json({ ok: true, messageSent });
3144
3161
  }
@@ -3212,27 +3229,18 @@ ${fileList}
3212
3229
  6. Report the summary: which files you touched, the key decisions you made, and the final test result.${cherryPickNote}
3213
3230
 
3214
3231
  Start now.`;
3215
- // Persist the prompt in the chat feed so the user sees what was dispatched.
3216
- const session = workspaceService.getActiveSession(workspace.id);
3217
- wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, session?.id ?? undefined);
3218
3232
  // Cancel any pending wakeup: the user is driving this turn, the
3219
3233
  // scheduler should not also wake the agent a few minutes later.
3220
3234
  wakeupService.cancel(workspace.id, 'user-message');
3221
3235
  let messageSent = false;
3222
3236
  try {
3223
- agentManager.sendMessage(workspace.id, prompt);
3237
+ const { agentSessionId } = await deliverAgentPrompt(workspace, worktreePath, prompt);
3224
3238
  messageSent = true;
3239
+ wsService.emit(workspace.id, 'user:message', { content: prompt, sender: 'user' }, agentSessionId);
3225
3240
  }
3226
- catch {
3227
- try {
3228
- agentManager.startAgent(workspace.id, worktreePath, prompt, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3229
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3230
- messageSent = true;
3231
- }
3232
- catch (resumeErr) {
3233
- const resumeMsg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3234
- console.warn(`[workspaces] resolve-with-agent: agent resume failed: ${resumeMsg}`);
3235
- }
3241
+ catch (deliveryErr) {
3242
+ const deliveryMessage = deliveryErr instanceof Error ? deliveryErr.message : String(deliveryErr);
3243
+ console.warn(`[workspaces] resolve-with-agent: agent resume failed: ${deliveryMessage}`);
3236
3244
  }
3237
3245
  return c.json({ ok: true, operation, files, messageSent });
3238
3246
  }
@@ -3507,29 +3515,18 @@ app.post('/:id/open-pr', async (c) => {
3507
3515
  diffStats,
3508
3516
  tasks,
3509
3517
  });
3510
- // Emit user:message into the chat feed
3511
- const session = workspaceService.getActiveSession(workspace.id);
3512
- wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, session?.id ?? undefined);
3513
3518
  // Cancel any pending wakeup: the user is driving this turn.
3514
3519
  wakeupService.cancel(workspace.id, 'user-message');
3515
3520
  // Send to the running agent, or resume the agent with the PR prompt
3516
3521
  let messageSent = false;
3517
3522
  try {
3518
- agentManager.sendMessage(workspace.id, rendered);
3523
+ const { agentSessionId } = await deliverAgentPrompt(workspace, workspace.worktreePath, rendered);
3519
3524
  messageSent = true;
3525
+ wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, agentSessionId);
3520
3526
  }
3521
- catch {
3522
- // Agent not running — resume it with the PR prompt
3523
- try {
3524
- const worktreePathForResume = workspace.worktreePath;
3525
- agentManager.startAgent(workspace.id, worktreePathForResume, rendered, workspace.model, true, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3526
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3527
- messageSent = true;
3528
- }
3529
- catch (resumeErr) {
3530
- const resumeMsg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3531
- console.warn(`[workspaces] open-pr: PR created but agent resume failed: ${resumeMsg}`);
3532
- }
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}`);
3533
3530
  }
3534
3531
  return c.json({ ok: true, prNumber, prUrl, messageSent });
3535
3532
  }
@@ -3619,20 +3616,13 @@ app.post('/:id/start-review', async (c) => {
3619
3616
  const session = workspaceService.getActiveSession(workspace.id);
3620
3617
  emitSessionId = session?.id;
3621
3618
  try {
3622
- agentManager.sendMessage(workspace.id, rendered);
3619
+ const delivery = await deliverAgentPrompt(workspace, worktreePath, rendered);
3620
+ emitSessionId = delivery.agentSessionId;
3623
3621
  messageSent = true;
3624
3622
  }
3625
- catch {
3626
- try {
3627
- const agent = agentManager.startAgent(workspace.id, worktreePath, rendered, workspace.model, true /* resume */, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3628
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3629
- emitSessionId = agent?.agentSessionId ?? emitSessionId;
3630
- messageSent = true;
3631
- }
3632
- catch (resumeErr) {
3633
- const msg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3634
- return c.json({ error: `Failed to dispatch review prompt: ${msg}` }, 500);
3635
- }
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);
3636
3626
  }
3637
3627
  }
3638
3628
  // Emit AFTER dispatch so the user:message lands in the correct session id —
@@ -3696,18 +3686,12 @@ app.post('/:id/start-ci-fix', migrationGuard, async (c) => {
3696
3686
  const session = workspaceService.getActiveSession(workspace.id);
3697
3687
  let emitSessionId = session?.id;
3698
3688
  try {
3699
- agentManager.sendMessage(workspace.id, rendered);
3689
+ const delivery = await deliverAgentPrompt(workspace, workspace.worktreePath, rendered);
3690
+ emitSessionId = delivery.agentSessionId;
3700
3691
  }
3701
- catch {
3702
- try {
3703
- const agent = agentManager.startAgent(workspace.id, workspace.worktreePath, rendered, workspace.model, true /* resume */, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
3704
- workspaceService.updateWorkspaceStatus(workspace.id, 'executing');
3705
- emitSessionId = agent?.agentSessionId ?? emitSessionId;
3706
- }
3707
- catch (resumeErr) {
3708
- const msg = resumeErr instanceof Error ? resumeErr.message : String(resumeErr);
3709
- return c.json({ error: `Failed to dispatch CI-fix prompt: ${msg}` }, 500);
3710
- }
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);
3711
3695
  }
3712
3696
  wsService.emit(workspace.id, 'user:message', { content: rendered, sender: 'user' }, emitSessionId);
3713
3697
  return c.json({ ok: true, failedChecksCount: failedChecks.length });
@@ -3819,18 +3803,49 @@ app.post('/:id/stop', migrationGuard, (c) => {
3819
3803
  }
3820
3804
  });
3821
3805
  // POST /api/workspaces/:id/interrupt — soft-interrupt agent (SIGINT, like Escape in Claude Code)
3822
- app.post('/:id/interrupt', migrationGuard, (c) => {
3806
+ app.post('/:id/interrupt', migrationGuard, async (c) => {
3823
3807
  try {
3824
3808
  const id = c.req.param('id');
3825
3809
  const workspace = workspaceService.getWorkspace(id);
3826
3810
  if (!workspace) {
3827
3811
  return c.json({ error: `Workspace '${id}' not found` }, 404);
3828
3812
  }
3829
- 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);
3830
3841
  return c.json({ status: 'interrupted' });
3831
3842
  }
3832
3843
  catch (err) {
3833
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
+ }
3834
3849
  return c.json({ error: message }, 500);
3835
3850
  }
3836
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
  }