@loicngr/kobo 1.11.15 → 1.11.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +13 -0
- package/dist/mcp-server/kobo-tasks-server.js +8 -7
- package/dist/server/db/migrations.js +57 -0
- package/dist/server/db/schema.js +10 -39
- package/dist/server/index.js +55 -7
- package/dist/server/routes/git.js +8 -3
- package/dist/server/routes/health.js +19 -2
- package/dist/server/routes/settings.js +24 -0
- package/dist/server/routes/workspaces.js +371 -72
- package/dist/server/services/agent/engines/claude-code/engine.js +39 -35
- package/dist/server/services/agent/engines/codex/client.js +16 -4
- package/dist/server/services/agent/engines/codex/engine.js +23 -2
- package/dist/server/services/agent/engines/codex/jsonrpc/peer.js +44 -5
- package/dist/server/services/agent/engines/types.js +13 -0
- package/dist/server/services/agent/orchestrator.js +354 -133
- package/dist/server/services/agent/session-controller.js +27 -0
- package/dist/server/services/agent/{engines/codex/turn-liveness.js → turn-liveness.js} +6 -2
- package/dist/server/services/change-source-branch-service.js +115 -11
- package/dist/server/services/db-backup-service.js +27 -16
- package/dist/server/services/pr-watcher-service.js +17 -2
- package/dist/server/services/settings-service.js +39 -0
- package/dist/server/services/websocket-service.js +114 -48
- package/dist/server/services/workspace-service.js +54 -2
- package/dist/server/services/worktree-purge-service.js +19 -5
- package/dist/server/services/worktree-service.js +38 -1
- package/dist/server/services/ws-events-retention-service.js +133 -0
- package/dist/server/utils/git-ops.js +209 -14
- package/dist/server/utils/git-repo-lock.js +45 -0
- package/dist/server/utils/logger.js +108 -0
- package/dist/server/utils/script-runner.js +11 -0
- package/package.json +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BlDBh5jB.js +8 -0
- package/src/client/dist/pwa/assets/ActivityFeed-tC8sR0GV.css +1 -0
- package/src/client/dist/pwa/assets/ChangelogPage-DACtknly.js +1 -0
- package/src/client/dist/pwa/assets/ChangelogPage-D_WOVByV.css +1 -0
- package/src/client/dist/pwa/assets/ClosePopup-DEy0yFXE.js +1 -0
- package/src/client/dist/pwa/assets/CreatePage-Bb-Qn-Rd.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-RMBX-Zjn.css +1 -0
- package/src/client/dist/pwa/assets/DiffViewer-DKt6gFi4.js +8 -0
- package/src/client/dist/pwa/assets/DiffViewer-ahWtFrRy.css +1 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-B8QY7FTj.css +1 -0
- package/src/client/dist/pwa/assets/DocumentsPanel-DhP1jk11.js +1 -0
- package/src/client/dist/pwa/assets/DrawerToggleButton-CBdOJzVZ.js +1 -0
- package/src/client/dist/pwa/assets/GitPanel-BKrtkPrV.css +1 -0
- package/src/client/dist/pwa/assets/GitPanel-DNCCB0G_.js +3 -0
- package/src/client/dist/pwa/assets/HealthPage-B1J4-dOH.js +1 -0
- package/src/client/dist/pwa/assets/HealthPage-BbqLoFQj.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-F8ShDnAy.css +1 -0
- package/src/client/dist/pwa/assets/MainLayout-qWwDy1B5.js +2 -0
- package/src/client/dist/pwa/assets/QBadge-BgS0-MXB.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-BoAbhM1O.js +1 -0
- package/src/client/dist/pwa/assets/QBtn-B7WFdfmy.js +1 -0
- package/src/client/dist/pwa/assets/QBtnGroup-BMQlX2YS.js +1 -0
- package/src/client/dist/pwa/assets/QBtnToggle-eMu_yinY.js +1 -0
- package/src/client/dist/pwa/assets/{QChip-CktOt0oC.js → QChip-BzB_GF4Z.js} +1 -1
- package/src/client/dist/pwa/assets/QExpansionItem-C8CWo-aP.js +1 -0
- package/src/client/dist/pwa/assets/QItemSection-CDBweQnA.js +1 -0
- package/src/client/dist/pwa/assets/QList-Ccqhimcb.js +1 -0
- package/src/client/dist/pwa/assets/QPage-RpDxx5ah.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-D9bHHxAA.js +1 -0
- package/src/client/dist/pwa/assets/QSlideTransition-6lWJvjLP.js +1 -0
- package/src/client/dist/pwa/assets/QSpace-D3PNLwa4.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-DWOFEezR.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-Brtqtz1H.js +1 -0
- package/src/client/dist/pwa/assets/SchedulePanel-BunlzsOY.css +1 -0
- package/src/client/dist/pwa/assets/SearchPage-CvZxJiJs.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-DdWM01rq.css +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-JVje8OPs.js +16 -0
- package/src/client/dist/pwa/assets/SettingsPage-dMAg4JP8.css +1 -0
- package/src/client/dist/pwa/assets/TerminalPanel-B6xJJs8p.css +1 -0
- package/src/client/dist/pwa/assets/TerminalPanel-Dtc6B3US.js +18 -0
- package/src/client/dist/pwa/assets/TouchPan-DnEDaTFL.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-BVxtefbj.js +4 -0
- package/src/client/dist/pwa/assets/WorkspacePage-DN-A6XpI.css +1 -0
- package/src/client/dist/pwa/assets/build-path-tree-BU8Y8vRw.js +1 -0
- package/src/client/dist/pwa/assets/clipboard-t9qe6lK7.js +1 -0
- package/src/client/dist/pwa/assets/{cssMode-DXZAn6vO.js → cssMode-D_JDSbNP.js} +1 -1
- package/src/client/dist/pwa/assets/debounce-CDIE7CV6.js +1 -0
- package/src/client/dist/pwa/assets/documents-B6eOZeFK.js +1 -0
- package/src/client/dist/pwa/assets/{editor.api-A6JQ_eAF.js → editor.api-Ccbr7NXr.js} +1 -1
- package/src/client/dist/pwa/assets/engineFeatures-DW9PydRL.js +1 -0
- package/src/client/dist/pwa/assets/expand-template-B4LAsXAF.css +1 -0
- package/src/client/dist/pwa/assets/expand-template-Xq6JwXsl.js +1 -0
- package/src/client/dist/pwa/assets/{freemarker2-CUIeOWM8.js → freemarker2-CUvQzJdx.js} +1 -1
- package/src/client/dist/pwa/assets/{handlebars-Cyql1jca.js → handlebars-BXiAPlES.js} +1 -1
- package/src/client/dist/pwa/assets/{html-tH-6-_8T.js → html-CLmfyTMC.js} +1 -1
- package/src/client/dist/pwa/assets/{htmlMode-VmrCyH2a.js → htmlMode-DWYo6_e_.js} +1 -1
- package/src/client/dist/pwa/assets/i18n-CJclRByY.js +1 -0
- package/src/client/dist/pwa/assets/index-Bri8-39E.css +1 -0
- package/src/client/dist/pwa/assets/index-Dv1p-1sA.js +82 -0
- package/src/client/dist/pwa/assets/{javascript-CDVRMaa9.js → javascript-C9gWFLC_.js} +1 -1
- package/src/client/dist/pwa/assets/{jsonMode-Ck7yXgSl.js → jsonMode-CbsjeFb8.js} +1 -1
- package/src/client/dist/pwa/assets/kobo-commands-DGygmOvc.js +9 -0
- package/src/client/dist/pwa/assets/layout-nWIyHeJf.js +1 -0
- package/src/client/dist/pwa/assets/{liquid-DuPNGLDX.js → liquid-BqJ3SIha.js} +1 -1
- package/src/client/dist/pwa/assets/{lspLanguageFeatures-nffl_SEi.js → lspLanguageFeatures-BB0y1B51.js} +1 -1
- package/src/client/dist/pwa/assets/{mdx-0sVmmIxp.js → mdx-rIxQLZDB.js} +1 -1
- package/src/client/dist/pwa/assets/network-auth-pzKTXRYP.js +1 -0
- package/src/client/dist/pwa/assets/notify-theme-D7RrwvJa.js +1 -0
- package/src/client/dist/pwa/assets/pending-diff-open-Cia8sndE.js +1 -0
- package/src/client/dist/pwa/assets/private.use-form-D8Yuc3aU.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-DPlEC0w5.js → project-color-Dur1z6Gn.js} +1 -1
- package/src/client/dist/pwa/assets/{python-C7z_oSTh.js → python-C6d5du9h.js} +1 -1
- package/src/client/dist/pwa/assets/{razor-Y5ev_nkW.js → razor-Aek1yDc0.js} +1 -1
- package/src/client/dist/pwa/assets/{render-chat-markdown-Btg3ez9I.js → render-chat-markdown-B6sa44Xr.js} +2 -2
- package/src/client/dist/pwa/assets/skill-suite-prompts-DdoGRwhz.js +36 -0
- package/src/client/dist/pwa/assets/{tsMode-RLBeqCG7.js → tsMode-rxNEyqyY.js} +1 -1
- package/src/client/dist/pwa/assets/{typescript-DLXsl8qJ.js → typescript-DndxWYhC.js} +1 -1
- package/src/client/dist/pwa/assets/use-checkbox-z-Qs4ZHY.js +1 -0
- package/src/client/dist/pwa/assets/use-id-DlouqdB8.js +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-BzSwN4xT.js +2 -0
- package/src/client/dist/pwa/assets/{vs-Ddu-KQME.js → vs-De8HKJ9r.js} +3 -3
- package/src/client/dist/pwa/assets/{workers-Dt0XxgF3.js → workers-DdNkyVm9.js} +1 -1
- package/src/client/dist/pwa/assets/{xml-CNH2EfiJ.js → xml-Cl1k1mQr.js} +1 -1
- package/src/client/dist/pwa/assets/{yaml-FpVlkkq1.js → yaml-C8G_k5TR.js} +1 -1
- package/src/client/dist/pwa/fonts/GEIST-LICENSE.txt +92 -0
- package/src/client/dist/pwa/fonts/Geist-Variable.woff2 +0 -0
- package/src/client/dist/pwa/fonts/GeistMono-Variable.woff2 +0 -0
- package/src/client/dist/pwa/index.html +7 -5
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/mcp-server/kobo-tasks-server.ts +8 -7
- package/dist/server/utils/process-tracker.js +0 -49
- package/src/client/dist/pwa/assets/ActivityFeed-BrjeLlhi.css +0 -1
- package/src/client/dist/pwa/assets/ActivityFeed-C7NLbzD6.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-CQ33An2f.css +0 -1
- package/src/client/dist/pwa/assets/ChangelogPage-DwWh9JgQ.js +0 -1
- package/src/client/dist/pwa/assets/ClosePopup-B6KJBZyG.js +0 -1
- package/src/client/dist/pwa/assets/CreatePage-CV_RpAuV.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-pyI9OKPs.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-CS3P9Ngi.css +0 -1
- package/src/client/dist/pwa/assets/DiffViewer-DuNeI5C3.js +0 -8
- package/src/client/dist/pwa/assets/HealthPage-CCJBw0MT.js +0 -1
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuYjalmUiAw-BCOjigVR.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuZtalmUiAw-Ocp2tNig.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWuaabVmUiAw-6jEn9xEl.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWub2bVmUiAw-CCs8I9zJ.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbFmUiAw-BLaboPi0.woff +0 -0
- package/src/client/dist/pwa/assets/KFOMCnqEu92Fr1ME7kSn66aGLdTylUAMQXC89YmC2DPNWubEbVmUiAw-DHuK6FBl.woff +0 -0
- package/src/client/dist/pwa/assets/MainLayout-CaVTdsZl.js +0 -19
- package/src/client/dist/pwa/assets/MainLayout-u-AM87m3.css +0 -1
- package/src/client/dist/pwa/assets/QBadge-sMJZ_eZm.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-DedhGMcj.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-CadHfv93.js +0 -1
- package/src/client/dist/pwa/assets/QIcon--SvgE-nS.js +0 -1
- package/src/client/dist/pwa/assets/QList-Be_FbTA3.js +0 -1
- package/src/client/dist/pwa/assets/QPage-DuIsRmYu.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-C0yTmkrq.js +0 -36
- package/src/client/dist/pwa/assets/QSeparator-BdyRE-AM.js +0 -1
- package/src/client/dist/pwa/assets/QSpace-D9UEvM99.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-B4RmFJUj.js +0 -1
- package/src/client/dist/pwa/assets/QTooltip-Cho7yoPz.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-B08_UXDs.css +0 -1
- package/src/client/dist/pwa/assets/SearchPage-DmfKy4pY.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-B-CtW1eE.js +0 -16
- package/src/client/dist/pwa/assets/SettingsPage-BJZWZYPC.css +0 -1
- package/src/client/dist/pwa/assets/TouchPan-BAN0KuNq.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-ChnyJ3xI.css +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-DHXPtEsY.js +0 -4
- package/src/client/dist/pwa/assets/_plugin-vue_export-helper-BDNMzG2s.js +0 -1
- package/src/client/dist/pwa/assets/documents-B23TsgeA.js +0 -1
- package/src/client/dist/pwa/assets/expand-template-CFOxSpLI.js +0 -1
- package/src/client/dist/pwa/assets/expand-template-D2yUa54D.css +0 -1
- package/src/client/dist/pwa/assets/i18n-Ds8Ww7mW.js +0 -1
- package/src/client/dist/pwa/assets/index-BvLZetuF.js +0 -82
- package/src/client/dist/pwa/assets/index-CK1nk7qr.css +0 -1
- package/src/client/dist/pwa/assets/kobo-commands-DmKgSiYn.js +0 -9
- package/src/client/dist/pwa/assets/layout-Bx3LBcNr.js +0 -1
- package/src/client/dist/pwa/assets/network-auth-Chk1CKmH.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-5x3bHt_s.js +0 -1
- package/src/client/dist/pwa/assets/use-checkbox-SrkgiJho.js +0 -1
- package/src/client/dist/pwa/assets/use-onboarding-B-38VMWL.js +0 -2
- package/src/client/dist/pwa/assets/use-quasar-BBt-28X1.js +0 -1
package/AGENTS.md
CHANGED
|
@@ -240,7 +240,7 @@ Sequence: `captureRestoreData` (best-effort forge lookup for PR number / URL / m
|
|
|
240
240
|
|
|
241
241
|
### Bulk workspace info refresh
|
|
242
242
|
|
|
243
|
-
`GET /api/workspaces/info` returns `{ workspaces, prSnapshots, gitStats }` in one shot. The client polls this endpoint every
|
|
243
|
+
`GET /api/workspaces/info` returns `{ workspaces, prSnapshots, gitStats }` in one shot. The client polls this endpoint every 15 s so every non-archived workspace stays ≤ 15 s fresh without a per-card stats request. The poll is skipped while the browser tab is hidden and fires once immediately when the tab becomes visible again. The server-side pr-watcher feeds the same caches (`lastKnownGitStats` map, PR snapshots) so the work is shared between the polling client and the watchdog loop.
|
|
244
244
|
|
|
245
245
|
### File editing in the diff viewer
|
|
246
246
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to Kōbō are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/). Each release is an `## <version>`
|
|
5
5
|
section — the in-app "What's new" dialog reads this file.
|
|
6
6
|
|
|
7
|
+
## 1.11.17
|
|
8
|
+
|
|
9
|
+
- feat(client): render in the design system's own type, palette and tokens (#34)
|
|
10
|
+
- perf(client): stop re-rendering frozen content and racing stale requests (#33)
|
|
11
|
+
- fix(client): make the review path usable and stop lying about navigation state (#32)
|
|
12
|
+
- fix(git,db): serialize git writes, bound event history, verify irreversible ops (#31)
|
|
13
|
+
- feat(client,server): tell the truth while working and when it fails (#30)
|
|
14
|
+
- fix(agent): stop reporting a running agent when none exists (#29)
|
|
15
|
+
|
|
16
|
+
## 1.11.16
|
|
17
|
+
|
|
18
|
+
- feat(client): responsive mobile layout for drawer, settings nav and workspace toolbar (#28)
|
|
19
|
+
|
|
7
20
|
## 1.11.15
|
|
8
21
|
|
|
9
22
|
- feat(agent): harden autonomous session lifecycle
|
|
@@ -3,7 +3,6 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { getDb } from '../server/db/index.js';
|
|
6
|
-
import { runMigrations } from '../server/db/migrations.js';
|
|
7
6
|
import { createTaskHandler, cronListHandler, deleteTaskHandler, getDevServerStatusHandler, getSessionUsageHandler, getSettingsHandler, getTicketSourcesHandler, getWorkspaceInfoHandler, listDocumentsHandler, listTasksHandler, listWorkspaceImagesHandler, listWorkspacesHandler, logThoughtHandler, markAutoLoopReadyHandler, markTaskDoneHandler, readDocumentHandler, readWorkspaceEventsCsvHandler, setWorkspaceAgentDescriptionHandler, updateTaskHandler, } from './kobo-tasks-handlers.js';
|
|
8
7
|
const workspaceId = process.env.KOBO_WORKSPACE_ID;
|
|
9
8
|
const dbPath = process.env.KOBO_DB_PATH;
|
|
@@ -23,13 +22,15 @@ try {
|
|
|
23
22
|
// KOBO_DB_PATH is passed). That would silently open a second connection
|
|
24
23
|
// against the user's prod DB at ~/.config/kobo/kobo.db, which may not
|
|
25
24
|
// even have the same schema as the dev DB the backend is using.
|
|
25
|
+
// On ouvre SANS migrer. Ce process est lancé une fois par workspace, sur le
|
|
26
|
+
// même fichier de base : `runMigrations` calcule l'ensemble des blocs
|
|
27
|
+
// appliqués AVANT de prendre le verrou, donc deux serveurs MCP démarrés
|
|
28
|
+
// ensemble peuvent rejouer le même bloc — et les blocs v2 à v19 ajoutent
|
|
29
|
+
// des colonnes sans garde d'existence. Un `duplicate column name` ici
|
|
30
|
+
// termine le process et prive l'agent de tous ses outils en pleine session.
|
|
31
|
+
// Le backend migre au démarrage, et il démarre nécessairement avant de
|
|
32
|
+
// pouvoir lancer ce serveur.
|
|
26
33
|
db = getDb(dbPath);
|
|
27
|
-
// Defensive: run migrations to ensure the schema is at the latest version.
|
|
28
|
-
// The backend already ran them at boot, but this MCP server might be the
|
|
29
|
-
// first to touch the DB (e.g. tests, race at first boot, or KOBO_DB_PATH
|
|
30
|
-
// pointing somewhere the backend hasn't migrated). Idempotent — no-op if
|
|
31
|
-
// the DB is already at SCHEMA_VERSION.
|
|
32
|
-
runMigrations(db);
|
|
33
34
|
}
|
|
34
35
|
catch (err) {
|
|
35
36
|
console.error('[kobo-tasks-server] Failed to open database:', err);
|
|
@@ -604,6 +604,63 @@ export const migrations = [
|
|
|
604
604
|
}
|
|
605
605
|
},
|
|
606
606
|
},
|
|
607
|
+
{
|
|
608
|
+
version: 36,
|
|
609
|
+
name: 'drop-metrics-delete-trigger-add-hot-indexes',
|
|
610
|
+
migrate: (db) => {
|
|
611
|
+
// The AFTER DELETE trigger installed by v34 re-aggregated the entire
|
|
612
|
+
// session for EVERY deleted row. Measured on an identical schema: 79.7 s
|
|
613
|
+
// for 20 000 events, 106.6 s through a cascade, with the SQLite write
|
|
614
|
+
// lock held for the whole duration — during which every WebSocket emit
|
|
615
|
+
// gives up after busy_timeout and loses the event for good.
|
|
616
|
+
// Metrics are now recomputed once, application-side, at the end of the
|
|
617
|
+
// transactions that delete events while keeping the session
|
|
618
|
+
// (recomputeSessionMetrics in workspace-service.ts).
|
|
619
|
+
db.exec('DROP TRIGGER IF EXISTS trg_ws_events_metrics_delete');
|
|
620
|
+
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((row) => row.name));
|
|
621
|
+
const hasColumns = (table, needed) => {
|
|
622
|
+
if (!tables.has(table))
|
|
623
|
+
return false;
|
|
624
|
+
const columns = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
625
|
+
return needed.every((column) => columns.has(column));
|
|
626
|
+
};
|
|
627
|
+
// Five hot paths measured as full table scans before this migration.
|
|
628
|
+
const indexes = [
|
|
629
|
+
{
|
|
630
|
+
table: 'tasks',
|
|
631
|
+
columns: ['workspace_id', 'sort_order'],
|
|
632
|
+
sql: 'CREATE INDEX IF NOT EXISTS idx_tasks_workspace_sort ON tasks(workspace_id, sort_order)',
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
table: 'agent_sessions',
|
|
636
|
+
columns: ['workspace_id', 'started_at'],
|
|
637
|
+
sql: 'CREATE INDEX IF NOT EXISTS idx_agent_sessions_workspace_started ON agent_sessions(workspace_id, started_at DESC)',
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
table: 'agent_sessions',
|
|
641
|
+
columns: ['engine_session_id'],
|
|
642
|
+
sql: 'CREATE INDEX IF NOT EXISTS idx_agent_sessions_engine_session ON agent_sessions(engine_session_id)',
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
table: 'workspaces',
|
|
646
|
+
columns: ['archived_at', 'updated_at'],
|
|
647
|
+
sql: 'CREATE INDEX IF NOT EXISTS idx_workspaces_archived_updated ON workspaces(archived_at, updated_at DESC)',
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
table: 'session_event_metrics',
|
|
651
|
+
columns: ['session_id'],
|
|
652
|
+
sql: 'CREATE INDEX IF NOT EXISTS idx_session_event_metrics_session ON session_event_metrics(session_id)',
|
|
653
|
+
},
|
|
654
|
+
];
|
|
655
|
+
// Column guards, not just table guards: several shipped migration tests
|
|
656
|
+
// build a stripped-down table (e.g. `workspaces(id, engine)`) and replay
|
|
657
|
+
// every later migration on top of it.
|
|
658
|
+
for (const index of indexes) {
|
|
659
|
+
if (hasColumns(index.table, index.columns))
|
|
660
|
+
db.exec(index.sql);
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
},
|
|
607
664
|
];
|
|
608
665
|
/** Current schema version — always equals the highest migration version. */
|
|
609
666
|
export const SCHEMA_VERSION = migrations.length > 0 ? migrations[migrations.length - 1].version : 1;
|
package/dist/server/db/schema.js
CHANGED
|
@@ -125,45 +125,6 @@ export function initSchema(db) {
|
|
|
125
125
|
output_tokens = MAX(output_tokens, excluded.output_tokens);
|
|
126
126
|
END;
|
|
127
127
|
|
|
128
|
-
CREATE TRIGGER IF NOT EXISTS trg_ws_events_metrics_delete
|
|
129
|
-
AFTER DELETE ON ws_events
|
|
130
|
-
WHEN OLD.type = 'agent:event' AND OLD.session_id IS NOT NULL
|
|
131
|
-
BEGIN
|
|
132
|
-
DELETE FROM session_event_metrics
|
|
133
|
-
WHERE workspace_id = OLD.workspace_id AND session_id = OLD.session_id;
|
|
134
|
-
|
|
135
|
-
INSERT INTO session_event_metrics (
|
|
136
|
-
workspace_id, session_id, tool_calls, errors, input_tokens, output_tokens
|
|
137
|
-
)
|
|
138
|
-
SELECT
|
|
139
|
-
e.workspace_id,
|
|
140
|
-
e.session_id,
|
|
141
|
-
SUM(CASE WHEN json_extract(e.payload, '$.kind') = 'tool:call' THEN 1 ELSE 0 END),
|
|
142
|
-
SUM(CASE
|
|
143
|
-
WHEN json_extract(e.payload, '$.kind') = 'error'
|
|
144
|
-
OR (json_extract(e.payload, '$.kind') = 'tool:result'
|
|
145
|
-
AND json_extract(e.payload, '$.isError') = 1)
|
|
146
|
-
THEN 1 ELSE 0
|
|
147
|
-
END),
|
|
148
|
-
MAX(CASE
|
|
149
|
-
WHEN json_extract(e.payload, '$.kind') = 'usage'
|
|
150
|
-
AND json_type(e.payload, '$.inputTokens') IN ('integer', 'real')
|
|
151
|
-
THEN CAST(json_extract(e.payload, '$.inputTokens') AS INTEGER) ELSE 0
|
|
152
|
-
END),
|
|
153
|
-
MAX(CASE
|
|
154
|
-
WHEN json_extract(e.payload, '$.kind') = 'usage'
|
|
155
|
-
AND json_type(e.payload, '$.outputTokens') IN ('integer', 'real')
|
|
156
|
-
THEN CAST(json_extract(e.payload, '$.outputTokens') AS INTEGER) ELSE 0
|
|
157
|
-
END)
|
|
158
|
-
FROM ws_events e
|
|
159
|
-
JOIN agent_sessions s ON s.id = e.session_id AND s.workspace_id = e.workspace_id
|
|
160
|
-
WHERE e.workspace_id = OLD.workspace_id
|
|
161
|
-
AND e.session_id = OLD.session_id
|
|
162
|
-
AND e.type = 'agent:event'
|
|
163
|
-
AND json_valid(e.payload)
|
|
164
|
-
GROUP BY e.workspace_id, e.session_id;
|
|
165
|
-
END;
|
|
166
|
-
|
|
167
128
|
CREATE TABLE IF NOT EXISTS workspace_permission_rules (
|
|
168
129
|
id TEXT PRIMARY KEY,
|
|
169
130
|
workspace_id TEXT NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
@@ -236,5 +197,15 @@ export function initSchema(db) {
|
|
|
236
197
|
ON ws_events(type, created_at DESC, id DESC);
|
|
237
198
|
CREATE INDEX IF NOT EXISTS idx_workspace_chat_history_workspace_id_id
|
|
238
199
|
ON workspace_chat_history(workspace_id, id DESC);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_workspace_sort
|
|
201
|
+
ON tasks(workspace_id, sort_order);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_agent_sessions_workspace_started
|
|
203
|
+
ON agent_sessions(workspace_id, started_at DESC);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_agent_sessions_engine_session
|
|
205
|
+
ON agent_sessions(engine_session_id);
|
|
206
|
+
CREATE INDEX IF NOT EXISTS idx_workspaces_archived_updated
|
|
207
|
+
ON workspaces(archived_at, updated_at DESC);
|
|
208
|
+
CREATE INDEX IF NOT EXISTS idx_session_event_metrics_session
|
|
209
|
+
ON session_event_metrics(session_id);
|
|
239
210
|
`);
|
|
240
211
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import templatesRouter from './routes/templates.js';
|
|
|
25
25
|
import usageRoutes from './routes/usage.js';
|
|
26
26
|
import voiceRouter from './routes/voice.js';
|
|
27
27
|
import workspacesRouter from './routes/workspaces.js';
|
|
28
|
-
import { getAvailableSkills, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
28
|
+
import { getAvailableSkills, isAgentUnavailableError, reconcileOrphanSessions, restoreRetryCountsFromDb, sendMessage, setBackendPort, startAgent, startWatchdog, stopAgent, stopAllAgents, stopWatchdog, } from './services/agent/orchestrator.js';
|
|
29
29
|
import * as autoLoopService from './services/auto-loop-service.js';
|
|
30
30
|
import { runContentMigrationIfNeeded } from './services/content-migration-service.js';
|
|
31
31
|
import * as cronService from './services/cron-service.js';
|
|
@@ -41,6 +41,7 @@ import { startUsagePoller, stopUsagePoller } from './services/usage/index.js';
|
|
|
41
41
|
import * as wakeupService from './services/wakeup-service.js';
|
|
42
42
|
import { emit, emitEphemeral, handleConnection, setMessageHandler } from './services/websocket-service.js';
|
|
43
43
|
import { getActiveSession, getWorkspace, updateWorkspaceStatus } from './services/workspace-service.js';
|
|
44
|
+
import { pruneWsEvents, resolveRetentionConfig } from './services/ws-events-retention-service.js';
|
|
44
45
|
import { getClientSpaPath, getDbPath, getKoboHome, getPackageVersion } from './utils/paths.js';
|
|
45
46
|
console.log(`[kobo] Kōbō home: ${getKoboHome()}`);
|
|
46
47
|
// Initialize DB + run migrations
|
|
@@ -53,12 +54,37 @@ try {
|
|
|
53
54
|
if (pending.length > 0) {
|
|
54
55
|
const result = await createPreMigrationBackup(db, getDbPath(), `v${pending[pending.length - 1]}`);
|
|
55
56
|
console.log(`[kobo] Pre-migration backup before applying ${pending.length} migration(s): ${result.created}`);
|
|
57
|
+
if (result.deleted.length > 0) {
|
|
58
|
+
console.log(`[kobo] Rotated ${result.deleted.length} old pre-migration backup(s)`);
|
|
59
|
+
}
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
catch (err) {
|
|
59
63
|
console.error('[kobo] Pre-migration backup failed (continuing — daily backup remains as fallback):', err);
|
|
60
64
|
}
|
|
61
65
|
runMigrations(db);
|
|
66
|
+
// Event retention. OPT-IN: disabled by default, so this is a no-op until the
|
|
67
|
+
// user sets a window in Settings → Worktrees. When enabled it runs at boot
|
|
68
|
+
// only: no session is alive yet, so the batched write lock disturbs nobody, and
|
|
69
|
+
// the daily backup that follows snapshots an already-compacted file.
|
|
70
|
+
// Best-effort — a failure must never block boot.
|
|
71
|
+
try {
|
|
72
|
+
const retentionConfig = resolveRetentionConfig(getGlobalSettings());
|
|
73
|
+
if (retentionConfig.retentionDays > 0) {
|
|
74
|
+
const retentionStartedAt = Date.now();
|
|
75
|
+
const retention = pruneWsEvents(db, retentionConfig);
|
|
76
|
+
if (retention.deleted > 0 || retention.vacuumed) {
|
|
77
|
+
console.log(`[kobo] Event retention (${retentionConfig.retentionDays} d, keeping ${retentionConfig.keepPerWorkspace}/workspace): ` +
|
|
78
|
+
`${retention.deleted} agent event(s) permanently deleted, ` +
|
|
79
|
+
`${retention.sessionsRecomputed} session metric(s) recomputed, ` +
|
|
80
|
+
`${retention.vacuumed ? `VACUUM reclaimed ${retention.freePagesBefore - retention.freePagesAfter} page(s), ` : ''}` +
|
|
81
|
+
`${Date.now() - retentionStartedAt} ms`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
console.error('[kobo] Event retention failed (continuing):', err);
|
|
87
|
+
}
|
|
62
88
|
// Daily DB backup (best-effort, fire-and-forget — never blocks boot).
|
|
63
89
|
// Creates a WAL-safe snapshot alongside kobo.db if no backup exists in the
|
|
64
90
|
// last 24h, and rotates out older backups beyond the retention window.
|
|
@@ -329,10 +355,11 @@ setMessageHandler(async (type, payload) => {
|
|
|
329
355
|
}
|
|
330
356
|
catch (err) {
|
|
331
357
|
const msg = err instanceof Error ? err.message : String(err);
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
358
|
+
// Resume on every shape that means "no agent can receive this message"
|
|
359
|
+
// — the closed-stdin case included. Anything else (queue full, disk
|
|
360
|
+
// error, a stop in progress) surfaces to the user instead of silently
|
|
361
|
+
// respawning a fresh agent.
|
|
362
|
+
if (!isAgentUnavailableError(msg)) {
|
|
336
363
|
emitEphemeral(p.workspaceId, 'chat:rejected', { sessionId: p.sessionId, content: p.content, message: msg });
|
|
337
364
|
console.error(`[ws] chat:message failed for workspace ${p.workspaceId}:`, err);
|
|
338
365
|
return;
|
|
@@ -446,7 +473,7 @@ server.on('upgrade', (request, socket, head) => {
|
|
|
446
473
|
});
|
|
447
474
|
// Graceful shutdown handler
|
|
448
475
|
let isShuttingDown = false;
|
|
449
|
-
async function gracefulShutdown(signal) {
|
|
476
|
+
async function gracefulShutdown(signal, exitCode = 0) {
|
|
450
477
|
if (isShuttingDown)
|
|
451
478
|
return;
|
|
452
479
|
isShuttingDown = true;
|
|
@@ -517,7 +544,28 @@ async function gracefulShutdown(signal) {
|
|
|
517
544
|
}
|
|
518
545
|
clearTimeout(forceExitTimer);
|
|
519
546
|
console.log('[kobo] Shutdown complete');
|
|
520
|
-
process.exit(
|
|
547
|
+
process.exit(exitCode);
|
|
521
548
|
}
|
|
522
549
|
process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
|
|
523
550
|
process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
|
|
551
|
+
// Fermeture du terminal parent : sans ce gestionnaire, Node termine le
|
|
552
|
+
// process sans passer par l'arrêt propre, donc sans fermer la base.
|
|
553
|
+
process.on('SIGHUP', () => void gracefulShutdown('SIGHUP'));
|
|
554
|
+
// Un `EPIPE` sur stdout/stderr — typiquement quand la sortie est redirigée
|
|
555
|
+
// vers un lecteur qui s'est fermé — est émis de façon asynchrone et tue le
|
|
556
|
+
// process. On l'ignore : perdre une ligne de journal ne doit jamais arrêter
|
|
557
|
+
// le serveur.
|
|
558
|
+
process.stdout.on('error', () => { });
|
|
559
|
+
process.stderr.on('error', () => { });
|
|
560
|
+
// Filets de dernier recours. On ne poursuit PAS l'exécution après une
|
|
561
|
+
// exception non capturée : l'état du process est indéterminé. On garantit
|
|
562
|
+
// seulement une trace exploitable et une fermeture propre de la base et des
|
|
563
|
+
// WebSockets — ce que la terminaison implicite de Node ne fait pas.
|
|
564
|
+
process.on('uncaughtException', (err) => {
|
|
565
|
+
console.error('[kobo] Uncaught exception — shutting down:', err);
|
|
566
|
+
void gracefulShutdown('uncaughtException', 1);
|
|
567
|
+
});
|
|
568
|
+
process.on('unhandledRejection', (reason) => {
|
|
569
|
+
console.error('[kobo] Unhandled promise rejection — shutting down:', reason);
|
|
570
|
+
void gracefulShutdown('unhandledRejection', 1);
|
|
571
|
+
});
|
|
@@ -20,15 +20,20 @@ app.get('/branches', (c) => {
|
|
|
20
20
|
return c.json({ error: message }, 500);
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/** Hard ceiling on the worktree listing. `listWorktreeFiles` already truncates
|
|
24
|
+
* at this value; exposing it lets a caller ask for LESS, never for more. */
|
|
25
|
+
const MAX_WORKTREE_FILES = 5000;
|
|
26
|
+
// GET /api/git/files?path=<worktreePath>&limit=<n> — list a worktree's files
|
|
27
|
+
// (tracked + untracked-but-not-ignored) for the chat's `@file` autocomplete.
|
|
25
28
|
app.get('/files', (c) => {
|
|
26
29
|
try {
|
|
27
30
|
const repoPath = c.req.query('path');
|
|
28
31
|
if (!repoPath) {
|
|
29
32
|
return c.json({ error: 'Missing required query parameter: path' }, 400);
|
|
30
33
|
}
|
|
31
|
-
|
|
34
|
+
const requested = Number.parseInt(c.req.query('limit') ?? '', 10);
|
|
35
|
+
const limit = Number.isFinite(requested) && requested > 0 ? Math.min(requested, MAX_WORKTREE_FILES) : MAX_WORKTREE_FILES;
|
|
36
|
+
return c.json({ files: listWorktreeFiles(repoPath, limit) });
|
|
32
37
|
}
|
|
33
38
|
catch (err) {
|
|
34
39
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6,6 +6,7 @@ import { getDb } from '../db/index.js';
|
|
|
6
6
|
import { SCHEMA_VERSION } from '../db/migrations.js';
|
|
7
7
|
import { resolveCodexBinary } from '../services/agent/engines/codex/spawn.js';
|
|
8
8
|
import { getGlobalSettings, getProjectSettings, SETTINGS_SCHEMA_VERSION } from '../services/settings-service.js';
|
|
9
|
+
import { readRecentLogs } from '../utils/logger.js';
|
|
9
10
|
import { getDbPath, getKoboHome, getPackageVersion } from '../utils/paths.js';
|
|
10
11
|
import { slugifyProjectName } from '../utils/project-slug.js';
|
|
11
12
|
import { resolveWorkspaceWorktreePath } from '../utils/worktree-paths.js';
|
|
@@ -48,12 +49,27 @@ function safeFileSize(p) {
|
|
|
48
49
|
return null;
|
|
49
50
|
}
|
|
50
51
|
}
|
|
52
|
+
const VALID_LOG_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
53
|
+
// GET /api/health/logs?limit=200&level=error — most recent entries, newest first.
|
|
54
|
+
// Declared BEFORE any dynamic segment, per the route-ordering invariant.
|
|
55
|
+
app.get('/logs', (c) => {
|
|
56
|
+
try {
|
|
57
|
+
const rawLimit = Number.parseInt(c.req.query('limit') ?? '', 10);
|
|
58
|
+
const limit = Number.isFinite(rawLimit) ? rawLimit : 200;
|
|
59
|
+
const rawLevel = c.req.query('level');
|
|
60
|
+
const level = VALID_LOG_LEVELS.includes(rawLevel) ? rawLevel : undefined;
|
|
61
|
+
return c.json({ entries: readRecentLogs(level ? { limit, level } : { limit }) });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return c.json({ error: err instanceof Error ? err.message : 'Failed to read logs' }, 500);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
51
67
|
// GET /api/health/report — detailed health diagnostics for the Health panel.
|
|
52
68
|
app.get('/report', async (c) => {
|
|
53
69
|
const db = getDb();
|
|
54
70
|
const dbPath = getDbPath();
|
|
55
71
|
const home = getKoboHome();
|
|
56
|
-
const cliChecks = Promise.all([checkClaudeCli(), checkCodexCli()]);
|
|
72
|
+
const cliChecks = Promise.all([checkClaudeCli(), checkCodexCli(), checkCli('gh'), checkCli('glab')]);
|
|
57
73
|
// DB schema version
|
|
58
74
|
const row = db.prepare('SELECT MAX(version) as v FROM schema_migrations').get();
|
|
59
75
|
const dbSchemaVersion = row?.v ?? 0;
|
|
@@ -145,7 +161,7 @@ app.get('/report', async (c) => {
|
|
|
145
161
|
.all();
|
|
146
162
|
const settingsRow = db.prepare('SELECT COUNT(*) as n FROM workspaces').get();
|
|
147
163
|
const archivedRow = db.prepare('SELECT COUNT(*) as n FROM workspaces WHERE archived_at IS NOT NULL').get();
|
|
148
|
-
const [claudeCli, codexCli] = await cliChecks;
|
|
164
|
+
const [claudeCli, codexCli, ghCli, glabCli] = await cliChecks;
|
|
149
165
|
const report = {
|
|
150
166
|
version: getPackageVersion(),
|
|
151
167
|
koboHome: home,
|
|
@@ -158,6 +174,7 @@ app.get('/report', async (c) => {
|
|
|
158
174
|
settings: { schemaVersion: SETTINGS_SCHEMA_VERSION },
|
|
159
175
|
claudeCli,
|
|
160
176
|
codexCli,
|
|
177
|
+
forgeCli: { gh: ghCli, glab: glabCli },
|
|
161
178
|
workspaces: {
|
|
162
179
|
total: settingsRow.n,
|
|
163
180
|
archived: archivedRow.n,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
+
import { getDb } from '../db/index.js';
|
|
2
3
|
import { getBackendPort } from '../services/agent/orchestrator.js';
|
|
3
4
|
import { DEFAULT_NOTION_INITIAL_PROMPT, DEFAULT_SENTRY_INITIAL_PROMPT, } from '../services/initial-prompt-template-service.js';
|
|
4
5
|
import { generateToken, getLanUrls } from '../services/network-access-service.js';
|
|
@@ -7,6 +8,7 @@ import { DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT } from '../services/settings-defaul
|
|
|
7
8
|
import * as settingsService from '../services/settings-service.js';
|
|
8
9
|
import { DEFAULT_CI_FIX_PROMPT_TEMPLATE, DEFAULT_FINALIZATION_PROMPT, DEFAULT_GIT_CONVENTIONS, DEFAULT_PR_PROMPT_TEMPLATE, } from '../services/settings-service.js';
|
|
9
10
|
import { listTemplates, replaceAllTemplates } from '../services/templates-service.js';
|
|
11
|
+
import { countPrunableWsEvents } from '../services/ws-events-retention-service.js';
|
|
10
12
|
/** Hono sub-router for global and per-project settings CRUD. */
|
|
11
13
|
const app = new Hono();
|
|
12
14
|
// GET /api/settings — return full settings
|
|
@@ -47,6 +49,28 @@ app.get('/defaults', (c) => {
|
|
|
47
49
|
changeSourceBranchScript: DEFAULT_CHANGE_SOURCE_BRANCH_SCRIPT,
|
|
48
50
|
});
|
|
49
51
|
});
|
|
52
|
+
// GET /api/settings/ws-events-retention-preview?days=<n>&keep=<n>
|
|
53
|
+
// Count what a retention window WOULD delete, without deleting anything. Backs
|
|
54
|
+
// the confirmation dialog: enabling retention on a year-old database destroys
|
|
55
|
+
// months of conversation in one go, and the user is entitled to the number
|
|
56
|
+
// before agreeing to it.
|
|
57
|
+
app.get('/ws-events-retention-preview', (c) => {
|
|
58
|
+
try {
|
|
59
|
+
const days = Number.parseInt(c.req.query('days') ?? '', 10);
|
|
60
|
+
const keep = Number.parseInt(c.req.query('keep') ?? '', 10);
|
|
61
|
+
if (!Number.isInteger(days) || days < 0 || !Number.isInteger(keep) || keep < 0) {
|
|
62
|
+
return c.json({ error: 'days and keep must be non-negative integers' }, 400);
|
|
63
|
+
}
|
|
64
|
+
const db = getDb();
|
|
65
|
+
const total = db.prepare('SELECT COUNT(*) AS c FROM ws_events').get().c;
|
|
66
|
+
const deletable = countPrunableWsEvents(db, { retentionDays: days, keepPerWorkspace: keep });
|
|
67
|
+
return c.json({ deletable, total });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
71
|
+
return c.json({ error: message }, 500);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
50
74
|
// GET /api/settings/network — network access state + LAN URLs
|
|
51
75
|
app.get('/network', (c) => {
|
|
52
76
|
try {
|