@loicngr/kobo 1.11.36 → 1.11.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/AGENTS.md +18 -1
  2. package/CHANGELOG.md +8 -0
  3. package/dist/server/index.js +20 -24
  4. package/dist/server/routes/dev-server.js +2 -1
  5. package/dist/server/routes/pull-requests.js +5 -4
  6. package/dist/server/routes/workspaces.js +161 -109
  7. package/dist/server/services/agent/engines/claude-code/engine.js +11 -0
  8. package/dist/server/services/agent/engines/codex/client.js +1 -0
  9. package/dist/server/services/agent/engines/codex/engine.js +47 -10
  10. package/dist/server/services/agent/engines/codex/event-mapper.js +8 -2
  11. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +21 -8
  12. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +30 -3
  13. package/dist/server/services/agent/event-router.js +1 -1
  14. package/dist/server/services/agent/orchestrator.js +219 -78
  15. package/dist/server/services/agent/session-controller.js +12 -2
  16. package/dist/server/services/auto-loop-service.js +60 -6
  17. package/dist/server/services/change-source-branch-service.js +36 -4
  18. package/dist/server/services/cron-service.js +19 -0
  19. package/dist/server/services/db-backup-service.js +30 -0
  20. package/dist/server/services/dev-server-service.js +167 -38
  21. package/dist/server/services/image-service.js +25 -28
  22. package/dist/server/services/pr-checkout-service.js +34 -5
  23. package/dist/server/services/pr-watcher-service.js +5 -3
  24. package/dist/server/services/quota-backoff-service.js +25 -8
  25. package/dist/server/services/wakeup-service.js +23 -3
  26. package/dist/server/services/websocket-service.js +10 -2
  27. package/dist/server/services/workspace-service.js +8 -4
  28. package/dist/server/services/worktree-purge-service.js +3 -7
  29. package/dist/server/utils/agent-stop-result.js +12 -0
  30. package/dist/server/utils/git-ops.js +8 -1
  31. package/dist/server/utils/git-worktree-fingerprint.js +86 -0
  32. package/dist/server/utils/safe-path.js +21 -0
  33. package/dist/server/utils/script-runner.js +32 -9
  34. package/dist/server/utils/workspace-lifecycle-guard.js +27 -0
  35. package/package.json +1 -1
  36. package/src/client/dist/pwa/assets/ActivityFeed-BzzvcOmq.js +8 -0
  37. package/src/client/dist/pwa/assets/{ActivityFeed-Bakgvay6.css → ActivityFeed-Chsw1Mh7.css} +1 -1
  38. package/src/client/dist/pwa/assets/{ChangelogPage-BFGYQ6eY.js → ChangelogPage-D8563008.js} +1 -1
  39. package/src/client/dist/pwa/assets/ComparisonPanel-Bd0X08sg.js +2 -0
  40. package/src/client/dist/pwa/assets/ComparisonPanel-CRRaDs2D.css +1 -0
  41. package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +1 -0
  42. package/src/client/dist/pwa/assets/CreatePage-Bon8qIka.js +2 -0
  43. package/src/client/dist/pwa/assets/DashboardPage-D8g7_r4S.css +1 -0
  44. package/src/client/dist/pwa/assets/DashboardPage-DYxdxEul.js +1 -0
  45. package/src/client/dist/pwa/assets/{DiffViewer-DSlSAIiW.js → DiffViewer-BpsLNxnj.js} +3 -3
  46. package/src/client/dist/pwa/assets/{DocumentsPanel-C1eXTw_Q.js → DocumentsPanel-BJsJ0fNs.js} +1 -1
  47. package/src/client/dist/pwa/assets/{DrawerToggleButton-BxYpUb4y.js → DrawerToggleButton-BVLO5K5d.js} +1 -1
  48. package/src/client/dist/pwa/assets/{GitPanel-DnBWSdVp.js → GitPanel-Cal_wzC0.js} +3 -3
  49. package/src/client/dist/pwa/assets/{HealthPage-rX1TiaP9.js → HealthPage-RFZS7Lao.js} +1 -1
  50. package/src/client/dist/pwa/assets/{MainLayout-Ceuh5uJj.js → MainLayout-vvyiRcql.js} +2 -2
  51. package/src/client/dist/pwa/assets/{QBtnDropdown-K7syiyyH.js → QBtnDropdown-X8oNU5Mb.js} +1 -1
  52. package/src/client/dist/pwa/assets/{QDrawer-DapzE8DH.js → QDrawer-Cij17skX.js} +1 -1
  53. package/src/client/dist/pwa/assets/{QSelect-Bodml89k.js → QSelect-xd-AlGo_.js} +1 -1
  54. package/src/client/dist/pwa/assets/{QTooltip-K-9OdDfq.js → QTooltip-CwcCGSWq.js} +1 -1
  55. package/src/client/dist/pwa/assets/{SchedulePanel-CfumTzAa.js → SchedulePanel-CXRUsz6G.js} +1 -1
  56. package/src/client/dist/pwa/assets/{SearchPage-AhZYS-dS.js → SearchPage-DopbgLxh.js} +1 -1
  57. package/src/client/dist/pwa/assets/{SettingsPage-BoHXWvKn.js → SettingsPage-Jx6TnOzR.js} +1 -1
  58. package/src/client/dist/pwa/assets/{SplitWorkspacePage-3wK11mcu.js → SplitWorkspacePage-D7lGFRKs.js} +1 -1
  59. package/src/client/dist/pwa/assets/{TerminalPanel-CKg0BeNK.js → TerminalPanel-DI8DHb9e.js} +1 -1
  60. package/src/client/dist/pwa/assets/{TouchPan-D6ctyPYf.js → TouchPan-B3nrkCY8.js} +1 -1
  61. package/src/client/dist/pwa/assets/{TourReplayButton-Dn4Dy_kH.js → TourReplayButton-DU1UShMY.js} +1 -1
  62. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-DnoabsfK.js → WorkspaceAttentionLabels-C-yN5IX8.js} +1 -1
  63. package/src/client/dist/pwa/assets/{WorkspacePage-DYQrfeYw.css → WorkspacePage-BAC6u9OP.css} +1 -1
  64. package/src/client/dist/pwa/assets/WorkspacePage-DKRpdpPg.js +4 -0
  65. package/src/client/dist/pwa/assets/{browser-DHpSUK8-.js → browser-f9MHQjfi.js} +1 -1
  66. package/src/client/dist/pwa/assets/{build-path-tree-DtEjsZlh.js → build-path-tree-BWBzP8os.js} +1 -1
  67. package/src/client/dist/pwa/assets/{de-Dt_m-H83.js → de-QkDLrcGq.js} +1 -1
  68. package/src/client/dist/pwa/assets/{documents-B2dg1wRf.js → documents-C2cLnEBQ.js} +1 -1
  69. package/src/client/dist/pwa/assets/{editor.api-Cd1D35oj.js → editor.api-sw146MWN.js} +1 -1
  70. package/src/client/dist/pwa/assets/{efforts-IB1vPjJQ.js → efforts-C5ZZgCH4.js} +1 -1
  71. package/src/client/dist/pwa/assets/{engineFeatures-DYSyDEJe.js → engineFeatures-CAVMDawq.js} +1 -1
  72. package/src/client/dist/pwa/assets/{es-DFg25DCS.js → es-BgQFKlo8.js} +1 -1
  73. package/src/client/dist/pwa/assets/{expand-template-CNAHiesL.js → expand-template-B6xAlAaM.js} +1 -1
  74. package/src/client/dist/pwa/assets/{format-DidwgbbX.js → format-BwWP6Aay.js} +1 -1
  75. package/src/client/dist/pwa/assets/{formatters-BjzyUHlu.js → formatters-DyWtO-nR.js} +1 -1
  76. package/src/client/dist/pwa/assets/{fr-CQ1slpol.js → fr-DAdLWjNM.js} +1 -1
  77. package/src/client/dist/pwa/assets/{freemarker2-jNGWawVw.js → freemarker2-DZ5Hm8UH.js} +1 -1
  78. package/src/client/dist/pwa/assets/{handlebars-DfSqClrW.js → handlebars-BRUGpTA7.js} +1 -1
  79. package/src/client/dist/pwa/assets/{html-ByLX-n8T.js → html-DNOayNNs.js} +1 -1
  80. package/src/client/dist/pwa/assets/i18n-CBez_vip.js +1 -0
  81. package/src/client/dist/pwa/assets/index-VL3fI9a3.js +20 -0
  82. package/src/client/dist/pwa/assets/{it-w09rfaP7.js → it-Bumaewp1.js} +1 -1
  83. package/src/client/dist/pwa/assets/{javascript-Dja1fHfv.js → javascript-C0tDQU3z.js} +1 -1
  84. package/src/client/dist/pwa/assets/{liquid-D315hW6C.js → liquid-CN46O6OF.js} +1 -1
  85. package/src/client/dist/pwa/assets/{mdx-B5yUwpzv.js → mdx-DM-lrr8Q.js} +1 -1
  86. package/src/client/dist/pwa/assets/{monaco-contributions-NfxrU0r3.js → monaco-contributions-CkN7y-8c.js} +1 -1
  87. package/src/client/dist/pwa/assets/{monaco.contribution-D1hXPkDZ.js → monaco.contribution-2BcAOLjc.js} +2 -2
  88. package/src/client/dist/pwa/assets/{network-auth-CARVtbNe.js → network-auth-KYS4snQH.js} +1 -1
  89. package/src/client/dist/pwa/assets/{notify-theme-CKC3-EAZ.js → notify-theme-a91Tcr6o.js} +1 -1
  90. package/src/client/dist/pwa/assets/{pending-diff-open-BlLrQaW7.js → pending-diff-open-DOtELZfB.js} +1 -1
  91. package/src/client/dist/pwa/assets/{permissionModes-DMzFEQhr.js → permissionModes-C-9Crj-c.js} +1 -1
  92. package/src/client/dist/pwa/assets/{project-color-CFjdIj6W.js → project-color-BRe4oyNx.js} +1 -1
  93. package/src/client/dist/pwa/assets/{python-Dc6mnEip.js → python-BRpJG6sZ.js} +1 -1
  94. package/src/client/dist/pwa/assets/{razor-DArVFBGO.js → razor-CehCmvHl.js} +1 -1
  95. package/src/client/dist/pwa/assets/{render-chat-markdown-BTPAjU2g.js → render-chat-markdown-DlbQLZ02.js} +1 -1
  96. package/src/client/dist/pwa/assets/{typescript-COE1vIyB.js → typescript-Dt_8mVuj.js} +1 -1
  97. package/src/client/dist/pwa/assets/{use-tours-C8x2V1w5.js → use-tours-CSVM_54K.js} +1 -1
  98. package/src/client/dist/pwa/assets/{workspace-templates-DHyA_GEW.js → workspace-templates-GCObcjO6.js} +1 -1
  99. package/src/client/dist/pwa/assets/{xml-eYjr4cGR.js → xml-_gOwdzB4.js} +1 -1
  100. package/src/client/dist/pwa/assets/{yaml-UooOcssF.js → yaml-BxeBqpGf.js} +1 -1
  101. package/src/client/dist/pwa/index.html +1 -1
  102. package/src/client/dist/pwa/sw.js +1 -1
  103. package/src/client/dist/pwa/assets/ActivityFeed-BPx-xhFY.js +0 -8
  104. package/src/client/dist/pwa/assets/ComparisonPanel-C6DfjpZZ.js +0 -2
  105. package/src/client/dist/pwa/assets/ComparisonPanel-x1iuICgd.css +0 -1
  106. package/src/client/dist/pwa/assets/CreatePage-DxOyY65u.css +0 -1
  107. package/src/client/dist/pwa/assets/CreatePage-ypEzkAKv.js +0 -2
  108. package/src/client/dist/pwa/assets/DashboardPage-DsVRjzzy.js +0 -1
  109. package/src/client/dist/pwa/assets/DashboardPage-DsWn5cWl.css +0 -1
  110. package/src/client/dist/pwa/assets/WorkspacePage-BI7CW2yT.js +0 -4
  111. package/src/client/dist/pwa/assets/i18n-CnjXQjxy.js +0 -1
  112. package/src/client/dist/pwa/assets/index-DcwcFDrZ.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
 
