@amoree-code/ocean 0.0.0-stage → 1.1.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.
- package/LICENSE +21 -0
- package/README.md +284 -2
- package/dist/application/auth/auth-orchestrator.js +77 -0
- package/dist/application/clients/clients.js +646 -0
- package/dist/application/clients/editors.js +398 -0
- package/dist/application/clients/hooks.js +335 -0
- package/dist/application/clients/native.js +410 -0
- package/dist/application/clients/opencode-plugin.js +158 -0
- package/dist/application/clients/registry.js +188 -0
- package/dist/application/gateway/webhook-gateway.js +137 -0
- package/dist/application/install/provider-installer.js +184 -0
- package/dist/application/integrations/charter-copies.js +76 -0
- package/dist/application/integrations/claude-vscode.js +106 -0
- package/dist/application/integrations/engine-entry-repair.js +105 -0
- package/dist/application/integrations/skill-filter.js +254 -0
- package/dist/application/layout/layout-apply.js +443 -0
- package/dist/application/layout/layout-plan.js +236 -0
- package/dist/application/layout/workspace-template.js +34 -0
- package/dist/application/onboarding/state.js +23 -0
- package/dist/application/onboarding/steps.js +249 -0
- package/dist/application/profiles/profile-distribution.js +53 -0
- package/dist/application/profiles/profile-suggestion.js +39 -0
- package/dist/application/profiles/profile-upgrade-service.js +208 -0
- package/dist/application/pull/history-index.js +476 -0
- package/dist/application/pull/project-match.js +132 -0
- package/dist/application/pull/pull.js +202 -0
- package/dist/application/pull/server-capture.js +307 -0
- package/dist/application/scheduler/local-scheduler.js +210 -0
- package/dist/composition/runtime.js +64 -0
- package/dist/engine-root.js +9 -0
- package/dist/infrastructure/filesystem/profile-loader.js +46 -0
- package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
- package/dist/infrastructure/filesystem/skill-loader.js +84 -0
- package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
- package/dist/infrastructure/observability/runtime-logger.js +16 -0
- package/dist/infrastructure/process/agent-tool-executor.js +99 -0
- package/dist/infrastructure/process/cli-process.js +49 -0
- package/dist/infrastructure/process/interactive-process.js +145 -0
- package/dist/infrastructure/process/process-supervisor.js +194 -0
- package/dist/infrastructure/process/service.js +252 -0
- package/dist/infrastructure/providers/browser-provider.js +2 -0
- package/dist/infrastructure/providers/claude-sdk.js +137 -0
- package/dist/infrastructure/providers/client-home.js +14 -0
- package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
- package/dist/infrastructure/providers/ollama-embedder.js +74 -0
- package/dist/infrastructure/providers/openai-compatible.js +268 -0
- package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
- package/dist/infrastructure/providers/provider-capabilities.js +50 -0
- package/dist/infrastructure/providers/provider-invocation.js +184 -0
- package/dist/infrastructure/providers/provider-registry.js +77 -0
- package/dist/infrastructure/providers/providers.js +134 -0
- package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
- package/dist/interfaces/cli/auth-command.js +54 -0
- package/dist/interfaces/cli/browser-bridge-command.js +93 -0
- package/dist/interfaces/cli/browser-command.js +119 -0
- package/dist/interfaces/cli/capture-command.js +125 -0
- package/dist/interfaces/cli/client-test-command.js +133 -0
- package/dist/interfaces/cli/clients-command.js +129 -0
- package/dist/interfaces/cli/command-registry.js +1211 -0
- package/dist/interfaces/cli/context-command.js +172 -0
- package/dist/interfaces/cli/daily-command.js +131 -0
- package/dist/interfaces/cli/governance-command.js +55 -0
- package/dist/interfaces/cli/handoff-command.js +35 -0
- package/dist/interfaces/cli/help-command.js +207 -0
- package/dist/interfaces/cli/idea-command.js +47 -0
- package/dist/interfaces/cli/intercept-command.js +317 -0
- package/dist/interfaces/cli/layout-command.js +27 -0
- package/dist/interfaces/cli/market-command.js +52 -0
- package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
- package/dist/interfaces/cli/memory-command.js +317 -0
- package/dist/interfaces/cli/migrate-command.js +26 -0
- package/dist/interfaces/cli/observe-command.js +78 -0
- package/dist/interfaces/cli/onboard-command.js +123 -0
- package/dist/interfaces/cli/operate-command.js +59 -0
- package/dist/interfaces/cli/profiles-command.js +70 -0
- package/dist/interfaces/cli/pull-command.js +54 -0
- package/dist/interfaces/cli/setup-command.js +143 -0
- package/dist/interfaces/cli/tasks-command.js +249 -0
- package/dist/main.js +49 -0
- package/dist/node-warnings.js +17 -0
- package/dist/version.js +8 -0
- package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
- package/market/mcp/gateway/ocean.capability.json +21 -0
- package/market/mcp/obsidian/ocean.capability.json +21 -0
- package/market/mcp/ocean/ocean.capability.json +21 -0
- package/market/mcp/playwright/ocean.capability.json +23 -0
- package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
- package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
- package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
- package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
- package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
- package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
- package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
- package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
- package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
- package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
- package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
- package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
- package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
- package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
- package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
- package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
- package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
- package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
- package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
- package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
- package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
- package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
- package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
- package/node_modules/@ocean/brain/dist/memory/signal-detection.js +66 -0
- package/node_modules/@ocean/brain/package.json +11 -0
- package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
- package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
- package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
- package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
- package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
- package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
- package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
- package/node_modules/@ocean/context/package.json +11 -0
- package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
- package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
- package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
- package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
- package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
- package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
- package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
- package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
- package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
- package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
- package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
- package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
- package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
- package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
- package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
- package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
- package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
- package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
- package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
- package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
- package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
- package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
- package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
- package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
- package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
- package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
- package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
- package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
- package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
- package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
- package/node_modules/@ocean/core/package.json +14 -0
- package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
- package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
- package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
- package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
- package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
- package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
- package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
- package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
- package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
- package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
- package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
- package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
- package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
- package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
- package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
- package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
- package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
- package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
- package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
- package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
- package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
- package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
- package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
- package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
- package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
- package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
- package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
- package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
- package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
- package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
- package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
- package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
- package/node_modules/@ocean/loop/package.json +11 -0
- package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
- package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
- package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
- package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
- package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
- package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
- package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
- package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
- package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
- package/node_modules/@ocean/market/dist/install/lock.js +34 -0
- package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
- package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
- package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
- package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
- package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
- package/node_modules/@ocean/market/dist/security/sandbox.js +74 -0
- package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
- package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
- package/node_modules/@ocean/market/dist/skills/skill-curation.js +123 -0
- package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
- package/node_modules/@ocean/market/dist/skills/task-observer.js +165 -0
- package/node_modules/@ocean/market/package.json +11 -0
- package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
- package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
- package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
- package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
- package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
- package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
- package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
- package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
- package/node_modules/@ocean/services/package.json +11 -0
- package/ocean-marketplace.json +415 -0
- package/package.json +67 -4
- package/skills/core/business-logic/SKILL.md +22 -0
- package/skills/core/catch-up/SKILL.md +17 -0
- package/skills/core/core-thinking/SKILL.md +11 -0
- package/skills/core/design-thinking/SKILL.md +19 -0
- package/skills/core/graft/SKILL.md +156 -0
- package/skills/core/loop/SKILL.md +20 -0
- package/skills/core/session-handoff/SKILL.md +15 -0
- package/skills/core/use-browser/SKILL.md +69 -0
- package/skills/core/verification/SKILL.md +11 -0
- package/skills/index.json +56 -0
- package/templates/README.md +4 -0
- package/templates/profiles/default.json +51 -0
- package/templates/profiles/developer.json +51 -0
- package/templates/profiles/reviewer.json +51 -0
- package/templates/profiles/strategist.json +51 -0
- package/templates/workspace/00-inbox/INBOX.md +7 -0
- package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
- package/templates/workspace/00-inbox/native/.gitkeep +0 -0
- package/templates/workspace/01-daily/.gitkeep +0 -0
- package/templates/workspace/02-personal/CONFLICTS.md +7 -0
- package/templates/workspace/02-personal/MEMORY.md +15 -0
- package/templates/workspace/02-personal/goals/.gitkeep +0 -0
- package/templates/workspace/02-personal/identity/.gitkeep +0 -0
- package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
- package/templates/workspace/03-professional/.gitkeep +0 -0
- package/templates/workspace/04-projects/PROJECTS.md +5 -0
- package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
- package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
- package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
- package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
- package/templates/workspace/06-templates/TEMPLATES.md +25 -0
- package/templates/workspace/06-templates/decision.md +8 -0
- package/templates/workspace/06-templates/intelligence.md +32 -0
- package/templates/workspace/06-templates/project-claude.md +48 -0
- package/templates/workspace/06-templates/session.md +38 -0
- package/templates/workspace/06-templates/task.md +42 -0
- package/templates/workspace/MANIFEST.json +51 -0
- package/templates/workspace/bridge/config/CONFIG.md +21 -0
- package/templates/workspace/bridge/config/runtime.json +4 -0
- package/templates/workspace/charter/core.md +104 -0
- package/templates/workspace/charter/policies/README.md +31 -0
- package/templates/workspace/charter/policies/claude-code-context.md +90 -0
- package/templates/workspace/charter/policies/concise-output.md +29 -0
- package/templates/workspace/charter/policies/context.md +91 -0
- package/templates/workspace/charter/policies/contracts/task.md +189 -0
- package/templates/workspace/charter/policies/git.md +60 -0
- package/templates/workspace/charter/policies/graph.md +47 -0
- package/templates/workspace/charter/policies/knowledge.md +33 -0
- package/templates/workspace/charter/policies/lifecycle.md +119 -0
- package/templates/workspace/charter/policies/memory.md +67 -0
- package/templates/workspace/charter/policies/minimal-change.md +43 -0
- package/templates/workspace/charter/policies/models.md +57 -0
- package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
- package/templates/workspace/charter/policies/response.md +39 -0
- package/templates/workspace/charter/policies/skill-observer.md +44 -0
- package/templates/workspace/charter/policies/stack.md +23 -0
- package/templates/workspace/charter/policies/task.md +139 -0
- package/templates/workspace/charter/policies/verification.md +29 -0
- 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
|