@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.
- package/CHANGELOG.md +8 -0
- package/dist/server/routes/workspaces.js +83 -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/server/utils/git-ops.js +14 -0
- package/dist/shared/whip-shortcut.js +41 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CdjS_Ay5.js +8 -0
- package/src/client/dist/pwa/assets/{ChangelogPage-BaSsYQ5d.js → ChangelogPage-BTU5fnD6.js} +1 -1
- package/src/client/dist/pwa/assets/{ClosePopup-C7qMeVHU.js → ClosePopup-BAuFTRhx.js} +1 -1
- package/src/client/dist/pwa/assets/{CreatePage-BYYyZ_Ff.js → CreatePage-CsEecOVF.js} +2 -2
- package/src/client/dist/pwa/assets/{DiffViewer-CCtHwGu6.js → DiffViewer-Dq8vqrxA.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-DIMw_XqU.js → HealthPage-sbDeyEHP.js} +1 -1
- package/src/client/dist/pwa/assets/MainLayout-DFwMZKt9.js +37 -0
- package/src/client/dist/pwa/assets/{MainLayout-BNYFr-Y6.css → MainLayout-DlS2wxkK.css} +1 -1
- package/src/client/dist/pwa/assets/{QBtnToggle-Di2dp1Lt.js → QBtnToggle-dJ_m8WhV.js} +1 -1
- package/src/client/dist/pwa/assets/{QChip-Y5_nvR_g.js → QChip-Bo3J1D2W.js} +1 -1
- package/src/client/dist/pwa/assets/QSelect-OaS8rLcb.js +36 -0
- package/src/client/dist/pwa/assets/QTooltip-j1MBzbTI.js +1 -0
- package/src/client/dist/pwa/assets/{SearchPage-DxfA-RpF.js → SearchPage-BmwtbAot.js} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-COvUlfw5.js +16 -0
- package/src/client/dist/pwa/assets/SettingsPage-DyZUZK5m.css +1 -0
- package/src/client/dist/pwa/assets/{ThinkingItem-CcLcHo1b.js → ThinkingItem-6MaIe4ws.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-uoWtAAB4.js → TouchPan-BIp_9Cq6.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-BE5DwWlF.css → WorkspacePage-Bgxg_Rua.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DLtgENoR.js +4 -0
- package/src/client/dist/pwa/assets/{build-path-tree-D1fWaedJ.js → build-path-tree-Du1muMLG.js} +1 -1
- package/src/client/dist/pwa/assets/{cssMode-BLTlRoqI.js → cssMode-DJXtqmV0.js} +1 -1
- package/src/client/dist/pwa/assets/documents-Br5SnzNo.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api2-C1zql2a3.js → editor.api2-CZ8VVoEU.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.main-BM_AJ8GC.js → editor.main-DevrSvQT.js} +2 -2
- package/src/client/dist/pwa/assets/{expand-template-CdOnyk7u.js → expand-template-CL8E3tTa.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-D_RkuRmV.js → formatters-BfN5-DOd.js} +1 -1
- package/src/client/dist/pwa/assets/fouet-ahh-DdxrUXRj.mp3 +0 -0
- package/src/client/dist/pwa/assets/{freemarker2-B2Am5RCx.js → freemarker2-C2qa_OD-.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-jtMsrPGt.js → handlebars-rmnqsHUd.js} +1 -1
- package/src/client/dist/pwa/assets/{html-XkaC_Vpq.js → html-BGmv8Xd1.js} +1 -1
- package/src/client/dist/pwa/assets/{htmlMode-BUuUfuL6.js → htmlMode-BAm18C8p.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-JdLLVp05.js +1 -0
- package/src/client/dist/pwa/assets/index-BbjyEh9i.js +84 -0
- package/src/client/dist/pwa/assets/{javascript-DFkvrBQi.js → javascript-CHZDr7Bk.js} +1 -1
- package/src/client/dist/pwa/assets/{jsonMode-DG-AnaCb.js → jsonMode-B6gDhGXQ.js} +1 -1
- package/src/client/dist/pwa/assets/{kobo-commands-lJHYm--K.js → kobo-commands-BR2agQLr.js} +1 -1
- package/src/client/dist/pwa/assets/{layout-WGOsD1fA.js → layout-D2SIWUB9.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Caxw51Te.js → liquid-oF7cqWzJ.js} +1 -1
- package/src/client/dist/pwa/assets/{lspLanguageFeatures-DJQ1N4bv.js → lspLanguageFeatures-B8EE1Ucr.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-D0tPH0_M.js → mdx-DXdcJzaj.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-D2e4yDW_.js → monaco.contribution-CAMlPG7z.js} +2 -2
- package/src/client/dist/pwa/assets/network-auth-BnLlhIx6.js +1 -0
- package/src/client/dist/pwa/assets/{pending-diff-open-DmfQOYj8.js → pending-diff-open-BMRVHZZL.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-ByPJHncQ.js → permissionModes-CfAFkWP3.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-z6Lk4hj8.js → project-color-DO7Am583.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DN4jsz0q.js → python-DKQcYTdf.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-CmVIxbMc.js → razor-2a-_ORi3.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-DOgKdkvV.js → render-chat-markdown-ESsukc9f.js} +1 -1
- package/src/client/dist/pwa/assets/{tsMode-O2eMFtPN.js → tsMode-DB3IvOW4.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-r7SGGrpF.js → typescript-Dc5EP7Bs.js} +1 -1
- package/src/client/dist/pwa/assets/{use-onboarding-5eX5X0EJ.js → use-onboarding-x0pF_4eq.js} +1 -1
- package/src/client/dist/pwa/assets/{use-quasar-gNcVRGYS.js → use-quasar-CBDv7_fL.js} +1 -1
- package/src/client/dist/pwa/assets/{workers-DZ00UUkn.js → workers-DO53K6md.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-CALFQGQ2.js → xml-D-yJwZeB.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-DNnrEGwW.js → yaml--VhNMxaM.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-DB4rs2R_.js +0 -8
- package/src/client/dist/pwa/assets/MainLayout-DUcc_q9G.js +0 -37
- package/src/client/dist/pwa/assets/QSelect-q-_4A_Q0.js +0 -36
- package/src/client/dist/pwa/assets/QTooltip-Dl9hEvDw.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-BAFLf2LW.js +0 -16
- package/src/client/dist/pwa/assets/SettingsPage-Os-6b-6H.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DEj_FbHT.js +0 -4
- package/src/client/dist/pwa/assets/documents-BjIvI6HK.js +0 -1
- package/src/client/dist/pwa/assets/i18n-B9ComK7B.js +0 -1
- package/src/client/dist/pwa/assets/index-D741YDZP.js +0 -84
- 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
|
-
|
|
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
|
-
|
|
3134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3523
|
-
|
|
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
|
-
|
|
3619
|
+
const delivery = await deliverAgentPrompt(workspace, worktreePath, rendered);
|
|
3620
|
+
emitSessionId = delivery.agentSessionId;
|
|
3623
3621
|
messageSent = true;
|
|
3624
3622
|
}
|
|
3625
|
-
catch {
|
|
3626
|
-
|
|
3627
|
-
|
|
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
|
-
|
|
3689
|
+
const delivery = await deliverAgentPrompt(workspace, workspace.worktreePath, rendered);
|
|
3690
|
+
emitSessionId = delivery.agentSessionId;
|
|
3700
3691
|
}
|
|
3701
|
-
catch {
|
|
3702
|
-
|
|
3703
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|