@@ -239,6 +241,8 @@ Each event maps to one effective-settings key (`sessionEndedScript` / `prMergedS
239
241
 
240
242
  ### Engine comparison
241
243
 
244
+ Successful paired creation opens the existing `split` route with `left` and `right` workspace ids in creation order. A single successful creation keeps the normal workspace route, including when the second creation fails.
245
+
242
246
  One task, two engines, two sibling worktrees. `POST /api/workspaces` accepts a `comparisonId`; the client posts twice with the same one (sequentially — parallel creations contend on the git index lock), suffixing the name with the engine's display name and the branch with the engine id and giving the non-configured engine the model picked for it on the form and its own effort / permission defaults. `workspaces.comparison_id` (migration v40) groups them; `listComparisonMembers` reads them back, treating an empty id as no group rather than as a match against the NULL majority. `GET /api/workspaces/:id/comparison` returns each member with its cached git stats (`null`, never zeros, when nothing has been measured). `ComparisonPanel.vue` renders the table at the top of the Git tab, only for workspaces that belong to a comparison. See [CONFIGURATION.md → Comparing two engines on one task](CONFIGURATION.md#comparing-two-engines-on-one-task).
243
247
 
244
248
  ### Unanswered question reminder
@@ -384,3 +388,16 @@ deviate from the documented system without explicit user approval.
384
388
  - Don't skip `try/catch` swallowing on best-effort cleanup (agent stop, dev-server stop, worktree removal). These must never break the primary operation.
385
389
  - Don't hardcode user-visible text in the frontend. Every string must go through `$t()` / `t()` with keys in all 5 locale files. See [Internationalization (i18n)](#internationalization-i18n).
386
390
  - Don't hardcode hex colors, spacing literals, or font names in components. Use the CSS variables from `src/client/src/css/design-tokens.scss` and the patterns in `DESIGN.md`. See [Design System](#design-system).
391
+
392
+
393
+ ## Audit remediation contracts (September 2026)
394
+
395
+ - `stopAgentAndWait` returns `stopped | not-running | timeout | failed`. Destructive callers must pass its result through `assertAgentStopped`. A timeout or failure retains the same controller in `stopping` until actual stop completion; it never authorizes a replacement or worktree removal. Pending replacements remain cancellable and are finalized as `killed` when stopped before starting. Dev-server shutdown retains ownership of the entire process group until no live member remains, including when the shell exits first; process inspection failures propagate to destructive callers. A successful custom stop can override a failed generic Docker shutdown only after an independent successful query confirms no running containers with the exact project label.
396
+ - Acquire `withWorkspaceLifecycleGuard` before `withGitRepoLock`. Agent/dev-server starts check lifecycle ownership centrally. Wakeups and cron defer while busy; auto-loop waiters are reconsidered on guard release and confirmed capacity release. A user stop persistently disables auto-loop, including while waiting for capacity without a controller. Internal stops explicitly pass `replacement`, `setup`, `archive` or `shutdown` to preserve that intent; delete and purge retain their dedicated causes.
397
+ - Quota-backoff `arm` receives the authoritative next `retryCount` from the orchestrator. Persist that value directly; consuming the preceding timer row must not reset the retry sequence.
398
+ - A quota retry with a residual controller awaits a confirmed technical stop before starting another iteration. Persist recovery before that await and retain its attempt count if the stop remains unconfirmed. Shutdown suspends quota, wakeup and cron timers without consuming their rows; late arms stay persisted but do not deliver until boot restores the schedulers. Automatic continuations also check shutdown when deferred lifecycle callbacks run, preserving auto-loop intent instead of treating a refused start as an error.
399
+ - Codex uses a 120-second stream-idle deadline and a bounded 30-minute deadline while normalized foreground tool calls remain unresolved. Synthetic `TodoWrite` calls emitted for completed plans must have matching results so they do not keep the tool deadline active. Human-input pauses remain independent of the tool timeout.
400
+ - Codex explicit stop confirms `exit` after each escalation, including `SIGKILL`; an unconfirmed exit rejects the stop instead of authorizing destructive follow-up. Exit listeners and deadlines are removed when that wait settles.
401
+ - Conversation loading indicators follow outstanding session/history/sync requests only while no content is available. Background refreshes must preserve the displayed conversation, component state and reading position, including feeds containing only user messages.
402
+ - PR diagnosis fingerprints are asynchronous and include binary staged/unstaged diffs and untracked file contents (symlink targets, not their destinations). Untracked nested Git repositories are fingerprinted recursively, including HEAD and index changes; ignored files and Git internals are excluded. Await them at every callsite. Workspace matching always includes normalized project identity.
403
+ - Daily DB backups are checked at startup and hourly, with the existing 24-hour minimum age and retention. Stop the scheduler and await its active backup before closing SQLite.
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.38
8
+
9
+ - feat(workspaces): show compaction status and block chat sends
10
+
11
+ ## 1.11.37
12
+
13
+ - fix(kobo): harden workspace lifecycle and comparison navigation
14
+
7
15
  ## 1.11.36
8
16
 
9
17
  - fix(agent): resume quota waits and scheduled background checks
@@ -29,12 +29,12 @@ import usageRoutes from './routes/usage.js';
29
29
  import voiceRouter from './routes/voice.js';
30
30
  import workspaceTemplatesRouter from './routes/workspace-templates.js';
31
31
  import workspacesRouter from './routes/workspaces.js';
32
- import { getAvailableSkills, isAgentUnavailableError, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
32
+ import { getAvailableSkills, isAgentUnavailableError, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgentAndWait, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
33
33
  import * as autoLoopService from './services/auto-loop-service.js';
34
34
  import { startAwaitingUserReminder, stopAwaitingUserReminder } from './services/awaiting-user-reminder-service.js';
35
35
  import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
36
36
  import * as cronService from './services/cron-service.js';
37
- import { createDailyDbBackupIfNeeded, createPreMigrationBackup } from './services/db-backup-service.js';
37
+ import { createDailyDbBackupIfNeeded, createPreMigrationBackup, startDailyDbBackupScheduler, } from './services/db-backup-service.js';
38
38
  import { startDevServer, stopAllDevServers, stopDevServer } from './services/dev-server-service.js';
39
39
  import { authorizeWsUpgrade, generateToken, getLanHostnames, getLanUrls, isAllowedOrigin, isAllowedRequestHost, resolveBindHost, resolveDevClientOrigin, resolveNetworkAccessEnvOverrides, resolveProxyHostname, } from './services/network-access-service.js';
40
40
  import { startPrWatcher, stopPrWatcher } from './services/pr-watcher-service.js';
@@ -101,23 +101,11 @@ runRetentionPass('at boot');
101
101
  const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1000;
102
102
  const retentionTimer = setInterval(() => runRetentionPass('daily'), RETENTION_INTERVAL_MS);
103
103
  retentionTimer.unref?.();
104
- // Daily DB backup (best-effort, fire-and-forget — never blocks boot).
105
- // Creates a WAL-safe snapshot alongside kobo.db if no backup exists in the
106
- // last 24h, and rotates out older backups beyond the retention window.
107
- void createDailyDbBackupIfNeeded(db, getDbPath())
108
- .then((r) => {
109
- if (r.created) {
110
- startupDebug(`[kobo] Daily DB backup: ${r.created}`);
111
- if (r.deleted.length > 0) {
112
- startupDebug(`[kobo] Rotated ${r.deleted.length} old DB backup(s)`);
113
- }
114
- }
115
- })
116
- .catch((err) => {
117
- // A full disk or an unwritable backup directory must not stop the server
118
- // from booting: without this catch the rejection reaches the last-resort
119
- // handler below and Kōbō exits before it ever listens.
120
- console.error('[kobo] Daily DB backup failed (continuing):', err);
104
+ // Check on boot and hourly so servers running longer than a day keep backing up.
105
+ const dailyBackupScheduler = startDailyDbBackupScheduler(async () => {
106
+ const result = await createDailyDbBackupIfNeeded(db, getDbPath());
107
+ if (result.created)
108
+ startupDebug(`[kobo] Daily DB backup: ${result.created}`);
121
109
  });
122
110
  // Initialize process cleanup, agent watchdog, PR watcher, and wakeup rehydration
123
111
  reconcileOrphanSessions();
@@ -128,7 +116,7 @@ autoLoopService.rehydrate();
128
116
  // otherwise the next arm() after restart would compute the next ladder rung
129
117
  // from retryCount=0 and undo the progression.
130
118
  restoreRetryCountsFromDb();
131
- quotaBackoffService.restoreOnBoot((workspaceId) => autoLoopService.onQuotaBackoffExpired(workspaceId));
119
+ quotaBackoffService.restoreOnBoot((workspaceId, pending) => autoLoopService.onQuotaBackoffExpired(workspaceId, pending));
132
120
  cronService.restoreOnBoot();
133
121
  // Deliver any new default prompt templates to existing installs (seed-once via the
134
122
  // seededDefaultSlugs watermark; never overwrites or re-adds deleted defaults).
@@ -358,6 +346,15 @@ terminalWss.on('connection', (ws, workspaceId) => {
358
346
  setMessageHandler(async (type, payload) => {
359
347
  const p = payload;
360
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
+ }
361
358
  // Auto-loop owns the agent's turns. A user message means the user wants
362
359
  // to redirect the conversation, so disable the loop (idempotent — the
363
360
  // `autoloop:disabled` event is emitted with reason='user-action' so the
@@ -453,10 +450,8 @@ setMessageHandler(async (type, payload) => {
453
450
  }
454
451
  if (type === 'workspace:stop' && p?.workspaceId) {
455
452
  try {
456
- // A deliberate stop must not be treated like a recoverable engine
457
- // interruption: disable before the engine emits session:ended(killed).
458
- autoLoopService.disable(p.workspaceId, 'user-action');
459
- stopAgent(p.workspaceId);
453
+ // Share the HTTP stop contract, including loops waiting without an agent.
454
+ await stopAgentAndWait(p.workspaceId);
460
455
  }
461
456
  catch (err) {
462
457
  console.error('[ws] Failed to stop agent:', err);
@@ -613,6 +608,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
613
608
  }
614
609
  finally {
615
610
  try {
611
+ await dailyBackupScheduler.stop();
616
612
  closeDb();
617
613
  console.log('[kobo] Database closed');
618
614
  }
@@ -3,6 +3,7 @@ import { migrationGuard } from '../middleware/migration-guard.js';
3
3
  import { getDevServerLogs, getStatus, startDevServer, stopDevServer } from '../services/dev-server-service.js';
4
4
  import { getProjectSettings } from '../services/settings-service.js';
5
5
  import { getWorkspace } from '../services/workspace-service.js';
6
+ import { WorkspaceLifecycleBusyError } from '../utils/workspace-lifecycle-guard.js';
6
7
  /** Hono sub-router for per-workspace dev server lifecycle (start, stop, status, logs). */
7
8
  const app = new Hono();
8
9
  // GET /api/dev-server/:workspaceId/status
@@ -51,7 +52,7 @@ app.post('/:workspaceId/start', migrationGuard, (c) => {
51
52
  }
52
53
  catch (err) {
53
54
  const message = err instanceof Error ? err.message : String(err);
54
- const status = message.includes('already starting') ? 409 : 500;
55
+ const status = err instanceof WorkspaceLifecycleBusyError || /already starting|is stopping/.test(message) ? 409 : 500;
55
56
  return c.json({ error: message }, status);
56
57
  }
57
58
  });
@@ -1,3 +1,4 @@
1
+ import path from 'node:path';
1
2
  import { Hono } from 'hono';
2
3
  import { getForgeProvider } from '../services/forge/registry.js';
3
4
  import { resolveForge } from '../services/forge/resolve.js';
@@ -61,7 +62,7 @@ app.post('/diagnose', async (c) => {
61
62
  ongoingOperation: null,
62
63
  branch: { state: 'absent' },
63
64
  };
64
- return c.json({ report, pr: null, fingerprint: prCheckout.computeFingerprint(report) });
65
+ return c.json({ report, pr: null, fingerprint: await prCheckout.computeFingerprint(report) });
65
66
  }
66
67
  // Page through the full list — a PR the user explicitly diagnosed by
67
68
  // number must never come back "not found" just for sitting past the
@@ -75,11 +76,11 @@ app.post('/diagnose', async (c) => {
75
76
  } while (!pr && cursor !== null);
76
77
  if (!pr)
77
78
  return c.json({ error: `Pull request #${prNumber} not found` }, 404);
78
- const workspaces = listWorkspaces(true);
79
+ const workspaces = listWorkspaces(true).filter((workspace) => path.resolve(workspace.projectPath) === path.resolve(projectPath));
79
80
  const report = prCheckout.diagnoseLocalState(projectPath, pr.headBranch, worktreesPath ?? null, attachedWorktreePaths(workspaces), workspaces);
80
81
  if (pr.isFork)
81
82
  report.blockers.push({ kind: 'fork-pr' });
82
- return c.json({ report, pr, fingerprint: prCheckout.computeFingerprint(report) });
83
+ return c.json({ report, pr, fingerprint: await prCheckout.computeFingerprint(report) });
83
84
  }
84
85
  catch (err) {
85
86
  return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
@@ -93,7 +94,7 @@ app.post('/resolve', async (c) => {
93
94
  if (!body.projectPath || !body.fingerprint) {
94
95
  return c.json({ error: 'Missing required fields: projectPath, fingerprint' }, 400);
95
96
  }
96
- const workspaces = listWorkspaces(true);
97
+ const workspaces = listWorkspaces(true).filter((workspace) => path.resolve(workspace.projectPath) === path.resolve(body.projectPath));
97
98
  const result = await prCheckout.resolvePrCheckout({
98
99
  projectPath: body.projectPath,
99
100
  headBranch: body.headBranch,