@loicngr/kobo 1.11.6 → 1.11.8
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.
- package/CHANGELOG.md +8 -0
- package/dist/server/routes/workspaces.js +66 -68
- package/dist/server/services/agent/engines/codex/engine.js +42 -7
- package/dist/server/services/agent/orchestrator.js +205 -54
- package/dist/server/services/agent/session-controller.js +20 -4
- package/dist/server/services/settings-service.js +58 -2
- package/dist/shared/whip-shortcut.js +41 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BkkyAw81.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-oLAJW1vx.js → ChangelogPage-BlvhBx6o.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-BiMd0EFR.js → ClosePopup-_rbZbpfb.js} +1 -1
- package/src/client/dist/pwa/assets/{CreatePage-D7R9aFEL.js → CreatePage-RSyoQwD7.js} +2 -2
- package/src/client/dist/pwa/assets/{DiffViewer-CS1roZQD.js → DiffViewer-BSNxNex2.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-cP6NvQlI.js → HealthPage-CUD1kipV.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-BCLgsn0C.js +37 -0
- package/src/client/dist/pwa/assets/{MainLayout-CYT-Kkia.css → MainLayout-DlS2wxkK.css} +1 -1
- package/src/client/dist/pwa/assets/{QBtnToggle-5Ypsr5PI.js → QBtnToggle-xg3LYW7D.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-CIwjmeox.js → QChip-DhUfnmbz.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-BhyT0k2N.js +36 -0
- package/src/client/dist/pwa/assets/QTooltip-ybbFi3KK.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-DnUu9LCd.js → SearchPage-Bb7TG3Vo.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-C2qe4xKu.css +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-CjPnnn60.js +16 -0
- package/src/client/dist/pwa/assets/{ThinkingItem-Dsh0S-4R.js → ThinkingItem-B1RKyCIS.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-BK8skWbU.js → TouchPan-DzZPYIol.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-6Rgjarvf.js +4 -0
- package/src/client/dist/pwa/assets/{WorkspacePage-BE5DwWlF.css → WorkspacePage-Bgxg_Rua.css} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-Dt3aRTqV.js → build-path-tree-KRACVkD3.js} +1 -1
- package/src/client/dist/pwa/assets/{cssMode-C2-EJrmK.js → cssMode-CQsB6mFO.js} +1 -1
- package/src/client/dist/pwa/assets/documents-C7efBF5t.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api2-CkPvAZB5.js → editor.api2-C58pkxmw.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.main-B-8Qv0oB.js → editor.main-BhLqjdq4.js} +2 -2
- package/src/client/dist/pwa/assets/{expand-template-Cnj1_kF0.js → expand-template-BA-hhG5v.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-BGHblyDL.js → formatters-DugWNOEs.js} +1 -1
- package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
- package/src/client/dist/pwa/assets/{freemarker2-VFyuTCM8.js → freemarker2-DczqHd5F.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-g2m5ALjb.js → handlebars-DmA7twi8.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DxnmOMw6.js → html-DtZDrViU.js} +1 -1
- package/src/client/dist/pwa/assets/{htmlMode-Df0wbbtZ.js → htmlMode-DO9_qUdT.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CipNqHTm.js +1 -0
- package/src/client/dist/pwa/assets/{index-CXciTur8.js → index-BxspWSDs.js} +9 -9
- package/src/client/dist/pwa/assets/{javascript-Df_-1Pv_.js → javascript-Bs83z_W4.js} +1 -1
- package/src/client/dist/pwa/assets/{jsonMode-BJFi57QF.js → jsonMode-Bc-8bzJi.js} +1 -1
- package/src/client/dist/pwa/assets/{kobo-commands-HJVSrhFj.js → kobo-commands-B2kM0INN.js} +1 -1
- package/src/client/dist/pwa/assets/{layout-DLVS_2RL.js → layout-C6frIxKZ.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Dy9wgTn4.js → liquid-Dnxe0NUB.js} +1 -1
- package/src/client/dist/pwa/assets/{lspLanguageFeatures-DcaGTZUU.js → lspLanguageFeatures-qDBRYovw.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-B1nH83dR.js → mdx-C1oke9bq.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution--RPgZPOx.js → monaco.contribution-BbFzrpO3.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-Bj1LS5R7.js +1 -0
- package/src/client/dist/pwa/assets/{pending-diff-open-BS9BktRr.js → pending-diff-open-C3voSObg.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-ClcSQKmQ.js → permissionModes-DMuXGtZu.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-SXRt-W03.js → project-color-DIFZ95_g.js} +1 -1
- package/src/client/dist/pwa/assets/{python-CJMnM2o7.js → python-Dgq-bJSM.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BUhyGYdm.js → razor-Bms4CQGF.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-BH1Z10ar.js → render-chat-markdown-BRSTe4je.js} +1 -1
- package/src/client/dist/pwa/assets/{tsMode-Cps5WrVq.js → tsMode-DOsxzgZi.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-DHc8Mn2c.js → typescript-CzOp_GqX.js} +1 -1
- package/src/client/dist/pwa/assets/{use-onboarding-BEN9LPyX.js → use-onboarding-BGy3sOAN.js} +1 -1
- package/src/client/dist/pwa/assets/{use-quasar-CgjwhVVN.js → use-quasar-DsmzxifG.js} +1 -1
- package/src/client/dist/pwa/assets/{workers-B_i0s5nK.js → workers-0fozwhvS.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-B4oQ8zNG.js → xml-CPa01uHm.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-CUpQdB7b.js → yaml-Cw3fzSuh.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sounds/mais-laisse-moi-dormir-zebi.mp3 +0 -0
- package/src/client/dist/pwa/sounds/yaaa.mp3 +0 -0
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BM5l2qIA.js +0 -8
- package/src/client/dist/pwa/assets/MainLayout-BS27uyox.js +0 -37
- package/src/client/dist/pwa/assets/QSelect-BYNOOfHf.js +0 -36
- package/src/client/dist/pwa/assets/QTooltip-A4le4l3f.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-BPplhEXC.js +0 -16
- package/src/client/dist/pwa/assets/SettingsPage-Os-6b-6H.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-YM_Qujn2.js +0 -4
- package/src/client/dist/pwa/assets/documents-BFIwenTv.js +0 -1
- package/src/client/dist/pwa/assets/i18n-Cf8eFhZ8.js +0 -1
- package/src/client/dist/pwa/assets/network-auth-DYx2-JTO.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.8
|
|
8
|
+
|
|
9
|
+
- fix(whip): preserve delivery and shortcut handling (#24)
|
|
10
|
+
|
|
11
|
+
## 1.11.7
|
|
12
|
+
|
|
13
|
+
- feat: add opt-in interactive whip (#23)
|
|
14
|
+
|
|
7
15
|
## 1.11.6
|
|
8
16
|
|
|
9
17
|
- 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
|
-
|
|
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
|
-
|
|
3151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3540
|
-
|
|
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
|
-
|
|
3619
|
+
const delivery = await deliverAgentPrompt(workspace, worktreePath, rendered);
|
|
3620
|
+
emitSessionId = delivery.agentSessionId;
|
|
3640
3621
|
messageSent = true;
|
|
3641
3622
|
}
|
|
3642
|
-
catch {
|
|
3643
|
-
|
|
3644
|
-
|
|
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
|
-
|
|
3689
|
+
const delivery = await deliverAgentPrompt(workspace, workspace.worktreePath, rendered);
|
|
3690
|
+
emitSessionId = delivery.agentSessionId;
|
|
3717
3691
|
}
|
|
3718
|
-
catch {
|
|
3719
|
-
|
|
3720
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|