@loicngr/kobo 2.1.21 → 2.1.23
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 +69 -4
- package/CHANGELOG.md +9 -0
- package/CONFIGURATION.md +112 -2
- package/dist/mcp-server/kobo-tasks-server.js +4 -4
- package/dist/server/index.js +21 -6
- package/dist/server/services/agent/engines/claude-code/engine.js +5 -2
- package/dist/server/services/agent/engines/codex/engine.js +3 -0
- package/dist/server/services/agent/orchestrator.js +2 -0
- package/dist/server/services/agent/turn-liveness.js +1 -1
- package/dist/server/services/dev-server-service.js +41 -13
- package/dist/server/services/lifecycle-hook-service.js +29 -2
- package/dist/server/services/settings-service.js +66 -0
- package/dist/server/utils/script-runner.js +3 -1
- package/dist/shared/launch-instructions.js +8 -4
- package/dist/shared/memory-tools.js +2 -2
- package/docker-compose.vps.yml +1 -1
- package/docs/deployment-vps.md +36 -10
- package/docs/getting-started.md +11 -0
- package/package.json +1 -1
- package/scripts/kobo-update +25 -13
- package/src/client/dist/pwa/assets/ActivityFeed-1OKX_g3B.js +2 -0
- package/src/client/dist/pwa/assets/{ActivityFeed-DdJvMzRI.css → ActivityFeed-CmAFv-2c.css} +1 -1
- package/src/client/dist/pwa/assets/{AutoLoopFinalReviewSettings-f4VDR5dd.js → AutoLoopFinalReviewSettings-VoA9uBw0.js} +1 -1
- package/src/client/dist/pwa/assets/{ChangelogPage-dEWpjTlp.js → ChangelogPage-DHl3hD3t.js} +1 -1
- package/src/client/dist/pwa/assets/{ComparisonPanel-Bwbqgvr4.js → ComparisonPanel-D6EKTtni.js} +1 -1
- package/src/client/dist/pwa/assets/{CreatePage-CCCCAhrx.js → CreatePage-CZzEOVVq.js} +1 -1
- package/src/client/dist/pwa/assets/{DashboardPage-3byhBITf.js → DashboardPage-DjLoUvjA.js} +1 -1
- package/src/client/dist/pwa/assets/{DiffViewer-Bj_NTfCF.js → DiffViewer-BQ6OEmoi.js} +3 -3
- package/src/client/dist/pwa/assets/{DocumentsPanel-xyLDV3T1.js → DocumentsPanel-yrQviqCd.js} +1 -1
- package/src/client/dist/pwa/assets/{DrawerToggleButton-pwBSstQP.js → DrawerToggleButton-D5qNeXnn.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-DvIdrkVw.js → GitPanel-DCXlhcAD.js} +3 -3
- package/src/client/dist/pwa/assets/{HealthPage-BSwhZQRo.js → HealthPage-COicuNQn.js} +1 -1
- package/src/client/dist/pwa/assets/{MainLayout-DZJ9BvZw.js → MainLayout-ZcExZTqC.js} +2 -2
- package/src/client/dist/pwa/assets/{MemoryPanel-ChR032zP.js → MemoryPanel-D73Qz9f7.js} +1 -1
- package/src/client/dist/pwa/assets/{MemoryProposalList-BRKSoS4K.js → MemoryProposalList-BGlq2DkD.js} +1 -1
- package/src/client/dist/pwa/assets/{QBtnDropdown-o3QH0Bb8.js → QBtnDropdown-CykBCmwa.js} +1 -1
- package/src/client/dist/pwa/assets/{QExpansionItem-BhFHiyq9.js → QExpansionItem-CAW9J1Uv.js} +1 -1
- package/src/client/dist/pwa/assets/{QScrollArea-BsNY6slB.js → QScrollArea-C_d-8eO7.js} +1 -1
- package/src/client/dist/pwa/assets/{QSelect-bEq3g_io.js → QSelect-C1AN93yA.js} +1 -1
- package/src/client/dist/pwa/assets/{QTooltip-xYZ6KNu2.js → QTooltip-C9qaLW_h.js} +1 -1
- package/src/client/dist/pwa/assets/{SchedulePanel-DIFv_FTY.js → SchedulePanel-CQpzDhq7.js} +1 -1
- package/src/client/dist/pwa/assets/{SearchPage-BqTBIt5E.js → SearchPage-fZlSSEu_.js} +1 -1
- package/src/client/dist/pwa/assets/{SettingsPage-BRvUAZ5r.css → SettingsPage-BCOF0ssp.css} +1 -1
- package/src/client/dist/pwa/assets/SettingsPage-eyJZxeCJ.js +9 -0
- package/src/client/dist/pwa/assets/{SplitWorkspacePage-D6r2pl5g.js → SplitWorkspacePage-CfeO6TZb.js} +1 -1
- package/src/client/dist/pwa/assets/{TerminalPanel-CladK9Vv.js → TerminalPanel-CfDZOteT.js} +1 -1
- package/src/client/dist/pwa/assets/{ToolCallItem-CdzEME4w.js → ToolCallItem-CHE6tylq.js} +2 -2
- package/src/client/dist/pwa/assets/{TouchPan-B4gQPwGN.js → TouchPan-Csev_qU6.js} +1 -1
- package/src/client/dist/pwa/assets/{TourReplayButton-B0j6AZwU.js → TourReplayButton-Crr7tF1b.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-CeVhoidC.js → WorkspaceAttentionLabels-DpUZOiJv.js} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-w2bF8wTd.css → WorkspacePage-Bk-WWOYM.css} +1 -1
- package/src/client/dist/pwa/assets/{WorkspacePage-CK5XCuG_.js → WorkspacePage-_QSKLO42.js} +3 -3
- package/src/client/dist/pwa/assets/{browser-B8tNScg1.js → browser-o7ftkZex.js} +1 -1
- package/src/client/dist/pwa/assets/{build-path-tree-kmVAcvs_.js → build-path-tree-D6dZQpIb.js} +1 -1
- package/src/client/dist/pwa/assets/{custom-sounds-CJwd-8TL.js → custom-sounds-DVX9zCET.js} +1 -1
- package/src/client/dist/pwa/assets/{de-BaO6kbdO.js → de-DOE4H_Jf.js} +2 -2
- package/src/client/dist/pwa/assets/{editor.api-CgbPVW9s.js → editor.api-C8TYnUXm.js} +1 -1
- package/src/client/dist/pwa/assets/{efforts-y7Wf_Rgd.js → efforts-DfsG0_o0.js} +1 -1
- package/src/client/dist/pwa/assets/{es-DTvSsbD0.js → es-BW57JPlm.js} +2 -2
- package/src/client/dist/pwa/assets/{expand-template-BFbbjj7X.js → expand-template-CJZlWMfI.js} +1 -1
- package/src/client/dist/pwa/assets/{format-BETfc4z9.js → format-DOCqk2Dn.js} +1 -1
- package/src/client/dist/pwa/assets/{formatters-C22K9vlC.js → formatters-BFocC6LJ.js} +1 -1
- package/src/client/dist/pwa/assets/{fr-BH7V66Yz.js → fr-DnmIKHaQ.js} +2 -2
- package/src/client/dist/pwa/assets/{freemarker2-DiRVvp9O.js → freemarker2-CfME4WOK.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BLYRroSQ.js → handlebars-uvKdDnJl.js} +1 -1
- package/src/client/dist/pwa/assets/{html-DAW2vteY.js → html-BrEPgLuD.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-BmTCjikk.js +1 -0
- package/src/client/dist/pwa/assets/{index-CbyHfF8I.js → index-z1Oh2-I_.js} +4 -4
- package/src/client/dist/pwa/assets/{it-DZGbkkoy.js → it-DSrEQgZQ.js} +2 -2
- package/src/client/dist/pwa/assets/{javascript-Bb5J8n0k.js → javascript-KH2xP4lQ.js} +1 -1
- package/src/client/dist/pwa/assets/{liquid-Dqx3adnJ.js → liquid-D08TlsVs.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-D_7igLV3.js → mdx-y74A51cS.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-C-0CbrAy.js → monaco-contributions-Cx8iaAhd.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco.contribution-c6JPBEZI.js → monaco.contribution-CFq0guCl.js} +2 -2
- package/src/client/dist/pwa/assets/{network-auth-BE4qbvWm.js → network-auth-DnGOOzCV.js} +1 -1
- package/src/client/dist/pwa/assets/{notify-theme-BVFBBGrJ.js → notify-theme-B9lUAgQl.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-CHOR2jB5.js → project-color-TgENPFLi.js} +1 -1
- package/src/client/dist/pwa/assets/{python-Dqubw1F5.js → python-Cy3bAVPq.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-BQphc9Kt.js → razor-gD9QOuiV.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-Dep00l0X.js → typescript-CEItY0bH.js} +1 -1
- package/src/client/dist/pwa/assets/{use-search-index-status-CQHHvHwM.js → use-search-index-status-BtJ0pOy9.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-IVbXTmz2.js → use-tours-COLDth2h.js} +2 -2
- package/src/client/dist/pwa/assets/{use-worktree-restore-B3s5xDGR.js → use-worktree-restore-CShbwpGy.js} +1 -1
- package/src/client/dist/pwa/assets/{workspace-templates-CyuTpztP.js → workspace-templates-BOMNQgBl.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-BMS2dvaF.js → xml-BJvL5qcE.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-CYw92aic.js → yaml-DtkVRZaX.js} +1 -1
- package/src/client/dist/pwa/index.html +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/README.md +13 -0
- package/src/mcp-server/kobo-tasks-server.ts +5 -4
- package/src/client/dist/pwa/assets/ActivityFeed-DiUxnmGJ.js +0 -2
- package/src/client/dist/pwa/assets/SettingsPage-BLD0xWFB.js +0 -9
- package/src/client/dist/pwa/assets/i18n-DnuTmw3O.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -201,6 +201,10 @@ Every feature that touches the schema:
|
|
|
201
201
|
|
|
202
202
|
## WebSocket protocol
|
|
203
203
|
|
|
204
|
+
Global `showProgressBars` defaults to true (settings migration v65). The panel
|
|
205
|
+
honors this preference without resetting its store or dismissing rows: MCP writes,
|
|
206
|
+
revisions and success expiry continue while hidden.
|
|
207
|
+
|
|
204
208
|
Session progress bars use the separate `progress:changed` ephemeral event with a
|
|
205
209
|
public record payload and workspace/session envelope. Persist the row before
|
|
206
210
|
broadcasting; never append numeric updates to chat history or overload native
|
|
@@ -233,7 +237,7 @@ locally queued or unacknowledged messages still block `turn:completed` and
|
|
|
233
237
|
input closure. UUID-less legacy results retain a conservative one-input
|
|
234
238
|
fallback for already yielded inputs only; a result never credits a future send.
|
|
235
239
|
Backpressure caps unacknowledged yielded inputs at 64, matching the SDK's result
|
|
236
|
-
UUID list bound. Keep the inactivity watchdog for genuinely unanswered work.
|
|
240
|
+
UUID list bound. Keep the opt-in inactivity watchdog for genuinely unanswered work.
|
|
237
241
|
|
|
238
242
|
Claude SDK `task_*` messages cover agents **and** background jobs. Preserve `task_type` by task/tool identity across progress and terminal messages, which often omit it. The existing `subagent:progress` envelope also carries this lifecycle metadata for compatibility; `local_bash`, `local_workflow` and `mcp_task` must be excluded from agent cards, busy-agent counts, chat agent-name resolution and the selective stop-subagents action. Keep tracking these jobs for engine liveness and whole-session interruption. Legacy/unknown classifications and Codex agents remain supported; never classify from a description or from the agent's last tool (`Bash` can be a real agent's current action).
|
|
239
243
|
|
|
@@ -275,6 +279,26 @@ Two emit flavors in `websocket-service.ts`:
|
|
|
275
279
|
|
|
276
280
|
## External integrations
|
|
277
281
|
|
|
282
|
+
### Graceful shutdown and development services
|
|
283
|
+
|
|
284
|
+
`stopDevServersOnShutdown` is a global boolean, default `true` (JSON settings
|
|
285
|
+
migration v66). Shutdown first rejects new dev-server starts and stops tracked
|
|
286
|
+
local processes/agents. Disabling the setting skips configured dev stop commands
|
|
287
|
+
and Compose teardown, preserving independent Docker services; manual Stop and
|
|
288
|
+
workspace deletion/purge remain unchanged. Status recovery uses the existing
|
|
289
|
+
instance-file and exact Compose-label inspection, never a duplicate startup.
|
|
290
|
+
|
|
291
|
+
`shutdownScript` follows the global/project script cascade: empty inherits,
|
|
292
|
+
whitespace disables. The `kobo-shutdown` lifecycle hook runs once per persisted
|
|
293
|
+
workspace with an existing, non-purged worktree (including archived workspaces),
|
|
294
|
+
after local writers stop and before optional Docker teardown. Skip unconfirmed
|
|
295
|
+
agent stops. Each hook has a 1-minute timeout, and the outer shutdown deadline
|
|
296
|
+
is 150 seconds (supervisors need at least 180 seconds). Inject `PROJECT_NAME` from
|
|
297
|
+
the workspace instance config, empty if unavailable, and `INSTANCE`; never guess
|
|
298
|
+
a Compose project. User hooks may explicitly remove Docker resources even when
|
|
299
|
+
automatic shutdown is disabled. Preserve hook outputs and keep SQLite open until
|
|
300
|
+
hooks settle. Do not run scripts on abrupt exits or claim they survive SIGKILL.
|
|
301
|
+
|
|
278
302
|
### VPS deployment tooling
|
|
279
303
|
|
|
280
304
|
`docker-compose.vps.yml` is the standalone fresh-instance example for an existing
|
|
@@ -315,13 +339,20 @@ credentials into an image or add web settings for these host-managed secrets.
|
|
|
315
339
|
|
|
316
340
|
`scripts/kobo-update` is a Python 3.10+ operator tool for this layout, separate
|
|
317
341
|
from the backend's update notifications. Preserve explicit invocation, idle checks,
|
|
318
|
-
unique image tags, private stopped
|
|
342
|
+
unique image tags, private backups of `/root/.config/kobo` while stopped, migration tests on a copy,
|
|
319
343
|
and durable failure records. The authenticated `/api/maintenance/update` reservation
|
|
320
344
|
checks runtime ownership and closes agent admission synchronously before shutdown.
|
|
321
345
|
Track in-flight workspace creations and PR checkouts through all asynchronous
|
|
322
346
|
steps and rollback. Maintenance and creation admission must exclude each other
|
|
323
347
|
synchronously; rejecting only the final agent start is too late. Direct HTTP and
|
|
324
348
|
MCP creation paths share this guard.
|
|
349
|
+
The updater's `kobo.tar.gz` contains only Kōbō data, with paths relative to `/root`;
|
|
350
|
+
never archive engine homes, skills, caches or projects as part of routine updates.
|
|
351
|
+
Record the archive name/scope in update state. Migration checks extract regular
|
|
352
|
+
SQLite files and sidecars only. Recovery must preserve the original volume and
|
|
353
|
+
clone it before replacing only `.config/kobo` in the clone; a partial data archive
|
|
354
|
+
must not be described as a complete home snapshot. Old `home.tar.gz` jobs retain
|
|
355
|
+
their original recovery instructions.
|
|
325
356
|
Scheduled cron/wakeup/quota deliveries defer without consuming persisted intent;
|
|
326
357
|
valid pending wakeups get `retry_at=target_at` before successful reservation (only
|
|
327
358
|
when `retry_at` was null), so long maintenance does not expire them at boot. Keep
|
|
@@ -370,6 +401,33 @@ expand tool permissions. Initial writable auto-loop launches seed tasks and end;
|
|
|
370
401
|
manual launches implement under the workflow policy. Preserve custom templates
|
|
371
402
|
and frozen migration text when changing generated guidance.
|
|
372
403
|
|
|
404
|
+
Prefer dedicated managed MCP operations and require checking their results.
|
|
405
|
+
Writable launches synchronize task progress and blockers during work using only
|
|
406
|
+
supported statuses; restricted launches must not receive task mutation guidance.
|
|
407
|
+
Delegation passes the objective, worktree path, constraints, permissions, test
|
|
408
|
+
runtime and applicable MCP/reporting rules explicitly. Keep final reporting
|
|
409
|
+
evidence-based and phase-aware: distinguish actual checks, delegated results,
|
|
410
|
+
blocked/unverified work and jobs still running. Reuse the shared progress lifecycle
|
|
411
|
+
instead of duplicating it or finishing ongoing work at the end of a turn.
|
|
412
|
+
|
|
413
|
+
Dev-server lifecycle guidance must cover backend/integration dependencies as well
|
|
414
|
+
as frontend/E2E checks. Agents and delegates inspect/reuse the managed instance,
|
|
415
|
+
start/stop only through its MCP tools under writable launch permissions, and
|
|
416
|
+
report unavailable tools/configuration or startup failures instead of bypassing
|
|
417
|
+
Kōbō with direct shell startup commands. Keep tool descriptions in sync.
|
|
418
|
+
Distinguish the agent runtime from project containers, running status from
|
|
419
|
+
application readiness, and interrupted reporting from terminated work. Keep
|
|
420
|
+
readiness probes bounded and do not suggest bypassing failed MCP authentication
|
|
421
|
+
or replaying an ambiguous mutation without checking its outcome.
|
|
422
|
+
|
|
423
|
+
Launch guidance explicitly directs agents to consult injected Kōbō
|
|
424
|
+
memory before targeted retrieval, and to save confirmed reusable facts through
|
|
425
|
+
`remember` only under the current mode/permissions. Preserve retrieval budgets,
|
|
426
|
+
scope selection and Hybrid proposal semantics; native provider memory is not a
|
|
427
|
+
replacement for this shared store.
|
|
428
|
+
Keep detailed workflow guidance in the application launch instructions: the
|
|
429
|
+
separate mode-specific memory bootstrap has a strict total retrieval budget.
|
|
430
|
+
|
|
373
431
|
The engine's `onPromptPrepared` callback supplies only its textual Kōbō prefix and
|
|
374
432
|
system append. Initial creation and pending initial retries opt into a single
|
|
375
433
|
session-scoped `user:message` with `sender: system-prompt`, displayed as Kōbō
|
|
@@ -387,11 +445,18 @@ through `ExitPlanMode`; a permission change requires a new launch.
|
|
|
387
445
|
|
|
388
446
|
Two engines live under `src/server/services/agent/engines/`, both implementing the `AgentEngine` contract in `types.ts`:
|
|
389
447
|
|
|
390
|
-
|
|
448
|
+
Global `agentWatchdogEnabled` is opt-in (false for fresh and existing installs;
|
|
449
|
+
settings migration v64). The orchestrator snapshots it into `StartOptions.watchdogEnabled`
|
|
450
|
+
on every launch/resume for both engines. Gate stream/tool inactivity, background
|
|
451
|
+
stall, compaction and failed-result-drain watchdogs; retain normal completion,
|
|
452
|
+
manual stop, protocol/startup deadlines and confirmed-dead-process cleanup.
|
|
453
|
+
The toggle is first in Settings → Agents and does not reconfigure active launches.
|
|
454
|
+
|
|
455
|
+
**Claude Code** (`claude-code/`): consumes the `@anthropic-ai/claude-agent-sdk` async iterator. The SDK manages the Claude runtime; Kōbō does not launch a separate `claude` CLI command. Authentication reuses the user's Claude login or `ANTHROPIC_API_KEY`. A settled result gets a **3 s continuation grace**, then an opt-in **15 s result-drain watchdog**. Trailing metadata and child output must not cancel the parent's drain/grace. When watchdogs are enabled, silence is bounded at **2 minutes** without tools, **30 minutes** for foreground tools and background shell/workflow/MCP jobs, and **10 minutes** for agents. Compaction has an absolute **10-minute** ceiling; repeated status messages cannot extend it. Human questions/permissions pause inactivity; resolution AND SDK cancellation re-evaluate that pause. Watchdogs request abort plus SDK `query.close()`; neither is proof of closure: controller ownership is released only when `EngineProcess.closed` confirms it. Never fake closure or allow a second writer if the SDK ignores termination. End emission is idempotent and timers are cleared on termination and in `finally`.
|
|
391
456
|
|
|
392
457
|
After the continuation grace, Claude explicitly calls `query.close()`; closing
|
|
393
458
|
the input iterator alone can leave the SDK waiting for an idle notification after
|
|
394
|
-
a successful result. The 15-second drain watchdog is a failed-close backstop,
|
|
459
|
+
a successful result. The opt-in 15-second drain watchdog is a failed-close backstop,
|
|
395
460
|
not normal cleanup. The SDK still constructs its runtime command; Kōbō's
|
|
396
461
|
`spawnClaudeCodeProcess` hook tracks actual child exit and forwards stderr.
|
|
397
462
|
`EngineProcess.closed` waits for both that exit and iterator completion because
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,15 @@ 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
|
+
## 2.1.23
|
|
8
|
+
|
|
9
|
+
- feat: configure shutdown hooks and improve agent guidance
|
|
10
|
+
|
|
11
|
+
## 2.1.22
|
|
12
|
+
|
|
13
|
+
- feat(settings): make agent watchdogs opt-in and progress bars configurable
|
|
14
|
+
- feat(update): refine `kobo-update` to backup only `.config/kobo` and improve migration handling
|
|
15
|
+
|
|
7
16
|
## 2.1.21
|
|
8
17
|
|
|
9
18
|
- feat(vps): introduce non-root runtime launcher and UID/GID support
|
package/CONFIGURATION.md
CHANGED
|
@@ -234,6 +234,8 @@ Projects override a subset of global settings. Anything you set here takes prece
|
|
|
234
234
|
| `taskPromptTemplate` | `string` | Prompt used to prefill the mission description when this project is selected. Empty leaves the description without a project-provided prompt. |
|
|
235
235
|
| `forge` | `'auto' \| 'github' \| 'gitlab' \| 'bitbucket-community' \| 'none'` | Which forge provides PR/MR features. `auto` detects from the git remote URL. See [Forge integration](#forge-integration). |
|
|
236
236
|
| `devServer.startCommand` / `stopCommand` | `string` | Per-workspace dev server commands. Docker, npm, or any shell-startable process. See [Dev server](#dev-server) for the status/URL contract. |
|
|
237
|
+
| `stopDevServersOnShutdown` | `boolean` | Global, default `true`. Disable to leave Docker services running when Kōbō exits or updates. Tracked local processes always stop. Explicit Stop and workspace teardown still stop services. |
|
|
238
|
+
| `shutdownScript` | `string` | Optional global/per-project shutdown hook, run in each existing workspace worktree before automatic Docker teardown. Empty project value inherits; whitespace disables. See [Kōbō shutdown](#kōbō-shutdown). |
|
|
237
239
|
| `e2e.framework` | `'cypress' \| 'playwright' \| 'jest' \| 'vitest' \| 'other' \| ''` | E2E framework auto-loop grooming should target. |
|
|
238
240
|
| `e2e.skill` | `string` | Optional skill name injected into the E2E grooming prompt. |
|
|
239
241
|
| `e2e.prompt` | `string` | Free-form prompt appended to every `[E2E] ` sub-task. |
|
|
@@ -859,6 +861,12 @@ External senders can supply `idempotency_key` once per intended message and reus
|
|
|
859
861
|
|
|
860
862
|
## Session progress bars
|
|
861
863
|
|
|
864
|
+
**Settings → Agents → Show progress bars** (`showProgressBars`) is enabled by
|
|
865
|
+
default. Saving this global preference hides or restores the panel immediately in
|
|
866
|
+
the current client; other clients receive it on their next settings refresh.
|
|
867
|
+
Hiding the panel does not stop reporting, dismiss activities or reset their
|
|
868
|
+
success expiry timer. Individually dismissed bars stay dismissed when re-enabled.
|
|
869
|
+
|
|
862
870
|
Agents can report long-running tests, builds or reviews through the
|
|
863
871
|
`create_progress_bar`, `update_progress_bar`, `finish_progress_bar` and
|
|
864
872
|
`list_progress_bars` MCP tools. These tools are available through the workspace
|
|
@@ -1365,6 +1373,9 @@ For the independent VPS layout, [`scripts/kobo-update`](./scripts/kobo-update)
|
|
|
1365
1373
|
accepts a stable version or defaults to npm `latest`. It has `--check`, `--prepare`
|
|
1366
1374
|
and `--directory` options. See [updater usage and recovery](./docs/deployment-vps.md#update-with-kobo-update).
|
|
1367
1375
|
This is an operator-invoked script; the in-app update banner does not install releases.
|
|
1376
|
+
Its `kobo.tar.gz` backup contains only `/root/.config/kobo` (database, settings and
|
|
1377
|
+
other Kōbō data). The rest of the persistent home, including engine credentials,
|
|
1378
|
+
skills and caches, is preserved in place but is not included in this backup.
|
|
1368
1379
|
|
|
1369
1380
|
A source-built image needs rebuilding for a new release; `docker compose pull`
|
|
1370
1381
|
alone cannot update an image that only exists locally. Recreate the service
|
|
@@ -1416,15 +1427,27 @@ The model selector is populated from [`src/shared/models.ts`](./src/shared/model
|
|
|
1416
1427
|
|
|
1417
1428
|
#### Session watchdogs
|
|
1418
1429
|
|
|
1430
|
+
**Settings → Agents → Enable agent inactivity watchdogs**, at the top of the tab,
|
|
1431
|
+
controls the global `agentWatchdogEnabled` boolean. It is **off by default**, for
|
|
1432
|
+
both new and existing installations. Each Claude/Codex launch or resume captures
|
|
1433
|
+
the current value; saving the toggle does not interrupt or reconfigure an already
|
|
1434
|
+
running agent. Stop and resume that agent to apply the new value.
|
|
1435
|
+
|
|
1436
|
+
When off, Kōbō does not force-close a session because of stream/tool silence,
|
|
1437
|
+
background agents/jobs, stalled compaction, or a stalled result drain. Long silent
|
|
1438
|
+
CI monitoring can continue. Normal completion, manual Stop, provider failures,
|
|
1439
|
+
protocol request/startup deadlines and cleanup of confirmed dead processes still
|
|
1440
|
+
work; this switch only controls the engine watchdog deadlines described below.
|
|
1441
|
+
|
|
1419
1442
|
Kōbō distinguishes a finished response from a fully closed SDK session. Once a
|
|
1420
1443
|
response is settled, it allows 3 seconds for continuation, then explicitly closes
|
|
1421
1444
|
the SDK query. Normal cleanup is reported as completion, not a watchdog failure.
|
|
1422
|
-
|
|
1445
|
+
When watchdogs are enabled, a 15-second drain watchdog remains if that close fails to end the iterator.
|
|
1423
1446
|
Metadata alone cannot extend this cleanup deadline. Writer ownership waits for
|
|
1424
1447
|
both iterator completion and confirmed runtime exit, even if the SDK's own
|
|
1425
1448
|
bounded cleanup returns earlier.
|
|
1426
1449
|
|
|
1427
|
-
|
|
1450
|
+
With watchdogs enabled, silence limits are 2 minutes without a pending tool, 10 minutes for background
|
|
1428
1451
|
agents, and 30 minutes for tools (including background Bash, workflow and MCP
|
|
1429
1452
|
jobs). A compaction has a separate absolute 10-minute limit. Waiting for your
|
|
1430
1453
|
answer or permission pauses the inactivity watchdog.
|
|
@@ -1576,6 +1599,48 @@ logs contract does not provide general process health monitoring.
|
|
|
1576
1599
|
**No instance file at all**: Start/Stop run your commands; the panel uses the
|
|
1577
1600
|
saved status when available, otherwise `unknown`, with no URL or Docker logs.
|
|
1578
1601
|
|
|
1602
|
+
### Kōbō shutdown
|
|
1603
|
+
|
|
1604
|
+
In **Settings → Scripts**, **Stop development servers when Kōbō exits** defaults
|
|
1605
|
+
to enabled. Disable it to preserve independently running Docker containers during
|
|
1606
|
+
an update/restart. Kōbō still terminates its tracked local processes, including
|
|
1607
|
+
startup commands. Use detached Docker services (`docker compose up -d`); a
|
|
1608
|
+
foreground Compose process or a custom signal trap can itself stop containers.
|
|
1609
|
+
After restart, the existing instance-file/Compose-label contract lets the panel
|
|
1610
|
+
inspect those containers and recover their URL and logs without starting them.
|
|
1611
|
+
The setting does not change manual Stop, workspace deletion/purge, or user hooks.
|
|
1612
|
+
|
|
1613
|
+
The optional **Hook: Kōbō shutdown** uses the global script by default. A project
|
|
1614
|
+
script replaces it; an empty project field inherits and whitespace disables it.
|
|
1615
|
+
It runs **once per workspace, in that workspace's worktree**, including archived
|
|
1616
|
+
workspaces with an existing checkout. Purged/missing worktrees and workspaces
|
|
1617
|
+
whose agent stop is unconfirmed are skipped. Scripts run after tracked agent/local
|
|
1618
|
+
process shutdown and before automatic Docker teardown, even when automatic Docker
|
|
1619
|
+
shutdown is disabled. Each script has a **1-minute timeout**; failures are logged
|
|
1620
|
+
and do not cancel shutdown. Output is recorded under `hook:kobo-shutdown`.
|
|
1621
|
+
|
|
1622
|
+
Available variables include `WORKSPACE_ID`, `WORKSPACE_NAME`, `BRANCH_NAME`,
|
|
1623
|
+
`SOURCE_BRANCH`, `PROJECT_PATH`, `KOBO_HOOK_EVENT=kobo-shutdown`, and
|
|
1624
|
+
`KOBO_SHUTDOWN_SIGNAL` (`SIGTERM`, `SIGINT`, etc.). `INSTANCE` and `PROJECT_NAME`
|
|
1625
|
+
come from that workspace's dev-server instance configuration. `PROJECT_NAME` is
|
|
1626
|
+
empty when no matching Docker instance is known; never guess a shared stack name.
|
|
1627
|
+
|
|
1628
|
+
For explicit destructive cleanup of the workspace's Compose resources:
|
|
1629
|
+
|
|
1630
|
+
```bash
|
|
1631
|
+
docker compose -p "${PROJECT_NAME:?Missing Docker project}" down --remove-orphans --volumes --rmi local
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
This removes Compose-managed volumes and images without custom tags; external
|
|
1635
|
+
volumes and tagged images remain. It can stop containers **even with automatic
|
|
1636
|
+
Docker shutdown disabled**, because the hook is an explicit user command. Supply
|
|
1637
|
+
any `-f` or `--env-file` arguments your project's Compose setup requires.
|
|
1638
|
+
|
|
1639
|
+
These hooks require a graceful Kōbō shutdown; they cannot run after `SIGKILL`,
|
|
1640
|
+
a host power loss or a crash that bypasses teardown. Kōbō's total shutdown deadline
|
|
1641
|
+
is 150 seconds. Allow at least 180 seconds in a container/process supervisor
|
|
1642
|
+
(`stop_grace_period: 180s` in the Compose examples).
|
|
1643
|
+
|
|
1579
1644
|
### Example: the full convention end-to-end
|
|
1580
1645
|
|
|
1581
1646
|
The reference implementation is the `sekur` project. Its Kōbō `startCommand` is effectively `make dev-docker`, where the `Makefile` calls `./sh/dev-docker.sh $(INSTANCE)` and `$(INSTANCE)` reads the `INSTANCE` env var Kōbō injects. The script:
|
|
@@ -2163,6 +2228,51 @@ Notion token environment overrides are applied consistently to both token variab
|
|
|
2163
2228
|
|
|
2164
2229
|
New workspaces receive the mission, repository context, current permissions, current phase and expected result. Agents first consult the available managed MCP tool descriptions and schemas, then prefer relevant Kōbō tools for workspace operations. This is not a request to call every tool or poll. Long tasks and delegated work use labeled progress bars; dismissing a bar hides only that activity and does not cancel its work.
|
|
2165
2230
|
|
|
2231
|
+
For tasks, development services, shared memory, workspace messages and state,
|
|
2232
|
+
agents prefer the dedicated managed MCP tool and check its result. Writable
|
|
2233
|
+
launches keep existing tasks current during work: `in_progress` when starting,
|
|
2234
|
+
blockers promptly reported in the conversation with the affected task, and done
|
|
2235
|
+
only after required checks pass. Task requirements and evidence are preserved.
|
|
2236
|
+
Blocked tasks stay unfinished; no unsupported field or status is invented.
|
|
2237
|
+
Delegates receive the objective, exact worktree path, user constraints, launch
|
|
2238
|
+
permissions, test runtime and applicable MCP/reporting rules. When a delegate
|
|
2239
|
+
lacks the tools, its parent coordinates the operation under the same permissions.
|
|
2240
|
+
The final report identifies changes/findings, checks actually executed and their
|
|
2241
|
+
results, and remaining blockers or ongoing work. Delegated results are attributed;
|
|
2242
|
+
progress bars finish only for activities with a known outcome, and completing a
|
|
2243
|
+
phase or review does not imply the whole mission is done.
|
|
2244
|
+
|
|
2245
|
+
Agents explicitly use Kōbō internal memory for shared knowledge across sessions,
|
|
2246
|
+
workspaces and engines. At task start/resume, they consult injected facts before
|
|
2247
|
+
targeted retrieval of missing decisions, conventions and known pitfalls. When
|
|
2248
|
+
permitted, they save confirmed reusable facts through `remember` in the narrowest
|
|
2249
|
+
scope, updating existing entries rather than duplicating them. Native provider
|
|
2250
|
+
memory does not replace this shared store. Retrieval budgets, Manual/read-only
|
|
2251
|
+
restrictions and Hybrid approval requirements still apply; secrets, raw logs and
|
|
2252
|
+
routine progress are not memory entries.
|
|
2253
|
+
|
|
2254
|
+
Development services have an explicit managed lifecycle rule: before backend,
|
|
2255
|
+
frontend, integration or E2E checks that need workspace services, inspect
|
|
2256
|
+
`get_dev_server_status` and reuse the existing instance. Authorized writable
|
|
2257
|
+
launches start/stop it through the Kōbō MCP tools, never by invoking the startup
|
|
2258
|
+
script, `make dev-docker` or Docker Compose directly. Diagnose failures with
|
|
2259
|
+
`get_dev_server_logs`; unavailable tools or configuration are reported as blockers.
|
|
2260
|
+
Delegates follow the same rule. Read-only launches cannot start/stop services;
|
|
2261
|
+
test execution inside an existing container still follows current permissions.
|
|
2262
|
+
|
|
2263
|
+
Agents choose test commands and runtimes from the project Makefile, scripts and
|
|
2264
|
+
documentation. Missing dependencies in the agent environment do not establish a
|
|
2265
|
+
project configuration defect when tests belong in a service container. A running
|
|
2266
|
+
dev server is not proof of application readiness: dependent checks require a
|
|
2267
|
+
documented health check or non-mutating URL probe with a timeout and bounded
|
|
2268
|
+
retries. Independent unit tests need not wait for application readiness.
|
|
2269
|
+
|
|
2270
|
+
After interruption/resume, agents verify relevant task/Git state and ongoing
|
|
2271
|
+
jobs, tests and containers before repeating work. MCP failures must be reported
|
|
2272
|
+
as failures, with safe diagnostics; authentication/permission errors never justify
|
|
2273
|
+
modifying credentials or Kōbō storage to bypass the tools. Ambiguous mutation
|
|
2274
|
+
outcomes require checking available status/receipts before retrying.
|
|
2275
|
+
|
|
2166
2276
|
Plan launches remain conversation-only. Writable auto-loop launches prepare the plan and task list before returning control to the loop; writable manual launches implement after plan approval, under the configured Git policy. Custom brainstorming, Notion and Sentry templates are preserved; they cannot expand launch permissions or authorize commits automatically. Review returns and handoff reports remain read-only, with only their scoped report and operational progress tools allowed. Plan approval does not change permissions; explicitly select an execution mode in Kōbō and start a new launch to implement.
|
|
2167
2277
|
|
|
2168
2278
|
The expandable **Kōbō instructions** chat entry records the application-owned text prepared by the engine builder for the initial launch or a pending initial-prompt retry, including progress guidance, continuation rules, workflow policy and managed integration notices. The pending initial mission is retained until this entry is durably recorded, so a failure during asynchronous launch preparation remains retryable. A later replacement mission is never cleared by an earlier launch. Preparation is not proof of provider delivery. The provider's own system preset is not included. Retrieved memory bodies and capability tokens are not copied to chat; memory usage remains visible separately in the Memory panel. Existing historical prompt entries retain their original content. Ordinary resumes receive current instructions without creating another initial-prompt entry.
|
|
@@ -408,25 +408,25 @@ const WORKSPACE_SCOPED_TOOLS = [
|
|
|
408
408
|
},
|
|
409
409
|
{
|
|
410
410
|
name: 'get_dev_server_status',
|
|
411
|
-
description: 'CALL BEFORE asking the user whether the app is running
|
|
411
|
+
description: 'CALL BEFORE backend, frontend, integration or E2E tests that need workspace services, or before asking the user whether the app is running. Reuse the existing instance and its returned URL/container names. Returns `not_configured` when this project has no dev server: report the blocker, do not launch one manually. Otherwise returns running/stopped/starting/error + URL, port, container names. Running is lifecycle status, not application readiness: use the documented health check with bounded retries before dependent tests.',
|
|
412
412
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
413
413
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
414
414
|
},
|
|
415
415
|
{
|
|
416
416
|
name: 'start_dev_server',
|
|
417
|
-
description: 'CALL WHEN
|
|
417
|
+
description: 'CALL WHEN an authorized task needs workspace services for app checks or backend/frontend/integration/E2E tests and get_dev_server_status reports stopped. Kōbō runs the configured startup command and tracks the instance. Use this tool instead of shell lifecycle commands such as make dev-docker, sh/dev-docker.sh, docker compose up or npm run dev. Respect launch permissions; read-only sessions cannot start services. On failure, inspect get_dev_server_logs and report the blocker, without a manual shell fallback.',
|
|
418
418
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
419
419
|
annotations: { destructiveHint: false, openWorldHint: false },
|
|
420
420
|
},
|
|
421
421
|
{
|
|
422
422
|
name: 'stop_dev_server',
|
|
423
|
-
description: 'CALL WHEN the user explicitly asks to stop the dev server, or
|
|
423
|
+
description: 'CALL WHEN the user explicitly asks to stop the dev server, or when an authorized operation requires a restart. Use this tool instead of shell shutdown commands such as docker compose down or killing the server process directly. Respect launch permissions; read-only sessions cannot stop services. Do not stop a shared workspace instance merely because your tests finished.',
|
|
424
424
|
inputSchema: { type: 'object', properties: {}, required: [] },
|
|
425
425
|
annotations: { destructiveHint: false, openWorldHint: false },
|
|
426
426
|
},
|
|
427
427
|
{
|
|
428
428
|
name: 'get_dev_server_logs',
|
|
429
|
-
description: 'CALL WHEN
|
|
429
|
+
description: 'CALL WHEN diagnosing a dev-server startup failure or a runtime issue during app checks or tests. Returns the last N lines of logs (default 200). Use these logs with get_dev_server_status before reporting a blocker; do not bypass a failed managed startup with a shell launch.',
|
|
430
430
|
inputSchema: {
|
|
431
431
|
type: 'object',
|
|
432
432
|
properties: {
|
package/dist/server/index.js
CHANGED
|
@@ -37,13 +37,14 @@ import voiceRouter from './routes/voice.js';
|
|
|
37
37
|
import workspaceMessagesRouter from './routes/workspace-messages.js';
|
|
38
38
|
import workspaceTemplatesRouter from './routes/workspace-templates.js';
|
|
39
39
|
import workspacesRouter from './routes/workspaces.js';
|
|
40
|
-
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, resumePendingReviewReturns, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
40
|
+
import { getActiveSessionId, getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, resumePendingReviewReturns, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
41
41
|
import * as autoLoopService from './services/auto-loop-service.js';
|
|
42
42
|
import { startAwaitingUserReminder, stopAwaitingUserReminder } from './services/awaiting-user-reminder-service.js';
|
|
43
43
|
import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
|
|
44
44
|
import * as cronService from './services/cron-service.js';
|
|
45
45
|
import { createDailyDbBackupIfNeeded, createPreMigrationBackup, startDailyDbBackupScheduler, } from './services/db-backup-service.js';
|
|
46
|
-
import { startDevServer, stopAllDevServers, stopDevServer } from './services/dev-server-service.js';
|
|
46
|
+
import { beginDevServerShutdown, startDevServer, stopAllDevServers, stopDevServer, } from './services/dev-server-service.js';
|
|
47
|
+
import { onKoboShutdown } from './services/lifecycle-hook-service.js';
|
|
47
48
|
import { reconcileMessageRequests } from './services/mcp-message-request-service.js';
|
|
48
49
|
import { reconcileMemoryContextsOnStartup } from './services/memory-context-service.js';
|
|
49
50
|
import { authorizeWsUpgrade, generateToken, getLanHostnames, getLanUrls, isAllowedOrigin, isAllowedRequestHost, resolveBindHost, resolveDevClientOrigin, resolveNetworkAccessEnvOverrides, resolveProxyHostname, } from './services/network-access-service.js';
|
|
@@ -422,13 +423,15 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
422
423
|
if (isShuttingDown)
|
|
423
424
|
return;
|
|
424
425
|
isShuttingDown = true;
|
|
426
|
+
beginDevServerShutdown();
|
|
427
|
+
const stopDevServersOnShutdown = getGlobalSettings().stopDevServersOnShutdown !== false;
|
|
425
428
|
stopUpdateChecker();
|
|
426
429
|
const groupMessagesStopped = stopGroupMessageBatches();
|
|
427
430
|
console.log(`\n[kobo] Received ${signal}, shutting down gracefully…`);
|
|
428
431
|
const forceExitTimer = setTimeout(() => {
|
|
429
432
|
console.error('[kobo] Graceful shutdown timed out; forcing exit');
|
|
430
433
|
process.exit(1);
|
|
431
|
-
},
|
|
434
|
+
}, 150_000);
|
|
432
435
|
forceExitTimer.unref();
|
|
433
436
|
// Ask WebSocket clients to leave so their upgraded HTTP connections do not
|
|
434
437
|
// keep server.close() waiting indefinitely.
|
|
@@ -469,14 +472,26 @@ async function gracefulShutdown(signal, exitCode = 0) {
|
|
|
469
472
|
// Best-effort
|
|
470
473
|
}
|
|
471
474
|
clearInterval(retentionTimer);
|
|
472
|
-
// Stop
|
|
475
|
+
// Stop local writers before user hooks, leaving Docker available to those hooks.
|
|
473
476
|
try {
|
|
474
|
-
await Promise.all([stopAllAgents(), stopAllDevServers()]);
|
|
475
|
-
|
|
477
|
+
await Promise.all([stopAllAgents(), stopAllDevServers({ preserveDocker: true })]);
|
|
478
|
+
await onKoboShutdown(signal, (workspaceId) => getActiveSessionId(workspaceId) !== undefined);
|
|
476
479
|
}
|
|
477
480
|
catch {
|
|
478
481
|
// Best-effort
|
|
479
482
|
}
|
|
483
|
+
try {
|
|
484
|
+
if (stopDevServersOnShutdown) {
|
|
485
|
+
await stopAllDevServers();
|
|
486
|
+
console.log('[kobo] Dev servers stopped');
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
console.log('[kobo] Local dev processes stopped; Docker services left to the configured shutdown hooks');
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
console.error('[kobo] Dev-server shutdown failed:', err);
|
|
494
|
+
}
|
|
480
495
|
// Stop accepting new HTTP/WS work, then let in-flight handlers finish before
|
|
481
496
|
// closing SQLite. Closing the DB earlier makes those handlers fail midway.
|
|
482
497
|
try {
|
|
@@ -565,6 +565,7 @@ export function createClaudeCodeEngine() {
|
|
|
565
565
|
// first SDK message, re-armed on meaningful progress, suspended while a human
|
|
566
566
|
// decision or a background subagent is outstanding, stopped in `finally`.
|
|
567
567
|
const turnLiveness = createTurnLiveness({
|
|
568
|
+
enabled: options.watchdogEnabled === true,
|
|
568
569
|
timeoutMs: CLAUDE_STREAM_IDLE_TIMEOUT_MS,
|
|
569
570
|
onTimeout() {
|
|
570
571
|
// The armed deadline tracks whether a foreground tool call is in
|
|
@@ -618,7 +619,7 @@ export function createClaudeCodeEngine() {
|
|
|
618
619
|
compactionStallTimer = undefined;
|
|
619
620
|
};
|
|
620
621
|
const armCompactionStallTimer = () => {
|
|
621
|
-
if (compactionStallTimer)
|
|
622
|
+
if (!options.watchdogEnabled || compactionStallTimer)
|
|
622
623
|
return;
|
|
623
624
|
compactionStallTimer = setTimeout(() => {
|
|
624
625
|
compactionStallTimer = undefined;
|
|
@@ -639,7 +640,7 @@ export function createClaudeCodeEngine() {
|
|
|
639
640
|
compactionStallTimer.unref?.();
|
|
640
641
|
};
|
|
641
642
|
const armResultDrainWatchdog = () => {
|
|
642
|
-
if (resultDrainTimer)
|
|
643
|
+
if (!options.watchdogEnabled || resultDrainTimer)
|
|
643
644
|
return;
|
|
644
645
|
resultDrainTimer = setTimeout(() => {
|
|
645
646
|
console.warn(`[claude-engine] SDK generator still open ${RESULT_DRAIN_TIMEOUT_MS}ms after 'result' — forcing session:ended`);
|
|
@@ -725,6 +726,8 @@ export function createClaudeCodeEngine() {
|
|
|
725
726
|
// Human waits suspend it independently; genuine activity restarts it.
|
|
726
727
|
const armSubagentStallWatchdog = () => {
|
|
727
728
|
clearSubagentStallWatchdog();
|
|
729
|
+
if (!options.watchdogEnabled)
|
|
730
|
+
return;
|
|
728
731
|
if (pendingResolvers.size > 0 || activeSubagentTaskIds.size === 0)
|
|
729
732
|
return;
|
|
730
733
|
const hasAgent = [...activeSubagentTaskIds].some((id) => isSubagentTask({ taskType: mapperState.taskTypes.get(id) }));
|
|
@@ -205,6 +205,8 @@ export function createCodexEngine() {
|
|
|
205
205
|
// deadline tracks the last activity, not the start of the wait.
|
|
206
206
|
const armSubagentStallWatchdog = () => {
|
|
207
207
|
clearSubagentStallWatchdog();
|
|
208
|
+
if (!options.watchdogEnabled)
|
|
209
|
+
return;
|
|
208
210
|
if (!waitingForBackgroundSubagents || pendingByCallId.size > 0 || activeSubagentThreads.size === 0)
|
|
209
211
|
return;
|
|
210
212
|
subagentStallTimer = setTimeout(() => {
|
|
@@ -251,6 +253,7 @@ export function createCodexEngine() {
|
|
|
251
253
|
}
|
|
252
254
|
};
|
|
253
255
|
const turnLiveness = createTurnLiveness({
|
|
256
|
+
enabled: options.watchdogEnabled === true,
|
|
254
257
|
timeoutMs: CODEX_TURN_IDLE_TIMEOUT_MS,
|
|
255
258
|
onTimeout() {
|
|
256
259
|
watchdogEnded = true;
|
|
@@ -486,6 +486,7 @@ function readEffectiveSettingsSafe(projectPath) {
|
|
|
486
486
|
archiveScript: '',
|
|
487
487
|
changeSourceBranchScript: '',
|
|
488
488
|
sessionEndedScript: '',
|
|
489
|
+
shutdownScript: '',
|
|
489
490
|
prMergedScript: '',
|
|
490
491
|
autoLoopDisabledScript: '',
|
|
491
492
|
notionStatusProperty: '',
|
|
@@ -1577,6 +1578,7 @@ export function startAgent(workspaceId, workingDir, prompt, model, resume = fals
|
|
|
1577
1578
|
const integrationServers = buildIntegrationMcpServers(getGlobalSettings(), engine.id, (name) => unavailableIntegrations.push(name));
|
|
1578
1579
|
const options = {
|
|
1579
1580
|
workspaceId,
|
|
1581
|
+
watchdogEnabled: getGlobalSettings().agentWatchdogEnabled === true,
|
|
1580
1582
|
workingDir,
|
|
1581
1583
|
prompt: buildAgentPrompt(`${ws ? renderWorkflowPolicy(ws.workflowPolicy) : ''}\n${integrationMcpPrompt(integrationServers, unavailableIntegrations)}\n${prompt}`, ws?.projectPath),
|
|
1582
1584
|
model,
|
|
@@ -13,7 +13,7 @@ export function createTurnLiveness(input) {
|
|
|
13
13
|
let timedOut = false;
|
|
14
14
|
let currentTimeoutMs = input.timeoutMs;
|
|
15
15
|
const arm = () => {
|
|
16
|
-
if (!active || paused || timedOut)
|
|
16
|
+
if (input.enabled === false || !active || paused || timedOut)
|
|
17
17
|
return;
|
|
18
18
|
if (timer)
|
|
19
19
|
clearTimeout(timer);
|
|
@@ -26,11 +26,16 @@ function runCommand(command, args, options = {}) {
|
|
|
26
26
|
const trackedProcesses = new Map();
|
|
27
27
|
const stoppingProcesses = new Map();
|
|
28
28
|
const generations = new Map();
|
|
29
|
+
let shuttingDown = false;
|
|
30
|
+
export function beginDevServerShutdown() {
|
|
31
|
+
shuttingDown = true;
|
|
32
|
+
}
|
|
29
33
|
/** Test-only: clear the tracked-processes map between tests. */
|
|
30
34
|
export function _resetTrackedProcessesForTests() {
|
|
31
35
|
trackedProcesses.clear();
|
|
32
36
|
stoppingProcesses.clear();
|
|
33
37
|
generations.clear();
|
|
38
|
+
shuttingDown = false;
|
|
34
39
|
}
|
|
35
40
|
// ── Pure helpers ───────────────────────────────────────────────────────────────
|
|
36
41
|
/**
|
|
@@ -177,6 +182,8 @@ export async function getStatus(projectPath, workingBranch, workspaceId) {
|
|
|
177
182
|
* Start the dev-server for a workspace.
|
|
178
183
|
*/
|
|
179
184
|
export function startDevServer(workspaceId) {
|
|
185
|
+
if (shuttingDown)
|
|
186
|
+
throw new Error('Kōbō is shutting down');
|
|
180
187
|
assertWorkspaceLifecycleAvailable(workspaceId);
|
|
181
188
|
const workspace = getWorkspace(workspaceId);
|
|
182
189
|
if (!workspace) {
|
|
@@ -231,7 +238,7 @@ export function startDevServer(workspaceId) {
|
|
|
231
238
|
return;
|
|
232
239
|
trackedProcesses.delete(workspaceId);
|
|
233
240
|
const currentStatus = await getStatus(workspace.projectPath, workspace.workingBranch);
|
|
234
|
-
if (generations.get(workspaceId) !== generation || stoppingProcesses.has(workspaceId))
|
|
241
|
+
if (shuttingDown || generations.get(workspaceId) !== generation || stoppingProcesses.has(workspaceId))
|
|
235
242
|
return;
|
|
236
243
|
updateDevServerStatus(workspaceId, currentStatus.status);
|
|
237
244
|
emitEphemeral(workspaceId, 'devserver:status', currentStatus);
|
|
@@ -244,7 +251,7 @@ export function startDevServer(workspaceId) {
|
|
|
244
251
|
console.error(`[dev-server] Process exited with code ${code} for workspace ${workspaceId}`);
|
|
245
252
|
});
|
|
246
253
|
proc.on('error', (err) => {
|
|
247
|
-
if (trackedProcesses.get(workspaceId) !== proc)
|
|
254
|
+
if (shuttingDown || trackedProcesses.get(workspaceId) !== proc || generations.get(workspaceId) !== generation)
|
|
248
255
|
return;
|
|
249
256
|
// A signal failure is also an 'error' event; it does not prove process exit.
|
|
250
257
|
if (!proc.pid)
|
|
@@ -373,6 +380,16 @@ function signalAndWait(proc, signal, timeoutMs) {
|
|
|
373
380
|
}
|
|
374
381
|
});
|
|
375
382
|
}
|
|
383
|
+
async function stopTrackedProcess(workspaceId) {
|
|
384
|
+
const tracked = trackedProcesses.get(workspaceId);
|
|
385
|
+
if (!tracked)
|
|
386
|
+
return;
|
|
387
|
+
const exited = (await signalAndWait(tracked, 'SIGTERM', 3_000)) || (await signalAndWait(tracked, 'SIGKILL', 1_000));
|
|
388
|
+
if (!exited)
|
|
389
|
+
throw new Error('Dev server stop is not confirmed; retry after the process exits');
|
|
390
|
+
if (trackedProcesses.get(workspaceId) === tracked)
|
|
391
|
+
trackedProcesses.delete(workspaceId);
|
|
392
|
+
}
|
|
376
393
|
async function stopDevServerOwned(workspaceId) {
|
|
377
394
|
const workspace = getWorkspace(workspaceId);
|
|
378
395
|
if (!workspace) {
|
|
@@ -383,14 +400,7 @@ async function stopDevServerOwned(workspaceId) {
|
|
|
383
400
|
const worktreePath = workspace.worktreePath;
|
|
384
401
|
const cwd = worktreePath && existsSync(worktreePath) ? worktreePath : workspace.projectPath;
|
|
385
402
|
// Kill tracked process first (covers Node servers and any spawned process)
|
|
386
|
-
|
|
387
|
-
if (tracked) {
|
|
388
|
-
const exited = (await signalAndWait(tracked, 'SIGTERM', 3_000)) || (await signalAndWait(tracked, 'SIGKILL', 1_000));
|
|
389
|
-
if (!exited)
|
|
390
|
-
throw new Error('Dev server stop is not confirmed; retry after the process exits');
|
|
391
|
-
if (trackedProcesses.get(workspaceId) === tracked)
|
|
392
|
-
trackedProcesses.delete(workspaceId);
|
|
393
|
-
}
|
|
403
|
+
await stopTrackedProcess(workspaceId);
|
|
394
404
|
const settings = getProjectSettings(workspace.projectPath);
|
|
395
405
|
let stopError;
|
|
396
406
|
let customStopSucceeded = false;
|
|
@@ -448,12 +458,30 @@ async function stopDevServerOwned(workspaceId) {
|
|
|
448
458
|
emitEphemeral(workspaceId, 'devserver:status', status);
|
|
449
459
|
return status;
|
|
450
460
|
}
|
|
451
|
-
/**
|
|
452
|
-
export async function stopAllDevServers() {
|
|
461
|
+
/** Shut down local processes; optionally leave independently running Docker services alone. */
|
|
462
|
+
export async function stopAllDevServers(options = {}) {
|
|
463
|
+
beginDevServerShutdown();
|
|
453
464
|
const workspaceIds = new Set([...listWorkspaces(true).map((workspace) => workspace.id), ...trackedProcesses.keys()]);
|
|
454
465
|
await Promise.all([...workspaceIds].map(async (workspaceId) => {
|
|
455
466
|
try {
|
|
456
|
-
|
|
467
|
+
if (!options.preserveDocker) {
|
|
468
|
+
await stopDevServer(workspaceId);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
// A stop already requested by the user must still finish.
|
|
472
|
+
const stopping = stoppingProcesses.get(workspaceId);
|
|
473
|
+
if (stopping) {
|
|
474
|
+
await stopping;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const hadProcess = trackedProcesses.has(workspaceId);
|
|
478
|
+
await stopTrackedProcess(workspaceId);
|
|
479
|
+
const workspace = getWorkspace(workspaceId);
|
|
480
|
+
// Preserve Docker state; getStatus re-inspects Compose after restart.
|
|
481
|
+
if (hadProcess && workspace && !resolveInstance(workspace.projectPath, workspace.workingBranch)) {
|
|
482
|
+
updateDevServerStatus(workspaceId, 'stopped');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
457
485
|
}
|
|
458
486
|
catch (err) {
|
|
459
487
|
console.error(`[dev-server] Failed to stop '${workspaceId}' during shutdown:`, err);
|