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.
Files changed (467) hide show
  1. package/.claude/hooks/log-session-end.sh +85 -0
  2. package/.claude/hooks/log-user-prompt.sh +85 -0
  3. package/.claude/hooks/mirror-inject.sh +85 -0
  4. package/.claude/hooks/session-start.sh +85 -0
  5. package/.gemini/hooks/log-assistant.sh +85 -0
  6. package/.gemini/hooks/log-user.sh +85 -0
  7. package/.gemini/hooks/session-end.sh +85 -0
  8. package/.gemini/hooks/session-start.sh +85 -0
  9. package/.pi/extensions/mirror-logger.ts +561 -0
  10. package/.pi/skills/mm-backup/SKILL.md +21 -0
  11. package/.pi/skills/mm-build/SKILL.md +1206 -0
  12. package/.pi/skills/mm-consolidate/SKILL.md +112 -0
  13. package/.pi/skills/mm-consult/SKILL.md +70 -0
  14. package/.pi/skills/mm-conversations/SKILL.md +16 -0
  15. package/.pi/skills/mm-discard/SKILL.md +39 -0
  16. package/.pi/skills/mm-explore/SKILL.md +255 -0
  17. package/.pi/skills/mm-help/SKILL.md +74 -0
  18. package/.pi/skills/mm-identity/SKILL.md +96 -0
  19. package/.pi/skills/mm-journal/SKILL.md +16 -0
  20. package/.pi/skills/mm-journey/SKILL.md +23 -0
  21. package/.pi/skills/mm-journeys/SKILL.md +15 -0
  22. package/.pi/skills/mm-memories/SKILL.md +17 -0
  23. package/.pi/skills/mm-mirror/SKILL.md +98 -0
  24. package/.pi/skills/mm-mute/SKILL.md +18 -0
  25. package/.pi/skills/mm-new/SKILL.md +18 -0
  26. package/.pi/skills/mm-recall/SKILL.md +17 -0
  27. package/.pi/skills/mm-release-notes/SKILL.md +52 -0
  28. package/.pi/skills/mm-seed/SKILL.md +19 -0
  29. package/.pi/skills/mm-shadow/SKILL.md +117 -0
  30. package/.pi/skills/mm-soul/SKILL.md +502 -0
  31. package/.pi/skills/mm-tasks/SKILL.md +24 -0
  32. package/.pi/skills/mm-update/SKILL.md +32 -0
  33. package/.pi/skills/mm-week/SKILL.md +28 -0
  34. package/.pi/skills/mm-welcome/SKILL.md +24 -0
  35. package/AGENTS.md +188 -0
  36. package/README.md +126 -9
  37. package/bin/loader.js +67 -0
  38. package/bin/mirror-hook.js +7 -0
  39. package/bin/mirror.js +7 -0
  40. package/docs/releases/index.md +99 -0
  41. package/docs/releases/pending-cutoffs.md +510 -0
  42. package/docs/releases/v0.10.0.md +123 -0
  43. package/docs/releases/v0.10.1.md +66 -0
  44. package/docs/releases/v0.10.10.md +51 -0
  45. package/docs/releases/v0.10.11.md +39 -0
  46. package/docs/releases/v0.10.12.md +57 -0
  47. package/docs/releases/v0.10.13.md +35 -0
  48. package/docs/releases/v0.10.14.md +42 -0
  49. package/docs/releases/v0.10.15.md +53 -0
  50. package/docs/releases/v0.10.16.md +35 -0
  51. package/docs/releases/v0.10.17.md +43 -0
  52. package/docs/releases/v0.10.2.md +64 -0
  53. package/docs/releases/v0.10.3.md +78 -0
  54. package/docs/releases/v0.10.4.md +42 -0
  55. package/docs/releases/v0.10.5.md +57 -0
  56. package/docs/releases/v0.10.6.md +47 -0
  57. package/docs/releases/v0.10.7.md +53 -0
  58. package/docs/releases/v0.10.8.md +49 -0
  59. package/docs/releases/v0.10.9.md +37 -0
  60. package/docs/releases/v0.11.0.md +86 -0
  61. package/docs/releases/v0.12.0.md +67 -0
  62. package/docs/releases/v0.13.0.md +64 -0
  63. package/docs/releases/v0.14.0.md +68 -0
  64. package/docs/releases/v0.15.0.md +48 -0
  65. package/docs/releases/v0.16.0.md +50 -0
  66. package/docs/releases/v0.16.1.md +55 -0
  67. package/docs/releases/v0.16.2.md +36 -0
  68. package/docs/releases/v0.17.0.md +50 -0
  69. package/docs/releases/v0.17.1.md +58 -0
  70. package/docs/releases/v0.17.2.md +42 -0
  71. package/docs/releases/v0.17.3.md +58 -0
  72. package/docs/releases/v0.18.0.md +84 -0
  73. package/docs/releases/v0.18.1.md +51 -0
  74. package/docs/releases/v0.18.2.md +69 -0
  75. package/docs/releases/v0.19.0.md +85 -0
  76. package/docs/releases/v0.19.1.md +46 -0
  77. package/docs/releases/v0.20.0.md +85 -0
  78. package/docs/releases/v0.21.0.md +61 -0
  79. package/docs/releases/v0.21.1.md +47 -0
  80. package/docs/releases/v0.22.0.md +77 -0
  81. package/docs/releases/v0.22.1.md +42 -0
  82. package/docs/releases/v0.23.0.md +105 -0
  83. package/docs/releases/v0.24.0.md +125 -0
  84. package/docs/releases/v0.25.0.md +107 -0
  85. package/docs/releases/v0.26.0.md +139 -0
  86. package/docs/releases/v0.26.1.md +65 -0
  87. package/docs/releases/v0.26.2.md +43 -0
  88. package/docs/releases/v0.27.0.md +63 -0
  89. package/docs/releases/v0.28.0.md +59 -0
  90. package/docs/releases/v0.29.0.md +61 -0
  91. package/docs/releases/v0.29.1.md +59 -0
  92. package/docs/releases/v0.29.2.md +59 -0
  93. package/docs/releases/v0.29.3.md +93 -0
  94. package/docs/releases/v0.30.0.md +75 -0
  95. package/docs/releases/v0.30.1.md +93 -0
  96. package/docs/releases/v0.30.2.md +66 -0
  97. package/docs/releases/v0.31.0.md +122 -0
  98. package/docs/releases/v0.31.1.md +142 -0
  99. package/docs/releases/v0.31.10.md +118 -0
  100. package/docs/releases/v0.31.11.md +88 -0
  101. package/docs/releases/v0.31.12.md +105 -0
  102. package/docs/releases/v0.31.13.md +99 -0
  103. package/docs/releases/v0.31.14.md +97 -0
  104. package/docs/releases/v0.31.2.md +115 -0
  105. package/docs/releases/v0.31.3.md +142 -0
  106. package/docs/releases/v0.31.4.md +117 -0
  107. package/docs/releases/v0.31.5.md +155 -0
  108. package/docs/releases/v0.31.6.md +132 -0
  109. package/docs/releases/v0.31.7.md +132 -0
  110. package/docs/releases/v0.31.8.md +120 -0
  111. package/docs/releases/v0.31.9.md +120 -0
  112. package/docs/releases/v0.32.0.md +261 -0
  113. package/docs/releases/v0.32.1.md +59 -0
  114. package/docs/releases/v0.8.0.md +118 -0
  115. package/docs/releases/v0.9.0.md +98 -0
  116. package/docs/releases/v0.9.1.md +83 -0
  117. package/package.json +102 -6
  118. package/plugins/mirror-mind/.claude-plugin/plugin.json +13 -0
  119. package/plugins/mirror-mind/hooks/hooks.json +50 -0
  120. package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
  121. package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
  122. package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
  123. package/plugins/mirror-mind/hooks/session-start.sh +98 -0
  124. package/plugins/mirror-mind/mcp/launch.sh +52 -0
  125. package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
  126. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
  127. package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
  128. package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
  129. package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
  130. package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
  131. package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
  132. package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
  133. package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
  134. package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
  135. package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
  136. package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
  137. package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
  138. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +65 -0
  139. package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
  140. package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
  141. package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
  142. package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
  143. package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
  144. package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
  145. package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
  146. package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
  147. package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
  148. package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
  149. package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
  150. package/scripts/codex-hooks/session-end.sh +85 -0
  151. package/scripts/codex-hooks/session-start.sh +85 -0
  152. package/scripts/codex-mirror.sh +48 -0
  153. package/templates/identity/README.md +119 -0
  154. package/templates/identity/ego/behavior.yaml +52 -0
  155. package/templates/identity/ego/constraints.yaml +15 -0
  156. package/templates/identity/ego/identity.yaml +43 -0
  157. package/templates/identity/journeys/personal-growth.yaml +39 -0
  158. package/templates/identity/organization/identity.yaml +24 -0
  159. package/templates/identity/organization/principles.yaml +17 -0
  160. package/templates/identity/personas/coach.yaml +94 -0
  161. package/templates/identity/personas/designer.yaml +93 -0
  162. package/templates/identity/personas/doctor.yaml +96 -0
  163. package/templates/identity/personas/engineer.yaml +77 -0
  164. package/templates/identity/personas/financial.yaml +97 -0
  165. package/templates/identity/personas/prompt-engineer.yaml +122 -0
  166. package/templates/identity/personas/researcher.yaml +88 -0
  167. package/templates/identity/personas/strategist.yaml +91 -0
  168. package/templates/identity/personas/teacher.yaml +95 -0
  169. package/templates/identity/personas/therapist.yaml +91 -0
  170. package/templates/identity/personas/thinker.yaml +74 -0
  171. package/templates/identity/personas/writer.yaml +76 -0
  172. package/templates/identity/self/config.yaml +16 -0
  173. package/templates/identity/self/soul.yaml +105 -0
  174. package/templates/identity/user/identity.yaml +30 -0
  175. package/ts/src/backup/staging.ts +62 -0
  176. package/ts/src/backup/zipBackup.ts +267 -0
  177. package/ts/src/backup/zipReader.ts +109 -0
  178. package/ts/src/backup/zipWriter.ts +148 -0
  179. package/ts/src/builder/activeCheckpoint.ts +134 -0
  180. package/ts/src/builder/aggregateStatus.ts +34 -0
  181. package/ts/src/builder/approve.ts +149 -0
  182. package/ts/src/builder/argv.ts +292 -0
  183. package/ts/src/builder/ariadMethod.ts +536 -0
  184. package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
  185. package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
  186. package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
  187. package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
  188. package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
  189. package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
  190. package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
  191. package/ts/src/builder/artifacts/storyFiles.ts +61 -0
  192. package/ts/src/builder/artifacts/storyIndex.ts +166 -0
  193. package/ts/src/builder/cadence.ts +59 -0
  194. package/ts/src/builder/card.ts +192 -0
  195. package/ts/src/builder/checkpointRefused.ts +66 -0
  196. package/ts/src/builder/closure.ts +964 -0
  197. package/ts/src/builder/commands.ts +2053 -0
  198. package/ts/src/builder/cursorPosition.ts +176 -0
  199. package/ts/src/builder/cursorTransitions.ts +77 -0
  200. package/ts/src/builder/deliveryCursor.ts +577 -0
  201. package/ts/src/builder/deliveryStoryClosure.ts +521 -0
  202. package/ts/src/builder/deliveryStoryPlan.ts +536 -0
  203. package/ts/src/builder/deliveryStoryReady.ts +160 -0
  204. package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
  205. package/ts/src/builder/expand.ts +336 -0
  206. package/ts/src/builder/flowUnit.ts +339 -0
  207. package/ts/src/builder/homeSurface.ts +122 -0
  208. package/ts/src/builder/implementationGuard.ts +125 -0
  209. package/ts/src/builder/index.ts +11 -0
  210. package/ts/src/builder/lifecycleRefusal.ts +92 -0
  211. package/ts/src/builder/lifecycleRibbon.ts +159 -0
  212. package/ts/src/builder/load.ts +313 -0
  213. package/ts/src/builder/methodAdoption.ts +113 -0
  214. package/ts/src/builder/methodDefinition.ts +429 -0
  215. package/ts/src/builder/methodInspection.ts +371 -0
  216. package/ts/src/builder/plan.ts +408 -0
  217. package/ts/src/builder/planPreauthorization.ts +265 -0
  218. package/ts/src/builder/prepare.ts +225 -0
  219. package/ts/src/builder/projectPaths.ts +64 -0
  220. package/ts/src/builder/pull.ts +356 -0
  221. package/ts/src/builder/pullCandidates.ts +463 -0
  222. package/ts/src/builder/pullCandidatesRender.ts +238 -0
  223. package/ts/src/builder/refinementField.ts +93 -0
  224. package/ts/src/builder/releaseIntent.ts +173 -0
  225. package/ts/src/builder/resumeState.ts +84 -0
  226. package/ts/src/builder/resumeSurface.ts +149 -0
  227. package/ts/src/builder/roadmapGrammar.ts +300 -0
  228. package/ts/src/builder/roadmapScan.ts +97 -0
  229. package/ts/src/builder/roadmapScope.ts +210 -0
  230. package/ts/src/builder/scopePhrases.ts +146 -0
  231. package/ts/src/builder/storyPaths.ts +196 -0
  232. package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
  233. package/ts/src/builder/surfaceProtocol.ts +42 -0
  234. package/ts/src/builder/templateGeneration.ts +148 -0
  235. package/ts/src/builder/transition.ts +178 -0
  236. package/ts/src/consult/args.ts +99 -0
  237. package/ts/src/consult/core.ts +122 -0
  238. package/ts/src/consult/modelCatalog.ts +55 -0
  239. package/ts/src/consult/render.ts +47 -0
  240. package/ts/src/conversation/append.ts +414 -0
  241. package/ts/src/conversation/backfill.ts +387 -0
  242. package/ts/src/conversation/closeTail.ts +467 -0
  243. package/ts/src/conversation/extraction.ts +333 -0
  244. package/ts/src/conversation/extractionDriver.ts +130 -0
  245. package/ts/src/conversation/extractionRun.ts +70 -0
  246. package/ts/src/conversation/journeyInference.ts +165 -0
  247. package/ts/src/conversation/journeyRepair.ts +114 -0
  248. package/ts/src/conversation/lifecycleFaces.ts +197 -0
  249. package/ts/src/conversation/lifecycleWrites.ts +313 -0
  250. package/ts/src/conversation/listing.ts +77 -0
  251. package/ts/src/conversation/logger.ts +576 -0
  252. package/ts/src/conversation/loggerCli.ts +282 -0
  253. package/ts/src/conversation/loggerRuntime.ts +259 -0
  254. package/ts/src/conversation/metadataLifecycle.ts +443 -0
  255. package/ts/src/conversation/recall.ts +66 -0
  256. package/ts/src/conversation/sessionComposites.ts +258 -0
  257. package/ts/src/conversation/sessionImport.ts +90 -0
  258. package/ts/src/conversation/transcriptBackfill.ts +141 -0
  259. package/ts/src/cultivation/applyActions.ts +241 -0
  260. package/ts/src/cultivation/cluster.ts +86 -0
  261. package/ts/src/cultivation/consolidationStore.ts +281 -0
  262. package/ts/src/cultivation/promptContext.ts +62 -0
  263. package/ts/src/cultivation/propose.ts +305 -0
  264. package/ts/src/cultivation/scan.ts +155 -0
  265. package/ts/src/db/backupGate.ts +47 -0
  266. package/ts/src/db/bootstrap.ts +108 -0
  267. package/ts/src/db/bootstrapLock.ts +223 -0
  268. package/ts/src/db/copyGuard.ts +60 -0
  269. package/ts/src/db/database.ts +397 -0
  270. package/ts/src/db/decode.ts +66 -0
  271. package/ts/src/db/ftsIntegrity.ts +41 -0
  272. package/ts/src/db/migrateOnOpen.ts +214 -0
  273. package/ts/src/db/migrations.ts +576 -0
  274. package/ts/src/db/readyOnOpen.ts +35 -0
  275. package/ts/src/db/rowDecode.ts +42 -0
  276. package/ts/src/db/schema.ts +432 -0
  277. package/ts/src/db/schemaInventory.ts +225 -0
  278. package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
  279. package/ts/src/db/schemaState.ts +91 -0
  280. package/ts/src/db/schemaTsDivergence.ts +114 -0
  281. package/ts/src/descriptor/descriptorRead.ts +49 -0
  282. package/ts/src/docs/docsLint.ts +265 -0
  283. package/ts/src/explorer/handoff.ts +522 -0
  284. package/ts/src/explorer/render.ts +335 -0
  285. package/ts/src/explorer/story.ts +769 -0
  286. package/ts/src/explorer/transition.ts +72 -0
  287. package/ts/src/extensions/bindings.ts +170 -0
  288. package/ts/src/extensions/catalog.ts +249 -0
  289. package/ts/src/extensions/catalogCommands.ts +513 -0
  290. package/ts/src/extensions/catalogWrites.ts +652 -0
  291. package/ts/src/extensions/contextRuntime.ts +325 -0
  292. package/ts/src/extensions/dispatch.ts +374 -0
  293. package/ts/src/extensions/errors.ts +24 -0
  294. package/ts/src/extensions/manifest.ts +356 -0
  295. package/ts/src/extensions/migrations.ts +350 -0
  296. package/ts/src/extraction/conversation.ts +304 -0
  297. package/ts/src/extraction/conversationMetadata.ts +206 -0
  298. package/ts/src/extraction/fencing.ts +106 -0
  299. package/ts/src/extraction/json.ts +17 -0
  300. package/ts/src/extraction/prompts.ts +470 -0
  301. package/ts/src/frontDoor/args.ts +30 -0
  302. package/ts/src/frontDoor/argvShape.ts +106 -0
  303. package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
  304. package/ts/src/frontDoor/buildRoute.ts +398 -0
  305. package/ts/src/frontDoor/cli.ts +1865 -0
  306. package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
  307. package/ts/src/frontDoor/consultRoute.ts +50 -0
  308. package/ts/src/frontDoor/contentTailRoute.ts +239 -0
  309. package/ts/src/frontDoor/conversationLoggerRoute.ts +73 -0
  310. package/ts/src/frontDoor/cultivationRoute.ts +210 -0
  311. package/ts/src/frontDoor/dbPath.ts +131 -0
  312. package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
  313. package/ts/src/frontDoor/exploreRoute.ts +572 -0
  314. package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
  315. package/ts/src/frontDoor/frontDoorLog.ts +57 -0
  316. package/ts/src/frontDoor/identityWrite.ts +36 -0
  317. package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
  318. package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
  319. package/ts/src/frontDoor/liveBackup.ts +100 -0
  320. package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
  321. package/ts/src/frontDoor/namedRefusal.ts +35 -0
  322. package/ts/src/frontDoor/nodeSupport.ts +24 -0
  323. package/ts/src/frontDoor/render/consolidate.ts +199 -0
  324. package/ts/src/frontDoor/render/conversations.ts +33 -0
  325. package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
  326. package/ts/src/frontDoor/render/descriptor.ts +20 -0
  327. package/ts/src/frontDoor/render/detectPersona.ts +50 -0
  328. package/ts/src/frontDoor/render/icons.ts +13 -0
  329. package/ts/src/frontDoor/render/identity.ts +59 -0
  330. package/ts/src/frontDoor/render/identityRows.ts +17 -0
  331. package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
  332. package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
  333. package/ts/src/frontDoor/render/journeys.ts +79 -0
  334. package/ts/src/frontDoor/render/list.ts +41 -0
  335. package/ts/src/frontDoor/render/memories.ts +67 -0
  336. package/ts/src/frontDoor/render/recall.ts +49 -0
  337. package/ts/src/frontDoor/render/shadow.ts +155 -0
  338. package/ts/src/frontDoor/render/tasks.ts +99 -0
  339. package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
  340. package/ts/src/frontDoor/render/week.ts +113 -0
  341. package/ts/src/frontDoor/routing.ts +999 -0
  342. package/ts/src/frontDoor/runtimeRoute.ts +623 -0
  343. package/ts/src/frontDoor/searchRoute.ts +166 -0
  344. package/ts/src/frontDoor/seedPaths.ts +98 -0
  345. package/ts/src/frontDoor/soulRoute.ts +433 -0
  346. package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
  347. package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
  348. package/ts/src/frontDoor/usage.ts +176 -0
  349. package/ts/src/guards/claudePlugin.ts +252 -0
  350. package/ts/src/guards/packContents.ts +306 -0
  351. package/ts/src/guards/piSourcedSkills.ts +70 -0
  352. package/ts/src/guards/retiredRouteShadows.ts +57 -0
  353. package/ts/src/guards/retiredSurfaces.ts +564 -0
  354. package/ts/src/hooks/claude.ts +176 -0
  355. package/ts/src/hooks/codex.ts +45 -0
  356. package/ts/src/hooks/gemini.ts +126 -0
  357. package/ts/src/hooks/main.ts +109 -0
  358. package/ts/src/hooks/mirrorState.ts +71 -0
  359. package/ts/src/hooks/payload.ts +68 -0
  360. package/ts/src/hooks/runtime.ts +159 -0
  361. package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
  362. package/ts/src/identity/descriptorGenerate.ts +162 -0
  363. package/ts/src/identity/identityEdit.ts +143 -0
  364. package/ts/src/identity/identityKey.ts +77 -0
  365. package/ts/src/identity/identityRead.ts +69 -0
  366. package/ts/src/identity/identityStore.ts +80 -0
  367. package/ts/src/identity/journeyListing.ts +53 -0
  368. package/ts/src/identity/personaInspect.ts +44 -0
  369. package/ts/src/identity/personaListing.ts +37 -0
  370. package/ts/src/identity/setIdentity.ts +64 -0
  371. package/ts/src/index.ts +231 -0
  372. package/ts/src/init/init.ts +111 -0
  373. package/ts/src/journey/journeyOptions.ts +131 -0
  374. package/ts/src/journey/journeyRemoval.ts +158 -0
  375. package/ts/src/journey/journeyStatus.ts +149 -0
  376. package/ts/src/journey/journeySyncFile.ts +87 -0
  377. package/ts/src/journey/journeyWrite.ts +203 -0
  378. package/ts/src/journey/parentJourney.ts +41 -0
  379. package/ts/src/journey/validateParentJourney.ts +55 -0
  380. package/ts/src/mcp/boundary.ts +151 -0
  381. package/ts/src/mcp/guards.ts +244 -0
  382. package/ts/src/mcp/main.ts +121 -0
  383. package/ts/src/mcp/payload.ts +141 -0
  384. package/ts/src/mcp/protocol.ts +141 -0
  385. package/ts/src/mcp/readModels.ts +241 -0
  386. package/ts/src/mcp/registry.ts +201 -0
  387. package/ts/src/mcp/serve.ts +120 -0
  388. package/ts/src/mcp/tools/deterministic.ts +137 -0
  389. package/ts/src/mcp/tools/providerCrossing.ts +187 -0
  390. package/ts/src/mcp/wire.ts +8 -0
  391. package/ts/src/memory/journal.ts +180 -0
  392. package/ts/src/memory/listing.ts +107 -0
  393. package/ts/src/memory/memoryWrite.ts +88 -0
  394. package/ts/src/memory/reinforcement.ts +35 -0
  395. package/ts/src/mirror/context.ts +161 -0
  396. package/ts/src/mirror/defaultResolution.ts +199 -0
  397. package/ts/src/mirror/orchestration.ts +364 -0
  398. package/ts/src/mirror/reception.ts +194 -0
  399. package/ts/src/mirror/render.ts +75 -0
  400. package/ts/src/mirror/runtimeSession.ts +205 -0
  401. package/ts/src/mode/operatingMode.ts +148 -0
  402. package/ts/src/observability/callOutcome.ts +115 -0
  403. package/ts/src/observability/inspectLedger.ts +329 -0
  404. package/ts/src/observability/ledgerHooks.ts +126 -0
  405. package/ts/src/observability/llmCalls.ts +154 -0
  406. package/ts/src/persona/detectPersona.ts +81 -0
  407. package/ts/src/planning/promptAssembly.ts +111 -0
  408. package/ts/src/planning/weekPending.ts +81 -0
  409. package/ts/src/planning/weekPlan.ts +183 -0
  410. package/ts/src/planning/weekSave.ts +117 -0
  411. package/ts/src/providers/config.ts +189 -0
  412. package/ts/src/providers/cost.ts +64 -0
  413. package/ts/src/providers/credits.ts +204 -0
  414. package/ts/src/providers/embedding.ts +321 -0
  415. package/ts/src/providers/familyProviders.ts +147 -0
  416. package/ts/src/providers/llm.ts +334 -0
  417. package/ts/src/providers/openrouter.ts +244 -0
  418. package/ts/src/providers/redaction.ts +110 -0
  419. package/ts/src/providers/replay.ts +15 -0
  420. package/ts/src/providers/transport.ts +410 -0
  421. package/ts/src/repair/encodingRepair.ts +253 -0
  422. package/ts/src/runtime/backup.ts +151 -0
  423. package/ts/src/runtime/channel.ts +146 -0
  424. package/ts/src/runtime/config.ts +168 -0
  425. package/ts/src/runtime/diagnose.ts +686 -0
  426. package/ts/src/runtime/git.ts +510 -0
  427. package/ts/src/runtime/installKind.ts +220 -0
  428. package/ts/src/runtime/migrate.ts +154 -0
  429. package/ts/src/runtime/packageIdentity.ts +102 -0
  430. package/ts/src/runtime/releaseNotes.ts +244 -0
  431. package/ts/src/runtime/status.ts +557 -0
  432. package/ts/src/runtime/strategies/clone.ts +200 -0
  433. package/ts/src/runtime/strategies/package.ts +328 -0
  434. package/ts/src/runtime/treeRoot.ts +30 -0
  435. package/ts/src/runtime/update.ts +445 -0
  436. package/ts/src/runtime/updateGate.ts +40 -0
  437. package/ts/src/runtime/updatePipeline.ts +159 -0
  438. package/ts/src/runtime/version.ts +36 -0
  439. package/ts/src/runtime/wiring.ts +130 -0
  440. package/ts/src/search/memorySearch.ts +338 -0
  441. package/ts/src/search/ranker.ts +154 -0
  442. package/ts/src/seed/seed.ts +256 -0
  443. package/ts/src/seed/seedYaml.ts +133 -0
  444. package/ts/src/soul/apply.ts +218 -0
  445. package/ts/src/soul/harvest.ts +267 -0
  446. package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
  447. package/ts/src/soul/prompts/soul_self_voice.md +219 -0
  448. package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
  449. package/ts/src/soul/prompts.ts +85 -0
  450. package/ts/src/soul/render.ts +358 -0
  451. package/ts/src/soul/state.ts +212 -0
  452. package/ts/src/soul/transition.ts +37 -0
  453. package/ts/src/tasks/journeyPathParse.ts +113 -0
  454. package/ts/src/tasks/taskImportSync.ts +142 -0
  455. package/ts/src/tasks/taskStore.ts +312 -0
  456. package/ts/src/tasks/weekView.ts +120 -0
  457. package/ts/src/util/clipCodePoints.ts +26 -0
  458. package/ts/src/util/paths.ts +63 -0
  459. package/ts/src/util/program.ts +9 -0
  460. package/ts/src/util/pyGenerators.ts +144 -0
  461. package/ts/src/util/pythonText.ts +317 -0
  462. package/ts/src/util/semver.ts +45 -0
  463. package/ts/src/util/shellWord.ts +25 -0
  464. package/ts/src/util/slug.ts +64 -0
  465. package/ts/src/welcome/card.ts +339 -0
  466. package/ts/src/welcome/statusLine.ts +101 -0
  467. package/ts/src/welcome/updateCache.ts +129 -0
