mirror-mind 0.0.1 → 0.32.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/.claude/hooks/log-session-end.sh +85 -0
- package/.claude/hooks/log-user-prompt.sh +85 -0
- package/.claude/hooks/mirror-inject.sh +85 -0
- package/.claude/hooks/session-start.sh +85 -0
- package/.gemini/hooks/log-assistant.sh +85 -0
- package/.gemini/hooks/log-user.sh +85 -0
- package/.gemini/hooks/session-end.sh +85 -0
- package/.gemini/hooks/session-start.sh +85 -0
- package/.pi/extensions/mirror-logger.ts +561 -0
- package/.pi/skills/mm-backup/SKILL.md +21 -0
- package/.pi/skills/mm-build/SKILL.md +1206 -0
- package/.pi/skills/mm-consolidate/SKILL.md +112 -0
- package/.pi/skills/mm-consult/SKILL.md +70 -0
- package/.pi/skills/mm-conversations/SKILL.md +16 -0
- package/.pi/skills/mm-discard/SKILL.md +39 -0
- package/.pi/skills/mm-explore/SKILL.md +255 -0
- package/.pi/skills/mm-help/SKILL.md +74 -0
- package/.pi/skills/mm-identity/SKILL.md +96 -0
- package/.pi/skills/mm-journal/SKILL.md +16 -0
- package/.pi/skills/mm-journey/SKILL.md +23 -0
- package/.pi/skills/mm-journeys/SKILL.md +15 -0
- package/.pi/skills/mm-memories/SKILL.md +17 -0
- package/.pi/skills/mm-mirror/SKILL.md +98 -0
- package/.pi/skills/mm-mute/SKILL.md +18 -0
- package/.pi/skills/mm-new/SKILL.md +18 -0
- package/.pi/skills/mm-recall/SKILL.md +17 -0
- package/.pi/skills/mm-release-notes/SKILL.md +52 -0
- package/.pi/skills/mm-seed/SKILL.md +19 -0
- package/.pi/skills/mm-shadow/SKILL.md +117 -0
- package/.pi/skills/mm-soul/SKILL.md +502 -0
- package/.pi/skills/mm-tasks/SKILL.md +24 -0
- package/.pi/skills/mm-update/SKILL.md +32 -0
- package/.pi/skills/mm-week/SKILL.md +28 -0
- package/.pi/skills/mm-welcome/SKILL.md +24 -0
- package/AGENTS.md +188 -0
- package/README.md +126 -9
- package/bin/loader.js +67 -0
- package/bin/mirror-hook.js +7 -0
- package/bin/mirror.js +7 -0
- package/docs/releases/index.md +99 -0
- package/docs/releases/pending-cutoffs.md +510 -0
- package/docs/releases/v0.10.0.md +123 -0
- package/docs/releases/v0.10.1.md +66 -0
- package/docs/releases/v0.10.10.md +51 -0
- package/docs/releases/v0.10.11.md +39 -0
- package/docs/releases/v0.10.12.md +57 -0
- package/docs/releases/v0.10.13.md +35 -0
- package/docs/releases/v0.10.14.md +42 -0
- package/docs/releases/v0.10.15.md +53 -0
- package/docs/releases/v0.10.16.md +35 -0
- package/docs/releases/v0.10.17.md +43 -0
- package/docs/releases/v0.10.2.md +64 -0
- package/docs/releases/v0.10.3.md +78 -0
- package/docs/releases/v0.10.4.md +42 -0
- package/docs/releases/v0.10.5.md +57 -0
- package/docs/releases/v0.10.6.md +47 -0
- package/docs/releases/v0.10.7.md +53 -0
- package/docs/releases/v0.10.8.md +49 -0
- package/docs/releases/v0.10.9.md +37 -0
- package/docs/releases/v0.11.0.md +86 -0
- package/docs/releases/v0.12.0.md +67 -0
- package/docs/releases/v0.13.0.md +64 -0
- package/docs/releases/v0.14.0.md +68 -0
- package/docs/releases/v0.15.0.md +48 -0
- package/docs/releases/v0.16.0.md +50 -0
- package/docs/releases/v0.16.1.md +55 -0
- package/docs/releases/v0.16.2.md +36 -0
- package/docs/releases/v0.17.0.md +50 -0
- package/docs/releases/v0.17.1.md +58 -0
- package/docs/releases/v0.17.2.md +42 -0
- package/docs/releases/v0.17.3.md +58 -0
- package/docs/releases/v0.18.0.md +84 -0
- package/docs/releases/v0.18.1.md +51 -0
- package/docs/releases/v0.18.2.md +69 -0
- package/docs/releases/v0.19.0.md +85 -0
- package/docs/releases/v0.19.1.md +46 -0
- package/docs/releases/v0.20.0.md +85 -0
- package/docs/releases/v0.21.0.md +61 -0
- package/docs/releases/v0.21.1.md +47 -0
- package/docs/releases/v0.22.0.md +77 -0
- package/docs/releases/v0.22.1.md +42 -0
- package/docs/releases/v0.23.0.md +105 -0
- package/docs/releases/v0.24.0.md +125 -0
- package/docs/releases/v0.25.0.md +107 -0
- package/docs/releases/v0.26.0.md +139 -0
- package/docs/releases/v0.26.1.md +65 -0
- package/docs/releases/v0.26.2.md +43 -0
- package/docs/releases/v0.27.0.md +63 -0
- package/docs/releases/v0.28.0.md +59 -0
- package/docs/releases/v0.29.0.md +61 -0
- package/docs/releases/v0.29.1.md +59 -0
- package/docs/releases/v0.29.2.md +59 -0
- package/docs/releases/v0.29.3.md +93 -0
- package/docs/releases/v0.30.0.md +75 -0
- package/docs/releases/v0.30.1.md +93 -0
- package/docs/releases/v0.30.2.md +66 -0
- package/docs/releases/v0.31.0.md +122 -0
- package/docs/releases/v0.31.1.md +142 -0
- package/docs/releases/v0.31.10.md +118 -0
- package/docs/releases/v0.31.11.md +88 -0
- package/docs/releases/v0.31.12.md +105 -0
- package/docs/releases/v0.31.13.md +99 -0
- package/docs/releases/v0.31.14.md +97 -0
- package/docs/releases/v0.31.2.md +115 -0
- package/docs/releases/v0.31.3.md +142 -0
- package/docs/releases/v0.31.4.md +117 -0
- package/docs/releases/v0.31.5.md +155 -0
- package/docs/releases/v0.31.6.md +132 -0
- package/docs/releases/v0.31.7.md +132 -0
- package/docs/releases/v0.31.8.md +120 -0
- package/docs/releases/v0.31.9.md +120 -0
- package/docs/releases/v0.32.0.md +261 -0
- package/docs/releases/v0.32.1.md +59 -0
- package/docs/releases/v0.8.0.md +118 -0
- package/docs/releases/v0.9.0.md +98 -0
- package/docs/releases/v0.9.1.md +83 -0
- package/package.json +102 -6
- package/plugins/mirror-mind/.claude-plugin/plugin.json +13 -0
- package/plugins/mirror-mind/hooks/hooks.json +50 -0
- package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
- package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
- package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
- package/plugins/mirror-mind/hooks/session-start.sh +98 -0
- package/plugins/mirror-mind/mcp/launch.sh +52 -0
- package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
- package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
- package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
- package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
- package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
- package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
- package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
- package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
- package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
- package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
- package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
- package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
- package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
- package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +65 -0
- package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
- package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
- package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
- package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
- package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
- package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
- package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
- package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
- package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
- package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
- package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
- package/scripts/codex-hooks/session-end.sh +85 -0
- package/scripts/codex-hooks/session-start.sh +85 -0
- package/scripts/codex-mirror.sh +48 -0
- package/templates/identity/README.md +119 -0
- package/templates/identity/ego/behavior.yaml +52 -0
- package/templates/identity/ego/constraints.yaml +15 -0
- package/templates/identity/ego/identity.yaml +43 -0
- package/templates/identity/journeys/personal-growth.yaml +39 -0
- package/templates/identity/organization/identity.yaml +24 -0
- package/templates/identity/organization/principles.yaml +17 -0
- package/templates/identity/personas/coach.yaml +94 -0
- package/templates/identity/personas/designer.yaml +93 -0
- package/templates/identity/personas/doctor.yaml +96 -0
- package/templates/identity/personas/engineer.yaml +77 -0
- package/templates/identity/personas/financial.yaml +97 -0
- package/templates/identity/personas/prompt-engineer.yaml +122 -0
- package/templates/identity/personas/researcher.yaml +88 -0
- package/templates/identity/personas/strategist.yaml +91 -0
- package/templates/identity/personas/teacher.yaml +95 -0
- package/templates/identity/personas/therapist.yaml +91 -0
- package/templates/identity/personas/thinker.yaml +74 -0
- package/templates/identity/personas/writer.yaml +76 -0
- package/templates/identity/self/config.yaml +16 -0
- package/templates/identity/self/soul.yaml +105 -0
- package/templates/identity/user/identity.yaml +30 -0
- package/ts/src/backup/staging.ts +62 -0
- package/ts/src/backup/zipBackup.ts +267 -0
- package/ts/src/backup/zipReader.ts +109 -0
- package/ts/src/backup/zipWriter.ts +148 -0
- package/ts/src/builder/activeCheckpoint.ts +134 -0
- package/ts/src/builder/aggregateStatus.ts +34 -0
- package/ts/src/builder/approve.ts +149 -0
- package/ts/src/builder/argv.ts +292 -0
- package/ts/src/builder/ariadMethod.ts +536 -0
- package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
- package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
- package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
- package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
- package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
- package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
- package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
- package/ts/src/builder/artifacts/storyFiles.ts +61 -0
- package/ts/src/builder/artifacts/storyIndex.ts +166 -0
- package/ts/src/builder/cadence.ts +59 -0
- package/ts/src/builder/card.ts +192 -0
- package/ts/src/builder/checkpointRefused.ts +66 -0
- package/ts/src/builder/closure.ts +964 -0
- package/ts/src/builder/commands.ts +2053 -0
- package/ts/src/builder/cursorPosition.ts +176 -0
- package/ts/src/builder/cursorTransitions.ts +77 -0
- package/ts/src/builder/deliveryCursor.ts +577 -0
- package/ts/src/builder/deliveryStoryClosure.ts +521 -0
- package/ts/src/builder/deliveryStoryPlan.ts +536 -0
- package/ts/src/builder/deliveryStoryReady.ts +160 -0
- package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
- package/ts/src/builder/expand.ts +336 -0
- package/ts/src/builder/flowUnit.ts +339 -0
- package/ts/src/builder/homeSurface.ts +122 -0
- package/ts/src/builder/implementationGuard.ts +125 -0
- package/ts/src/builder/index.ts +11 -0
- package/ts/src/builder/lifecycleRefusal.ts +92 -0
- package/ts/src/builder/lifecycleRibbon.ts +159 -0
- package/ts/src/builder/load.ts +313 -0
- package/ts/src/builder/methodAdoption.ts +113 -0
- package/ts/src/builder/methodDefinition.ts +429 -0
- package/ts/src/builder/methodInspection.ts +371 -0
- package/ts/src/builder/plan.ts +408 -0
- package/ts/src/builder/planPreauthorization.ts +265 -0
- package/ts/src/builder/prepare.ts +225 -0
- package/ts/src/builder/projectPaths.ts +64 -0
- package/ts/src/builder/pull.ts +356 -0
- package/ts/src/builder/pullCandidates.ts +463 -0
- package/ts/src/builder/pullCandidatesRender.ts +238 -0
- package/ts/src/builder/refinementField.ts +93 -0
- package/ts/src/builder/releaseIntent.ts +173 -0
- package/ts/src/builder/resumeState.ts +84 -0
- package/ts/src/builder/resumeSurface.ts +149 -0
- package/ts/src/builder/roadmapGrammar.ts +300 -0
- package/ts/src/builder/roadmapScan.ts +97 -0
- package/ts/src/builder/roadmapScope.ts +210 -0
- package/ts/src/builder/scopePhrases.ts +146 -0
- package/ts/src/builder/storyPaths.ts +196 -0
- package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
- package/ts/src/builder/surfaceProtocol.ts +42 -0
- package/ts/src/builder/templateGeneration.ts +148 -0
- package/ts/src/builder/transition.ts +178 -0
- package/ts/src/consult/args.ts +99 -0
- package/ts/src/consult/core.ts +122 -0
- package/ts/src/consult/modelCatalog.ts +55 -0
- package/ts/src/consult/render.ts +47 -0
- package/ts/src/conversation/append.ts +414 -0
- package/ts/src/conversation/backfill.ts +387 -0
- package/ts/src/conversation/closeTail.ts +467 -0
- package/ts/src/conversation/extraction.ts +333 -0
- package/ts/src/conversation/extractionDriver.ts +130 -0
- package/ts/src/conversation/extractionRun.ts +70 -0
- package/ts/src/conversation/journeyInference.ts +165 -0
- package/ts/src/conversation/journeyRepair.ts +114 -0
- package/ts/src/conversation/lifecycleFaces.ts +197 -0
- package/ts/src/conversation/lifecycleWrites.ts +313 -0
- package/ts/src/conversation/listing.ts +77 -0
- package/ts/src/conversation/logger.ts +576 -0
- package/ts/src/conversation/loggerCli.ts +282 -0
- package/ts/src/conversation/loggerRuntime.ts +259 -0
- package/ts/src/conversation/metadataLifecycle.ts +443 -0
- package/ts/src/conversation/recall.ts +66 -0
- package/ts/src/conversation/sessionComposites.ts +258 -0
- package/ts/src/conversation/sessionImport.ts +90 -0
- package/ts/src/conversation/transcriptBackfill.ts +141 -0
- package/ts/src/cultivation/applyActions.ts +241 -0
- package/ts/src/cultivation/cluster.ts +86 -0
- package/ts/src/cultivation/consolidationStore.ts +281 -0
- package/ts/src/cultivation/promptContext.ts +62 -0
- package/ts/src/cultivation/propose.ts +305 -0
- package/ts/src/cultivation/scan.ts +155 -0
- package/ts/src/db/backupGate.ts +47 -0
- package/ts/src/db/bootstrap.ts +108 -0
- package/ts/src/db/bootstrapLock.ts +223 -0
- package/ts/src/db/copyGuard.ts +60 -0
- package/ts/src/db/database.ts +397 -0
- package/ts/src/db/decode.ts +66 -0
- package/ts/src/db/ftsIntegrity.ts +41 -0
- package/ts/src/db/migrateOnOpen.ts +214 -0
- package/ts/src/db/migrations.ts +576 -0
- package/ts/src/db/readyOnOpen.ts +35 -0
- package/ts/src/db/rowDecode.ts +42 -0
- package/ts/src/db/schema.ts +432 -0
- package/ts/src/db/schemaInventory.ts +225 -0
- package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
- package/ts/src/db/schemaState.ts +91 -0
- package/ts/src/db/schemaTsDivergence.ts +114 -0
- package/ts/src/descriptor/descriptorRead.ts +49 -0
- package/ts/src/docs/docsLint.ts +265 -0
- package/ts/src/explorer/handoff.ts +522 -0
- package/ts/src/explorer/render.ts +335 -0
- package/ts/src/explorer/story.ts +769 -0
- package/ts/src/explorer/transition.ts +72 -0
- package/ts/src/extensions/bindings.ts +170 -0
- package/ts/src/extensions/catalog.ts +249 -0
- package/ts/src/extensions/catalogCommands.ts +513 -0
- package/ts/src/extensions/catalogWrites.ts +652 -0
- package/ts/src/extensions/contextRuntime.ts +325 -0
- package/ts/src/extensions/dispatch.ts +374 -0
- package/ts/src/extensions/errors.ts +24 -0
- package/ts/src/extensions/manifest.ts +356 -0
- package/ts/src/extensions/migrations.ts +350 -0
- package/ts/src/extraction/conversation.ts +304 -0
- package/ts/src/extraction/conversationMetadata.ts +206 -0
- package/ts/src/extraction/fencing.ts +106 -0
- package/ts/src/extraction/json.ts +17 -0
- package/ts/src/extraction/prompts.ts +470 -0
- package/ts/src/frontDoor/args.ts +30 -0
- package/ts/src/frontDoor/argvShape.ts +106 -0
- package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
- package/ts/src/frontDoor/buildRoute.ts +398 -0
- package/ts/src/frontDoor/cli.ts +1865 -0
- package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
- package/ts/src/frontDoor/consultRoute.ts +50 -0
- package/ts/src/frontDoor/contentTailRoute.ts +239 -0
- package/ts/src/frontDoor/conversationLoggerRoute.ts +73 -0
- package/ts/src/frontDoor/cultivationRoute.ts +210 -0
- package/ts/src/frontDoor/dbPath.ts +131 -0
- package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
- package/ts/src/frontDoor/exploreRoute.ts +572 -0
- package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
- package/ts/src/frontDoor/frontDoorLog.ts +57 -0
- package/ts/src/frontDoor/identityWrite.ts +36 -0
- package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
- package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
- package/ts/src/frontDoor/liveBackup.ts +100 -0
- package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
- package/ts/src/frontDoor/namedRefusal.ts +35 -0
- package/ts/src/frontDoor/nodeSupport.ts +24 -0
- package/ts/src/frontDoor/render/consolidate.ts +199 -0
- package/ts/src/frontDoor/render/conversations.ts +33 -0
- package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
- package/ts/src/frontDoor/render/descriptor.ts +20 -0
- package/ts/src/frontDoor/render/detectPersona.ts +50 -0
- package/ts/src/frontDoor/render/icons.ts +13 -0
- package/ts/src/frontDoor/render/identity.ts +59 -0
- package/ts/src/frontDoor/render/identityRows.ts +17 -0
- package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
- package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
- package/ts/src/frontDoor/render/journeys.ts +79 -0
- package/ts/src/frontDoor/render/list.ts +41 -0
- package/ts/src/frontDoor/render/memories.ts +67 -0
- package/ts/src/frontDoor/render/recall.ts +49 -0
- package/ts/src/frontDoor/render/shadow.ts +155 -0
- package/ts/src/frontDoor/render/tasks.ts +99 -0
- package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
- package/ts/src/frontDoor/render/week.ts +113 -0
- package/ts/src/frontDoor/routing.ts +999 -0
- package/ts/src/frontDoor/runtimeRoute.ts +623 -0
- package/ts/src/frontDoor/searchRoute.ts +166 -0
- package/ts/src/frontDoor/seedPaths.ts +98 -0
- package/ts/src/frontDoor/soulRoute.ts +433 -0
- package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
- package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
- package/ts/src/frontDoor/usage.ts +176 -0
- package/ts/src/guards/claudePlugin.ts +252 -0
- package/ts/src/guards/packContents.ts +306 -0
- package/ts/src/guards/piSourcedSkills.ts +70 -0
- package/ts/src/guards/retiredRouteShadows.ts +57 -0
- package/ts/src/guards/retiredSurfaces.ts +564 -0
- package/ts/src/hooks/claude.ts +176 -0
- package/ts/src/hooks/codex.ts +45 -0
- package/ts/src/hooks/gemini.ts +126 -0
- package/ts/src/hooks/main.ts +109 -0
- package/ts/src/hooks/mirrorState.ts +71 -0
- package/ts/src/hooks/payload.ts +68 -0
- package/ts/src/hooks/runtime.ts +159 -0
- package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
- package/ts/src/identity/descriptorGenerate.ts +162 -0
- package/ts/src/identity/identityEdit.ts +143 -0
- package/ts/src/identity/identityKey.ts +77 -0
- package/ts/src/identity/identityRead.ts +69 -0
- package/ts/src/identity/identityStore.ts +80 -0
- package/ts/src/identity/journeyListing.ts +53 -0
- package/ts/src/identity/personaInspect.ts +44 -0
- package/ts/src/identity/personaListing.ts +37 -0
- package/ts/src/identity/setIdentity.ts +64 -0
- package/ts/src/index.ts +231 -0
- package/ts/src/init/init.ts +111 -0
- package/ts/src/journey/journeyOptions.ts +131 -0
- package/ts/src/journey/journeyRemoval.ts +158 -0
- package/ts/src/journey/journeyStatus.ts +149 -0
- package/ts/src/journey/journeySyncFile.ts +87 -0
- package/ts/src/journey/journeyWrite.ts +203 -0
- package/ts/src/journey/parentJourney.ts +41 -0
- package/ts/src/journey/validateParentJourney.ts +55 -0
- package/ts/src/mcp/boundary.ts +151 -0
- package/ts/src/mcp/guards.ts +244 -0
- package/ts/src/mcp/main.ts +121 -0
- package/ts/src/mcp/payload.ts +141 -0
- package/ts/src/mcp/protocol.ts +141 -0
- package/ts/src/mcp/readModels.ts +241 -0
- package/ts/src/mcp/registry.ts +201 -0
- package/ts/src/mcp/serve.ts +120 -0
- package/ts/src/mcp/tools/deterministic.ts +137 -0
- package/ts/src/mcp/tools/providerCrossing.ts +187 -0
- package/ts/src/mcp/wire.ts +8 -0
- package/ts/src/memory/journal.ts +180 -0
- package/ts/src/memory/listing.ts +107 -0
- package/ts/src/memory/memoryWrite.ts +88 -0
- package/ts/src/memory/reinforcement.ts +35 -0
- package/ts/src/mirror/context.ts +161 -0
- package/ts/src/mirror/defaultResolution.ts +199 -0
- package/ts/src/mirror/orchestration.ts +364 -0
- package/ts/src/mirror/reception.ts +194 -0
- package/ts/src/mirror/render.ts +75 -0
- package/ts/src/mirror/runtimeSession.ts +205 -0
- package/ts/src/mode/operatingMode.ts +148 -0
- package/ts/src/observability/callOutcome.ts +115 -0
- package/ts/src/observability/inspectLedger.ts +329 -0
- package/ts/src/observability/ledgerHooks.ts +126 -0
- package/ts/src/observability/llmCalls.ts +154 -0
- package/ts/src/persona/detectPersona.ts +81 -0
- package/ts/src/planning/promptAssembly.ts +111 -0
- package/ts/src/planning/weekPending.ts +81 -0
- package/ts/src/planning/weekPlan.ts +183 -0
- package/ts/src/planning/weekSave.ts +117 -0
- package/ts/src/providers/config.ts +189 -0
- package/ts/src/providers/cost.ts +64 -0
- package/ts/src/providers/credits.ts +204 -0
- package/ts/src/providers/embedding.ts +321 -0
- package/ts/src/providers/familyProviders.ts +147 -0
- package/ts/src/providers/llm.ts +334 -0
- package/ts/src/providers/openrouter.ts +244 -0
- package/ts/src/providers/redaction.ts +110 -0
- package/ts/src/providers/replay.ts +15 -0
- package/ts/src/providers/transport.ts +410 -0
- package/ts/src/repair/encodingRepair.ts +253 -0
- package/ts/src/runtime/backup.ts +151 -0
- package/ts/src/runtime/channel.ts +146 -0
- package/ts/src/runtime/config.ts +168 -0
- package/ts/src/runtime/diagnose.ts +686 -0
- package/ts/src/runtime/git.ts +510 -0
- package/ts/src/runtime/installKind.ts +220 -0
- package/ts/src/runtime/migrate.ts +154 -0
- package/ts/src/runtime/packageIdentity.ts +102 -0
- package/ts/src/runtime/releaseNotes.ts +244 -0
- package/ts/src/runtime/status.ts +557 -0
- package/ts/src/runtime/strategies/clone.ts +200 -0
- package/ts/src/runtime/strategies/package.ts +328 -0
- package/ts/src/runtime/treeRoot.ts +30 -0
- package/ts/src/runtime/update.ts +445 -0
- package/ts/src/runtime/updateGate.ts +40 -0
- package/ts/src/runtime/updatePipeline.ts +159 -0
- package/ts/src/runtime/version.ts +36 -0
- package/ts/src/runtime/wiring.ts +130 -0
- package/ts/src/search/memorySearch.ts +338 -0
- package/ts/src/search/ranker.ts +154 -0
- package/ts/src/seed/seed.ts +256 -0
- package/ts/src/seed/seedYaml.ts +133 -0
- package/ts/src/soul/apply.ts +218 -0
- package/ts/src/soul/harvest.ts +267 -0
- package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
- package/ts/src/soul/prompts/soul_self_voice.md +219 -0
- package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
- package/ts/src/soul/prompts.ts +85 -0
- package/ts/src/soul/render.ts +358 -0
- package/ts/src/soul/state.ts +212 -0
- package/ts/src/soul/transition.ts +37 -0
- package/ts/src/tasks/journeyPathParse.ts +113 -0
- package/ts/src/tasks/taskImportSync.ts +142 -0
- package/ts/src/tasks/taskStore.ts +312 -0
- package/ts/src/tasks/weekView.ts +120 -0
- package/ts/src/util/clipCodePoints.ts +26 -0
- package/ts/src/util/paths.ts +63 -0
- package/ts/src/util/program.ts +9 -0
- package/ts/src/util/pyGenerators.ts +144 -0
- package/ts/src/util/pythonText.ts +317 -0
- package/ts/src/util/semver.ts +45 -0
- package/ts/src/util/shellWord.ts +25 -0
- package/ts/src/util/slug.ts +64 -0
- package/ts/src/welcome/card.ts +339 -0
- package/ts/src/welcome/statusLine.ts +101 -0
- package/ts/src/welcome/updateCache.ts +129 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Cross-process bootstrap lock — CV22.DS6.TS3.
|
|
2
|
+
//
|
|
3
|
+
// TS equivalent of Python's sibling `.bootstrap.lock` + `fcntl.flock(LOCK_EX)`
|
|
4
|
+
// (`src/memory/db/connection.py::_bootstrap_lock`). Serializes the bootstrap
|
|
5
|
+
// phase (pragma discipline + migrations + schema DDL) for one database path
|
|
6
|
+
// across concurrent *processes*.
|
|
7
|
+
//
|
|
8
|
+
// Zero-dependency by design (Navigator decision, CV22.DS6.TS3 plan): no native
|
|
9
|
+
// `flock` addon, because CV22's endgame is a single-language npm package. The
|
|
10
|
+
// primitive is an atomic exclusive link: the holder's record is written to a
|
|
11
|
+
// file only it names, then `link()`ed to the lock path, which fails with
|
|
12
|
+
// `EEXIST` while any lock is there. Every POSIX and Windows filesystem Node
|
|
13
|
+
// targets treats that as atomic -- exactly one caller wins a given path.
|
|
14
|
+
//
|
|
15
|
+
// Why link and not `O_CREAT|O_EXCL` (CR084). Creating the lock file and then
|
|
16
|
+
// writing its record were two syscalls, and between them the lock existed
|
|
17
|
+
// EMPTY. A contender that read it then took "unreadable" as abandonment,
|
|
18
|
+
// removed the live holder's lock, and took its own: two holders, two migration
|
|
19
|
+
// runs, two backups racing on one path. It surfaced as migrateOnOpenConcurrency's
|
|
20
|
+
// intermittent red, on CI three times. With the record in place before the path
|
|
21
|
+
// exists, no live holder's lock is ever unreadable -- so unreadable content is no
|
|
22
|
+
// longer evidence of abandonment, and ages out by the file's mtime instead.
|
|
23
|
+
//
|
|
24
|
+
// The one honest gap against `fcntl.flock`: an OS-level flock is released
|
|
25
|
+
// automatically when its holding process dies, no matter how. A lock *file*
|
|
26
|
+
// is not — it survives a crash unless something notices and removes it. This
|
|
27
|
+
// module's accepted parity equivalent (Navigator decision) is stale-lock
|
|
28
|
+
// reclamation: a lock is considered abandoned, and safe to reclaim, when its
|
|
29
|
+
// recorded holder process is no longer alive (`process.kill(pid, 0)` failing
|
|
30
|
+
// with ESRCH) or its record is older than `staleMs`. Reclamation is a trust
|
|
31
|
+
// decision, not a hard guarantee — see the security review recorded in this
|
|
32
|
+
// story's plan: prefer waiting too long over breaking a live lock.
|
|
33
|
+
//
|
|
34
|
+
// Removal, by a release or a reclaim, checks that the lock at the path is still
|
|
35
|
+
// the one it judged (CR084): otherwise a slow process could free a lock another
|
|
36
|
+
// process acquired in between. A read-then-remove leaves a window of one read
|
|
37
|
+
// and one unlink, far narrower than the judge-then-remove it replaces; closing
|
|
38
|
+
// it entirely would need an OS lock, which this module chose not to depend on.
|
|
39
|
+
|
|
40
|
+
import { randomBytes } from "node:crypto";
|
|
41
|
+
import { linkSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
42
|
+
|
|
43
|
+
/** Raised when a lock cannot be acquired within the configured timeout. */
|
|
44
|
+
export class BootstrapLockTimeoutError extends Error {}
|
|
45
|
+
|
|
46
|
+
export interface BootstrapLockOptions {
|
|
47
|
+
/** Total time to wait for a live lock before failing closed. Default 30s. */
|
|
48
|
+
timeoutMs?: number;
|
|
49
|
+
/** Age after which a dead-holder, aged, or unreadable lock is reclaimed. Default 30s. */
|
|
50
|
+
staleMs?: number;
|
|
51
|
+
/** Backoff between contention retries. Default 50ms. */
|
|
52
|
+
pollIntervalMs?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BootstrapLockHandle {
|
|
56
|
+
/** Release the lock. Idempotent; safe to call once, always, in a `finally`. */
|
|
57
|
+
release(): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
61
|
+
const DEFAULT_STALE_MS = 30_000;
|
|
62
|
+
const DEFAULT_POLL_INTERVAL_MS = 50;
|
|
63
|
+
|
|
64
|
+
interface LockRecord {
|
|
65
|
+
pid: number;
|
|
66
|
+
createdAt: number;
|
|
67
|
+
/** Tells apart two locks one process takes within the same millisecond. */
|
|
68
|
+
nonce?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function lockPathFor(dbPath: string): string {
|
|
72
|
+
return `${dbPath}.bootstrap.lock`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Synchronous sleep — bootstrap runs once at startup, before anything is
|
|
76
|
+
* served, so blocking the event loop here is an accepted, bootstrap-only
|
|
77
|
+
* exception (see the engineer review in this story's plan). Do not reuse
|
|
78
|
+
* this pattern on any runtime request path. */
|
|
79
|
+
function sleepSync(ms: number): void {
|
|
80
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function isProcessAlive(pid: number): boolean {
|
|
84
|
+
try {
|
|
85
|
+
process.kill(pid, 0);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
89
|
+
// ESRCH: no such process — definitively dead. EPERM: exists, owned by
|
|
90
|
+
// someone else — treat as alive (fail toward waiting, not reclaiming).
|
|
91
|
+
return code !== "ESRCH";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readLockRecord(lockPath: string): LockRecord | null {
|
|
96
|
+
try {
|
|
97
|
+
const raw = readFileSync(lockPath, "utf8");
|
|
98
|
+
const parsed = JSON.parse(raw) as Partial<LockRecord>;
|
|
99
|
+
if (typeof parsed.pid !== "number" || typeof parsed.createdAt !== "number") return null;
|
|
100
|
+
return {
|
|
101
|
+
pid: parsed.pid,
|
|
102
|
+
createdAt: parsed.createdAt,
|
|
103
|
+
...(typeof parsed.nonce === "string" ? { nonce: parsed.nonce } : {}),
|
|
104
|
+
};
|
|
105
|
+
} catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function sameRecord(a: LockRecord | null, b: LockRecord | null): boolean {
|
|
111
|
+
if (a === null || b === null) return a === b;
|
|
112
|
+
return a.pid === b.pid && a.createdAt === b.createdAt && a.nonce === b.nonce;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Is the lock at `lockPath` abandoned, and what did it hold when judged?
|
|
117
|
+
*
|
|
118
|
+
* A readable record is abandoned when its holder is dead or it is older than
|
|
119
|
+
* `staleMs`. An unreadable one is corruption or a pre-CR084 leftover -- a live
|
|
120
|
+
* holder's record is in the file before the file is at the path -- so it is
|
|
121
|
+
* judged by the file's own age rather than taken as abandoned on sight.
|
|
122
|
+
*/
|
|
123
|
+
function judgeLock(
|
|
124
|
+
lockPath: string,
|
|
125
|
+
staleMs: number,
|
|
126
|
+
): { abandoned: boolean; record: LockRecord | null } {
|
|
127
|
+
const record = readLockRecord(lockPath);
|
|
128
|
+
if (record !== null) {
|
|
129
|
+
const abandoned = !isProcessAlive(record.pid) || Date.now() - record.createdAt > staleMs;
|
|
130
|
+
return { abandoned, record };
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
return { abandoned: Date.now() - statSync(lockPath).mtimeMs > staleMs, record: null };
|
|
134
|
+
} catch {
|
|
135
|
+
// Gone since the read: nothing to reclaim, and the next create may win.
|
|
136
|
+
return { abandoned: false, record: null };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Remove the lock at `lockPath` only while it still holds `judged` (CR084). */
|
|
141
|
+
function removeLockIfUnchanged(lockPath: string, judged: LockRecord | null): void {
|
|
142
|
+
if (sameRecord(readLockRecord(lockPath), judged)) rmSync(lockPath, { force: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Try to take the lock: write `record` to a file only this call names, then
|
|
147
|
+
* link that file to the lock path. `EEXIST` means a lock is already there.
|
|
148
|
+
* The private file goes either way; the lock path keeps the linked copy.
|
|
149
|
+
*/
|
|
150
|
+
function tryLink(lockPath: string, record: LockRecord): boolean {
|
|
151
|
+
const privatePath = `${lockPath}.${process.pid}-${record.nonce}.tmp`;
|
|
152
|
+
// `wx`: exclusive create, so a file planted at the private name refuses the
|
|
153
|
+
// write instead of being written through (the O_NOFOLLOW guard the previous
|
|
154
|
+
// `openSync` carried, security review for CV22.DS6.TS3).
|
|
155
|
+
writeFileSync(privatePath, JSON.stringify(record), { mode: 0o600, flag: "wx" });
|
|
156
|
+
try {
|
|
157
|
+
linkSync(privatePath, lockPath);
|
|
158
|
+
return true;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
|
|
161
|
+
throw error;
|
|
162
|
+
} finally {
|
|
163
|
+
rmSync(privatePath, { force: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Acquire the cross-process bootstrap lock for `dbPath`, blocking (via bounded
|
|
169
|
+
* synchronous polling) until it is held, a live holder's lock is released, or
|
|
170
|
+
* `timeoutMs` elapses. Reclaims an abandoned lock (dead holder, aged past
|
|
171
|
+
* `staleMs`, or unreadable and aged past `staleMs`) rather than waiting on it
|
|
172
|
+
* forever.
|
|
173
|
+
*/
|
|
174
|
+
export function acquireBootstrapLock(
|
|
175
|
+
dbPath: string,
|
|
176
|
+
options: BootstrapLockOptions = {},
|
|
177
|
+
): BootstrapLockHandle {
|
|
178
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
179
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_MS;
|
|
180
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
181
|
+
const lockPath = lockPathFor(dbPath);
|
|
182
|
+
const deadline = Date.now() + timeoutMs;
|
|
183
|
+
|
|
184
|
+
for (;;) {
|
|
185
|
+
const record: LockRecord = {
|
|
186
|
+
pid: process.pid,
|
|
187
|
+
createdAt: Date.now(),
|
|
188
|
+
nonce: randomBytes(6).toString("hex"),
|
|
189
|
+
};
|
|
190
|
+
if (tryLink(lockPath, record)) {
|
|
191
|
+
let released = false;
|
|
192
|
+
return {
|
|
193
|
+
release: (): void => {
|
|
194
|
+
if (released) return;
|
|
195
|
+
released = true;
|
|
196
|
+
try {
|
|
197
|
+
removeLockIfUnchanged(lockPath, record);
|
|
198
|
+
} catch {
|
|
199
|
+
// Already gone -- nothing of ours left to remove.
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const { abandoned, record: judged } = judgeLock(lockPath, staleMs);
|
|
206
|
+
if (abandoned) {
|
|
207
|
+
try {
|
|
208
|
+
removeLockIfUnchanged(lockPath, judged);
|
|
209
|
+
} catch {
|
|
210
|
+
// Another process reclaimed it first; loop and race for the link again.
|
|
211
|
+
}
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (Date.now() >= deadline) {
|
|
216
|
+
throw new BootstrapLockTimeoutError(
|
|
217
|
+
`timed out after ${timeoutMs}ms waiting for the bootstrap lock at ${lockPath} ` +
|
|
218
|
+
"(another process is bootstrapping this database)",
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
sleepSync(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copy-only write guard for the database seam.
|
|
2
|
+
//
|
|
3
|
+
// A write-parity proof — and any DS4 write applied for validation — mutates its
|
|
4
|
+
// target. It must therefore run only against a *copy* of the database under a
|
|
5
|
+
// `tmp/` directory, never the authors' live `memory.db`. This guard is a db-layer
|
|
6
|
+
// safety primitive so the seam can refuse a dangerous open before the driver
|
|
7
|
+
// touches the file; the parity harness reuses it rather than owning it.
|
|
8
|
+
//
|
|
9
|
+
// The rules run against the RESOLVED filesystem object, not the handed string
|
|
10
|
+
// (CR030): a symlink named `copy.db` pointing at a live database, or a
|
|
11
|
+
// `tmp/../` traversal, must not pass checks that only read the path text.
|
|
12
|
+
|
|
13
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
14
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
15
|
+
|
|
16
|
+
/** Raised when a write path is a live database or otherwise not a safe copy. */
|
|
17
|
+
export class CopyOnlyGuardError extends Error {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the object the write would actually touch. An existing target must
|
|
21
|
+
* be a regular file (symlinks are refused outright — fail closed on
|
|
22
|
+
* indirection) and is fully realpath-resolved. A not-yet-existing target
|
|
23
|
+
* resolves through its parent directory; when even the parent is missing the
|
|
24
|
+
* lexical absolute path is used — dot-dot segments still normalize away, and
|
|
25
|
+
* SQLite cannot create a file under a missing parent anyway.
|
|
26
|
+
*/
|
|
27
|
+
function resolveWriteTarget(path: string): string {
|
|
28
|
+
let isSymlink = false;
|
|
29
|
+
let exists = true;
|
|
30
|
+
try {
|
|
31
|
+
isSymlink = lstatSync(path).isSymbolicLink();
|
|
32
|
+
} catch {
|
|
33
|
+
exists = false;
|
|
34
|
+
}
|
|
35
|
+
if (isSymlink) {
|
|
36
|
+
throw new CopyOnlyGuardError(`write target is a symlink; refusing indirection: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
if (exists) return realpathSync(path);
|
|
39
|
+
try {
|
|
40
|
+
return join(realpathSync(dirname(path)), basename(path));
|
|
41
|
+
} catch {
|
|
42
|
+
return resolve(path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Refuse to operate on anything but a DB copy. Fails closed: a `memory.db`
|
|
48
|
+
* basename, or any resolved path without a `tmp` segment, aborts before any
|
|
49
|
+
* write.
|
|
50
|
+
*/
|
|
51
|
+
export function assertCopyTarget(path: string): void {
|
|
52
|
+
const resolved = resolveWriteTarget(path);
|
|
53
|
+
if (basename(resolved) === "memory.db") {
|
|
54
|
+
throw new CopyOnlyGuardError("refusing to run a write against a live memory.db");
|
|
55
|
+
}
|
|
56
|
+
const segments = resolved.split(/[/\\]/);
|
|
57
|
+
if (!segments.includes("tmp")) {
|
|
58
|
+
throw new CopyOnlyGuardError("write target must be a copy under a tmp/ directory");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// Driver seam for the Mirror Mind TypeScript core.
|
|
2
|
+
//
|
|
3
|
+
// This is the ONLY module allowed to import `node:sqlite`. The rest of the core
|
|
4
|
+
// depends on the `Database` interface below, never on the driver directly, so a
|
|
5
|
+
// future swap to another driver (e.g. better-sqlite3) rewrites only this file.
|
|
6
|
+
// `node:sqlite` is still experimental and emits an ExperimentalWarning to stderr;
|
|
7
|
+
// that is non-fatal and stays out of stdout.
|
|
8
|
+
|
|
9
|
+
import { DatabaseSync, type StatementSync } from "node:sqlite";
|
|
10
|
+
|
|
11
|
+
import { type BackupRecord, requireBackup } from "./backupGate.ts";
|
|
12
|
+
import { assertCopyTarget } from "./copyGuard.ts";
|
|
13
|
+
|
|
14
|
+
/** A SQLite-storable value. Mirrored here so callers don't import driver types. */
|
|
15
|
+
export type SqlValue = null | number | bigint | string | Uint8Array;
|
|
16
|
+
|
|
17
|
+
/** One result row, keyed by column name. */
|
|
18
|
+
export type Row = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
/** A prepared read query. */
|
|
21
|
+
export interface PreparedQuery {
|
|
22
|
+
all(...params: SqlValue[]): Row[];
|
|
23
|
+
get(...params: SqlValue[]): Row | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A read handle over a SQLite file. */
|
|
27
|
+
export interface Database {
|
|
28
|
+
prepare(sql: string): PreparedQuery;
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Connection discipline matching the Python core (`src/memory/db/connection.py`):
|
|
34
|
+
* every connection — read-only included — gets `busy_timeout=30000` so lock
|
|
35
|
+
* contention waits instead of failing instantly, and `foreign_keys=ON` so FK
|
|
36
|
+
* enforcement never depends on an unasserted driver default. The timeout is
|
|
37
|
+
* configurable only so contention tests can use a short one.
|
|
38
|
+
*/
|
|
39
|
+
export interface OpenOptions {
|
|
40
|
+
busyTimeoutMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DEFAULT_BUSY_TIMEOUT_MS = 30_000;
|
|
44
|
+
|
|
45
|
+
function applyConnectionPragmas(driver: DatabaseSync, options: OpenOptions): void {
|
|
46
|
+
driver.exec(`PRAGMA busy_timeout=${options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS}`);
|
|
47
|
+
driver.exec("PRAGMA foreign_keys=ON");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Open a SQLite file read-only. The handle can prepare and run read queries;
|
|
52
|
+
* any write rejects at the driver level, keeping the database-as-seam contract
|
|
53
|
+
* safe against the authors' live `memory.db`.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Wrap a driver statement's read methods with row normalization. `node:sqlite`
|
|
57
|
+
* returns null-prototype row objects; normalizing to plain objects keeps the
|
|
58
|
+
* seam's row shape driver-independent and predictable for every consumer (and
|
|
59
|
+
* for strict equality in tests). Shared by the read-only and writable handles.
|
|
60
|
+
*/
|
|
61
|
+
function readableStatement(statement: StatementSync): PreparedQuery {
|
|
62
|
+
return {
|
|
63
|
+
all: (...params: SqlValue[]): Row[] =>
|
|
64
|
+
(statement.all(...params) as Row[]).map((row) => ({ ...row })),
|
|
65
|
+
get: (...params: SqlValue[]): Row | undefined => {
|
|
66
|
+
const row = statement.get(...params) as Row | undefined;
|
|
67
|
+
return row === undefined ? undefined : { ...row };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function openDatabaseReadOnly(path: string, options: OpenOptions = {}): Database {
|
|
73
|
+
const driver = new DatabaseSync(path, { readOnly: true });
|
|
74
|
+
applyConnectionPragmas(driver, options);
|
|
75
|
+
return {
|
|
76
|
+
prepare: (sql: string): PreparedQuery => readableStatement(driver.prepare(sql)),
|
|
77
|
+
close: (): void => {
|
|
78
|
+
driver.close();
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** `PRAGMA quick_check`'s first verdict: `ok`, or the first problem it reports. */
|
|
84
|
+
function quickCheckVerdict(driver: DatabaseSync): string {
|
|
85
|
+
const rows = driver.prepare("PRAGMA quick_check").all() as Record<string, SqlValue>[];
|
|
86
|
+
const first = rows[0];
|
|
87
|
+
return first === undefined ? "no result" : String(Object.values(first)[0] ?? "");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `PRAGMA quick_check` against a standalone database FILE (CV22.DS10.US2).
|
|
92
|
+
*
|
|
93
|
+
* For verifying a backup: `runtime backup --verify` extracts the archive's
|
|
94
|
+
* `memory.db` to a throwaway copy and asks whether it actually opens, because
|
|
95
|
+
* the oracle verifies entry NAMES and will call 4 KB of zeros a valid backup.
|
|
96
|
+
*
|
|
97
|
+
* It lives here because this module is the only one allowed to touch the
|
|
98
|
+
* driver. The first implementation spawned the `sqlite3` CLI, which was a
|
|
99
|
+
* second SQLite access path and an external binary no user is guaranteed to
|
|
100
|
+
* have; CI found it on the macOS leg.
|
|
101
|
+
*
|
|
102
|
+
* The copy is opened READ-WRITE on purpose. Every archive Mirror writes holds
|
|
103
|
+
* a WAL-mode database (header byte 18 == 2) and carries no `-shm`/`-wal`
|
|
104
|
+
* sidecars, and a WAL database opened read-only cannot create the `-shm` file
|
|
105
|
+
* it needs -- SQLITE_CANTOPEN. The target is a temp copy the caller deletes,
|
|
106
|
+
* never the live database, so allowing SQLite to create sidecars beside it
|
|
107
|
+
* costs nothing and is the only way the check can run at all.
|
|
108
|
+
*/
|
|
109
|
+
export function quickCheckDatabaseFile(path: string): { ok: boolean; verdict: string } {
|
|
110
|
+
let driver: DatabaseSync;
|
|
111
|
+
try {
|
|
112
|
+
driver = new DatabaseSync(path);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return { ok: false, verdict: sqliteMessage(error) };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const verdict = quickCheckVerdict(driver);
|
|
118
|
+
return { ok: verdict === "ok", verdict };
|
|
119
|
+
} catch (error) {
|
|
120
|
+
return { ok: false, verdict: sqliteMessage(error) };
|
|
121
|
+
} finally {
|
|
122
|
+
try {
|
|
123
|
+
driver.close();
|
|
124
|
+
} catch {
|
|
125
|
+
// A database that failed to open has nothing to close.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The driver's own message, never a wrapper's, and never a path. */
|
|
131
|
+
function sqliteMessage(error: unknown): string {
|
|
132
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
+
return message.split("\n")[0] ?? "database could not be opened";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A prepared query that can also execute a write. */
|
|
137
|
+
/** What a write reports back. `node:sqlite` returns bigint counts. */
|
|
138
|
+
export interface RunResult {
|
|
139
|
+
readonly changes: number | bigint;
|
|
140
|
+
readonly lastInsertRowid: number | bigint;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface WritablePreparedQuery extends PreparedQuery {
|
|
144
|
+
/**
|
|
145
|
+
* Returns the driver's result rather than `void` (CV22.DS7.US8 plateau 2).
|
|
146
|
+
*
|
|
147
|
+
* The delivery cursor's compare-and-swap is a conditional `UPDATE … WHERE
|
|
148
|
+
* metadata = ?`, and whether it applied is only observable through
|
|
149
|
+
* `changes` — Python reads `cursor.rowcount` for exactly this. Existing
|
|
150
|
+
* callers that ignore the value are unaffected; widening a return type is
|
|
151
|
+
* not a breaking change.
|
|
152
|
+
*/
|
|
153
|
+
run(...params: SqlValue[]): RunResult;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The narrowest write capability: prepare a statement and run it.
|
|
158
|
+
*
|
|
159
|
+
* Both `WritableDatabase` and the `llm_calls`-only `LedgerAppendDatabase`
|
|
160
|
+
* satisfy this, so append-only writers (the ledger hooks) can depend on what
|
|
161
|
+
* they actually use instead of demanding a handle that can also `exec` DDL.
|
|
162
|
+
*/
|
|
163
|
+
export interface StatementRunner {
|
|
164
|
+
prepare(sql: string): WritablePreparedQuery;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A writable handle over a SQLite *copy*. */
|
|
168
|
+
export interface WritableDatabase extends Database, StatementRunner {
|
|
169
|
+
prepare(sql: string): WritablePreparedQuery;
|
|
170
|
+
exec(sql: string): void;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Wrap a raw driver as the writable seam handle (prepare/exec/close). */
|
|
174
|
+
function writableHandle(driver: DatabaseSync): WritableDatabase {
|
|
175
|
+
return {
|
|
176
|
+
prepare(sql: string): WritablePreparedQuery {
|
|
177
|
+
const statement = driver.prepare(sql);
|
|
178
|
+
return {
|
|
179
|
+
...readableStatement(statement),
|
|
180
|
+
run: (...params: SqlValue[]): RunResult => statement.run(...params),
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
exec: (sql: string): void => {
|
|
184
|
+
driver.exec(sql);
|
|
185
|
+
},
|
|
186
|
+
close: (): void => {
|
|
187
|
+
driver.close();
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Open (creating if absent) a database file for the bootstrap authority
|
|
194
|
+
* (CV22.DS6.TS3). Unlike `openDatabaseCopyForWrite`/`openDatabaseForWrite`,
|
|
195
|
+
* this is permitted against the real `memory.db` path — bootstrap's whole
|
|
196
|
+
* purpose is to create/migrate the live file when TS, not Python, owns
|
|
197
|
+
* custody. Safety here comes from the caller: `bootstrap.ts` always wraps
|
|
198
|
+
* this open in the cross-process bootstrap lock, and every statement run
|
|
199
|
+
* against the result (`runMigrations`, `createSchema`) is idempotent, so a
|
|
200
|
+
* lock-protected re-open is always a safe no-op, never a destructive write.
|
|
201
|
+
*/
|
|
202
|
+
export function openDatabaseForBootstrap(
|
|
203
|
+
path: string,
|
|
204
|
+
options: OpenOptions = {},
|
|
205
|
+
): WritableDatabase {
|
|
206
|
+
const driver = new DatabaseSync(path);
|
|
207
|
+
applyConnectionPragmas(driver, options);
|
|
208
|
+
return writableHandle(driver);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Open a SQLite file for writing — permitted ONLY for a copy. `assertCopyTarget`
|
|
213
|
+
* runs first and throws before the driver touches the file if the path is a live
|
|
214
|
+
* `memory.db` or is not under a `tmp/` directory. This is how DS4 mutates state
|
|
215
|
+
* during parity proofs without ever risking the authors' real database.
|
|
216
|
+
*/
|
|
217
|
+
export function openDatabaseCopyForWrite(
|
|
218
|
+
path: string,
|
|
219
|
+
options: OpenOptions = {},
|
|
220
|
+
): WritableDatabase {
|
|
221
|
+
assertCopyTarget(path);
|
|
222
|
+
const driver = new DatabaseSync(path);
|
|
223
|
+
applyConnectionPragmas(driver, options);
|
|
224
|
+
return writableHandle(driver);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Run `fn` inside an explicit `BEGIN IMMEDIATE` transaction, committing on
|
|
229
|
+
* success and rolling back on any throw. Multi-statement writes must use this
|
|
230
|
+
* so a failure between statements can never leave partial state — the Python
|
|
231
|
+
* core commits such sequences as one transaction (e.g. `log_access`).
|
|
232
|
+
*/
|
|
233
|
+
export function withTransaction<T>(db: WritableDatabase, fn: () => T): T {
|
|
234
|
+
db.exec("BEGIN IMMEDIATE");
|
|
235
|
+
try {
|
|
236
|
+
const result = fn();
|
|
237
|
+
db.exec("COMMIT");
|
|
238
|
+
return result;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
db.exec("ROLLBACK");
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Open a real database for writing — the sanctioned live-write seam (US4). Unlike
|
|
247
|
+
* the copy path, this is allowed to touch a live `memory.db`, so it fails closed
|
|
248
|
+
* without a hash-verified backup: `requireBackup` throws before the driver opens
|
|
249
|
+
* the file if no valid backup was recorded. Used only by the front door's
|
|
250
|
+
* allowlisted deterministic write commands.
|
|
251
|
+
*/
|
|
252
|
+
export function openDatabaseForWrite(
|
|
253
|
+
path: string,
|
|
254
|
+
backup: BackupRecord,
|
|
255
|
+
options: OpenOptions = {},
|
|
256
|
+
): WritableDatabase {
|
|
257
|
+
requireBackup(backup);
|
|
258
|
+
const driver = new DatabaseSync(path);
|
|
259
|
+
applyConnectionPragmas(driver, options);
|
|
260
|
+
return writableHandle(driver);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* A handle that can append to the `llm_calls` ledger and read — nothing else.
|
|
265
|
+
*
|
|
266
|
+
* Deliberately NOT a `WritableDatabase`: it exposes no `exec`, so there is no
|
|
267
|
+
* DDL or multi-statement path, and its `prepare` refuses any statement but the
|
|
268
|
+
* ledger append. Structural typing would otherwise let it satisfy
|
|
269
|
+
* `WritableDatabase` and travel anywhere a write is accepted.
|
|
270
|
+
*/
|
|
271
|
+
export interface LedgerAppendDatabase extends Database, StatementRunner {
|
|
272
|
+
prepare(sql: string): WritablePreparedQuery;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The one statement shape this handle may form. Matched on the normalized text
|
|
277
|
+
* so formatting (the multi-line template `logLlmCall` builds) is irrelevant,
|
|
278
|
+
* while anything with a second statement, a different table, or a different
|
|
279
|
+
* verb fails. Reads are allowed so the writer can verify its own row.
|
|
280
|
+
*/
|
|
281
|
+
const LEDGER_APPEND = /^insert\s+into\s+llm_calls\s*\(/;
|
|
282
|
+
const READ_ONLY_STATEMENT = /^(select|with)\b/;
|
|
283
|
+
|
|
284
|
+
function assertLedgerStatement(sql: string): void {
|
|
285
|
+
const normalized = sql.trim().replace(/\s+/g, " ").toLowerCase();
|
|
286
|
+
const singleStatement = !normalized.replace(/;\s*$/, "").includes(";");
|
|
287
|
+
const permitted =
|
|
288
|
+
singleStatement && (LEDGER_APPEND.test(normalized) || READ_ONLY_STATEMENT.test(normalized));
|
|
289
|
+
if (!permitted) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`this connection may prepare only the llm_calls append or a read; refused: ${sql.trim().slice(0, 60)}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Open a live database for the `llm_calls` append and nothing else
|
|
298
|
+
* (CV22.DS9.TS2 D1) — the MCP server's single sanctioned write.
|
|
299
|
+
*
|
|
300
|
+
* **Why this is not `openDatabaseForWrite`.** The DS4 backup gate is a
|
|
301
|
+
* last-write undo built for a snapshot→write→exit CLI: `openLiveWriteDatabase`
|
|
302
|
+
* promotes a fresh snapshot to a fixed name, and the record is verified once, at
|
|
303
|
+
* open. The MCP server
|
|
304
|
+
* lives for an entire client session, so a snapshot taken at launch is not the
|
|
305
|
+
* state before a row written forty minutes later, and re-snapshotting per write
|
|
306
|
+
* would put a 399 ms / ~50 MB `VACUUM INTO` on the agent's response path while
|
|
307
|
+
* overwriting the front door's undo with an observability row. The gate is the
|
|
308
|
+
* wrong shape here, not merely the wrong price.
|
|
309
|
+
*
|
|
310
|
+
* **What replaces it.** Narrowing, enforced at the statement level rather than
|
|
311
|
+
* documented: an append-only INSERT into an observability table, through a
|
|
312
|
+
* handle that can form no other statement and exposes no `exec`. It cannot
|
|
313
|
+
* UPDATE, DELETE, DROP, open a transaction, or touch `memories`, `identity`,
|
|
314
|
+
* `conversations`, or any other table — so the failure modes the gate protects
|
|
315
|
+
* against are unreachable rather than undone. `logLlmCall` is already
|
|
316
|
+
* fail-soft, matching Python, whose MCP server writes this same row through an
|
|
317
|
+
* ungated connection.
|
|
318
|
+
*
|
|
319
|
+
* The sole sanctioned caller is `ts/src/mcp/main.ts`, which keeps its tools on a
|
|
320
|
+
* driver-level read-only handle and hands them only a sink function.
|
|
321
|
+
*/
|
|
322
|
+
export function openDatabaseForLedgerAppend(
|
|
323
|
+
path: string,
|
|
324
|
+
options: OpenOptions = {},
|
|
325
|
+
): LedgerAppendDatabase {
|
|
326
|
+
const driver = new DatabaseSync(path);
|
|
327
|
+
applyConnectionPragmas(driver, options);
|
|
328
|
+
return {
|
|
329
|
+
prepare(sql: string): WritablePreparedQuery {
|
|
330
|
+
assertLedgerStatement(sql);
|
|
331
|
+
const statement = driver.prepare(sql);
|
|
332
|
+
return {
|
|
333
|
+
...readableStatement(statement),
|
|
334
|
+
run: (...params: SqlValue[]): RunResult => statement.run(...params),
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
close: (): void => {
|
|
338
|
+
driver.close();
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Snapshot a live database into `targetPath` using `VACUUM INTO` from a
|
|
345
|
+
* read-only connection. Unlike a raw file copy, this captures committed
|
|
346
|
+
* transactions still living in the `-wal` sidecar and cannot produce a torn
|
|
347
|
+
* copy mid-checkpoint — it is the only sanctioned way to back up a live
|
|
348
|
+
* `memory.db` (the parity harness uses the equivalent Python `backup()` API
|
|
349
|
+
* for the same reason). The target must not exist; callers remove any
|
|
350
|
+
* previous snapshot first.
|
|
351
|
+
*/
|
|
352
|
+
export function snapshotDatabaseTo(
|
|
353
|
+
sourcePath: string,
|
|
354
|
+
targetPath: string,
|
|
355
|
+
options: OpenOptions = {},
|
|
356
|
+
): void {
|
|
357
|
+
const driver = new DatabaseSync(sourcePath, { readOnly: true });
|
|
358
|
+
// Every other seam connection waits on lock contention rather than failing
|
|
359
|
+
// instantly; the snapshot read must too. Without this, a `VACUUM INTO` that
|
|
360
|
+
// races a concurrent writer (e.g. migrate-on-open backing up under multi-process
|
|
361
|
+
// contention) raises SQLITE_BUSY ("database is locked") immediately instead of
|
|
362
|
+
// waiting for the writer to finish.
|
|
363
|
+
applyConnectionPragmas(driver, options);
|
|
364
|
+
try {
|
|
365
|
+
driver.prepare("VACUUM INTO ?").run(targetPath);
|
|
366
|
+
} finally {
|
|
367
|
+
driver.close();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Make a `VACUUM INTO` snapshot restorable as the database it was taken from
|
|
373
|
+
* (CR061), then check it. A snapshot comes out in rollback-journal mode, and
|
|
374
|
+
* Mirror sets WAL only when bootstrap creates a missing file, so a restored
|
|
375
|
+
* snapshot would otherwise run in a mode the original never ran in. Switching
|
|
376
|
+
* it to WAL and closing leaves one self-contained file whose header records
|
|
377
|
+
* WAL, with no sidecars. The snapshot is a private copy nothing else holds
|
|
378
|
+
* open, which is why this connection may write to it.
|
|
379
|
+
*
|
|
380
|
+
* Returns `PRAGMA quick_check`'s verdict; the caller publishes the snapshot
|
|
381
|
+
* only when it is `ok` -- a backup that has never been opened is a belief.
|
|
382
|
+
*/
|
|
383
|
+
export function prepareArchiveSnapshot(path: string): { ok: boolean; verdict: string } {
|
|
384
|
+
const driver = new DatabaseSync(path);
|
|
385
|
+
try {
|
|
386
|
+
const mode = driver.prepare("PRAGMA journal_mode=WAL").get() as { journal_mode?: string };
|
|
387
|
+
if (mode.journal_mode !== "wal") {
|
|
388
|
+
return { ok: false, verdict: `journal_mode is ${mode.journal_mode ?? "unknown"}, not wal` };
|
|
389
|
+
}
|
|
390
|
+
const verdict = quickCheckVerdict(driver);
|
|
391
|
+
return { ok: verdict === "ok", verdict };
|
|
392
|
+
} catch (error) {
|
|
393
|
+
return { ok: false, verdict: sqliteMessage(error) };
|
|
394
|
+
} finally {
|
|
395
|
+
driver.close();
|
|
396
|
+
}
|
|
397
|
+
}
|