@loicngr/kobo 1.11.35 → 1.11.37

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 (102) hide show
  1. package/AGENTS.md +15 -0
  2. package/CHANGELOG.md +9 -0
  3. package/dist/server/index.js +11 -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 +122 -91
  7. package/dist/server/services/agent/engines/claude-code/engine.js +26 -0
  8. package/dist/server/services/agent/engines/claude-code/event-mapper.js +10 -10
  9. package/dist/server/services/agent/engines/codex/client.js +1 -0
  10. package/dist/server/services/agent/engines/codex/engine.js +47 -10
  11. package/dist/server/services/agent/engines/codex/event-mapper.js +5 -1
  12. package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +21 -8
  13. package/dist/server/services/agent/engines/codex/jsonrpc/transport.js +30 -3
  14. package/dist/server/services/agent/orchestrator.js +175 -68
  15. package/dist/server/services/agent/session-controller.js +12 -2
  16. package/dist/server/services/auto-loop-service.js +71 -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/quota-backoff-service.js +25 -8
  24. package/dist/server/services/wakeup-service.js +37 -3
  25. package/dist/server/services/worktree-purge-service.js +3 -7
  26. package/dist/server/utils/agent-stop-result.js +12 -0
  27. package/dist/server/utils/git-ops.js +8 -1
  28. package/dist/server/utils/git-worktree-fingerprint.js +86 -0
  29. package/dist/server/utils/safe-path.js +21 -0
  30. package/dist/server/utils/script-runner.js +32 -9
  31. package/dist/server/utils/workspace-lifecycle-guard.js +27 -0
  32. package/package.json +1 -1
  33. package/src/client/dist/pwa/assets/{ActivityFeed-Bakgvay6.css → ActivityFeed-BDyFBoxd.css} +1 -1
  34. package/src/client/dist/pwa/assets/ActivityFeed-C_-hxIo8.js +8 -0
  35. package/src/client/dist/pwa/assets/{ChangelogPage-DVLWWvvz.js → ChangelogPage-CBlGJ4zT.js} +1 -1
  36. package/src/client/dist/pwa/assets/{ComparisonPanel-LZrvk6a6.js → ComparisonPanel-DZAE2X9j.js} +1 -1
  37. package/src/client/dist/pwa/assets/CreatePage-A3pVradx.css +1 -0
  38. package/src/client/dist/pwa/assets/CreatePage-ArmtcF4d.js +2 -0
  39. package/src/client/dist/pwa/assets/{DashboardPage-0odIhseb.js → DashboardPage-eQTCXcdz.js} +1 -1
  40. package/src/client/dist/pwa/assets/{DiffViewer-NWB2uQkz.js → DiffViewer-CaT7vuSr.js} +3 -3
  41. package/src/client/dist/pwa/assets/{DocumentsPanel-3yApHgbl.js → DocumentsPanel-A0isCTaI.js} +1 -1
  42. package/src/client/dist/pwa/assets/{DrawerToggleButton-C_bNtZsE.js → DrawerToggleButton-T70fh8dp.js} +1 -1
  43. package/src/client/dist/pwa/assets/{GitPanel-BMjWwiAX.js → GitPanel-CAvk0G7h.js} +3 -3
  44. package/src/client/dist/pwa/assets/{HealthPage-Vyr1YG2o.js → HealthPage-D8Mr0V6w.js} +1 -1
  45. package/src/client/dist/pwa/assets/{MainLayout-s8_9FPE2.js → MainLayout-x6Es4Da-.js} +2 -2
  46. package/src/client/dist/pwa/assets/{QBtnDropdown-BxuXEBp5.js → QBtnDropdown-CQqzzlFI.js} +1 -1
  47. package/src/client/dist/pwa/assets/{QDrawer-BdlDiU6a.js → QDrawer-Cwj2qh0r.js} +1 -1
  48. package/src/client/dist/pwa/assets/{QSelect-BrSemS3o.js → QSelect-BSPtuXJg.js} +1 -1
  49. package/src/client/dist/pwa/assets/{QTooltip-CLCFHW2D.js → QTooltip-DC_kDKku.js} +1 -1
  50. package/src/client/dist/pwa/assets/{SchedulePanel-F9bdAq6K.js → SchedulePanel-9aJSoz6Y.js} +1 -1
  51. package/src/client/dist/pwa/assets/{SearchPage-CBCefYqc.js → SearchPage-BjNvtE_U.js} +1 -1
  52. package/src/client/dist/pwa/assets/{SettingsPage-BPgTHmKj.js → SettingsPage-CvLnT73V.js} +1 -1
  53. package/src/client/dist/pwa/assets/{SplitWorkspacePage-DDs2nuoT.js → SplitWorkspacePage-DTao0DHr.js} +1 -1
  54. package/src/client/dist/pwa/assets/{TerminalPanel-CulE0wTU.js → TerminalPanel-BrKIW-bJ.js} +1 -1
  55. package/src/client/dist/pwa/assets/{TouchPan-DUgqs2cW.js → TouchPan-_n1AQgoo.js} +1 -1
  56. package/src/client/dist/pwa/assets/{TourReplayButton-CdHpGBdr.js → TourReplayButton-CJF3nVvn.js} +1 -1
  57. package/src/client/dist/pwa/assets/{WorkspaceAttentionLabels-W5SvqzXO.js → WorkspaceAttentionLabels-CxesUiIm.js} +1 -1
  58. package/src/client/dist/pwa/assets/{WorkspacePage-DYQrfeYw.css → WorkspacePage-DoXtwpjV.css} +1 -1
  59. package/src/client/dist/pwa/assets/WorkspacePage-MxBE0mr3.js +4 -0
  60. package/src/client/dist/pwa/assets/{browser-CnuqDTyc.js → browser-DGWpkvN3.js} +1 -1
  61. package/src/client/dist/pwa/assets/{build-path-tree-JodIO7-I.js → build-path-tree-Ba1dSLgi.js} +1 -1
  62. package/src/client/dist/pwa/assets/{de-Dt_m-H83.js → de-DVK21RB6.js} +1 -1
  63. package/src/client/dist/pwa/assets/{documents-3ZBK1pXE.js → documents-BMHzjW_g.js} +1 -1
  64. package/src/client/dist/pwa/assets/{editor.api-DZ9WO_J7.js → editor.api-CYTuNwqu.js} +1 -1
  65. package/src/client/dist/pwa/assets/{efforts-LrtJPx-j.js → efforts-BPlRSHXp.js} +1 -1
  66. package/src/client/dist/pwa/assets/{engineFeatures-DmnHj_uD.js → engineFeatures-DVB2rklO.js} +1 -1
  67. package/src/client/dist/pwa/assets/{es-DFg25DCS.js → es-C8cQwDZM.js} +1 -1
  68. package/src/client/dist/pwa/assets/{expand-template-Dv4JC2zU.js → expand-template-Cj82YE-O.js} +1 -1
  69. package/src/client/dist/pwa/assets/{format-CHjfjuhr.js → format-Dk7O3LhA.js} +1 -1
  70. package/src/client/dist/pwa/assets/{formatters-BsgH8CoH.js → formatters-TGjvs7Ca.js} +1 -1
  71. package/src/client/dist/pwa/assets/{fr-CQ1slpol.js → fr-BVOMZ2Bl.js} +1 -1
  72. package/src/client/dist/pwa/assets/{freemarker2-Dla55SIR.js → freemarker2-39lO_dWH.js} +1 -1
  73. package/src/client/dist/pwa/assets/{handlebars-DtuuZy7D.js → handlebars-M_0V82cD.js} +1 -1
  74. package/src/client/dist/pwa/assets/{html-CKYOCR8W.js → html-yILqYDsl.js} +1 -1
  75. package/src/client/dist/pwa/assets/i18n-2UeMfd5m.js +1 -0
  76. package/src/client/dist/pwa/assets/{index-CIP08MpU.js → index-BJU5dnkF.js} +2 -2
  77. package/src/client/dist/pwa/assets/{it-w09rfaP7.js → it-CxkAoOj4.js} +1 -1
  78. package/src/client/dist/pwa/assets/{javascript-BxzTnsIQ.js → javascript-DrW-amDc.js} +1 -1
  79. package/src/client/dist/pwa/assets/{liquid-C8BfPFUy.js → liquid-D_mXmUoL.js} +1 -1
  80. package/src/client/dist/pwa/assets/{mdx-DZm_YXkC.js → mdx-E8O3lvR5.js} +1 -1
  81. package/src/client/dist/pwa/assets/{monaco-contributions-HDbRHiNR.js → monaco-contributions-C2K3sl_V.js} +1 -1
  82. package/src/client/dist/pwa/assets/{monaco.contribution-DRW-851R.js → monaco.contribution-DLhHQIJn.js} +2 -2
  83. package/src/client/dist/pwa/assets/{network-auth-Ce9hsryo.js → network-auth-BUwWtnui.js} +1 -1
  84. package/src/client/dist/pwa/assets/{notify-theme-OBpMEBvP.js → notify-theme-1mOpY2vN.js} +1 -1
  85. package/src/client/dist/pwa/assets/{pending-diff-open-CVQxWyzP.js → pending-diff-open-BL7dKRqF.js} +1 -1
  86. package/src/client/dist/pwa/assets/{permissionModes-CVFHS2ec.js → permissionModes-BtC3_P-P.js} +1 -1
  87. package/src/client/dist/pwa/assets/{project-color-mcpQp2o6.js → project-color-BFBFZWeG.js} +1 -1
  88. package/src/client/dist/pwa/assets/{python-CO5iQN74.js → python-DoZAYim6.js} +1 -1
  89. package/src/client/dist/pwa/assets/{razor-BC7D8LMf.js → razor-9bZ30g2y.js} +1 -1
  90. package/src/client/dist/pwa/assets/{render-chat-markdown-Cb15kvox.js → render-chat-markdown-7EtWNjG0.js} +1 -1
  91. package/src/client/dist/pwa/assets/{typescript-BCHCSNpD.js → typescript-C1CBfL3L.js} +1 -1
  92. package/src/client/dist/pwa/assets/{use-tours-DhLhglwr.js → use-tours-BJ81clv6.js} +1 -1
  93. package/src/client/dist/pwa/assets/{workspace-templates-CoWrOEac.js → workspace-templates-r1-bipjM.js} +1 -1
  94. package/src/client/dist/pwa/assets/{xml-y9M3NAIZ.js → xml-COuu5isK.js} +1 -1
  95. package/src/client/dist/pwa/assets/{yaml-CthlToPo.js → yaml-ChZ7WqaJ.js} +1 -1
  96. package/src/client/dist/pwa/index.html +1 -1
  97. package/src/client/dist/pwa/sw.js +1 -1
  98. package/src/client/dist/pwa/assets/ActivityFeed-YRbIRutK.js +0 -8
  99. package/src/client/dist/pwa/assets/CreatePage-DxOyY65u.css +0 -1
  100. package/src/client/dist/pwa/assets/CreatePage-gFrn4xl9.js +0 -2
  101. package/src/client/dist/pwa/assets/WorkspacePage-B6O7j57u.js +0 -4
  102. package/src/client/dist/pwa/assets/i18n-DTfKQUMf.js +0 -1
