@amoree-code/ocean 0.0.0-stage → 1.2.0

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 (300) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -2
  3. package/dist/application/auth/auth-orchestrator.js +77 -0
  4. package/dist/application/clients/clients.js +646 -0
  5. package/dist/application/clients/editors.js +398 -0
  6. package/dist/application/clients/hooks.js +335 -0
  7. package/dist/application/clients/native.js +410 -0
  8. package/dist/application/clients/opencode-plugin.js +158 -0
  9. package/dist/application/clients/registry.js +188 -0
  10. package/dist/application/gateway/webhook-gateway.js +137 -0
  11. package/dist/application/install/provider-installer.js +184 -0
  12. package/dist/application/integrations/charter-copies.js +76 -0
  13. package/dist/application/integrations/claude-vscode.js +106 -0
  14. package/dist/application/integrations/engine-entry-repair.js +105 -0
  15. package/dist/application/integrations/skill-filter.js +254 -0
  16. package/dist/application/layout/layout-apply.js +443 -0
  17. package/dist/application/layout/layout-plan.js +236 -0
  18. package/dist/application/layout/workspace-template.js +34 -0
  19. package/dist/application/onboarding/state.js +23 -0
  20. package/dist/application/onboarding/steps.js +249 -0
  21. package/dist/application/profiles/profile-distribution.js +53 -0
  22. package/dist/application/profiles/profile-suggestion.js +39 -0
  23. package/dist/application/profiles/profile-upgrade-service.js +208 -0
  24. package/dist/application/pull/history-index.js +476 -0
  25. package/dist/application/pull/project-match.js +132 -0
  26. package/dist/application/pull/pull.js +202 -0
  27. package/dist/application/pull/server-capture.js +307 -0
  28. package/dist/application/scheduler/local-scheduler.js +210 -0
  29. package/dist/composition/runtime.js +64 -0
  30. package/dist/engine-root.js +9 -0
  31. package/dist/infrastructure/filesystem/profile-loader.js +46 -0
  32. package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
  33. package/dist/infrastructure/filesystem/skill-loader.js +84 -0
  34. package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
  35. package/dist/infrastructure/observability/runtime-logger.js +16 -0
  36. package/dist/infrastructure/process/agent-tool-executor.js +99 -0
  37. package/dist/infrastructure/process/cli-process.js +49 -0
  38. package/dist/infrastructure/process/interactive-process.js +145 -0
  39. package/dist/infrastructure/process/process-supervisor.js +194 -0
  40. package/dist/infrastructure/process/service.js +252 -0
  41. package/dist/infrastructure/providers/browser-provider.js +2 -0
  42. package/dist/infrastructure/providers/claude-sdk.js +137 -0
  43. package/dist/infrastructure/providers/client-home.js +14 -0
  44. package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
  45. package/dist/infrastructure/providers/ollama-embedder.js +74 -0
  46. package/dist/infrastructure/providers/openai-compatible.js +268 -0
  47. package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
  48. package/dist/infrastructure/providers/provider-capabilities.js +50 -0
  49. package/dist/infrastructure/providers/provider-invocation.js +184 -0
  50. package/dist/infrastructure/providers/provider-registry.js +77 -0
  51. package/dist/infrastructure/providers/providers.js +134 -0
  52. package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
  53. package/dist/interfaces/cli/auth-command.js +54 -0
  54. package/dist/interfaces/cli/browser-bridge-command.js +93 -0
  55. package/dist/interfaces/cli/browser-command.js +119 -0
  56. package/dist/interfaces/cli/capture-command.js +125 -0
  57. package/dist/interfaces/cli/client-test-command.js +133 -0
  58. package/dist/interfaces/cli/clients-command.js +129 -0
  59. package/dist/interfaces/cli/command-registry.js +1211 -0
  60. package/dist/interfaces/cli/context-command.js +172 -0
  61. package/dist/interfaces/cli/daily-command.js +131 -0
  62. package/dist/interfaces/cli/governance-command.js +55 -0
  63. package/dist/interfaces/cli/handoff-command.js +35 -0
  64. package/dist/interfaces/cli/help-command.js +207 -0
  65. package/dist/interfaces/cli/idea-command.js +47 -0
  66. package/dist/interfaces/cli/intercept-command.js +317 -0
  67. package/dist/interfaces/cli/layout-command.js +27 -0
  68. package/dist/interfaces/cli/market-command.js +52 -0
  69. package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
  70. package/dist/interfaces/cli/memory-command.js +317 -0
  71. package/dist/interfaces/cli/migrate-command.js +26 -0
  72. package/dist/interfaces/cli/observe-command.js +78 -0
  73. package/dist/interfaces/cli/onboard-command.js +123 -0
  74. package/dist/interfaces/cli/operate-command.js +59 -0
  75. package/dist/interfaces/cli/profiles-command.js +70 -0
  76. package/dist/interfaces/cli/pull-command.js +54 -0
  77. package/dist/interfaces/cli/setup-command.js +143 -0
  78. package/dist/interfaces/cli/tasks-command.js +249 -0
  79. package/dist/main.js +49 -0
  80. package/dist/node-warnings.js +17 -0
  81. package/dist/version.js +8 -0
  82. package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
  83. package/market/mcp/gateway/ocean.capability.json +21 -0
  84. package/market/mcp/obsidian/ocean.capability.json +21 -0
  85. package/market/mcp/ocean/ocean.capability.json +21 -0
  86. package/market/mcp/playwright/ocean.capability.json +23 -0
  87. package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
  88. package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
  89. package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
  90. package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
  91. package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
  92. package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
  93. package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
  94. package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
  95. package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
  96. package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
  97. package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
  98. package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
  99. package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
  100. package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
  101. package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
  102. package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
  103. package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
  104. package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
  105. package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
  106. package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
  107. package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
  108. package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
  109. package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
  110. package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
  111. package/node_modules/@ocean/brain/package.json +11 -0
  112. package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
  113. package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
  114. package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
  115. package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
  116. package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
  117. package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
  118. package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
  119. package/node_modules/@ocean/context/package.json +11 -0
  120. package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
  121. package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
  122. package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
  123. package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
  124. package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
  125. package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
  126. package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
  127. package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
  128. package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
  129. package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
  130. package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
  131. package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
  132. package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
  133. package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
  134. package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
  135. package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
  136. package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
  137. package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
  138. package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
  139. package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
  140. package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
  141. package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
  142. package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
  143. package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
  144. package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
  145. package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
  146. package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
  147. package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
  148. package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
  149. package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
  150. package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
  151. package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
  152. package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
  153. package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
  154. package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
  155. package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
  156. package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
  157. package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
  158. package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
  159. package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
  160. package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
  161. package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
  162. package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
  163. package/node_modules/@ocean/core/package.json +14 -0
  164. package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
  165. package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
  166. package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
  167. package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
  168. package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
  169. package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
  170. package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
  171. package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
  172. package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
  173. package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
  174. package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
  175. package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
  176. package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
  177. package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
  178. package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
  179. package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
  180. package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
  181. package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
  182. package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
  183. package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
  184. package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
  185. package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
  186. package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
  187. package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
  188. package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
  189. package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
  190. package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
  191. package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
  192. package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
  193. package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
  194. package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
  195. package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
  196. package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
  197. package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
  198. package/node_modules/@ocean/loop/package.json +11 -0
  199. package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
  200. package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
  201. package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
  202. package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
  203. package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
  204. package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
  205. package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
  206. package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
  207. package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
  208. package/node_modules/@ocean/market/dist/install/lock.js +34 -0
  209. package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
  210. package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
  211. package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
  212. package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
  213. package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
  214. package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
  215. package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
  216. package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
  217. package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
  218. package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
  219. package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
  220. package/node_modules/@ocean/market/package.json +11 -0
  221. package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
  222. package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
  223. package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
  224. package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
  225. package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
  226. package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
  227. package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
  228. package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
  229. package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
  230. package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
  231. package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
  232. package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
  233. package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
  234. package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
  235. package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
  236. package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
  237. package/node_modules/@ocean/services/package.json +11 -0
  238. package/ocean-marketplace.json +415 -0
  239. package/package.json +67 -4
  240. package/skills/core/business-logic/SKILL.md +22 -0
  241. package/skills/core/catch-up/SKILL.md +17 -0
  242. package/skills/core/core-thinking/SKILL.md +11 -0
  243. package/skills/core/design-thinking/SKILL.md +19 -0
  244. package/skills/core/graft/SKILL.md +156 -0
  245. package/skills/core/loop/SKILL.md +20 -0
  246. package/skills/core/session-handoff/SKILL.md +15 -0
  247. package/skills/core/use-browser/SKILL.md +69 -0
  248. package/skills/core/verification/SKILL.md +11 -0
  249. package/skills/index.json +56 -0
  250. package/templates/README.md +4 -0
  251. package/templates/profiles/default.json +51 -0
  252. package/templates/profiles/developer.json +51 -0
  253. package/templates/profiles/reviewer.json +51 -0
  254. package/templates/profiles/strategist.json +51 -0
  255. package/templates/workspace/00-inbox/INBOX.md +7 -0
  256. package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
  257. package/templates/workspace/00-inbox/native/.gitkeep +0 -0
  258. package/templates/workspace/01-daily/.gitkeep +0 -0
  259. package/templates/workspace/02-personal/CONFLICTS.md +7 -0
  260. package/templates/workspace/02-personal/MEMORY.md +15 -0
  261. package/templates/workspace/02-personal/goals/.gitkeep +0 -0
  262. package/templates/workspace/02-personal/identity/.gitkeep +0 -0
  263. package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
  264. package/templates/workspace/03-professional/.gitkeep +0 -0
  265. package/templates/workspace/04-projects/PROJECTS.md +5 -0
  266. package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
  267. package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
  268. package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
  269. package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
  270. package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
  271. package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
  272. package/templates/workspace/06-templates/TEMPLATES.md +25 -0
  273. package/templates/workspace/06-templates/decision.md +8 -0
  274. package/templates/workspace/06-templates/intelligence.md +32 -0
  275. package/templates/workspace/06-templates/project-claude.md +48 -0
  276. package/templates/workspace/06-templates/session.md +38 -0
  277. package/templates/workspace/06-templates/task.md +42 -0
  278. package/templates/workspace/MANIFEST.json +51 -0
  279. package/templates/workspace/bridge/config/CONFIG.md +21 -0
  280. package/templates/workspace/bridge/config/runtime.json +4 -0
  281. package/templates/workspace/charter/core.md +104 -0
  282. package/templates/workspace/charter/policies/README.md +31 -0
  283. package/templates/workspace/charter/policies/claude-code-context.md +90 -0
  284. package/templates/workspace/charter/policies/concise-output.md +29 -0
  285. package/templates/workspace/charter/policies/context.md +91 -0
  286. package/templates/workspace/charter/policies/contracts/task.md +189 -0
  287. package/templates/workspace/charter/policies/git.md +60 -0
  288. package/templates/workspace/charter/policies/graph.md +47 -0
  289. package/templates/workspace/charter/policies/knowledge.md +33 -0
  290. package/templates/workspace/charter/policies/lifecycle.md +119 -0
  291. package/templates/workspace/charter/policies/memory.md +67 -0
  292. package/templates/workspace/charter/policies/minimal-change.md +43 -0
  293. package/templates/workspace/charter/policies/models.md +57 -0
  294. package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
  295. package/templates/workspace/charter/policies/response.md +39 -0
  296. package/templates/workspace/charter/policies/skill-observer.md +44 -0
  297. package/templates/workspace/charter/policies/stack.md +23 -0
  298. package/templates/workspace/charter/policies/task.md +139 -0
  299. package/templates/workspace/charter/policies/verification.md +29 -0
  300. package/templates/workspace/sessions/.gitkeep +0 -0