@@ -0,0 +1,333 @@
1
+ import { metadataDict } from "#conversation/closeTail.ts";
2
+ import type { WritableDatabase } from "#db/database.ts";
3
+ import { embeddingToBytes } from "#db/decode.ts";
4
+ import { optionalString, requireString } from "#db/rowDecode.ts";
5
+ import {
6
+ curateAgainstExisting,
7
+ type ExistingMemoryForCuration,
8
+ type ExtractedMemory,
9
+ type ExtractionMessage,
10
+ type ExtractionStatus,
11
+ extractMemoriesWithStatus,
12
+ extractTasks,
13
+ formatTranscript,
14
+ naiveSummary,
15
+ type OnLlmCall,
16
+ } from "#extraction/conversation.ts";
17
+ import { createMemoryRow } from "#memory/memoryWrite.ts";
18
+ import { chatLedgerHook, embeddingLedgerHook } from "#observability/ledgerHooks.ts";
19
+ import { resolveExtractionModel } from "#providers/config.ts";
20
+ import {
21
+ addEmbeddingProvenance,
22
+ type EmbeddingAttemptInfo,
23
+ type EmbeddingProvider,
24
+ generateEmbeddingSafely,
25
+ } from "#providers/embedding.ts";
26
+ import type { LlmProvider } from "#providers/llm.ts";
27
+ import { newId, nowIso, pythonJsonDumps, pythonJsonDumpsEnsureAscii } from "#util/pyGenerators.ts";
28
+
29
+ export interface ConversationExtractionOptions {
30
+ llm: LlmProvider;
31
+ embeddings: EmbeddingProvider;
32
+ now?: () => string;
33
+ id?: () => string;
34
+ summarize?: boolean;
35
+ twoPass?: boolean;
36
+ curationExisting?: readonly ExistingMemoryForCuration[];
37
+ /**
38
+ * Model-call ledger (CV9.E2.S13/S14). Python's `_make_logger(role,
39
+ * conversation_id)` writes one `llm_calls` row per successful call in
40
+ * `extraction`, `curation`, `task_extraction`, and `summary`; the embedding
41
+ * rows were already logged. Off by default so the DS5 unit surface stays
42
+ * ledger-free; the front door turns it on.
43
+ */
44
+ ledger?: boolean;
45
+ }
46
+
47
+ export interface ConversationExtractionResult {
48
+ memoryIds: string[];
49
+ taskIds: string[];
50
+ extracted: number;
51
+ }
52
+
53
+ interface ConversationRow {
54
+ id: string;
55
+ persona: string | null;
56
+ journey: string | null;
57
+ metadata: string | null;
58
+ }
59
+
60
+ export async function runConversationExtraction(
61
+ db: WritableDatabase,
62
+ conversationId: string,
63
+ options: ConversationExtractionOptions,
64
+ ): Promise<ConversationExtractionResult> {
65
+ const conv = getConversation(db, conversationId);
66
+ const messages = getMessages(db, conversationId);
67
+ if (!conv?.journey || messages.length < 4) {
68
+ return { memoryIds: [], taskIds: [], extracted: 0 };
69
+ }
70
+
71
+ const now = options.now ?? nowIso;
72
+ const id = options.id ?? newId;
73
+ const userName = resolveUserName(db);
74
+ const ledger = options.ledger ? llmLedger(db, conversationId, now) : undefined;
75
+
76
+ // A failure anywhere below propagates unmodified. Python records it -- the
77
+ // `llm_failed` status, the attempt counter, and quarantine at the max --
78
+ // OUTSIDE this orchestration, in `_run_extraction`; the port keeps that
79
+ // accounting in `extractionRun.ts` for the same reason: the stored metadata
80
+ // key order depends on it being written once, there, never partly here.
81
+ const outcome = await extractMemoriesWithStatus(options.llm, messages, {
82
+ persona: conv.persona,
83
+ journey: conv.journey,
84
+ userName,
85
+ onLlmCall: ledger?.("extraction"),
86
+ });
87
+ let extractedMemories: ExtractedMemory[] = outcome.memories;
88
+ const extractionStatus: ExtractionStatus = outcome.status;
89
+
90
+ if (options.twoPass && extractedMemories.length > 0) {
91
+ extractedMemories = await curateAgainstExisting(
92
+ options.llm,
93
+ extractedMemories,
94
+ options.curationExisting ?? [],
95
+ { onLlmCall: ledger?.("curation") },
96
+ );
97
+ }
98
+
99
+ const taskIds = await persistExtractedTasks(db, options.llm, messages, {
100
+ journey: conv.journey,
101
+ userName,
102
+ now,
103
+ id,
104
+ onLlmCall: ledger?.("task_extraction"),
105
+ });
106
+
107
+ const summaryText =
108
+ options.summarize === false
109
+ ? naiveSummary(messages)
110
+ : await replayedSummary(options.llm, messages, userName, ledger?.("summary"));
111
+ const finalSummary = summaryText || naiveSummary(messages);
112
+
113
+ // Stage EVERY network embedding first -- the summary and each memory -- so a
114
+ // failure leaves nothing persisted. The retry then starts clean and does not
115
+ // duplicate rows or re-spend on embeddings (AI-03 / CV9.E2.S9).
116
+ //
117
+ // This mirrors Python's `_extract_and_persist` deliberately. The port
118
+ // originally interleaved embed -> insert per memory, which is invisible
119
+ // under replay -- replayed embeddings never fail -- and becomes a partial
120
+ // write the moment a live provider is on the other end: memories 1..n-1
121
+ // persisted, `extracted` unset, and a retry that inserts them a second time
122
+ // with a second paid embedding each (CV22.DS8.US2 plan review).
123
+ const summaryEmbeddingBytes = finalSummary
124
+ ? embeddingToBytes(
125
+ // Logged to the ledger (AI-09/D-003), but NOT provenance-stamped:
126
+ // conversation_embeddings has no metadata column, matching Python's own
127
+ // narrower scope (add_embedding_provenance is called from
128
+ // add_memory/add_attachment only, never from the summary path).
129
+ await generateEmbeddingSafely(options.embeddings, finalSummary, {
130
+ onAttempt: logEmbeddingAttempt(db, conversationId, now),
131
+ }),
132
+ )
133
+ : null;
134
+
135
+ const stagedMemories: { memory: (typeof extractedMemories)[number]; embedding: Uint8Array }[] =
136
+ [];
137
+ for (const memory of extractedMemories) {
138
+ const embeddingText = `${memory.title}. ${memory.content}${memory.context ? ` Context: ${memory.context}` : ""}`;
139
+ const embedding = await generateEmbeddingSafely(options.embeddings, embeddingText, {
140
+ onAttempt: logEmbeddingAttempt(db, conversationId, now),
141
+ });
142
+ stagedMemories.push({ memory, embedding: embeddingToBytes(embedding) });
143
+ }
144
+
145
+ // All embeddings succeeded -- only local writes remain.
146
+ if (summaryEmbeddingBytes !== null && finalSummary) {
147
+ db.prepare(
148
+ `INSERT INTO conversation_embeddings (conversation_id, summary_embedding) VALUES (?, ?) ` +
149
+ `ON CONFLICT(conversation_id) DO UPDATE SET summary_embedding = excluded.summary_embedding`,
150
+ ).run(conversationId, summaryEmbeddingBytes);
151
+ db.prepare("UPDATE conversations SET summary = ? WHERE id = ?").run(
152
+ finalSummary.slice(0, 1000),
153
+ conversationId,
154
+ );
155
+ }
156
+
157
+ const memoryIds: string[] = [];
158
+ for (const { memory, embedding } of stagedMemories) {
159
+ const memoryId = id();
160
+ insertMemory(db, memoryId, conversationId, memory, embedding, now());
161
+ memoryIds.push(memoryId);
162
+ }
163
+
164
+ const metadata = metadataDict(conv.metadata);
165
+ metadata.extracted = true;
166
+ metadata.extraction_status = extractionStatus.status;
167
+ if (
168
+ extractionStatus.dropped &&
169
+ Object.values(extractionStatus.dropped).some((count) => count > 0)
170
+ ) {
171
+ metadata.extraction_dropped = extractionStatus.dropped;
172
+ }
173
+ db.prepare("UPDATE conversations SET metadata = ? WHERE id = ?").run(
174
+ pythonJsonDumps(metadata),
175
+ conversationId,
176
+ );
177
+
178
+ return { memoryIds, taskIds, extracted: memoryIds.length };
179
+ }
180
+
181
+ async function persistExtractedTasks(
182
+ db: WritableDatabase,
183
+ llm: LlmProvider,
184
+ messages: readonly ExtractionMessage[],
185
+ options: {
186
+ journey: string;
187
+ userName: string;
188
+ now: () => string;
189
+ id: () => string;
190
+ onLlmCall?: OnLlmCall;
191
+ },
192
+ ): Promise<string[]> {
193
+ try {
194
+ const tasks = await extractTasks(llm, messages, {
195
+ journey: options.journey,
196
+ userName: options.userName,
197
+ onLlmCall: options.onLlmCall,
198
+ });
199
+ const inserted: string[] = [];
200
+ for (const task of tasks) {
201
+ const existing = db
202
+ .prepare("SELECT id FROM tasks WHERE title LIKE ? AND journey = ? ORDER BY created_at DESC")
203
+ .all(`%${task.title}%`, task.journey ?? options.journey);
204
+ if (existing.length > 0) continue;
205
+ const taskId = options.id();
206
+ const now = options.now();
207
+ db.prepare(
208
+ `INSERT INTO tasks ` +
209
+ `(id, journey, title, status, due_date, scheduled_at, time_hint, stage, context, source, created_at, updated_at, completed_at, metadata) ` +
210
+ `VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
211
+ ).run(
212
+ taskId,
213
+ task.journey,
214
+ task.title,
215
+ "todo",
216
+ task.due_date,
217
+ null,
218
+ null,
219
+ task.stage,
220
+ task.context,
221
+ "conversation",
222
+ now,
223
+ now,
224
+ null,
225
+ null,
226
+ );
227
+ inserted.push(taskId);
228
+ }
229
+ return inserted;
230
+ } catch {
231
+ return [];
232
+ }
233
+ }
234
+
235
+ async function replayedSummary(
236
+ llm: LlmProvider,
237
+ messages: readonly ExtractionMessage[],
238
+ userName: string,
239
+ onLlmCall?: OnLlmCall,
240
+ ): Promise<string> {
241
+ try {
242
+ const prompt = formatTranscript(messages, userName);
243
+ const response = await llm.complete({
244
+ role: "summary",
245
+ prompt,
246
+ model: resolveExtractionModel(),
247
+ temperature: 0.3,
248
+ });
249
+ onLlmCall?.(response, prompt);
250
+ return response.content.trim();
251
+ } catch {
252
+ return naiveSummary(messages);
253
+ }
254
+ }
255
+
256
+ function getConversation(db: WritableDatabase, conversationId: string): ConversationRow | null {
257
+ const row = db
258
+ .prepare("SELECT id, persona, journey, metadata FROM conversations WHERE id = ?")
259
+ .get(conversationId);
260
+ if (!row) return null;
261
+ return {
262
+ id: requireString(row, "id"),
263
+ persona: optionalString(row, "persona"),
264
+ journey: optionalString(row, "journey"),
265
+ metadata: optionalString(row, "metadata"),
266
+ };
267
+ }
268
+
269
+ function getMessages(db: WritableDatabase, conversationId: string): ExtractionMessage[] {
270
+ return db
271
+ .prepare("SELECT role, content FROM messages WHERE conversation_id = ? ORDER BY created_at ASC")
272
+ .all(conversationId)
273
+ .map((row) => ({ role: requireString(row, "role"), content: requireString(row, "content") }));
274
+ }
275
+
276
+ function resolveUserName(db: WritableDatabase): string {
277
+ const row = db
278
+ .prepare("SELECT content FROM identity WHERE layer = ? AND key = ? LIMIT 1")
279
+ .get("user", "identity");
280
+ const content = row ? optionalString(row, "content") : null;
281
+ const match = content?.match(
282
+ /(?:You are talking to|Você está falando com) ([A-Z][a-zA-Záéíóúãõ]+)/,
283
+ );
284
+ return match?.[1] ?? "User";
285
+ }
286
+
287
+ /**
288
+ * The close tail's two ledger hooks. Roles match Python's `build_llm_logger`
289
+ * roles so the `llm_calls` ledger agrees across engines; the orchestration's
290
+ * clock stamps every row.
291
+ */
292
+ function llmLedger(
293
+ db: WritableDatabase,
294
+ conversationId: string,
295
+ now: () => string,
296
+ ): (role: string) => OnLlmCall {
297
+ return (role) => chatLedgerHook(db, role, { conversationId, now });
298
+ }
299
+
300
+ function logEmbeddingAttempt(
301
+ db: WritableDatabase,
302
+ conversationId: string,
303
+ now: () => string,
304
+ ): (info: EmbeddingAttemptInfo) => void {
305
+ return embeddingLedgerHook(db, { conversationId, now });
306
+ }
307
+
308
+ function insertMemory(
309
+ db: WritableDatabase,
310
+ id: string,
311
+ conversationId: string,
312
+ memory: ExtractedMemory,
313
+ embedding: Uint8Array,
314
+ createdAt: string,
315
+ ): void {
316
+ createMemoryRow(db, {
317
+ id,
318
+ conversationId,
319
+ memoryType: memory.memory_type,
320
+ layer: memory.layer,
321
+ title: memory.title,
322
+ content: memory.content,
323
+ context: memory.context,
324
+ journey: memory.journey,
325
+ persona: memory.persona,
326
+ // Memory tags go through `services/memory.py`'s plain `json.dumps(tags)`,
327
+ // which keeps `ensure_ascii=True` -- unlike conversation tags above.
328
+ tags: memory.tags.length > 0 ? pythonJsonDumpsEnsureAscii(memory.tags) : null,
329
+ createdAt,
330
+ embedding,
331
+ metadata: addEmbeddingProvenance(null),
332
+ });
333
+ }
@@ -0,0 +1,130 @@
1
+ // CV22.DS7.US5 slice C — the budgeted extraction driver.
2
+ //
3
+ // Ports `extract_pending` and the maintenance counters from
4
+ // `src/memory/cli/conversation_logger.py`, plus the eligibility query in
5
+ // `ConversationStore.get_unextracted_conversations`.
6
+ //
7
+ // The extraction call is injected rather than imported: it is the DS5
8
+ // orchestration (`runConversationExtraction`) behind the replay transport, so
9
+ // this module — the part that decides *what* gets extracted and *how failure
10
+ // behaves* — stays deterministic and provider-free. Two released properties
11
+ // live here and must not regress:
12
+ //
13
+ // * AI-05 (CV9.E2.S26): a backlog can never turn one session start into an
14
+ // unbounded spend loop. At most `limit` conversations per call,
15
+ // oldest-ended first, remainder visibly carried over.
16
+ // * CV9.E2.S7: one poison-pill conversation is recorded and skipped, never
17
+ // allowed to crash the batch or block the queue behind it.
18
+
19
+ import type { WritableDatabase } from "#db/database.ts";
20
+
21
+ /** Python's `MEMORY_MAINTENANCE_MAX_EXTRACTIONS` default. */
22
+ export const DEFAULT_MAINTENANCE_MAX_EXTRACTIONS = 10;
23
+
24
+ /**
25
+ * Eligibility, byte-for-byte from `ConversationStore._UNEXTRACTED_WHERE`.
26
+ * `json_extract` returns NULL for malformed metadata, and `IS NOT 1` keeps
27
+ * such a row eligible rather than silently dropping it from the queue.
28
+ */
29
+ const UNEXTRACTED_WHERE = `
30
+ WHERE c.ended_at IS NOT NULL
31
+ AND c.journey IS NOT NULL
32
+ AND (c.metadata IS NULL OR json_extract(c.metadata, '$.extracted') IS NOT 1)
33
+ AND (c.metadata IS NULL
34
+ OR json_extract(c.metadata, '$.extraction_quarantined') IS NOT 1)
35
+ AND (SELECT COUNT(*) FROM messages m WHERE m.conversation_id = c.id) >= 4
36
+ `;
37
+
38
+ export interface UnextractedConversation {
39
+ id: string;
40
+ journey: string | null;
41
+ persona: string | null;
42
+ metadata: string | null;
43
+ }
44
+
45
+ /** Ended, eligible conversations, oldest-ended first (FIFO drain). */
46
+ export function selectUnextractedConversations(
47
+ db: WritableDatabase,
48
+ limit: number | null,
49
+ ): UnextractedConversation[] {
50
+ const base = `SELECT c.id, c.journey, c.persona, c.metadata FROM conversations c${UNEXTRACTED_WHERE}ORDER BY c.ended_at ASC`;
51
+ const rows = limit === null ? db.prepare(base).all() : db.prepare(`${base} LIMIT ?`).all(limit);
52
+ return rows.map((row) => ({
53
+ id: String(row.id),
54
+ journey: typeof row.journey === "string" ? row.journey : null,
55
+ persona: typeof row.persona === "string" ? row.persona : null,
56
+ metadata: typeof row.metadata === "string" ? row.metadata : null,
57
+ }));
58
+ }
59
+
60
+ export interface ExtractPendingOptions {
61
+ /** Defaults to the released maintenance budget. */
62
+ limit?: number;
63
+ /**
64
+ * The DS5 orchestration behind replay, wrapped in Python's `_run_extraction`
65
+ * accounting (`extractionRun.ts`); a throw or a rejection marks this one
66
+ * failed. Async because the orchestration awaits providers -- a synchronous
67
+ * signature here would let a rejected extraction escape the isolation below
68
+ * and count as a success.
69
+ */
70
+ runExtraction: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
71
+ }
72
+
73
+ /**
74
+ * Extract memories from ended conversations not yet processed, one at a time
75
+ * in queue order.
76
+ *
77
+ * Returns the number of conversations successfully extracted. Each is isolated:
78
+ * a failure is skipped so it cannot crash the batch or block the queue behind
79
+ * it, matching Python's bare `except Exception: continue`.
80
+ */
81
+ export async function extractPending(
82
+ db: WritableDatabase,
83
+ options: ExtractPendingOptions,
84
+ ): Promise<number> {
85
+ const limit = options.limit ?? DEFAULT_MAINTENANCE_MAX_EXTRACTIONS;
86
+ if (limit <= 0) return 0;
87
+
88
+ let extracted = 0;
89
+ for (const conversation of selectUnextractedConversations(db, limit)) {
90
+ try {
91
+ await options.runExtraction(db, conversation.id);
92
+ extracted += 1;
93
+ } catch {}
94
+ }
95
+ return extracted;
96
+ }
97
+
98
+ function countScalar(db: WritableDatabase, sql: string, ...params: string[]): number {
99
+ const row = db.prepare(sql).get(...params);
100
+ const value = row?.n;
101
+ if (typeof value === "bigint") return Number(value);
102
+ return typeof value === "number" ? value : 0;
103
+ }
104
+
105
+ /** Conversations quarantined after repeated extraction failure (CV9.E2.S7). */
106
+ export function countQuarantinedConversations(db: WritableDatabase): number {
107
+ return countScalar(
108
+ db,
109
+ `SELECT COUNT(*) AS n FROM conversations c
110
+ WHERE json_extract(c.metadata, '$.extraction_quarantined') IS 1`,
111
+ );
112
+ }
113
+
114
+ /** Conversations whose recorded extraction status matches (AI-10). */
115
+ export function countConversationsWithExtractionStatus(
116
+ db: WritableDatabase,
117
+ status: string,
118
+ ): number {
119
+ return countScalar(
120
+ db,
121
+ `SELECT COUNT(*) AS n FROM conversations c
122
+ WHERE json_extract(c.metadata, '$.extraction_status') = ?`,
123
+ status,
124
+ );
125
+ }
126
+
127
+ /** Conversations still eligible after a capped run (CV9.E2.S26, AI-05). */
128
+ export function countCarriedOverConversations(db: WritableDatabase): number {
129
+ return countScalar(db, `SELECT COUNT(*) AS n FROM conversations c${UNEXTRACTED_WHERE}`);
130
+ }
@@ -0,0 +1,70 @@
1
+ // Extraction failure accounting (CV22.DS7.US10 slice F).
2
+ //
3
+ // Ports `ConversationService._run_extraction` -- the wrapper around the DS5
4
+ // orchestration that Python's `extract_conversation` and `end_conversation`
5
+ // both go through -- and `_record_failed_extraction_attempt` (CV9.E2.S7).
6
+ //
7
+ // The orchestration itself (`runConversationExtraction`) is DS5's; what it
8
+ // lacked was the accounting Python keeps OUTSIDE it: every failed attempt
9
+ // increments `extraction_attempts`, stamps `extraction_status=llm_failed`,
10
+ // and quarantines the conversation at `EXTRACTION_MAX_ATTEMPTS` so a
11
+ // poison-pill transcript stops being retried at every session start. Without
12
+ // this wrapper a conversation whose extraction failed under TypeScript would
13
+ // be retried forever and the maintenance report's quarantine tail would never
14
+ // appear -- a divergence `session-maintenance` parity cannot tolerate.
15
+ //
16
+ // The metadata key ORDER is part of the stored bytes: Python appends
17
+ // `extraction_attempts` before `extraction_status` when neither exists yet, so
18
+ // the write happens here, once, rather than partly inside the orchestration.
19
+
20
+ import { metadataDict } from "#conversation/closeTail.ts";
21
+ import type { WritableDatabase } from "#db/database.ts";
22
+ import { pythonJsonDumps } from "#util/pyGenerators.ts";
23
+
24
+ /** Python's `MEMORY_EXTRACTION_MAX_ATTEMPTS` default. */
25
+ export const DEFAULT_EXTRACTION_MAX_ATTEMPTS = 3;
26
+
27
+ /**
28
+ * Python `_record_failed_extraction_attempt`: bump the counter in the
29
+ * conversation's metadata JSON, mark the failure, quarantine at the max.
30
+ * Returns the new attempt count (0 when the conversation does not exist).
31
+ */
32
+ export function recordFailedExtractionAttempt(
33
+ db: WritableDatabase,
34
+ conversationId: string,
35
+ maxAttempts: number = DEFAULT_EXTRACTION_MAX_ATTEMPTS,
36
+ ): number {
37
+ const row = db.prepare("SELECT metadata FROM conversations WHERE id = ?").get(conversationId);
38
+ if (!row) return 0;
39
+ const metadata = metadataDict(typeof row.metadata === "string" ? row.metadata : null);
40
+ // Python: `int(meta.get("extraction_attempts", 0)) + 1`.
41
+ const attempts = Math.trunc(Number(metadata.extraction_attempts ?? 0)) + 1;
42
+ metadata.extraction_attempts = attempts;
43
+ metadata.extraction_status = "llm_failed";
44
+ if (attempts >= maxAttempts) metadata.extraction_quarantined = true;
45
+ db.prepare("UPDATE conversations SET metadata = ? WHERE id = ?").run(
46
+ pythonJsonDumps(metadata),
47
+ conversationId,
48
+ );
49
+ return attempts;
50
+ }
51
+
52
+ /**
53
+ * Python `_run_extraction`: run the orchestration; on ANY failure record the
54
+ * attempt and re-raise, so the caller's per-conversation loop can isolate it.
55
+ * The eligibility gate (journey, four messages) lives in the orchestration and
56
+ * returns before touching the row, exactly as Python's does.
57
+ */
58
+ export async function runExtractionWithAccounting(
59
+ db: WritableDatabase,
60
+ conversationId: string,
61
+ run: (db: WritableDatabase, conversationId: string) => Promise<unknown> | unknown,
62
+ maxAttempts: number = DEFAULT_EXTRACTION_MAX_ATTEMPTS,
63
+ ): Promise<void> {
64
+ try {
65
+ await run(db, conversationId);
66
+ } catch (error) {
67
+ recordFailedExtractionAttempt(db, conversationId, maxAttempts);
68
+ throw error;
69
+ }
70
+ }
@@ -0,0 +1,165 @@
1
+ // Journey inference for conversations (CV22.DS7.US10 slice E).
2
+ //
3
+ // Ports `_journey_aliases`, `_first_user_message`, `_activation_text`, and
4
+ // `_infer_journey_for_conversation` from
5
+ // `src/memory/cli/conversation_logger.py`.
6
+ //
7
+ // This is the deterministic half of `diagnose-journeys`/`repair-journeys`: it
8
+ // decides which journeyless conversation has a high-confidence match. The
9
+ // decision matters because `--apply` writes it, so a false positive silently
10
+ // reassigns a user's conversation to the wrong journey.
11
+ //
12
+ // Pure: no storage, no clock, no provider. The caller supplies the alias table
13
+ // and the conversation's title and first user message.
14
+
15
+ import type { WritableDatabase } from "#db/database.ts";
16
+
17
+ /** slug -> alias strings, longest first (Python sorts by length descending). */
18
+ export type JourneyAliases = Record<string, string[]>;
19
+
20
+ export interface JourneyInference {
21
+ journey: string | null;
22
+ reason: string | null;
23
+ }
24
+
25
+ /**
26
+ * Python's `_journey_aliases`: the slug, the slug with hyphens as spaces, and
27
+ * the first Markdown heading in the journey's identity content.
28
+ *
29
+ * Python builds the value set with `{slug.lower(), slug.replace("-", " ").lower()}`,
30
+ * so a slug without hyphens contributes one alias, not two.
31
+ */
32
+ export function journeyAliases(db: WritableDatabase): JourneyAliases {
33
+ const rows = db
34
+ .prepare("SELECT key, content FROM identity WHERE layer = 'journey' ORDER BY key")
35
+ .all() as Record<string, unknown>[];
36
+
37
+ const aliases: JourneyAliases = {};
38
+ for (const row of rows) {
39
+ const slug = String(row.key);
40
+ const content = typeof row.content === "string" ? row.content : "";
41
+ const values = new Set([slug.toLowerCase(), slug.replaceAll("-", " ").toLowerCase()]);
42
+ for (const line of content.split("\n")) {
43
+ const heading = line.trim();
44
+ if (heading.startsWith("#")) {
45
+ // Python breaks at the FIRST heading line, whether or not it yielded a
46
+ // title, so a bare `#` stops the scan instead of falling through.
47
+ const title = heading.replace(/^#+/, "").trim();
48
+ if (title) values.add(title.toLowerCase());
49
+ break;
50
+ }
51
+ }
52
+ aliases[slug] = sortByLengthDescending([...values]);
53
+ }
54
+ return aliases;
55
+ }
56
+
57
+ /**
58
+ * Python's `sorted(values, key=len, reverse=True)`, with equal lengths broken
59
+ * lexicographically.
60
+ *
61
+ * Python sorts a SET and its sort is stable, so aliases of equal length come
62
+ * out in set-iteration order -- which string hash randomization varies between
63
+ * processes. Observed directly while building the golden: `beta-two` alternates
64
+ * between `["beta two", "beta-two"]` and the reverse across runs.
65
+ *
66
+ * The ordering is behaviorally inert for inference: only the matched alias's
67
+ * LENGTH feeds the cross-slug tie-break, and equal-length entries tie by
68
+ * definition. Breaking those ties lexicographically therefore preserves
69
+ * behavior while making the alias table reproducible, which is what lets the
70
+ * golden serve as a determinism gate. The generator normalizes the oracle's
71
+ * output the same way and asserts the two agree on membership and lengths.
72
+ */
73
+ function sortByLengthDescending(values: string[]): string[] {
74
+ return [...values].sort(
75
+ (left, right) => right.length - left.length || (left < right ? -1 : left > right ? 1 : 0),
76
+ );
77
+ }
78
+
79
+ /** Python's `_first_user_message`. */
80
+ export function firstUserMessage(db: WritableDatabase, conversationId: string): string {
81
+ const row = db
82
+ .prepare(
83
+ `SELECT content FROM messages
84
+ WHERE conversation_id = ? AND role = 'user'
85
+ ORDER BY created_at ASC LIMIT 1`,
86
+ )
87
+ .get(conversationId) as Record<string, unknown> | undefined;
88
+ return row && typeof row.content === "string" ? row.content : "";
89
+ }
90
+
91
+ /**
92
+ * Python's `_activation_text`: the title, the text after a closing `</skill>`
93
+ * tag when present, and the first line -- joined with newlines and lowercased.
94
+ */
95
+ export function activationText(title: string | null, firstUser: string): string {
96
+ const parts: string[] = [title ?? ""];
97
+ const stripped = firstUser.trim();
98
+ if (stripped.includes("</skill>")) {
99
+ parts.push(stripped.slice(stripped.lastIndexOf("</skill>") + "</skill>".length));
100
+ }
101
+ const firstLine = stripped ? (stripped.split("\n")[0] as string) : "";
102
+ parts.push(firstLine);
103
+ return parts.join("\n").toLowerCase();
104
+ }
105
+
106
+ function escapeRegExp(value: string): string {
107
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108
+ }
109
+
110
+ /**
111
+ * Python's `_infer_journey_for_conversation`.
112
+ *
113
+ * Two routes: an explicit `/mm-build <slug>` command, or an alias match that is
114
+ * either standalone on its own line or wrapped in an activation phrase. A tie
115
+ * between the two longest matching aliases returns no journey -- ambiguity is
116
+ * resolved by refusing, never by picking, because the caller may write it.
117
+ */
118
+ export function inferJourneyForConversation(
119
+ title: string | null,
120
+ firstUser: string,
121
+ aliases: JourneyAliases,
122
+ ): JourneyInference {
123
+ const text = activationText(title, firstUser);
124
+ if (!text.trim()) return { journey: null, reason: null };
125
+
126
+ const buildMatch = /(?:\/mm-build|\$mm-build|\/mm:build)\s+([a-z0-9][a-z0-9_-]*)/.exec(text);
127
+ if (buildMatch) {
128
+ const slug = buildMatch[1] as string;
129
+ if (slug in aliases) return { journey: slug, reason: "explicit build command" };
130
+ }
131
+
132
+ const candidates: { slug: string; alias: string; reason: string }[] = [];
133
+ for (const [slug, values] of Object.entries(aliases)) {
134
+ for (const alias of values) {
135
+ const escaped = escapeRegExp(alias);
136
+ // Python's `$` without MULTILINE means end of string, or immediately
137
+ // before a trailing newline -- NOT the end of every line. A JavaScript
138
+ // `m` flag would match an alias sitting mid-text and silently widen the
139
+ // rule that decides what `--apply` rewrites, so the trailing-newline case
140
+ // is spelled out with a lookahead instead.
141
+ if (new RegExp(`(?:^|\\n)\\s*${escaped}\\s*(?=\\n?$)`).test(text)) {
142
+ candidates.push({ slug, alias, reason: "standalone journey slug/name" });
143
+ break;
144
+ }
145
+ const activation =
146
+ `(?:vamos|quero|preciso|let'?s)\\s+` +
147
+ `(?:retomar\\s+)?(?:trabalhar|ativar|continuar|retomar|work)` +
148
+ `(?:\\s+\\w+){0,5}\\s+(?:journey|jornada|travessia|projeto|project)?\\s*` +
149
+ `(?:no|na|em|on|with)?\\s*${escaped}\\b` +
150
+ `(?=$|\\s*[.,;:!?)]|\\s+(?:em|in|journey|jornada|travessia|modo)\\b)`;
151
+ if (new RegExp(activation).test(text)) {
152
+ candidates.push({ slug, alias, reason: "activation phrase" });
153
+ break;
154
+ }
155
+ }
156
+ }
157
+
158
+ if (candidates.length === 0) return { journey: null, reason: null };
159
+ candidates.sort((left, right) => right.alias.length - left.alias.length);
160
+ const best = candidates[0] as { slug: string; alias: string; reason: string };
161
+ if (candidates.length > 1 && best.alias.length === (candidates[1]?.alias.length ?? -1)) {
162
+ return { journey: null, reason: null };
163
+ }
164
+ return { journey: best.slug, reason: best.reason };
165
+ }