@loicngr/kobo 2.1.4 → 2.1.6

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 (173) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONFIGURATION.md +11 -3
  3. package/dist/server/index.js +24 -91
  4. package/dist/server/routes/git.js +6 -7
  5. package/dist/server/routes/settings.js +21 -11
  6. package/dist/server/routes/workspaces.js +84 -74
  7. package/dist/server/services/agent/engines/claude-code/event-mapper.js +33 -0
  8. package/dist/server/services/agent/engines/codex/engine.js +10 -1
  9. package/dist/server/services/agent/engines/codex/event-mapper.js +2 -0
  10. package/dist/server/services/agent/integration-mcp.js +4 -3
  11. package/dist/server/services/agent/orchestrator.js +1 -1
  12. package/dist/server/services/agent/streaming-batcher.js +14 -3
  13. package/dist/server/services/engine-handoff-service.js +120 -24
  14. package/dist/server/services/pr-checkout-service.js +90 -53
  15. package/dist/server/services/pr-watcher-service.js +13 -7
  16. package/dist/server/services/retention/worker.js +40 -0
  17. package/dist/server/services/review-service.js +8 -4
  18. package/dist/server/services/session-event-metrics.js +43 -0
  19. package/dist/server/services/session-handoff-service.js +6 -4
  20. package/dist/server/services/templates-service.js +67 -49
  21. package/dist/server/services/terminal-service.js +45 -14
  22. package/dist/server/services/terminal-websocket-service.js +143 -0
  23. package/dist/server/services/wakeup-service.js +23 -21
  24. package/dist/server/services/workspace-service.js +3 -44
  25. package/dist/server/services/workspace-template-service.js +16 -18
  26. package/dist/server/services/worktree-purge-service.js +1 -4
  27. package/dist/server/services/ws-events-retention-service.js +197 -104
  28. package/dist/server/services/ws-events-retention-worker-service.js +112 -0
  29. package/dist/server/utils/atomic-json-file.js +23 -0
  30. package/dist/server/utils/git-ops.js +262 -60
  31. package/dist/shared/codex-models.js +20 -3
  32. package/dist/shared/models.js +12 -0
  33. package/package.json +6 -6
  34. package/src/client/dist/pwa/assets/ActivityFeed-BO1mKMJ0.js +1 -0
  35. package/src/client/dist/pwa/assets/ActivityFeed-ZTxXVmNM.css +1 -0
  36. package/src/client/dist/pwa/assets/ChangelogPage-BXNz7PGb.js +1 -0
  37. package/src/client/dist/pwa/assets/ChangelogPage-DVyx9ZjV.css +1 -0
  38. package/src/client/dist/pwa/assets/ComparisonPanel-ByVbqYCs.css +1 -0
  39. package/src/client/dist/pwa/assets/ComparisonPanel-CfmJHeH_.js +2 -0
  40. package/src/client/dist/pwa/assets/CreatePage-6_AP5zso.js +2 -0
  41. package/src/client/dist/pwa/assets/CreatePage-CMyl2j8Z.css +1 -0
  42. package/src/client/dist/pwa/assets/DashboardPage-BKCrV6u0.js +1 -0
  43. package/src/client/dist/pwa/assets/DashboardPage-CtaHaVdf.css +1 -0
  44. package/src/client/dist/pwa/assets/DiffViewer-BMzrb9fB.js +8 -0
  45. package/src/client/dist/pwa/assets/DocumentsPanel-CqGg38Oi.js +1 -0
  46. package/src/client/dist/pwa/assets/{DrawerToggleButton-Bh4FXdtJ.js → DrawerToggleButton-DKytIBlk.js} +1 -1
  47. package/src/client/dist/pwa/assets/{GitPanel-C4wOcZV2.css → GitPanel-C3blk_Pd.css} +1 -1
  48. package/src/client/dist/pwa/assets/GitPanel-DRzhrs_V.js +3 -0
  49. package/src/client/dist/pwa/assets/HealthPage-DT8FGrKA.css +1 -0
  50. package/src/client/dist/pwa/assets/HealthPage-DWotmfkh.js +1 -0
  51. package/src/client/dist/pwa/assets/MainLayout-DkSavoiV.css +1 -0
  52. package/src/client/dist/pwa/assets/MainLayout-fRVZ01LN.js +2 -0
  53. package/src/client/dist/pwa/assets/QBanner-mzrnn82R.js +1 -0
  54. package/src/client/dist/pwa/assets/{QBtnDropdown-DVbxGTNb.js → QBtnDropdown-BGGvQIvH.js} +1 -1
  55. package/src/client/dist/pwa/assets/QChip-Chu7r6RZ.js +1 -0
  56. package/src/client/dist/pwa/assets/QExpansionItem-C2XXHOHk.js +1 -0
  57. package/src/client/dist/pwa/assets/QItemSection-CR3Kfibl.js +1 -0
  58. package/src/client/dist/pwa/assets/QList-DnezW6gW.js +1 -0
  59. package/src/client/dist/pwa/assets/QMarkupTable-wuDf50Oh.js +1 -0
  60. package/src/client/dist/pwa/assets/QScrollArea-BnmQ9US7.js +1 -0
  61. package/src/client/dist/pwa/assets/QSelect-BskTzVCM.js +1 -0
  62. package/src/client/dist/pwa/assets/{QTooltip-BLhkNT4s.js → QTooltip-CO84H0cq.js} +1 -1
  63. package/src/client/dist/pwa/assets/SchedulePanel-CBM_GpS3.css +1 -0
  64. package/src/client/dist/pwa/assets/SchedulePanel-CPirTYw5.js +1 -0
  65. package/src/client/dist/pwa/assets/SearchPage-CnT0-Hpe.js +1 -0
  66. package/src/client/dist/pwa/assets/{SettingsPage-BYdLXJEJ.css → SettingsPage-CpykRKq2.css} +1 -1
  67. package/src/client/dist/pwa/assets/SettingsPage-DCMZL7GO.js +9 -0
  68. package/src/client/dist/pwa/assets/SplitWorkspacePage-DwWsD7f5.js +1 -0
  69. package/src/client/dist/pwa/assets/{TerminalPanel-D2j5AA8S.js → TerminalPanel-C4dYabx6.js} +2 -2
  70. package/src/client/dist/pwa/assets/{TerminalPanel-DbVZk0vD.css → TerminalPanel-Dz9EE-Ph.css} +1 -1
  71. package/src/client/dist/pwa/assets/ToolCallItem-CjXWiKJc.js +8 -0
  72. package/src/client/dist/pwa/assets/ToolCallItem-DhjdG8vp.css +1 -0
  73. package/src/client/dist/pwa/assets/{TouchPan-C0EqFcrr.js → TouchPan-6fI8xFPq.js} +1 -1
  74. package/src/client/dist/pwa/assets/{TourReplayButton-DlMvPblb.js → TourReplayButton-DhbNI6O5.js} +1 -1
  75. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CZxW7QSq.js +1 -0
  76. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DSw8Upcp.css +1 -0
  77. package/src/client/dist/pwa/assets/WorkspacePage-B0wCQdev.js +4 -0
  78. package/src/client/dist/pwa/assets/{WorkspacePage--5HsnMQ1.css → WorkspacePage-BnW-gp3u.css} +1 -1
  79. package/src/client/dist/pwa/assets/{browser-DNEbLF-4.js → browser-CTkGHrN5.js} +1 -1
  80. package/src/client/dist/pwa/assets/build-path-tree-Bd50EZMW.js +1 -0
  81. package/src/client/dist/pwa/assets/custom-sounds-DoPDmcY5.js +1 -0
  82. package/src/client/dist/pwa/assets/de-BPtzBQu0.js +17 -0
  83. package/src/client/dist/pwa/assets/defineProperty-BbfpZ9Tg.js +1 -0
  84. package/src/client/dist/pwa/assets/{editor.api-PENN0io6.js → editor.api-CKcTKXcM.js} +1 -1
  85. package/src/client/dist/pwa/assets/efforts-CDsBbk8o.js +9 -0
  86. package/src/client/dist/pwa/assets/es-BoNOfG0w.js +17 -0
  87. package/src/client/dist/pwa/assets/expand-template-D6zFPzzB.js +1 -0
  88. package/src/client/dist/pwa/assets/{format-CVI7mr7G.js → format-MtzcnR3z.js} +7 -7
  89. package/src/client/dist/pwa/assets/formatters-DCjBZ47F.js +1 -0
  90. package/src/client/dist/pwa/assets/fr-Bu92fK5U.js +17 -0
  91. package/src/client/dist/pwa/assets/{freemarker2-BxN_Aq6C.js → freemarker2-JGUTjYvR.js} +1 -1
  92. package/src/client/dist/pwa/assets/{handlebars-C9UHXtO1.js → handlebars-BWStl2Hc.js} +1 -1
  93. package/src/client/dist/pwa/assets/{html-CTH9wSJr.js → html-CjGCxUEe.js} +1 -1
  94. package/src/client/dist/pwa/assets/i18n-DKj-umtn.js +19 -0
  95. package/src/client/dist/pwa/assets/i18n-DtmbeJu8.js +1 -0
  96. package/src/client/dist/pwa/assets/index-BPmUmc6D.js +2 -0
  97. package/src/client/dist/pwa/assets/it-CrA4Q7oc.js +17 -0
  98. package/src/client/dist/pwa/assets/{javascript-WT4BS8p4.js → javascript-C_jd0j2i.js} +1 -1
  99. package/src/client/dist/pwa/assets/latest-request-CBPb8UWQ.js +1 -0
  100. package/src/client/dist/pwa/assets/{liquid-D6dfe-gU.js → liquid-DMyQVGkp.js} +1 -1
  101. package/src/client/dist/pwa/assets/{mdx-B8pNXNeC.js → mdx-DeW5qzOU.js} +1 -1
  102. package/src/client/dist/pwa/assets/{monaco-contributions-COLfslyi.js → monaco-contributions-Be_o6s3m.js} +1 -1
  103. package/src/client/dist/pwa/assets/monaco.contribution-VUGNbVME.js +2 -0
  104. package/src/client/dist/pwa/assets/network-auth-CSx0jS5X.js +1 -0
  105. package/src/client/dist/pwa/assets/{notify-theme-7MsG-MyN.js → notify-theme-uhSG2r09.js} +1 -1
  106. package/src/client/dist/pwa/assets/{permissionModes-B-ciuUbz.js → permissionModes-Cc5VaOdC.js} +1 -1
  107. package/src/client/dist/pwa/assets/preload-helper-DjzSQ1t3.js +1 -0
  108. package/src/client/dist/pwa/assets/{project-color-Bo6iEcqN.js → project-color-BQbOnYBN.js} +1 -1
  109. package/src/client/dist/pwa/assets/{python-D2gb6Jjz.js → python-C1v3VovI.js} +1 -1
  110. package/src/client/dist/pwa/assets/{razor-dksrCQF2.js → razor-DMEmKSdO.js} +1 -1
  111. package/src/client/dist/pwa/assets/{render-chat-markdown-px_6WDIX.js → render-chat-markdown-CafzCcmu.js} +1 -1
  112. package/src/client/dist/pwa/assets/{typescript-DTPyRTrW.js → typescript-rKyiSQID.js} +1 -1
  113. package/src/client/dist/pwa/assets/use-checkbox-BqDMQ6x7.js +1 -0
  114. package/src/client/dist/pwa/assets/use-search-index-status-BL3cb04x.js +1 -0
  115. package/src/client/dist/pwa/assets/{use-tours-b00RruX_.js → use-tours-Dum-bOyQ.js} +2 -2
  116. package/src/client/dist/pwa/assets/use-worktree-restore-BQyoz3Qa.js +1 -0
  117. package/src/client/dist/pwa/assets/workspace-templates-YRwFjkUh.js +1 -0
  118. package/src/client/dist/pwa/assets/{xml-mxUZCM4r.js → xml-U_pDcywM.js} +1 -1
  119. package/src/client/dist/pwa/assets/{yaml-BuN8sUWC.js → yaml-CRCd_ET9.js} +1 -1
  120. package/src/client/dist/pwa/index.html +6 -3
  121. package/src/client/dist/pwa/sw.js +1 -1
  122. package/src/client/dist/pwa/assets/ActivityFeed-DCmiJP-z.css +0 -1
  123. package/src/client/dist/pwa/assets/ActivityFeed-DdGARu8o.js +0 -8
  124. package/src/client/dist/pwa/assets/ChangelogPage-CGenjiI5.js +0 -1
  125. package/src/client/dist/pwa/assets/ChangelogPage-DKJkFiU_.css +0 -1
  126. package/src/client/dist/pwa/assets/ComparisonPanel-CRRaDs2D.css +0 -1
  127. package/src/client/dist/pwa/assets/ComparisonPanel-aXgjEulK.js +0 -2
  128. package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +0 -1
  129. package/src/client/dist/pwa/assets/CreatePage-BaB_30xJ.js +0 -2
  130. package/src/client/dist/pwa/assets/DashboardPage-DsbwmO2W.js +0 -1
  131. package/src/client/dist/pwa/assets/DashboardPage-XiCj24i2.css +0 -1
  132. package/src/client/dist/pwa/assets/DiffViewer--dUMf7b3.js +0 -8
  133. package/src/client/dist/pwa/assets/DocumentsPanel-DTjEUYUd.js +0 -1
  134. package/src/client/dist/pwa/assets/GitPanel-PUPLj0M8.js +0 -3
  135. package/src/client/dist/pwa/assets/HealthPage-BHgucifz.js +0 -1
  136. package/src/client/dist/pwa/assets/HealthPage-CFZ-tdKK.css +0 -1
  137. package/src/client/dist/pwa/assets/MainLayout-CNmwCnDD.css +0 -1
  138. package/src/client/dist/pwa/assets/MainLayout-CQVLnTX3.js +0 -2
  139. package/src/client/dist/pwa/assets/QBanner-XE0EMe9L.js +0 -1
  140. package/src/client/dist/pwa/assets/QChip-Bt_JvnHb.js +0 -1
  141. package/src/client/dist/pwa/assets/QExpansionItem-CxEBY3jY.js +0 -1
  142. package/src/client/dist/pwa/assets/QItemSection-Cf6OZEH4.js +0 -1
  143. package/src/client/dist/pwa/assets/QList-Z90BspT4.js +0 -1
  144. package/src/client/dist/pwa/assets/QMarkupTable-Bwjknbmw.js +0 -1
  145. package/src/client/dist/pwa/assets/QSelect-CMzJDhi-.js +0 -1
  146. package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +0 -1
  147. package/src/client/dist/pwa/assets/SchedulePanel-DTVwe7sX.js +0 -1
  148. package/src/client/dist/pwa/assets/SearchPage-CtrR7dNr.js +0 -1
  149. package/src/client/dist/pwa/assets/SettingsPage-Bl_RNTGi.js +0 -9
  150. package/src/client/dist/pwa/assets/SplitWorkspacePage-Bac_Pzmm.js +0 -1
  151. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-BAk5l4Ik.js +0 -1
  152. package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CrLP_I5S.css +0 -1
  153. package/src/client/dist/pwa/assets/WorkspacePage-FScR5YAD.js +0 -4
  154. package/src/client/dist/pwa/assets/build-path-tree-C0b61nxI.js +0 -1
  155. package/src/client/dist/pwa/assets/custom-sounds-DE4JLdGG.js +0 -1
  156. package/src/client/dist/pwa/assets/de-DgZ9jKq_.js +0 -17
  157. package/src/client/dist/pwa/assets/documents-CALwmao-.js +0 -1
  158. package/src/client/dist/pwa/assets/efforts-Detdm-FH.js +0 -9
  159. package/src/client/dist/pwa/assets/es-zvlgzw9H.js +0 -17
  160. package/src/client/dist/pwa/assets/expand-template-ZIVLPdW2.js +0 -1
  161. package/src/client/dist/pwa/assets/formatters-C_zcfJBt.js +0 -1
  162. package/src/client/dist/pwa/assets/fr-BICzq59I.js +0 -17
  163. package/src/client/dist/pwa/assets/i18n-DG_vChZ3.js +0 -1
  164. package/src/client/dist/pwa/assets/index-fuLr-ZVa.js +0 -20
  165. package/src/client/dist/pwa/assets/it-BIGArQrS.js +0 -17
  166. package/src/client/dist/pwa/assets/monaco.contribution-CDhuQbzi.js +0 -2
  167. package/src/client/dist/pwa/assets/network-auth-QouPCx6A.js +0 -1
  168. package/src/client/dist/pwa/assets/pending-diff-open-CX24u9gW.js +0 -1
  169. package/src/client/dist/pwa/assets/pinia-BdFjQEqa.js +0 -1
  170. package/src/client/dist/pwa/assets/use-checkbox-Bh9EQhkV.js +0 -1
  171. package/src/client/dist/pwa/assets/use-search-index-status-_nCyACc7.js +0 -1
  172. package/src/client/dist/pwa/assets/use-worktree-restore-BDOZc9fk.js +0 -1
  173. package/src/client/dist/pwa/assets/workspace-templates-DV9Njz0T.js +0 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,14 @@ All notable changes to Kōbō are documented here. The format is based on
