mirror-mind 0.0.1 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (474) hide show
  1. package/.claude/hooks/log-assistant-reply.sh +85 -0
  2. package/.claude/hooks/log-session-end.sh +85 -0
  3. package/.claude/hooks/log-user-prompt.sh +85 -0
  4. package/.claude/hooks/mirror-inject.sh +85 -0
  5. package/.claude/hooks/session-start.sh +85 -0
  6. package/.gemini/hooks/log-assistant.sh +85 -0
  7. package/.gemini/hooks/log-user.sh +85 -0
  8. package/.gemini/hooks/session-end.sh +85 -0
  9. package/.gemini/hooks/session-start.sh +85 -0
  10. package/.pi/extensions/mirror-logger.ts +646 -0
  11. package/.pi/skills/mm-backup/SKILL.md +21 -0
  12. package/.pi/skills/mm-build/SKILL.md +1206 -0
  13. package/.pi/skills/mm-consolidate/SKILL.md +112 -0
  14. package/.pi/skills/mm-consult/SKILL.md +70 -0
  15. package/.pi/skills/mm-conversations/SKILL.md +16 -0
  16. package/.pi/skills/mm-discard/SKILL.md +39 -0
  17. package/.pi/skills/mm-explore/SKILL.md +255 -0
  18. package/.pi/skills/mm-help/SKILL.md +74 -0
  19. package/.pi/skills/mm-identity/SKILL.md +96 -0
  20. package/.pi/skills/mm-journal/SKILL.md +16 -0
  21. package/.pi/skills/mm-journey/SKILL.md +23 -0
  22. package/.pi/skills/mm-journeys/SKILL.md +15 -0
  23. package/.pi/skills/mm-memories/SKILL.md +17 -0
  24. package/.pi/skills/mm-mirror/SKILL.md +98 -0
  25. package/.pi/skills/mm-mute/SKILL.md +18 -0
  26. package/.pi/skills/mm-new/SKILL.md +18 -0
  27. package/.pi/skills/mm-recall/SKILL.md +17 -0
  28. package/.pi/skills/mm-release-notes/SKILL.md +52 -0
  29. package/.pi/skills/mm-seed/SKILL.md +19 -0
  30. package/.pi/skills/mm-shadow/SKILL.md +117 -0
  31. package/.pi/skills/mm-soul/SKILL.md +502 -0
  32. package/.pi/skills/mm-tasks/SKILL.md +24 -0
  33. package/.pi/skills/mm-update/SKILL.md +32 -0
  34. package/.pi/skills/mm-week/SKILL.md +28 -0
  35. package/.pi/skills/mm-welcome/SKILL.md +24 -0
  36. package/AGENTS.md +188 -0
  37. package/README.md +126 -9
  38. package/bin/loader.js +67 -0
  39. package/bin/mirror-hook.js +7 -0
  40. package/bin/mirror.js +7 -0
  41. package/docs/releases/index.md +100 -0
  42. package/docs/releases/pending-cutoffs.md +510 -0
  43. package/docs/releases/v0.10.0.md +123 -0
  44. package/docs/releases/v0.10.1.md +66 -0
  45. package/docs/releases/v0.10.10.md +51 -0
  46. package/docs/releases/v0.10.11.md +39 -0
  47. package/docs/releases/v0.10.12.md +57 -0
  48. package/docs/releases/v0.10.13.md +35 -0
  49. package/docs/releases/v0.10.14.md +42 -0
  50. package/docs/releases/v0.10.15.md +53 -0
  51. package/docs/releases/v0.10.16.md +35 -0
  52. package/docs/releases/v0.10.17.md +43 -0
  53. package/docs/releases/v0.10.2.md +64 -0
  54. package/docs/releases/v0.10.3.md +78 -0
  55. package/docs/releases/v0.10.4.md +42 -0
  56. package/docs/releases/v0.10.5.md +57 -0
  57. package/docs/releases/v0.10.6.md +47 -0
  58. package/docs/releases/v0.10.7.md +53 -0
  59. package/docs/releases/v0.10.8.md +49 -0
  60. package/docs/releases/v0.10.9.md +37 -0
  61. package/docs/releases/v0.11.0.md +86 -0
  62. package/docs/releases/v0.12.0.md +67 -0
  63. package/docs/releases/v0.13.0.md +64 -0
  64. package/docs/releases/v0.14.0.md +68 -0
  65. package/docs/releases/v0.15.0.md +48 -0
  66. package/docs/releases/v0.16.0.md +50 -0
  67. package/docs/releases/v0.16.1.md +55 -0
  68. package/docs/releases/v0.16.2.md +36 -0
  69. package/docs/releases/v0.17.0.md +50 -0
  70. package/docs/releases/v0.17.1.md +58 -0
  71. package/docs/releases/v0.17.2.md +42 -0
  72. package/docs/releases/v0.17.3.md +58 -0
  73. package/docs/releases/v0.18.0.md +84 -0
  74. package/docs/releases/v0.18.1.md +51 -0
  75. package/docs/releases/v0.18.2.md +69 -0
  76. package/docs/releases/v0.19.0.md +85 -0
  77. package/docs/releases/v0.19.1.md +46 -0
  78. package/docs/releases/v0.20.0.md +85 -0
  79. package/docs/releases/v0.21.0.md +61 -0
  80. package/docs/releases/v0.21.1.md +47 -0
  81. package/docs/releases/v0.22.0.md +77 -0
  82. package/docs/releases/v0.22.1.md +42 -0
  83. package/docs/releases/v0.23.0.md +105 -0
  84. package/docs/releases/v0.24.0.md +125 -0
  85. package/docs/releases/v0.25.0.md +107 -0
  86. package/docs/releases/v0.26.0.md +139 -0
  87. package/docs/releases/v0.26.1.md +65 -0
  88. package/docs/releases/v0.26.2.md +43 -0
  89. package/docs/releases/v0.27.0.md +63 -0
  90. package/docs/releases/v0.28.0.md +59 -0
  91. package/docs/releases/v0.29.0.md +61 -0
  92. package/docs/releases/v0.29.1.md +59 -0
  93. package/docs/releases/v0.29.2.md +59 -0
  94. package/docs/releases/v0.29.3.md +93 -0
  95. package/docs/releases/v0.30.0.md +75 -0
  96. package/docs/releases/v0.30.1.md +93 -0
  97. package/docs/releases/v0.30.2.md +66 -0
  98. package/docs/releases/v0.31.0.md +122 -0
  99. package/docs/releases/v0.31.1.md +142 -0
  100. package/docs/releases/v0.31.10.md +118 -0
  101. package/docs/releases/v0.31.11.md +88 -0
  102. package/docs/releases/v0.31.12.md +105 -0
  103. package/docs/releases/v0.31.13.md +99 -0
  104. package/docs/releases/v0.31.14.md +97 -0
  105. package/docs/releases/v0.31.2.md +115 -0
  106. package/docs/releases/v0.31.3.md +142 -0
  107. package/docs/releases/v0.31.4.md +117 -0
  108. package/docs/releases/v0.31.5.md +155 -0
  109. package/docs/releases/v0.31.6.md +132 -0
  110. package/docs/releases/v0.31.7.md +132 -0
  111. package/docs/releases/v0.31.8.md +120 -0
  112. package/docs/releases/v0.31.9.md +120 -0
  113. package/docs/releases/v0.32.0.md +261 -0
  114. package/docs/releases/v0.32.1.md +59 -0
  115. package/docs/releases/v0.33.0.md +164 -0
  116. package/docs/releases/v0.8.0.md +118 -0
  117. package/docs/releases/v0.9.0.md +98 -0
  118. package/docs/releases/v0.9.1.md +83 -0
  119. package/package.json +102 -6
  120. package/plugins/mirror-mind/.claude-plugin/plugin.json +17 -0
  121. package/plugins/mirror-mind/hooks/hooks.json +62 -0
  122. package/plugins/mirror-mind/hooks/log-assistant-reply.sh +98 -0
  123. package/plugins/mirror-mind/hooks/log-session-end.sh +98 -0
  124. package/plugins/mirror-mind/hooks/log-user-prompt.sh +98 -0
  125. package/plugins/mirror-mind/hooks/mirror-inject.sh +98 -0
  126. package/plugins/mirror-mind/hooks/session-start.sh +98 -0
  127. package/plugins/mirror-mind/skills/mm-backup/SKILL.md +27 -0
  128. package/plugins/mirror-mind/skills/mm-build/SKILL.md +1206 -0
  129. package/plugins/mirror-mind/skills/mm-consolidate/SKILL.md +112 -0
  130. package/plugins/mirror-mind/skills/mm-consult/SKILL.md +83 -0
  131. package/plugins/mirror-mind/skills/mm-conversations/SKILL.md +17 -0
  132. package/plugins/mirror-mind/skills/mm-discard/SKILL.md +40 -0
  133. package/plugins/mirror-mind/skills/mm-explore/SKILL.md +241 -0
  134. package/plugins/mirror-mind/skills/mm-help/SKILL.md +104 -0
  135. package/plugins/mirror-mind/skills/mm-identity/SKILL.md +96 -0
  136. package/plugins/mirror-mind/skills/mm-journal/SKILL.md +21 -0
  137. package/plugins/mirror-mind/skills/mm-journey/SKILL.md +37 -0
  138. package/plugins/mirror-mind/skills/mm-journeys/SKILL.md +15 -0
  139. package/plugins/mirror-mind/skills/mm-memories/SKILL.md +20 -0
  140. package/plugins/mirror-mind/skills/mm-mirror/SKILL.md +67 -0
  141. package/plugins/mirror-mind/skills/mm-mute/SKILL.md +18 -0
  142. package/plugins/mirror-mind/skills/mm-new/SKILL.md +18 -0
  143. package/plugins/mirror-mind/skills/mm-recall/SKILL.md +17 -0
  144. package/plugins/mirror-mind/skills/mm-release-notes/SKILL.md +31 -0
  145. package/plugins/mirror-mind/skills/mm-seed/SKILL.md +15 -0
  146. package/plugins/mirror-mind/skills/mm-shadow/SKILL.md +117 -0
  147. package/plugins/mirror-mind/skills/mm-soul/SKILL.md +488 -0
  148. package/plugins/mirror-mind/skills/mm-tasks/SKILL.md +36 -0
  149. package/plugins/mirror-mind/skills/mm-update/SKILL.md +30 -0
  150. package/plugins/mirror-mind/skills/mm-week/SKILL.md +46 -0
  151. package/plugins/mirror-mind/skills/mm-welcome/SKILL.md +22 -0
  152. package/scripts/codex-hooks/session-end.sh +85 -0
  153. package/scripts/codex-hooks/session-start.sh +85 -0
  154. package/scripts/codex-mirror.sh +48 -0
  155. package/templates/identity/README.md +119 -0
  156. package/templates/identity/ego/behavior.yaml +52 -0
  157. package/templates/identity/ego/constraints.yaml +15 -0
  158. package/templates/identity/ego/identity.yaml +43 -0
  159. package/templates/identity/journeys/personal-growth.yaml +39 -0
  160. package/templates/identity/organization/identity.yaml +24 -0
  161. package/templates/identity/organization/principles.yaml +17 -0
  162. package/templates/identity/personas/coach.yaml +94 -0
  163. package/templates/identity/personas/designer.yaml +93 -0
  164. package/templates/identity/personas/doctor.yaml +96 -0
  165. package/templates/identity/personas/engineer.yaml +77 -0
  166. package/templates/identity/personas/financial.yaml +97 -0
  167. package/templates/identity/personas/prompt-engineer.yaml +122 -0
  168. package/templates/identity/personas/researcher.yaml +88 -0
  169. package/templates/identity/personas/strategist.yaml +91 -0
  170. package/templates/identity/personas/teacher.yaml +95 -0
  171. package/templates/identity/personas/therapist.yaml +91 -0
  172. package/templates/identity/personas/thinker.yaml +74 -0
  173. package/templates/identity/personas/writer.yaml +76 -0
  174. package/templates/identity/self/config.yaml +16 -0
  175. package/templates/identity/self/soul.yaml +105 -0
  176. package/templates/identity/user/identity.yaml +30 -0
  177. package/ts/src/backup/staging.ts +62 -0
  178. package/ts/src/backup/zipBackup.ts +267 -0
  179. package/ts/src/backup/zipReader.ts +109 -0
  180. package/ts/src/backup/zipWriter.ts +148 -0
  181. package/ts/src/builder/activeCheckpoint.ts +134 -0
  182. package/ts/src/builder/aggregateStatus.ts +34 -0
  183. package/ts/src/builder/approve.ts +149 -0
  184. package/ts/src/builder/argv.ts +292 -0
  185. package/ts/src/builder/ariadMethod.ts +536 -0
  186. package/ts/src/builder/artifacts/artifactSurfaces.ts +106 -0
  187. package/ts/src/builder/artifacts/artifactWriter.ts +113 -0
  188. package/ts/src/builder/artifacts/closureArtifacts.ts +199 -0
  189. package/ts/src/builder/artifacts/deliveryStoryArtifacts.ts +316 -0
  190. package/ts/src/builder/artifacts/planArtifacts.ts +242 -0
  191. package/ts/src/builder/artifacts/scaffoldSections.ts +273 -0
  192. package/ts/src/builder/artifacts/scaffoldState.ts +227 -0
  193. package/ts/src/builder/artifacts/storyFiles.ts +61 -0
  194. package/ts/src/builder/artifacts/storyIndex.ts +166 -0
  195. package/ts/src/builder/cadence.ts +59 -0
  196. package/ts/src/builder/card.ts +192 -0
  197. package/ts/src/builder/checkpointRefused.ts +66 -0
  198. package/ts/src/builder/closure.ts +964 -0
  199. package/ts/src/builder/commands.ts +2053 -0
  200. package/ts/src/builder/cursorPosition.ts +176 -0
  201. package/ts/src/builder/cursorTransitions.ts +77 -0
  202. package/ts/src/builder/deliveryCursor.ts +577 -0
  203. package/ts/src/builder/deliveryStoryClosure.ts +521 -0
  204. package/ts/src/builder/deliveryStoryPlan.ts +536 -0
  205. package/ts/src/builder/deliveryStoryReady.ts +160 -0
  206. package/ts/src/builder/deliveryStoryRoadmapClosure.ts +151 -0
  207. package/ts/src/builder/expand.ts +336 -0
  208. package/ts/src/builder/flowUnit.ts +339 -0
  209. package/ts/src/builder/homeSurface.ts +122 -0
  210. package/ts/src/builder/implementationGuard.ts +125 -0
  211. package/ts/src/builder/index.ts +11 -0
  212. package/ts/src/builder/lifecycleRefusal.ts +92 -0
  213. package/ts/src/builder/lifecycleRibbon.ts +159 -0
  214. package/ts/src/builder/load.ts +313 -0
  215. package/ts/src/builder/methodAdoption.ts +113 -0
  216. package/ts/src/builder/methodDefinition.ts +429 -0
  217. package/ts/src/builder/methodInspection.ts +371 -0
  218. package/ts/src/builder/plan.ts +408 -0
  219. package/ts/src/builder/planPreauthorization.ts +265 -0
  220. package/ts/src/builder/prepare.ts +225 -0
  221. package/ts/src/builder/projectPaths.ts +64 -0
  222. package/ts/src/builder/pull.ts +356 -0
  223. package/ts/src/builder/pullCandidates.ts +463 -0
  224. package/ts/src/builder/pullCandidatesRender.ts +238 -0
  225. package/ts/src/builder/refinementField.ts +93 -0
  226. package/ts/src/builder/releaseIntent.ts +173 -0
  227. package/ts/src/builder/resumeState.ts +84 -0
  228. package/ts/src/builder/resumeSurface.ts +149 -0
  229. package/ts/src/builder/roadmapGrammar.ts +300 -0
  230. package/ts/src/builder/roadmapScan.ts +97 -0
  231. package/ts/src/builder/roadmapScope.ts +210 -0
  232. package/ts/src/builder/scopePhrases.ts +146 -0
  233. package/ts/src/builder/storyPaths.ts +196 -0
  234. package/ts/src/builder/storyPlanPreauthorization.ts +287 -0
  235. package/ts/src/builder/surfaceProtocol.ts +42 -0
  236. package/ts/src/builder/templateGeneration.ts +148 -0
  237. package/ts/src/builder/transition.ts +178 -0
  238. package/ts/src/consult/args.ts +99 -0
  239. package/ts/src/consult/core.ts +122 -0
  240. package/ts/src/consult/modelCatalog.ts +55 -0
  241. package/ts/src/consult/render.ts +47 -0
  242. package/ts/src/conversation/append.ts +414 -0
  243. package/ts/src/conversation/backfill.ts +387 -0
  244. package/ts/src/conversation/closeTail.ts +467 -0
  245. package/ts/src/conversation/extraction.ts +333 -0
  246. package/ts/src/conversation/extractionDriver.ts +130 -0
  247. package/ts/src/conversation/extractionRun.ts +70 -0
  248. package/ts/src/conversation/hookPayload.ts +51 -0
  249. package/ts/src/conversation/journeyInference.ts +165 -0
  250. package/ts/src/conversation/journeyRepair.ts +114 -0
  251. package/ts/src/conversation/lifecycleFaces.ts +197 -0
  252. package/ts/src/conversation/lifecycleWrites.ts +313 -0
  253. package/ts/src/conversation/listing.ts +77 -0
  254. package/ts/src/conversation/logger.ts +851 -0
  255. package/ts/src/conversation/loggerCli.ts +321 -0
  256. package/ts/src/conversation/loggerRuntime.ts +259 -0
  257. package/ts/src/conversation/metadataLifecycle.ts +443 -0
  258. package/ts/src/conversation/recall.ts +66 -0
  259. package/ts/src/conversation/sessionComposites.ts +258 -0
  260. package/ts/src/conversation/sessionImport.ts +90 -0
  261. package/ts/src/conversation/transcriptBackfill.ts +141 -0
  262. package/ts/src/cultivation/applyActions.ts +241 -0
  263. package/ts/src/cultivation/cluster.ts +86 -0
  264. package/ts/src/cultivation/consolidationStore.ts +281 -0
  265. package/ts/src/cultivation/promptContext.ts +62 -0
  266. package/ts/src/cultivation/propose.ts +305 -0
  267. package/ts/src/cultivation/scan.ts +155 -0
  268. package/ts/src/db/backupGate.ts +47 -0
  269. package/ts/src/db/bootstrap.ts +108 -0
  270. package/ts/src/db/bootstrapLock.ts +223 -0
  271. package/ts/src/db/copyGuard.ts +60 -0
  272. package/ts/src/db/database.ts +397 -0
  273. package/ts/src/db/decode.ts +66 -0
  274. package/ts/src/db/ftsIntegrity.ts +41 -0
  275. package/ts/src/db/migrateOnOpen.ts +214 -0
  276. package/ts/src/db/migrations.ts +576 -0
  277. package/ts/src/db/readyOnOpen.ts +35 -0
  278. package/ts/src/db/rowDecode.ts +42 -0
  279. package/ts/src/db/schema.ts +432 -0
  280. package/ts/src/db/schemaInventory.ts +225 -0
  281. package/ts/src/db/schemaInventorySnapshot.ts +2107 -0
  282. package/ts/src/db/schemaState.ts +91 -0
  283. package/ts/src/db/schemaTsDivergence.ts +114 -0
  284. package/ts/src/descriptor/descriptorRead.ts +49 -0
  285. package/ts/src/docs/docsLint.ts +265 -0
  286. package/ts/src/explorer/handoff.ts +522 -0
  287. package/ts/src/explorer/render.ts +335 -0
  288. package/ts/src/explorer/story.ts +769 -0
  289. package/ts/src/explorer/transition.ts +72 -0
  290. package/ts/src/extensions/bindings.ts +170 -0
  291. package/ts/src/extensions/catalog.ts +249 -0
  292. package/ts/src/extensions/catalogCommands.ts +513 -0
  293. package/ts/src/extensions/catalogWrites.ts +652 -0
  294. package/ts/src/extensions/contextRuntime.ts +325 -0
  295. package/ts/src/extensions/dispatch.ts +374 -0
  296. package/ts/src/extensions/errors.ts +24 -0
  297. package/ts/src/extensions/manifest.ts +356 -0
  298. package/ts/src/extensions/migrations.ts +350 -0
  299. package/ts/src/extraction/conversation.ts +304 -0
  300. package/ts/src/extraction/conversationMetadata.ts +206 -0
  301. package/ts/src/extraction/fencing.ts +106 -0
  302. package/ts/src/extraction/json.ts +17 -0
  303. package/ts/src/extraction/prompts.ts +470 -0
  304. package/ts/src/frontDoor/args.ts +30 -0
  305. package/ts/src/frontDoor/argvShape.ts +106 -0
  306. package/ts/src/frontDoor/buildLoadRuntime.ts +139 -0
  307. package/ts/src/frontDoor/buildRoute.ts +398 -0
  308. package/ts/src/frontDoor/cli.ts +1872 -0
  309. package/ts/src/frontDoor/cloneRoleGuard.ts +98 -0
  310. package/ts/src/frontDoor/consultRoute.ts +50 -0
  311. package/ts/src/frontDoor/contentTailRoute.ts +239 -0
  312. package/ts/src/frontDoor/conversationLoggerRoute.ts +83 -0
  313. package/ts/src/frontDoor/cultivationRoute.ts +210 -0
  314. package/ts/src/frontDoor/dbPath.ts +131 -0
  315. package/ts/src/frontDoor/dbSafetyToolsRoute.ts +240 -0
  316. package/ts/src/frontDoor/exploreRoute.ts +572 -0
  317. package/ts/src/frontDoor/extensionCatalogRoute.ts +252 -0
  318. package/ts/src/frontDoor/frontDoorLog.ts +57 -0
  319. package/ts/src/frontDoor/identityWrite.ts +36 -0
  320. package/ts/src/frontDoor/journeyWriteRoute.ts +26 -0
  321. package/ts/src/frontDoor/lifecycleWriteRoute.ts +110 -0
  322. package/ts/src/frontDoor/liveBackup.ts +100 -0
  323. package/ts/src/frontDoor/mirrorModeRoute.ts +163 -0
  324. package/ts/src/frontDoor/namedRefusal.ts +35 -0
  325. package/ts/src/frontDoor/nodeSupport.ts +24 -0
  326. package/ts/src/frontDoor/render/consolidate.ts +199 -0
  327. package/ts/src/frontDoor/render/conversations.ts +33 -0
  328. package/ts/src/frontDoor/render/cultivationShared.ts +21 -0
  329. package/ts/src/frontDoor/render/descriptor.ts +20 -0
  330. package/ts/src/frontDoor/render/detectPersona.ts +50 -0
  331. package/ts/src/frontDoor/render/icons.ts +13 -0
  332. package/ts/src/frontDoor/render/identity.ts +59 -0
  333. package/ts/src/frontDoor/render/identityRows.ts +17 -0
  334. package/ts/src/frontDoor/render/inspectPersona.ts +53 -0
  335. package/ts/src/frontDoor/render/journeyStatus.ts +78 -0
  336. package/ts/src/frontDoor/render/journeys.ts +79 -0
  337. package/ts/src/frontDoor/render/list.ts +41 -0
  338. package/ts/src/frontDoor/render/memories.ts +67 -0
  339. package/ts/src/frontDoor/render/recall.ts +49 -0
  340. package/ts/src/frontDoor/render/shadow.ts +155 -0
  341. package/ts/src/frontDoor/render/tasks.ts +99 -0
  342. package/ts/src/frontDoor/render/tasksImportSync.ts +63 -0
  343. package/ts/src/frontDoor/render/week.ts +113 -0
  344. package/ts/src/frontDoor/routing.ts +1001 -0
  345. package/ts/src/frontDoor/runtimeRoute.ts +623 -0
  346. package/ts/src/frontDoor/searchRoute.ts +166 -0
  347. package/ts/src/frontDoor/seedPaths.ts +98 -0
  348. package/ts/src/frontDoor/sessionStartWelcomeRoute.ts +166 -0
  349. package/ts/src/frontDoor/soulRoute.ts +433 -0
  350. package/ts/src/frontDoor/stdinSource.ts +37 -0
  351. package/ts/src/frontDoor/tasksImportSyncRoute.ts +110 -0
  352. package/ts/src/frontDoor/tasksWriteRoute.ts +101 -0
  353. package/ts/src/frontDoor/usage.ts +176 -0
  354. package/ts/src/guards/claudePlugin.ts +298 -0
  355. package/ts/src/guards/packContents.ts +306 -0
  356. package/ts/src/guards/piSourcedSkills.ts +70 -0
  357. package/ts/src/guards/retiredRouteShadows.ts +57 -0
  358. package/ts/src/guards/retiredSurfaces.ts +564 -0
  359. package/ts/src/hooks/claude.ts +214 -0
  360. package/ts/src/hooks/codex.ts +45 -0
  361. package/ts/src/hooks/gemini.ts +126 -0
  362. package/ts/src/hooks/main.ts +109 -0
  363. package/ts/src/hooks/mirrorState.ts +71 -0
  364. package/ts/src/hooks/payload.ts +73 -0
  365. package/ts/src/hooks/runtime.ts +179 -0
  366. package/ts/src/hooks/runtimeDetection.ts +34 -0
  367. package/ts/src/identity/applyConsolidationIdentityUpdate.ts +67 -0
  368. package/ts/src/identity/descriptorGenerate.ts +162 -0
  369. package/ts/src/identity/identityEdit.ts +143 -0
  370. package/ts/src/identity/identityKey.ts +77 -0
  371. package/ts/src/identity/identityRead.ts +69 -0
  372. package/ts/src/identity/identityStore.ts +80 -0
  373. package/ts/src/identity/journeyListing.ts +53 -0
  374. package/ts/src/identity/personaInspect.ts +44 -0
  375. package/ts/src/identity/personaListing.ts +37 -0
  376. package/ts/src/identity/setIdentity.ts +64 -0
  377. package/ts/src/index.ts +231 -0
  378. package/ts/src/init/init.ts +111 -0
  379. package/ts/src/journey/journeyOptions.ts +131 -0
  380. package/ts/src/journey/journeyRemoval.ts +158 -0
  381. package/ts/src/journey/journeyStatus.ts +149 -0
  382. package/ts/src/journey/journeySyncFile.ts +87 -0
  383. package/ts/src/journey/journeyWrite.ts +203 -0
  384. package/ts/src/journey/parentJourney.ts +41 -0
  385. package/ts/src/journey/validateParentJourney.ts +55 -0
  386. package/ts/src/mcp/boundary.ts +151 -0
  387. package/ts/src/mcp/guards.ts +244 -0
  388. package/ts/src/mcp/main.ts +121 -0
  389. package/ts/src/mcp/payload.ts +141 -0
  390. package/ts/src/mcp/protocol.ts +141 -0
  391. package/ts/src/mcp/readModels.ts +241 -0
  392. package/ts/src/mcp/registry.ts +201 -0
  393. package/ts/src/mcp/serve.ts +120 -0
  394. package/ts/src/mcp/tools/deterministic.ts +137 -0
  395. package/ts/src/mcp/tools/providerCrossing.ts +187 -0
  396. package/ts/src/mcp/wire.ts +8 -0
  397. package/ts/src/memory/journal.ts +180 -0
  398. package/ts/src/memory/listing.ts +107 -0
  399. package/ts/src/memory/memoryWrite.ts +88 -0
  400. package/ts/src/memory/reinforcement.ts +35 -0
  401. package/ts/src/mirror/context.ts +161 -0
  402. package/ts/src/mirror/defaultResolution.ts +199 -0
  403. package/ts/src/mirror/mirrorForm.ts +43 -0
  404. package/ts/src/mirror/orchestration.ts +364 -0
  405. package/ts/src/mirror/reception.ts +194 -0
  406. package/ts/src/mirror/render.ts +75 -0
  407. package/ts/src/mirror/runtimeSession.ts +205 -0
  408. package/ts/src/mode/operatingMode.ts +148 -0
  409. package/ts/src/observability/callOutcome.ts +115 -0
  410. package/ts/src/observability/inspectLedger.ts +329 -0
  411. package/ts/src/observability/ledgerHooks.ts +126 -0
  412. package/ts/src/observability/llmCalls.ts +154 -0
  413. package/ts/src/persona/detectPersona.ts +81 -0
  414. package/ts/src/planning/promptAssembly.ts +111 -0
  415. package/ts/src/planning/weekPending.ts +81 -0
  416. package/ts/src/planning/weekPlan.ts +183 -0
  417. package/ts/src/planning/weekSave.ts +117 -0
  418. package/ts/src/providers/config.ts +189 -0
  419. package/ts/src/providers/cost.ts +64 -0
  420. package/ts/src/providers/credits.ts +204 -0
  421. package/ts/src/providers/embedding.ts +321 -0
  422. package/ts/src/providers/familyProviders.ts +147 -0
  423. package/ts/src/providers/llm.ts +334 -0
  424. package/ts/src/providers/openrouter.ts +244 -0
  425. package/ts/src/providers/redaction.ts +110 -0
  426. package/ts/src/providers/replay.ts +15 -0
  427. package/ts/src/providers/transport.ts +410 -0
  428. package/ts/src/repair/encodingRepair.ts +253 -0
  429. package/ts/src/runtime/backup.ts +151 -0
  430. package/ts/src/runtime/channel.ts +146 -0
  431. package/ts/src/runtime/config.ts +168 -0
  432. package/ts/src/runtime/diagnose.ts +686 -0
  433. package/ts/src/runtime/git.ts +510 -0
  434. package/ts/src/runtime/installKind.ts +220 -0
  435. package/ts/src/runtime/migrate.ts +154 -0
  436. package/ts/src/runtime/packageIdentity.ts +102 -0
  437. package/ts/src/runtime/releaseNotes.ts +244 -0
  438. package/ts/src/runtime/status.ts +557 -0
  439. package/ts/src/runtime/strategies/clone.ts +200 -0
  440. package/ts/src/runtime/strategies/package.ts +328 -0
  441. package/ts/src/runtime/treeRoot.ts +30 -0
  442. package/ts/src/runtime/update.ts +445 -0
  443. package/ts/src/runtime/updateGate.ts +40 -0
  444. package/ts/src/runtime/updatePipeline.ts +159 -0
  445. package/ts/src/runtime/version.ts +36 -0
  446. package/ts/src/runtime/wiring.ts +130 -0
  447. package/ts/src/search/memorySearch.ts +338 -0
  448. package/ts/src/search/ranker.ts +154 -0
  449. package/ts/src/seed/seed.ts +256 -0
  450. package/ts/src/seed/seedYaml.ts +133 -0
  451. package/ts/src/soul/apply.ts +218 -0
  452. package/ts/src/soul/harvest.ts +267 -0
  453. package/ts/src/soul/prompts/soul_beauty_voice.md +103 -0
  454. package/ts/src/soul/prompts/soul_self_voice.md +219 -0
  455. package/ts/src/soul/prompts/soul_wisdom_voice.md +121 -0
  456. package/ts/src/soul/prompts.ts +85 -0
  457. package/ts/src/soul/render.ts +358 -0
  458. package/ts/src/soul/state.ts +212 -0
  459. package/ts/src/soul/transition.ts +37 -0
  460. package/ts/src/tasks/journeyPathParse.ts +113 -0
  461. package/ts/src/tasks/taskImportSync.ts +142 -0
  462. package/ts/src/tasks/taskStore.ts +312 -0
  463. package/ts/src/tasks/weekView.ts +120 -0
  464. package/ts/src/util/clipCodePoints.ts +26 -0
  465. package/ts/src/util/paths.ts +63 -0
  466. package/ts/src/util/program.ts +9 -0
  467. package/ts/src/util/pyGenerators.ts +144 -0
  468. package/ts/src/util/pythonText.ts +317 -0
  469. package/ts/src/util/semver.ts +45 -0
  470. package/ts/src/util/shellWord.ts +25 -0
  471. package/ts/src/util/slug.ts +64 -0
  472. package/ts/src/welcome/card.ts +339 -0
  473. package/ts/src/welcome/statusLine.ts +101 -0
  474. package/ts/src/welcome/updateCache.ts +129 -0