@@ -0,0 +1,690 @@
1
+ import { access, mkdir } from "node:fs/promises";
2
+ import { backup, DatabaseSync } from "node:sqlite";
3
+ import { assertValidStatusTransition, } from "@ocean/core/domain/sessions/session.js";
4
+ import { oceanPath, SYSTEM_DIR } from "@ocean/core/shared/paths.js";
5
+ import { validateSession } from "./session-validator.js";
6
+ export class SessionStore {
7
+ database;
8
+ constructor(databaseFile = oceanPath(SYSTEM_DIR, "sessions", "sessions.sqlite"), options = {}) {
9
+ this.database = new DatabaseSync(databaseFile, options.readOnly ? { readOnly: true } : {});
10
+ if (options.readOnly) {
11
+ this.database.exec("PRAGMA query_only = ON; PRAGMA busy_timeout = 5000;");
12
+ return;
13
+ }
14
+ this.database.exec(`
15
+ PRAGMA journal_mode = WAL;
16
+ PRAGMA busy_timeout = 5000;
17
+ CREATE TABLE IF NOT EXISTS sessions (
18
+ session_id TEXT PRIMARY KEY,
19
+ title TEXT NOT NULL DEFAULT '',
20
+ task_id TEXT,
21
+ handoff_id TEXT,
22
+ provider TEXT NOT NULL,
23
+ provider_session_id TEXT,
24
+ parent_session_id TEXT,
25
+ profile TEXT NOT NULL,
26
+ profile_identity TEXT NOT NULL DEFAULT '',
27
+ working_directory TEXT NOT NULL,
28
+ status TEXT NOT NULL,
29
+ created_at TEXT NOT NULL,
30
+ updated_at TEXT NOT NULL,
31
+ resume_data TEXT,
32
+ context_hash TEXT,
33
+ context_bytes INTEGER NOT NULL DEFAULT 0,
34
+ next_action TEXT NOT NULL DEFAULT '',
35
+ verification_status TEXT NOT NULL DEFAULT 'unknown',
36
+ summary_path TEXT,
37
+ summary_hash TEXT,
38
+ summary_bytes INTEGER NOT NULL DEFAULT 0,
39
+ closeout_status TEXT NOT NULL DEFAULT 'pending',
40
+ closeout_version TEXT NOT NULL DEFAULT '1',
41
+ closed_at TEXT
42
+ );
43
+ CREATE TABLE IF NOT EXISTS handoffs (
44
+ handoff_id TEXT PRIMARY KEY,
45
+ task_id TEXT,
46
+ title TEXT NOT NULL,
47
+ objective TEXT NOT NULL DEFAULT '',
48
+ state TEXT NOT NULL DEFAULT 'paused',
49
+ profile_id TEXT NOT NULL DEFAULT '',
50
+ profile_identity TEXT NOT NULL DEFAULT '',
51
+ source_session_id TEXT,
52
+ provider TEXT NOT NULL DEFAULT '',
53
+ parent_session_id TEXT,
54
+ decisions_json TEXT NOT NULL DEFAULT '[]',
55
+ changed_files_json TEXT NOT NULL DEFAULT '[]',
56
+ commit_ref TEXT,
57
+ verification_json TEXT NOT NULL DEFAULT '[]',
58
+ not_proven_json TEXT NOT NULL DEFAULT '[]',
59
+ blocked_json TEXT NOT NULL DEFAULT '[]',
60
+ permissions_json TEXT NOT NULL DEFAULT '{}',
61
+ context_manifest_json TEXT NOT NULL DEFAULT '{}',
62
+ next_action TEXT NOT NULL DEFAULT '',
63
+ source_summary_path TEXT,
64
+ content TEXT NOT NULL DEFAULT '',
65
+ content_bytes INTEGER NOT NULL DEFAULT 0,
66
+ created_at TEXT NOT NULL,
67
+ updated_at TEXT NOT NULL
68
+ );
69
+ CREATE TABLE IF NOT EXISTS ideas (
70
+ idea_id TEXT PRIMARY KEY,
71
+ title TEXT NOT NULL,
72
+ content TEXT NOT NULL DEFAULT '',
73
+ source_session_id TEXT,
74
+ status TEXT NOT NULL DEFAULT 'raw',
75
+ target TEXT,
76
+ created_at TEXT NOT NULL,
77
+ updated_at TEXT NOT NULL
78
+ );
79
+ CREATE INDEX IF NOT EXISTS ideas_status_updated_at ON ideas(status, updated_at);
80
+ CREATE TABLE IF NOT EXISTS session_events (
81
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
82
+ session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
83
+ type TEXT NOT NULL,
84
+ data TEXT NOT NULL,
85
+ created_at TEXT NOT NULL
86
+ );
87
+ CREATE TABLE IF NOT EXISTS session_links (
88
+ parent_session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
89
+ child_session_id TEXT NOT NULL REFERENCES sessions(session_id) ON DELETE CASCADE,
90
+ PRIMARY KEY (parent_session_id, child_session_id)
91
+ );
92
+ CREATE TABLE IF NOT EXISTS capture_items (
93
+ capture_id INTEGER PRIMARY KEY AUTOINCREMENT,
94
+ source_event_id INTEGER NOT NULL UNIQUE REFERENCES session_events(event_id) ON DELETE CASCADE,
95
+ type TEXT NOT NULL DEFAULT 'unclassified',
96
+ status TEXT NOT NULL DEFAULT 'new',
97
+ target TEXT,
98
+ created_at TEXT NOT NULL,
99
+ reviewed_at TEXT
100
+ );
101
+ CREATE TABLE IF NOT EXISTS session_pids (
102
+ session_id TEXT PRIMARY KEY REFERENCES sessions(session_id) ON DELETE CASCADE,
103
+ pid INTEGER NOT NULL,
104
+ updated_at TEXT NOT NULL
105
+ );
106
+ CREATE INDEX IF NOT EXISTS session_events_session_id ON session_events(session_id);
107
+ CREATE INDEX IF NOT EXISTS session_links_parent_id ON session_links(parent_session_id);
108
+ CREATE INDEX IF NOT EXISTS sessions_status_updated_at ON sessions(status, updated_at);
109
+ CREATE INDEX IF NOT EXISTS handoffs_updated_at ON handoffs(updated_at);
110
+ `);
111
+ this.migrateTicketIdToTaskId();
112
+ this.migrateProfileIdentityColumn();
113
+ this.migrateHandoffSummaryColumn();
114
+ this.migrateProjectColumns();
115
+ this.database.exec("CREATE INDEX IF NOT EXISTS handoffs_task_id ON handoffs(task_id);");
116
+ }
117
+ // Older databases used `ticket_id`. Rename it in place so history survives the
118
+ // terminology change instead of silently dropping the association.
119
+ migrateTicketIdToTaskId() {
120
+ for (const table of ["sessions", "handoffs"]) {
121
+ const columns = this.database
122
+ .prepare(`PRAGMA table_info(${table})`)
123
+ .all();
124
+ const hasTicketId = columns.some((column) => column.name === "ticket_id");
125
+ const hasTaskId = columns.some((column) => column.name === "task_id");
126
+ if (hasTicketId && !hasTaskId) {
127
+ this.database.exec(`ALTER TABLE ${table} RENAME COLUMN ticket_id TO task_id`);
128
+ }
129
+ else if (hasTicketId && hasTaskId) {
130
+ this.database.exec(`UPDATE ${table} SET task_id = ticket_id WHERE task_id IS NULL AND ticket_id IS NOT NULL`);
131
+ }
132
+ }
133
+ }
134
+ // CREATE TABLE IF NOT EXISTS does not add columns to a table that already exists, so a
135
+ // database created before profile identity existed needs this column added explicitly.
136
+ migrateProfileIdentityColumn() {
137
+ const columns = this.database
138
+ .prepare("PRAGMA table_info(sessions)")
139
+ .all();
140
+ if (!columns.some((column) => column.name === "profile_identity")) {
141
+ this.database.exec("ALTER TABLE sessions ADD COLUMN profile_identity TEXT NOT NULL DEFAULT ''");
142
+ }
143
+ const additions = [
144
+ ["title", "TEXT NOT NULL DEFAULT ''"],
145
+ ["task_id", "TEXT"],
146
+ ["handoff_id", "TEXT"],
147
+ ["context_hash", "TEXT"],
148
+ ["context_bytes", "INTEGER NOT NULL DEFAULT 0"],
149
+ ["next_action", "TEXT NOT NULL DEFAULT ''"],
150
+ ["verification_status", "TEXT NOT NULL DEFAULT 'unknown'"],
151
+ ["summary_path", "TEXT"],
152
+ ["summary_hash", "TEXT"],
153
+ ["summary_bytes", "INTEGER NOT NULL DEFAULT 0"],
154
+ ["closeout_status", "TEXT NOT NULL DEFAULT 'pending'"],
155
+ ["closeout_version", "TEXT NOT NULL DEFAULT '1'"],
156
+ ["closed_at", "TEXT"],
157
+ ];
158
+ for (const [name, definition] of additions) {
159
+ if (!columns.some((column) => column.name === name))
160
+ this.database.exec(`ALTER TABLE sessions ADD COLUMN ${name} ${definition}`);
161
+ }
162
+ }
163
+ migrateHandoffSummaryColumn() {
164
+ const columns = this.database
165
+ .prepare("PRAGMA table_info(handoffs)")
166
+ .all();
167
+ if (!columns.some((column) => column.name === "source_summary_path")) {
168
+ this.database.exec("ALTER TABLE handoffs ADD COLUMN source_summary_path TEXT");
169
+ }
170
+ }
171
+ // T-256: sessions and handoffs know their project. Additive only, and deliberately not in the
172
+ // CREATE TABLE block above, so an old and a new database take the same path.
173
+ migrateProjectColumns() {
174
+ for (const table of ["sessions", "handoffs"]) {
175
+ const columns = this.database
176
+ .prepare(`PRAGMA table_info(${table})`)
177
+ .all();
178
+ if (!columns.some((column) => column.name === "project_id"))
179
+ this.database.exec(`ALTER TABLE ${table} ADD COLUMN project_id TEXT`);
180
+ }
181
+ this.database.exec(`
182
+ CREATE INDEX IF NOT EXISTS sessions_project_id ON sessions(project_id);
183
+ CREATE INDEX IF NOT EXISTS handoffs_project_task ON handoffs(project_id, task_id);
184
+ `);
185
+ }
186
+ create(input) {
187
+ const now = new Date().toISOString();
188
+ const session = validateSession({
189
+ ...input,
190
+ status: input.status ?? "created",
191
+ createdAt: now,
192
+ updatedAt: now,
193
+ });
194
+ this.database
195
+ .prepare(`
196
+ INSERT INTO sessions (session_id, title, task_id, handoff_id, provider, provider_session_id, parent_session_id, profile,
197
+ profile_identity, working_directory, status, created_at, updated_at, resume_data, context_hash, context_bytes, next_action, verification_status, project_id)
198
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
199
+ `)
200
+ .run(session.sessionId, session.title, session.taskId, session.handoffId, session.provider, session.providerSessionId, session.parentSessionId, session.profile, session.profileIdentity, session.workingDirectory, session.status, now, now, session.resumeData, session.contextHash, session.contextBytes, session.nextAction, session.verificationStatus, session.projectId);
201
+ if (session.parentSessionId) {
202
+ this.database
203
+ .prepare("INSERT INTO session_links (parent_session_id, child_session_id) VALUES (?, ?)")
204
+ .run(session.parentSessionId, session.sessionId);
205
+ }
206
+ return session;
207
+ }
208
+ get(sessionId) {
209
+ const row = this.database
210
+ .prepare("SELECT * FROM sessions WHERE session_id = ?")
211
+ .get(sessionId);
212
+ return row ? this.toSession(row) : null;
213
+ }
214
+ updateStatus(sessionId, status, resumeData) {
215
+ const current = this.get(sessionId);
216
+ if (!current)
217
+ throw new Error(`Session not found: ${sessionId}`);
218
+ assertValidStatusTransition(current.status, status);
219
+ this.database
220
+ .prepare("UPDATE sessions SET status = ?, resume_data = COALESCE(?, resume_data), updated_at = ? WHERE session_id = ?")
221
+ .run(status, resumeData ?? null, new Date().toISOString(), sessionId);
222
+ }
223
+ updateProviderSessionId(sessionId, providerSessionId) {
224
+ this.database
225
+ .prepare("UPDATE sessions SET provider_session_id = ?, updated_at = ? WHERE session_id = ?")
226
+ .run(providerSessionId, new Date().toISOString(), sessionId);
227
+ }
228
+ // Tracks the OS pid of the provider process currently running for a session, so a
229
+ // stale-session reconciliation can actually stop the work instead of only relabeling it.
230
+ setProviderPid(sessionId, pid) {
231
+ this.database
232
+ .prepare("INSERT INTO session_pids (session_id, pid, updated_at) VALUES (?, ?, ?) " +
233
+ "ON CONFLICT(session_id) DO UPDATE SET pid = excluded.pid, updated_at = excluded.updated_at")
234
+ .run(sessionId, pid, new Date().toISOString());
235
+ }
236
+ getProviderPid(sessionId) {
237
+ const row = this.database
238
+ .prepare("SELECT pid FROM session_pids WHERE session_id = ?")
239
+ .get(sessionId);
240
+ return row?.pid ?? null;
241
+ }
242
+ clearProviderPid(sessionId) {
243
+ this.database
244
+ .prepare("DELETE FROM session_pids WHERE session_id = ?")
245
+ .run(sessionId);
246
+ }
247
+ updateContext(sessionId, contextHash, contextBytes) {
248
+ this.database
249
+ .prepare("UPDATE sessions SET context_hash = ?, context_bytes = ?, updated_at = ? WHERE session_id = ?")
250
+ .run(contextHash, contextBytes, new Date().toISOString(), sessionId);
251
+ }
252
+ updateHandoff(sessionId, handoffId, nextAction = "") {
253
+ this.database
254
+ .prepare("UPDATE sessions SET handoff_id = ?, next_action = ?, updated_at = ? WHERE session_id = ?")
255
+ .run(handoffId, nextAction, new Date().toISOString(), sessionId);
256
+ }
257
+ updateCloseout(sessionId, input) {
258
+ this.database
259
+ .prepare(`UPDATE sessions SET summary_path = ?, summary_hash = ?, summary_bytes = ?, closeout_status = ?, closeout_version = '1', closed_at = ?, updated_at = ? WHERE session_id = ?`)
260
+ .run(input.summaryPath, input.summaryHash, input.summaryBytes, input.closeoutStatus, input.closedAt, input.closedAt, sessionId);
261
+ }
262
+ saveHandoff(input) {
263
+ const now = new Date().toISOString();
264
+ const value = (key, fallback) => {
265
+ const candidate = input[key] ?? fallback;
266
+ return typeof candidate === "string" ||
267
+ typeof candidate === "number" ||
268
+ candidate === null
269
+ ? candidate
270
+ : String(candidate);
271
+ };
272
+ const jsonValue = (key, fallback) => JSON.stringify(input[key] ?? fallback);
273
+ this.database
274
+ .prepare(`
275
+ INSERT INTO handoffs (handoff_id, task_id, title, objective, state, profile_id, profile_identity,
276
+ source_session_id, provider, parent_session_id, decisions_json, changed_files_json, commit_ref,
277
+ verification_json, not_proven_json, blocked_json, permissions_json, context_manifest_json,
278
+ next_action, source_summary_path, content, content_bytes, created_at, updated_at, project_id)
279
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
280
+ ON CONFLICT(handoff_id) DO UPDATE SET task_id=excluded.task_id, title=excluded.title,
281
+ objective=excluded.objective, state=excluded.state, profile_id=excluded.profile_id,
282
+ profile_identity=excluded.profile_identity, source_session_id=excluded.source_session_id,
283
+ provider=excluded.provider, parent_session_id=excluded.parent_session_id,
284
+ decisions_json=excluded.decisions_json, changed_files_json=excluded.changed_files_json,
285
+ commit_ref=excluded.commit_ref, verification_json=excluded.verification_json,
286
+ not_proven_json=excluded.not_proven_json, blocked_json=excluded.blocked_json,
287
+ permissions_json=excluded.permissions_json, context_manifest_json=excluded.context_manifest_json,
288
+ next_action=excluded.next_action, source_summary_path=excluded.source_summary_path,
289
+ content=excluded.content, content_bytes=excluded.content_bytes,
290
+ updated_at=excluded.updated_at,
291
+ project_id=COALESCE(excluded.project_id, handoffs.project_id)
292
+ `)
293
+ .run(value("handoffId", ""), value("taskId", null), value("title", ""), value("objective", ""), value("state", "paused"), value("profileId", ""), value("profileIdentity", ""), value("sourceSessionId", null), value("provider", ""), value("parentSessionId", null), jsonValue("decisions", []), jsonValue("changedFiles", []), value("commit", null), jsonValue("verification", []), jsonValue("notProven", []), jsonValue("blocked", []), jsonValue("permissions", {}), jsonValue("contextManifest", {}), value("nextAction", ""), value("sourceSummaryPath", null), value("content", ""), value("contentBytes", 0), value("createdAt", now), now, value("projectId", null));
294
+ }
295
+ getHandoff(handoffId) {
296
+ const row = this.database
297
+ .prepare("SELECT * FROM handoffs WHERE handoff_id = ?")
298
+ .get(handoffId);
299
+ return row ? this.deserializeHandoff(row) : null;
300
+ }
301
+ listHandoffs(taskId, projectId) {
302
+ const filters = [];
303
+ const values = [];
304
+ if (taskId) {
305
+ filters.push("task_id = ?");
306
+ values.push(taskId);
307
+ }
308
+ if (projectId) {
309
+ filters.push("project_id = ?");
310
+ values.push(projectId);
311
+ }
312
+ const where = filters.length ? ` WHERE ${filters.join(" AND ")}` : "";
313
+ const rows = this.database
314
+ .prepare(`SELECT * FROM handoffs${where} ORDER BY updated_at DESC`)
315
+ .all(...values);
316
+ return rows.map((row) => this.deserializeHandoff(row));
317
+ }
318
+ // --- project backfill (T-256) --------------------------------------------------------
319
+ sessionDirectoriesMissingProject() {
320
+ return this.database
321
+ .prepare("SELECT DISTINCT working_directory FROM sessions WHERE project_id IS NULL")
322
+ .all().map((row) => row.working_directory);
323
+ }
324
+ setProjectForDirectory(directory, projectId) {
325
+ return Number(this.database
326
+ .prepare("UPDATE sessions SET project_id = ? WHERE working_directory = ? AND project_id IS NULL")
327
+ .run(projectId, directory).changes);
328
+ }
329
+ backfillHandoffProjects() {
330
+ return Number(this.database
331
+ .prepare(`UPDATE handoffs SET project_id = (SELECT s.project_id FROM sessions s WHERE s.session_id = handoffs.source_session_id)
332
+ WHERE project_id IS NULL AND source_session_id IS NOT NULL
333
+ AND (SELECT s.project_id FROM sessions s WHERE s.session_id = handoffs.source_session_id) IS NOT NULL`)
334
+ .run().changes);
335
+ }
336
+ saveIdea(input) {
337
+ const now = new Date().toISOString();
338
+ this.database
339
+ .prepare(`INSERT INTO ideas (idea_id, title, content, source_session_id, status, created_at, updated_at)
340
+ VALUES (?, ?, ?, ?, 'raw', ?, ?)`)
341
+ .run(input.ideaId, input.title, input.content, input.sourceSessionId ?? null, now, now);
342
+ }
343
+ listIdeas(status) {
344
+ const rows = (status
345
+ ? this.database
346
+ .prepare("SELECT * FROM ideas WHERE status = ? ORDER BY created_at DESC")
347
+ .all(status)
348
+ : this.database
349
+ .prepare("SELECT * FROM ideas ORDER BY created_at DESC")
350
+ .all());
351
+ return rows.map((row) => ({
352
+ ideaId: row.idea_id,
353
+ title: row.title,
354
+ content: row.content,
355
+ sourceSessionId: row.source_session_id,
356
+ status: row.status,
357
+ target: row.target,
358
+ createdAt: row.created_at,
359
+ updatedAt: row.updated_at,
360
+ }));
361
+ }
362
+ updateIdea(ideaId, status, target) {
363
+ const result = this.database
364
+ .prepare("UPDATE ideas SET status = ?, target = ?, updated_at = ? WHERE idea_id = ?")
365
+ .run(status, target ?? null, new Date().toISOString(), ideaId);
366
+ if (!result.changes)
367
+ throw new Error(`Idea not found: ${ideaId}`);
368
+ }
369
+ deserializeHandoff(row) {
370
+ const json = (key, fallback) => {
371
+ try {
372
+ return JSON.parse(String(row[key] ?? ""));
373
+ }
374
+ catch {
375
+ return fallback;
376
+ }
377
+ };
378
+ return {
379
+ handoffId: row.handoff_id,
380
+ taskId: row.task_id,
381
+ title: row.title,
382
+ objective: row.objective,
383
+ state: row.state,
384
+ profileId: row.profile_id,
385
+ profileIdentity: row.profile_identity,
386
+ sourceSessionId: row.source_session_id,
387
+ projectId: row.project_id ?? null,
388
+ provider: row.provider,
389
+ parentSessionId: row.parent_session_id,
390
+ decisions: json("decisions_json", []),
391
+ changedFiles: json("changed_files_json", []),
392
+ commit: row.commit_ref,
393
+ verification: json("verification_json", []),
394
+ notProven: json("not_proven_json", []),
395
+ blocked: json("blocked_json", []),
396
+ permissions: json("permissions_json", {}),
397
+ contextManifest: json("context_manifest_json", {}),
398
+ nextAction: row.next_action,
399
+ sourceSummaryPath: row.source_summary_path ?? null,
400
+ content: row.content,
401
+ contentBytes: row.content_bytes,
402
+ createdAt: row.created_at,
403
+ updatedAt: row.updated_at,
404
+ };
405
+ }
406
+ list() {
407
+ return this.database
408
+ .prepare("SELECT * FROM sessions ORDER BY created_at DESC")
409
+ .all().map((row) => this.toSession(row));
410
+ }
411
+ listStaleRunning(olderThanMs, now = Date.now()) {
412
+ if (!Number.isFinite(olderThanMs) || olderThanMs <= 0)
413
+ throw new Error("Stale-session threshold must be positive");
414
+ const cutoff = new Date(now - olderThanMs).toISOString();
415
+ return this.database
416
+ .prepare("SELECT * FROM sessions WHERE status = 'running' AND updated_at < ? ORDER BY updated_at")
417
+ .all(cutoff).map((row) => this.toSession(row));
418
+ }
419
+ reconcileStaleRunning(olderThanMs, now = Date.now()) {
420
+ const stale = this.listStaleRunning(olderThanMs, now);
421
+ for (const session of stale) {
422
+ const killed = this.reapProviderProcess(session.sessionId);
423
+ this.updateStatus(session.sessionId, "cancelled");
424
+ this.appendEvent(session.sessionId, "session_reconciled", JSON.stringify({
425
+ reason: "stale-running",
426
+ thresholdMs: olderThanMs,
427
+ reconciledAt: new Date(now).toISOString(),
428
+ providerProcessSignaled: killed,
429
+ }));
430
+ }
431
+ return stale;
432
+ }
433
+ // Best-effort: send SIGTERM to the tracked provider pid, if any. A missing pid (process
434
+ // already exited, or it started before this tracking existed) is not an error. Always
435
+ // clears the tracked pid afterward so a reused pid can never be signaled twice.
436
+ reapProviderProcess(sessionId) {
437
+ const pid = this.getProviderPid(sessionId);
438
+ let signaled = false;
439
+ if (pid !== null) {
440
+ try {
441
+ process.kill(pid, "SIGTERM");
442
+ signaled = true;
443
+ }
444
+ catch {
445
+ /* process already gone */
446
+ }
447
+ this.clearProviderPid(sessionId);
448
+ }
449
+ return signaled;
450
+ }
451
+ integrityCheck() {
452
+ const integrity = this.database.prepare("PRAGMA integrity_check").get().integrity_check;
453
+ const foreignKeys = this.database.prepare("PRAGMA foreign_key_check").all();
454
+ return { integrity, foreignKeys };
455
+ }
456
+ appendEvent(sessionId, type, data) {
457
+ const createdAt = new Date().toISOString();
458
+ const result = this.database
459
+ .prepare("INSERT INTO session_events (session_id, type, data, created_at) VALUES (?, ?, ?, ?)")
460
+ .run(sessionId, type, data, createdAt);
461
+ return {
462
+ eventId: Number(result.lastInsertRowid),
463
+ sessionId,
464
+ type,
465
+ data,
466
+ createdAt,
467
+ };
468
+ }
469
+ listEvents(sessionId) {
470
+ return this.database
471
+ .prepare("SELECT event_id, session_id, type, data, created_at FROM session_events WHERE session_id = ? ORDER BY event_id")
472
+ .all(sessionId)
473
+ .map((row) => {
474
+ const event = row;
475
+ return {
476
+ eventId: event.event_id,
477
+ sessionId: event.session_id,
478
+ type: event.type,
479
+ data: event.data,
480
+ createdAt: event.created_at,
481
+ };
482
+ });
483
+ }
484
+ scanCaptureItems(sessionId) {
485
+ const query = sessionId
486
+ ? `SELECT event_id FROM session_events WHERE session_id = ? AND type = 'user_input'
487
+ AND event_id NOT IN (SELECT source_event_id FROM capture_items) ORDER BY event_id`
488
+ : `SELECT event_id FROM session_events WHERE type = 'user_input'
489
+ AND event_id NOT IN (SELECT source_event_id FROM capture_items) ORDER BY event_id`;
490
+ const rows = (sessionId
491
+ ? this.database.prepare(query).all(sessionId)
492
+ : this.database.prepare(query).all());
493
+ const insert = this.database.prepare("INSERT INTO capture_items (source_event_id, created_at) VALUES (?, ?)");
494
+ const now = new Date().toISOString();
495
+ for (const row of rows)
496
+ insert.run(row.event_id, now);
497
+ return rows.length;
498
+ }
499
+ listCaptureItems(status = "new") {
500
+ const rows = this.database
501
+ .prepare(`
502
+ SELECT c.capture_id, c.source_event_id, e.session_id, e.data, c.type, c.status, c.target, c.created_at
503
+ FROM capture_items c JOIN session_events e ON e.event_id = c.source_event_id
504
+ WHERE c.status = ? ORDER BY c.capture_id
505
+ `)
506
+ .all(status);
507
+ return rows.map((row) => ({
508
+ captureId: row.capture_id,
509
+ sourceEventId: row.source_event_id,
510
+ sessionId: row.session_id,
511
+ content: row.data,
512
+ type: row.type,
513
+ status: row.status,
514
+ target: row.target,
515
+ createdAt: row.created_at,
516
+ }));
517
+ }
518
+ getCaptureItem(captureId) {
519
+ return (this.listCaptureItems("new").find((item) => item.captureId === captureId) ?? null);
520
+ }
521
+ updateCaptureStatus(captureId, status, target) {
522
+ const result = this.database
523
+ .prepare("UPDATE capture_items SET status = ?, target = ?, reviewed_at = ? WHERE capture_id = ?")
524
+ .run(status, target ?? null, new Date().toISOString(), captureId);
525
+ if (!result.changes)
526
+ throw new Error(`Capture item not found: ${captureId}`);
527
+ }
528
+ // --- retention (T-228) --------------------------------------------------------------
529
+ // Every method below is read-only or scoped to raw provider_output/terminal_input events
530
+ // of already-finished sessions; nothing here deletes a session row, a handoff, an idea, or
531
+ // a capture-referenced event. session-retention.ts (application layer) is the only caller,
532
+ // and it never applies anything against the live database without an explicit
533
+ // fingerprint-gated --apply.
534
+ static RETENTION_STATUSES = [
535
+ "completed",
536
+ "failed",
537
+ "cancelled",
538
+ ];
539
+ static RAW_EVENT_TYPES = [
540
+ "provider_output",
541
+ "terminal_input",
542
+ ];
543
+ // Sessions eligible for retention: finished (completed/failed/cancelled) and closed (or
544
+ // last updated) before the cutoff.
545
+ retentionEligibleSessions(beforeIso) {
546
+ const placeholders = SessionStore.RETENTION_STATUSES.map(() => "?").join(",");
547
+ return this.database
548
+ .prepare(`SELECT * FROM sessions WHERE status IN (${placeholders})
549
+ AND COALESCE(closed_at, updated_at) < ?
550
+ ORDER BY session_id`)
551
+ .all(...SessionStore.RETENTION_STATUSES, beforeIso).map((row) => this.toSession(row));
552
+ }
553
+ // SQLite refuses a prepared statement with more than ~thousands of bound parameters
554
+ // (SQLITE_MAX_VARIABLE_NUMBER) — a real workspace can have tens of thousands of prunable
555
+ // events, so every IN (...) query here is chunked well under that ceiling rather than
556
+ // built as one unbounded placeholder list.
557
+ static SQL_CHUNK_SIZE = 500;
558
+ static chunk(items, size = SessionStore.SQL_CHUNK_SIZE) {
559
+ const chunks = [];
560
+ for (let start = 0; start < items.length; start += size)
561
+ chunks.push(items.slice(start, start + size));
562
+ return chunks;
563
+ }
564
+ // Raw event ids for the given sessions that are safe to delete: provider_output/
565
+ // terminal_input only, and never one referenced by capture_items.source_event_id (the
566
+ // ON DELETE CASCADE hazard) — those survive retention regardless of age.
567
+ retentionPrunableEventIds(sessionIds) {
568
+ if (sessionIds.length === 0)
569
+ return [];
570
+ const typePlaceholders = SessionStore.RAW_EVENT_TYPES.map(() => "?").join(",");
571
+ const eventIds = [];
572
+ for (const batch of SessionStore.chunk(sessionIds)) {
573
+ const sessionPlaceholders = batch.map(() => "?").join(",");
574
+ const rows = this.database
575
+ .prepare(`SELECT event_id FROM session_events
576
+ WHERE session_id IN (${sessionPlaceholders})
577
+ AND type IN (${typePlaceholders})
578
+ AND event_id NOT IN (SELECT source_event_id FROM capture_items)`)
579
+ .all(...batch, ...SessionStore.RAW_EVENT_TYPES);
580
+ for (const row of rows)
581
+ eventIds.push(row.event_id);
582
+ }
583
+ return eventIds;
584
+ }
585
+ // Total bytes of the `data` column across the given event ids — a before/after size
586
+ // estimate without needing a VACUUMed copy.
587
+ eventsDataBytes(eventIds) {
588
+ if (eventIds.length === 0)
589
+ return 0;
590
+ let total = 0;
591
+ for (const batch of SessionStore.chunk(eventIds)) {
592
+ const placeholders = batch.map(() => "?").join(",");
593
+ const row = this.database
594
+ .prepare(`SELECT COALESCE(SUM(LENGTH(data)), 0) AS bytes FROM session_events WHERE event_id IN (${placeholders})`)
595
+ .get(...batch);
596
+ total += row.bytes;
597
+ }
598
+ return total;
599
+ }
600
+ // Only ever touches summary_path/summary_hash/summary_bytes — never closeout_status or
601
+ // closed_at, which retention must not alter.
602
+ setSummary(sessionId, summary) {
603
+ this.database
604
+ .prepare("UPDATE sessions SET summary_path = ?, summary_hash = ?, summary_bytes = ? WHERE session_id = ?")
605
+ .run(summary.summaryPath, summary.summaryHash, summary.summaryBytes, sessionId);
606
+ }
607
+ // Deletes exactly the given session_events rows, in one transaction. Callers are
608
+ // responsible for having excluded capture-referenced ids (retentionPrunableEventIds does
609
+ // this) — this method itself does not re-check, so it must never be called with an
610
+ // arbitrary, unfiltered id list.
611
+ deleteEvents(eventIds) {
612
+ if (eventIds.length === 0)
613
+ return;
614
+ this.database.exec("BEGIN");
615
+ try {
616
+ for (const batch of SessionStore.chunk(eventIds)) {
617
+ const placeholders = batch.map(() => "?").join(",");
618
+ this.database
619
+ .prepare(`DELETE FROM session_events WHERE event_id IN (${placeholders})`)
620
+ .run(...batch);
621
+ }
622
+ this.database.exec("COMMIT");
623
+ }
624
+ catch (error) {
625
+ this.database.exec("ROLLBACK");
626
+ throw error;
627
+ }
628
+ }
629
+ // Appends a small audit event per session so a future reader can see retention ran,
630
+ // without needing to diff row counts.
631
+ recordRetentionPrune(sessionId, prunedEventCount, beforeIso) {
632
+ this.appendEvent(sessionId, "events_pruned", JSON.stringify({
633
+ prunedEventCount,
634
+ before: beforeIso,
635
+ prunedAt: new Date().toISOString(),
636
+ }));
637
+ }
638
+ checkpointAndVacuum() {
639
+ this.database.exec("PRAGMA wal_checkpoint(TRUNCATE);");
640
+ this.database.exec("VACUUM;");
641
+ }
642
+ // Delegates to node:sqlite's own backup() (a live, consistent copy), for applyRetention's
643
+ // pre-delete safety backup. Never used against a destination inside the same directory as
644
+ // sessions.sqlite by the caller's own convention (see session-retention.ts).
645
+ async backupTo(file) {
646
+ await backup(this.database, file);
647
+ }
648
+ close() {
649
+ this.database.close();
650
+ }
651
+ toSession(row) {
652
+ return validateSession({
653
+ sessionId: row.session_id,
654
+ title: row.title,
655
+ taskId: row.task_id,
656
+ handoffId: row.handoff_id,
657
+ provider: row.provider,
658
+ providerSessionId: row.provider_session_id,
659
+ parentSessionId: row.parent_session_id,
660
+ profile: row.profile,
661
+ profileIdentity: row.profile_identity,
662
+ workingDirectory: row.working_directory,
663
+ status: row.status,
664
+ createdAt: row.created_at,
665
+ updatedAt: row.updated_at,
666
+ resumeData: row.resume_data,
667
+ contextHash: row.context_hash,
668
+ contextBytes: row.context_bytes,
669
+ nextAction: row.next_action,
670
+ verificationStatus: row.verification_status,
671
+ summaryPath: row.summary_path,
672
+ summaryHash: row.summary_hash,
673
+ summaryBytes: row.summary_bytes,
674
+ closeoutStatus: row.closeout_status,
675
+ closeoutVersion: row.closeout_version,
676
+ closedAt: row.closed_at,
677
+ projectId: row.project_id ?? null,
678
+ });
679
+ }
680
+ }
681
+ export async function openSessionStore() {
682
+ await mkdir(oceanPath(SYSTEM_DIR, "sessions"), { recursive: true });
683
+ return new SessionStore();
684
+ }
685
+ export async function openSessionStoreReadOnly() {
686
+ const databaseFile = oceanPath(SYSTEM_DIR, "sessions", "sessions.sqlite");
687
+ await access(databaseFile);
688
+ return new SessionStore(databaseFile, { readOnly: true });
689
+ }
690
+ //# sourceMappingURL=session-store.js.map