@selesai/code 0.13.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/core/agent-session.d.ts +1 -1
- package/dist/core/agent-session.js +1 -1
- package/dist/core/sdk.d.ts +2 -2
- package/dist/core/sdk.js +1 -1
- package/dist/core/system-prompt.d.ts +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/extensions/ascii.txt +9 -0
- package/dist/extensions/package.json +1 -2
- package/dist/extensions/pi-hermes-memory/CHANGELOG.md +371 -0
- package/dist/extensions/pi-hermes-memory/LICENSE +21 -0
- package/dist/extensions/pi-hermes-memory/README.md +643 -0
- package/dist/extensions/pi-hermes-memory/docs/0.1/TASKS.md +197 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/PLAN.md +290 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TASKS.md +134 -0
- package/dist/extensions/pi-hermes-memory/docs/0.2/TEST-PLAN.md +216 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/PLAN.md +330 -0
- package/dist/extensions/pi-hermes-memory/docs/0.3/TASKS.md +125 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/PLAN.md +160 -0
- package/dist/extensions/pi-hermes-memory/docs/0.4/TASKS.md +113 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/CHANGELOG.md +57 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/PLAN.md +202 -0
- package/dist/extensions/pi-hermes-memory/docs/0.6/TASKS.md +144 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/PLAN.md +349 -0
- package/dist/extensions/pi-hermes-memory/docs/0.7/TASKS.md +110 -0
- package/dist/extensions/pi-hermes-memory/docs/PUBLISHING.md +149 -0
- package/dist/extensions/pi-hermes-memory/docs/ROADMAP.md +468 -0
- package/dist/extensions/pi-hermes-memory/docs/images/memory-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi-logo.svg +22 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.png +0 -0
- package/dist/extensions/pi-hermes-memory/docs/images/pi_memory.svg +19 -0
- package/dist/extensions/pi-hermes-memory/docs/images/security-flow.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/session-lifecycle.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/images/source-architecture.svg +1 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/memory-architecture.mmd +49 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/security-flow.mmd +40 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/session-lifecycle.mmd +56 -0
- package/dist/extensions/pi-hermes-memory/docs/mermaid/source-architecture.mmd +54 -0
- package/dist/extensions/pi-hermes-memory/package-lock.json +3799 -0
- package/dist/extensions/pi-hermes-memory/package.json +66 -0
- package/dist/extensions/pi-hermes-memory/scripts/check-min-sdk.mjs +99 -0
- package/dist/extensions/pi-hermes-memory/scripts/ensure-dev.mjs +57 -0
- package/dist/extensions/pi-hermes-memory/src/auto-consolidation-warning.ts +6 -0
- package/dist/extensions/pi-hermes-memory/src/config.ts +178 -0
- package/dist/extensions/pi-hermes-memory/src/constants.ts +424 -0
- package/dist/extensions/pi-hermes-memory/src/extension-root-migration.ts +639 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/auto-consolidate.ts +401 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/background-review.ts +297 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/child-process-watchdog.mjs +90 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/correction-detector.ts +300 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/index-sessions.ts +86 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/insights.ts +78 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/interview.ts +37 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/learn-memory.ts +186 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/message-parts.ts +27 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/pi-child-process.ts +485 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/preview-context.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/review-memory-ops.ts +506 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-backfill.ts +145 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-flush.ts +151 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/session-live-index.ts +94 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/skills-command.ts +1334 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/standing-pin.ts +111 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/switch-project.ts +75 -0
- package/dist/extensions/pi-hermes-memory/src/handlers/sync-markdown-memories.ts +266 -0
- package/dist/extensions/pi-hermes-memory/src/index.ts +378 -0
- package/dist/extensions/pi-hermes-memory/src/lifecycle-timing.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/paths.ts +61 -0
- package/dist/extensions/pi-hermes-memory/src/project-context.ts +13 -0
- package/dist/extensions/pi-hermes-memory/src/project-memory-migration.ts +93 -0
- package/dist/extensions/pi-hermes-memory/src/project.ts +157 -0
- package/dist/extensions/pi-hermes-memory/src/prompt-context.ts +55 -0
- package/dist/extensions/pi-hermes-memory/src/store/atomic-lock-coordinator.ts +375 -0
- package/dist/extensions/pi-hermes-memory/src/store/canonical-storage-path.ts +54 -0
- package/dist/extensions/pi-hermes-memory/src/store/content-scanner.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/db.ts +1180 -0
- package/dist/extensions/pi-hermes-memory/src/store/fts-query.ts +101 -0
- package/dist/extensions/pi-hermes-memory/src/store/markdown-mutation-lock.ts +38 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-lookup.ts +24 -0
- package/dist/extensions/pi-hermes-memory/src/store/memory-store.ts +1191 -0
- package/dist/extensions/pi-hermes-memory/src/store/recovery-maintenance.ts +66 -0
- package/dist/extensions/pi-hermes-memory/src/store/schema.ts +118 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-anchor-search.ts +472 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-indexer.ts +511 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-parser.ts +213 -0
- package/dist/extensions/pi-hermes-memory/src/store/session-search.ts +234 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-store.ts +950 -0
- package/dist/extensions/pi-hermes-memory/src/store/skill-utils.ts +133 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-memory-store.ts +1002 -0
- package/dist/extensions/pi-hermes-memory/src/store/sqlite-native.ts +241 -0
- package/dist/extensions/pi-hermes-memory/src/store/standing-instructions.ts +247 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-search-tool.ts +95 -0
- package/dist/extensions/pi-hermes-memory/src/tools/memory-tool.ts +469 -0
- package/dist/extensions/pi-hermes-memory/src/tools/session-search-tool.ts +246 -0
- package/dist/extensions/pi-hermes-memory/src/tools/shared-output-view.ts +170 -0
- package/dist/extensions/pi-hermes-memory/src/tools/skill-tool.ts +333 -0
- package/dist/extensions/pi-hermes-memory/src/tools/tool-result-views.ts +105 -0
- package/dist/extensions/pi-hermes-memory/src/types.ts +214 -0
- package/dist/extensions/pi-hermes-memory/tests/config.test.ts +434 -0
- package/dist/extensions/pi-hermes-memory/tests/extension-root-migration.test.ts +368 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/auto-consolidate.test.ts +984 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/background-review.test.ts +950 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/correction-detector.test.ts +671 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/insights.test.ts +157 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/interview.test.ts +127 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/pi-child-process.test.ts +905 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/preview-context.test.ts +152 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/prompt-context.test.ts +166 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/resources-discover.test.ts +71 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/review-memory-ops.test.ts +614 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-backfill.test.ts +192 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-flush.test.ts +677 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/session-live-index.test.ts +225 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/skills-command.test.ts +827 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/standing-pin.test.ts +134 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/sync-markdown-memories.test.ts +491 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/system-prompt.test.ts +199 -0
- package/dist/extensions/pi-hermes-memory/tests/handlers/thinking-max.test.ts +64 -0
- package/dist/extensions/pi-hermes-memory/tests/index.test.ts +30 -0
- package/dist/extensions/pi-hermes-memory/tests/integration/flow.test.ts +126 -0
- package/dist/extensions/pi-hermes-memory/tests/lifecycle-timing.test.ts +76 -0
- package/dist/extensions/pi-hermes-memory/tests/paths.test.ts +23 -0
- package/dist/extensions/pi-hermes-memory/tests/project-memory-migration.test.ts +80 -0
- package/dist/extensions/pi-hermes-memory/tests/project-rebinding.test.ts +115 -0
- package/dist/extensions/pi-hermes-memory/tests/project.test.ts +120 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all-timeout.test.ts +107 -0
- package/dist/extensions/pi-hermes-memory/tests/run-all.sh +45 -0
- package/dist/extensions/pi-hermes-memory/tests/store/atomic-lock-coordinator.test.ts +381 -0
- package/dist/extensions/pi-hermes-memory/tests/store/content-scanner.test.ts +388 -0
- package/dist/extensions/pi-hermes-memory/tests/store/db.test.ts +1046 -0
- package/dist/extensions/pi-hermes-memory/tests/store/markdown-mutation-lock.test.ts +34 -0
- package/dist/extensions/pi-hermes-memory/tests/store/memory-store.test.ts +2150 -0
- package/dist/extensions/pi-hermes-memory/tests/store/recovery-maintenance.test.ts +170 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-anchor-search.test.ts +210 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-indexer.test.ts +643 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-parser.test.ts +282 -0
- package/dist/extensions/pi-hermes-memory/tests/store/session-search.test.ts +287 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-store.test.ts +678 -0
- package/dist/extensions/pi-hermes-memory/tests/store/skill-utils.test.ts +21 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-lazy-load.test.ts +78 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-memory-store.test.ts +543 -0
- package/dist/extensions/pi-hermes-memory/tests/store/sqlite-native.test.ts +164 -0
- package/dist/extensions/pi-hermes-memory/tests/store/standing-instructions.test.ts +171 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-search-tool.test.ts +84 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/memory-tool.test.ts +709 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/session-search-tool.test.ts +345 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/shared-output-view.test.ts +362 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/skill-tool.test.ts +478 -0
- package/dist/extensions/pi-hermes-memory/tests/tools/tool-result-renderer-wiring.test.ts +40 -0
- package/dist/extensions/pi-hermes-memory/tsconfig.json +19 -0
- package/dist/extensions/pi-subagents/.oxlintrc.json +43 -0
- package/dist/extensions/pi-subagents/AGENTS.md +5 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +157 -0
- package/dist/extensions/pi-subagents/README.md +12 -6
- package/dist/extensions/pi-subagents/VISION.md +81 -0
- package/dist/extensions/pi-subagents/agents/claude-code-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/claude-code.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec-writer.md +15 -0
- package/dist/extensions/pi-subagents/agents/codex-exec.md +15 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent-writer.md +14 -0
- package/dist/extensions/pi-subagents/agents/cursor-agent.md +14 -0
- package/dist/extensions/pi-subagents/agents/reviewer.md +1 -3
- package/dist/extensions/pi-subagents/docs/agents.md +488 -0
- package/dist/extensions/pi-subagents/docs/configuration.md +46 -9
- package/dist/extensions/pi-subagents/docs/extension-api.md +42 -3
- package/dist/extensions/pi-subagents/docs/models.md +27 -4
- package/dist/extensions/pi-subagents/docs/observability.md +10 -9
- package/dist/extensions/pi-subagents/docs/tool-reference.md +102 -6
- package/dist/extensions/pi-subagents/docs/workflows.md +135 -0
- package/dist/extensions/pi-subagents/package-lock.json +389 -2
- package/dist/extensions/pi-subagents/package.json +3 -1
- package/dist/extensions/pi-subagents/prompts/review-loop.md +2 -2
- package/dist/extensions/pi-subagents/skills/council-mode/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +3 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +7 -3
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +26 -5
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -22
- package/dist/extensions/pi-subagents/src/agents/agent-refinements.ts +4 -4
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +9 -2
- package/dist/extensions/pi-subagents/src/agents/agents.ts +322 -75
- package/dist/extensions/pi-subagents/src/agents/builtin-names.ts +6 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-events.ts +70 -0
- package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +27 -20
- package/dist/extensions/pi-subagents/src/api/agents.ts +10 -5
- package/dist/extensions/pi-subagents/src/api/background-work.ts +5 -1
- package/dist/extensions/pi-subagents/src/api/delegation.ts +0 -7
- package/dist/extensions/pi-subagents/src/api/preflight.ts +34 -12
- package/dist/extensions/pi-subagents/src/extension/config.ts +70 -0
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +5 -3
- package/dist/extensions/pi-subagents/src/extension/index.ts +83 -30
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +44 -15
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +55 -19
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +41 -15
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -31
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +2 -1
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +2 -10
- package/dist/extensions/pi-subagents/src/inspectors/herdr/session-roots-codec.ts +42 -0
- package/dist/extensions/pi-subagents/src/integrations/herdr-status.ts +51 -3
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +2 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +5 -6
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +77 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +176 -53
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +19 -12
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +29 -27
- package/dist/extensions/pi-subagents/src/runs/background/async-retention.ts +20 -3
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +23 -261
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +73 -7
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +74 -8
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +37 -21
- package/dist/extensions/pi-subagents/src/runs/background/inspect-rpc.ts +8 -8
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +184 -9
- package/dist/extensions/pi-subagents/src/runs/background/result-delivery-ownership.ts +45 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +28 -14
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +38 -15
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +50 -13
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +93 -8
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +10 -4
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -14
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +549 -377
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +58 -10
- package/dist/extensions/pi-subagents/src/runs/background/terminal-run-index.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +26 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +23 -9
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +9 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +204 -129
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +9 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-history.ts +23 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +773 -311
- package/dist/extensions/pi-subagents/src/runs/foreground/workflow-detach-reconcile.ts +140 -115
- package/dist/extensions/pi-subagents/src/runs/shared/abort-recovery.ts +119 -0
- package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +463 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-identity.ts +19 -4
- package/dist/extensions/pi-subagents/src/runs/shared/child-launch-plan.ts +151 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +21 -7
- package/dist/extensions/pi-subagents/src/runs/shared/claude-code-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/codex-exec-adapter.ts +129 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-evidence.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +5 -4
- package/dist/extensions/pi-subagents/src/runs/shared/cursor-agent-adapter.ts +114 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +3 -3
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-contract.ts +167 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-preflight.ts +122 -0
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +348 -55
- package/dist/extensions/pi-subagents/src/runs/shared/fast-mode-extension.ts +5 -5
- package/dist/extensions/pi-subagents/src/runs/shared/host-step-status.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +105 -0
- package/dist/extensions/pi-subagents/src/runs/shared/launch-cwd.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-config-sources.ts +422 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/dist/extensions/pi-subagents/src/runs/shared/model-exclusions.ts +69 -7
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +47 -6
- package/dist/extensions/pi-subagents/src/runs/shared/mutation-evidence.ts +7 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +9 -3
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +9 -5
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +419 -7
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +15 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +81 -8
- package/dist/extensions/pi-subagents/src/runs/shared/process-signal.ts +13 -0
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +21 -1
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +44 -4
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +98 -13
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +18 -0
- package/dist/extensions/pi-subagents/src/shared/child-session-name.ts +46 -0
- package/dist/extensions/pi-subagents/src/shared/extension-context.ts +24 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/formatters.ts +18 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +5 -1
- package/dist/extensions/pi-subagents/src/shared/pruned-fork.ts +450 -0
- package/dist/extensions/pi-subagents/src/shared/session-file-trust.ts +19 -0
- package/dist/extensions/pi-subagents/src/shared/session-tokens.ts +14 -3
- package/dist/extensions/pi-subagents/src/shared/settings.ts +19 -105
- package/dist/extensions/pi-subagents/src/shared/shortcuts.ts +17 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +359 -40
- package/dist/extensions/pi-subagents/src/shared/utils.ts +41 -84
- package/dist/extensions/pi-subagents/src/shared/workflow-child-permit.ts +116 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +1 -9
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +0 -4
- package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -2
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +378 -88
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +22 -11
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +3 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +147 -79
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +11 -5
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +39 -18
- package/dist/extensions/pi-subagents/src/tui/render.ts +369 -46
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +1 -1
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +6 -3
- package/dist/extensions/pi-subagents/src/workflows/host-command.ts +230 -0
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +740 -58
- package/dist/extensions/pi-subagents/src/workflows/workflow-child-summary.ts +121 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +270 -0
- package/dist/extensions/pi-subagents/src/workflows/workflow-receipt.ts +195 -6
- package/dist/extensions/pi-subagents/src/workflows/workflow-settlement.ts +246 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1114 -260
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +150 -3
- package/dist/extensions/pi-subagents/test/integration/claude-code-smoke.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/integration/claude-code-writer-smoke.test.ts +55 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-smoke.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/integration/codex-exec-writer-smoke.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-smoke.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/integration/cursor-agent-writer-smoke.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +57 -1
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +4 -3
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +54 -4
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +482 -4
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +157 -9
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +5 -5
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1508 -291
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +423 -1
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/support/cursor-smoke-inputs.ts +51 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +2 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +5 -5
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +6 -6
- package/dist/extensions/pi-subagents/test/unit/abort-recovery.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +48 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +184 -6
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +66 -6
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/agent-refinements.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/anti-slop-oxlint.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +101 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/async-recovery-descriptor.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +14 -7
- package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +190 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +42 -0
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +8 -4
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +78 -1
- package/dist/extensions/pi-subagents/test/unit/child-launch-plan.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/child-session-name.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/claude-code-adapter.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/codex-exec-adapter.test.ts +192 -0
- package/dist/extensions/pi-subagents/test/unit/compaction-resume.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/completion-evidence.test.ts +150 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-agent-adapter.test.ts +259 -0
- package/dist/extensions/pi-subagents/test/unit/cursor-smoke-inputs.test.ts +77 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +1 -4
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/extension-context.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/external-cli-runner.test.ts +223 -0
- package/dist/extensions/pi-subagents/test/unit/fast-mode-extension.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +208 -2
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +29 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +110 -1
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +10 -0
- package/dist/extensions/pi-subagents/test/unit/formatters.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +18 -9
- package/dist/extensions/pi-subagents/test/unit/herdr-session-roots-codec.test.ts +63 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/herdr-status-bridge.test.ts +132 -0
- package/dist/extensions/pi-subagents/test/unit/host-command.test.ts +156 -0
- package/dist/extensions/pi-subagents/test/unit/host-step-status.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +178 -26
- package/dist/extensions/pi-subagents/test/unit/index-segment.test.ts +2 -1
- package/dist/extensions/pi-subagents/test/unit/inspect-rpc.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/mcp-direct-tool-grant.test.ts +138 -0
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/model-exclusions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +72 -7
- package/dist/extensions/pi-subagents/test/unit/mutation-evidence.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +20 -2
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +129 -1
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +4 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +358 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +574 -21
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +109 -1
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +7 -7
- package/dist/extensions/pi-subagents/test/unit/pruned-fork.test.ts +214 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +29 -8
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +53 -0
- package/dist/extensions/pi-subagents/test/unit/result-delivery-ownership.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +101 -3
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +10 -8
- package/dist/extensions/pi-subagents/test/unit/run-fanout-budget.test.ts +71 -1
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +41 -7
- package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +136 -2
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +137 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +33 -12
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +504 -1
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +12 -1
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +3 -5
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +5 -6
- package/dist/extensions/pi-subagents/test/unit/subagent-action-recovery.test.ts +0 -8
- package/dist/extensions/pi-subagents/test/unit/subagent-guide.test.ts +5 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +157 -2
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +63 -4
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +27 -23
- package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +7 -5
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +106 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-detach-reconcile.test.ts +484 -9
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +38 -4
- package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +155 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-receipt.test.ts +318 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-resume-hint.test.ts +365 -0
- package/dist/extensions/pi-subagents/test/unit/worktree-cleanup-plan.test.ts +349 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +21 -0
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +2 -7
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/index.ts +13 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/effect/rules/no-service-constructor-imports.ts +52 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/index.ts +41 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts +77 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts +49 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-known-value-widening.ts +247 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-module-mocking.ts +91 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-object-parameters.ts +126 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-apply.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-reflect-get.ts +28 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-runtime-typeof.ts +67 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts +39 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-parameters.ts +83 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-returns.ts +115 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts +72 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts +134 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/no-widen-then-assert.ts +366 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts +62 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/dictionary-types.ts +502 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/lexical-type-parameters.ts +60 -0
- package/dist/extensions/pi-subagents/tools/oxlint/anti-slop/shared/reflect-method.ts +35 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +1 -1
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +2 -2
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -7
- package/dist/skills/workflow/SKILL.md +65 -50
- package/package.json +112 -110
- package/dist/extensions/enable-readonly-tools.test.ts +0 -63
- package/dist/extensions/enable-readonly-tools.ts +0 -24
- package/dist/extensions/model-prompt-injector/config.json +0 -16
- package/dist/extensions/model-prompt-injector/index.test.ts +0 -102
- package/dist/extensions/model-prompt-injector/index.ts +0 -148
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +0 -98
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +0 -250
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { SCHEMA_SQL } from './schema.js';
|
|
5
|
+
import { AtomicLockCoordinator } from './atomic-lock-coordinator.js';
|
|
6
|
+
import { canonicalStoragePathSync } from './canonical-storage-path.js';
|
|
7
|
+
import { isBunRuntime, loadBetterSqlite3 } from './sqlite-native.js';
|
|
8
|
+
import { measureLifecycleSync } from '../lifecycle-timing.js';
|
|
9
|
+
|
|
10
|
+
type StatementLike = {
|
|
11
|
+
run: (...args: any[]) => any;
|
|
12
|
+
get: (...args: any[]) => any;
|
|
13
|
+
all: (...args: any[]) => any;
|
|
14
|
+
iterate?: (...args: any[]) => Iterable<Record<string, unknown>>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type DatabaseLike = {
|
|
18
|
+
prepare: (sql: string) => StatementLike;
|
|
19
|
+
exec: (sql: string) => void;
|
|
20
|
+
close: () => void;
|
|
21
|
+
pragma?: (query: string, options?: any) => any;
|
|
22
|
+
transaction?: (fn: any) => any;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type DatabaseCtor = new (dbPath: string) => DatabaseLike;
|
|
26
|
+
type BunDatabaseInstance = {
|
|
27
|
+
prepare: (sql: string) => StatementLike;
|
|
28
|
+
exec: (sql: string) => void;
|
|
29
|
+
close: (throwOnError?: boolean) => void;
|
|
30
|
+
transaction?: (fn: any) => any;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type DatabaseFileSuffix = '' | '-wal' | '-shm';
|
|
34
|
+
|
|
35
|
+
type MovedDatabaseFile = {
|
|
36
|
+
original: string;
|
|
37
|
+
backup: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export interface DatabaseRecoveryResult {
|
|
41
|
+
strategy: 'rebuilt' | 'recreated-empty' | 'reused';
|
|
42
|
+
backupPaths: string[];
|
|
43
|
+
recoveredRows?: Record<string, number>;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface DatabaseRecoveryOptions {
|
|
48
|
+
recoveryLockWaitMs?: number;
|
|
49
|
+
recoveryLockPollMs?: number;
|
|
50
|
+
recoveryLockStaleMs?: number;
|
|
51
|
+
recoveryCircuitLimit?: number;
|
|
52
|
+
recoveryCircuitWindowMs?: number;
|
|
53
|
+
recoveryBackupRetention?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ResolvedDatabaseRecoveryOptions {
|
|
57
|
+
recoveryLockWaitMs: number;
|
|
58
|
+
recoveryLockPollMs: number;
|
|
59
|
+
recoveryLockStaleMs: number;
|
|
60
|
+
recoveryCircuitLimit: number;
|
|
61
|
+
recoveryCircuitWindowMs: number;
|
|
62
|
+
recoveryBackupRetention: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class DatabaseCorruptionError extends Error {
|
|
66
|
+
code = 'SQLITE_CORRUPT';
|
|
67
|
+
|
|
68
|
+
constructor(message: string) {
|
|
69
|
+
super(message);
|
|
70
|
+
this.name = 'DatabaseCorruptionError';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const SQLITE_BUSY_TIMEOUT_MS = 5000;
|
|
75
|
+
export const SQLITE_WAL_AUTOCHECKPOINT_PAGES = 1000;
|
|
76
|
+
export const FTS5_MIGRATION_MAX_LOCK_ATTEMPTS = 3;
|
|
77
|
+
|
|
78
|
+
const FTS5_TOKENIZER_VERSION_KEY = 'fts5_tokenizer_version';
|
|
79
|
+
const FTS5_TOKENIZER_VERSION = 'trigram-v1';
|
|
80
|
+
const FTS5_TRIGRAM_TABLES = {
|
|
81
|
+
message: `CREATE VIRTUAL TABLE message_fts USING fts5(
|
|
82
|
+
content,
|
|
83
|
+
content='messages',
|
|
84
|
+
content_rowid='rowid',
|
|
85
|
+
tokenize='trigram'
|
|
86
|
+
)`,
|
|
87
|
+
memory: `CREATE VIRTUAL TABLE memory_fts USING fts5(
|
|
88
|
+
content,
|
|
89
|
+
content='memories',
|
|
90
|
+
content_rowid='id',
|
|
91
|
+
tokenize='trigram'
|
|
92
|
+
)`,
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
const DATABASE_FILE_SUFFIXES: readonly DatabaseFileSuffix[] = ['', '-wal', '-shm'];
|
|
96
|
+
const MEMORY_TARGETS = new Set(['memory', 'user', 'failure']);
|
|
97
|
+
const MEMORY_CATEGORIES = new Set(['failure', 'correction', 'insight', 'preference', 'convention', 'tool-quirk']);
|
|
98
|
+
const DEFAULT_RECOVERY_OPTIONS: ResolvedDatabaseRecoveryOptions = {
|
|
99
|
+
recoveryLockWaitMs: 5000,
|
|
100
|
+
recoveryLockPollMs: 50,
|
|
101
|
+
recoveryLockStaleMs: 300000,
|
|
102
|
+
recoveryCircuitLimit: 3,
|
|
103
|
+
recoveryCircuitWindowMs: 300000,
|
|
104
|
+
recoveryBackupRetention: 3,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function quoteIdentifier(identifier: string): string {
|
|
108
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createBunCompatDatabaseCtor(require: NodeRequire): DatabaseCtor {
|
|
112
|
+
const bunSqlite = require('bun:sqlite') as { Database: new (dbPath: string) => BunDatabaseInstance };
|
|
113
|
+
|
|
114
|
+
return class BunCompatDatabase implements DatabaseLike {
|
|
115
|
+
private readonly db: BunDatabaseInstance;
|
|
116
|
+
|
|
117
|
+
constructor(dbPath: string) {
|
|
118
|
+
this.db = new bunSqlite.Database(dbPath);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
prepare(sql: string): StatementLike {
|
|
122
|
+
return this.db.prepare(sql);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
exec(sql: string): void {
|
|
126
|
+
this.db.exec(sql);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
close(): void {
|
|
130
|
+
this.db.close();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
transaction(fn: any): any {
|
|
134
|
+
if (!this.db.transaction) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return this.db.transaction(fn);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let cachedDatabaseCtor: DatabaseCtor | null = null;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Resolved on first use, never at import time. A module-scope native load turns
|
|
146
|
+
* any SQLite resolve/ABI failure into "Failed to load extension", which hides
|
|
147
|
+
* the actionable rebuild message and bricks the whole extension (issue #117).
|
|
148
|
+
*/
|
|
149
|
+
function getDatabaseCtor(): DatabaseCtor {
|
|
150
|
+
if (!cachedDatabaseCtor) {
|
|
151
|
+
const require = createRequire(import.meta.url);
|
|
152
|
+
cachedDatabaseCtor = isBunRuntime()
|
|
153
|
+
? createBunCompatDatabaseCtor(require)
|
|
154
|
+
: (loadBetterSqlite3({ requireImpl: require }) as DatabaseCtor);
|
|
155
|
+
}
|
|
156
|
+
return cachedDatabaseCtor;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export class DatabaseManager {
|
|
160
|
+
private db: DatabaseLike | null = null;
|
|
161
|
+
private readonly displayDbPath: string;
|
|
162
|
+
private canonicalDbPath: string | null = null;
|
|
163
|
+
private readonly recoveryOptions: ResolvedDatabaseRecoveryOptions;
|
|
164
|
+
private lastRecovery: DatabaseRecoveryResult | null = null;
|
|
165
|
+
private openGuard: (() => void) | null = null;
|
|
166
|
+
private pendingOpenIntegrityScan: Promise<void> | null = null;
|
|
167
|
+
private activeRecoveryLease: { coordinator: AtomicLockCoordinator; key: string; token: string } | null = null;
|
|
168
|
+
|
|
169
|
+
constructor(memoryDir: string, recoveryOptions: DatabaseRecoveryOptions = {}) {
|
|
170
|
+
this.displayDbPath = path.join(memoryDir, 'sessions.db');
|
|
171
|
+
this.recoveryOptions = { ...DEFAULT_RECOVERY_OPTIONS, ...recoveryOptions };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private get dbPath(): string {
|
|
175
|
+
if (!this.canonicalDbPath) {
|
|
176
|
+
this.canonicalDbPath = canonicalStoragePathSync(this.displayDbPath);
|
|
177
|
+
}
|
|
178
|
+
return this.canonicalDbPath;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
setOpenGuard(guard: (() => void) | null): void {
|
|
182
|
+
this.openGuard = guard;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* True when an error indicates SQLite file/page corruption rather than a
|
|
187
|
+
* normal constraint, migration, or query failure.
|
|
188
|
+
*/
|
|
189
|
+
static isCorruptionError(err: unknown): boolean {
|
|
190
|
+
if (!err) return false;
|
|
191
|
+
|
|
192
|
+
const code = typeof err === 'object' && 'code' in err ? String((err as { code?: unknown }).code) : '';
|
|
193
|
+
if (code === 'SQLITE_CORRUPT' || code === 'SQLITE_NOTADB') return true;
|
|
194
|
+
|
|
195
|
+
const message = DatabaseManager.errorMessage(err).toLowerCase();
|
|
196
|
+
return message.includes('database disk image is malformed')
|
|
197
|
+
|| message.includes('file is not a database')
|
|
198
|
+
|| message.includes('database schema is corrupt')
|
|
199
|
+
|| message.includes('malformed database schema')
|
|
200
|
+
|| message.includes('btreeinitpage')
|
|
201
|
+
|| message.includes('sqlite_corrupt')
|
|
202
|
+
|| message.includes('sqlite_notadb');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private static errorMessage(err: unknown): string {
|
|
206
|
+
if (err instanceof Error) return err.message;
|
|
207
|
+
return String(err);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the database instance. Creates/opens on first call.
|
|
212
|
+
*/
|
|
213
|
+
getDb(): DatabaseLike {
|
|
214
|
+
if (!this.db) {
|
|
215
|
+
this.openGuard?.();
|
|
216
|
+
this.db = this.open();
|
|
217
|
+
}
|
|
218
|
+
return this.db;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Last self-heal performed by this manager, if any. Exposed for diagnostics
|
|
223
|
+
* and tests; normal callers do not need it.
|
|
224
|
+
*/
|
|
225
|
+
getLastRecovery(): DatabaseRecoveryResult | null {
|
|
226
|
+
return this.lastRecovery;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Retry a DB operation once after quarantining/rebuilding a corrupt DB.
|
|
231
|
+
*/
|
|
232
|
+
withCorruptionRecovery<T>(operation: () => T): T {
|
|
233
|
+
try {
|
|
234
|
+
return operation();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (!DatabaseManager.isCorruptionError(err)) {
|
|
237
|
+
throw err;
|
|
238
|
+
}
|
|
239
|
+
this.recoverFromCorruption(err);
|
|
240
|
+
return operation();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Close any open handle, rebuild/quarantine the DB file set, and let the next
|
|
246
|
+
* getDb() reopen a clean database.
|
|
247
|
+
*/
|
|
248
|
+
recoverFromCorruption(cause?: unknown): DatabaseRecoveryResult {
|
|
249
|
+
this.close();
|
|
250
|
+
let verifiedDb: DatabaseLike | null = null;
|
|
251
|
+
let recovery: DatabaseRecoveryResult;
|
|
252
|
+
try {
|
|
253
|
+
recovery = this.recoverDatabaseFile(cause, () => {
|
|
254
|
+
verifiedDb = this.openUnchecked();
|
|
255
|
+
});
|
|
256
|
+
} finally {
|
|
257
|
+
if (verifiedDb) this.safeClose(verifiedDb);
|
|
258
|
+
}
|
|
259
|
+
this.lastRecovery = recovery;
|
|
260
|
+
return recovery;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Open the database and initialize schema.
|
|
265
|
+
*/
|
|
266
|
+
private open(): DatabaseLike {
|
|
267
|
+
return measureLifecycleSync('database.open', () => {
|
|
268
|
+
const dir = path.dirname(this.dbPath);
|
|
269
|
+
if (!fs.existsSync(dir)) {
|
|
270
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
let opened: DatabaseLike;
|
|
274
|
+
try {
|
|
275
|
+
opened = this.openUnchecked();
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (!DatabaseManager.isCorruptionError(err)) {
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let recoveredDb: DatabaseLike | null = null;
|
|
282
|
+
let recovery: DatabaseRecoveryResult;
|
|
283
|
+
try {
|
|
284
|
+
recovery = this.recoverDatabaseFile(err, () => {
|
|
285
|
+
recoveredDb = this.openUnchecked();
|
|
286
|
+
});
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (recoveredDb) this.safeClose(recoveredDb);
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
this.lastRecovery = recovery;
|
|
292
|
+
if (!recoveredDb) throw new Error(`SQLite recovery verification did not open ${this.displayDbPath}`);
|
|
293
|
+
return recoveredDb;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this.scheduleOpenIntegrityScan(opened);
|
|
297
|
+
return opened;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* quick_check walks the whole DB, so open() never pays that cost: the scan
|
|
303
|
+
* runs after open returns and failures go through the same recovery used
|
|
304
|
+
* at operation time.
|
|
305
|
+
*/
|
|
306
|
+
private scheduleOpenIntegrityScan(db: DatabaseLike): void {
|
|
307
|
+
if (this.pendingOpenIntegrityScan) return;
|
|
308
|
+
const scan = new Promise<void>((resolve) => {
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
try {
|
|
311
|
+
if (this.db !== db) {
|
|
312
|
+
// Manager closed or reopened; the newest open schedules its own
|
|
313
|
+
// scan, so skip to avoid a stale-handle recovery.
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
measureLifecycleSync('database.quick-check', () => {
|
|
317
|
+
try {
|
|
318
|
+
this.assertIntegrityOk(db, 'quick_check', 'after open');
|
|
319
|
+
} catch (err) {
|
|
320
|
+
try {
|
|
321
|
+
this.recoverFromCorruption(err);
|
|
322
|
+
} catch {
|
|
323
|
+
// Best-effort here; at-operation withCorruptionRecovery still
|
|
324
|
+
// quarantines and rebuilds if a later statement hits the corruption.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
} finally {
|
|
329
|
+
if (this.pendingOpenIntegrityScan === scan) {
|
|
330
|
+
this.pendingOpenIntegrityScan = null;
|
|
331
|
+
}
|
|
332
|
+
resolve();
|
|
333
|
+
}
|
|
334
|
+
}, 0);
|
|
335
|
+
});
|
|
336
|
+
this.pendingOpenIntegrityScan = scan;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Test aid. */
|
|
340
|
+
async waitForStartupIntegrityScan(): Promise<void> {
|
|
341
|
+
await this.pendingOpenIntegrityScan;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private openUnchecked(): DatabaseLike {
|
|
345
|
+
const db = new (getDatabaseCtor())(this.dbPath);
|
|
346
|
+
let ok = false;
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
this.configureConnection(db);
|
|
350
|
+
this.initializeSchema(db);
|
|
351
|
+
ok = true;
|
|
352
|
+
return db;
|
|
353
|
+
} finally {
|
|
354
|
+
if (!ok) {
|
|
355
|
+
this.safeClose(db);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private configureConnection(db: DatabaseLike): void {
|
|
361
|
+
// Wait briefly for concurrent writers across Pi processes instead of failing
|
|
362
|
+
// immediately with SQLITE_BUSY. Connection-local; applies before WAL/schema.
|
|
363
|
+
db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
|
|
364
|
+
// Enable WAL mode + FK enforcement for each connection. Keep SQLite's
|
|
365
|
+
// default WAL autocheckpoint size; a very aggressive checkpoint cadence
|
|
366
|
+
// increases the chance that abrupt VM/host shutdown catches a checkpoint.
|
|
367
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
368
|
+
db.exec(`PRAGMA wal_autocheckpoint = ${SQLITE_WAL_AUTOCHECKPOINT_PAGES}`);
|
|
369
|
+
db.exec('PRAGMA journal_size_limit = 5242880');
|
|
370
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private initializeSchema(db: DatabaseLike): void {
|
|
374
|
+
// Create tables and triggers
|
|
375
|
+
try {
|
|
376
|
+
db.exec(SCHEMA_SQL);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
if (!this.isLegacySchemaError(err)) {
|
|
379
|
+
throw err;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Legacy DBs can be missing v0.6 failure-memory columns and/or the project
|
|
383
|
+
// column on sessions/memories. Add missing columns, then retry schema.
|
|
384
|
+
this.ensureLegacySchemaColumns(db);
|
|
385
|
+
db.exec(SCHEMA_SQL);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Extra safety: always ensure legacy columns exist, then migrate legacy
|
|
389
|
+
// CHECK(target IN ('memory','user')) constraints to include 'failure'.
|
|
390
|
+
this.ensureLegacySchemaColumns(db);
|
|
391
|
+
this.migrateLegacyMemoriesTargetConstraint(db);
|
|
392
|
+
// Recreate indexes after any legacy table replacement. `DROP TABLE`
|
|
393
|
+
// removes indexes attached to the old memories table.
|
|
394
|
+
this.ensureMemoryIndexes(db);
|
|
395
|
+
this.migrateFtsTokenizer(db);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private hasExistingMainDatabaseFile(): boolean {
|
|
399
|
+
try {
|
|
400
|
+
return fs.existsSync(this.dbPath) && fs.statSync(this.dbPath).size > 0;
|
|
401
|
+
} catch {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private databaseFileSetExists(): boolean {
|
|
407
|
+
return DATABASE_FILE_SUFFIXES.some((suffix) => fs.existsSync(`${this.dbPath}${suffix}`));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private assertIntegrityOk(
|
|
411
|
+
db: DatabaseLike,
|
|
412
|
+
check: 'quick_check' | 'integrity_check' = 'quick_check',
|
|
413
|
+
context = '',
|
|
414
|
+
): void {
|
|
415
|
+
const rows = db.prepare(`PRAGMA ${check}`).all() as Record<string, unknown>[];
|
|
416
|
+
const messages = rows.map((row) => String(Object.values(row)[0] ?? ''));
|
|
417
|
+
const failures = messages.filter((message) => message.toLowerCase() !== 'ok');
|
|
418
|
+
|
|
419
|
+
if (rows.length === 0 || failures.length > 0) {
|
|
420
|
+
const detail = failures.length > 0 ? failures.slice(0, 5).join('\n') : 'no result rows';
|
|
421
|
+
const suffix = context ? ` ${context}` : '';
|
|
422
|
+
throw new DatabaseCorruptionError(`SQLite ${check} failed${suffix}: ${detail}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private assertForeignKeysOk(db: DatabaseLike): void {
|
|
427
|
+
const rows = db.prepare('PRAGMA foreign_key_check').all() as Record<string, unknown>[];
|
|
428
|
+
if (rows.length > 0) {
|
|
429
|
+
throw new Error(`SQLite foreign_key_check failed after rebuild (${rows.length} violation${rows.length === 1 ? '' : 's'})`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private recoverDatabaseFile(cause: unknown, verify: () => void): DatabaseRecoveryResult {
|
|
434
|
+
const coordinator = AtomicLockCoordinator.shared(path.join(path.dirname(this.dbPath), '.pi-hermes-locks.sqlite'));
|
|
435
|
+
const lockKey = `recovery:${this.dbPath}`;
|
|
436
|
+
const deadline = Date.now() + Math.max(0, this.recoveryOptions.recoveryLockWaitMs);
|
|
437
|
+
|
|
438
|
+
while (true) {
|
|
439
|
+
const lease = coordinator.tryAcquire(lockKey, { staleMs: this.recoveryOptions.recoveryLockStaleMs });
|
|
440
|
+
if (!lease) {
|
|
441
|
+
if (Date.now() >= deadline) {
|
|
442
|
+
throw new Error(`SQLite recovery already in progress for ${this.displayDbPath}; timed out after ${this.recoveryOptions.recoveryLockWaitMs}ms`);
|
|
443
|
+
}
|
|
444
|
+
DatabaseManager.sleepSync(Math.min(
|
|
445
|
+
this.recoveryOptions.recoveryLockPollMs,
|
|
446
|
+
Math.max(1, deadline - Date.now()),
|
|
447
|
+
));
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this.activeRecoveryLease = { coordinator, key: lockKey, token: lease.token };
|
|
452
|
+
try {
|
|
453
|
+
if (this.currentDatabaseIsHealthy()) {
|
|
454
|
+
try {
|
|
455
|
+
verify();
|
|
456
|
+
this.clearRecoveryFailuresBestEffort();
|
|
457
|
+
return { strategy: 'reused', backupPaths: [] };
|
|
458
|
+
} catch (error) {
|
|
459
|
+
this.recordRecoveryFailure();
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
this.assertRecoveryCircuitClosed();
|
|
465
|
+
try {
|
|
466
|
+
this.cleanupRecoveryArtifactsBestEffort();
|
|
467
|
+
const result = this.recoverDatabaseFileUnlocked(cause);
|
|
468
|
+
verify();
|
|
469
|
+
this.cleanupRecoveryArtifactsBestEffort();
|
|
470
|
+
this.clearRecoveryFailuresBestEffort();
|
|
471
|
+
return result;
|
|
472
|
+
} catch (error) {
|
|
473
|
+
this.recordRecoveryFailure();
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
476
|
+
} finally {
|
|
477
|
+
this.activeRecoveryLease = null;
|
|
478
|
+
lease.release();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private recoverDatabaseFileUnlocked(cause?: unknown): DatabaseRecoveryResult {
|
|
484
|
+
const backupBase = this.corruptBackupBase();
|
|
485
|
+
let rebuildError: unknown;
|
|
486
|
+
|
|
487
|
+
if (this.databaseFileSetExists()) {
|
|
488
|
+
try {
|
|
489
|
+
return this.rebuildDatabaseFromReadableRows(backupBase);
|
|
490
|
+
} catch (err) {
|
|
491
|
+
rebuildError = err;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const moved = this.moveDatabaseFilesToBackup(backupBase);
|
|
496
|
+
return {
|
|
497
|
+
strategy: 'recreated-empty',
|
|
498
|
+
backupPaths: moved.map((file) => file.backup),
|
|
499
|
+
error: DatabaseManager.errorMessage(rebuildError ?? cause ?? 'unknown corruption'),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private currentDatabaseIsHealthy(): boolean {
|
|
504
|
+
if (!this.hasExistingMainDatabaseFile()) return false;
|
|
505
|
+
let db: DatabaseLike | null = null;
|
|
506
|
+
try {
|
|
507
|
+
db = new (getDatabaseCtor())(this.dbPath);
|
|
508
|
+
this.assertIntegrityOk(db, 'quick_check', 'while joining corruption recovery');
|
|
509
|
+
return true;
|
|
510
|
+
} catch {
|
|
511
|
+
return false;
|
|
512
|
+
} finally {
|
|
513
|
+
if (db) this.safeClose(db);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private recoveryCircuitPath(): string {
|
|
518
|
+
return `${this.dbPath}.recovery-state.json`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
private recentRecoveryFailures(): number[] {
|
|
522
|
+
try {
|
|
523
|
+
const parsed = JSON.parse(fs.readFileSync(this.recoveryCircuitPath(), 'utf-8')) as { failures?: unknown };
|
|
524
|
+
if (!Array.isArray(parsed.failures)) return [];
|
|
525
|
+
const cutoff = Date.now() - Math.max(0, this.recoveryOptions.recoveryCircuitWindowMs);
|
|
526
|
+
return parsed.failures.filter((value): value is number => typeof value === 'number' && value >= cutoff);
|
|
527
|
+
} catch {
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
private assertRecoveryCircuitClosed(): void {
|
|
533
|
+
if (this.recentRecoveryFailures().length >= Math.max(1, this.recoveryOptions.recoveryCircuitLimit)) {
|
|
534
|
+
throw new Error(
|
|
535
|
+
`SQLite recovery circuit is open for ${this.displayDbPath}: too many failed recovery attempts within ${this.recoveryOptions.recoveryCircuitWindowMs}ms`,
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private recordRecoveryFailure(): void {
|
|
541
|
+
const statePath = this.recoveryCircuitPath();
|
|
542
|
+
const tempPath = `${statePath}.tmp-${process.pid}-${Math.random().toString(16).slice(2, 8)}`;
|
|
543
|
+
const failures = [...this.recentRecoveryFailures(), Date.now()];
|
|
544
|
+
try {
|
|
545
|
+
fs.writeFileSync(tempPath, JSON.stringify({ failures }), { encoding: 'utf-8', mode: 0o600 });
|
|
546
|
+
fs.renameSync(tempPath, statePath);
|
|
547
|
+
} finally {
|
|
548
|
+
fs.rmSync(tempPath, { force: true });
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private clearRecoveryFailures(): void {
|
|
553
|
+
fs.rmSync(this.recoveryCircuitPath(), { force: true });
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private clearRecoveryFailuresBestEffort(): void {
|
|
557
|
+
try { this.clearRecoveryFailures(); } catch {}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
private cleanupRecoveryArtifactsBestEffort(): void {
|
|
561
|
+
try { this.cleanupRecoveryArtifacts(); } catch {}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private cleanupRecoveryArtifacts(): void {
|
|
565
|
+
const dir = path.dirname(this.dbPath);
|
|
566
|
+
const databaseName = path.basename(this.dbPath);
|
|
567
|
+
let names: string[];
|
|
568
|
+
try {
|
|
569
|
+
names = fs.readdirSync(dir);
|
|
570
|
+
} catch {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
for (const name of names) {
|
|
575
|
+
if (name.startsWith(`${databaseName}.rebuild-`)) {
|
|
576
|
+
fs.rmSync(path.join(dir, name), { recursive: true, force: true });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const backupGroups = new Map<string, number>();
|
|
581
|
+
for (const name of names) {
|
|
582
|
+
if (!name.startsWith(`${databaseName}.corrupt-`)) continue;
|
|
583
|
+
const group = name.replace(/-(?:wal|shm)$/, '');
|
|
584
|
+
try {
|
|
585
|
+
const mtimeMs = fs.statSync(path.join(dir, name)).mtimeMs;
|
|
586
|
+
backupGroups.set(group, Math.max(backupGroups.get(group) ?? 0, mtimeMs));
|
|
587
|
+
} catch {
|
|
588
|
+
// Artifact disappeared while scanning.
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const retained = Math.max(0, this.recoveryOptions.recoveryBackupRetention);
|
|
593
|
+
const expired = [...backupGroups.entries()]
|
|
594
|
+
.sort((left, right) => right[1] - left[1])
|
|
595
|
+
.slice(retained);
|
|
596
|
+
for (const [group] of expired) {
|
|
597
|
+
for (const suffix of DATABASE_FILE_SUFFIXES) {
|
|
598
|
+
fs.rmSync(path.join(dir, `${group}${suffix}`), { force: true });
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private static sleepSync(milliseconds: number): void {
|
|
604
|
+
if (milliseconds <= 0) return;
|
|
605
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
606
|
+
Atomics.wait(signal, 0, 0, milliseconds);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private rebuildDatabaseFromReadableRows(backupBase: string): DatabaseRecoveryResult {
|
|
610
|
+
const tempPath = this.rebuildTempPath();
|
|
611
|
+
this.removeDatabaseFileSet(tempPath);
|
|
612
|
+
|
|
613
|
+
let source: DatabaseLike | null = null;
|
|
614
|
+
let target: DatabaseLike | null = null;
|
|
615
|
+
let recoveredRows: Record<string, number> | undefined;
|
|
616
|
+
let rebuildOk = false;
|
|
617
|
+
|
|
618
|
+
try {
|
|
619
|
+
const Database = getDatabaseCtor();
|
|
620
|
+
source = new Database(this.dbPath);
|
|
621
|
+
target = new Database(tempPath);
|
|
622
|
+
target.exec('PRAGMA journal_mode = DELETE');
|
|
623
|
+
target.exec('PRAGMA foreign_keys = OFF');
|
|
624
|
+
target.exec(SCHEMA_SQL);
|
|
625
|
+
|
|
626
|
+
recoveredRows = this.copyRecoverableRows(source, target);
|
|
627
|
+
this.rebuildFtsTables(target);
|
|
628
|
+
this.assertForeignKeysOk(target);
|
|
629
|
+
this.assertIntegrityOk(target, 'quick_check', 'after corruption rebuild');
|
|
630
|
+
rebuildOk = true;
|
|
631
|
+
} finally {
|
|
632
|
+
if (source) this.safeClose(source);
|
|
633
|
+
if (target) this.safeClose(target);
|
|
634
|
+
if (!rebuildOk) this.removeDatabaseFileSet(tempPath);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const moved = this.swapRebuiltDatabase(tempPath, backupBase);
|
|
638
|
+
this.removeDatabaseFileSet(tempPath);
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
strategy: 'rebuilt',
|
|
642
|
+
backupPaths: moved.map((file) => file.backup),
|
|
643
|
+
recoveredRows,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private copyRecoverableRows(source: DatabaseLike, target: DatabaseLike): Record<string, number> {
|
|
648
|
+
return {
|
|
649
|
+
extension_metadata: this.copyExtensionMetadata(source, target),
|
|
650
|
+
sessions: this.copySessions(source, target),
|
|
651
|
+
messages: this.copyMessages(source, target),
|
|
652
|
+
session_files: this.copySessionFiles(source, target),
|
|
653
|
+
memories: this.copyMemories(source, target),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private copyExtensionMetadata(source: DatabaseLike, target: DatabaseLike): number {
|
|
658
|
+
const insert = target.prepare('INSERT OR REPLACE INTO extension_metadata (key, value) VALUES (?, ?)');
|
|
659
|
+
let copied = 0;
|
|
660
|
+
|
|
661
|
+
for (const row of this.readTableRows(source, 'extension_metadata', ['key', 'value'])) {
|
|
662
|
+
if (typeof row.key !== 'string' || typeof row.value !== 'string') continue;
|
|
663
|
+
insert.run(row.key, row.value);
|
|
664
|
+
copied++;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return copied;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private copySessions(source: DatabaseLike, target: DatabaseLike): number {
|
|
671
|
+
const insert = target.prepare(`
|
|
672
|
+
INSERT OR IGNORE INTO sessions (id, project, cwd, started_at, ended_at, message_count)
|
|
673
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
674
|
+
`);
|
|
675
|
+
let copied = 0;
|
|
676
|
+
|
|
677
|
+
for (const row of this.readTableRows(source, 'sessions', ['id', 'project', 'cwd', 'started_at', 'ended_at', 'message_count'])) {
|
|
678
|
+
if (typeof row.id !== 'string' || typeof row.cwd !== 'string' || typeof row.started_at !== 'string') continue;
|
|
679
|
+
const project = typeof row.project === 'string' && row.project ? row.project : (path.basename(row.cwd) || 'unknown');
|
|
680
|
+
insert.run(
|
|
681
|
+
row.id,
|
|
682
|
+
project,
|
|
683
|
+
row.cwd,
|
|
684
|
+
row.started_at,
|
|
685
|
+
this.nullableString(row.ended_at),
|
|
686
|
+
this.integerOr(row.message_count, 0),
|
|
687
|
+
);
|
|
688
|
+
copied++;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return copied;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private copyMessages(source: DatabaseLike, target: DatabaseLike): number {
|
|
695
|
+
const insert = target.prepare(`
|
|
696
|
+
INSERT OR IGNORE INTO messages (id, session_id, role, content, timestamp, tool_calls)
|
|
697
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
698
|
+
`);
|
|
699
|
+
let copied = 0;
|
|
700
|
+
|
|
701
|
+
for (const row of this.readTableRows(source, 'messages', ['id', 'session_id', 'role', 'content', 'timestamp', 'tool_calls'])) {
|
|
702
|
+
if (
|
|
703
|
+
typeof row.id !== 'string'
|
|
704
|
+
|| typeof row.session_id !== 'string'
|
|
705
|
+
|| (row.role !== 'user' && row.role !== 'assistant' && row.role !== 'system')
|
|
706
|
+
|| typeof row.content !== 'string'
|
|
707
|
+
|| typeof row.timestamp !== 'string'
|
|
708
|
+
) {
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
insert.run(row.id, row.session_id, row.role, row.content, row.timestamp, this.nullableString(row.tool_calls));
|
|
713
|
+
copied++;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return copied;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private copySessionFiles(source: DatabaseLike, target: DatabaseLike): number {
|
|
720
|
+
const insert = target.prepare(`
|
|
721
|
+
INSERT OR IGNORE INTO session_files (path, session_id, size, mtime_ms, indexed_at)
|
|
722
|
+
VALUES (?, ?, ?, ?, ?)
|
|
723
|
+
`);
|
|
724
|
+
let copied = 0;
|
|
725
|
+
|
|
726
|
+
for (const row of this.readTableRows(source, 'session_files', ['path', 'session_id', 'size', 'mtime_ms', 'indexed_at'])) {
|
|
727
|
+
if (typeof row.path !== 'string' || typeof row.session_id !== 'string') continue;
|
|
728
|
+
insert.run(
|
|
729
|
+
row.path,
|
|
730
|
+
row.session_id,
|
|
731
|
+
this.integerOr(row.size, 0),
|
|
732
|
+
this.integerOr(row.mtime_ms, 0),
|
|
733
|
+
typeof row.indexed_at === 'string' ? row.indexed_at : new Date(0).toISOString(),
|
|
734
|
+
);
|
|
735
|
+
copied++;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return copied;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private copyMemories(source: DatabaseLike, target: DatabaseLike): number {
|
|
742
|
+
const insert = target.prepare(`
|
|
743
|
+
INSERT OR IGNORE INTO memories (id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced)
|
|
744
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
745
|
+
`);
|
|
746
|
+
let copied = 0;
|
|
747
|
+
|
|
748
|
+
for (const row of this.readTableRows(source, 'memories', [
|
|
749
|
+
'id',
|
|
750
|
+
'project',
|
|
751
|
+
'target',
|
|
752
|
+
'category',
|
|
753
|
+
'content',
|
|
754
|
+
'failure_reason',
|
|
755
|
+
'tool_state',
|
|
756
|
+
'corrected_to',
|
|
757
|
+
'created',
|
|
758
|
+
'last_referenced',
|
|
759
|
+
])) {
|
|
760
|
+
const id = this.integerOr(row.id, NaN);
|
|
761
|
+
if (!Number.isFinite(id) || typeof row.content !== 'string') continue;
|
|
762
|
+
|
|
763
|
+
const targetName = typeof row.target === 'string' && MEMORY_TARGETS.has(row.target) ? row.target : 'memory';
|
|
764
|
+
const category = typeof row.category === 'string' && MEMORY_CATEGORIES.has(row.category) ? row.category : null;
|
|
765
|
+
const created = typeof row.created === 'string' ? row.created : new Date(0).toISOString();
|
|
766
|
+
const lastReferenced = typeof row.last_referenced === 'string' ? row.last_referenced : created;
|
|
767
|
+
|
|
768
|
+
insert.run(
|
|
769
|
+
id,
|
|
770
|
+
this.nullableString(row.project),
|
|
771
|
+
targetName,
|
|
772
|
+
category,
|
|
773
|
+
row.content,
|
|
774
|
+
this.nullableString(row.failure_reason),
|
|
775
|
+
this.nullableString(row.tool_state),
|
|
776
|
+
this.nullableString(row.corrected_to),
|
|
777
|
+
created,
|
|
778
|
+
lastReferenced,
|
|
779
|
+
);
|
|
780
|
+
copied++;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return copied;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
private readTableRows(source: DatabaseLike, table: string, desiredColumns: string[]): Iterable<Record<string, unknown>> {
|
|
787
|
+
const columns = this.getColumnNames(source, table);
|
|
788
|
+
const selected = desiredColumns.filter((column) => columns.has(column));
|
|
789
|
+
if (selected.length === 0) return [];
|
|
790
|
+
|
|
791
|
+
const sql = `SELECT ${selected.map(quoteIdentifier).join(', ')} FROM ${quoteIdentifier(table)} NOT INDEXED`;
|
|
792
|
+
const statement = source.prepare(sql);
|
|
793
|
+
if (statement.iterate) {
|
|
794
|
+
return statement.iterate() as Iterable<Record<string, unknown>>;
|
|
795
|
+
}
|
|
796
|
+
return statement.all() as Record<string, unknown>[];
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private getColumnNames(db: DatabaseLike, table: string): Set<string> {
|
|
800
|
+
const rows = db.prepare(`PRAGMA table_info(${quoteIdentifier(table)})`).all() as { name?: unknown }[];
|
|
801
|
+
return new Set(rows.map((row) => row.name).filter((name): name is string => typeof name === 'string'));
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
private nullableString(value: unknown): string | null {
|
|
805
|
+
return typeof value === 'string' ? value : null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private integerOr(value: unknown, fallback: number): number {
|
|
809
|
+
if (typeof value === 'number' && Number.isFinite(value)) return Math.trunc(value);
|
|
810
|
+
if (typeof value === 'bigint') return Number(value);
|
|
811
|
+
if (typeof value === 'string' && value.trim()) {
|
|
812
|
+
const parsed = Number.parseInt(value, 10);
|
|
813
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
814
|
+
}
|
|
815
|
+
return fallback;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private rebuildFtsTables(db: DatabaseLike): void {
|
|
819
|
+
db.exec("INSERT INTO message_fts(message_fts) VALUES('rebuild')");
|
|
820
|
+
db.exec("INSERT INTO memory_fts(memory_fts) VALUES('rebuild')");
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
private corruptBackupBase(): string {
|
|
824
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
825
|
+
const nonce = Math.random().toString(16).slice(2, 8);
|
|
826
|
+
return `${this.dbPath}.corrupt-${stamp}-${process.pid}-${nonce}`;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private rebuildTempPath(): string {
|
|
830
|
+
const stamp = Date.now();
|
|
831
|
+
const nonce = Math.random().toString(16).slice(2, 8);
|
|
832
|
+
return `${this.dbPath}.rebuild-${process.pid}-${stamp}-${nonce}.tmp`;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private swapRebuiltDatabase(tempPath: string, backupBase: string): MovedDatabaseFile[] {
|
|
836
|
+
const moved = this.moveDatabaseFilesToBackup(backupBase);
|
|
837
|
+
try {
|
|
838
|
+
fs.renameSync(tempPath, this.dbPath);
|
|
839
|
+
return moved;
|
|
840
|
+
} catch (err) {
|
|
841
|
+
this.restoreMovedDatabaseFiles(moved);
|
|
842
|
+
this.removeDatabaseFileSet(tempPath);
|
|
843
|
+
throw err;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private moveDatabaseFilesToBackup(backupBase: string): MovedDatabaseFile[] {
|
|
848
|
+
this.assertStillRecoveryOwner();
|
|
849
|
+
const moved: MovedDatabaseFile[] = [];
|
|
850
|
+
for (const suffix of DATABASE_FILE_SUFFIXES) {
|
|
851
|
+
const original = `${this.dbPath}${suffix}`;
|
|
852
|
+
if (!fs.existsSync(original)) continue;
|
|
853
|
+
|
|
854
|
+
const backup = `${backupBase}${suffix}`;
|
|
855
|
+
fs.rmSync(backup, { force: true });
|
|
856
|
+
fs.renameSync(original, backup);
|
|
857
|
+
moved.push({ original, backup });
|
|
858
|
+
}
|
|
859
|
+
return moved;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Verifies this instance still holds the recovery lease immediately before
|
|
864
|
+
* a destructive rename that has no independent compare-and-swap of its
|
|
865
|
+
* own (unlike the Markdown mutation path, which re-checks a content
|
|
866
|
+
* fingerprint at publish time). If the lease was reclaimed as stale while
|
|
867
|
+
* this call was in flight, abort rather than race the new owner.
|
|
868
|
+
*/
|
|
869
|
+
private assertStillRecoveryOwner(): void {
|
|
870
|
+
const active = this.activeRecoveryLease;
|
|
871
|
+
if (!active) return;
|
|
872
|
+
if (!active.coordinator.isCurrentOwner(active.key, active.token)) {
|
|
873
|
+
throw new Error(`SQLite recovery lease lost for ${this.displayDbPath}; another process took over`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private restoreMovedDatabaseFiles(moved: MovedDatabaseFile[]): void {
|
|
878
|
+
for (const file of [...moved].reverse()) {
|
|
879
|
+
try {
|
|
880
|
+
if (!fs.existsSync(file.backup)) continue;
|
|
881
|
+
fs.rmSync(file.original, { force: true });
|
|
882
|
+
fs.renameSync(file.backup, file.original);
|
|
883
|
+
} catch {
|
|
884
|
+
// Best effort. The backup path remains available if restoration fails.
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private removeDatabaseFileSet(basePath: string): void {
|
|
890
|
+
for (const suffix of DATABASE_FILE_SUFFIXES) {
|
|
891
|
+
fs.rmSync(`${basePath}${suffix}`, { force: true });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private safeClose(db: DatabaseLike): void {
|
|
896
|
+
try { db.close(); } catch { /* best effort */ }
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
private isLegacySchemaError(err: unknown): boolean {
|
|
900
|
+
if (!(err instanceof Error)) return false;
|
|
901
|
+
const msg = err.message.toLowerCase();
|
|
902
|
+
return msg.includes('no such column: category')
|
|
903
|
+
|| msg.includes('memories(category)')
|
|
904
|
+
|| msg.includes('no such column: project')
|
|
905
|
+
|| msg.includes('sessions(project)')
|
|
906
|
+
|| msg.includes('memories(project)');
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private ensureLegacySchemaColumns(db: DatabaseLike): void {
|
|
910
|
+
this.ensureMemoriesColumns(db);
|
|
911
|
+
this.ensureSessionsColumns(db);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private ensureMemoriesColumns(db: DatabaseLike): void {
|
|
915
|
+
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memories'").get() as { name: string } | undefined;
|
|
916
|
+
if (!tableExists) return;
|
|
917
|
+
|
|
918
|
+
const names = this.getColumnNames(db, 'memories');
|
|
919
|
+
|
|
920
|
+
if (!names.has('project')) {
|
|
921
|
+
db.exec('ALTER TABLE memories ADD COLUMN project TEXT');
|
|
922
|
+
}
|
|
923
|
+
if (!names.has('category')) {
|
|
924
|
+
db.exec('ALTER TABLE memories ADD COLUMN category TEXT');
|
|
925
|
+
}
|
|
926
|
+
if (!names.has('failure_reason')) {
|
|
927
|
+
db.exec('ALTER TABLE memories ADD COLUMN failure_reason TEXT');
|
|
928
|
+
}
|
|
929
|
+
if (!names.has('tool_state')) {
|
|
930
|
+
db.exec('ALTER TABLE memories ADD COLUMN tool_state TEXT');
|
|
931
|
+
}
|
|
932
|
+
if (!names.has('corrected_to')) {
|
|
933
|
+
db.exec('ALTER TABLE memories ADD COLUMN corrected_to TEXT');
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private ensureSessionsColumns(db: DatabaseLike): void {
|
|
938
|
+
const tableExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='sessions'").get() as { name: string } | undefined;
|
|
939
|
+
if (!tableExists) return;
|
|
940
|
+
|
|
941
|
+
const names = this.getColumnNames(db, 'sessions');
|
|
942
|
+
if (!names.has('project')) {
|
|
943
|
+
db.exec('ALTER TABLE sessions ADD COLUMN project TEXT');
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
this.backfillSessionsProject(db);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private backfillSessionsProject(db: DatabaseLike): void {
|
|
950
|
+
const names = this.getColumnNames(db, 'sessions');
|
|
951
|
+
if (!names.has('project') || !names.has('cwd') || !names.has('id')) return;
|
|
952
|
+
|
|
953
|
+
const rows = db.prepare('SELECT id, cwd, project FROM sessions').all() as Array<{
|
|
954
|
+
id?: unknown;
|
|
955
|
+
cwd?: unknown;
|
|
956
|
+
project?: unknown;
|
|
957
|
+
}>;
|
|
958
|
+
const update = db.prepare('UPDATE sessions SET project = ? WHERE id = ?');
|
|
959
|
+
|
|
960
|
+
for (const row of rows) {
|
|
961
|
+
if (typeof row.id !== 'string') continue;
|
|
962
|
+
if (typeof row.project === 'string' && row.project.trim()) continue;
|
|
963
|
+
|
|
964
|
+
const project = typeof row.cwd === 'string' && row.cwd.trim()
|
|
965
|
+
? (path.basename(row.cwd) || 'unknown')
|
|
966
|
+
: 'unknown';
|
|
967
|
+
update.run(project, row.id);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
private ensureMemoryIndexes(db: DatabaseLike): void {
|
|
971
|
+
db.exec(`
|
|
972
|
+
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
|
|
973
|
+
CREATE INDEX IF NOT EXISTS idx_memories_target ON memories(target);
|
|
974
|
+
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
975
|
+
`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
private migrateLegacyMemoriesTargetConstraint(db: DatabaseLike): void {
|
|
980
|
+
const tableSqlRow = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='memories'").get() as { sql?: string } | undefined;
|
|
981
|
+
const tableSql = tableSqlRow?.sql ?? '';
|
|
982
|
+
if (!tableSql) return;
|
|
983
|
+
|
|
984
|
+
// Legacy schema allowed only memory/user. New schema must allow failure too.
|
|
985
|
+
const hasLegacyTargetCheck = /target\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*target\s+IN\s*\(\s*'memory'\s*,\s*'user'\s*\)\s*\)/i.test(tableSql);
|
|
986
|
+
if (!hasLegacyTargetCheck) return;
|
|
987
|
+
|
|
988
|
+
if (!db.transaction) {
|
|
989
|
+
db.exec('PRAGMA foreign_keys = OFF');
|
|
990
|
+
try {
|
|
991
|
+
db.exec('BEGIN IMMEDIATE');
|
|
992
|
+
db.exec(`
|
|
993
|
+
CREATE TABLE memories_new (
|
|
994
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
995
|
+
project TEXT,
|
|
996
|
+
target TEXT NOT NULL CHECK (target IN ('memory', 'user', 'failure')),
|
|
997
|
+
category TEXT CHECK (category IN ('failure', 'correction', 'insight', 'preference', 'convention', 'tool-quirk')),
|
|
998
|
+
content TEXT NOT NULL,
|
|
999
|
+
failure_reason TEXT,
|
|
1000
|
+
tool_state TEXT,
|
|
1001
|
+
corrected_to TEXT,
|
|
1002
|
+
created DATE NOT NULL,
|
|
1003
|
+
last_referenced DATE NOT NULL
|
|
1004
|
+
);
|
|
1005
|
+
`);
|
|
1006
|
+
|
|
1007
|
+
db.exec(`
|
|
1008
|
+
INSERT INTO memories_new (id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced)
|
|
1009
|
+
SELECT id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced
|
|
1010
|
+
FROM memories;
|
|
1011
|
+
`);
|
|
1012
|
+
|
|
1013
|
+
db.exec('DROP TABLE memories');
|
|
1014
|
+
db.exec('ALTER TABLE memories_new RENAME TO memories');
|
|
1015
|
+
db.exec('COMMIT');
|
|
1016
|
+
} catch (err) {
|
|
1017
|
+
db.exec('ROLLBACK');
|
|
1018
|
+
throw err;
|
|
1019
|
+
} finally {
|
|
1020
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
1021
|
+
}
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const tx = db.transaction(() => {
|
|
1026
|
+
db.exec(`
|
|
1027
|
+
CREATE TABLE memories_new (
|
|
1028
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1029
|
+
project TEXT,
|
|
1030
|
+
target TEXT NOT NULL CHECK (target IN ('memory', 'user', 'failure')),
|
|
1031
|
+
category TEXT CHECK (category IN ('failure', 'correction', 'insight', 'preference', 'convention', 'tool-quirk')),
|
|
1032
|
+
content TEXT NOT NULL,
|
|
1033
|
+
failure_reason TEXT,
|
|
1034
|
+
tool_state TEXT,
|
|
1035
|
+
corrected_to TEXT,
|
|
1036
|
+
created DATE NOT NULL,
|
|
1037
|
+
last_referenced DATE NOT NULL
|
|
1038
|
+
);
|
|
1039
|
+
`);
|
|
1040
|
+
|
|
1041
|
+
db.exec(`
|
|
1042
|
+
INSERT INTO memories_new (id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced)
|
|
1043
|
+
SELECT id, project, target, category, content, failure_reason, tool_state, corrected_to, created, last_referenced
|
|
1044
|
+
FROM memories;
|
|
1045
|
+
`);
|
|
1046
|
+
|
|
1047
|
+
db.exec('DROP TABLE memories');
|
|
1048
|
+
db.exec('ALTER TABLE memories_new RENAME TO memories');
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
db.exec('PRAGMA foreign_keys = OFF');
|
|
1052
|
+
try {
|
|
1053
|
+
tx();
|
|
1054
|
+
} finally {
|
|
1055
|
+
db.exec('PRAGMA foreign_keys = ON');
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* Upgrade existing unicode61 FTS tables to the trigram tokenizer.
|
|
1061
|
+
*
|
|
1062
|
+
* `CREATE VIRTUAL TABLE IF NOT EXISTS` cannot change an existing FTS
|
|
1063
|
+
* tokenizer, so this migration drops and recreates both external-content
|
|
1064
|
+
* indexes, then repopulates them from their source tables. The metadata
|
|
1065
|
+
* marker makes the migration versioned and idempotent.
|
|
1066
|
+
*/
|
|
1067
|
+
private migrateFtsTokenizer(db: DatabaseLike): void {
|
|
1068
|
+
const usesTrigram = (tableName: string): boolean => {
|
|
1069
|
+
const row = db.prepare(
|
|
1070
|
+
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?",
|
|
1071
|
+
).get(tableName) as { sql?: string } | undefined;
|
|
1072
|
+
return typeof row?.sql === 'string'
|
|
1073
|
+
&& /\btokenize\s*=\s*['"]trigram['"]/i.test(row.sql);
|
|
1074
|
+
};
|
|
1075
|
+
const migrationComplete = (): boolean => {
|
|
1076
|
+
const versionRow = db.prepare(
|
|
1077
|
+
'SELECT value FROM extension_metadata WHERE key = ?',
|
|
1078
|
+
).get(FTS5_TOKENIZER_VERSION_KEY) as { value?: string } | undefined;
|
|
1079
|
+
return versionRow?.value === FTS5_TOKENIZER_VERSION
|
|
1080
|
+
&& usesTrigram('message_fts')
|
|
1081
|
+
&& usesTrigram('memory_fts');
|
|
1082
|
+
};
|
|
1083
|
+
const isBusy = (error: unknown): boolean => {
|
|
1084
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
1085
|
+
? String(error.code)
|
|
1086
|
+
: '';
|
|
1087
|
+
return code.startsWith('SQLITE_BUSY') || code.startsWith('SQLITE_LOCKED');
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
let lockAttempts = 0;
|
|
1091
|
+
while (!migrationComplete()) {
|
|
1092
|
+
try {
|
|
1093
|
+
db.exec('BEGIN IMMEDIATE');
|
|
1094
|
+
} catch (error) {
|
|
1095
|
+
// Each attempt waits up to busy_timeout. Cap the total attempts so a
|
|
1096
|
+
// permanently held writer cannot hang extension startup forever.
|
|
1097
|
+
if (isBusy(error) && ++lockAttempts < FTS5_MIGRATION_MAX_LOCK_ATTEMPTS) continue;
|
|
1098
|
+
if (isBusy(error)) {
|
|
1099
|
+
throw new Error(
|
|
1100
|
+
`Timed out waiting for the FTS tokenizer migration lock after ${lockAttempts} attempts. `
|
|
1101
|
+
+ "Close the other Pi process and retry.",
|
|
1102
|
+
{ cause: error },
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
try {
|
|
1109
|
+
// Another Pi process may have completed the migration while this
|
|
1110
|
+
// connection waited for the write lock.
|
|
1111
|
+
if (migrationComplete()) {
|
|
1112
|
+
db.exec('COMMIT');
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
db.exec(`
|
|
1116
|
+
DROP TABLE IF EXISTS message_fts;
|
|
1117
|
+
DROP TABLE IF EXISTS memory_fts;
|
|
1118
|
+
${FTS5_TRIGRAM_TABLES.message};
|
|
1119
|
+
${FTS5_TRIGRAM_TABLES.memory};
|
|
1120
|
+
INSERT INTO message_fts(message_fts) VALUES ('rebuild');
|
|
1121
|
+
INSERT INTO memory_fts(memory_fts) VALUES ('rebuild');
|
|
1122
|
+
`);
|
|
1123
|
+
db.prepare(`
|
|
1124
|
+
INSERT INTO extension_metadata (key, value)
|
|
1125
|
+
VALUES (?, ?)
|
|
1126
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
1127
|
+
`).run(FTS5_TOKENIZER_VERSION_KEY, FTS5_TOKENIZER_VERSION);
|
|
1128
|
+
db.exec('COMMIT');
|
|
1129
|
+
return;
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
try { db.exec('ROLLBACK'); } catch { /* preserve migration error */ }
|
|
1132
|
+
throw error;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Close the database connection.
|
|
1139
|
+
*/
|
|
1140
|
+
close(): void {
|
|
1141
|
+
const db = this.db;
|
|
1142
|
+
if (db) {
|
|
1143
|
+
try {
|
|
1144
|
+
measureLifecycleSync('database.checkpoint', () => db.exec('PRAGMA wal_checkpoint(TRUNCATE)'));
|
|
1145
|
+
} catch { /* best effort */ }
|
|
1146
|
+
try { db.close(); } catch { /* best effort — close may throw on a corrupt handle */ }
|
|
1147
|
+
this.db = null;
|
|
1148
|
+
}
|
|
1149
|
+
this.pendingOpenIntegrityScan = null;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Get the database file path.
|
|
1154
|
+
*/
|
|
1155
|
+
getPath(): string {
|
|
1156
|
+
return this.displayDbPath;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/**
|
|
1160
|
+
* Check if the database file exists.
|
|
1161
|
+
*/
|
|
1162
|
+
exists(): boolean {
|
|
1163
|
+
return fs.existsSync(this.dbPath);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Get stats about the database.
|
|
1168
|
+
*/
|
|
1169
|
+
getStats(): { sessions: number; messages: number; memories: number } {
|
|
1170
|
+
const db = this.getDb();
|
|
1171
|
+
const sessions = db.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number };
|
|
1172
|
+
const messages = db.prepare('SELECT COUNT(*) as count FROM messages').get() as { count: number };
|
|
1173
|
+
const memories = db.prepare('SELECT COUNT(*) as count FROM memories').get() as { count: number };
|
|
1174
|
+
return {
|
|
1175
|
+
sessions: sessions.count,
|
|
1176
|
+
messages: messages.count,
|
|
1177
|
+
memories: memories.count,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
}
|