@@ -0,0 +1,258 @@
1
+ // Session lifecycle composites (CV22.DS7.US10 slice D).
2
+ //
3
+ // Ports `session_start`, `session_start_fast`, `session_maintenance`,
4
+ // `close_stale_orphans`, `retitle_pending_conversations`, and
5
+ // `_reset_session_orientation` from `src/memory/cli/conversation_logger.py`.
6
+ //
7
+ // These are the composites Pi runs at session boundaries, and their REPORT is
8
+ // the user-visible surface: Pi prints it. Parity is therefore string-exact in
9
+ // grammar, labels, order, and the warning tails — everything except the
10
+ // elapsed seconds, which are wall-clock and cannot be byte-stable across runs
11
+ // or cores.
12
+ //
13
+ // The clock is injected rather than read (Navigator decision 2, 2026-09-03):
14
+ // tests pin durations, and cross-core comparison normalizes the timing token
15
+ // only after validating its exact grammar, so a re-worded or re-punctuated
16
+ // report still fails. Freezing real timings would have faked the evidence
17
+ // instead of isolating it.
18
+
19
+ import {
20
+ countCarriedOverConversations,
21
+ countConversationsWithExtractionStatus,
22
+ countQuarantinedConversations,
23
+ extractPending,
24
+ } from "#conversation/extractionDriver.ts";
25
+ import { setMute } from "#conversation/logger.ts";
26
+ import type { WritableDatabase } from "#db/database.ts";
27
+ import { GLOBAL_STICKY_DEFAULTS_SESSION_ID } from "#mirror/runtimeSession.ts";
28
+ import { deactivateOperatingMode } from "#mode/operatingMode.ts";
29
+ import { nowIso } from "#util/pyGenerators.ts";
30
+
31
+ /** Python's `close_stale_orphans` default. */
32
+ export const STALE_ORPHAN_THRESHOLD_MINUTES = 30;
33
+ /** Python's `retitle_pending_conversations` default. */
34
+ export const RETITLE_LIMIT = 5;
35
+
36
+ export interface MaintenanceDeps {
37
+ /** Close one conversation through the full close tail. Throwing is isolated. */
38
+ closeConversation: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
39
+ /** Improve one ended conversation's title; returns true when it changed. */
40
+ retitleConversation: (db: WritableDatabase, conversationId: string) => Promise<boolean> | boolean;
41
+ /** The DS5 orchestration behind replay with its accounting, as the driver injects it. */
42
+ runExtraction: (db: WritableDatabase, conversationId: string) => Promise<void> | void;
43
+ /**
44
+ * `backfillPiSessions` (slice E) bound to its resolved sessions directory.
45
+ * Injected rather than resolved here so the composite never reads the
46
+ * environment itself; the front door owns that resolution.
47
+ */
48
+ backfillPiSessions: (db: WritableDatabase) => Promise<number> | number;
49
+ /** Python's `MEMORY_MAINTENANCE_MAX_EXTRACTIONS` (AI-05); defaults to the released budget. */
50
+ extractionLimit?: number;
51
+ /** Monotonic seconds, injected so report timings are pinnable. */
52
+ monotonic?: () => number;
53
+ now?: () => string;
54
+ }
55
+
56
+ /**
57
+ * Python's `_reset_session_orientation`: a new runtime session starts from
58
+ * intention, not stale mode or journey context.
59
+ */
60
+ export function resetSessionOrientation(db: WritableDatabase, now: () => string = nowIso): void {
61
+ deactivateOperatingMode(db, null, now());
62
+ db.prepare("DELETE FROM runtime_sessions WHERE session_id = ?").run(
63
+ GLOBAL_STICKY_DEFAULTS_SESSION_ID,
64
+ );
65
+ }
66
+
67
+ /** Conversation ids bound to an active runtime session. */
68
+ function activeConversationIds(db: WritableDatabase): Set<string> {
69
+ const rows = db
70
+ .prepare(
71
+ "SELECT conversation_id FROM runtime_sessions WHERE active = 1 AND conversation_id IS NOT NULL",
72
+ )
73
+ .all() as Record<string, unknown>[];
74
+ return new Set(rows.map((row) => String(row.conversation_id)));
75
+ }
76
+
77
+ /** Python's `get_open_conversations_idle_since`. */
78
+ function openConversationsIdleSince(db: WritableDatabase, threshold: string): string[] {
79
+ const rows = db
80
+ .prepare(
81
+ `SELECT c.id FROM conversations c
82
+ WHERE c.ended_at IS NULL
83
+ AND (
84
+ (SELECT MAX(m.created_at) FROM messages m WHERE m.conversation_id = c.id) < ?
85
+ OR NOT EXISTS (SELECT 1 FROM messages m WHERE m.conversation_id = c.id)
86
+ )`,
87
+ )
88
+ .all(threshold) as Record<string, unknown>[];
89
+ return rows.map((row) => String(row.id));
90
+ }
91
+
92
+ /**
93
+ * Python's `close_stale_orphans`: end open conversations idle longer than the
94
+ * threshold, skipping any still bound to an active runtime session.
95
+ *
96
+ * The count increments even when closing raises: `ended_at` is written before
97
+ * the tail, so the orphan IS closed and an extraction failure must not stop the
98
+ * loop or under-report the work. That asymmetry is deliberate in Python.
99
+ *
100
+ * KNOWN UNBOUNDED SPEND (registered for Debt Review): AI-05 caps extraction per
101
+ * maintenance run, but this loop has no cap on orphan count and each orphan
102
+ * runs the full close tail. Ported as-is; naming it here so DS8 prices it.
103
+ */
104
+ export async function closeStaleOrphans(
105
+ db: WritableDatabase,
106
+ deps: Pick<MaintenanceDeps, "closeConversation" | "now"> & { thresholdMinutes?: number },
107
+ ): Promise<number> {
108
+ const thresholdMinutes = deps.thresholdMinutes ?? STALE_ORPHAN_THRESHOLD_MINUTES;
109
+ const now = deps.now ?? nowIso;
110
+ const threshold = new Date(Date.parse(now()) - thresholdMinutes * 60_000).toISOString();
111
+
112
+ const active = activeConversationIds(db);
113
+ let count = 0;
114
+ for (const conversationId of openConversationsIdleSince(db, threshold)) {
115
+ if (active.has(conversationId)) continue;
116
+ try {
117
+ await deps.closeConversation(db, conversationId);
118
+ } catch {
119
+ // The orphan is closed regardless; a failed tail must not stop the loop.
120
+ }
121
+ count += 1;
122
+ }
123
+ return count;
124
+ }
125
+
126
+ /**
127
+ * Python's `retitle_pending_conversations`: opportunistically improve a small
128
+ * batch of ended conversation titles at startup.
129
+ */
130
+ export async function retitlePendingConversations(
131
+ db: WritableDatabase,
132
+ deps: Pick<MaintenanceDeps, "retitleConversation"> & { limit?: number },
133
+ ): Promise<number> {
134
+ const limit = deps.limit ?? RETITLE_LIMIT;
135
+ if (limit <= 0) return 0;
136
+
137
+ let candidates: string[];
138
+ try {
139
+ const scanLimit = Math.max(limit * 10, 50);
140
+ const rows = db
141
+ .prepare(
142
+ `SELECT c.id
143
+ FROM conversations c
144
+ WHERE c.ended_at IS NOT NULL
145
+ AND EXISTS (SELECT 1 FROM messages m WHERE m.conversation_id = c.id AND m.role = 'user')
146
+ AND EXISTS (SELECT 1 FROM messages m WHERE m.conversation_id = c.id AND m.role = 'assistant')
147
+ ORDER BY c.ended_at DESC, c.started_at DESC
148
+ LIMIT ?`,
149
+ )
150
+ .all(scanLimit) as Record<string, unknown>[];
151
+ candidates = rows.map((row) => String(row.id));
152
+ } catch {
153
+ return 0;
154
+ }
155
+
156
+ let changed = 0;
157
+ for (const conversationId of candidates) {
158
+ if (changed >= limit) break;
159
+ try {
160
+ if (await deps.retitleConversation(db, conversationId)) changed += 1;
161
+ } catch {
162
+ // Python swallows per-conversation failures and continues the batch.
163
+ }
164
+ }
165
+ return changed;
166
+ }
167
+
168
+ interface TimedStep {
169
+ label: string;
170
+ count: number;
171
+ elapsed: number;
172
+ }
173
+
174
+ async function timedStep(
175
+ label: string,
176
+ monotonic: () => number,
177
+ run: () => Promise<number> | number,
178
+ ): Promise<TimedStep> {
179
+ const started = monotonic();
180
+ const count = await run();
181
+ return { label, count, elapsed: monotonic() - started };
182
+ }
183
+
184
+ /** Python's `%.1f` seconds rendering inside the report. */
185
+ function formatElapsed(elapsed: number): string {
186
+ return elapsed.toFixed(1);
187
+ }
188
+
189
+ /**
190
+ * Python's `session_maintenance`: four timed steps in a fixed order, then the
191
+ * warning tails. The step order is behavior, not presentation — closing stale
192
+ * orphans first is what makes their conversations eligible for the extraction
193
+ * step in the same run.
194
+ */
195
+ export async function sessionMaintenance(
196
+ db: WritableDatabase,
197
+ deps: MaintenanceDeps,
198
+ ): Promise<string> {
199
+ const monotonic = deps.monotonic ?? (() => performance.now() / 1000);
200
+
201
+ const steps: TimedStep[] = [
202
+ await timedStep("Closed stale conversations", monotonic, () =>
203
+ closeStaleOrphans(db, {
204
+ closeConversation: deps.closeConversation,
205
+ now: deps.now,
206
+ thresholdMinutes: STALE_ORPHAN_THRESHOLD_MINUTES,
207
+ }),
208
+ ),
209
+ await timedStep("Backfilled Pi sessions", monotonic, () => deps.backfillPiSessions(db)),
210
+ await timedStep("Retitled pending conversations", monotonic, () =>
211
+ retitlePendingConversations(db, { retitleConversation: deps.retitleConversation }),
212
+ ),
213
+ await timedStep("Extracted pending conversations", monotonic, () =>
214
+ extractPending(db, { runExtraction: deps.runExtraction, limit: deps.extractionLimit }),
215
+ ),
216
+ ];
217
+
218
+ const parts = ["Conversation maintenance complete."];
219
+ for (const step of steps) {
220
+ parts.push(`${step.label}: ${step.count} (${formatElapsed(step.elapsed)}s)`);
221
+ }
222
+
223
+ const quarantined = countQuarantinedConversations(db);
224
+ if (quarantined) {
225
+ parts.push(`⚠ ${quarantined} conversation(s) quarantined after repeated extraction failure`);
226
+ }
227
+ const parseFailed = countConversationsWithExtractionStatus(db, "parse_failed");
228
+ if (parseFailed) {
229
+ parts.push(`⚠ ${parseFailed} conversation(s) with unreadable model output (parse_failed)`);
230
+ }
231
+ const carriedOver = countCarriedOverConversations(db);
232
+ if (carriedOver) {
233
+ parts.push(
234
+ `${carriedOver} conversation(s) carried over to the next run ` +
235
+ "(maintenance extraction budget)",
236
+ );
237
+ }
238
+ return parts.join("\n");
239
+ }
240
+
241
+ /** Python's `session_start_fast`: unmute and reorient, defer the expensive work. */
242
+ export function sessionStartFast(db: WritableDatabase, mirrorHome: string, now = nowIso): string {
243
+ setMute(false, mirrorHome);
244
+ resetSessionOrientation(db, now);
245
+ return "Conversation logging ACTIVE. Maintenance deferred.";
246
+ }
247
+
248
+ /** Python's `session_start`: unmute, reorient, then run full maintenance. */
249
+ export async function sessionStart(
250
+ db: WritableDatabase,
251
+ mirrorHome: string,
252
+ deps: MaintenanceDeps,
253
+ ): Promise<string> {
254
+ setMute(false, mirrorHome);
255
+ resetSessionOrientation(db, deps.now ?? nowIso);
256
+ const maintenance = await sessionMaintenance(db, deps);
257
+ return ["Conversation logging ACTIVE.", maintenance].join("\n");
258
+ }
@@ -0,0 +1,90 @@
1
+ // Atomic closed-session import (CV22.DS7.US10 slice E).
2
+ //
3
+ // Ports `RuntimeSessionService.import_closed_conversation` from
4
+ // `src/memory/services/runtime_session.py`, the seam both backfills write
5
+ // through. It is the sibling of `getOrCreateSessionConversation` in
6
+ // `logger.ts` and shares its transaction discipline for the same reason:
7
+ // a backfill imports against the SAME `runtime_sessions` key a live hook
8
+ // binds (Pi's live session id is the session file path), and the two can
9
+ // interleave.
10
+ //
11
+ // Resolved decision 4B (plan.md): the binding check runs INSIDE the
12
+ // `BEGIN IMMEDIATE` transaction, so a binding a concurrent hook committed
13
+ // while this import waited for the write lock is seen and honored -- the
14
+ // import returns null instead of clobbering a live session with
15
+ // `active=0`/`closed_at`. The same atomicity removes the crash-window
16
+ // duplicate-import path: either the conversation, every message, and the
17
+ // binding all land, or nothing does.
18
+ //
19
+ // Titles are deliberately NOT set here. The two callers title differently
20
+ // (Pi through `setProvisionalTitle`, Codex through a bare column write) and
21
+ // Python applies both after commit; reproducing that asymmetry is the
22
+ // callers' job.
23
+
24
+ import type { LoggerDeps } from "#conversation/logger.ts";
25
+ import { type WritableDatabase, withTransaction } from "#db/database.ts";
26
+
27
+ export interface ImportedMessage {
28
+ role: string;
29
+ content: string;
30
+ createdAt: string;
31
+ }
32
+
33
+ export interface ClosedSessionImport {
34
+ interface: string;
35
+ messages: readonly ImportedMessage[];
36
+ endedAt: string;
37
+ }
38
+
39
+ /**
40
+ * Import a closed session transcript as one conversation bound to
41
+ * `sessionId`. Returns the new conversation id, or null when the session is
42
+ * already tracked -- including by a writer that won the binding while this
43
+ * call waited for the lock.
44
+ */
45
+ export function importClosedConversation(
46
+ db: WritableDatabase,
47
+ sessionId: string,
48
+ session: ClosedSessionImport,
49
+ deps: LoggerDeps,
50
+ ): string | null {
51
+ return withTransaction(db, () => {
52
+ const tracked = db
53
+ .prepare("SELECT 1 FROM runtime_sessions WHERE session_id = ?")
54
+ .get(sessionId);
55
+ if (tracked) return null;
56
+
57
+ // Python's Conversation(...) draws its id before its started_at; keep the
58
+ // same generator order so injected sequences line up with the oracle.
59
+ const conversationId = deps.newId();
60
+ const startedAt = deps.nowIso();
61
+ db.prepare(
62
+ `INSERT INTO conversations
63
+ (id, title, started_at, ended_at, interface, persona, journey, summary, tags, metadata)
64
+ VALUES (?, NULL, ?, ?, ?, NULL, NULL, NULL, NULL, NULL)`,
65
+ ).run(conversationId, startedAt, session.endedAt, session.interface);
66
+
67
+ const insertMessage = db.prepare(
68
+ `INSERT INTO messages (id, conversation_id, role, content, created_at, token_count, metadata)
69
+ VALUES (?, ?, ?, ?, ?, NULL, NULL)`,
70
+ );
71
+ for (const message of session.messages) {
72
+ insertMessage.run(
73
+ deps.newId(),
74
+ conversationId,
75
+ message.role,
76
+ message.content,
77
+ message.createdAt,
78
+ );
79
+ }
80
+
81
+ db.prepare(
82
+ `INSERT INTO runtime_sessions
83
+ (session_id, conversation_id, interface, mirror_active, persona, journey,
84
+ hook_injected, active, started_at, updated_at, closed_at, metadata)
85
+ VALUES (?, ?, ?, 0, NULL, NULL, 0, 0, ?, ?, ?, NULL)`,
86
+ ).run(sessionId, conversationId, session.interface, startedAt, startedAt, session.endedAt);
87
+
88
+ return conversationId;
89
+ });
90
+ }
@@ -0,0 +1,141 @@
1
+ // Assistant-turn backfill from a Claude transcript (CV22.DS7.US10 slice E).
2
+ //
3
+ // Ports `backfill_assistant_messages` from
4
+ // `src/memory/cli/conversation_logger.py` together with the two helpers it
5
+ // borrows from `src/memory/cli/transcript_export.py` (`parse_jsonl`,
6
+ // `_assistant_text`). `hook_session_end` calls it after ending the session --
7
+ // and, when the payload carries no session id at all, INSTEAD of ending one:
8
+ // that session-less route is the path US5's hook port deliberately left out.
9
+ //
10
+ // This is not an import: nothing is created or bound. It appends assistant
11
+ // turns to conversations that overlap the transcript's time window and logged
12
+ // no assistant message of their own, stamping each with `now` (Python's
13
+ // `Message` default), not with the transcript timestamp. Python commits every
14
+ // message as it goes and swallows nothing here; a malformed entry reached
15
+ // mid-walk raises with the earlier appends already committed. The port keeps
16
+ // that sequence so partial state matches, and leaves swallowing to the hook.
17
+
18
+ import { readFileSync } from "node:fs";
19
+ import type { LoggerDeps } from "#conversation/logger.ts";
20
+ import type { WritableDatabase } from "#db/database.ts";
21
+
22
+ function isRecord(value: unknown): value is Record<string, unknown> {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value);
24
+ }
25
+
26
+ /** Python `dict.get` on something that must be a dict; see `backfill.ts`. */
27
+ function getOr(container: unknown, key: string, fallback: unknown): unknown {
28
+ if (!isRecord(container)) throw new TypeError(`expected an object with '${key}'`);
29
+ return key in container ? container[key] : fallback;
30
+ }
31
+
32
+ function getFrom(container: unknown, key: string): unknown {
33
+ return getOr(container, key, undefined);
34
+ }
35
+
36
+ /** Python `parse_jsonl`: blank lines skipped, unparseable lines skipped. */
37
+ export function parseJsonl(path: string): unknown[] {
38
+ const entries: unknown[] = [];
39
+ for (const raw of readFileSync(path, "utf8").split(/\r?\n|\r/)) {
40
+ const line = raw.trim();
41
+ if (!line) continue;
42
+ try {
43
+ entries.push(JSON.parse(line));
44
+ } catch {
45
+ // json.JSONDecodeError -> continue
46
+ }
47
+ }
48
+ return entries;
49
+ }
50
+
51
+ /**
52
+ * Python `_assistant_text`: the stripped, non-empty `text` blocks of an
53
+ * assistant message joined by a blank line. Iterating a string or a dict
54
+ * yields no dict items (so no text); a null or number cannot be iterated and
55
+ * raises, as does a text block whose `text` is not a string.
56
+ */
57
+ export function assistantText(contentBlocks: unknown): string {
58
+ const iterable = Array.isArray(contentBlocks)
59
+ ? contentBlocks
60
+ : typeof contentBlocks === "string" || isRecord(contentBlocks)
61
+ ? []
62
+ : null;
63
+ if (iterable === null) throw new TypeError("assistant content is not iterable");
64
+ const parts: string[] = [];
65
+ for (const block of iterable) {
66
+ if (!isRecord(block) || block.type !== "text") continue;
67
+ const text = getOr(block, "text", "");
68
+ if (typeof text !== "string") throw new TypeError("text block is not a string");
69
+ const trimmed = text.trim();
70
+ if (trimmed) parts.push(trimmed);
71
+ }
72
+ return parts.join("\n\n");
73
+ }
74
+
75
+ /** Python `str` truthiness on the timestamp: absent, null, or empty is skipped. */
76
+ function timestampOf(entry: unknown): string | null {
77
+ const value = getOr(entry, "timestamp", "");
78
+ return typeof value === "string" && value ? value : null;
79
+ }
80
+
81
+ /**
82
+ * Python `backfill_assistant_messages`. The transcript window is
83
+ * `[min(timestamp), max(timestamp)]` over entries that carry one; a
84
+ * conversation is in range when `started_at <= window end` and it either
85
+ * ended at or after the window start or is still open. Within an in-range
86
+ * conversation that has no assistant turn, every `assistant` entry whose
87
+ * timestamp falls inside `[started_at, ended_at or window end]` is appended.
88
+ */
89
+ export function backfillAssistantMessages(
90
+ db: WritableDatabase,
91
+ transcriptPath: string,
92
+ deps: LoggerDeps,
93
+ ): void {
94
+ const entries = parseJsonl(transcriptPath);
95
+ if (entries.length === 0) return;
96
+
97
+ const timestamps = entries.map(timestampOf).filter((value): value is string => value !== null);
98
+ if (timestamps.length === 0) return;
99
+ // Python's min()/max() on str compare by code point; ISO timestamps are
100
+ // ASCII, so plain comparison is the same order.
101
+ let startTime = timestamps[0] as string;
102
+ let endTime = timestamps[0] as string;
103
+ for (const value of timestamps) {
104
+ if (value < startTime) startTime = value;
105
+ if (value > endTime) endTime = value;
106
+ }
107
+
108
+ const conversations = db
109
+ .prepare(
110
+ `SELECT id, started_at, ended_at FROM conversations
111
+ WHERE started_at <= ? AND (ended_at >= ? OR ended_at IS NULL)`,
112
+ )
113
+ .all(endTime, startTime) as { id: string; started_at: string; ended_at: string | null }[];
114
+
115
+ const hasAssistant = db.prepare(
116
+ "SELECT 1 FROM messages WHERE conversation_id = ? AND role = 'assistant' LIMIT 1",
117
+ );
118
+ const insert = db.prepare(
119
+ `INSERT INTO messages (id, conversation_id, role, content, created_at, token_count, metadata)
120
+ VALUES (?, ?, 'assistant', ?, ?, NULL, NULL)`,
121
+ );
122
+
123
+ for (const conversation of conversations) {
124
+ if (hasAssistant.get(conversation.id)) continue;
125
+ const conversationStart = conversation.started_at;
126
+ const conversationEnd = conversation.ended_at ?? endTime;
127
+
128
+ for (const entry of entries) {
129
+ const timestamp = timestampOf(entry);
130
+ if (!timestamp || !(conversationStart <= timestamp && timestamp <= conversationEnd)) continue;
131
+ if (getFrom(entry, "type") !== "assistant") continue;
132
+ const message = getOr(entry, "message", {});
133
+ const text = assistantText(getOr(message, "content", []));
134
+ if (text) {
135
+ // Python: Message(...) draws its id before its created_at.
136
+ const id = deps.newId();
137
+ insert.run(id, conversation.id, text, deps.nowIso());
138
+ }
139
+ }
140
+ }
141
+ }