@@ -1,5 +1,7 @@
1
1
  import { execFile as execFileCb, execFileSync, spawn } from 'node:child_process';
2
2
  import { promisify } from 'node:util';
3
+ import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
4
+ import { withGitRepoLock } from '../utils/git-repo-lock.js';
3
5
  const execFileAsync = promisify(execFileCb);
4
6
  import fs from 'node:fs';
5
7
  import path from 'node:path';
@@ -50,8 +52,25 @@ import * as safePath from '../utils/safe-path.js';
50
52
  import { WorkspaceLifecycleBusyError, withWorkspaceLifecycleGuard } from '../utils/workspace-lifecycle-guard.js';
51
53
  import { resolveExtractedName } from '../utils/workspace-name.js';
52
54
  import { resolveSiblingWorkspaceWorktreePath } from '../utils/worktree-paths.js';
53
- /** Hono sub-router for workspace CRUD, tasks, agent lifecycle, git operations, and PR creation. */
55
+ /** Lifecycle conflicts can be retried once the current owner has stopped. */
56
+ function workspaceErrorStatus(err) {
57
+ return err instanceof AgentStopError || err instanceof WorkspaceLifecycleBusyError ? 409 : 500;
58
+ }
59
+ /** Hono sub-router for workspace CRUD, tasks, agents, Git and PR creation. */
54
60
  const app = new Hono();
61
+ // These handlers own teardown across awaits; purge/delete/restore own their guard in the service.
62
+ for (const route of ['/:id/archive', '/:id/run-setup-script', '/:id/cancel-source-change']) {
63
+ app.use(route, async (c, next) => {
64
+ try {
65
+ await withWorkspaceLifecycleGuard(c.req.param('id'), next);
66
+ }
67
+ catch (err) {
68
+ if (err instanceof WorkspaceLifecycleBusyError)
69
+ return c.json({ error: err.message, code: err.code }, 409);
70
+ throw err;
71
+ }
72
+ });
73
+ }
55
74
  /** Tracks workspaces currently running a setup script to prevent concurrent executions. */
56
75
  const setupScriptRunning = new Set();