4
4
  [Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
5
5
  section — the in-app "What's new" dialog reads this file.
6
6
 
7
+ ## 2.1.6
8
+
9
+ - feat(subagents): add focused activity views
10
+
11
+ ## 2.1.5
12
+
13
+ - fix(workspace): improve reliability and git controls
14
+
7
15
  ## 2.1.4
8
16
 
9
17
  - chore(deps): update server client and pwa packages
package/CONFIGURATION.md CHANGED
@@ -751,9 +751,17 @@ both global (the database is single):
751
751
  | Events always kept per workspace | `0` | The most recent events of each workspace survive whatever their age. Set it (5000 is a good starting point) at the same time you enable retention, so an old but still-open workspace keeps a readable history. |
752
752
 
753
753
  The prune runs **at server startup and every 24 hours** while the backend stays
754
- running. Each pass reads the current settings and deletes in batches of 5,000
755
- rows. When enough pages have been freed, a `VACUUM` returns the space to the
756
- filesystem.
754
+ running. Each pass reads the current settings, calculates the protected workspace
755
+ tails once, and deletes in batches of 500 rows on a dedicated worker. Preview
756
+ counts also run on that worker, so maintenance does not block HTTP or agent
757
+ event handling. Shutdown waits for the worker to finish its current batch and
758
+ reconcile affected metrics before closing the database.
759
+
760
+ Freed database pages are reused for new events. Live maintenance performs a
761
+ non-blocking WAL checkpoint and deliberately does not run a full `VACUUM`, which
762
+ would hold SQLite's writer lock and interrupt live agents. To return database
763
+ file space to the filesystem immediately, stop Kōbō and perform an explicit
764
+ offline SQLite `VACUUM` after making a backup.
757
765
 
758
766
  **What it deletes:** eligible rows of `ws_events` — the recorded agent output.
759
767
  Affected session metrics and the derived search index are reconciled with the
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { serve } from '@hono/node-server';
5
5
  import { Hono } from 'hono';
6
- import WebSocket, { WebSocketServer } from 'ws';
6
+ import { WebSocketServer } from 'ws';
7
7
  import { closeDb, getDb } from './db/index.js';
8
8
  import { getPendingMigrations, runMigrations } from './db/migrations.js';
9
9
  import { hostCheckMiddleware } from './middleware/host-check-middleware.js';
@@ -49,14 +49,16 @@ import { startSearchIndex, stopSearchIndex } from './services/search-service.js'
49
49
  import { reconcileSessionHandoffs } from './services/session-handoff-service.js';
50
50
  import { getGlobalSettings, updateNetworkAccessSettings } from './services/settings-service.js';
51
51
  import { reloadDefaultTemplates } from './services/templates-service.js';
52
- import { createTerminal, destroyAllTerminals, getTerminal } from './services/terminal-service.js';
52
+ import { destroyAllTerminals } from './services/terminal-service.js';
53
+ import { handleTerminalConnection } from './services/terminal-websocket-service.js';
53
54
  import { startUpdateChecker, stopUpdateChecker } from './services/update-check-service.js';
54
55
  import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
55
56
  import * as wakeupService from './services/wakeup-service.js';
56
57
  import { handleConnection, setMessageHandler } from './services/websocket-service.js';
57
58
  import { deliverWorkspaceMessage } from './services/workspace-message-service.js';
58
59
  import { getWorkspace } from './services/workspace-service.js';
59
- import { pruneWsEvents, resolveRetentionConfig } from './services/ws-events-retention-service.js';
60
+ import { resolveRetentionConfig } from './services/ws-events-retention-service.js';
61
+ import { runWsEventsRetention, stopWsEventsRetention } from './services/ws-events-retention-worker-service.js';
60
62
  import { getChangelogPath, getClientSpaPath, getDbPath, getKoboHome, getPackageVersion, resolveSpaFile, } from './utils/paths.js';
61
63
  import { formatStartupBanner, readStartupNotes, startStartupSpinner, startupDebug } from './utils/startup-display.js';
62
64
  startupDebug(`[kobo] Kōbō home: ${getKoboHome()}`);
@@ -87,13 +89,13 @@ startSearchIndex();
87
89
  // conversation on the next upgrade, which is precisely why the UI shows a
88
90
  // count and asks first.
89
91
  // Best-effort — a failure must never block boot.
90
- function runRetentionPass(context) {
92
+ async function runRetentionPass(context) {
91
93
  try {
92
94
  const retentionConfig = resolveRetentionConfig(getGlobalSettings());
93
95
  if (retentionConfig.retentionDays <= 0)
94
96
  return;
95
97
  const retentionStartedAt = Date.now();
96
- const retention = pruneWsEvents(db, retentionConfig);
98
+ const retention = await runWsEventsRetention(retentionConfig);
97
99
  if (retention.deleted > 0 || retention.vacuumed) {
98
100
  console.log(`[kobo] Event retention ${context} (${retentionConfig.retentionDays} d, keeping ${retentionConfig.keepPerWorkspace}/workspace): ` +
99
101
  `${retention.deleted} agent event(s) permanently deleted, ` +
@@ -106,12 +108,12 @@ function runRetentionPass(context) {
106
108
  console.error('[kobo] Event retention failed (continuing):', err);
107
109
  }
108
110
  }
109
- runRetentionPass('at boot');
111
+ void runRetentionPass('at boot');
110
112
  // Kōbō is a daemon people leave running for weeks, and `emit` writes a row per
111
113
  // agent event. Running the pass only at boot meant a window the user enabled
112
114
  // was not applied again until the next restart.
113
115
  const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1000;
114
- const retentionTimer = setInterval(() => runRetentionPass('daily'), RETENTION_INTERVAL_MS);
116
+ const retentionTimer = setInterval(() => void runRetentionPass('daily'), RETENTION_INTERVAL_MS);
115
117
  retentionTimer.unref?.();
116
118
  // Check on boot and hourly so servers running longer than a day keep backing up.
117
119
  const dailyBackupScheduler = startDailyDbBackupScheduler(async () => {
@@ -278,87 +280,7 @@ wss.on('connection', (ws) => {
278
280
  });
279
281
  // Wire terminal WebSocket connections
280
282
  terminalWss.on('connection', (ws, workspaceId) => {
281
- let currentPty = getTerminal(workspaceId);
282
- let dataDisposable = null;
283
- let exitDisposable = null;
284
- function attachListeners(ptyInstance) {
285
- // Dispose previous listeners to avoid stacking on reconnect
286
- dataDisposable?.dispose();
287
- exitDisposable?.dispose();
288
- dataDisposable = ptyInstance.onData((output) => {
289
- if (ws.readyState === WebSocket.OPEN) {
290
- ws.send(Buffer.from(output), { binary: true });
291
- }
292
- });
293
- exitDisposable = ptyInstance.onExit(({ exitCode }) => {
294
- if (ws.readyState === WebSocket.OPEN) {
295
- ws.send(JSON.stringify({ type: 'exited', code: exitCode }));
296
- ws.close();
297
- }
298
- });
299
- }
300
- ws.on('close', () => {
301
- dataDisposable?.dispose();
302
- exitDisposable?.dispose();
303
- dataDisposable = null;
304
- exitDisposable = null;
305
- });
306
- ws.on('error', (err) => {
307
- console.error(`[terminal] WebSocket error for workspace ${workspaceId}:`, err);
308
- });
309
- ws.on('message', (data, isBinary) => {
310
- if (isBinary) {
311
- if (currentPty) {
312
- currentPty.write(data.toString());
313
- }
314
- return;
315
- }
316
- let msg;
317
- try {
318
- msg = JSON.parse(data.toString());
319
- }
320
- catch {
321
- return; // Invalid JSON — ignore
322
- }
323
- if (msg.type === 'create') {
324
- if (!currentPty) {
325
- const workspace = getWorkspace(workspaceId);
326
- if (!workspace) {
327
- ws.send(JSON.stringify({ type: 'error', message: 'Workspace not found' }));
328
- return;
329
- }
330
- if (workspace.archivedAt) {
331
- ws.send(JSON.stringify({ type: 'error', message: 'Workspace is archived' }));
332
- return;
333
- }
334
- const cwd = workspace.worktreePath;
335
- try {
336
- currentPty = createTerminal(workspaceId, cwd);
337
- }
338
- catch (err) {
339
- const message = err instanceof Error ? err.message : String(err);
340
- ws.send(JSON.stringify({ type: 'error', message }));
341
- return;
342
- }
343
- }
344
- attachListeners(currentPty);
345
- ws.send(JSON.stringify({ type: 'ready' }));
346
- return;
347
- }
348
- if (msg.type === 'resize' && msg.cols && msg.rows) {
349
- if (currentPty) {
350
- const cols = Math.max(1, Math.floor(msg.cols));
351
- const rows = Math.max(1, Math.floor(msg.rows));
352
- try {
353
- currentPty.resize(cols, rows);
354
- }
355
- catch (err) {
356
- console.error(`[terminal] resize failed for workspace ${workspaceId}:`, err);
357
- }
358
- }
359
- return;
360
- }
361
- });
283
+ handleTerminalConnection(ws, workspaceId, () => isShuttingDown);
362
284
  });
363
285
  // Wire websocket-service message handler to the agent orchestrator
364
286
  setMessageHandler(async (type, payload) => {
@@ -411,6 +333,10 @@ setMessageHandler(async (type, payload) => {
411
333
  });
412
334
  // Handle WebSocket upgrade requests on /ws path
413
335
  server.on('upgrade', (request, socket, head) => {
336
+ if (isShuttingDown) {
337
+ socket.destroy();
338
+ return;
339
+ }
414
340
  const { pathname } = new URL(request.url ?? '/', `http://localhost:${PORT}`);
415
341
  const wsGlobal = getGlobalSettings();
416
342
  // WebSockets are exempt from the same-origin policy and send no preflight,
@@ -495,7 +421,7 @@ async function gracefulShutdown(signal, exitCode = 0) {
495
421
  for (const client of terminalWss.clients)
496
422
  client.close(1001, 'Server shutting down');
497
423
  try {
498
- destroyAllTerminals();
424
+ await destroyAllTerminals();
499
425
  console.log('[kobo] Terminals killed');
500
426
  }
501
427
  catch {
@@ -547,8 +473,15 @@ async function gracefulShutdown(signal, exitCode = 0) {
547
473
  }
548
474
  finally {
549
475
  try {
550
- await stopSearchIndex();
551
- await dailyBackupScheduler.stop();
476
+ const stopped = await Promise.allSettled([
477
+ stopWsEventsRetention(),
478
+ stopSearchIndex(),
479
+ dailyBackupScheduler.stop(),
480
+ ]);
481
+ for (const result of stopped) {
482
+ if (result.status === 'rejected')
483
+ console.error('[kobo] Background service shutdown failed:', result.reason);
484
+ }
552
485
  closeDb();
553
486
  console.log('[kobo] Database closed');
554
487
  }
@@ -1,18 +1,17 @@
1
1
  import { Hono } from 'hono';
2
2
  import { getDb } from '../db/index.js';
3
3
  import { listOrphanWorktrees } from '../services/worktree-service.js';
4
- import { listBranches, listRemoteBranches, listWorktreeFiles } from '../utils/git-ops.js';
4
+ import { listBranchesAsync, listRemoteBranchesAsync, listWorktreeFilesAsync } from '../utils/git-ops.js';
5
5
  /** Hono sub-router for git-related endpoints (branch listing). */
6
6
  const app = new Hono();
7
7
  // GET /api/git/branches?path=<repoPath> — list branches for a repo
8
- app.get('/branches', (c) => {
8
+ app.get('/branches', async (c) => {
9
9
  try {
10
10
  const repoPath = c.req.query('path');
11
11
  if (!repoPath) {
12
12
  return c.json({ error: 'Missing required query parameter: path' }, 400);
13
13
  }
14
- const local = listBranches(repoPath);
15
- const remote = listRemoteBranches(repoPath);
14
+ const [local, remote] = await Promise.all([listBranchesAsync(repoPath), listRemoteBranchesAsync(repoPath)]);
16
15
  return c.json({ local, remote });
17
16
  }
18
17
  catch (err) {
@@ -20,12 +19,12 @@ app.get('/branches', (c) => {
20
19
  return c.json({ error: message }, 500);
21
20
  }
22
21
  });
23
- /** Hard ceiling on the worktree listing. `listWorktreeFiles` already truncates
22
+ /** Hard ceiling on the worktree listing. `listWorktreeFilesAsync` already truncates
24
23
  * at this value; exposing it lets a caller ask for LESS, never for more. */
25
24
  const MAX_WORKTREE_FILES = 5000;
26
25
  // GET /api/git/files?path=<worktreePath>&limit=<n> — list a worktree's files
27
26
  // (tracked + untracked-but-not-ignored) for the chat's `@file` autocomplete.
28
- app.get('/files', (c) => {
27
+ app.get('/files', async (c) => {
29
28
  try {
30
29
  const repoPath = c.req.query('path');
31
30
  if (!repoPath) {
@@ -33,7 +32,7 @@ app.get('/files', (c) => {
33
32
  }
34
33
  const requested = Number.parseInt(c.req.query('limit') ?? '', 10);
35
34
  const limit = Number.isFinite(requested) && requested > 0 ? Math.min(requested, MAX_WORKTREE_FILES) : MAX_WORKTREE_FILES;
36
- return c.json({ files: listWorktreeFiles(repoPath, limit) });
35
+ return c.json({ files: await listWorktreeFilesAsync(repoPath, limit) });
37
36
  }
38
37
  catch (err) {
39
38
  const message = err instanceof Error ? err.message : String(err);
@@ -11,8 +11,8 @@ import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from '../services/settings-defaul
11
11
  import * as settingsService from '../services/settings-service.js';
12
12
  import { DEFAULT_CI_FIX_PROMPT_TEMPLATE, DEFAULT_FINALIZATION_PROMPT, DEFAULT_GIT_CONVENTIONS, DEFAULT_PR_PROMPT_TEMPLATE, } from '../services/settings-service.js';
13
13
  import { getSuitePrompts } from '../services/skill-suite-prompts.js';
14
- import { listTemplates, replaceAllTemplates } from '../services/templates-service.js';
15
- import { countPrunableWsEvents } from '../services/ws-events-retention-service.js';
14
+ import { listTemplates, replaceAllTemplates, validateTemplatesForReplacement } from '../services/templates-service.js';
15
+ import { previewWsEventsRetention } from '../services/ws-events-retention-worker-service.js';
16
16
  /** Hono sub-router for global and per-project settings CRUD. */
17
17
  const app = new Hono();
18
18
  // GET /api/settings — return full settings, credentials masked
@@ -67,17 +67,15 @@ app.get('/defaults', (c) => {
67
67
  // the confirmation dialog: enabling retention on a year-old database destroys
68
68
  // months of conversation in one go, and the user is entitled to the number
69
69
  // before agreeing to it.
70
- app.get('/ws-events-retention-preview', (c) => {
70
+ app.get('/ws-events-retention-preview', async (c) => {
71
71
  try {
72
72
  const days = Number.parseInt(c.req.query('days') ?? '', 10);
73
73
  const keep = Number.parseInt(c.req.query('keep') ?? '', 10);
74
74
  if (!Number.isInteger(days) || days < 0 || !Number.isInteger(keep) || keep < 0) {
75
75
  return c.json({ error: 'days and keep must be non-negative integers' }, 400);
76
76
  }
77
- const db = getDb();
78
- const total = db.prepare('SELECT COUNT(*) AS c FROM ws_events').get().c;
79
- const deletable = countPrunableWsEvents(db, { retentionDays: days, keepPerWorkspace: keep });
80
- return c.json({ deletable, total });
77
+ const preview = await previewWsEventsRetention({ retentionDays: days, keepPerWorkspace: keep });
78
+ return c.json(preview);
81
79
  }
82
80
  catch (err) {
83
81
  const message = err instanceof Error ? err.message : String(err);
@@ -252,11 +250,23 @@ app.get('/export', (c) => {
252
250
  app.post('/import', async (c) => {
253
251
  try {
254
252
  const body = (await c.req.json());
255
- // Validate settings first — throws on malformed payload before we touch disk.
253
+ // Validate templates before settings import writes anything. An invalid
254
+ // row must not leave an otherwise rejected bundle partially applied.
255
+ let validatedTemplates;
256
+ if (body?.templates !== undefined) {
257
+ try {
258
+ validatedTemplates = validateTemplatesForReplacement(body.templates);
259
+ }
260
+ catch (error) {
261
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
262
+ }
263
+ }
264
+ // Settings validates its section before writing. Each file is replaced
265
+ // atomically; this is not a transaction spanning the two files.
256
266
  settingsService.importConfigBundle(body);
257
- if (body.templates !== undefined) {
258
- // Accept missing templates (backward-compatible). Otherwise validate and replace.
259
- replaceAllTemplates(body.templates);
267
+ if (validatedTemplates !== undefined) {
268
+ // Accept missing templates for backward-compatible bundles.
269
+ replaceAllTemplates(validatedTemplates);
260
270
  }
261
271
  return c.json({ ok: true });
262
272
  }