@loicngr/kobo 2.1.2 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/CONFIGURATION.md +11 -3
- package/dist/server/index.js +24 -91
- package/dist/server/routes/git.js +6 -7
- package/dist/server/routes/settings.js +21 -11
- package/dist/server/routes/workspaces.js +82 -41
- package/dist/server/services/agent/engines/claude-code/engine.js +87 -20
- package/dist/server/services/agent/engines/claude-code/event-mapper.js +88 -22
- package/dist/server/services/agent/engines/claude-code/options-builder.js +1 -0
- package/dist/server/services/agent/engines/codex/engine.js +27 -5
- package/dist/server/services/agent/engines/codex/options-builder.js +1 -0
- package/dist/server/services/agent/integration-mcp.js +4 -3
- package/dist/server/services/agent/orchestrator.js +36 -1
- package/dist/server/services/agent/session-controller.js +12 -0
- package/dist/server/services/engine-handoff-service.js +120 -24
- package/dist/server/services/pr-checkout-service.js +90 -53
- package/dist/server/services/pr-watcher-service.js +13 -7
- package/dist/server/services/retention/worker.js +40 -0
- package/dist/server/services/review-service.js +8 -4
- package/dist/server/services/session-event-metrics.js +43 -0
- package/dist/server/services/session-handoff-service.js +6 -4
- package/dist/server/services/templates-service.js +67 -49
- package/dist/server/services/terminal-service.js +45 -14
- package/dist/server/services/terminal-websocket-service.js +143 -0
- package/dist/server/services/wakeup-service.js +23 -21
- package/dist/server/services/workspace-service.js +3 -44
- package/dist/server/services/workspace-template-service.js +16 -18
- package/dist/server/services/worktree-purge-service.js +1 -4
- package/dist/server/services/ws-events-retention-service.js +197 -104
- package/dist/server/services/ws-events-retention-worker-service.js +112 -0
- package/dist/server/utils/atomic-json-file.js +23 -0
- package/dist/server/utils/git-ops.js +262 -60
- package/dist/shared/codex-models.js +20 -3
- package/dist/shared/models.js +12 -0
- package/package.json +8 -8
- package/src/client/dist/pwa/assets/{ActivityFeed-DCmiJP-z.css → ActivityFeed-B4lTKMVE.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-DXWXioW9.js +8 -0
- package/src/client/dist/pwa/assets/ChangelogPage-BL5Jh8-v.js +1 -0
- package/src/client/dist/pwa/assets/ChangelogPage-DVyx9ZjV.css +1 -0
- package/src/client/dist/pwa/assets/{ClosePopup-BikuzYle.js → ClosePopup-C1a82pL9.js} +1 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-Bv2SZrVZ.js +2 -0
- package/src/client/dist/pwa/assets/ComparisonPanel-ByVbqYCs.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-CMyl2j8Z.css +1 -0
- package/src/client/dist/pwa/assets/CreatePage-d43ITOC1.js +2 -0
- package/src/client/dist/pwa/assets/DashboardPage-BEXXRguE.js +1 -0
- package/src/client/dist/pwa/assets/DashboardPage-CtaHaVdf.css +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-B92w_jd7.js +8 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-C9JXyqHg.js +1 -0
- package/src/client/dist/pwa/assets/{DrawerToggleButton-Dfvkaro0.js → DrawerToggleButton-Btx8QwHo.js} +1 -1
- package/src/client/dist/pwa/assets/{GitPanel-C4wOcZV2.css → GitPanel-C3blk_Pd.css} +1 -1
- package/src/client/dist/pwa/assets/GitPanel-Cgz9crLN.js +3 -0
- package/src/client/dist/pwa/assets/HealthPage-CDhgJQkL.js +1 -0
- package/src/client/dist/pwa/assets/HealthPage-DT8FGrKA.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-DTTtj9sq.js +2 -0
- package/src/client/dist/pwa/assets/{MainLayout-gHENJEFQ.css → MainLayout-knNNtkre.css} +1 -1
- package/src/client/dist/pwa/assets/QBadge-rGILsUYl.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-7EWRdc4U.js +1 -0
- package/src/client/dist/pwa/assets/QBtn-b_baQtAt.js +1 -0
- package/src/client/dist/pwa/assets/QBtnDropdown-vjat4GZl.js +1 -0
- package/src/client/dist/pwa/assets/QBtnGroup-CUmAgBV8.js +1 -0
- package/src/client/dist/pwa/assets/QBtnToggle-COL46j6W.js +1 -0
- package/src/client/dist/pwa/assets/QChip-CG1kWhFm.js +1 -0
- package/src/client/dist/pwa/assets/QExpansionItem-BQuy89vv.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-BrVklYfM.js +1 -0
- package/src/client/dist/pwa/assets/QList-BVX-b8nc.js +1 -0
- package/src/client/dist/pwa/assets/QMarkupTable-B8_FKLvo.js +1 -0
- package/src/client/dist/pwa/assets/QPage-BCDXjfVj.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-DBW_TZri.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-BsKmccpX.js +1 -0
- package/src/client/dist/pwa/assets/QTooltip-DzYbguTR.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BX6iKh6S.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-CBM_GpS3.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-_In2gH2P.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-BqkNYzC5.js +9 -0
- package/src/client/dist/pwa/assets/{SettingsPage-BYdLXJEJ.css → SettingsPage-CpykRKq2.css} +1 -1
- package/src/client/dist/pwa/assets/SplitWorkspacePage-DRoXbSRP.js +1 -0
- package/src/client/dist/pwa/assets/{TerminalPanel-B9LDe7uG.js → TerminalPanel-CfqiuSuk.js} +2 -2
- package/src/client/dist/pwa/assets/{TerminalPanel-DbVZk0vD.css → TerminalPanel-Dz9EE-Ph.css} +1 -1
- package/src/client/dist/pwa/assets/TouchPan-DcOCRf7x.js +1 -0
- package/src/client/dist/pwa/assets/{TourReplayButton-Dtv_UDaY.js → TourReplayButton-gp7m6tSV.js} +1 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-DSw8Upcp.css +1 -0
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-IcoMGkJh.js +1 -0
- package/src/client/dist/pwa/assets/{WorkspacePage--5HsnMQ1.css → WorkspacePage-BnW-gp3u.css} +1 -1
- package/src/client/dist/pwa/assets/WorkspacePage-CR2xAzW_.js +4 -0
- package/src/client/dist/pwa/assets/{browser-B15GGwtW.js → browser-BveHkbe-.js} +1 -1
- package/src/client/dist/pwa/assets/build-path-tree-Dtp7rlxS.js +1 -0
- package/src/client/dist/pwa/assets/custom-sounds-CBoWzJ85.js +1 -0
- package/src/client/dist/pwa/assets/de-BJsWkZro.js +17 -0
- package/src/client/dist/pwa/assets/defineProperty-BbfpZ9Tg.js +1 -0
- package/src/client/dist/pwa/assets/documents-sb2sQ4JC.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-DhaLhNl-.js → editor.api-CbkPxoJf.js} +1 -1
- package/src/client/dist/pwa/assets/efforts-BCli1Unj.js +9 -0
- package/src/client/dist/pwa/assets/es-tNYw7itB.js +17 -0
- package/src/client/dist/pwa/assets/expand-template-CfaEhb2f.js +1 -0
- package/src/client/dist/pwa/assets/{format-2JisC3s1.js → format-BRl2DUlm.js} +7 -7
- package/src/client/dist/pwa/assets/formatters-C5TjPSR_.js +1 -0
- package/src/client/dist/pwa/assets/fr-B5sFski4.js +17 -0
- package/src/client/dist/pwa/assets/{freemarker2-PYrDqU04.js → freemarker2-CDmvOQyn.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-BuiY0fFf.js → handlebars-Bx_w8mx1.js} +1 -1
- package/src/client/dist/pwa/assets/{html-Du3cjdug.js → html-DVRfiRkS.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-Bm-L4nW3.js +1 -0
- package/src/client/dist/pwa/assets/i18n-D21JKsVt.js +19 -0
- package/src/client/dist/pwa/assets/index-C7vFf6p2.js +2 -0
- package/src/client/dist/pwa/assets/index-C8zAZ-k-.css +1 -0
- package/src/client/dist/pwa/assets/it-Bc2FncM9.js +17 -0
- package/src/client/dist/pwa/assets/{javascript-Bb48hPli.js → javascript-CPvttU5J.js} +1 -1
- package/src/client/dist/pwa/assets/latest-request-CN4-B22z.js +1 -0
- package/src/client/dist/pwa/assets/{liquid-Be77iQe3.js → liquid-CI0gQddb.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-CfohNJja.js → mdx-BveBwYJH.js} +1 -1
- package/src/client/dist/pwa/assets/{monaco-contributions-CyM4wsIg.js → monaco-contributions-CHXFh4P2.js} +1 -1
- package/src/client/dist/pwa/assets/monaco.contribution-DOK8t_9B.js +2 -0
- package/src/client/dist/pwa/assets/network-auth-CItnNZDh.js +1 -0
- package/src/client/dist/pwa/assets/{notify-theme-DvvaSyg1.js → notify-theme-BO6r1Rd7.js} +1 -1
- package/src/client/dist/pwa/assets/pending-diff-open-DpS4iyDM.js +1 -0
- package/src/client/dist/pwa/assets/{permissionModes-B-ciuUbz.js → permissionModes-Cc5VaOdC.js} +1 -1
- package/src/client/dist/pwa/assets/preload-helper-BZ1Pz5am.js +1 -0
- package/src/client/dist/pwa/assets/{private.use-form-Bk8ti264.js → private.use-form-vxvbh_VN.js} +1 -1
- package/src/client/dist/pwa/assets/{project-color-Be0t12-n.js → project-color-Bnv02P9D.js} +1 -1
- package/src/client/dist/pwa/assets/{python-BNemmtx8.js → python-BesLCxpi.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-3NM8iFVf.js → razor-Dv1PMxuY.js} +1 -1
- package/src/client/dist/pwa/assets/{reactivity.esm-bundler-Bo9anJjg.js → reactivity.esm-bundler-tuEvQ47f.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-BWJNIDv3.js → render-chat-markdown-CafzCcmu.js} +1 -1
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-DOiZcasU.js +1 -0
- package/src/client/dist/pwa/assets/{typescript-DwyZ4CF7.js → typescript-26L2Xsu9.js} +1 -1
- package/src/client/dist/pwa/assets/use-checkbox-BLeif5-t.js +1 -0
- package/src/client/dist/pwa/assets/use-id-b4A2I8LB.js +1 -0
- package/src/client/dist/pwa/assets/use-is-mobile-DLIZxEo9.js +1 -0
- package/src/client/dist/pwa/assets/use-search-index-status-Mv3pIO6p.js +1 -0
- package/src/client/dist/pwa/assets/{use-slide-transition-CiaDm-tR.js → use-slide-transition-C5eDGOmu.js} +1 -1
- package/src/client/dist/pwa/assets/{use-tours-CjASQcG5.js → use-tours-B5uIqxRS.js} +2 -2
- package/src/client/dist/pwa/assets/use-worktree-restore-BBChALUM.js +1 -0
- package/src/client/dist/pwa/assets/workspace-templates-CDY-MiH2.js +1 -0
- package/src/client/dist/pwa/assets/{xml-B1UujXdS.js → xml-CkRojTcc.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-mHN8kNgU.js → yaml-DKkQHwew.js} +1 -1
- package/src/client/dist/pwa/index.html +12 -9
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-CO5rOaa5.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-DKJkFiU_.css +0 -1
- package/src/client/dist/pwa/assets/ChangelogPage-DNRcPRtW.js +0 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-CRRaDs2D.css +0 -1
- package/src/client/dist/pwa/assets/ComparisonPanel-DZoDqgJI.js +0 -2
- package/src/client/dist/pwa/assets/CreatePage-B9C9J9Pw.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-wNH35uTG.js +0 -2
- package/src/client/dist/pwa/assets/DashboardPage-DUyy4SzS.js +0 -1
- package/src/client/dist/pwa/assets/DashboardPage-XiCj24i2.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-CZ5QwXqi.js +0 -8
- package/src/client/dist/pwa/assets/DocumentsPanel-sBatgKA-.js +0 -1
- package/src/client/dist/pwa/assets/GitPanel-B8e1olfm.js +0 -3
- package/src/client/dist/pwa/assets/HealthPage-BwXYpczh.js +0 -1
- package/src/client/dist/pwa/assets/HealthPage-CFZ-tdKK.css +0 -1
- package/src/client/dist/pwa/assets/MainLayout-DFXIS_wv.js +0 -2
- package/src/client/dist/pwa/assets/QBadge-CQF1smL-.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-bdDA-A5g.js +0 -1
- package/src/client/dist/pwa/assets/QBtn-CcQQrYA_.js +0 -1
- package/src/client/dist/pwa/assets/QBtnDropdown-C-1EhO9J.js +0 -1
- package/src/client/dist/pwa/assets/QBtnGroup-wd5lKRXu.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-BnC8rTql.js +0 -1
- package/src/client/dist/pwa/assets/QChip-D75nKi-L.js +0 -1
- package/src/client/dist/pwa/assets/QExpansionItem-RpMPvfxX.js +0 -1
- package/src/client/dist/pwa/assets/QItemSection-Bt9H35JJ.js +0 -1
- package/src/client/dist/pwa/assets/QList-D5msNbSZ.js +0 -1
- package/src/client/dist/pwa/assets/QMarkupTable-D3REW6RC.js +0 -1
- package/src/client/dist/pwa/assets/QPage-HAaeQ-cv.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-Dpru_T81.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-DDsPDUH9.js +0 -1
- package/src/client/dist/pwa/assets/QTooltip-C31vXtse.js +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-BYKSFoFj.css +0 -1
- package/src/client/dist/pwa/assets/SchedulePanel-D6LTiRFg.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-Dj487BdP.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-CJmgFiAa.js +0 -9
- package/src/client/dist/pwa/assets/SplitWorkspacePage-BGSDgGIO.js +0 -1
- package/src/client/dist/pwa/assets/TouchPan-DJqqxDBF.js +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-CrLP_I5S.css +0 -1
- package/src/client/dist/pwa/assets/WorkspaceAttentionLabels-VlJCIkwU.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-B_SlX8YB.js +0 -4
- package/src/client/dist/pwa/assets/build-path-tree-CU6uXunR.js +0 -1
- package/src/client/dist/pwa/assets/custom-sounds-CDJsgChY.js +0 -1
- package/src/client/dist/pwa/assets/de-B_Y4EArT.js +0 -17
- package/src/client/dist/pwa/assets/documents-Cf6nmu8Z.js +0 -1
- package/src/client/dist/pwa/assets/efforts-DoYZi0AZ.js +0 -9
- package/src/client/dist/pwa/assets/engineFeatures-C9XiCfvv.js +0 -1
- package/src/client/dist/pwa/assets/es-DtsTTShP.js +0 -17
- package/src/client/dist/pwa/assets/expand-template-LDLM-6iN.js +0 -1
- package/src/client/dist/pwa/assets/formatters-ZVNs2VOl.js +0 -1
- package/src/client/dist/pwa/assets/fr-C82MIVrA.js +0 -17
- package/src/client/dist/pwa/assets/i18n-BumeGGx_.js +0 -1
- package/src/client/dist/pwa/assets/index-DolbyIM4.js +0 -20
- package/src/client/dist/pwa/assets/index-VMFLfvp2.css +0 -1
- package/src/client/dist/pwa/assets/it-BJNuTcj2.js +0 -17
- package/src/client/dist/pwa/assets/monaco.contribution-BcIfN6Mi.js +0 -2
- package/src/client/dist/pwa/assets/network-auth-B1xAaHmx.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-B6N0vI5n.js +0 -1
- package/src/client/dist/pwa/assets/pinia-CTJ3OfP3.js +0 -1
- package/src/client/dist/pwa/assets/runtime-core.esm-bundler-CNFKm4ij.js +0 -1
- package/src/client/dist/pwa/assets/use-checkbox-2xzX4yYj.js +0 -1
- package/src/client/dist/pwa/assets/use-id-vOr8PRTG.js +0 -1
- package/src/client/dist/pwa/assets/use-is-mobile-CJfSpTHx.js +0 -1
- package/src/client/dist/pwa/assets/use-search-index-status-Bluf9L99.js +0 -1
- package/src/client/dist/pwa/assets/workspace-templates-BxYxJt3S.js +0 -1
- /package/src/client/dist/pwa/assets/{engineFeatures-B-9jHocY.css → use-worktree-restore-B-9jHocY.css} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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.5
|
|
8
|
+
|
|
9
|
+
- fix(workspace): improve reliability and git controls
|
|
10
|
+
|
|
11
|
+
## 2.1.4
|
|
12
|
+
|
|
13
|
+
- chore(deps): update server client and pwa packages
|
|
14
|
+
|
|
15
|
+
## 2.1.3
|
|
16
|
+
|
|
17
|
+
- fix(subagents): reliable lifecycle tracking and a stop button
|
|
18
|
+
|
|
7
19
|
## 2.1.2
|
|
8
20
|
|
|
9
21
|
- fix: read-only returning reviews, resumed-turn stdin, feed and tag filter
|
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
|
|
755
|
-
|
|
756
|
-
|
|
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
|
package/dist/server/index.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
|
551
|
-
|
|
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 {
|
|
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 =
|
|
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. `
|
|
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:
|
|
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 {
|
|
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
|
|
78
|
-
|
|
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
|
|
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 (
|
|
258
|
-
// Accept missing templates
|
|
259
|
-
replaceAllTemplates(
|
|
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
|
}
|
|
@@ -6,7 +6,7 @@ import { getSearchIndexStatus, searchEvents } from '../services/search-service.j
|
|
|
6
6
|
import { createSessionHandoff, decideSessionHandoff, getCurrentSessionHandoff, SessionHandoffError, submitSessionHandoff, waitForSessionHandoff, } from '../services/session-handoff-service.js';
|
|
7
7
|
import { AgentStopError, assertAgentStopped } from '../utils/agent-stop-result.js';
|
|
8
8
|
import { withGitRepoLock } from '../utils/git-repo-lock.js';
|
|
9
|
-
import { isWorkspaceLifecycleBusy, workspaceLifecycleReason } from '../utils/workspace-lifecycle-guard.js';
|
|
9
|
+
import { assertWorkspaceLifecycleAvailable, isWorkspaceLifecycleBusy, workspaceLifecycleReason, } from '../utils/workspace-lifecycle-guard.js';
|
|
10
10
|
import autoLoopMessagesRoutes from './auto-loop-messages.js';
|
|
11
11
|
const execFileAsync = promisify(execFileCb);
|
|
12
12
|
import fs from 'node:fs';
|
|
@@ -73,7 +73,11 @@ function realPathOrResolved(p) {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
function workspaceErrorStatus(err) {
|
|
76
|
-
return err instanceof AgentStopError ||
|
|
76
|
+
return err instanceof AgentStopError ||
|
|
77
|
+
err instanceof WorkspaceLifecycleBusyError ||
|
|
78
|
+
err instanceof terminalService.TerminalStopError
|
|
79
|
+
? 409
|
|
80
|
+
: 500;
|
|
77
81
|
}
|
|
78
82
|
/** Hono sub-router for workspace CRUD, tasks, agents, Git and PR creation. */
|
|
79
83
|
const app = new Hono();
|
|
@@ -381,7 +385,7 @@ app.post('/:id/engine-handoff-preview', migrationGuard, async (c) => {
|
|
|
381
385
|
const engine = listEngines().find((item) => item.id === body.engine);
|
|
382
386
|
if (!engine)
|
|
383
387
|
return c.json({ error: `Unknown engine '${body.engine ?? ''}'` }, 400);
|
|
384
|
-
return c.json({ handoff: buildEngineHandoff(workspace, workspace.engine, engine.id) });
|
|
388
|
+
return c.json({ handoff: await buildEngineHandoff(workspace, workspace.engine, engine.id) });
|
|
385
389
|
}
|
|
386
390
|
catch (err) {
|
|
387
391
|
return c.json({ error: err instanceof Error ? err.message : 'Failed to build engine handoff' }, 500);
|
|
@@ -414,7 +418,7 @@ app.post('/:id/switch-engine', migrationGuard, async (c) => {
|
|
|
414
418
|
const effort = typeof body.reasoningEffort === 'string' ? body.reasoningEffort : 'auto';
|
|
415
419
|
const handoff = typeof body.handoff === 'string' && body.handoff.trim()
|
|
416
420
|
? body.handoff.trim().slice(0, 30_000)
|
|
417
|
-
: buildEngineHandoff(workspace, workspace.engine, body.engine);
|
|
421
|
+
: await buildEngineHandoff(workspace, workspace.engine, body.engine);
|
|
418
422
|
if (workspaceService.getWorkspace(workspace.id)?.status === 'compacting') {
|
|
419
423
|
return c.json({
|
|
420
424
|
code: 'compacting',
|
|
@@ -1914,13 +1918,19 @@ app.post('/:id/pending-wakeup', async (c) => {
|
|
|
1914
1918
|
if (rawMode !== 'fresh' && rawMode !== 'resume') {
|
|
1915
1919
|
return c.json({ error: "mode must be 'fresh' or 'resume'" }, 400);
|
|
1916
1920
|
}
|
|
1921
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
1922
|
+
if (!workspace)
|
|
1923
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
1924
|
+
if (workspace.archivedAt || workspace.worktreePurgedAt) {
|
|
1925
|
+
return c.json({ error: 'Restore the workspace before scheduling a wakeup' }, 409);
|
|
1926
|
+
}
|
|
1927
|
+
assertWorkspaceLifecycleAvailable(id);
|
|
1917
1928
|
// 'resume' pins the active session so the wakeup resumes that conversation;
|
|
1918
1929
|
// 'fresh' (default) — or 'resume' with no active session — leaves it unpinned
|
|
1919
1930
|
// so the wakeup fires a brand-new session running `prompt`. This is what makes
|
|
1920
1931
|
// manual scheduling on an idle workspace possible (no more hard 409).
|
|
1921
1932
|
const agentSessionId = rawMode === 'resume' ? (agentManager.getActiveSessionId(id) ?? undefined) : undefined;
|
|
1922
|
-
wakeupService.schedule(id, delaySeconds, prompt, reason, agentSessionId);
|
|
1923
|
-
const pending = wakeupService.getPending(id);
|
|
1933
|
+
const pending = wakeupService.schedule(id, delaySeconds, prompt, reason, agentSessionId);
|
|
1924
1934
|
return c.json({ ok: true, pending });
|
|
1925
1935
|
}
|
|
1926
1936
|
catch (err) {
|
|
@@ -3018,12 +3028,7 @@ app.post('/:id/archive', migrationGuard, async (c) => {
|
|
|
3018
3028
|
console.error(`[workspaces] stopDevServer during archive failed: ${message}`);
|
|
3019
3029
|
throw err;
|
|
3020
3030
|
}
|
|
3021
|
-
|
|
3022
|
-
terminalService.destroyTerminal(id);
|
|
3023
|
-
}
|
|
3024
|
-
catch {
|
|
3025
|
-
// Terminal may not exist — ignore
|
|
3026
|
-
}
|
|
3031
|
+
await terminalService.destroyTerminal(id);
|
|
3027
3032
|
const updated = workspaceService.archiveWorkspace(id);
|
|
3028
3033
|
wsService.emitEphemeral(id, 'workspace:archived', { workspace: updated });
|
|
3029
3034
|
// Run the project's archive script (best-effort — never blocks the archive).
|
|
@@ -3140,12 +3145,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
|
|
|
3140
3145
|
console.error(`[workspaces] stopDevServer during delete failed for '${workspace.name}':`, err);
|
|
3141
3146
|
throw err;
|
|
3142
3147
|
}
|
|
3143
|
-
|
|
3144
|
-
terminalService.destroyTerminal(workspace.id);
|
|
3145
|
-
}
|
|
3146
|
-
catch {
|
|
3147
|
-
// Terminal may not exist — ignore
|
|
3148
|
-
}
|
|
3148
|
+
await terminalService.destroyTerminal(workspace.id);
|
|
3149
3149
|
// Collected best-effort warnings: the DB deletion always proceeds, but
|
|
3150
3150
|
// side-effects (worktree, local/remote branches) can fail independently.
|
|
3151
3151
|
// We surface a user-friendly message per failure so the UI can show a
|
|
@@ -3178,7 +3178,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
|
|
|
3178
3178
|
// Delete local branch if requested
|
|
3179
3179
|
if (opts.deleteLocalBranch) {
|
|
3180
3180
|
try {
|
|
3181
|
-
gitOps.
|
|
3181
|
+
await gitOps.deleteLocalBranchAsync(workspace.projectPath, workspace.workingBranch);
|
|
3182
3182
|
}
|
|
3183
3183
|
catch (err) {
|
|
3184
3184
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3191,7 +3191,7 @@ async function deleteWorkspaceWithSideEffectsUnlocked(workspace, opts) {
|
|
|
3191
3191
|
// Delete remote branch if requested
|
|
3192
3192
|
if (opts.deleteRemoteBranch) {
|
|
3193
3193
|
try {
|
|
3194
|
-
gitOps.
|
|
3194
|
+
await gitOps.deleteRemoteBranchAsync(workspace.projectPath, workspace.workingBranch);
|
|
3195
3195
|
}
|
|
3196
3196
|
catch (err) {
|
|
3197
3197
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -3405,7 +3405,7 @@ app.get('/:id/diff', async (c) => {
|
|
|
3405
3405
|
if (!from || !to) {
|
|
3406
3406
|
return c.json({ error: 'mode=commits requires from and to query params' }, 400);
|
|
3407
3407
|
}
|
|
3408
|
-
if (!gitOps.
|
|
3408
|
+
if (!(await gitOps.commitExistsAsync(worktreePath, to))) {
|
|
3409
3409
|
return c.json({ error: `Invalid commit ref '${to}'` }, 400);
|
|
3410
3410
|
}
|
|
3411
3411
|
// `to` is strict (400 above). `from` is intentionally lenient: any ref it
|
|
@@ -3413,8 +3413,8 @@ app.get('/:id/diff', async (c) => {
|
|
|
3413
3413
|
// only unresolved `from` the UI ever sends is a root commit's `<sha>^`
|
|
3414
3414
|
// (single-commit diff of the first commit); the compare dialog otherwise
|
|
3415
3415
|
// only offers refs that resolve.
|
|
3416
|
-
const fromRef = gitOps.
|
|
3417
|
-
const files = gitOps.
|
|
3416
|
+
const fromRef = (await gitOps.commitExistsAsync(worktreePath, from)) ? from : gitOps.EMPTY_TREE_SHA;
|
|
3417
|
+
const files = await gitOps.getChangedFilesBetweenAsync(worktreePath, fromRef, to);
|
|
3418
3418
|
c.header('Cache-Control', 'no-store');
|
|
3419
3419
|
return c.json({ files, mode: 'commits', from: fromRef, to });
|
|
3420
3420
|
}
|
|
@@ -3426,8 +3426,8 @@ app.get('/:id/diff', async (c) => {
|
|
|
3426
3426
|
await gitOps.fetchSourceBranchAsync(worktreePath, workspace.sourceBranch);
|
|
3427
3427
|
}
|
|
3428
3428
|
const files = mode === 'unpushed'
|
|
3429
|
-
? gitOps.
|
|
3430
|
-
: gitOps.
|
|
3429
|
+
? await gitOps.getUnpushedChangedFilesAsync(worktreePath, workspace.workingBranch)
|
|
3430
|
+
: await gitOps.getChangedFilesAsync(worktreePath, workspace.sourceBranch, includeUntracked);
|
|
3431
3431
|
c.header('Cache-Control', 'no-store');
|
|
3432
3432
|
return c.json({
|
|
3433
3433
|
files,
|
|
@@ -3446,7 +3446,7 @@ app.get('/:id/diff', async (c) => {
|
|
|
3446
3446
|
// - `branch` → sourceBranch
|
|
3447
3447
|
// - `unpushed` → origin/<workingBranch>
|
|
3448
3448
|
// `modified` is always the current worktree content.
|
|
3449
|
-
app.get('/:id/diff-file', (c) => {
|
|
3449
|
+
app.get('/:id/diff-file', async (c) => {
|
|
3450
3450
|
try {
|
|
3451
3451
|
const id = c.req.param('id');
|
|
3452
3452
|
const filePath = c.req.query('path');
|
|
@@ -3472,14 +3472,16 @@ app.get('/:id/diff-file', (c) => {
|
|
|
3472
3472
|
if (!from || !to) {
|
|
3473
3473
|
return c.json({ error: 'mode=commits requires from and to query params' }, 400);
|
|
3474
3474
|
}
|
|
3475
|
-
if (!gitOps.
|
|
3475
|
+
if (!(await gitOps.commitExistsAsync(worktreePath, to))) {
|
|
3476
3476
|
return c.json({ error: `Invalid commit ref '${to}'` }, 400);
|
|
3477
3477
|
}
|
|
3478
3478
|
// `from` is lenient (empty-tree fallback) for a root commit's `<sha>^` —
|
|
3479
3479
|
// see the matching note in the `/diff` commits branch above.
|
|
3480
|
-
const fromRef = gitOps.
|
|
3481
|
-
const original =
|
|
3482
|
-
|
|
3480
|
+
const fromRef = (await gitOps.commitExistsAsync(worktreePath, from)) ? from : gitOps.EMPTY_TREE_SHA;
|
|
3481
|
+
const [original, modified] = await Promise.all([
|
|
3482
|
+
gitOps.getFileAtRefAsync(worktreePath, fromRef, filePath),
|
|
3483
|
+
gitOps.getFileAtRefAsync(worktreePath, to, filePath),
|
|
3484
|
+
]);
|
|
3483
3485
|
c.header('Cache-Control', 'no-store');
|
|
3484
3486
|
return c.json({
|
|
3485
3487
|
original: original ?? '',
|
|
@@ -3491,7 +3493,7 @@ app.get('/:id/diff-file', (c) => {
|
|
|
3491
3493
|
});
|
|
3492
3494
|
}
|
|
3493
3495
|
const baseRef = mode === 'unpushed' ? `origin/${workspace.workingBranch}` : workspace.sourceBranch;
|
|
3494
|
-
const original = gitOps.
|
|
3496
|
+
const original = await gitOps.getFileAtRefAsync(worktreePath, baseRef, filePath);
|
|
3495
3497
|
const modified = gitOps.getFileContent(worktreePath, filePath);
|
|
3496
3498
|
c.header('Cache-Control', 'no-store');
|
|
3497
3499
|
return c.json({
|
|
@@ -3555,7 +3557,7 @@ app.post('/:id/rollback-file', async (c) => {
|
|
|
3555
3557
|
// GET /api/workspaces/:id/branch-divergence?limit=50
|
|
3556
3558
|
// Returns commits on the working branch ahead of `origin/<sourceBranch>`
|
|
3557
3559
|
// (`ahead`) and commits on `origin/<sourceBranch>` not yet on the working
|
|
3558
|
-
// branch (`behind`).
|
|
3560
|
+
// branch (`behind`). Awaits refreshing `origin/<sourceBranch>` so the
|
|
3559
3561
|
// counts and lists are never computed against a stale local source ref.
|
|
3560
3562
|
app.get('/:id/branch-divergence', async (c) => {
|
|
3561
3563
|
try {
|
|
@@ -3569,8 +3571,8 @@ app.get('/:id/branch-divergence', async (c) => {
|
|
|
3569
3571
|
const limit = Math.min(Math.max(1, Number.isNaN(parsed) ? 50 : parsed), 200);
|
|
3570
3572
|
const worktreePath = workspace.worktreePath;
|
|
3571
3573
|
await gitOps.fetchSourceBranchAsync(worktreePath, workspace.sourceBranch);
|
|
3572
|
-
const ahead = gitOps.
|
|
3573
|
-
const behind = gitOps.
|
|
3574
|
+
const ahead = await gitOps.listBranchCommitsAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
|
|
3575
|
+
const behind = await gitOps.listCommitsBehindAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
|
|
3574
3576
|
c.header('Cache-Control', 'no-store');
|
|
3575
3577
|
return c.json({
|
|
3576
3578
|
ahead,
|
|
@@ -3586,7 +3588,7 @@ app.get('/:id/branch-divergence', async (c) => {
|
|
|
3586
3588
|
});
|
|
3587
3589
|
// GET /api/workspaces/:id/commits?limit=50 — list commits between
|
|
3588
3590
|
// `origin/<sourceBranch>` and HEAD, each tagged with whether it's already
|
|
3589
|
-
// pushed to origin/<branch>.
|
|
3591
|
+
// pushed to origin/<branch>. Awaits refreshing `origin/<sourceBranch>`
|
|
3590
3592
|
// so the list is not computed against a stale local source ref.
|
|
3591
3593
|
app.get('/:id/commits', async (c) => {
|
|
3592
3594
|
try {
|
|
@@ -3599,7 +3601,7 @@ app.get('/:id/commits', async (c) => {
|
|
|
3599
3601
|
const limit = Math.min(Math.max(1, parseInt(limitRaw ?? '50', 10) || 50), 200);
|
|
3600
3602
|
const worktreePath = workspace.worktreePath;
|
|
3601
3603
|
await gitOps.fetchSourceBranchAsync(worktreePath, workspace.sourceBranch);
|
|
3602
|
-
const commits = gitOps.
|
|
3604
|
+
const commits = await gitOps.listBranchCommitsAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch, limit);
|
|
3603
3605
|
c.header('Cache-Control', 'no-store');
|
|
3604
3606
|
return c.json({ commits, sourceBranch: workspace.sourceBranch, workingBranch: workspace.workingBranch });
|
|
3605
3607
|
}
|
|
@@ -3609,13 +3611,13 @@ app.get('/:id/commits', async (c) => {
|
|
|
3609
3611
|
}
|
|
3610
3612
|
});
|
|
3611
3613
|
// GET /:id/working-tree-files — list uncommitted working-tree files (read-only)
|
|
3612
|
-
app.get('/:id/working-tree-files', (c) => {
|
|
3614
|
+
app.get('/:id/working-tree-files', async (c) => {
|
|
3613
3615
|
try {
|
|
3614
3616
|
const id = c.req.param('id');
|
|
3615
3617
|
const workspace = workspaceService.getWorkspace(id);
|
|
3616
3618
|
if (!workspace)
|
|
3617
3619
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3618
|
-
const files = gitOps.
|
|
3620
|
+
const files = await gitOps.getWorkingTreeFilesAsync(workspace.worktreePath);
|
|
3619
3621
|
return c.json({ files });
|
|
3620
3622
|
}
|
|
3621
3623
|
catch (err) {
|
|
@@ -3954,7 +3956,7 @@ app.post('/:id/git/commit-with-agent', migrationGuard, async (c) => {
|
|
|
3954
3956
|
const workspace = workspaceService.getWorkspace(id);
|
|
3955
3957
|
if (!workspace)
|
|
3956
3958
|
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
3957
|
-
const workingTree = gitOps.
|
|
3959
|
+
const workingTree = await gitOps.getWorkingTreeStatusAsync(workspace.worktreePath);
|
|
3958
3960
|
if (workingTree.staged + workingTree.modified + workingTree.untracked === 0) {
|
|
3959
3961
|
return c.json({ error: 'No uncommitted changes to commit' }, 400);
|
|
3960
3962
|
}
|
|
@@ -4372,8 +4374,8 @@ app.post('/:id/open-pr', async (c) => {
|
|
|
4372
4374
|
return c.json({ ok: true, prNumber, prUrl, messageSent: false });
|
|
4373
4375
|
}
|
|
4374
4376
|
// Build context and render the PR prompt template
|
|
4375
|
-
const commits = gitOps.
|
|
4376
|
-
const diffStats = gitOps.
|
|
4377
|
+
const commits = await gitOps.getCommitsBetweenAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
4378
|
+
const diffStats = await gitOps.getDiffStatsBetweenAsync(worktreePath, workspace.sourceBranch, workspace.workingBranch);
|
|
4377
4379
|
const tasks = workspaceService.listTasks(workspace.id);
|
|
4378
4380
|
const rendered = renderPrTemplate(effective.prPromptTemplate, {
|
|
4379
4381
|
workspace,
|
|
@@ -4621,4 +4623,43 @@ app.post('/:id/interrupt', migrationGuard, async (c) => {
|
|
|
4621
4623
|
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
4622
4624
|
}
|
|
4623
4625
|
});
|
|
4626
|
+
// POST /api/workspaces/:id/subagents/stop - stop one running sub-agent (body
|
|
4627
|
+
// `{ id }`, its canonical card id) or all of them, without interrupting the turn
|
|
4628
|
+
app.post('/:id/subagents/stop', migrationGuard, async (c) => {
|
|
4629
|
+
try {
|
|
4630
|
+
const id = c.req.param('id');
|
|
4631
|
+
const workspace = workspaceService.getWorkspace(id);
|
|
4632
|
+
if (!workspace) {
|
|
4633
|
+
return c.json({ error: `Workspace '${id}' not found` }, 404);
|
|
4634
|
+
}
|
|
4635
|
+
const rawBody = await c.req.text();
|
|
4636
|
+
let body = {};
|
|
4637
|
+
if (rawBody.trim().length > 0) {
|
|
4638
|
+
let parsed;
|
|
4639
|
+
try {
|
|
4640
|
+
parsed = JSON.parse(rawBody);
|
|
4641
|
+
}
|
|
4642
|
+
catch {
|
|
4643
|
+
return c.json({ error: 'Request body must be valid JSON' }, 400);
|
|
4644
|
+
}
|
|
4645
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
4646
|
+
return c.json({ error: 'Request body must be a JSON object' }, 400);
|
|
4647
|
+
}
|
|
4648
|
+
body = parsed;
|
|
4649
|
+
}
|
|
4650
|
+
if (body.id !== undefined && (typeof body.id !== 'string' || body.id.trim().length === 0)) {
|
|
4651
|
+
return c.json({ error: 'id must be a non-empty string when provided' }, 400);
|
|
4652
|
+
}
|
|
4653
|
+
const stopped = agentManager.stopSubagents(id, body.id);
|
|
4654
|
+
return c.json({ stopped });
|
|
4655
|
+
}
|
|
4656
|
+
catch (err) {
|
|
4657
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4658
|
+
if (err instanceof agentManager.StopSubagentsError) {
|
|
4659
|
+
const status = err.code === 'subagent_not_running' ? 404 : 409;
|
|
4660
|
+
return c.json({ error: message, code: err.code }, status);
|
|
4661
|
+
}
|
|
4662
|
+
return c.json({ error: message }, workspaceErrorStatus(err));
|
|
4663
|
+
}
|
|
4664
|
+
});
|
|
4624
4665
|
export default app;
|