57
76
  /**
@@ -243,7 +262,7 @@ app.get('/', (c) => {
243
262
  }
244
263
  catch (err) {
245
264
  const message = err instanceof Error ? err.message : String(err);
246
- return c.json({ error: message }, 500);
265
+ return c.json({ error: message }, workspaceErrorStatus(err));
247
266
  }
248
267
  });
249
268
  // POST /api/workspaces/:id/engine-handoff-preview — build an editable, deterministic handoff.
@@ -294,7 +313,7 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
294
313
  // A new engine is started three lines below: the previous one must be dead
295
314
  // first, or two agents share the worktree.
296
315
  if (agentManager.hasController(id))
297
- await agentManager.stopAgentAndWait(id);
316
+ assertAgentStopped(await agentManager.stopAgentAndWait(id, undefined, 'replacement'));
298
317
  const updated = workspaceService.updateWorkspaceEngineConfiguration(id, body.engine, model, effort, body.agentPermissionMode);
299
318
  const session = agentManager.startAgent(id, updated.worktreePath, handoff, updated.model, false, updated.agentPermissionMode, undefined, updated.reasoningEffort);
300
319
  workspaceService.updateWorkspaceStatus(id, 'executing');
@@ -1366,7 +1385,7 @@ app.post('/:id/sessions', migrationGuard, (c) => {
1366
1385
  }
1367
1386
  catch (err) {
1368
1387
  const message = err instanceof Error ? err.message : String(err);
1369
- return c.json({ error: message }, 500);
1388
+ return c.json({ error: message }, workspaceErrorStatus(err));
1370
1389
  }
1371
1390
  });
1372
1391
  // GET /api/workspaces/:id/sessions — list sessions for a workspace
@@ -1381,7 +1400,7 @@ app.get('/:id/sessions', (c) => {
1381
1400
  }
1382
1401
  catch (err) {
1383
1402
  const message = err instanceof Error ? err.message : String(err);
1384
- return c.json({ error: message }, 500);
1403
+ return c.json({ error: message }, workspaceErrorStatus(err));
1385
1404
  }
1386
1405
  });
1387
1406
  // GET /api/workspaces/pr-states — batch snapshot of PR states known to the
@@ -1395,7 +1414,7 @@ app.get('/pr-states', (c) => {
1395
1414
  }
1396
1415
  catch (err) {
1397
1416
  const message = err instanceof Error ? err.message : String(err);
1398
- return c.json({ error: message }, 500);
1417
+ return c.json({ error: message }, workspaceErrorStatus(err));
1399
1418
  }
1400
1419
  });
1401
1420
  // GET /api/workspaces/info — bulk snapshot for the 30s client refresh:
@@ -1415,7 +1434,7 @@ app.get('/info', (c) => {
1415
1434
  }
1416
1435
  catch (err) {
1417
1436
  const message = err instanceof Error ? err.message : String(err);
1418
- return c.json({ error: message }, 500);
1437
+ return c.json({ error: message }, workspaceErrorStatus(err));
1419
1438
  }
1420
1439
  });
1421
1440
  // GET /api/workspaces/:id/comparison — the workspaces running the same task on
@@ -1449,7 +1468,7 @@ app.get('/:id/comparison', (c) => {
1449
1468
  }
1450
1469
  catch (err) {
1451
1470
  const message = err instanceof Error ? err.message : String(err);
1452
- return c.json({ error: message }, 500);
1471
+ return c.json({ error: message }, workspaceErrorStatus(err));
1453
1472
  }
1454
1473
  });
1455
1474
  // GET /api/workspaces/:id/preset — the create-form preset this workspace would
@@ -1464,7 +1483,7 @@ app.get('/:id/preset', (c) => {
1464
1483
  }
1465
1484
  catch (err) {
1466
1485
  const message = err instanceof Error ? err.message : String(err);
1467
- return c.json({ error: message }, 500);
1486
+ return c.json({ error: message }, workspaceErrorStatus(err));
1468
1487
  }
1469
1488
  });
1470
1489
  // POST /api/workspaces/pr-snapshot/refresh/:id — on-demand refresh of a single
@@ -1483,7 +1502,7 @@ app.post('/pr-snapshot/refresh/:id', async (c) => {
1483
1502
  const message = err instanceof Error ? err.message : String(err);
1484
1503
  if (/not found/i.test(message))
1485
1504
  return c.json({ error: message }, 404);
1486
- return c.json({ error: message }, 500);
1505
+ return c.json({ error: message }, workspaceErrorStatus(err));
1487
1506
  }
1488
1507
  });
1489
1508
  // GET /api/workspaces/auto-loop-states — batch snapshot keyed by workspace id.
@@ -1518,7 +1537,7 @@ app.get('/auto-loop-states', (c) => {
1518
1537
  }
1519
1538
  catch (err) {
1520
1539
  const message = err instanceof Error ? err.message : String(err);
1521
- return c.json({ error: message }, 500);
1540
+ return c.json({ error: message }, workspaceErrorStatus(err));
1522
1541
  }
1523
1542
  });
1524
1543
  // GET /api/workspaces/:id/auto-loop — current auto-loop status for one workspace.
@@ -1534,7 +1553,7 @@ app.get('/:id/auto-loop', (c) => {
1534
1553
  }
1535
1554
  catch (err) {
1536
1555
  const message = err instanceof Error ? err.message : String(err);
1537
- return c.json({ error: message }, 500);
1556
+ return c.json({ error: message }, workspaceErrorStatus(err));
1538
1557
  }
1539
1558
  });
1540
1559
  // POST /api/workspaces/:id/auto-loop — enable the loop (user toggle ON).
@@ -1556,7 +1575,7 @@ app.delete('/:id/auto-loop', (c) => {
1556
1575
  }
1557
1576
  catch (err) {
1558
1577
  const message = err instanceof Error ? err.message : String(err);
1559
- return c.json({ error: message }, 500);
1578
+ return c.json({ error: message }, workspaceErrorStatus(err));
1560
1579
  }
1561
1580
  });
1562
1581
  // POST /api/workspaces/:id/auto-loop-ready — force auto_loop_ready=true.
@@ -1576,7 +1595,7 @@ app.post('/:id/auto-loop-ready', (c) => {
1576
1595
  }
1577
1596
  catch (err) {
1578
1597
  const message = err instanceof Error ? err.message : String(err);
1579
- return c.json({ error: message }, 500);
1598
+ return c.json({ error: message }, workspaceErrorStatus(err));
1580
1599
  }
1581
1600
  });
1582
1601
  // GET /api/workspaces/:id/crons — list pending crons for a workspace.
@@ -1590,7 +1609,7 @@ app.get('/:id/crons', (c) => {
1590
1609
  }
1591
1610
  catch (err) {
1592
1611
  const message = err instanceof Error ? err.message : String(err);
1593
- return c.json({ error: message }, 500);
1612
+ return c.json({ error: message }, workspaceErrorStatus(err));
1594
1613
  }
1595
1614
  });
1596
1615
  // POST /api/workspaces/:id/crons — arm a new cron. Validates the expression
@@ -1638,7 +1657,7 @@ app.post('/:id/crons', async (c) => {
1638
1657
  }
1639
1658
  catch (err) {
1640
1659
  const message = err instanceof Error ? err.message : String(err);
1641
- return c.json({ error: message }, 500);
1660
+ return c.json({ error: message }, workspaceErrorStatus(err));
1642
1661
  }
1643
1662
  });
1644
1663
  // DELETE /api/workspaces/:id/crons/:cronId — cancel a single cron. Idempotent:
@@ -1655,7 +1674,7 @@ app.delete('/:id/crons/:cronId', (c) => {
1655
1674
  }
1656
1675
  catch (err) {
1657
1676
  const message = err instanceof Error ? err.message : String(err);
1658
- return c.json({ error: message }, 500);
1677
+ return c.json({ error: message }, workspaceErrorStatus(err));
1659
1678
  }
1660
1679
  });
1661
1680
  // GET /api/workspaces/:id/pending-wakeup — returns the pending wakeup or null.
@@ -1667,7 +1686,7 @@ app.get('/:id/pending-wakeup', (c) => {
1667
1686
  }
1668
1687
  catch (err) {
1669
1688
  const message = err instanceof Error ? err.message : String(err);
1670
- return c.json({ error: message }, 500);
1689
+ return c.json({ error: message }, workspaceErrorStatus(err));
1671
1690
  }
1672
1691
  });
1673
1692
  // DELETE /api/workspaces/:id/pending-wakeup — user-initiated cancel ("×" button)
@@ -1680,7 +1699,7 @@ app.delete('/:id/pending-wakeup', (c) => {
1680
1699
  }
1681
1700
  catch (err) {
1682
1701
  const message = err instanceof Error ? err.message : String(err);
1683
- return c.json({ error: message }, 500);
1702
+ return c.json({ error: message }, workspaceErrorStatus(err));
1684
1703
  }
1685
1704
  });
1686
1705
  // GET /api/workspaces/:id/quota-backoff — returns the pending quota backoff or null.
@@ -1696,7 +1715,7 @@ app.get('/:id/quota-backoff', (c) => {
1696
1715
  }
1697
1716
  catch (err) {
1698
1717
  const message = err instanceof Error ? err.message : String(err);
1699
- return c.json({ error: message }, 500);
1718
+ return c.json({ error: message }, workspaceErrorStatus(err));
1700
1719
  }
1701
1720
  });
1702
1721
  // DELETE /api/workspaces/:id/quota-backoff — user-initiated cancel ("×" button).
@@ -1711,7 +1730,7 @@ app.delete('/:id/quota-backoff', (c) => {
1711
1730
  }
1712
1731
  catch (err) {
1713
1732
  const message = err instanceof Error ? err.message : String(err);
1714
- return c.json({ error: message }, 500);
1733
+ return c.json({ error: message }, workspaceErrorStatus(err));
1715
1734
  }
1716
1735
  });
1717
1736
  // POST /api/workspaces/:id/pending-wakeup — schedule a wakeup, either from
@@ -1748,7 +1767,7 @@ app.post('/:id/pending-wakeup', async (c) => {
1748
1767
  }
1749
1768
  catch (err) {
1750
1769
  const message = err instanceof Error ? err.message : String(err);
1751
- return c.json({ error: message }, 500);
1770
+ return c.json({ error: message }, workspaceErrorStatus(err));
1752
1771
  }
1753
1772
  });
1754
1773
  // PATCH /api/workspaces/:id/sessions/:sessionId — rename a session
@@ -1772,7 +1791,7 @@ app.patch('/:id/sessions/:sessionId', async (c) => {
1772
1791
  }
1773
1792
  catch (err) {
1774
1793
  const message = err instanceof Error ? err.message : String(err);
1775
- return c.json({ error: message }, 500);
1794
+ return c.json({ error: message }, workspaceErrorStatus(err));
1776
1795
  }
1777
1796
  });
1778
1797
  // DELETE /api/workspaces/:id/sessions/:sessionId — remove a stopped session and its history.
@@ -1909,7 +1928,7 @@ app.delete('/:id/events/:eventId', (c) => {
1909
1928
  }
1910
1929
  catch (err) {
1911
1930
  const message = err instanceof Error ? err.message : 'unknown';
1912
- return c.json({ error: message }, 500);
1931
+ return c.json({ error: message }, workspaceErrorStatus(err));
1913
1932
  }
1914
1933
  });
1915
1934
  // POST /api/workspaces/:id/tasks — create a new task
@@ -1937,7 +1956,7 @@ app.post('/:id/tasks', async (c) => {
1937
1956
  }
1938
1957
  catch (err) {
1939
1958
  const message = err instanceof Error ? err.message : String(err);
1940
- return c.json({ error: message }, 500);
1959
+ return c.json({ error: message }, workspaceErrorStatus(err));
1941
1960
  }
1942
1961
  });
1943
1962
  // PATCH /api/workspaces/:id/tasks/:taskId — update task status and/or title
@@ -1976,7 +1995,7 @@ app.patch('/:id/tasks/:taskId', async (c) => {
1976
1995
  }
1977
1996
  catch (err) {
1978
1997
  const message = err instanceof Error ? err.message : String(err);
1979
- return c.json({ error: message }, 500);
1998
+ return c.json({ error: message }, workspaceErrorStatus(err));
1980
1999
  }
1981
2000
  });
1982
2001
  // DELETE /api/workspaces/:id/tasks/:taskId — delete a task
@@ -1997,7 +2016,7 @@ app.delete('/:id/tasks/:taskId', (c) => {
1997
2016
  }
1998
2017
  catch (err) {
1999
2018
  const message = err instanceof Error ? err.message : String(err);
2000
- return c.json({ error: message }, 500);
2019
+ return c.json({ error: message }, workspaceErrorStatus(err));
2001
2020
  }
2002
2021
  });
2003
2022
  // POST /api/workspaces/:id/tasks/notify-updated — broadcast generic task list change
@@ -2015,7 +2034,7 @@ app.post('/:id/tasks/notify-updated', (c) => {
2015
2034
  }
2016
2035
  catch (err) {
2017
2036
  const message = err instanceof Error ? err.message : String(err);
2018
- return c.json({ error: message }, 500);
2037
+ return c.json({ error: message }, workspaceErrorStatus(err));
2019
2038
  }
2020
2039
  });
2021
2040
  // POST /api/workspaces/:id/agent-description/notify-updated — broadcast
@@ -2037,7 +2056,7 @@ app.post('/:id/agent-description/notify-updated', (c) => {
2037
2056
  }
2038
2057
  catch (err) {
2039
2058
  const message = err instanceof Error ? err.message : String(err);
2040
- return c.json({ error: message }, 500);
2059
+ return c.json({ error: message }, workspaceErrorStatus(err));
2041
2060
  }
2042
2061
  });
2043
2062
  // POST /api/workspaces/:id/crons/notify-updated — broadcast cron list changed
@@ -2053,7 +2072,7 @@ app.post('/:id/crons/notify-updated', (c) => {
2053
2072
  }
2054
2073
  catch (err) {
2055
2074
  const message = err instanceof Error ? err.message : String(err);
2056
- return c.json({ error: message }, 500);
2075
+ return c.json({ error: message }, workspaceErrorStatus(err));
2057
2076
  }
2058
2077
  });
2059
2078
  // POST /api/workspaces/:id/tasks/:taskId/notify-done — broadcast task:updated event
@@ -2070,7 +2089,7 @@ app.post('/:id/tasks/:taskId/notify-done', (c) => {
2070
2089
  }
2071
2090
  catch (err) {
2072
2091
  const message = err instanceof Error ? err.message : String(err);
2073
- return c.json({ error: message }, 500);
2092
+ return c.json({ error: message }, workspaceErrorStatus(err));
2074
2093
  }
2075
2094
  });
2076
2095
  // Cap how many recent rows one search may scan: the LIKE below cannot use an
@@ -2201,7 +2220,7 @@ app.get('/:id/sessions/:sessionId/summary', (c) => {
2201
2220
  }
2202
2221
  catch (err) {
2203
2222
  const message = err instanceof Error ? err.message : String(err);
2204
- return c.json({ error: message }, 500);
2223
+ return c.json({ error: message }, workspaceErrorStatus(err));
2205
2224
  }
2206
2225
  });
2207
2226
  // GET /api/workspaces/:id/session-metrics — compact observability data for the timeline.
@@ -2378,7 +2397,7 @@ app.get('/:id/events', (c) => {
2378
2397
  }
2379
2398
  catch (err) {
2380
2399
  const message = err instanceof Error ? err.message : String(err);
2381
- return c.json({ error: message }, 500);
2400
+ return c.json({ error: message }, workspaceErrorStatus(err));
2382
2401
  }
2383
2402
  });
2384
2403
  // GET /api/workspaces/archived — list archived workspaces (must be before GET /:id)
@@ -2388,7 +2407,7 @@ app.get('/archived', (c) => {
2388
2407
  }
2389
2408
  catch (err) {
2390
2409
  const message = err instanceof Error ? err.message : String(err);
2391
- return c.json({ error: message }, 500);
2410
+ return c.json({ error: message }, workspaceErrorStatus(err));
2392
2411
  }
2393
2412
  });
2394
2413
  // GET /:id/prep-autoloop-prompt — compose the project-aware grooming
@@ -2415,7 +2434,7 @@ ${AUTO_LOOP_HARD_RULES}`;
2415
2434
  }
2416
2435
  catch (err) {
2417
2436
  const message = err instanceof Error ? err.message : String(err);
2418
- return c.json({ error: message }, 500);
2437
+ return c.json({ error: message }, workspaceErrorStatus(err));
2419
2438
  }
2420
2439
  });
2421
2440
  // GET /:id/chat-history — list up to 200 most recent chat-input messages
@@ -2431,7 +2450,7 @@ app.get('/:id/chat-history', (c) => {
2431
2450
  }
2432
2451
  catch (err) {
2433
2452
  const message = err instanceof Error ? err.message : String(err);
2434
- return c.json({ error: message }, 500);
2453
+ return c.json({ error: message }, workspaceErrorStatus(err));
2435
2454
  }
2436
2455
  });
2437
2456
  // POST /:id/chat-history — append a message to this workspace's history.
@@ -2454,7 +2473,7 @@ app.post('/:id/chat-history', async (c) => {
2454
2473
  }
2455
2474
  catch (err) {
2456
2475
  const message = err instanceof Error ? err.message : String(err);
2457
- return c.json({ error: message }, 500);
2476
+ return c.json({ error: message }, workspaceErrorStatus(err));
2458
2477
  }
2459
2478
  });
2460
2479
  // POST /:id/save-file — persist edited file content from the diff viewer.
@@ -2490,7 +2509,7 @@ app.post('/:id/save-file', async (c) => {
2490
2509
  }
2491
2510
  catch (err) {
2492
2511
  const message = err instanceof Error ? err.message : String(err);
2493
- return c.json({ error: message }, 500);
2512
+ return c.json({ error: message }, workspaceErrorStatus(err));
2494
2513
  }
2495
2514
  });
2496
2515
  // GET /api/workspaces/:id — get workspace details with tasks
@@ -2505,7 +2524,7 @@ app.get('/:id', (c) => {
2505
2524
  }
2506
2525
  catch (err) {
2507
2526
  const message = err instanceof Error ? err.message : String(err);
2508
- return c.json({ error: message }, 500);
2527
+ return c.json({ error: message }, workspaceErrorStatus(err));
2509
2528
  }
2510
2529
  });
2511
2530
  // POST /api/workspaces/:id/favorite — mark workspace as favorite
@@ -2660,7 +2679,7 @@ app.patch('/:id', migrationGuard, async (c) => {
2660
2679
  message.includes('Description must be')) {
2661
2680
  return c.json({ error: message }, 400);
2662
2681
  }
2663
- return c.json({ error: message }, 500);
2682
+ return c.json({ error: message }, workspaceErrorStatus(err));
2664
2683
  }
2665
2684
  });
2666
2685
  /** Open the workspace worktree in the user's configured editor. */
