@loicngr/kobo 1.11.37 → 1.11.39
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/AGENTS.md +3 -1
- package/CHANGELOG.md +8 -0
- package/dist/server/index.js +9 -0
- package/dist/server/routes/workspaces.js +39 -18
- package/dist/server/services/agent/engines/claude-code/engine.js +11 -0
- package/dist/server/services/agent/engines/codex/event-mapper.js +3 -1
- package/dist/server/services/agent/event-router.js +1 -1
- package/dist/server/services/agent/orchestrator.js +73 -10
- package/dist/server/services/auto-loop-service.js +3 -3
- package/dist/server/services/pr-watcher-service.js +5 -3
- package/dist/server/services/websocket-service.js +10 -2
- package/dist/server/services/workspace-service.js +8 -4
- package/package.json +2 -2
- package/src/client/dist/pwa/assets/{ActivityFeed-C_-hxIo8.js → ActivityFeed-BzzvcOmq.js} +2 -2
- package/src/client/dist/pwa/assets/{ActivityFeed-BDyFBoxd.css → ActivityFeed-Chsw1Mh7.css} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-CBlGJ4zT.js → ChangelogPage-D8563008.js} +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-Bd0X08sg.js +2 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-CRRaDs2D.css +1 -0
- package/src/client/dist/pwa/assets/{CreatePage-ArmtcF4d.js → CreatePage-Bon8qIka.js} +1 -1
- package/src/client/dist/pwa/assets/DashboardPage-D8g7_r4S.css +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +1 -0
- package/src/client/dist/pwa/assets/{DiffViewer-CaT7vuSr.js → DiffViewer-BpsLNxnj.js} +3 -3
- package/src/client/dist/pwa/assets/{DocumentsPanel-A0isCTaI.js → DocumentsPanel-BJsJ0fNs.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-T70fh8dp.js → DrawerToggleButton-BVLO5K5d.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-CAvk0G7h.js → GitPanel-Cal_wzC0.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-D8Mr0V6w.js → HealthPage-RFZS7Lao.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-x6Es4Da-.js → MainLayout-vvyiRcql.js} +2 -2
- package/src/client/dist/pwa/assets/{QBtnDropdown-CQqzzlFI.js → QBtnDropdown-X8oNU5Mb.js} +1 -1
- package/src/client/dist/pwa/assets/{QDrawer-Cwj2qh0r.js → QDrawer-Cij17skX.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-BSPtuXJg.js → QSelect-xd-AlGo_.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-DC_kDKku.js → QTooltip-CwcCGSWq.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-9aJSoz6Y.js → SchedulePanel-CXRUsz6G.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-BjNvtE_U.js → SearchPage-DopbgLxh.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-CvLnT73V.js → SettingsPage-Jx6TnOzR.js} +1 -1
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-DTao0DHr.js → SplitWorkspacePage-D7lGFRKs.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-BrKIW-bJ.js → TerminalPanel-DI8DHb9e.js} +1 -1
- package/src/client/dist/pwa/assets/{TouchPan-_n1AQgoo.js → TouchPan-B3nrkCY8.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-CJF3nVvn.js → TourReplayButton-DU1UShMY.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CxesUiIm.js → WorkspaceAttentionLabels-C-yN5IX8.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-DoXtwpjV.css → WorkspacePage-BAC6u9OP.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +4 -0
- package/src/client/dist/pwa/assets/{browser-DGWpkvN3.js → browser-f9MHQjfi.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-Ba1dSLgi.js → build-path-tree-BWBzP8os.js} +1 -1
- package/src/client/dist/pwa/assets/{de-DVK21RB6.js → de-QkDLrcGq.js} +1 -1
- package/src/client/dist/pwa/assets/{documents-BMHzjW_g.js → documents-C2cLnEBQ.js} +1 -1
- package/src/client/dist/pwa/assets/{editor.api-CYTuNwqu.js → editor.api-sw146MWN.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-BPlRSHXp.js → efforts-C5ZZgCH4.js} +1 -1
- package/src/client/dist/pwa/assets/{engineFeatures-DVB2rklO.js → engineFeatures-CAVMDawq.js} +1 -1
- package/src/client/dist/pwa/assets/{es-C8cQwDZM.js → es-BgQFKlo8.js} +1 -1
- package/src/client/dist/pwa/assets/{expand-template-Cj82YE-O.js → expand-template-B6xAlAaM.js} +1 -1
- package/src/client/dist/pwa/assets/{format-Dk7O3LhA.js → format-BwWP6Aay.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-TGjvs7Ca.js → formatters-DyWtO-nR.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-BVOMZ2Bl.js → fr-DAdLWjNM.js} +1 -1
- package/src/client/dist/pwa/assets/{freemarker2-39lO_dWH.js → freemarker2-DZ5Hm8UH.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-M_0V82cD.js → handlebars-BRUGpTA7.js} +1 -1
- package/src/client/dist/pwa/assets/{html-yILqYDsl.js → html-DNOayNNs.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CBez_vip.js +1 -0
- package/src/client/dist/pwa/assets/index-VL3fI9a3.js +20 -0
- package/src/client/dist/pwa/assets/{it-CxkAoOj4.js → it-Bumaewp1.js} +1 -1
- package/src/client/dist/pwa/assets/{javascript-DrW-amDc.js → javascript-C0tDQU3z.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-D_mXmUoL.js → liquid-CN46O6OF.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-E8O3lvR5.js → mdx-DM-lrr8Q.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-C2K3sl_V.js → monaco-contributions-CkN7y-8c.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-DLhHQIJn.js → monaco.contribution-2BcAOLjc.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-BUwWtnui.js → network-auth-KYS4snQH.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-1mOpY2vN.js → notify-theme-a91Tcr6o.js} +1 -1
- package/src/client/dist/pwa/assets/{pending-diff-open-BL7dKRqF.js → pending-diff-open-DOtELZfB.js} +1 -1
- package/src/client/dist/pwa/assets/{permissionModes-BtC3_P-P.js → permissionModes-C-9Crj-c.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-BFBFZWeG.js → project-color-BRe4oyNx.js} +1 -1
- package/src/client/dist/pwa/assets/{python-DoZAYim6.js → python-BRpJG6sZ.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-9bZ30g2y.js → razor-CehCmvHl.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-7EtWNjG0.js → render-chat-markdown-DlbQLZ02.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-C1CBfL3L.js → typescript-Dt_8mVuj.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-BJ81clv6.js → use-tours-CSVM_54K.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-r1-bipjM.js → workspace-templates-GCObcjO6.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-COuu5isK.js → xml-_gOwdzB4.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-ChZ7WqaJ.js → yaml-BxeBqpGf.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-DZAE2X9j.js +0 -2
- package/src/client/dist/pwa/assets/ComparisonPanel-x1iuICgd.css +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-DsWn5cWl.css +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-eQTCXcdz.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-MxBE0mr3.js +0 -4
- package/src/client/dist/pwa/assets/i18n-2UeMfd5m.js +0 -1
- package/src/client/dist/pwa/assets/index-BJU5dnkF.js +0 -20
package/AGENTS.md
CHANGED
|
@@ -114,7 +114,9 @@ src/
|
|
|
114
114
|
| `workspace_chat_history` | chat-input history per workspace: message text + `created_at`, ordered by autoincrement id, capped at 200 entries by the service; CASCADE DELETE on workspace |
|
|
115
115
|
| `workspace_permission_rules` | remembered per-workspace tool approvals, scoped to an exact operation or every invocation of a tool; CASCADE DELETE on workspace |
|
|
116
116
|
|
|
117
|
-
`status` enum: `created | extracting | brainstorming | executing | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
|
|
117
|
+
`status` enum: `created | extracting | brainstorming | executing | compacting | awaiting-user | completed | idle | error | quota`. Transitions are validated in `updateWorkspaceStatus` against `VALID_TRANSITIONS`.
|
|
118
|
+
|
|
119
|
+
`compacting` is a server-owned busy state while the agent compacts context. The orchestrator associates the previous status with the current controller and restores it when compaction ends; stop, errors and startup reconciliation must not leave the workspace stuck in compaction. Normal workspace status updates make this state available after reload and across clients. Chat sends are refused during compaction before delivery or fallback starts, and the client keeps the user's draft while disabling send. The existing status column is unrestricted TEXT, so adding this state does not change the database schema.
|
|
118
120
|
|
|
119
121
|
`archived_at` is **orthogonal** to `status`. Archiving is a visibility flag, not a lifecycle state. Unarchive restores the exact pre-archive `status`.
|
|
120
122
|
|
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.39
|
|
8
|
+
|
|
9
|
+
- fix(deps): require Claude SDK with Fable 5.1 support
|
|
10
|
+
|
|
11
|
+
## 1.11.38
|
|
12
|
+
|
|
13
|
+
- feat(workspaces): show compaction status and block chat sends
|
|
14
|
+
|
|
7
15
|
## 1.11.37
|
|
8
16
|
|
|
9
17
|
- fix(kobo): harden workspace lifecycle and comparison navigation
|
package/dist/server/index.js
CHANGED
|
@@ -346,6 +346,15 @@ terminalWss.on('connection', (ws, workspaceId) => {
|
|
|
346
346
|
setMessageHandler(async (type, payload) => {
|
|
347
347
|
const p = payload;
|
|
348
348
|
if (type === 'chat:message' && p?.workspaceId && p?.content) {
|
|
349
|
+
if (getWorkspace(p.workspaceId)?.status === 'compacting') {
|
|
350
|
+
emitEphemeral(p.workspaceId, 'chat:rejected', {
|
|
351
|
+
reason: 'compacting',
|
|
352
|
+
sessionId: p.sessionId,
|
|
353
|
+
content: p.content,
|
|
354
|
+
message: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
355
|
+
});
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
349
358
|
// Auto-loop owns the agent's turns. A user message means the user wants
|
|
350
359
|
// to redirect the conversation, so disable the loop (idempotent — the
|
|
351
360
|
// `autoloop:disabled` event is emitted with reason='user-action' so the
|
|
@@ -71,6 +71,27 @@ for (const route of ['/:id/archive', '/:id/run-setup-script', '/:id/cancel-sourc
|
|
|
71
71
|
}
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
+
// User prompt actions must reject before cancelling timers, stopping engines,
|
|
75
|
+
// creating a PR or persisting input. The orchestrator also checks at delivery.
|
|
76
|
+
for (const route of [
|
|
77
|
+
'/:id/start',
|
|
78
|
+
'/:id/switch-engine',
|
|
79
|
+
'/:id/git/commit-with-agent',
|
|
80
|
+
'/:id/git/resolve-with-agent',
|
|
81
|
+
'/:id/open-pr',
|
|
82
|
+
'/:id/start-review',
|
|
83
|
+
'/:id/start-ci-fix',
|
|
84
|
+
]) {
|
|
85
|
+
app.use(route, async (c, next) => {
|
|
86
|
+
if (c.req.method === 'POST' && workspaceService.getWorkspace(c.req.param('id'))?.status === 'compacting') {
|
|
87
|
+
return c.json({
|
|
88
|
+
code: 'compacting',
|
|
89
|
+
error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
90
|
+
}, 409);
|
|
91
|
+
}
|
|
92
|
+
await next();
|
|
93
|
+
});
|
|
94
|
+
}
|
|
74
95
|
/** Tracks workspaces currently running a setup script to prevent concurrent executions. */
|
|
75
96
|
const setupScriptRunning = new Set();
|
|
76
97
|
/**
|
|
@@ -310,6 +331,12 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
|
|
|
310
331
|
const handoff = typeof body.handoff === 'string' && body.handoff.trim()
|
|
311
332
|
? body.handoff.trim().slice(0, 30_000)
|
|
312
333
|
: buildEngineHandoff(workspace, workspace.engine, body.engine);
|
|
334
|
+
if (workspaceService.getWorkspace(workspace.id)?.status === 'compacting') {
|
|
335
|
+
return c.json({
|
|
336
|
+
code: 'compacting',
|
|
337
|
+
error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
338
|
+
}, 409);
|
|
339
|
+
}
|
|
313
340
|
// A new engine is started three lines below: the previous one must be dead
|
|
314
341
|
// first, or two agents share the worktree.
|
|
315
342
|
if (agentManager.hasController(id))
|
|
@@ -3180,6 +3207,12 @@ app.post('/:id/start', migrationGuard, async (c) => {
|
|
|
3180
3207
|
const prompt = body.prompt ?? pendingInitialPrompt ?? 'Continue the previous task where you left off.';
|
|
3181
3208
|
const agentSessionId = body.agentSessionId;
|
|
3182
3209
|
const resume = body.resume === true;
|
|
3210
|
+
if (workspaceService.getWorkspace(id)?.status === 'compacting') {
|
|
3211
|
+
return c.json({
|
|
3212
|
+
code: 'compacting',
|
|
3213
|
+
error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
3214
|
+
}, 409);
|
|
3215
|
+
}
|
|
3183
3216
|
// A manual start supersedes any scheduled retry (quota or transient
|
|
3184
3217
|
// watchdog recovery). Leaving its timer armed could spawn a second agent
|
|
3185
3218
|
// after the user has already resumed the workspace.
|
|
@@ -3831,7 +3864,6 @@ app.post('/:id/git/commit-with-agent', migrationGuard, async (c) => {
|
|
|
3831
3864
|
5. Do NOT push the branch.
|
|
3832
3865
|
|
|
3833
3866
|
When finished, report the commit SHA, its message, the files included, and the checks you ran.`;
|
|
3834
|
-
wakeupService.cancel(workspace.id, 'user-message');
|
|
3835
3867
|
let messageSent = false;
|
|
3836
3868
|
try {
|
|
3837
3869
|
const { agentSessionId } = await deliverAgentPrompt(workspace, workspace.worktreePath, prompt);
|
|
@@ -3914,9 +3946,6 @@ ${fileList}
|
|
|
3914
3946
|
6. Report the summary: which files you touched, the key decisions you made, and the final test result.${cherryPickNote}
|
|
3915
3947
|
|
|
3916
3948
|
Start now.`;
|
|
3917
|
-
// Cancel any pending wakeup: the user is driving this turn, the
|
|
3918
|
-
// scheduler should not also wake the agent a few minutes later.
|
|
3919
|
-
wakeupService.cancel(workspace.id, 'user-message');
|
|
3920
3949
|
let messageSent = false;
|
|
3921
3950
|
try {
|
|
3922
3951
|
const { agentSessionId } = await deliverAgentPrompt(workspace, worktreePath, prompt);
|
|
@@ -4251,8 +4280,6 @@ app.post('/:id/open-pr', async (c) => {
|
|
|
4251
4280
|
diffStats,
|
|
4252
4281
|
tasks,
|
|
4253
4282
|
});
|
|
4254
|
-
// Cancel any pending wakeup: the user is driving this turn.
|
|
4255
|
-
wakeupService.cancel(workspace.id, 'user-message');
|
|
4256
4283
|
// Send to the running agent, or resume the agent with the PR prompt
|
|
4257
4284
|
let messageSent = false;
|
|
4258
4285
|
try {
|
|
@@ -4320,14 +4347,14 @@ app.post('/:id/start-review', async (c) => {
|
|
|
4320
4347
|
baseCommit,
|
|
4321
4348
|
additionalInstructions,
|
|
4322
4349
|
});
|
|
4323
|
-
try {
|
|
4324
|
-
wakeupService.cancel(workspace.id, 'user-message');
|
|
4325
|
-
}
|
|
4326
|
-
catch {
|
|
4327
|
-
/* swallow */
|
|
4328
|
-
}
|
|
4329
4350
|
let messageSent = false;
|
|
4330
4351
|
let emitSessionId;
|
|
4352
|
+
if (workspaceService.getWorkspace(workspace.id)?.status === 'compacting') {
|
|
4353
|
+
return c.json({
|
|
4354
|
+
code: 'compacting',
|
|
4355
|
+
error: 'Workspace is compacting its context; wait until compaction finishes before sending a message',
|
|
4356
|
+
}, 409);
|
|
4357
|
+
}
|
|
4331
4358
|
if (newSession) {
|
|
4332
4359
|
// Stop current agent, wait for it, then start fresh.
|
|
4333
4360
|
try {
|
|
@@ -4414,12 +4441,6 @@ app.post('/:id/start-ci-fix', migrationGuard, async (c) => {
|
|
|
4414
4441
|
failedChecks,
|
|
4415
4442
|
ciRunUrl,
|
|
4416
4443
|
});
|
|
4417
|
-
try {
|
|
4418
|
-
wakeupService.cancel(workspace.id, 'user-message');
|
|
4419
|
-
}
|
|
4420
|
-
catch {
|
|
4421
|
-
/* swallow */
|
|
4422
|
-
}
|
|
4423
4444
|
const session = workspaceService.getActiveSession(workspace.id);
|
|
4424
4445
|
let emitSessionId = session?.id;
|
|
4425
4446
|
try {
|
|
@@ -353,6 +353,7 @@ export function createClaudeCodeEngine() {
|
|
|
353
353
|
return;
|
|
354
354
|
console.warn(`[claude-engine] Compaction still reported active ${COMPACTION_STALL_TIMEOUT_MS}ms after it started — resuming the liveness deadline.`);
|
|
355
355
|
isCompacting = false;
|
|
356
|
+
safeEmit({ kind: 'session:compacting', active: false });
|
|
356
357
|
reevaluateLivenessPause();
|
|
357
358
|
if (activeSubagentTaskIds.size > 0 && !subagentStallTimer) {
|
|
358
359
|
// Resume declined because a subagent is still tracked — but the
|
|
@@ -468,6 +469,16 @@ export function createClaudeCodeEngine() {
|
|
|
468
469
|
clearCompactionStallTimer();
|
|
469
470
|
}
|
|
470
471
|
}
|
|
472
|
+
// Actual foreground output proves work resumed even if the SDK
|
|
473
|
+
// omitted its trailing status/compact_boundary message. Tool results
|
|
474
|
+
// and background progress can drain during compaction, so exclude them.
|
|
475
|
+
if (isCompacting &&
|
|
476
|
+
!('parent_tool_use_id' in msg && msg.parent_tool_use_id != null) &&
|
|
477
|
+
events.some((ev) => ev.kind === 'message:text' || ev.kind === 'message:thinking' || ev.kind === 'tool:call')) {
|
|
478
|
+
isCompacting = false;
|
|
479
|
+
clearCompactionStallTimer();
|
|
480
|
+
safeEmit({ kind: 'session:compacting', active: false });
|
|
481
|
+
}
|
|
471
482
|
for (const ev of events) {
|
|
472
483
|
if (ev.kind === 'tool:call')
|
|
473
484
|
pendingToolCallIds.add(ev.toolCallId);
|
|
@@ -179,7 +179,7 @@ export function handleItemStarted(item, state) {
|
|
|
179
179
|
return events;
|
|
180
180
|
}
|
|
181
181
|
if (item.type === 'contextCompaction') {
|
|
182
|
-
events.push({ kind: 'session:
|
|
182
|
+
events.push({ kind: 'session:compacting', active: true });
|
|
183
183
|
return events;
|
|
184
184
|
}
|
|
185
185
|
return events;
|
|
@@ -189,6 +189,8 @@ export function handleItemStarted(item, state) {
|
|
|
189
189
|
*/
|
|
190
190
|
export function handleItemCompleted(item, state) {
|
|
191
191
|
const events = [];
|
|
192
|
+
if (item.type === 'contextCompaction')
|
|
193
|
+
return [{ kind: 'session:compacted' }];
|
|
192
194
|
if (item.type === 'agentMessage') {
|
|
193
195
|
const text = item.text ?? '';
|
|
194
196
|
const messageId = scopedId(state, item.id);
|
|
@@ -3,7 +3,7 @@ export function routeEvent(workspaceId, agentSessionId, event) {
|
|
|
3
3
|
// The compaction "in progress" signal is a transient live indicator — deliver
|
|
4
4
|
// it once and never persist/replay it. The persistent trace is `session:compacted`.
|
|
5
5
|
if (event.kind === 'session:compacting') {
|
|
6
|
-
emitEphemeral(workspaceId, 'agent:event', event);
|
|
6
|
+
emitEphemeral(workspaceId, 'agent:event', event, agentSessionId);
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
if (event.kind === 'mcp:status' && event.status !== 'error') {
|
|
@@ -131,6 +131,25 @@ const pendingQueue = new Map();
|
|
|
131
131
|
* returns to that status instead of being yanked to `executing`.
|
|
132
132
|
*/
|
|
133
133
|
const preAwaitStatus = new Map();
|
|
134
|
+
/** The lifecycle phase hidden by this controller's temporary compaction. */
|
|
135
|
+
const preCompactionStatus = new Map();
|
|
136
|
+
function finishCompaction(workspaceId, controller) {
|
|
137
|
+
const previous = preCompactionStatus.get(controller);
|
|
138
|
+
preCompactionStatus.delete(controller);
|
|
139
|
+
if (getWs(workspaceId)?.status !== 'compacting')
|
|
140
|
+
return;
|
|
141
|
+
const restored = peekPending(workspaceId)
|
|
142
|
+
? 'awaiting-user'
|
|
143
|
+
: previous === 'awaiting-user'
|
|
144
|
+
? consumePreAwaitStatus(workspaceId)
|
|
145
|
+
: (previous ?? 'executing');
|
|
146
|
+
updateWorkspaceStatus(workspaceId, restored);
|
|
147
|
+
}
|
|
148
|
+
export function assertWorkspaceNotCompacting(workspaceId) {
|
|
149
|
+
if (getWs(workspaceId)?.status === 'compacting') {
|
|
150
|
+
throw new Error('Workspace is compacting its context; wait until compaction finishes before sending a message');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
134
153
|
function enqueuePending(workspaceId, item) {
|
|
135
154
|
const arr = pendingQueue.get(workspaceId) ?? [];
|
|
136
155
|
arr.push(item);
|
|
@@ -198,6 +217,13 @@ function rememberPreAwaitStatus(workspaceId) {
|
|
|
198
217
|
return;
|
|
199
218
|
if (ws.status === 'awaiting-user')
|
|
200
219
|
return;
|
|
220
|
+
if (ws.status === 'compacting') {
|
|
221
|
+
const controller = controllers.get(workspaceId);
|
|
222
|
+
const previous = controller && preCompactionStatus.get(controller);
|
|
223
|
+
if (previous && previous !== 'awaiting-user')
|
|
224
|
+
preAwaitStatus.set(workspaceId, previous);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
201
227
|
preAwaitStatus.set(workspaceId, ws.status);
|
|
202
228
|
}
|
|
203
229
|
/**
|
|
@@ -298,6 +324,7 @@ function runWatchdog() {
|
|
|
298
324
|
controllers.delete(workspaceId);
|
|
299
325
|
notifyCapacityAvailable(workspaceId);
|
|
300
326
|
}
|
|
327
|
+
preCompactionStatus.delete(ctrl);
|
|
301
328
|
retryCounts.delete(workspaceId);
|
|
302
329
|
// This end never goes through handleEvent → onSessionEnded, so the user's
|
|
303
330
|
// hook would otherwise miss precisely the case it is most useful for.
|
|
@@ -363,7 +390,7 @@ export function reconcileOrphanSessions() {
|
|
|
363
390
|
const db = getDb();
|
|
364
391
|
const result = db
|
|
365
392
|
.prepare(`UPDATE workspaces SET status = 'idle', updated_at = ?
|
|
366
|
-
WHERE status IN ('executing', 'brainstorming', 'extracting', 'awaiting-user')`)
|
|
393
|
+
WHERE status IN ('executing', 'brainstorming', 'extracting', 'awaiting-user', 'compacting')`)
|
|
367
394
|
.run(now);
|
|
368
395
|
if (result.changes > 0) {
|
|
369
396
|
console.log(`[orchestrator] Reconciled ${result.changes} orphan workspace status(es) at boot.`);
|
|
@@ -723,8 +750,24 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev) {
|
|
|
723
750
|
// `awaiting-user`/`executing`. Every branch that does one of those must
|
|
724
751
|
// check this guard.
|
|
725
752
|
const sourceControllerIsStopping = sourceController?.status === 'stopping';
|
|
753
|
+
if (sourceControllerIsStopping && ev.kind === 'session:compacting')
|
|
754
|
+
return;
|
|
726
755
|
const routedEvent = ev.kind === 'session:ended' && (hasReplacement || sourceIsSuperseded) ? { ...ev, superseded: true } : ev;
|
|
727
756
|
routeEvent(workspaceId, agentSessionId, routedEvent);
|
|
757
|
+
if (sourceController && !sourceControllerIsStopping) {
|
|
758
|
+
if (ev.kind === 'session:compacting' && ev.active) {
|
|
759
|
+
const status = getWs(workspaceId)?.status;
|
|
760
|
+
if (status && ['extracting', 'brainstorming', 'executing', 'awaiting-user'].includes(status)) {
|
|
761
|
+
preCompactionStatus.set(sourceController, status);
|
|
762
|
+
updateWorkspaceStatus(workspaceId, 'compacting');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
else if ((ev.kind === 'session:compacting' && !ev.active) ||
|
|
766
|
+
ev.kind === 'session:compacted' ||
|
|
767
|
+
ev.kind === 'error') {
|
|
768
|
+
finishCompaction(workspaceId, sourceController);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
728
771
|
// A fresh tool invocation proves this controller resumed working (for
|
|
729
772
|
// example after an account change). Tool results can still drain after a
|
|
730
773
|
// rejection, so they must never count as recovery.
|
|
@@ -815,7 +858,9 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev) {
|
|
|
815
858
|
// nothing but this status transition (no separate bookkeeping to preserve),
|
|
816
859
|
// so it is entirely behind the guard; the event itself is still
|
|
817
860
|
// persisted/broadcast unconditionally via `routeEvent` above.
|
|
818
|
-
if (ev.kind === 'session:brainstorm-complete' &&
|
|
861
|
+
if (ev.kind === 'session:brainstorm-complete' &&
|
|
862
|
+
!sourceControllerIsStopping &&
|
|
863
|
+
getWs(workspaceId)?.status !== 'compacting') {
|
|
819
864
|
try {
|
|
820
865
|
const ws = getWs(workspaceId);
|
|
821
866
|
if (ws && ws.status !== 'executing') {
|
|
@@ -930,7 +975,8 @@ function handleEvent(workspaceId, agentSessionId, sourceController, ev) {
|
|
|
930
975
|
}
|
|
931
976
|
rememberPreAwaitStatus(workspaceId);
|
|
932
977
|
try {
|
|
933
|
-
|
|
978
|
+
if (getWs(workspaceId)?.status !== 'compacting')
|
|
979
|
+
updateWorkspaceStatus(workspaceId, 'awaiting-user');
|
|
934
980
|
}
|
|
935
981
|
catch (err) {
|
|
936
982
|
console.warn('[orchestrator] Failed to transition to awaiting-user:', err);
|
|
@@ -1007,6 +1053,8 @@ function onSessionEnded(workspaceId, agentSessionId, sourceController, exitCode,
|
|
|
1007
1053
|
}
|
|
1008
1054
|
const registeredController = controllers.get(workspaceId);
|
|
1009
1055
|
const isSuperseded = sourceController !== undefined && registeredController !== undefined && registeredController !== sourceController;
|
|
1056
|
+
if (sourceController)
|
|
1057
|
+
preCompactionStatus.delete(sourceController);
|
|
1010
1058
|
if (isSuperseded)
|
|
1011
1059
|
return false;
|
|
1012
1060
|
// A superseded end (above) is a session that was replaced, not one that
|
|
@@ -1093,6 +1141,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1093
1141
|
const workspace = getWs(workspaceId);
|
|
1094
1142
|
if (!workspace)
|
|
1095
1143
|
throw new Error(`Workspace '${workspaceId}' not found`);
|
|
1144
|
+
assertWorkspaceNotCompacting(workspaceId);
|
|
1096
1145
|
if (workspace.archivedAt)
|
|
1097
1146
|
throw new Error(`Workspace '${workspaceId}' is archived`);
|
|
1098
1147
|
if (workspace.worktreePurgedAt)
|
|
@@ -1272,6 +1321,7 @@ export function interruptAgent(workspaceId, options = {}) {
|
|
|
1272
1321
|
export const STOP_AGENT_TIMEOUT_MS = 15_000;
|
|
1273
1322
|
async function stopController(workspaceId, ctrl, cause = 'user') {
|
|
1274
1323
|
ctrl.stopCause = cause;
|
|
1324
|
+
preCompactionStatus.delete(ctrl);
|
|
1275
1325
|
// Normalize the state synchronously so callers (archive, delete, manual
|
|
1276
1326
|
// stop) see a clean workspace immediately — without waiting for the async
|
|
1277
1327
|
// controller.stop() → session:ended round-trip, which onSessionEnded skips
|
|
@@ -1288,9 +1338,9 @@ async function stopController(workspaceId, ctrl, cause = 'user') {
|
|
|
1288
1338
|
// Steps 1-2 are `awaiting-user`-specific: they only make sense when the
|
|
1289
1339
|
// session actually left a pending question/permission behind.
|
|
1290
1340
|
const wsBefore = getWs(workspaceId);
|
|
1291
|
-
const activeStatuses = ['executing', 'brainstorming', 'extracting', 'awaiting-user'];
|
|
1341
|
+
const activeStatuses = ['executing', 'brainstorming', 'extracting', 'awaiting-user', 'compacting'];
|
|
1292
1342
|
if (wsBefore && activeStatuses.includes(wsBefore.status)) {
|
|
1293
|
-
if (wsBefore.status === 'awaiting-user') {
|
|
1343
|
+
if (wsBefore.status === 'awaiting-user' || wsBefore.status === 'compacting') {
|
|
1294
1344
|
clearPendingForSession(workspaceId, ctrl.agentSessionId);
|
|
1295
1345
|
purgeAllPersistedUserInputRequests(workspaceId, ctrl.agentSessionId);
|
|
1296
1346
|
}
|
|
@@ -1432,7 +1482,7 @@ export function sendWakeupIfWaiting(workspaceId, content, expectedSessionId) {
|
|
|
1432
1482
|
const workspace = getWs(workspaceId);
|
|
1433
1483
|
if (!workspace || workspace.archivedAt || workspace.worktreePurgedAt)
|
|
1434
1484
|
return false;
|
|
1435
|
-
if (workspace.status === 'awaiting-user' || workspace.status === 'quota')
|
|
1485
|
+
if (workspace.status === 'awaiting-user' || workspace.status === 'quota' || workspace.status === 'compacting')
|
|
1436
1486
|
return false;
|
|
1437
1487
|
if (!ctrl.engineProcess?.sendWakeupIfWaiting?.(content))
|
|
1438
1488
|
return false;
|
|
@@ -1441,6 +1491,7 @@ export function sendWakeupIfWaiting(workspaceId, content, expectedSessionId) {
|
|
|
1441
1491
|
}
|
|
1442
1492
|
/** Write a user message to the running agent. */
|
|
1443
1493
|
export async function sendMessage(workspaceId, content, expectedSessionId) {
|
|
1494
|
+
assertWorkspaceNotCompacting(workspaceId);
|
|
1444
1495
|
const ctrl = controllers.get(workspaceId);
|
|
1445
1496
|
if (!ctrl) {
|
|
1446
1497
|
throw new Error(`No agent running for workspace '${workspaceId}'`);
|
|
@@ -1452,15 +1503,17 @@ export async function sendMessage(workspaceId, content, expectedSessionId) {
|
|
|
1452
1503
|
await ctrl.sendMessage(content);
|
|
1453
1504
|
}
|
|
1454
1505
|
export async function sendMessageForFallback(workspaceId, content) {
|
|
1506
|
+
assertWorkspaceNotCompacting(workspaceId);
|
|
1455
1507
|
const capturedController = controllers.get(workspaceId);
|
|
1508
|
+
wakeupService.cancel(workspaceId, 'user-message');
|
|
1456
1509
|
if (!capturedController)
|
|
1457
1510
|
return { status: 'stopped' };
|
|
1458
|
-
wakeupService.cancel(workspaceId, 'user-message');
|
|
1459
1511
|
try {
|
|
1460
1512
|
await capturedController.sendMessage(content);
|
|
1461
1513
|
return { status: 'sent', sessionId: capturedController.agentSessionId };
|
|
1462
1514
|
}
|
|
1463
1515
|
catch {
|
|
1516
|
+
assertWorkspaceNotCompacting(workspaceId);
|
|
1464
1517
|
const deadline = Date.now() + FALLBACK_CONTROLLER_TURNOVER_TIMEOUT_MS;
|
|
1465
1518
|
while (controllers.get(workspaceId) === capturedController) {
|
|
1466
1519
|
const remaining = deadline - Date.now();
|
|
@@ -1474,6 +1527,7 @@ export async function sendMessageForFallback(workspaceId, content) {
|
|
|
1474
1527
|
const replacementController = controllers.get(workspaceId);
|
|
1475
1528
|
if (!replacementController)
|
|
1476
1529
|
return { status: 'stopped' };
|
|
1530
|
+
assertWorkspaceNotCompacting(workspaceId);
|
|
1477
1531
|
await replacementController.sendMessage(content);
|
|
1478
1532
|
return { status: 'sent', sessionId: replacementController.agentSessionId };
|
|
1479
1533
|
}
|
|
@@ -1593,7 +1647,10 @@ export async function answerPendingQuestion(workspaceId, answers, expectedToolCa
|
|
|
1593
1647
|
purgePersistedUserInputRequest(workspaceId, head.toolCallId);
|
|
1594
1648
|
const restoreTo = peekPending(workspaceId) ? 'awaiting-user' : consumePreAwaitStatus(workspaceId);
|
|
1595
1649
|
try {
|
|
1596
|
-
|
|
1650
|
+
if (getWs(workspaceId)?.status === 'compacting')
|
|
1651
|
+
preCompactionStatus.set(ctrl, restoreTo);
|
|
1652
|
+
else
|
|
1653
|
+
updateWorkspaceStatus(workspaceId, restoreTo);
|
|
1597
1654
|
}
|
|
1598
1655
|
catch (err) {
|
|
1599
1656
|
console.warn(`[orchestrator] Failed to transition awaiting-user → ${restoreTo}:`, err);
|
|
@@ -1681,7 +1738,10 @@ export async function answerPendingPermission(workspaceId, decision) {
|
|
|
1681
1738
|
purgePersistedUserInputRequest(workspaceId, head.toolCallId);
|
|
1682
1739
|
const restoreTo = peekPending(workspaceId) ? 'awaiting-user' : consumePreAwaitStatus(workspaceId);
|
|
1683
1740
|
try {
|
|
1684
|
-
|
|
1741
|
+
if (getWs(workspaceId)?.status === 'compacting')
|
|
1742
|
+
preCompactionStatus.set(ctrl, restoreTo);
|
|
1743
|
+
else
|
|
1744
|
+
updateWorkspaceStatus(workspaceId, restoreTo);
|
|
1685
1745
|
}
|
|
1686
1746
|
catch (err) {
|
|
1687
1747
|
console.warn(`[orchestrator] Failed to transition awaiting-user → ${restoreTo}:`, err);
|
|
@@ -1739,7 +1799,10 @@ export async function cancelPendingQuestion(workspaceId, reason, expectedToolCal
|
|
|
1739
1799
|
purgePersistedUserInputRequest(workspaceId, head.toolCallId);
|
|
1740
1800
|
const restoreTo = peekPending(workspaceId) ? 'awaiting-user' : consumePreAwaitStatus(workspaceId);
|
|
1741
1801
|
try {
|
|
1742
|
-
|
|
1802
|
+
if (getWs(workspaceId)?.status === 'compacting')
|
|
1803
|
+
preCompactionStatus.set(ctrl, restoreTo);
|
|
1804
|
+
else
|
|
1805
|
+
updateWorkspaceStatus(workspaceId, restoreTo);
|
|
1743
1806
|
}
|
|
1744
1807
|
catch (err) {
|
|
1745
1808
|
console.warn(`[orchestrator] Failed to transition awaiting-user → ${restoreTo}:`, err);
|
|
@@ -131,7 +131,7 @@ export function onSessionEnded(workspaceId, reason, taskProgressDelta) {
|
|
|
131
131
|
return;
|
|
132
132
|
// Don't spawn a competing session while paused on canUseTool — the user
|
|
133
133
|
// will resume the deferred turn explicitly.
|
|
134
|
-
if (row.status === 'awaiting-user')
|
|
134
|
+
if (row.status === 'awaiting-user' || row.status === 'compacting')
|
|
135
135
|
return;
|
|
136
136
|
// A watchdog denotes Kōbō's own forced recovery from a stuck engine stream.
|
|
137
137
|
// The orchestrator owns its bounded backoff/retry path; counting it as a
|
|
@@ -315,7 +315,7 @@ export function resumeWaitingWorkspaces(excluded = new Set()) {
|
|
|
315
315
|
continue;
|
|
316
316
|
// Same guards as spawnNextIteration's own callers: a quota backoff or a
|
|
317
317
|
// pending question owns the next start of that workspace.
|
|
318
|
-
if (row.status === 'awaiting-user' || row.status === 'quota')
|
|
318
|
+
if (row.status === 'awaiting-user' || row.status === 'compacting' || row.status === 'quota')
|
|
319
319
|
continue;
|
|
320
320
|
try {
|
|
321
321
|
spawnNextIteration(row.id);
|
|
@@ -337,7 +337,7 @@ function spawnNextIteration(workspaceId, opts = {}) {
|
|
|
337
337
|
if (orchestrator.hasController(workspaceId))
|
|
338
338
|
return;
|
|
339
339
|
// Same guard as onSessionEnded — never race a deferred-resume start.
|
|
340
|
-
if (row.status === 'awaiting-user')
|
|
340
|
+
if (row.status === 'awaiting-user' || row.status === 'compacting')
|
|
341
341
|
return;
|
|
342
342
|
const task = pickNextTask(workspaceId);
|
|
343
343
|
if (!task) {
|
|
@@ -226,7 +226,7 @@ export async function checkPrStatuses() {
|
|
|
226
226
|
});
|
|
227
227
|
prMergedHook = lifecycleHookService.onPrMerged(ws.id, { prNumber: pr.number, prUrl: pr.url });
|
|
228
228
|
}
|
|
229
|
-
if (['extracting', 'brainstorming', 'executing'].includes(ws.status) || hasController(ws.id)) {
|
|
229
|
+
if (['extracting', 'brainstorming', 'executing', 'compacting'].includes(ws.status) || hasController(ws.id)) {
|
|
230
230
|
// Agent is working — update the cache but skip auto-archive.
|
|
231
231
|
// (The defensive base preservation from the no-base branch doesn't apply here
|
|
232
232
|
// because we ARE in the OPEN→MERGED/CLOSED branch which always has a base.)
|
|
@@ -252,7 +252,9 @@ export async function checkPrStatuses() {
|
|
|
252
252
|
if (stale())
|
|
253
253
|
return;
|
|
254
254
|
const current = getWorkspace(ws.id);
|
|
255
|
-
if (!current ||
|
|
255
|
+
if (!current ||
|
|
256
|
+
['extracting', 'brainstorming', 'executing', 'compacting'].includes(current.status) ||
|
|
257
|
+
hasController(ws.id))
|
|
256
258
|
return;
|
|
257
259
|
try {
|
|
258
260
|
destroyTerminal(ws.id);
|
|
@@ -320,7 +322,7 @@ export async function checkPrStatuses() {
|
|
|
320
322
|
emitEphemeral(ws.id, 'pr:merge-conflict', payload);
|
|
321
323
|
markUnread(ws.id);
|
|
322
324
|
}
|
|
323
|
-
const notBusy = !['extracting', 'brainstorming', 'executing'].includes(ws.status);
|
|
325
|
+
const notBusy = !['extracting', 'brainstorming', 'executing', 'compacting'].includes(ws.status);
|
|
324
326
|
if (notBusy && !prev.readyToMerge && pr.readyToMerge) {
|
|
325
327
|
emitEphemeral(ws.id, 'pr:ready-to-merge', payload);
|
|
326
328
|
markUnread(ws.id);
|
|
@@ -199,11 +199,19 @@ export function emit(workspaceId, type, payload, sessionId) {
|
|
|
199
199
|
* Broadcast an event to subscribed clients WITHOUT persisting to the database.
|
|
200
200
|
* Used for ephemeral status updates (e.g., dev-server status) that don't need replay.
|
|
201
201
|
*/
|
|
202
|
-
export function emitEphemeral(workspaceId, type, payload) {
|
|
202
|
+
export function emitEphemeral(workspaceId, type, payload, sessionId) {
|
|
203
203
|
recordActivity(workspaceId, type, payload);
|
|
204
204
|
const id = nanoid();
|
|
205
205
|
const createdAt = new Date().toISOString();
|
|
206
|
-
const event = {
|
|
206
|
+
const event = {
|
|
207
|
+
id,
|
|
208
|
+
workspaceId,
|
|
209
|
+
type,
|
|
210
|
+
payload,
|
|
211
|
+
createdAt,
|
|
212
|
+
replayable: false,
|
|
213
|
+
...(sessionId ? { sessionId } : {}),
|
|
214
|
+
};
|
|
207
215
|
const message = JSON.stringify(event);
|
|
208
216
|
let sendErrorLogged = false;
|
|
209
217
|
for (const [ws, subs] of clients) {
|
|
@@ -16,10 +16,11 @@ const VALID_TRANSITIONS = {
|
|
|
16
16
|
// workspace with a distinct brainstormModel silently lost its backoff: the
|
|
17
17
|
// transition threw, the error was swallowed, and the very next lines
|
|
18
18
|
// cancelled the timer that had just been armed.
|
|
19
|
-
extracting: ['extracting', 'brainstorming', 'idle', 'error', 'awaiting-user', 'quota'],
|
|
20
|
-
brainstorming: ['executing', 'completed', 'idle', 'error', 'awaiting-user', 'quota'],
|
|
21
|
-
executing: ['completed', 'idle', 'error', 'quota', 'awaiting-user'],
|
|
22
|
-
'awaiting-user': ['executing', 'brainstorming', 'extracting', 'idle', 'error', 'completed', 'quota'],
|
|
19
|
+
extracting: ['compacting', 'extracting', 'brainstorming', 'idle', 'error', 'awaiting-user', 'quota'],
|
|
20
|
+
brainstorming: ['compacting', 'executing', 'completed', 'idle', 'error', 'awaiting-user', 'quota'],
|
|
21
|
+
executing: ['compacting', 'completed', 'idle', 'error', 'quota', 'awaiting-user'],
|
|
22
|
+
'awaiting-user': ['compacting', 'executing', 'brainstorming', 'extracting', 'idle', 'error', 'completed', 'quota'],
|
|
23
|
+
compacting: ['extracting', 'brainstorming', 'executing', 'awaiting-user', 'completed', 'idle', 'error', 'quota'],
|
|
23
24
|
completed: ['idle', 'executing'],
|
|
24
25
|
idle: ['executing', 'brainstorming', 'extracting', 'error'],
|
|
25
26
|
error: ['idle', 'executing', 'brainstorming', 'extracting'],
|
|
@@ -196,6 +197,9 @@ export function updateWorkspaceStatus(id, status) {
|
|
|
196
197
|
}
|
|
197
198
|
const now = new Date().toISOString();
|
|
198
199
|
db.prepare('UPDATE workspaces SET status = ?, updated_at = ? WHERE id = ?').run(status, now, id);
|
|
200
|
+
if (status === 'compacting' || workspace.status === 'compacting') {
|
|
201
|
+
emitEphemeral(id, 'workspace:status', { status });
|
|
202
|
+
}
|
|
199
203
|
return getWorkspace(id);
|
|
200
204
|
}
|
|
201
205
|
const WORKSPACE_NAME_MAX_LENGTH = 200;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.39",
|
|
4
4
|
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"prepublishOnly": "npm run build"
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
71
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.267",
|
|
72
72
|
"@emnapi/core": "^1.11.3",
|
|
73
73
|
"@emnapi/runtime": "^1.11.3",
|
|
74
74
|
"@hono/node-server": "^2.1.0",
|