@@ -2696,7 +2715,7 @@ app.post('/:id/open-editor', (c) => {
2696
2715
  }
2697
2716
  catch (err) {
2698
2717
  const message = err instanceof Error ? err.message : String(err);
2699
- return c.json({ error: message }, 500);
2718
+ return c.json({ error: message }, workspaceErrorStatus(err));
2700
2719
  }
2701
2720
  });
2702
2721
  /** Open the workspace worktree in the user's configured terminal emulator. */
@@ -2736,7 +2755,7 @@ app.post('/:id/open-terminal', async (c) => {
2736
2755
  }
2737
2756
  catch (err) {
2738
2757
  const message = err instanceof Error ? err.message : String(err);
2739
- return c.json({ error: message }, 500);
2758
+ return c.json({ error: message }, workspaceErrorStatus(err));
2740
2759
  }
2741
2760
  });
2742
2761
  /** Open the workspace worktree in the user's configured file manager. */
@@ -2769,7 +2788,7 @@ app.post('/:id/open-file-manager', async (c) => {
2769
2788
  }
2770
2789
  catch (err) {
2771
2790
  const message = err instanceof Error ? err.message : String(err);
2772
- return c.json({ error: message }, 500);
2791
+ return c.json({ error: message }, workspaceErrorStatus(err));
2773
2792
  }
2774
2793
  });
2775
2794
  /** Spawn a detached process and resolve as soon as it has started (or reject
@@ -2814,11 +2833,12 @@ app.post('/:id/run-setup-script', async (c) => {
2814
2833
  // rewrites the worktree the agent may still be writing to.
2815
2834
  try {
2816
2835
  if (agentManager.getAgentStatus(id)) {
2817
- await agentManager.stopAgentAndWait(id);
2836
+ assertAgentStopped(await agentManager.stopAgentAndWait(id, undefined, 'setup'));
2818
2837
  }
2819
2838
  }
2820
2839
  catch (err) {
2821
2840
  console.error(`[workspaces] stopAgentAndWait before setup script failed for '${id}':`, err);
2841
+ throw err;
2822
2842
  }
2823
2843
  const effectiveSettings = settingsService.getEffectiveSettings(workspace.projectPath);
2824
2844
  if (!effectiveSettings.setupScript) {
@@ -2845,7 +2865,7 @@ app.post('/:id/run-setup-script', async (c) => {
2845
2865
  }
2846
2866
  catch (err) {
2847
2867
  const message = err instanceof Error ? err.message : String(err);
2848
- return c.json({ error: message }, 500);
2868
+ return c.json({ error: message }, workspaceErrorStatus(err));
2849
2869
  }
2850
2870
  });
2851
2871
  // POST /api/workspaces/:id/archive — mark workspace as archived (soft-delete)
@@ -2860,10 +2880,11 @@ app.post('/:id/archive', migrationGuard, async (c) => {
2860
2880
  return c.json({ error: 'Already archived' }, 400);
2861
2881
  }
2862
2882
  try {
2863
- await agentManager.stopAgentAndWait(id);
2883
+ assertAgentStopped(await agentManager.stopAgentAndWait(id, undefined, 'archive'));
2864
2884
  }
2865
2885
  catch (err) {
2866
2886
  console.error(`[workspaces] stopAgentAndWait during archive failed for '${id}':`, err);
2887
+ throw err;
2867
2888
  }
2868
2889
  try {
2869
2890
  await devServerService.stopDevServer(id);
@@ -2871,6 +2892,7 @@ app.post('/:id/archive', migrationGuard, async (c) => {
2871
2892
  catch (err) {
2872
2893
  const message = err instanceof Error ? err.message : String(err);
2873
2894
  console.error(`[workspaces] stopDevServer during archive failed: ${message}`);
2895
+ throw err;
2874
2896
  }
2875
2897
  try {
2876
2898
  terminalService.destroyTerminal(id);
@@ -2886,7 +2908,7 @@ app.post('/:id/archive', migrationGuard, async (c) => {
2886
2908
  }
2887
2909
  catch (err) {
2888
2910
  const message = err instanceof Error ? err.message : String(err);
2889
- return c.json({ error: message }, 500);
2911
+ return c.json({ error: message }, workspaceErrorStatus(err));
2890
2912
  }
2891
2913
  });
2892
2914
  // POST /api/workspaces/:id/purge-worktree
@@ -2909,7 +2931,7 @@ app.post('/:id/purge-worktree', migrationGuard, async (c) => {
2909
2931
  if (err instanceof WorkspaceLifecycleBusyError)
2910
2932
  return c.json({ code: err.code, error: err.message }, 409);
2911
2933
  const message = err instanceof Error ? err.message : String(err);
2912
- return c.json({ error: message }, 500);
2934
+ return c.json({ error: message }, workspaceErrorStatus(err));
2913
2935
  }
2914
2936
  });
2915
2937
  // Recreate the checkout before unarchiving; never starts agents or setup scripts.
@@ -2953,7 +2975,7 @@ app.post('/:id/unarchive', migrationGuard, (c) => {
2953
2975
  }
2954
2976
  catch (err) {
2955
2977
  const message = err instanceof Error ? err.message : String(err);
2956
- return c.json({ error: message }, 500);
2978
+ return c.json({ error: message }, workspaceErrorStatus(err));
2957
2979
  }
2958
2980
  });
2959
2981
  // Shared teardown for a single workspace: stops the agent, destroys the
@@ -2979,10 +3001,11 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
2979
3001
  // a few lines below: firing and forgetting here pulled the directory from
2980
3002
  // under an agent still writing to it, destroying uncommitted work.
2981
3003
  try {
2982
- await agentManager.stopAgentAndWait(workspace.id, undefined, 'delete');
3004
+ assertAgentStopped(await agentManager.stopAgentAndWait(workspace.id, undefined, 'delete'));
2983
3005
  }
2984
3006
  catch (err) {
2985
3007
  console.error(`[workspaces] stopAgentAndWait during delete failed for '${workspace.name}':`, err);
3008
+ throw err;
2986
3009
  }
2987
3010
  // Stop dev server if it was running. The processSpawn would otherwise
2988
3011
  // outlive the workspace (and keep its port + docker containers alive).
@@ -2991,6 +3014,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
2991
3014
  }
2992
3015
  catch (err) {
2993
3016
  console.error(`[workspaces] stopDevServer during delete failed for '${workspace.name}':`, err);
3017
+ throw err;
2994
3018
  }
2995
3019
  try {
2996
3020
  terminalService.destroyTerminal(workspace.id);
@@ -3089,7 +3113,7 @@ app.delete('/archived', migrationGuard, async (c) => {
3089
3113
  }
3090
3114
  catch (err) {
3091
3115
  const message = err instanceof Error ? err.message : String(err);
3092
- return c.json({ error: message }, 500);
3116
+ return c.json({ error: message }, workspaceErrorStatus(err));
3093
3117
  }
3094
3118
  });
3095
3119
  // DELETE /api/workspaces/:id — delete workspace
@@ -3117,7 +3141,7 @@ app.delete('/:id', migrationGuard, async (c) => {
3117
3141
  if (err instanceof WorkspaceLifecycleBusyError)
3118
3142
  return c.json({ code: err.code, error: err.message }, 409);
3119
3143
  const message = err instanceof Error ? err.message : String(err);
3120
- return c.json({ error: message }, 500);
3144
+ return c.json({ error: message }, workspaceErrorStatus(err));
3121
3145
  }
3122
3146
  });
3123
3147
  // POST /api/workspaces/:id/start — start/restart agent
@@ -3162,10 +3186,11 @@ app.post('/:id/start', migrationGuard, async (c) => {
3162
3186
  quotaBackoffService.cancel(id, 'user');
3163
3187
  // Stop the existing agent and wait: a fresh one starts right below.
3164
3188
  try {
3165
- await agentManager.stopAgentAndWait(id);
3189
+ assertAgentStopped(await agentManager.stopAgentAndWait(id, undefined, 'replacement'));
3166
3190
  }
3167
3191
  catch (err) {
3168
3192
  console.error(`[workspaces] stopAgentAndWait before start failed for '${id}':`, err);
3193
+ throw err;
3169
3194
  }
3170
3195
  const worktreePath = workspace.worktreePath;
3171
3196
  const agent = agentManager.startAgent(id, worktreePath, prompt, workspace.model, resume, workspace.agentPermissionMode, agentSessionId, workspace.reasoningEffort);
@@ -3195,7 +3220,7 @@ app.post('/:id/start', migrationGuard, async (c) => {
3195
3220
  }
3196
3221
  catch (err) {
3197
3222
  const message = err instanceof Error ? err.message : String(err);
3198
- return c.json({ error: message }, 500);
3223
+ return c.json({ error: message }, workspaceErrorStatus(err));
3199
3224
  }
3200
3225
  });
3201
3226
  // GET /api/workspaces/:id/git-stats — commit count and diff stats for the branch
@@ -3221,7 +3246,7 @@ app.get('/:id/git-stats', async (c) => {
3221
3246
  }
3222
3247
  catch (err) {
3223
3248
  const message = err instanceof Error ? err.message : String(err);
3224
- return c.json({ error: message }, 500);
3249
+ return c.json({ error: message }, workspaceErrorStatus(err));
3225
3250
  }
3226
3251
  });
3227
3252
  // GET /api/workspaces/:id/diff?mode=branch|unpushed — list changed files
@@ -3283,7 +3308,7 @@ app.get('/:id/diff', async (c) => {
3283
3308
  }
3284
3309
  catch (err) {
3285
3310
  const message = err instanceof Error ? err.message : String(err);
3286
- return c.json({ error: message }, 500);
3311
+ return c.json({ error: message }, workspaceErrorStatus(err));
3287
3312
  }
3288
3313
  });
3289
3314
  // GET /api/workspaces/:id/diff-file?path=...&mode=branch|unpushed
@@ -3349,7 +3374,7 @@ app.get('/:id/diff-file', (c) => {
3349
3374
  }
3350
3375
  catch (err) {
3351
3376
  const message = err instanceof Error ? err.message : String(err);
3352
- return c.json({ error: message }, 500);
3377
+ return c.json({ error: message }, workspaceErrorStatus(err));
3353
3378
  }
3354
3379
  });
3355
3380
  // POST /api/workspaces/:id/rollback-file { path }
@@ -3388,7 +3413,7 @@ app.post('/:id/rollback-file', async (c) => {
3388
3413
  }
3389
3414
  catch (err) {
3390
3415
  const message = err instanceof Error ? err.message : String(err);
3391
- return c.json({ error: message }, 500);
3416
+ return c.json({ error: message }, workspaceErrorStatus(err));
3392
3417
  }
3393
3418
  });
3394
3419
  // GET /api/workspaces/:id/branch-divergence?limit=50
@@ -3420,7 +3445,7 @@ app.get('/:id/branch-divergence', async (c) => {
3420
3445
  }
3421
3446
  catch (err) {
3422
3447
  const message = err instanceof Error ? err.message : String(err);
3423
- return c.json({ error: message }, 500);
3448
+ return c.json({ error: message }, workspaceErrorStatus(err));
3424
3449
  }
3425
3450
  });
3426
3451
  // GET /api/workspaces/:id/commits?limit=50 — list commits between
@@ -3444,7 +3469,7 @@ app.get('/:id/commits', async (c) => {
3444
3469
  }
3445
3470
  catch (err) {
3446
3471
  const message = err instanceof Error ? err.message : String(err);
3447
- return c.json({ error: message }, 500);
3472
+ return c.json({ error: message }, workspaceErrorStatus(err));
3448
3473
  }
3449
3474
  });
3450
3475
  // GET /:id/working-tree-files — list uncommitted working-tree files (read-only)
@@ -3459,7 +3484,7 @@ app.get('/:id/working-tree-files', (c) => {
3459
3484
  }
3460
3485
  catch (err) {
3461
3486
  const message = err instanceof Error ? err.message : String(err);
3462
- return c.json({ error: message }, 500);
3487
+ return c.json({ error: message }, workspaceErrorStatus(err));
3463
3488
  }
3464
3489
  });
3465
3490
  // POST /api/workspaces/:id/rename-branch { newName }
@@ -3531,7 +3556,7 @@ app.post('/:id/rename-branch', async (c) => {
3531
3556
  }
3532
3557
  catch (err) {
3533
3558
  const message = err instanceof Error ? err.message : String(err);
3534
- return c.json({ error: message }, 500);
3559
+ return c.json({ error: message }, workspaceErrorStatus(err));
3535
3560
  }
3536
3561
  });
3537
3562
  // POST /api/workspaces/:id/resync-branch
@@ -3587,7 +3612,7 @@ app.post('/:id/resync-branch', (c) => {
3587
3612
  }
3588
3613
  catch (err) {
3589
3614
  const message = err instanceof Error ? err.message : String(err);
3590
- return c.json({ error: message }, 500);
3615
+ return c.json({ error: message }, workspaceErrorStatus(err));
3591
3616
  }
3592
3617
  });
3593
3618
  // POST /api/workspaces/:id/push — push working branch to origin
@@ -3624,7 +3649,7 @@ app.post('/:id/push', async (c) => {
3624
3649
  }
3625
3650
  catch (err) {
3626
3651
  const message = err instanceof Error ? err.message : String(err);
3627
- return c.json({ error: message }, 500);
3652
+ return c.json({ error: message }, workspaceErrorStatus(err));
3628
3653
  }
3629
3654
  // Emit a trace into the chat feed so the user sees the action
3630
3655
  const session = workspaceService.getActiveSession(id);
@@ -3638,7 +3663,7 @@ app.post('/:id/push', async (c) => {
3638
3663
  }
3639
3664
  catch (err) {
3640
3665
  const message = err instanceof Error ? err.message : String(err);
3641
- return c.json({ error: message }, 500);
3666
+ return c.json({ error: message }, workspaceErrorStatus(err));
3642
3667
  }
3643
3668
  });
3644
3669
  // POST /api/workspaces/:id/fetch — git fetch the workspace repo (all branches of origin).
@@ -3655,7 +3680,7 @@ app.post('/:id/fetch', async (c) => {
3655
3680
  }
3656
3681
  catch (err) {
3657
3682
  const message = err instanceof Error ? err.message : String(err);
3658
- return c.json({ error: message }, 500);
3683
+ return c.json({ error: message }, workspaceErrorStatus(err));
3659
3684
  }
3660
3685
  });
3661
3686
  // POST /api/workspaces/:id/pull — pull working branch from origin (fast-forward only)
@@ -3676,7 +3701,7 @@ app.post('/:id/pull', async (c) => {
3676
3701
  return c.json({ error: err.message, code: 'dirty_worktree', operation: err.operation, status: err.status }, 409);
3677
3702
  }
3678
3703
  const message = err instanceof Error ? err.message : String(err);
3679
- return c.json({ error: message }, 500);
3704
+ return c.json({ error: message }, workspaceErrorStatus(err));
3680
3705
  }
3681
3706
  // Emit a trace into the chat feed so the user sees the action
3682
3707
  const session = workspaceService.getActiveSession(id);
@@ -3685,7 +3710,7 @@ app.post('/:id/pull', async (c) => {
3685
3710
  }
3686
3711
  catch (err) {
3687
3712
  const message = err instanceof Error ? err.message : String(err);
3688
- return c.json({ error: message }, 500);
3713
+ return c.json({ error: message }, workspaceErrorStatus(err));
3689
3714
  }
3690
3715
  });
3691
3716
  /** Rebase the workspace branch onto its source branch. */
@@ -3708,7 +3733,7 @@ app.post('/:id/rebase', async (c) => {
3708
3733
  return c.json({ error: err.message, code: 'dirty_worktree', operation: err.operation, status: err.status }, 409);
3709
3734
  }
3710
3735
  const message = err instanceof Error ? err.message : String(err);
3711
- return c.json({ error: message }, 500);
3736
+ return c.json({ error: message }, workspaceErrorStatus(err));
3712
3737
  }
3713
3738
  });
3714
3739
  /** Merge the source branch into the workspace branch (non-fast-forward). */
@@ -3731,7 +3756,7 @@ app.post('/:id/merge', async (c) => {
3731
3756
  return c.json({ error: err.message, code: 'dirty_worktree', operation: err.operation, status: err.status }, 409);
3732
3757
  }
3733
3758
  const message = err instanceof Error ? err.message : String(err);
3734
- return c.json({ error: message }, 500);
3759
+ return c.json({ error: message }, workspaceErrorStatus(err));
3735
3760
  }
3736
3761
  });
3737
3762
  /** Abort any in-progress merge or rebase in the worktree. */
@@ -3747,7 +3772,7 @@ app.post('/:id/git/abort', (c) => {
3747
3772
  }
3748
3773
  catch (err) {
3749
3774
  const message = err instanceof Error ? err.message : String(err);
3750
- return c.json({ error: message }, 500);
3775
+ return c.json({ error: message }, workspaceErrorStatus(err));
3751
3776
  }
3752
3777
  });
3753
3778
  /** Continue an in-progress merge, rebase or cherry-pick after resolution. */
@@ -3764,7 +3789,7 @@ app.post('/:id/git/continue', (c) => {
3764
3789
  }
3765
3790
  catch (err) {
3766
3791
  const message = err instanceof Error ? err.message : String(err);
3767
- return c.json({ error: message }, 500);
3792
+ return c.json({ error: message }, workspaceErrorStatus(err));
3768
3793
  }
3769
3794
  });
3770
3795
  /** Stage + commit all working-tree changes (recovery action for a dirty rebase/merge). */
@@ -3783,7 +3808,7 @@ app.post('/:id/git/commit-all', async (c) => {
3783
3808
  }
3784
3809
  catch (err) {
3785
3810
  const message = err instanceof Error ? err.message : String(err);
3786
- return c.json({ error: message }, 500);
3811
+ return c.json({ error: message }, workspaceErrorStatus(err));
3787
3812
  }
3788
3813
  });
3789
3814
  /** Ask the workspace agent to inspect and commit the current working tree. */
@@ -3835,7 +3860,7 @@ app.post('/:id/git/discard', (c) => {
3835
3860
  }
3836
3861
  catch (err) {
3837
3862
  const message = err instanceof Error ? err.message : String(err);
3838
- return c.json({ error: message }, 500);
3863
+ return c.json({ error: message }, workspaceErrorStatus(err));
3839
3864
  }
3840
3865
  });
3841
3866
  /** Hand off merge/rebase conflicts to the workspace agent with an intelligent-resolution prompt. */
@@ -3906,7 +3931,7 @@ Start now.`;
3906
3931
  }
3907
3932
  catch (err) {
3908
3933
  const message = err instanceof Error ? err.message : String(err);
3909
- return c.json({ error: message }, 500);
3934
+ return c.json({ error: message }, workspaceErrorStatus(err));
3910
3935
  }
3911
3936
  });
3912
3937
  /** Change the base branch of an existing PR via the resolved forge provider. */
@@ -3943,7 +3968,7 @@ app.post('/:id/change-pr-base', async (c) => {
3943
3968
  return c.json({ error: err.message, code: 'forge_unavailable' }, 409);
3944
3969
  }
3945
3970
  const message = err instanceof Error ? err.message : String(err);
3946
- return c.json({ error: message }, 500);
3971
+ return c.json({ error: message }, workspaceErrorStatus(err));
3947
3972
  }
3948
3973
  });
3949
3974
  /** Merge a ready, open PR/MR through its resolved forge provider. */
@@ -3980,7 +4005,7 @@ app.post('/:id/merge-pr', async (c) => {
3980
4005
  return c.json({ error: err.message, code: 'forge_unavailable' }, 409);
3981
4006
  }
3982
4007
  const message = err instanceof Error ? err.message : String(err);
3983
- return c.json({ error: message }, 500);
4008
+ return c.json({ error: message }, workspaceErrorStatus(err));
3984
4009
  }
3985
4010
  });
3986
4011
  /** Delete the workspace branch from the remote after its PR/MR was merged. */
@@ -4005,7 +4030,7 @@ app.post('/:id/delete-remote-branch', async (c) => {
4005
4030
  }
4006
4031
  catch (err) {
4007
4032
  const message = err instanceof Error ? err.message : String(err);
4008
- return c.json({ error: message }, 500);
4033
+ return c.json({ error: message }, workspaceErrorStatus(err));
4009
4034
  }
4010
4035
  });
4011
4036
  /** Re-target a workspace onto a new source branch (metadata + worktree + PR base). */
@@ -4077,13 +4102,17 @@ app.post('/:id/cancel-source-change', async (c) => {
4077
4102
  if (backups.length === 0) {
4078
4103
  return c.json({ error: 'No backup branch found — cannot auto-restore', code: 'no_backup' }, 409);
4079
4104
  }
4080
- gitOps.restoreBranchFromBackup(workspace.worktreePath, workspace.workingBranch, backups[0]);
4105
+ if (agentManager.hasController(id))
4106
+ return c.json({ error: 'Stop the agent before restoring the branch' }, 409);
4107
+ await withGitRepoLock(workspace.worktreePath, async () => {
4108
+ gitOps.restoreBranchFromBackup(workspace.worktreePath, workspace.workingBranch, backups[0]);
4109
+ });
4081
4110
  workspaceService.updateWorkspaceSourceBranch(id, body.previousBase);
4082
4111
  return c.json({ success: true, restoredFrom: backups[0] });
4083
4112
  }
4084
4113
  catch (err) {
4085
4114
  const message = err instanceof Error ? err.message : String(err);
4086
- return c.json({ error: message }, 500);
4115
+ return c.json({ error: message }, workspaceErrorStatus(err));
4087
4116
  }
4088
4117
  });
4089
4118
  /** Force-push the working branch with --force-with-lease (after a history rewrite). */
@@ -4119,7 +4148,7 @@ app.post('/:id/force-push', async (c) => {
4119
4148
  }
4120
4149
  catch (err) {
4121
4150
  const message = err instanceof Error ? err.message : String(err);
4122
- return c.json({ error: message }, 500);
4151
+ return c.json({ error: message }, workspaceErrorStatus(err));
4123
4152
  }
4124
4153
  });
4125
4154
  // POST /api/workspaces/:id/open-pr — create a GitHub PR and send a templated prompt to the agent
@@ -4239,7 +4268,7 @@ app.post('/:id/open-pr', async (c) => {
4239
4268
  }
4240
4269
  catch (err) {
4241
4270
  const message = err instanceof Error ? err.message : String(err);
4242
- return c.json({ error: message }, 500);
4271
+ return c.json({ error: message }, workspaceErrorStatus(err));
4243
4272
  }
4244
4273
  });
4245
4274
  // POST /api/workspaces/:id/start-review — ask the agent to review committed + uncommitted changes
@@ -4302,11 +4331,12 @@ app.post('/:id/start-review', async (c) => {
4302
4331
  if (newSession) {
4303
4332
  // Stop current agent, wait for it, then start fresh.
4304
4333
  try {
4305
- await agentManager.stopAgentAndWait(workspace.id);
4334
+ assertAgentStopped(await agentManager.stopAgentAndWait(workspace.id, undefined, 'replacement'));
4306
4335
  }
4307
4336
  catch (err) {
4308
4337
  const msg = err instanceof Error ? err.message : String(err);
4309
4338
  console.error(`[start-review] stopAgentAndWait failed (continuing): ${msg}`);
4339
+ throw err;
4310
4340
  }
4311
4341
  try {
4312
4342
  const agent = agentManager.startAgent(workspace.id, worktreePath, rendered, workspace.model, false /* resume */, workspace.agentPermissionMode, undefined, workspace.reasoningEffort);
@@ -4339,7 +4369,7 @@ app.post('/:id/start-review', async (c) => {
4339
4369
  }
4340
4370
  catch (err) {
4341
4371
  const message = err instanceof Error ? err.message : String(err);
4342
- return c.json({ error: message }, 500);
4372
+ return c.json({ error: message }, workspaceErrorStatus(err));
4343
4373
  }
4344
4374
  });
4345
4375
  // POST /api/workspaces/:id/start-ci-fix — dispatch the configured CI-fix
@@ -4405,7 +4435,7 @@ app.post('/:id/start-ci-fix', migrationGuard, async (c) => {
4405
4435
  }
4406
4436
  catch (err) {
4407
4437
  const message = err instanceof Error ? err.message : String(err);
4408
- return c.json({ error: message }, 500);
4438
+ return c.json({ error: message }, workspaceErrorStatus(err));
4409
4439
  }
4410
4440
  });
4411
4441
  /**
@@ -4435,7 +4465,7 @@ app.post('/:id/dismiss-pr-attention', async (c) => {
4435
4465
  }
4436
4466
  catch (err) {
4437
4467
  const message = err instanceof Error ? err.message : String(err);
4438
- return c.json({ error: message }, 500);
4468
+ return c.json({ error: message }, workspaceErrorStatus(err));
4439
4469
  }
4440
4470
  });
4441
4471
  /**
@@ -4461,7 +4491,7 @@ app.post('/:id/restore-pr-attention', async (c) => {
4461
4491
  }
4462
4492
  catch (err) {
4463
4493
  const message = err instanceof Error ? err.message : String(err);
4464
- return c.json({ error: message }, 500);
4494
+ return c.json({ error: message }, workspaceErrorStatus(err));
4465
4495
  }
4466
4496
  });
4467
4497
  /** POST /api/workspaces/:id/mark-read — mark workspace as read (clear unread indicator). */
@@ -4478,7 +4508,7 @@ app.post('/:id/mark-read', (c) => {
4478
4508
  }
4479
4509
  catch (err) {
4480
4510
  const message = err instanceof Error ? err.message : String(err);
4481
- return c.json({ error: message }, 500);
4511
+ return c.json({ error: message }, workspaceErrorStatus(err));
4482
4512
  }
4483
4513
  });
4484
4514
  // POST /api/workspaces/:id/stop — stop agent
@@ -4491,11 +4521,12 @@ app.post('/:id/stop', migrationGuard, async (c) => {
4491
4521
  }
4492
4522
  try {
4493
4523
  // The response says "stopped": make that true before returning.
4494
- await agentManager.stopAgentAndWait(id);
4524
+ assertAgentStopped(await agentManager.stopAgentAndWait(id));
4495
4525
  }
4496
4526
  catch (err) {
4497
4527
  // Agent may not be tracked (e.g. server restarted) — just update status
4498
4528
  console.error(`[workspaces] stopAgentAndWait on manual stop failed for '${id}':`, err);
4529
+ throw err;
4499
4530
  }
4500
4531
  // Always transition to idle so the UI reflects the stopped state
4501
4532
  try {
@@ -4508,7 +4539,7 @@ app.post('/:id/stop', migrationGuard, async (c) => {
4508
4539
  }
4509
4540
  catch (err) {
4510
4541
  const message = err instanceof Error ? err.message : String(err);
4511
- return c.json({ error: message }, 500);
4542
+ return c.json({ error: message }, workspaceErrorStatus(err));
4512
4543
  }
4513
4544
  });
4514
4545
  // POST /api/workspaces/:id/interrupt — soft-interrupt agent (SIGINT, like Escape in Claude Code)
@@ -4555,7 +4586,7 @@ app.post('/:id/interrupt', migrationGuard, async (c) => {
4555
4586
  const status = err.code === 'interrupt_failed' ? 500 : 409;
4556
4587
  return c.json({ error: message, code: err.code }, status);
4557
4588
  }
4558
- return c.json({ error: message }, 500);
4589
+ return c.json({ error: message }, workspaceErrorStatus(err));
4559
4590
  }
4560
4591
  });
4561
4592
  export default app;