claude-flow 3.42.2 → 3.42.3

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 (461) hide show
  1. package/.claude/agents/MIGRATION_SUMMARY.md +221 -221
  2. package/.claude/agents/analysis/analyze-code-quality.md +57 -57
  3. package/.claude/agents/analysis/code-analyzer.md +188 -188
  4. package/.claude/agents/analysis/code-review/analyze-code-quality.md +57 -57
  5. package/.claude/agents/architecture/system-design/arch-system-design.md +35 -35
  6. package/.claude/agents/base-template-generator.md +41 -41
  7. package/.claude/agents/consensus/byzantine-coordinator.md +42 -42
  8. package/.claude/agents/consensus/crdt-synchronizer.md +976 -976
  9. package/.claude/agents/consensus/gossip-coordinator.md +42 -42
  10. package/.claude/agents/consensus/performance-benchmarker.md +830 -830
  11. package/.claude/agents/consensus/quorum-manager.md +802 -802
  12. package/.claude/agents/consensus/raft-manager.md +42 -42
  13. package/.claude/agents/consensus/security-manager.md +601 -601
  14. package/.claude/agents/core/coder.md +254 -254
  15. package/.claude/agents/core/planner.md +151 -151
  16. package/.claude/agents/core/researcher.md +173 -173
  17. package/.claude/agents/core/reviewer.md +308 -308
  18. package/.claude/agents/core/tester.md +299 -299
  19. package/.claude/agents/custom/test-long-runner.md +43 -43
  20. package/.claude/agents/data/ml/data-ml-model.md +75 -75
  21. package/.claude/agents/database-specialist.md +9 -9
  22. package/.claude/agents/development/backend/dev-backend-api.md +28 -28
  23. package/.claude/agents/development/dev-backend-api.md +177 -177
  24. package/.claude/agents/devops/ci-cd/ops-cicd-github.md +51 -51
  25. package/.claude/agents/documentation/api-docs/docs-api-openapi.md +62 -62
  26. package/.claude/agents/dual-mode/codex-coordinator.md +206 -206
  27. package/.claude/agents/dual-mode/codex-worker.md +190 -190
  28. package/.claude/agents/dual-mode/dual-orchestrator.md +253 -253
  29. package/.claude/agents/flow-nexus/app-store.md +87 -87
  30. package/.claude/agents/flow-nexus/authentication.md +68 -68
  31. package/.claude/agents/flow-nexus/challenges.md +80 -80
  32. package/.claude/agents/flow-nexus/neural-network.md +87 -87
  33. package/.claude/agents/flow-nexus/payments.md +82 -82
  34. package/.claude/agents/flow-nexus/sandbox.md +75 -75
  35. package/.claude/agents/flow-nexus/swarm.md +75 -75
  36. package/.claude/agents/flow-nexus/user-tools.md +95 -95
  37. package/.claude/agents/flow-nexus/workflow.md +83 -83
  38. package/.claude/agents/github/code-review-swarm.md +520 -520
  39. package/.claude/agents/github/github-modes.md +153 -153
  40. package/.claude/agents/github/issue-tracker.md +298 -298
  41. package/.claude/agents/github/multi-repo-swarm.md +524 -524
  42. package/.claude/agents/github/pr-manager.md +162 -162
  43. package/.claude/agents/github/project-board-sync.md +477 -477
  44. package/.claude/agents/github/release-manager.md +337 -337
  45. package/.claude/agents/github/release-swarm.md +550 -550
  46. package/.claude/agents/github/repo-architect.md +364 -364
  47. package/.claude/agents/github/swarm-issue.md +550 -550
  48. package/.claude/agents/github/swarm-pr.md +401 -401
  49. package/.claude/agents/github/sync-coordinator.md +424 -424
  50. package/.claude/agents/github/workflow-automation.md +604 -604
  51. package/.claude/agents/goal/agent.md +816 -816
  52. package/.claude/agents/goal/code-goal-planner.md +444 -444
  53. package/.claude/agents/goal/goal-planner.md +167 -167
  54. package/.claude/agents/hive-mind/collective-intelligence-coordinator.md +128 -128
  55. package/.claude/agents/hive-mind/queen-coordinator.md +201 -201
  56. package/.claude/agents/hive-mind/scout-explorer.md +240 -240
  57. package/.claude/agents/hive-mind/swarm-memory-manager.md +191 -191
  58. package/.claude/agents/hive-mind/worker-specialist.md +215 -215
  59. package/.claude/agents/neural/safla-neural.md +73 -73
  60. package/.claude/agents/optimization/benchmark-suite.md +662 -662
  61. package/.claude/agents/optimization/load-balancer.md +428 -428
  62. package/.claude/agents/optimization/performance-monitor.md +669 -669
  63. package/.claude/agents/optimization/resource-allocator.md +671 -671
  64. package/.claude/agents/optimization/topology-optimizer.md +805 -805
  65. package/.claude/agents/payments/agentic-payments.md +126 -126
  66. package/.claude/agents/project-coordinator.md +8 -8
  67. package/.claude/agents/python-specialist.md +9 -9
  68. package/.claude/agents/reasoning/agent.md +816 -816
  69. package/.claude/agents/reasoning/goal-planner.md +72 -72
  70. package/.claude/agents/security-auditor.md +9 -9
  71. package/.claude/agents/sona/sona-learning-optimizer.md +65 -65
  72. package/.claude/agents/sparc/architecture.md +452 -452
  73. package/.claude/agents/sparc/pseudocode.md +298 -298
  74. package/.claude/agents/sparc/refinement.md +503 -503
  75. package/.claude/agents/sparc/specification.md +257 -257
  76. package/.claude/agents/specialized/mobile/spec-mobile-react-native.md +87 -87
  77. package/.claude/agents/sublinear/consensus-coordinator.md +337 -337
  78. package/.claude/agents/sublinear/matrix-optimizer.md +184 -184
  79. package/.claude/agents/sublinear/pagerank-analyzer.md +298 -298
  80. package/.claude/agents/sublinear/performance-optimizer.md +367 -367
  81. package/.claude/agents/sublinear/trading-predictor.md +245 -245
  82. package/.claude/agents/swarm/adaptive-coordinator.md +363 -363
  83. package/.claude/agents/swarm/hierarchical-coordinator.md +299 -299
  84. package/.claude/agents/swarm/mesh-coordinator.md +362 -362
  85. package/.claude/agents/templates/automation-smart-agent.md +184 -184
  86. package/.claude/agents/templates/coordinator-swarm-init.md +82 -82
  87. package/.claude/agents/templates/github-pr-manager.md +154 -154
  88. package/.claude/agents/templates/implementer-sparc-coder.md +242 -242
  89. package/.claude/agents/templates/memory-coordinator.md +162 -162
  90. package/.claude/agents/templates/migration-plan.md +723 -723
  91. package/.claude/agents/templates/orchestrator-task.md +119 -119
  92. package/.claude/agents/templates/performance-analyzer.md +178 -178
  93. package/.claude/agents/templates/sparc-coordinator.md +162 -162
  94. package/.claude/agents/testing/production-validator.md +372 -372
  95. package/.claude/agents/testing/tdd-london-swarm.md +221 -221
  96. package/.claude/agents/testing/unit/tdd-london-swarm.md +221 -221
  97. package/.claude/agents/testing/validation/production-validator.md +372 -372
  98. package/.claude/agents/typescript-specialist.md +9 -9
  99. package/.claude/agents/v3/database-specialist.md +9 -9
  100. package/.claude/agents/v3/project-coordinator.md +8 -8
  101. package/.claude/agents/v3/python-specialist.md +9 -9
  102. package/.claude/agents/v3/test-architect.md +9 -9
  103. package/.claude/agents/v3/typescript-specialist.md +9 -9
  104. package/.claude/agents/v3/v3-integration-architect.md +311 -311
  105. package/.claude/agents/v3/v3-memory-specialist.md +280 -280
  106. package/.claude/agents/v3/v3-performance-engineer.md +362 -362
  107. package/.claude/agents/v3/v3-queen-coordinator.md +62 -62
  108. package/.claude/agents/v3/v3-security-architect.md +139 -139
  109. package/.claude/checkpoints/1767754460.json +8 -8
  110. package/.claude/commands/agents/README.md +10 -10
  111. package/.claude/commands/agents/agent-capabilities.md +21 -21
  112. package/.claude/commands/agents/agent-coordination.md +28 -28
  113. package/.claude/commands/agents/agent-spawning.md +28 -28
  114. package/.claude/commands/agents/agent-types.md +26 -26
  115. package/.claude/commands/analysis/COMMAND_COMPLIANCE_REPORT.md +53 -53
  116. package/.claude/commands/analysis/README.md +9 -9
  117. package/.claude/commands/analysis/bottleneck-detect.md +162 -162
  118. package/.claude/commands/analysis/performance-bottlenecks.md +58 -58
  119. package/.claude/commands/analysis/performance-report.md +25 -25
  120. package/.claude/commands/analysis/token-efficiency.md +44 -44
  121. package/.claude/commands/analysis/token-usage.md +25 -25
  122. package/.claude/commands/automation/README.md +9 -9
  123. package/.claude/commands/automation/auto-agent.md +122 -122
  124. package/.claude/commands/automation/self-healing.md +105 -105
  125. package/.claude/commands/automation/session-memory.md +89 -89
  126. package/.claude/commands/automation/smart-agents.md +72 -72
  127. package/.claude/commands/automation/smart-spawn.md +25 -25
  128. package/.claude/commands/automation/workflow-select.md +25 -25
  129. package/.claude/commands/claude-flow-help.md +103 -103
  130. package/.claude/commands/claude-flow-memory.md +107 -107
  131. package/.claude/commands/claude-flow-swarm.md +205 -205
  132. package/.claude/commands/coordination/README.md +9 -9
  133. package/.claude/commands/coordination/agent-spawn.md +25 -25
  134. package/.claude/commands/coordination/init.md +44 -44
  135. package/.claude/commands/coordination/orchestrate.md +43 -43
  136. package/.claude/commands/coordination/spawn.md +45 -45
  137. package/.claude/commands/coordination/swarm-init.md +85 -85
  138. package/.claude/commands/coordination/task-orchestrate.md +25 -25
  139. package/.claude/commands/flow-nexus/app-store.md +123 -123
  140. package/.claude/commands/flow-nexus/challenges.md +119 -119
  141. package/.claude/commands/flow-nexus/login-registration.md +64 -64
  142. package/.claude/commands/flow-nexus/neural-network.md +133 -133
  143. package/.claude/commands/flow-nexus/payments.md +115 -115
  144. package/.claude/commands/flow-nexus/sandbox.md +82 -82
  145. package/.claude/commands/flow-nexus/swarm.md +86 -86
  146. package/.claude/commands/flow-nexus/user-tools.md +151 -151
  147. package/.claude/commands/flow-nexus/workflow.md +114 -114
  148. package/.claude/commands/github/README.md +11 -11
  149. package/.claude/commands/github/code-review-swarm.md +513 -513
  150. package/.claude/commands/github/code-review.md +25 -25
  151. package/.claude/commands/github/github-modes.md +146 -146
  152. package/.claude/commands/github/github-swarm.md +121 -121
  153. package/.claude/commands/github/issue-tracker.md +291 -291
  154. package/.claude/commands/github/issue-triage.md +25 -25
  155. package/.claude/commands/github/multi-repo-swarm.md +518 -518
  156. package/.claude/commands/github/pr-enhance.md +26 -26
  157. package/.claude/commands/github/pr-manager.md +169 -169
  158. package/.claude/commands/github/project-board-sync.md +470 -470
  159. package/.claude/commands/github/release-manager.md +337 -337
  160. package/.claude/commands/github/release-swarm.md +543 -543
  161. package/.claude/commands/github/repo-analyze.md +25 -25
  162. package/.claude/commands/github/repo-architect.md +366 -366
  163. package/.claude/commands/github/swarm-issue.md +481 -481
  164. package/.claude/commands/github/swarm-pr.md +284 -284
  165. package/.claude/commands/github/sync-coordinator.md +300 -300
  166. package/.claude/commands/github/workflow-automation.md +441 -441
  167. package/.claude/commands/hive-mind/README.md +17 -17
  168. package/.claude/commands/hive-mind/hive-mind-consensus.md +8 -8
  169. package/.claude/commands/hive-mind/hive-mind-init.md +18 -18
  170. package/.claude/commands/hive-mind/hive-mind-memory.md +8 -8
  171. package/.claude/commands/hive-mind/hive-mind-metrics.md +8 -8
  172. package/.claude/commands/hive-mind/hive-mind-resume.md +8 -8
  173. package/.claude/commands/hive-mind/hive-mind-sessions.md +8 -8
  174. package/.claude/commands/hive-mind/hive-mind-spawn.md +21 -21
  175. package/.claude/commands/hive-mind/hive-mind-status.md +8 -8
  176. package/.claude/commands/hive-mind/hive-mind-stop.md +8 -8
  177. package/.claude/commands/hive-mind/hive-mind-wizard.md +8 -8
  178. package/.claude/commands/hive-mind/hive-mind.md +27 -27
  179. package/.claude/commands/hooks/README.md +11 -11
  180. package/.claude/commands/hooks/overview.md +57 -57
  181. package/.claude/commands/hooks/post-edit.md +117 -117
  182. package/.claude/commands/hooks/post-task.md +112 -112
  183. package/.claude/commands/hooks/pre-edit.md +113 -113
  184. package/.claude/commands/hooks/pre-task.md +111 -111
  185. package/.claude/commands/hooks/session-end.md +118 -118
  186. package/.claude/commands/hooks/setup.md +102 -102
  187. package/.claude/commands/memory/README.md +9 -9
  188. package/.claude/commands/memory/memory-persist.md +25 -25
  189. package/.claude/commands/memory/memory-search.md +25 -25
  190. package/.claude/commands/memory/memory-usage.md +25 -25
  191. package/.claude/commands/memory/neural.md +47 -47
  192. package/.claude/commands/monitoring/README.md +9 -9
  193. package/.claude/commands/monitoring/agent-metrics.md +25 -25
  194. package/.claude/commands/monitoring/agents.md +44 -44
  195. package/.claude/commands/monitoring/real-time-view.md +25 -25
  196. package/.claude/commands/monitoring/status.md +46 -46
  197. package/.claude/commands/monitoring/swarm-monitor.md +25 -25
  198. package/.claude/commands/optimization/README.md +9 -9
  199. package/.claude/commands/optimization/auto-topology.md +61 -61
  200. package/.claude/commands/optimization/cache-manage.md +25 -25
  201. package/.claude/commands/optimization/parallel-execute.md +25 -25
  202. package/.claude/commands/optimization/parallel-execution.md +49 -49
  203. package/.claude/commands/optimization/topology-optimize.md +25 -25
  204. package/.claude/commands/pair/README.md +260 -260
  205. package/.claude/commands/pair/commands.md +545 -545
  206. package/.claude/commands/pair/config.md +509 -509
  207. package/.claude/commands/pair/examples.md +511 -511
  208. package/.claude/commands/pair/modes.md +347 -347
  209. package/.claude/commands/pair/session.md +406 -406
  210. package/.claude/commands/pair/start.md +208 -208
  211. package/.claude/commands/sparc/analyzer.md +51 -51
  212. package/.claude/commands/sparc/architect.md +53 -53
  213. package/.claude/commands/sparc/ask.md +97 -97
  214. package/.claude/commands/sparc/batch-executor.md +54 -54
  215. package/.claude/commands/sparc/code.md +89 -89
  216. package/.claude/commands/sparc/coder.md +54 -54
  217. package/.claude/commands/sparc/debug.md +83 -83
  218. package/.claude/commands/sparc/debugger.md +54 -54
  219. package/.claude/commands/sparc/designer.md +53 -53
  220. package/.claude/commands/sparc/devops.md +109 -109
  221. package/.claude/commands/sparc/docs-writer.md +80 -80
  222. package/.claude/commands/sparc/documenter.md +54 -54
  223. package/.claude/commands/sparc/innovator.md +54 -54
  224. package/.claude/commands/sparc/integration.md +83 -83
  225. package/.claude/commands/sparc/mcp.md +117 -117
  226. package/.claude/commands/sparc/memory-manager.md +54 -54
  227. package/.claude/commands/sparc/optimizer.md +54 -54
  228. package/.claude/commands/sparc/orchestrator.md +131 -131
  229. package/.claude/commands/sparc/post-deployment-monitoring-mode.md +83 -83
  230. package/.claude/commands/sparc/refinement-optimization-mode.md +83 -83
  231. package/.claude/commands/sparc/researcher.md +54 -54
  232. package/.claude/commands/sparc/reviewer.md +54 -54
  233. package/.claude/commands/sparc/security-review.md +80 -80
  234. package/.claude/commands/sparc/sparc-modes.md +174 -174
  235. package/.claude/commands/sparc/sparc.md +111 -111
  236. package/.claude/commands/sparc/spec-pseudocode.md +80 -80
  237. package/.claude/commands/sparc/supabase-admin.md +348 -348
  238. package/.claude/commands/sparc/swarm-coordinator.md +54 -54
  239. package/.claude/commands/sparc/tdd.md +54 -54
  240. package/.claude/commands/sparc/tester.md +54 -54
  241. package/.claude/commands/sparc/tutorial.md +79 -79
  242. package/.claude/commands/sparc/workflow-manager.md +54 -54
  243. package/.claude/commands/sparc.md +166 -166
  244. package/.claude/commands/stream-chain/pipeline.md +120 -120
  245. package/.claude/commands/stream-chain/run.md +69 -69
  246. package/.claude/commands/swarm/README.md +15 -15
  247. package/.claude/commands/swarm/analysis.md +95 -95
  248. package/.claude/commands/swarm/development.md +96 -96
  249. package/.claude/commands/swarm/examples.md +168 -168
  250. package/.claude/commands/swarm/maintenance.md +102 -102
  251. package/.claude/commands/swarm/optimization.md +117 -117
  252. package/.claude/commands/swarm/research.md +136 -136
  253. package/.claude/commands/swarm/swarm-analysis.md +8 -8
  254. package/.claude/commands/swarm/swarm-background.md +8 -8
  255. package/.claude/commands/swarm/swarm-init.md +19 -19
  256. package/.claude/commands/swarm/swarm-modes.md +8 -8
  257. package/.claude/commands/swarm/swarm-monitor.md +8 -8
  258. package/.claude/commands/swarm/swarm-spawn.md +19 -19
  259. package/.claude/commands/swarm/swarm-status.md +8 -8
  260. package/.claude/commands/swarm/swarm-strategies.md +8 -8
  261. package/.claude/commands/swarm/swarm.md +27 -27
  262. package/.claude/commands/swarm/testing.md +131 -131
  263. package/.claude/commands/training/README.md +9 -9
  264. package/.claude/commands/training/model-update.md +25 -25
  265. package/.claude/commands/training/neural-patterns.md +73 -73
  266. package/.claude/commands/training/neural-train.md +25 -25
  267. package/.claude/commands/training/pattern-learn.md +25 -25
  268. package/.claude/commands/training/specialization.md +62 -62
  269. package/.claude/commands/truth/start.md +142 -142
  270. package/.claude/commands/verify/check.md +49 -49
  271. package/.claude/commands/verify/start.md +127 -127
  272. package/.claude/commands/workflows/README.md +9 -9
  273. package/.claude/commands/workflows/development.md +77 -77
  274. package/.claude/commands/workflows/research.md +62 -62
  275. package/.claude/commands/workflows/workflow-create.md +25 -25
  276. package/.claude/commands/workflows/workflow-execute.md +25 -25
  277. package/.claude/commands/workflows/workflow-export.md +25 -25
  278. package/.claude/config/v3-dependency-optimization.json +265 -265
  279. package/.claude/config/v3-performance-targets.json +250 -250
  280. package/.claude/helpers/.LOCKED +2 -2
  281. package/.claude/helpers/README.md +96 -96
  282. package/.claude/helpers/adr-compliance.sh +186 -186
  283. package/.claude/helpers/aggressive-microcompact.mjs +36 -36
  284. package/.claude/helpers/auto-commit.sh +178 -178
  285. package/.claude/helpers/auto-memory-hook.mjs +430 -430
  286. package/.claude/helpers/checkpoint-manager.sh +251 -251
  287. package/.claude/helpers/context-persistence-hook.mjs +2001 -2001
  288. package/.claude/helpers/daemon-manager.sh +252 -252
  289. package/.claude/helpers/ddd-tracker.sh +144 -144
  290. package/.claude/helpers/github-safe.js +156 -156
  291. package/.claude/helpers/github-setup.sh +45 -45
  292. package/.claude/helpers/guidance-hook.sh +13 -13
  293. package/.claude/helpers/guidance-hooks.sh +102 -102
  294. package/.claude/helpers/health-monitor.sh +108 -108
  295. package/.claude/helpers/hook-handler.cjs +606 -606
  296. package/.claude/helpers/intelligence.cjs +1169 -1169
  297. package/.claude/helpers/learning-hooks.sh +329 -329
  298. package/.claude/helpers/learning-optimizer.sh +127 -127
  299. package/.claude/helpers/learning-service.mjs +1144 -1144
  300. package/.claude/helpers/memory.cjs +84 -84
  301. package/.claude/helpers/metrics-db.mjs +503 -503
  302. package/.claude/helpers/patch-aggressive-prune.mjs +184 -184
  303. package/.claude/helpers/pattern-consolidator.sh +86 -86
  304. package/.claude/helpers/perf-worker.sh +160 -160
  305. package/.claude/helpers/quick-start.sh +19 -19
  306. package/.claude/helpers/router.cjs +62 -62
  307. package/.claude/helpers/security-scanner.sh +127 -127
  308. package/.claude/helpers/session.cjs +125 -125
  309. package/.claude/helpers/setup-mcp.sh +18 -18
  310. package/.claude/helpers/standard-checkpoint-hooks.sh +189 -189
  311. package/.claude/helpers/statusline.cjs +0 -0
  312. package/.claude/helpers/swarm-comms.sh +353 -353
  313. package/.claude/helpers/swarm-hooks.sh +761 -761
  314. package/.claude/helpers/swarm-monitor.sh +210 -210
  315. package/.claude/helpers/sync-v3-metrics.sh +245 -245
  316. package/.claude/helpers/update-v3-progress.sh +165 -165
  317. package/.claude/helpers/v3-quick-status.sh +57 -57
  318. package/.claude/helpers/v3.sh +110 -110
  319. package/.claude/helpers/validate-v3-config.sh +215 -215
  320. package/.claude/helpers/worker-manager.sh +170 -170
  321. package/.claude/mcp.json +12 -12
  322. package/.claude/proven-config.json +1 -1
  323. package/.claude/settings.json +284 -284
  324. package/.claude/settings.json.bak +526 -526
  325. package/.claude/skills/agentdb-advanced/SKILL.md +550 -550
  326. package/.claude/skills/agentdb-learning/SKILL.md +545 -545
  327. package/.claude/skills/agentdb-memory-patterns/SKILL.md +339 -339
  328. package/.claude/skills/agentdb-optimization/SKILL.md +509 -509
  329. package/.claude/skills/agentdb-vector-search/SKILL.md +339 -339
  330. package/.claude/skills/agentic-jujutsu/SKILL.md +645 -645
  331. package/.claude/skills/browser/SKILL.md +204 -204
  332. package/.claude/skills/dual-mode/README.md +71 -71
  333. package/.claude/skills/dual-mode/dual-collect.md +103 -103
  334. package/.claude/skills/dual-mode/dual-coordinate.md +85 -85
  335. package/.claude/skills/dual-mode/dual-spawn.md +81 -81
  336. package/.claude/skills/flow-nexus-neural/SKILL.md +727 -727
  337. package/.claude/skills/flow-nexus-platform/SKILL.md +1154 -1154
  338. package/.claude/skills/flow-nexus-swarm/SKILL.md +604 -604
  339. package/.claude/skills/github-code-review/SKILL.md +1125 -1125
  340. package/.claude/skills/github-multi-repo/SKILL.md +862 -862
  341. package/.claude/skills/github-project-management/SKILL.md +1262 -1262
  342. package/.claude/skills/github-release-management/SKILL.md +1064 -1064
  343. package/.claude/skills/github-workflow-automation/SKILL.md +1047 -1047
  344. package/.claude/skills/hive-mind-advanced/SKILL.md +709 -709
  345. package/.claude/skills/hooks-automation/SKILL.md +1201 -1201
  346. package/.claude/skills/pair-programming/SKILL.md +1202 -1202
  347. package/.claude/skills/performance-analysis/SKILL.md +560 -560
  348. package/.claude/skills/reasoningbank-agentdb/SKILL.md +446 -446
  349. package/.claude/skills/reasoningbank-intelligence/SKILL.md +201 -201
  350. package/.claude/skills/skill-builder/SKILL.md +910 -910
  351. package/.claude/skills/sparc-methodology/SKILL.md +1106 -1106
  352. package/.claude/skills/stream-chain/SKILL.md +560 -560
  353. package/.claude/skills/swarm-advanced/SKILL.md +970 -970
  354. package/.claude/skills/swarm-orchestration/SKILL.md +179 -179
  355. package/.claude/skills/v3-cli-modernization/SKILL.md +871 -871
  356. package/.claude/skills/v3-core-implementation/SKILL.md +796 -796
  357. package/.claude/skills/v3-ddd-architecture/SKILL.md +441 -441
  358. package/.claude/skills/v3-integration-deep/SKILL.md +240 -240
  359. package/.claude/skills/v3-mcp-optimization/SKILL.md +776 -776
  360. package/.claude/skills/v3-memory-unification/SKILL.md +173 -173
  361. package/.claude/skills/v3-performance-optimization/SKILL.md +389 -389
  362. package/.claude/skills/v3-security-overhaul/SKILL.md +81 -81
  363. package/.claude/skills/v3-swarm-coordination/SKILL.md +339 -339
  364. package/.claude/skills/verification-quality/SKILL.md +691 -691
  365. package/.claude/skills/worker-benchmarks/SKILL.md +129 -129
  366. package/.claude/skills/worker-integration/SKILL.md +147 -147
  367. package/.claude/statusline-command.sh +176 -176
  368. package/.claude/statusline.mjs +109 -109
  369. package/.claude/statusline.sh +431 -431
  370. package/.claude/workflows/full-system-test.js +65 -65
  371. package/.claude/workflows/intelligence-system-hardening.js +120 -120
  372. package/.claude/workflows/plugin-contract-audit.js +91 -91
  373. package/.claude-plugin/README.md +720 -720
  374. package/.claude-plugin/docs/INSTALLATION.md +261 -261
  375. package/.claude-plugin/docs/PLUGIN_SUMMARY.md +361 -361
  376. package/.claude-plugin/docs/QUICKSTART.md +361 -361
  377. package/.claude-plugin/docs/STRUCTURE.md +128 -128
  378. package/.claude-plugin/hooks/hooks.json +77 -77
  379. package/.claude-plugin/marketplace.json +204 -204
  380. package/.claude-plugin/plugin.json +71 -71
  381. package/.claude-plugin/scripts/install.sh +234 -234
  382. package/.claude-plugin/scripts/ruflo-hook.cjs +304 -166
  383. package/.claude-plugin/scripts/ruflo-hook.sh +52 -52
  384. package/.claude-plugin/scripts/uninstall.sh +36 -36
  385. package/.claude-plugin/scripts/verify.sh +108 -108
  386. package/LICENSE +21 -21
  387. package/README.md +422 -422
  388. package/bin/cli.js +11 -11
  389. package/bin/npx-repair.js +7 -7
  390. package/bin/npx-safe-launch.js +9 -9
  391. package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -32
  392. package/node_modules/@claude-flow/codex/.agents/skills/memory-management/SKILL.md +45 -45
  393. package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -32
  394. package/node_modules/@claude-flow/codex/.agents/skills/security-audit/SKILL.md +46 -46
  395. package/node_modules/@claude-flow/codex/.agents/skills/sparc-methodology/SKILL.md +46 -46
  396. package/node_modules/@claude-flow/codex/.agents/skills/swarm-orchestration/SKILL.md +53 -53
  397. package/node_modules/@claude-flow/codex/README.md +1044 -1044
  398. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +13 -13
  399. package/node_modules/@claude-flow/codex/dist/generators/agents-md.js +664 -664
  400. package/node_modules/@claude-flow/codex/dist/generators/config-toml.js +455 -455
  401. package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +45 -45
  402. package/node_modules/@claude-flow/codex/dist/initializer.js +167 -167
  403. package/node_modules/@claude-flow/codex/dist/templates/index.js +15 -15
  404. package/node_modules/@claude-flow/mcp/README.md +429 -429
  405. package/node_modules/@claude-flow/plugin-agent-federation/README.md +49 -49
  406. package/node_modules/@claude-flow/security/README.md +292 -292
  407. package/node_modules/@claude-flow/security/dist/credential-generator.js +9 -9
  408. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  409. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +9 -9
  410. package/package.json +222 -222
  411. package/v3/@claude-flow/cli/README.md +422 -422
  412. package/v3/@claude-flow/cli/bin/cli.js +338 -338
  413. package/v3/@claude-flow/cli/bin/mcp-server.js +224 -224
  414. package/v3/@claude-flow/cli/bin/preinstall.cjs +2 -2
  415. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  416. package/v3/@claude-flow/cli/dist/src/benchmarks/gaia-critic.js +24 -24
  417. package/v3/@claude-flow/cli/dist/src/business-pods/bbs-budget-tracker.js +53 -53
  418. package/v3/@claude-flow/cli/dist/src/commands/completions.js +409 -409
  419. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +44 -44
  420. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +4 -4
  421. package/v3/@claude-flow/cli/dist/src/commands/embeddings.js +26 -26
  422. package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +97 -97
  423. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +9 -9
  424. package/v3/@claude-flow/cli/dist/src/commands/init.js +75 -75
  425. package/v3/@claude-flow/cli/dist/src/commands/ruvector/backup.js +23 -23
  426. package/v3/@claude-flow/cli/dist/src/commands/ruvector/benchmark.js +31 -31
  427. package/v3/@claude-flow/cli/dist/src/commands/ruvector/import.js +14 -14
  428. package/v3/@claude-flow/cli/dist/src/commands/ruvector/init.js +115 -115
  429. package/v3/@claude-flow/cli/dist/src/commands/ruvector/migrate.js +99 -99
  430. package/v3/@claude-flow/cli/dist/src/commands/ruvector/optimize.js +51 -51
  431. package/v3/@claude-flow/cli/dist/src/commands/ruvector/setup.js +624 -624
  432. package/v3/@claude-flow/cli/dist/src/commands/ruvector/status.js +38 -38
  433. package/v3/@claude-flow/cli/dist/src/config/proven-config.js +2 -2
  434. package/v3/@claude-flow/cli/dist/src/init/claudemd-generator.js +273 -273
  435. package/v3/@claude-flow/cli/dist/src/init/executor.js +453 -453
  436. package/v3/@claude-flow/cli/dist/src/init/helper-signing.js +2 -2
  437. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +917 -751
  438. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +24 -24
  439. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +15 -15
  440. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +19 -19
  441. package/v3/@claude-flow/cli/dist/src/mcp-tools/seraphina-tools.js +4 -4
  442. package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +22 -22
  443. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +114 -114
  444. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +416 -416
  445. package/v3/@claude-flow/cli/dist/src/memory/rabitq-index.js +5 -5
  446. package/v3/@claude-flow/cli/dist/src/proxy/verify.js +2 -2
  447. package/v3/@claude-flow/cli/dist/src/runtime/headless.js +28 -28
  448. package/v3/@claude-flow/cli/dist/src/ruvector/diskann-backend.d.ts +78 -0
  449. package/v3/@claude-flow/cli/dist/src/ruvector/diskann-backend.js +310 -0
  450. package/v3/@claude-flow/cli/dist/src/services/distill-tuning.js +7 -7
  451. package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.js +84 -84
  452. package/v3/@claude-flow/cli/dist/src/services/memory-distillation.js +18 -18
  453. package/v3/@claude-flow/cli/dist/src/transfer/deploy-seraphine.js +23 -23
  454. package/v3/@claude-flow/cli/package.json +181 -181
  455. package/v3/@claude-flow/guidance/README.md +1195 -1195
  456. package/v3/@claude-flow/guidance/package.json +198 -198
  457. package/v3/@claude-flow/shared/README.md +323 -323
  458. package/v3/@claude-flow/shared/dist/events/event-store.js +31 -31
  459. package/v3/@claude-flow/shared/dist/hooks/safety/git-commit.js +3 -3
  460. package/v3/@claude-flow/shared/package.json +43 -43
  461. package/v3/README.md +493 -493
@@ -1,2001 +1,2001 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Context Persistence Hook (ADR-051)
4
- *
5
- * Intercepts Claude Code's PreCompact, SessionStart, and UserPromptSubmit
6
- * lifecycle events to persist conversation history in SQLite (primary),
7
- * RuVector PostgreSQL (optional), or JSON (fallback), enabling "infinite
8
- * context" across compaction boundaries.
9
- *
10
- * Backend priority:
11
- * 1. better-sqlite3 (native, WAL mode, indexed queries, ACID transactions)
12
- * 2. RuVector PostgreSQL (if RUVECTOR_* env vars set - TB-scale, GNN search)
13
- * 3. AgentDB from @claude-flow/memory (HNSW vector search)
14
- * 4. JsonFileBackend (zero dependencies, always works)
15
- *
16
- * Proactive archiving:
17
- * - UserPromptSubmit hook archives on every prompt, BEFORE context fills up
18
- * - PreCompact hook is a safety net that catches any remaining unarchived turns
19
- * - SessionStart hook restores context after compaction
20
- * - Together, compaction becomes invisible — no information is ever lost
21
- *
22
- * Usage:
23
- * node context-persistence-hook.mjs pre-compact # PreCompact: archive transcript
24
- * node context-persistence-hook.mjs session-start # SessionStart: restore context
25
- * node context-persistence-hook.mjs user-prompt-submit # UserPromptSubmit: proactive archive
26
- * node context-persistence-hook.mjs status # Show archive stats
27
- */
28
-
29
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
30
- import { createHash } from 'crypto';
31
- import { join, dirname } from 'path';
32
- import { fileURLToPath } from 'url';
33
- import { createRequire } from 'module';
34
-
35
- const __filename = fileURLToPath(import.meta.url);
36
- const __dirname = dirname(__filename);
37
- const PROJECT_ROOT = join(__dirname, '../..');
38
- const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
39
- const ARCHIVE_JSON_PATH = join(DATA_DIR, 'transcript-archive.json');
40
- const ARCHIVE_DB_PATH = join(DATA_DIR, 'transcript-archive.db');
41
-
42
- const NAMESPACE = 'transcript-archive';
43
- const RESTORE_BUDGET = parseInt(process.env.CLAUDE_FLOW_COMPACT_RESTORE_BUDGET || '4000', 10);
44
- const MAX_MESSAGES = 500;
45
- const BLOCK_COMPACTION = process.env.CLAUDE_FLOW_BLOCK_COMPACTION === 'true';
46
- const COMPACT_INSTRUCTION_BUDGET = parseInt(process.env.CLAUDE_FLOW_COMPACT_INSTRUCTION_BUDGET || '2000', 10);
47
- const RETENTION_DAYS = parseInt(process.env.CLAUDE_FLOW_RETENTION_DAYS || '30', 10);
48
- const AUTO_OPTIMIZE = process.env.CLAUDE_FLOW_AUTO_OPTIMIZE !== 'false'; // on by default
49
-
50
- // ============================================================================
51
- // Context Autopilot — prevent compaction by managing context size in real-time
52
- // ============================================================================
53
- const AUTOPILOT_ENABLED = process.env.CLAUDE_FLOW_CONTEXT_AUTOPILOT !== 'false'; // on by default
54
- const CONTEXT_WINDOW_TOKENS = parseInt(process.env.CLAUDE_FLOW_CONTEXT_WINDOW || '200000', 10);
55
- const AUTOPILOT_WARN_PCT = parseFloat(process.env.CLAUDE_FLOW_AUTOPILOT_WARN || '0.70');
56
- const AUTOPILOT_PRUNE_PCT = parseFloat(process.env.CLAUDE_FLOW_AUTOPILOT_PRUNE || '0.85');
57
- const AUTOPILOT_STATE_PATH = join(DATA_DIR, 'autopilot-state.json');
58
-
59
- // Approximate tokens per character (Claude averages ~3.5 chars per token)
60
- const CHARS_PER_TOKEN = 3.5;
61
-
62
- const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
63
-
64
- // ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
65
- // The active backend is created mid-handler and closed at the end. SQLite holds
66
- // a native handle and a WAL; if a SIGTERM/SIGINT arrives between creation and
67
- // `backend.shutdown()`, that close is skipped — risking an unflushed WAL or a
68
- // stale lock file. Track the active backend and flush it on signal before exit.
69
- let activeBackend = null;
70
- let shuttingDown = false;
71
- function trackBackend(b) { activeBackend = b; return b; }
72
- async function gracefulExit(signal) {
73
- if (shuttingDown) return;
74
- shuttingDown = true;
75
- if (DEBUG) process.stderr.write(`[ContextPersistence] received ${signal}, flushing backend before exit\n`);
76
- try {
77
- if (activeBackend && typeof activeBackend.shutdown === 'function') await activeBackend.shutdown();
78
- } catch { /* best effort — never block exit on cleanup */ }
79
- process.exit(0);
80
- }
81
- process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
82
- process.on('SIGINT', () => { gracefulExit('SIGINT'); });
83
-
84
- // Ensure data dir
85
- if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
86
-
87
- // ============================================================================
88
- // SQLite Backend (better-sqlite3 — synchronous, fast, WAL mode)
89
- // ============================================================================
90
-
91
- class SQLiteBackend {
92
- constructor(dbPath) {
93
- this.dbPath = dbPath;
94
- this.db = null;
95
- }
96
-
97
- async initialize() {
98
- const require = createRequire(import.meta.url);
99
- const Database = require('better-sqlite3');
100
- this.db = new Database(this.dbPath);
101
-
102
- // Performance optimizations
103
- this.db.pragma('journal_mode = WAL');
104
- this.db.pragma('synchronous = NORMAL');
105
- this.db.pragma('cache_size = 5000');
106
- this.db.pragma('temp_store = MEMORY');
107
-
108
- // Create schema
109
- this.db.exec(`
110
- CREATE TABLE IF NOT EXISTS transcript_entries (
111
- id TEXT PRIMARY KEY,
112
- key TEXT NOT NULL,
113
- content TEXT NOT NULL,
114
- type TEXT NOT NULL DEFAULT 'episodic',
115
- namespace TEXT NOT NULL DEFAULT 'transcript-archive',
116
- tags TEXT NOT NULL DEFAULT '[]',
117
- metadata TEXT NOT NULL DEFAULT '{}',
118
- access_level TEXT NOT NULL DEFAULT 'private',
119
- created_at INTEGER NOT NULL,
120
- updated_at INTEGER NOT NULL,
121
- version INTEGER NOT NULL DEFAULT 1,
122
- access_count INTEGER NOT NULL DEFAULT 0,
123
- last_accessed_at INTEGER NOT NULL,
124
- content_hash TEXT,
125
- session_id TEXT,
126
- chunk_index INTEGER,
127
- summary TEXT
128
- );
129
-
130
- CREATE INDEX IF NOT EXISTS idx_te_namespace ON transcript_entries(namespace);
131
- CREATE INDEX IF NOT EXISTS idx_te_session ON transcript_entries(session_id);
132
- CREATE INDEX IF NOT EXISTS idx_te_hash ON transcript_entries(content_hash);
133
- CREATE INDEX IF NOT EXISTS idx_te_chunk ON transcript_entries(session_id, chunk_index);
134
- CREATE INDEX IF NOT EXISTS idx_te_created ON transcript_entries(created_at);
135
- `);
136
-
137
- // Schema migration: add confidence + embedding columns (self-learning support)
138
- try {
139
- this.db.exec(`ALTER TABLE transcript_entries ADD COLUMN confidence REAL NOT NULL DEFAULT 0.8`);
140
- } catch { /* column already exists */ }
141
- try {
142
- this.db.exec(`ALTER TABLE transcript_entries ADD COLUMN embedding BLOB`);
143
- } catch { /* column already exists */ }
144
- try {
145
- this.db.exec(`CREATE INDEX IF NOT EXISTS idx_te_confidence ON transcript_entries(confidence)`);
146
- } catch { /* index already exists */ }
147
-
148
- // Prepare statements for reuse
149
- this._stmts = {
150
- insert: this.db.prepare(`
151
- INSERT OR IGNORE INTO transcript_entries
152
- (id, key, content, type, namespace, tags, metadata, access_level,
153
- created_at, updated_at, version, access_count, last_accessed_at,
154
- content_hash, session_id, chunk_index, summary)
155
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
156
- `),
157
- queryByNamespace: this.db.prepare(
158
- 'SELECT * FROM transcript_entries WHERE namespace = ? ORDER BY created_at DESC'
159
- ),
160
- queryBySession: this.db.prepare(
161
- 'SELECT * FROM transcript_entries WHERE namespace = ? AND session_id = ? ORDER BY chunk_index DESC'
162
- ),
163
- countAll: this.db.prepare('SELECT COUNT(*) as cnt FROM transcript_entries'),
164
- countByNamespace: this.db.prepare(
165
- 'SELECT COUNT(*) as cnt FROM transcript_entries WHERE namespace = ?'
166
- ),
167
- hashExists: this.db.prepare(
168
- 'SELECT 1 FROM transcript_entries WHERE content_hash = ? LIMIT 1'
169
- ),
170
- listNamespaces: this.db.prepare(
171
- 'SELECT DISTINCT namespace FROM transcript_entries'
172
- ),
173
- listSessions: this.db.prepare(
174
- 'SELECT session_id, COUNT(*) as cnt FROM transcript_entries WHERE namespace = ? GROUP BY session_id ORDER BY MAX(created_at) DESC'
175
- ),
176
- };
177
-
178
- this._bulkInsert = this.db.transaction((entries) => {
179
- for (const e of entries) {
180
- this._stmts.insert.run(
181
- e.id, e.key, e.content, e.type, e.namespace,
182
- JSON.stringify(e.tags), JSON.stringify(e.metadata), e.accessLevel,
183
- e.createdAt, e.updatedAt, e.version, e.accessCount, e.lastAccessedAt,
184
- e.metadata?.contentHash || null,
185
- e.metadata?.sessionId || null,
186
- e.metadata?.chunkIndex ?? null,
187
- e.metadata?.summary || null
188
- );
189
- }
190
- });
191
-
192
- // Optimization statements
193
- this._stmts.markAccessed = this.db.prepare(
194
- 'UPDATE transcript_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?'
195
- );
196
- this._stmts.pruneStale = this.db.prepare(
197
- 'DELETE FROM transcript_entries WHERE namespace = ? AND access_count = 0 AND created_at < ?'
198
- );
199
- this._stmts.queryByImportance = this.db.prepare(`
200
- SELECT *, (
201
- (CAST(access_count AS REAL) + 1) *
202
- (1.0 / (1.0 + (? - created_at) / 86400000.0)) *
203
- (CASE WHEN json_array_length(json_extract(metadata, '$.toolNames')) > 0 THEN 1.5 ELSE 1.0 END) *
204
- (CASE WHEN json_array_length(json_extract(metadata, '$.filePaths')) > 0 THEN 1.3 ELSE 1.0 END)
205
- ) AS importance_score
206
- FROM transcript_entries
207
- WHERE namespace = ? AND session_id = ?
208
- ORDER BY importance_score DESC
209
- `);
210
- this._stmts.allForSync = this.db.prepare(
211
- 'SELECT * FROM transcript_entries WHERE namespace = ? ORDER BY created_at ASC'
212
- );
213
- }
214
-
215
- async store(entry) {
216
- this._stmts.insert.run(
217
- entry.id, entry.key, entry.content, entry.type, entry.namespace,
218
- JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.accessLevel,
219
- entry.createdAt, entry.updatedAt, entry.version, entry.accessCount, entry.lastAccessedAt,
220
- entry.metadata?.contentHash || null,
221
- entry.metadata?.sessionId || null,
222
- entry.metadata?.chunkIndex ?? null,
223
- entry.metadata?.summary || null
224
- );
225
- }
226
-
227
- async bulkInsert(entries) {
228
- this._bulkInsert(entries);
229
- }
230
-
231
- async query(opts) {
232
- let rows;
233
- if (opts?.namespace && opts?.sessionId) {
234
- rows = this._stmts.queryBySession.all(opts.namespace, opts.sessionId);
235
- } else if (opts?.namespace) {
236
- rows = this._stmts.queryByNamespace.all(opts.namespace);
237
- } else {
238
- rows = this.db.prepare('SELECT * FROM transcript_entries ORDER BY created_at DESC').all();
239
- }
240
- return rows.map(r => this._rowToEntry(r));
241
- }
242
-
243
- async queryBySession(namespace, sessionId) {
244
- const rows = this._stmts.queryBySession.all(namespace, sessionId);
245
- return rows.map(r => this._rowToEntry(r));
246
- }
247
-
248
- hashExists(hash) {
249
- return !!this._stmts.hashExists.get(hash);
250
- }
251
-
252
- async count(namespace) {
253
- if (namespace) {
254
- return this._stmts.countByNamespace.get(namespace).cnt;
255
- }
256
- return this._stmts.countAll.get().cnt;
257
- }
258
-
259
- async listNamespaces() {
260
- return this._stmts.listNamespaces.all().map(r => r.namespace);
261
- }
262
-
263
- async listSessions(namespace) {
264
- return this._stmts.listSessions.all(namespace || NAMESPACE);
265
- }
266
-
267
- markAccessed(ids) {
268
- const now = Date.now();
269
- const boostStmt = this.db.prepare(
270
- 'UPDATE transcript_entries SET access_count = access_count + 1, last_accessed_at = ?, confidence = MIN(1.0, confidence + 0.03) WHERE id = ?'
271
- );
272
- for (const id of ids) {
273
- boostStmt.run(now, id);
274
- }
275
- }
276
-
277
- /**
278
- * Confidence decay: reduce confidence for entries not accessed recently.
279
- * Decay rate: 0.5% per hour (matches LearningBridge default).
280
- * Entries with confidence below 0.1 are floor-clamped.
281
- */
282
- decayConfidence(namespace, hoursElapsed = 1) {
283
- const decayRate = 0.005 * hoursElapsed;
284
- const result = this.db.prepare(
285
- 'UPDATE transcript_entries SET confidence = MAX(0.1, confidence - ?) WHERE namespace = ? AND confidence > 0.1'
286
- ).run(decayRate, namespace || NAMESPACE);
287
- return result.changes;
288
- }
289
-
290
- /**
291
- * Store embedding blob for an entry (Float32Array → Buffer; 384-dim ONNX today, 768-dim legacy hash blobs still readable).
292
- */
293
- storeEmbedding(id, embedding) {
294
- const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
295
- this.db.prepare('UPDATE transcript_entries SET embedding = ? WHERE id = ?').run(buf, id);
296
- }
297
-
298
- /**
299
- * Cosine similarity search across all entries with embeddings.
300
- * Handles both 384-dim (ONNX) and 768-dim (legacy hash) embeddings.
301
- * Returns top-k entries ranked by similarity to the query embedding.
302
- */
303
- semanticSearch(queryEmbedding, k = 10, namespace) {
304
- const rows = this.db.prepare(
305
- 'SELECT id, embedding, summary, session_id, chunk_index, confidence, access_count FROM transcript_entries WHERE namespace = ? AND embedding IS NOT NULL'
306
- ).all(namespace || NAMESPACE);
307
-
308
- const queryDim = queryEmbedding.length;
309
- const scored = [];
310
- for (const row of rows) {
311
- if (!row.embedding) continue;
312
- const stored = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
313
- // Only compare if dimensions match
314
- if (stored.length !== queryDim) continue;
315
- let dot = 0;
316
- for (let i = 0; i < queryDim; i++) {
317
- dot += queryEmbedding[i] * stored[i];
318
- }
319
- // Boost by confidence (self-learning signal)
320
- const score = dot * (row.confidence || 0.8);
321
- scored.push({ id: row.id, score, summary: row.summary, sessionId: row.session_id, chunkIndex: row.chunk_index, confidence: row.confidence, accessCount: row.access_count });
322
- }
323
-
324
- scored.sort((a, b) => b.score - a.score);
325
- return scored.slice(0, k);
326
- }
327
-
328
- /**
329
- * Smart pruning: prune by confidence instead of just age.
330
- * Removes entries with confidence <= threshold AND access_count = 0.
331
- */
332
- pruneByConfidence(namespace, threshold = 0.2) {
333
- const result = this.db.prepare(
334
- 'DELETE FROM transcript_entries WHERE namespace = ? AND confidence <= ? AND access_count = 0'
335
- ).run(namespace || NAMESPACE, threshold);
336
- return result.changes;
337
- }
338
-
339
- pruneStale(namespace, maxAgeDays) {
340
- const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
341
- const result = this._stmts.pruneStale.run(namespace || NAMESPACE, cutoff);
342
- return result.changes;
343
- }
344
-
345
- queryByImportance(namespace, sessionId) {
346
- const now = Date.now();
347
- const rows = this._stmts.queryByImportance.all(now, namespace, sessionId);
348
- return rows.map(r => ({ ...this._rowToEntry(r), importanceScore: r.importance_score }));
349
- }
350
-
351
- allForSync(namespace) {
352
- const rows = this._stmts.allForSync.all(namespace || NAMESPACE);
353
- return rows.map(r => this._rowToEntry(r));
354
- }
355
-
356
- async shutdown() {
357
- if (this.db) {
358
- this.db.pragma('optimize');
359
- this.db.close();
360
- this.db = null;
361
- }
362
- }
363
-
364
- _rowToEntry(row) {
365
- return {
366
- id: row.id,
367
- key: row.key,
368
- content: row.content,
369
- type: row.type,
370
- namespace: row.namespace,
371
- tags: JSON.parse(row.tags),
372
- metadata: JSON.parse(row.metadata),
373
- accessLevel: row.access_level,
374
- createdAt: row.created_at,
375
- updatedAt: row.updated_at,
376
- version: row.version,
377
- accessCount: row.access_count,
378
- lastAccessedAt: row.last_accessed_at,
379
- references: [],
380
- };
381
- }
382
- }
383
-
384
- // ============================================================================
385
- // JSON File Backend (fallback when better-sqlite3 unavailable)
386
- // ============================================================================
387
-
388
- class JsonFileBackend {
389
- constructor(filePath) {
390
- this.filePath = filePath;
391
- this.entries = new Map();
392
- }
393
-
394
- async initialize() {
395
- if (existsSync(this.filePath)) {
396
- try {
397
- const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
398
- if (Array.isArray(data)) {
399
- for (const entry of data) this.entries.set(entry.id, entry);
400
- }
401
- } catch { /* start fresh */ }
402
- }
403
- }
404
-
405
- async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
406
-
407
- async bulkInsert(entries) {
408
- for (const e of entries) this.entries.set(e.id, e);
409
- this._persist();
410
- }
411
-
412
- async query(opts) {
413
- let results = [...this.entries.values()];
414
- if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
415
- if (opts?.type) results = results.filter(e => e.type === opts.type);
416
- if (opts?.limit) results = results.slice(0, opts.limit);
417
- return results;
418
- }
419
-
420
- async queryBySession(namespace, sessionId) {
421
- return [...this.entries.values()]
422
- .filter(e => e.namespace === namespace && e.metadata?.sessionId === sessionId)
423
- .sort((a, b) => (b.metadata?.chunkIndex ?? 0) - (a.metadata?.chunkIndex ?? 0));
424
- }
425
-
426
- hashExists(hash) {
427
- for (const e of this.entries.values()) {
428
- if (e.metadata?.contentHash === hash) return true;
429
- }
430
- return false;
431
- }
432
-
433
- async count(namespace) {
434
- if (!namespace) return this.entries.size;
435
- let n = 0;
436
- for (const e of this.entries.values()) {
437
- if (e.namespace === namespace) n++;
438
- }
439
- return n;
440
- }
441
-
442
- async listNamespaces() {
443
- const ns = new Set();
444
- for (const e of this.entries.values()) ns.add(e.namespace || 'default');
445
- return [...ns];
446
- }
447
-
448
- async listSessions(namespace) {
449
- const sessions = new Map();
450
- for (const e of this.entries.values()) {
451
- if (e.namespace === (namespace || NAMESPACE) && e.metadata?.sessionId) {
452
- sessions.set(e.metadata.sessionId, (sessions.get(e.metadata.sessionId) || 0) + 1);
453
- }
454
- }
455
- return [...sessions.entries()].map(([session_id, cnt]) => ({ session_id, cnt }));
456
- }
457
-
458
- async shutdown() { this._persist(); }
459
-
460
- _persist() {
461
- try {
462
- writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
463
- } catch { /* best effort */ }
464
- }
465
- }
466
-
467
- // ============================================================================
468
- // RuVector PostgreSQL Backend (optional, TB-scale, GNN-enhanced)
469
- // ============================================================================
470
-
471
- class RuVectorBackend {
472
- constructor(config) {
473
- this.config = config;
474
- this.pool = null;
475
- }
476
-
477
- async initialize() {
478
- const pg = await import('pg');
479
- const Pool = pg.default?.Pool || pg.Pool;
480
- this.pool = new Pool({
481
- host: this.config.host,
482
- port: this.config.port || 5432,
483
- database: this.config.database,
484
- user: this.config.user,
485
- password: this.config.password,
486
- ssl: this.config.ssl || false,
487
- max: 3,
488
- idleTimeoutMillis: 10000,
489
- connectionTimeoutMillis: 3000,
490
- application_name: 'claude-flow-context-persistence',
491
- });
492
-
493
- // Test connection and create schema
494
- const client = await this.pool.connect();
495
- try {
496
- await client.query(`
497
- CREATE TABLE IF NOT EXISTS transcript_entries (
498
- id TEXT PRIMARY KEY,
499
- key TEXT NOT NULL,
500
- content TEXT NOT NULL,
501
- type TEXT NOT NULL DEFAULT 'episodic',
502
- namespace TEXT NOT NULL DEFAULT 'transcript-archive',
503
- tags JSONB NOT NULL DEFAULT '[]',
504
- metadata JSONB NOT NULL DEFAULT '{}',
505
- access_level TEXT NOT NULL DEFAULT 'private',
506
- created_at BIGINT NOT NULL,
507
- updated_at BIGINT NOT NULL,
508
- version INTEGER NOT NULL DEFAULT 1,
509
- access_count INTEGER NOT NULL DEFAULT 0,
510
- last_accessed_at BIGINT NOT NULL,
511
- content_hash TEXT,
512
- session_id TEXT,
513
- chunk_index INTEGER,
514
- summary TEXT,
515
- embedding vector(768)
516
- );
517
-
518
- CREATE INDEX IF NOT EXISTS idx_te_namespace ON transcript_entries(namespace);
519
- CREATE INDEX IF NOT EXISTS idx_te_session ON transcript_entries(session_id);
520
- CREATE INDEX IF NOT EXISTS idx_te_hash ON transcript_entries(content_hash);
521
- CREATE INDEX IF NOT EXISTS idx_te_chunk ON transcript_entries(session_id, chunk_index);
522
- CREATE INDEX IF NOT EXISTS idx_te_created ON transcript_entries(created_at);
523
- `);
524
- } finally {
525
- client.release();
526
- }
527
- }
528
-
529
- async store(entry) {
530
- const embeddingArr = entry._embedding
531
- ? `[${Array.from(entry._embedding).join(',')}]`
532
- : null;
533
- await this.pool.query(
534
- `INSERT INTO transcript_entries
535
- (id, key, content, type, namespace, tags, metadata, access_level,
536
- created_at, updated_at, version, access_count, last_accessed_at,
537
- content_hash, session_id, chunk_index, summary, embedding)
538
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
539
- ON CONFLICT (id) DO NOTHING`,
540
- [
541
- entry.id, entry.key, entry.content, entry.type, entry.namespace,
542
- JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.accessLevel,
543
- entry.createdAt, entry.updatedAt, entry.version, entry.accessCount, entry.lastAccessedAt,
544
- entry.metadata?.contentHash || null,
545
- entry.metadata?.sessionId || null,
546
- entry.metadata?.chunkIndex ?? null,
547
- entry.metadata?.summary || null,
548
- embeddingArr,
549
- ]
550
- );
551
- }
552
-
553
- async bulkInsert(entries) {
554
- const client = await this.pool.connect();
555
- try {
556
- await client.query('BEGIN');
557
- for (const entry of entries) {
558
- const embeddingArr = entry._embedding
559
- ? `[${Array.from(entry._embedding).join(',')}]`
560
- : null;
561
- await client.query(
562
- `INSERT INTO transcript_entries
563
- (id, key, content, type, namespace, tags, metadata, access_level,
564
- created_at, updated_at, version, access_count, last_accessed_at,
565
- content_hash, session_id, chunk_index, summary, embedding)
566
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
567
- ON CONFLICT (id) DO NOTHING`,
568
- [
569
- entry.id, entry.key, entry.content, entry.type, entry.namespace,
570
- JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.accessLevel,
571
- entry.createdAt, entry.updatedAt, entry.version, entry.accessCount, entry.lastAccessedAt,
572
- entry.metadata?.contentHash || null,
573
- entry.metadata?.sessionId || null,
574
- entry.metadata?.chunkIndex ?? null,
575
- entry.metadata?.summary || null,
576
- embeddingArr,
577
- ]
578
- );
579
- }
580
- await client.query('COMMIT');
581
- } catch (err) {
582
- await client.query('ROLLBACK');
583
- throw err;
584
- } finally {
585
- client.release();
586
- }
587
- }
588
-
589
- async query(opts) {
590
- let sql = 'SELECT * FROM transcript_entries';
591
- const params = [];
592
- const clauses = [];
593
- if (opts?.namespace) { params.push(opts.namespace); clauses.push(`namespace = $${params.length}`); }
594
- if (clauses.length) sql += ' WHERE ' + clauses.join(' AND ');
595
- sql += ' ORDER BY created_at DESC';
596
- if (opts?.limit) { params.push(opts.limit); sql += ` LIMIT $${params.length}`; }
597
- const { rows } = await this.pool.query(sql, params);
598
- return rows.map(r => this._rowToEntry(r));
599
- }
600
-
601
- async queryBySession(namespace, sessionId) {
602
- const { rows } = await this.pool.query(
603
- 'SELECT * FROM transcript_entries WHERE namespace = $1 AND session_id = $2 ORDER BY chunk_index DESC',
604
- [namespace, sessionId]
605
- );
606
- return rows.map(r => this._rowToEntry(r));
607
- }
608
-
609
- hashExists(hash) {
610
- // Synchronous check not possible with pg — use a cached check
611
- // The bulkInsert uses ON CONFLICT DO NOTHING for dedup at DB level
612
- return false;
613
- }
614
-
615
- async hashExistsAsync(hash) {
616
- const { rows } = await this.pool.query(
617
- 'SELECT 1 FROM transcript_entries WHERE content_hash = $1 LIMIT 1',
618
- [hash]
619
- );
620
- return rows.length > 0;
621
- }
622
-
623
- async count(namespace) {
624
- const sql = namespace
625
- ? 'SELECT COUNT(*) as cnt FROM transcript_entries WHERE namespace = $1'
626
- : 'SELECT COUNT(*) as cnt FROM transcript_entries';
627
- const params = namespace ? [namespace] : [];
628
- const { rows } = await this.pool.query(sql, params);
629
- return parseInt(rows[0].cnt, 10);
630
- }
631
-
632
- async listNamespaces() {
633
- const { rows } = await this.pool.query('SELECT DISTINCT namespace FROM transcript_entries');
634
- return rows.map(r => r.namespace);
635
- }
636
-
637
- async listSessions(namespace) {
638
- const { rows } = await this.pool.query(
639
- `SELECT session_id, COUNT(*) as cnt FROM transcript_entries
640
- WHERE namespace = $1 GROUP BY session_id ORDER BY MAX(created_at) DESC`,
641
- [namespace || NAMESPACE]
642
- );
643
- return rows.map(r => ({ session_id: r.session_id, cnt: parseInt(r.cnt, 10) }));
644
- }
645
-
646
- async markAccessed(ids) {
647
- const now = Date.now();
648
- for (const id of ids) {
649
- await this.pool.query(
650
- 'UPDATE transcript_entries SET access_count = access_count + 1, last_accessed_at = $1 WHERE id = $2',
651
- [now, id]
652
- );
653
- }
654
- }
655
-
656
- async pruneStale(namespace, maxAgeDays) {
657
- const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
658
- const { rowCount } = await this.pool.query(
659
- 'DELETE FROM transcript_entries WHERE namespace = $1 AND access_count = 0 AND created_at < $2',
660
- [namespace || NAMESPACE, cutoff]
661
- );
662
- return rowCount;
663
- }
664
-
665
- async queryByImportance(namespace, sessionId) {
666
- const now = Date.now();
667
- const { rows } = await this.pool.query(`
668
- SELECT *, (
669
- (CAST(access_count AS REAL) + 1) *
670
- (1.0 / (1.0 + ($1 - created_at) / 86400000.0)) *
671
- (CASE WHEN jsonb_array_length(metadata->'toolNames') > 0 THEN 1.5 ELSE 1.0 END) *
672
- (CASE WHEN jsonb_array_length(metadata->'filePaths') > 0 THEN 1.3 ELSE 1.0 END)
673
- ) AS importance_score
674
- FROM transcript_entries
675
- WHERE namespace = $2 AND session_id = $3
676
- ORDER BY importance_score DESC
677
- `, [now, namespace, sessionId]);
678
- return rows.map(r => ({ ...this._rowToEntry(r), importanceScore: r.importance_score }));
679
- }
680
-
681
- async shutdown() {
682
- if (this.pool) {
683
- await this.pool.end();
684
- this.pool = null;
685
- }
686
- }
687
-
688
- _rowToEntry(row) {
689
- return {
690
- id: row.id,
691
- key: row.key,
692
- content: row.content,
693
- type: row.type,
694
- namespace: row.namespace,
695
- tags: typeof row.tags === 'string' ? JSON.parse(row.tags) : row.tags,
696
- metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
697
- accessLevel: row.access_level,
698
- createdAt: parseInt(row.created_at, 10),
699
- updatedAt: parseInt(row.updated_at, 10),
700
- version: row.version,
701
- accessCount: row.access_count,
702
- lastAccessedAt: parseInt(row.last_accessed_at, 10),
703
- references: [],
704
- };
705
- }
706
- }
707
-
708
- /**
709
- * Parse RuVector config from environment variables.
710
- * Returns null if required vars are not set.
711
- */
712
- function getRuVectorConfig() {
713
- const host = process.env.RUVECTOR_HOST || process.env.PGHOST;
714
- const database = process.env.RUVECTOR_DATABASE || process.env.PGDATABASE;
715
- const user = process.env.RUVECTOR_USER || process.env.PGUSER;
716
- const password = process.env.RUVECTOR_PASSWORD || process.env.PGPASSWORD;
717
-
718
- if (!host || !database || !user) return null;
719
-
720
- return {
721
- host,
722
- port: parseInt(process.env.RUVECTOR_PORT || process.env.PGPORT || '5432', 10),
723
- database,
724
- user,
725
- password: password || '',
726
- ssl: process.env.RUVECTOR_SSL === 'true',
727
- };
728
- }
729
-
730
- // ============================================================================
731
- // Backend resolution: SQLite > RuVector PostgreSQL > AgentDB > JSON
732
- // ============================================================================
733
-
734
- async function resolveBackend() {
735
- // Tier 1: better-sqlite3 (native, fastest, local)
736
- try {
737
- const backend = new SQLiteBackend(ARCHIVE_DB_PATH);
738
- await backend.initialize();
739
- return { backend: trackBackend(backend), type: 'sqlite' };
740
- } catch { /* fall through */ }
741
-
742
- // Tier 2: RuVector PostgreSQL (TB-scale, vector search, GNN)
743
- try {
744
- const rvConfig = getRuVectorConfig();
745
- if (rvConfig) {
746
- const backend = new RuVectorBackend(rvConfig);
747
- await backend.initialize();
748
- return { backend: trackBackend(backend), type: 'ruvector' };
749
- }
750
- } catch { /* fall through */ }
751
-
752
- // Tier 3: AgentDB from @claude-flow/memory (HNSW)
753
- try {
754
- const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
755
- let memPkg = null;
756
- if (existsSync(localDist)) {
757
- memPkg = await import(`file://${localDist}`);
758
- } else {
759
- memPkg = await import('@claude-flow/memory');
760
- }
761
- if (memPkg?.AgentDBBackend) {
762
- const backend = new memPkg.AgentDBBackend();
763
- await backend.initialize();
764
- return { backend: trackBackend(backend), type: 'agentdb' };
765
- }
766
- } catch { /* fall through */ }
767
-
768
- // Tier 4: JSON file (always works)
769
- const backend = new JsonFileBackend(ARCHIVE_JSON_PATH);
770
- await backend.initialize();
771
- return { backend: trackBackend(backend), type: 'json' };
772
- }
773
-
774
- // ============================================================================
775
- // ONNX Embedding (384-dim, all-MiniLM-L6-v2 via @xenova/transformers)
776
- // ============================================================================
777
-
778
- const EMBEDDING_DIM = 384; // ONNX all-MiniLM-L6-v2 output dimension
779
- let _onnxPipeline = null;
780
- let _onnxFailed = false;
781
-
782
- /**
783
- * Initialize ONNX embedding pipeline (lazy, cached).
784
- * Returns null if @xenova/transformers is not available.
785
- */
786
- async function getOnnxPipeline() {
787
- if (_onnxFailed) return null;
788
- if (_onnxPipeline) return _onnxPipeline;
789
- try {
790
- const { pipeline } = await import('@xenova/transformers');
791
- _onnxPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
792
- return _onnxPipeline;
793
- } catch {
794
- _onnxFailed = true;
795
- return null;
796
- }
797
- }
798
-
799
- /**
800
- * Generate ONNX embedding (384-dim, high quality semantic vectors).
801
- * Falls back to hash embedding if ONNX is unavailable.
802
- */
803
- async function createEmbedding(text) {
804
- // Try ONNX first (384-dim, real semantic understanding)
805
- const pipe = await getOnnxPipeline();
806
- if (pipe) {
807
- try {
808
- const truncated = text.slice(0, 512); // MiniLM max ~512 tokens
809
- const output = await pipe(truncated, { pooling: 'mean', normalize: true });
810
- return { embedding: new Float32Array(output.data), dim: 384, method: 'onnx' };
811
- } catch { /* fall through to hash */ }
812
- }
813
- // Fallback: hash embedding (384-dim to match ONNX dimension)
814
- return { embedding: createHashEmbedding(text, 384), dim: 384, method: 'hash' };
815
- }
816
-
817
- // ============================================================================
818
- // Hash embedding fallback (deterministic, sub-millisecond)
819
- // ============================================================================
820
-
821
- function createHashEmbedding(text, dimensions = 384) {
822
- const embedding = new Float32Array(dimensions);
823
- const normalized = text.toLowerCase().trim();
824
- for (let i = 0; i < dimensions; i++) {
825
- let hash = 0;
826
- for (let j = 0; j < normalized.length; j++) {
827
- hash = ((hash << 5) - hash + normalized.charCodeAt(j) * (i + 1)) | 0;
828
- }
829
- embedding[i] = (Math.sin(hash) + 1) / 2;
830
- }
831
- let norm = 0;
832
- for (let i = 0; i < dimensions; i++) norm += embedding[i] * embedding[i];
833
- norm = Math.sqrt(norm);
834
- if (norm > 0) for (let i = 0; i < dimensions; i++) embedding[i] /= norm;
835
- return embedding;
836
- }
837
-
838
- // ============================================================================
839
- // Content hash for dedup
840
- // ============================================================================
841
-
842
- function hashContent(content) {
843
- return createHash('sha256').update(content).digest('hex');
844
- }
845
-
846
- // ============================================================================
847
- // Read stdin with timeout (hooks receive JSON input on stdin)
848
- // ============================================================================
849
-
850
- function readStdin(timeoutMs = 100) {
851
- return new Promise((resolve) => {
852
- let data = '';
853
- const timer = setTimeout(() => {
854
- process.stdin.removeAllListeners();
855
- resolve(data ? JSON.parse(data) : null);
856
- }, timeoutMs);
857
-
858
- if (process.stdin.isTTY) {
859
- clearTimeout(timer);
860
- resolve(null);
861
- return;
862
- }
863
-
864
- process.stdin.setEncoding('utf-8');
865
- process.stdin.on('data', (chunk) => { data += chunk; });
866
- process.stdin.on('end', () => {
867
- clearTimeout(timer);
868
- try { resolve(data ? JSON.parse(data) : null); }
869
- catch { resolve(null); }
870
- });
871
- process.stdin.on('error', () => {
872
- clearTimeout(timer);
873
- resolve(null);
874
- });
875
- process.stdin.resume();
876
- });
877
- }
878
-
879
- // ============================================================================
880
- // Transcript parsing
881
- // ============================================================================
882
-
883
- function parseTranscript(transcriptPath) {
884
- if (!existsSync(transcriptPath)) return [];
885
- const content = readFileSync(transcriptPath, 'utf-8');
886
- const lines = content.split('\n').filter(Boolean);
887
- const messages = [];
888
- for (const line of lines) {
889
- try {
890
- const parsed = JSON.parse(line);
891
- // SDK transcript wraps messages: { type: "user"|"A", message: { role, content } }
892
- // Unwrap to get the inner API message with role/content
893
- if (parsed.message && parsed.message.role) {
894
- messages.push(parsed.message);
895
- } else if (parsed.role) {
896
- // Already in API message format (e.g. from tests)
897
- messages.push(parsed);
898
- }
899
- // Skip non-message entries (progress, file-history-snapshot, queue-operation)
900
- } catch { /* skip malformed lines */ }
901
- }
902
- return messages;
903
- }
904
-
905
- // ============================================================================
906
- // Extract text content from message content blocks
907
- // ============================================================================
908
-
909
- function extractTextContent(message) {
910
- if (!message) return '';
911
- if (typeof message.content === 'string') return message.content;
912
- if (Array.isArray(message.content)) {
913
- return message.content
914
- .filter(b => b.type === 'text')
915
- .map(b => b.text || '')
916
- .join('\n');
917
- }
918
- if (typeof message.text === 'string') return message.text;
919
- return '';
920
- }
921
-
922
- // ============================================================================
923
- // Extract tool calls from assistant message
924
- // ============================================================================
925
-
926
- function extractToolCalls(message) {
927
- if (!message || !Array.isArray(message.content)) return [];
928
- return message.content
929
- .filter(b => b.type === 'tool_use')
930
- .map(b => ({
931
- name: b.name || 'unknown',
932
- input: b.input || {},
933
- }));
934
- }
935
-
936
- // ============================================================================
937
- // Extract file paths from tool calls
938
- // ============================================================================
939
-
940
- function extractFilePaths(toolCalls) {
941
- const paths = new Set();
942
- for (const tc of toolCalls) {
943
- if (tc.input?.file_path) paths.add(tc.input.file_path);
944
- if (tc.input?.path) paths.add(tc.input.path);
945
- if (tc.input?.notebook_path) paths.add(tc.input.notebook_path);
946
- }
947
- return [...paths];
948
- }
949
-
950
- // ============================================================================
951
- // Chunk transcript into conversation turns
952
- // ============================================================================
953
-
954
- function chunkTranscript(messages) {
955
- const relevant = messages.filter(
956
- m => m.role === 'user' || m.role === 'assistant'
957
- );
958
- const capped = relevant.slice(-MAX_MESSAGES);
959
-
960
- const chunks = [];
961
- let currentChunk = null;
962
-
963
- for (const msg of capped) {
964
- if (msg.role === 'user') {
965
- const isSynthetic = Array.isArray(msg.content) &&
966
- msg.content.every(b => b.type === 'tool_result');
967
- if (isSynthetic && currentChunk) continue;
968
- if (currentChunk) chunks.push(currentChunk);
969
- currentChunk = {
970
- userMessage: msg,
971
- assistantMessage: null,
972
- toolCalls: [],
973
- turnIndex: chunks.length,
974
- };
975
- } else if (msg.role === 'assistant' && currentChunk) {
976
- currentChunk.assistantMessage = msg;
977
- currentChunk.toolCalls = extractToolCalls(msg);
978
- }
979
- }
980
-
981
- if (currentChunk) chunks.push(currentChunk);
982
- return chunks;
983
- }
984
-
985
- // ============================================================================
986
- // Extract summary from chunk (no LLM, extractive only)
987
- // ============================================================================
988
-
989
- function extractSummary(chunk) {
990
- const parts = [];
991
-
992
- const userText = extractTextContent(chunk.userMessage);
993
- const firstUserLine = userText.split('\n').find(l => l.trim()) || '';
994
- if (firstUserLine) parts.push(firstUserLine.slice(0, 100));
995
-
996
- const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
997
- if (toolNames.length) parts.push('Tools: ' + toolNames.join(', '));
998
-
999
- const filePaths = extractFilePaths(chunk.toolCalls);
1000
- if (filePaths.length) {
1001
- const shortPaths = filePaths.slice(0, 5).map(p => {
1002
- const segs = p.split('/');
1003
- return segs.length > 2 ? '.../' + segs.slice(-2).join('/') : p;
1004
- });
1005
- parts.push('Files: ' + shortPaths.join(', '));
1006
- }
1007
-
1008
- const assistantText = extractTextContent(chunk.assistantMessage);
1009
- const assistantLines = assistantText.split('\n').filter(l => l.trim()).slice(0, 2);
1010
- if (assistantLines.length) parts.push(assistantLines.join(' ').slice(0, 120));
1011
-
1012
- return parts.join(' | ').slice(0, 300);
1013
- }
1014
-
1015
- // ============================================================================
1016
- // Generate unique ID
1017
- // ============================================================================
1018
-
1019
- let idCounter = 0;
1020
- function generateId() {
1021
- return `ctx-${Date.now()}-${++idCounter}-${Math.random().toString(36).slice(2, 8)}`;
1022
- }
1023
-
1024
- // ============================================================================
1025
- // Build MemoryEntry from chunk
1026
- // ============================================================================
1027
-
1028
- function buildEntry(chunk, sessionId, trigger, timestamp) {
1029
- const userText = extractTextContent(chunk.userMessage);
1030
- const assistantText = extractTextContent(chunk.assistantMessage);
1031
- const fullContent = `User: ${userText}\n\nAssistant: ${assistantText}`;
1032
- const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
1033
- const filePaths = extractFilePaths(chunk.toolCalls);
1034
- const summary = extractSummary(chunk);
1035
- const contentHash = hashContent(fullContent);
1036
-
1037
- const now = Date.now();
1038
- return {
1039
- id: generateId(),
1040
- key: `transcript:${sessionId}:${chunk.turnIndex}:${timestamp}`,
1041
- content: fullContent,
1042
- type: 'episodic',
1043
- namespace: NAMESPACE,
1044
- tags: ['transcript', 'compaction', sessionId, ...toolNames],
1045
- metadata: {
1046
- sessionId,
1047
- chunkIndex: chunk.turnIndex,
1048
- trigger,
1049
- timestamp,
1050
- toolNames,
1051
- filePaths,
1052
- summary,
1053
- contentHash,
1054
- turnRange: [chunk.turnIndex, chunk.turnIndex],
1055
- },
1056
- accessLevel: 'private',
1057
- createdAt: now,
1058
- updatedAt: now,
1059
- version: 1,
1060
- references: [],
1061
- accessCount: 0,
1062
- lastAccessedAt: now,
1063
- };
1064
- }
1065
-
1066
- // ============================================================================
1067
- // Store chunks with dedup (uses indexed hash lookup for SQLite)
1068
- // ============================================================================
1069
-
1070
- async function storeChunks(backend, chunks, sessionId, trigger) {
1071
- const timestamp = new Date().toISOString();
1072
-
1073
- const entries = [];
1074
- for (const chunk of chunks) {
1075
- const entry = buildEntry(chunk, sessionId, trigger, timestamp);
1076
- // Fast hash-based dedup (indexed lookup in SQLite, scan in JSON)
1077
- if (!backend.hashExists(entry.metadata.contentHash)) {
1078
- entries.push(entry);
1079
- }
1080
- }
1081
-
1082
- if (entries.length > 0) {
1083
- await backend.bulkInsert(entries);
1084
- }
1085
-
1086
- return { stored: entries.length, deduped: chunks.length - entries.length };
1087
- }
1088
-
1089
- // ============================================================================
1090
- // Retrieve context for restoration (uses indexed session query for SQLite)
1091
- // ============================================================================
1092
-
1093
- async function retrieveContext(backend, sessionId, budget) {
1094
- // Use optimized session query if available, otherwise filter manually
1095
- const sessionEntries = backend.queryBySession
1096
- ? await backend.queryBySession(NAMESPACE, sessionId)
1097
- : (await backend.query({ namespace: NAMESPACE }))
1098
- .filter(e => e.metadata?.sessionId === sessionId)
1099
- .sort((a, b) => (b.metadata?.chunkIndex ?? 0) - (a.metadata?.chunkIndex ?? 0));
1100
-
1101
- if (sessionEntries.length === 0) return '';
1102
-
1103
- const lines = [];
1104
- let charCount = 0;
1105
- const header = `## Restored Context (from pre-compaction archive)\n\nPrevious conversation included ${sessionEntries.length} archived turns:\n\n`;
1106
- charCount += header.length;
1107
-
1108
- for (const entry of sessionEntries) {
1109
- const meta = entry.metadata || {};
1110
- const toolStr = meta.toolNames?.length ? ` Tools: ${meta.toolNames.join(', ')}.` : '';
1111
- const fileStr = meta.filePaths?.length ? ` Files: ${meta.filePaths.slice(0, 3).join(', ')}.` : '';
1112
- const line = `- [Turn ${meta.chunkIndex ?? '?'}] ${meta.summary || '(no summary)'}${toolStr}${fileStr}`;
1113
-
1114
- if (charCount + line.length + 1 > budget) break;
1115
- lines.push(line);
1116
- charCount += line.length + 1;
1117
- }
1118
-
1119
- if (lines.length === 0) return '';
1120
-
1121
- const footer = `\n\nFull archive: ${NAMESPACE} namespace in AgentDB (query with session ID: ${sessionId})`;
1122
- return header + lines.join('\n') + footer;
1123
- }
1124
-
1125
- // ============================================================================
1126
- // Build custom compact instructions (exit code 0 stdout)
1127
- // Guides Claude on what to preserve during compaction summary
1128
- // ============================================================================
1129
-
1130
- function buildCompactInstructions(chunks, sessionId, archiveResult) {
1131
- const parts = [];
1132
-
1133
- parts.push('COMPACTION GUIDANCE (from context-persistence-hook):');
1134
- parts.push('');
1135
- parts.push(`All ${chunks.length} conversation turns have been archived to the transcript-archive database.`);
1136
- parts.push(`Session: ${sessionId} | Stored: ${archiveResult.stored} new, ${archiveResult.deduped} deduped.`);
1137
- parts.push('After compaction, archived context will be automatically restored via SessionStart hook.');
1138
- parts.push('');
1139
-
1140
- // Collect unique tools and files across all chunks for preservation hints
1141
- const allTools = new Set();
1142
- const allFiles = new Set();
1143
- const decisions = [];
1144
-
1145
- for (const chunk of chunks) {
1146
- const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
1147
- for (const t of toolNames) allTools.add(t);
1148
- const filePaths = extractFilePaths(chunk.toolCalls);
1149
- for (const f of filePaths) allFiles.add(f);
1150
-
1151
- // Look for decision indicators in assistant text
1152
- const assistantText = extractTextContent(chunk.assistantMessage);
1153
- if (assistantText) {
1154
- const lower = assistantText.toLowerCase();
1155
- if (lower.includes('decided') || lower.includes('choosing') || lower.includes('approach')
1156
- || lower.includes('instead of') || lower.includes('rather than')) {
1157
- const firstLine = assistantText.split('\n').find(l => l.trim()) || '';
1158
- if (firstLine.length > 10) decisions.push(firstLine.slice(0, 120));
1159
- }
1160
- }
1161
- }
1162
-
1163
- parts.push('PRESERVE in compaction summary:');
1164
-
1165
- if (allFiles.size > 0) {
1166
- const fileList = [...allFiles].slice(0, 15).map(f => {
1167
- const segs = f.split('/');
1168
- return segs.length > 3 ? '.../' + segs.slice(-3).join('/') : f;
1169
- });
1170
- parts.push(`- Files modified/read: ${fileList.join(', ')}`);
1171
- }
1172
-
1173
- if (allTools.size > 0) {
1174
- parts.push(`- Tools used: ${[...allTools].join(', ')}`);
1175
- }
1176
-
1177
- if (decisions.length > 0) {
1178
- parts.push('- Key decisions:');
1179
- for (const d of decisions.slice(0, 5)) {
1180
- parts.push(` * ${d}`);
1181
- }
1182
- }
1183
-
1184
- // Recent turns summary (most important context)
1185
- const recentChunks = chunks.slice(-5);
1186
- if (recentChunks.length > 0) {
1187
- parts.push('');
1188
- parts.push('MOST RECENT TURNS (prioritize preserving):');
1189
- for (const chunk of recentChunks) {
1190
- const userText = extractTextContent(chunk.userMessage);
1191
- const firstLine = userText.split('\n').find(l => l.trim()) || '';
1192
- const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
1193
- parts.push(`- [Turn ${chunk.turnIndex}] ${firstLine.slice(0, 80)}${toolNames.length ? ` (${toolNames.join(', ')})` : ''}`);
1194
- }
1195
- }
1196
-
1197
- // Cap at budget
1198
- let result = parts.join('\n');
1199
- if (result.length > COMPACT_INSTRUCTION_BUDGET) {
1200
- result = result.slice(0, COMPACT_INSTRUCTION_BUDGET - 3) + '...';
1201
- }
1202
- return result;
1203
- }
1204
-
1205
- // ============================================================================
1206
- // Importance scoring for retrieval ranking
1207
- // ============================================================================
1208
-
1209
- function computeImportance(entry, now) {
1210
- const meta = entry.metadata || {};
1211
- const accessCount = entry.accessCount || 0;
1212
- const createdAt = entry.createdAt || now;
1213
- const ageMs = Math.max(1, now - createdAt);
1214
- const ageDays = ageMs / 86400000;
1215
-
1216
- // Recency: exponential decay, half-life of 7 days
1217
- const recency = Math.exp(-0.693 * ageDays / 7);
1218
-
1219
- // Frequency: log-scaled access count
1220
- const frequency = Math.log2(accessCount + 1) + 1;
1221
-
1222
- // Richness: tool calls and file paths indicate actionable context
1223
- const toolCount = meta.toolNames?.length || 0;
1224
- const fileCount = meta.filePaths?.length || 0;
1225
- const richness = 1.0 + (toolCount > 0 ? 0.5 : 0) + (fileCount > 0 ? 0.3 : 0);
1226
-
1227
- return recency * frequency * richness;
1228
- }
1229
-
1230
- // ============================================================================
1231
- // Smart retrieval: importance-ranked instead of just recency
1232
- // ============================================================================
1233
-
1234
- async function retrieveContextSmart(backend, sessionId, budget) {
1235
- let sessionEntries;
1236
-
1237
- // Use importance-ranked query if backend supports it
1238
- if (backend.queryByImportance) {
1239
- try {
1240
- sessionEntries = backend.queryByImportance(NAMESPACE, sessionId);
1241
- } catch {
1242
- // Fall back to standard query
1243
- sessionEntries = null;
1244
- }
1245
- }
1246
-
1247
- if (!sessionEntries) {
1248
- // Fall back: fetch all, compute importance in JS
1249
- const raw = backend.queryBySession
1250
- ? await backend.queryBySession(NAMESPACE, sessionId)
1251
- : (await backend.query({ namespace: NAMESPACE }))
1252
- .filter(e => e.metadata?.sessionId === sessionId);
1253
-
1254
- const now = Date.now();
1255
- sessionEntries = raw
1256
- .map(e => ({ ...e, importanceScore: computeImportance(e, now) }))
1257
- .sort((a, b) => b.importanceScore - a.importanceScore);
1258
- }
1259
-
1260
- if (sessionEntries.length === 0) return { text: '', accessedIds: [] };
1261
-
1262
- const lines = [];
1263
- const accessedIds = [];
1264
- let charCount = 0;
1265
- const header = `## Restored Context (importance-ranked from archive)\n\nPrevious conversation: ${sessionEntries.length} archived turns, ranked by importance:\n\n`;
1266
- charCount += header.length;
1267
-
1268
- for (const entry of sessionEntries) {
1269
- const meta = entry.metadata || {};
1270
- const score = entry.importanceScore?.toFixed(2) || '?';
1271
- const toolStr = meta.toolNames?.length ? ` Tools: ${meta.toolNames.join(', ')}.` : '';
1272
- const fileStr = meta.filePaths?.length ? ` Files: ${meta.filePaths.slice(0, 3).join(', ')}.` : '';
1273
- const line = `- [Turn ${meta.chunkIndex ?? '?'}, score:${score}] ${meta.summary || '(no summary)'}${toolStr}${fileStr}`;
1274
-
1275
- if (charCount + line.length + 1 > budget) break;
1276
- lines.push(line);
1277
- accessedIds.push(entry.id);
1278
- charCount += line.length + 1;
1279
- }
1280
-
1281
- if (lines.length === 0) return { text: '', accessedIds: [] };
1282
-
1283
- // Cross-session semantic search: find related context from previous sessions
1284
- let crossSessionText = '';
1285
- if (backend.semanticSearch && sessionEntries.length > 0) {
1286
- try {
1287
- // Use the most recent turn's summary as the search query
1288
- const recentSummary = sessionEntries[0]?.metadata?.summary || '';
1289
- if (recentSummary) {
1290
- const crossResults = await crossSessionSearch(backend, recentSummary, sessionId, 3);
1291
- if (crossResults.length > 0) {
1292
- const crossLines = crossResults.map(r =>
1293
- `- [Session ${r.sessionId?.slice(0, 8)}..., turn ${r.chunkIndex ?? '?'}, conf:${(r.confidence || 0).toFixed(2)}] ${r.summary || '(no summary)'}`
1294
- );
1295
- crossSessionText = `\n\nRelated context from previous sessions:\n${crossLines.join('\n')}`;
1296
- }
1297
- }
1298
- } catch { /* cross-session search is best-effort */ }
1299
- }
1300
-
1301
- const footer = `\n\nFull archive: ${NAMESPACE} namespace (session: ${sessionId}). ${sessionEntries.length - lines.length} additional turns available.`;
1302
- return { text: header + lines.join('\n') + crossSessionText + footer, accessedIds };
1303
- }
1304
-
1305
- // ============================================================================
1306
- // Auto-optimize: prune stale entries, run after archiving
1307
- // ============================================================================
1308
-
1309
- async function autoOptimize(backend, backendType) {
1310
- if (!AUTO_OPTIMIZE) return { pruned: 0, synced: 0, decayed: 0, embedded: 0 };
1311
-
1312
- let pruned = 0;
1313
- let decayed = 0;
1314
- let embedded = 0;
1315
-
1316
- // Step 1: Confidence decay — reduce confidence for unaccessed entries
1317
- if (backend.decayConfidence) {
1318
- try {
1319
- decayed = backend.decayConfidence(NAMESPACE, 1); // 1 hour worth of decay per optimize cycle
1320
- } catch { /* non-critical */ }
1321
- }
1322
-
1323
- // Step 2: Smart pruning — remove low-confidence entries first
1324
- if (backend.pruneByConfidence) {
1325
- try {
1326
- pruned += backend.pruneByConfidence(NAMESPACE, 0.15);
1327
- } catch { /* non-critical */ }
1328
- }
1329
-
1330
- // Step 3: Age-based pruning as fallback
1331
- if (backend.pruneStale) {
1332
- try {
1333
- pruned += backend.pruneStale(NAMESPACE, RETENTION_DAYS);
1334
- } catch { /* non-critical */ }
1335
- }
1336
-
1337
- // Step 4: Generate ONNX embeddings (384-dim) for entries missing them
1338
- if (backend.storeEmbedding) {
1339
- try {
1340
- const rows = backend.db?.prepare?.(
1341
- 'SELECT id, content FROM transcript_entries WHERE namespace = ? AND embedding IS NULL LIMIT 20'
1342
- )?.all(NAMESPACE);
1343
- if (rows) {
1344
- for (const row of rows) {
1345
- const { embedding } = await createEmbedding(row.content);
1346
- backend.storeEmbedding(row.id, embedding);
1347
- embedded++;
1348
- }
1349
- }
1350
- } catch { /* non-critical */ }
1351
- }
1352
-
1353
- // Step 5: Auto-sync to RuVector if available
1354
- let synced = 0;
1355
- if (backendType === 'sqlite' && backend.allForSync) {
1356
- try {
1357
- const rvConfig = getRuVectorConfig();
1358
- if (rvConfig) {
1359
- const rvBackend = new RuVectorBackend(rvConfig);
1360
- await rvBackend.initialize();
1361
-
1362
- const allEntries = backend.allForSync(NAMESPACE);
1363
- if (allEntries.length > 0) {
1364
- // Add hash embeddings for vector search in RuVector
1365
- const entriesToSync = allEntries.map(e => ({
1366
- ...e,
1367
- _embedding: createHashEmbedding(e.content),
1368
- }));
1369
- await rvBackend.bulkInsert(entriesToSync);
1370
- synced = entriesToSync.length;
1371
- }
1372
-
1373
- await rvBackend.shutdown();
1374
- }
1375
- } catch { /* RuVector sync is best-effort */ }
1376
- }
1377
-
1378
- return { pruned, synced, decayed, embedded };
1379
- }
1380
-
1381
- // ============================================================================
1382
- // Cross-session semantic retrieval
1383
- // ============================================================================
1384
-
1385
- /**
1386
- * Find relevant context from OTHER sessions using semantic similarity.
1387
- * This enables "What did we discuss about auth?" across sessions.
1388
- */
1389
- async function crossSessionSearch(backend, queryText, currentSessionId, k = 5) {
1390
- if (!backend.semanticSearch) return [];
1391
- try {
1392
- const { embedding: queryEmb } = await createEmbedding(queryText);
1393
- const results = backend.semanticSearch(queryEmb, k * 2, NAMESPACE);
1394
- // Filter out current session entries (we already have those)
1395
- return results
1396
- .filter(r => r.sessionId !== currentSessionId)
1397
- .slice(0, k);
1398
- } catch { return []; }
1399
- }
1400
-
1401
- // ============================================================================
1402
- // Context Autopilot Engine
1403
- // ============================================================================
1404
-
1405
- /**
1406
- * Estimate context token usage from transcript JSONL.
1407
- *
1408
- * Primary method: Read the most recent assistant message's `usage` field which
1409
- * contains `input_tokens` + `cache_read_input_tokens` — this is the ACTUAL
1410
- * context size as reported by the Claude API. This includes system prompt,
1411
- * CLAUDE.md, tool definitions, all messages, and everything Claude sees.
1412
- *
1413
- * Fallback: Sum character lengths and divide by CHARS_PER_TOKEN.
1414
- */
1415
- function estimateContextTokens(transcriptPath) {
1416
- if (!existsSync(transcriptPath)) return { tokens: 0, turns: 0, method: 'none' };
1417
-
1418
- const content = readFileSync(transcriptPath, 'utf-8');
1419
- const lines = content.split('\n').filter(Boolean);
1420
-
1421
- // Track the most recent usage data (from the last assistant message)
1422
- let lastInputTokens = 0;
1423
- let lastCacheRead = 0;
1424
- let lastCacheCreate = 0;
1425
- let turns = 0;
1426
- let lastPreTokens = 0;
1427
- let totalChars = 0;
1428
-
1429
- for (let i = 0; i < lines.length; i++) {
1430
- try {
1431
- const parsed = JSON.parse(lines[i]);
1432
-
1433
- // Check for compact_boundary
1434
- if (parsed.type === 'system' && parsed.subtype === 'compact_boundary') {
1435
- lastPreTokens = parsed.compactMetadata?.preTokens
1436
- || parsed.compact_metadata?.pre_tokens || 0;
1437
- // Reset after compaction — new context starts here
1438
- totalChars = 0;
1439
- turns = 0;
1440
- lastInputTokens = 0;
1441
- lastCacheRead = 0;
1442
- lastCacheCreate = 0;
1443
- continue;
1444
- }
1445
-
1446
- // Extract ACTUAL token usage from assistant messages
1447
- // The SDK transcript stores: { message: { role, content, usage: { input_tokens, cache_read_input_tokens, ... } } }
1448
- const msg = parsed.message || parsed;
1449
- const usage = msg.usage;
1450
- if (usage && (msg.role === 'assistant' || parsed.type === 'assistant')) {
1451
- const inputTokens = usage.input_tokens || 0;
1452
- const cacheRead = usage.cache_read_input_tokens || 0;
1453
- const cacheCreate = usage.cache_creation_input_tokens || 0;
1454
-
1455
- // The total context sent to Claude = input_tokens + cache_read + cache_create
1456
- // input_tokens: non-cached tokens actually processed
1457
- // cache_read: tokens served from cache (still in context)
1458
- // cache_create: tokens newly cached (still in context)
1459
- const totalContext = inputTokens + cacheRead + cacheCreate;
1460
-
1461
- if (totalContext > 0) {
1462
- lastInputTokens = inputTokens;
1463
- lastCacheRead = cacheRead;
1464
- lastCacheCreate = cacheCreate;
1465
- }
1466
- }
1467
-
1468
- // Count turns for display
1469
- const role = msg.role || parsed.type;
1470
- if (role === 'user') turns++;
1471
-
1472
- // Char fallback accumulation
1473
- if (role === 'user' || role === 'assistant') {
1474
- const c = msg.content;
1475
- if (typeof c === 'string') totalChars += c.length;
1476
- else if (Array.isArray(c)) {
1477
- for (const block of c) {
1478
- if (block.text) totalChars += block.text.length;
1479
- else if (block.input) totalChars += JSON.stringify(block.input).length;
1480
- }
1481
- }
1482
- }
1483
- } catch { /* skip */ }
1484
- }
1485
-
1486
- // Primary: use actual API usage data
1487
- const actualTotal = lastInputTokens + lastCacheRead + lastCacheCreate;
1488
- if (actualTotal > 0) {
1489
- return {
1490
- tokens: actualTotal,
1491
- turns,
1492
- method: 'api-usage',
1493
- lastPreTokens,
1494
- breakdown: {
1495
- input: lastInputTokens,
1496
- cacheRead: lastCacheRead,
1497
- cacheCreate: lastCacheCreate,
1498
- },
1499
- };
1500
- }
1501
-
1502
- // Fallback: char-based estimate
1503
- const estimatedTokens = Math.ceil(totalChars / CHARS_PER_TOKEN);
1504
- if (lastPreTokens > 0) {
1505
- const compactSummaryTokens = 3000;
1506
- return {
1507
- tokens: compactSummaryTokens + estimatedTokens,
1508
- turns,
1509
- method: 'post-compact-char-estimate',
1510
- lastPreTokens,
1511
- };
1512
- }
1513
-
1514
- return { tokens: estimatedTokens, turns, method: 'char-estimate' };
1515
- }
1516
-
1517
- /**
1518
- * Load autopilot state (persisted across hook invocations).
1519
- */
1520
- function loadAutopilotState() {
1521
- try {
1522
- if (existsSync(AUTOPILOT_STATE_PATH)) {
1523
- return JSON.parse(readFileSync(AUTOPILOT_STATE_PATH, 'utf-8'));
1524
- }
1525
- } catch { /* fresh state */ }
1526
- return {
1527
- sessionId: null,
1528
- lastTokenEstimate: 0,
1529
- lastPercentage: 0,
1530
- pruneCount: 0,
1531
- warningIssued: false,
1532
- lastCheck: 0,
1533
- history: [], // Track token growth over time
1534
- };
1535
- }
1536
-
1537
- /**
1538
- * Save autopilot state.
1539
- */
1540
- function saveAutopilotState(state) {
1541
- try {
1542
- writeFileSync(AUTOPILOT_STATE_PATH, JSON.stringify(state, null, 2), 'utf-8');
1543
- } catch { /* best effort */ }
1544
- }
1545
-
1546
- /**
1547
- * Build a context optimization report for additionalContext injection.
1548
- */
1549
- function buildAutopilotReport(percentage, tokens, windowSize, turns, state) {
1550
- const bar = buildProgressBar(percentage);
1551
- const status = percentage >= AUTOPILOT_PRUNE_PCT
1552
- ? 'OPTIMIZING'
1553
- : percentage >= AUTOPILOT_WARN_PCT
1554
- ? 'WARNING'
1555
- : 'OK';
1556
-
1557
- const parts = [
1558
- `[ContextAutopilot] ${bar} ${(percentage * 100).toFixed(1)}% context used`,
1559
- `(~${formatTokens(tokens)}/${formatTokens(windowSize)} tokens, ${turns} turns)`,
1560
- `Status: ${status}`,
1561
- ];
1562
-
1563
- if (state.pruneCount > 0) {
1564
- parts.push(`| Optimizations: ${state.pruneCount} prune cycles`);
1565
- }
1566
-
1567
- // Add trend if we have history
1568
- if (state.history.length >= 2) {
1569
- const recent = state.history.slice(-3);
1570
- const avgGrowth = recent.reduce((sum, h, i) => {
1571
- if (i === 0) return 0;
1572
- return sum + (h.pct - recent[i - 1].pct);
1573
- }, 0) / (recent.length - 1);
1574
-
1575
- if (avgGrowth > 0) {
1576
- const turnsUntilFull = Math.ceil((1.0 - percentage) / avgGrowth);
1577
- parts.push(`| ~${turnsUntilFull} turns until optimization needed`);
1578
- }
1579
- }
1580
-
1581
- return parts.join(' ');
1582
- }
1583
-
1584
- /**
1585
- * Visual progress bar for context usage.
1586
- */
1587
- function buildProgressBar(percentage) {
1588
- const width = 20;
1589
- const filled = Math.round(percentage * width);
1590
- const empty = width - filled;
1591
- const fillChar = percentage >= AUTOPILOT_PRUNE_PCT ? '!' : percentage >= AUTOPILOT_WARN_PCT ? '#' : '=';
1592
- return `[${fillChar.repeat(filled)}${'-'.repeat(empty)}]`;
1593
- }
1594
-
1595
- /**
1596
- * Format token count for display.
1597
- */
1598
- function formatTokens(n) {
1599
- if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1600
- if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
1601
- return String(n);
1602
- }
1603
-
1604
- /**
1605
- * Context Autopilot: run on every UserPromptSubmit.
1606
- * Returns { additionalContext, shouldBlock } for the hook output.
1607
- */
1608
- async function runAutopilot(transcriptPath, sessionId, backend, backendType) {
1609
- const state = loadAutopilotState();
1610
-
1611
- // Reset state if session changed
1612
- if (state.sessionId !== sessionId) {
1613
- state.sessionId = sessionId;
1614
- state.lastTokenEstimate = 0;
1615
- state.lastPercentage = 0;
1616
- state.pruneCount = 0;
1617
- state.warningIssued = false;
1618
- state.history = [];
1619
- }
1620
-
1621
- // Estimate current context usage
1622
- const { tokens, turns, method, lastPreTokens } = estimateContextTokens(transcriptPath);
1623
- const percentage = Math.min(tokens / CONTEXT_WINDOW_TOKENS, 1.0);
1624
-
1625
- // Track history (keep last 50 data points)
1626
- state.history.push({ ts: Date.now(), tokens, pct: percentage, turns });
1627
- if (state.history.length > 50) state.history.shift();
1628
-
1629
- state.lastTokenEstimate = tokens;
1630
- state.lastPercentage = percentage;
1631
- state.lastCheck = Date.now();
1632
-
1633
- let optimizationMessage = '';
1634
-
1635
- // Phase 1: Warning zone (70-85%) — advise concise responses
1636
- if (percentage >= AUTOPILOT_WARN_PCT && percentage < AUTOPILOT_PRUNE_PCT) {
1637
- if (!state.warningIssued) {
1638
- state.warningIssued = true;
1639
- optimizationMessage = ` | Context at ${(percentage * 100).toFixed(0)}%. Keep responses concise to extend session.`;
1640
- }
1641
- }
1642
-
1643
- // Phase 2: Critical zone (85%+) — session rotation needed
1644
- if (percentage >= AUTOPILOT_PRUNE_PCT) {
1645
- state.pruneCount++;
1646
-
1647
- // Prune stale entries from archive to free up storage
1648
- if (backend.pruneStale) {
1649
- try {
1650
- const pruned = backend.pruneStale(NAMESPACE, Math.min(RETENTION_DAYS, 7));
1651
- if (pruned > 0) {
1652
- optimizationMessage += ` | Pruned ${pruned} stale archive entries.`;
1653
- }
1654
- } catch { /* non-critical */ }
1655
- }
1656
-
1657
- const turnsLeft = Math.max(0, Math.ceil((1.0 - percentage) / 0.03));
1658
- optimizationMessage += ` | CRITICAL: ${(percentage * 100).toFixed(0)}% context used (~${turnsLeft} turns left). All ${turns} turns archived. Start a new session with /clear — context will be fully restored via SessionStart hook.`;
1659
- }
1660
-
1661
- const report = buildAutopilotReport(percentage, tokens, CONTEXT_WINDOW_TOKENS, turns, state);
1662
- saveAutopilotState(state);
1663
-
1664
- return {
1665
- additionalContext: report + optimizationMessage,
1666
- percentage,
1667
- tokens,
1668
- turns,
1669
- method,
1670
- state,
1671
- };
1672
- }
1673
-
1674
- // ============================================================================
1675
- // Commands
1676
- // ============================================================================
1677
-
1678
- async function doPreCompact() {
1679
- const input = await readStdin(200);
1680
- if (!input) return;
1681
-
1682
- const { session_id: sessionId, transcript_path: transcriptPath, trigger } = input;
1683
- if (!transcriptPath || !sessionId) return;
1684
-
1685
- const messages = parseTranscript(transcriptPath);
1686
- if (messages.length === 0) return;
1687
-
1688
- const chunks = chunkTranscript(messages);
1689
- if (chunks.length === 0) return;
1690
-
1691
- const { backend, type } = await resolveBackend();
1692
-
1693
- const archiveResult = await storeChunks(backend, chunks, sessionId, trigger || 'auto');
1694
-
1695
- // Auto-optimize: prune stale entries + sync to RuVector if available
1696
- const optimizeResult = await autoOptimize(backend, type);
1697
-
1698
- const total = await backend.count(NAMESPACE);
1699
- await backend.shutdown();
1700
-
1701
- const optParts = [];
1702
- if (optimizeResult.pruned > 0) optParts.push(`${optimizeResult.pruned} pruned`);
1703
- if (optimizeResult.decayed > 0) optParts.push(`${optimizeResult.decayed} decayed`);
1704
- if (optimizeResult.embedded > 0) optParts.push(`${optimizeResult.embedded} embedded`);
1705
- if (optimizeResult.synced > 0) optParts.push(`${optimizeResult.synced} synced`);
1706
- const optimizeMsg = optParts.length > 0 ? ` Optimized: ${optParts.join(', ')}.` : '';
1707
- process.stderr.write(
1708
- `[ContextPersistence] Archived ${archiveResult.stored} turns (${archiveResult.deduped} deduped) via ${type}. Total: ${total}.${optimizeMsg}\n`
1709
- );
1710
-
1711
- // Exit code 0: stdout is appended as custom compact instructions
1712
- // This guides Claude on what to preserve in the compaction summary
1713
- const instructions = buildCompactInstructions(chunks, sessionId, archiveResult);
1714
- process.stdout.write(instructions);
1715
-
1716
- // Context Autopilot: track state and log archival status
1717
- // NOTE: Claude Code 2.0.76 executePreCompactHooks uses executeHooksOutsideREPL
1718
- // which does NOT support exit code 2 blocking. Compaction always proceeds.
1719
- // Our "infinite context" comes from archive + restore, not blocking.
1720
- if (AUTOPILOT_ENABLED) {
1721
- const state = loadAutopilotState();
1722
- const pct = state.lastPercentage || 0;
1723
- const bar = buildProgressBar(pct);
1724
-
1725
- process.stderr.write(
1726
- `[ContextAutopilot] ${bar} ${(pct * 100).toFixed(1)}% | ${trigger} compact — ${chunks.length} turns archived. Context will be restored after compaction.\n`
1727
- );
1728
-
1729
- // Reset autopilot state for post-compaction fresh start
1730
- state.lastTokenEstimate = 0;
1731
- state.lastPercentage = 0;
1732
- state.warningIssued = false;
1733
- saveAutopilotState(state);
1734
- }
1735
- }
1736
-
1737
- async function doSessionStart() {
1738
- const input = await readStdin(200);
1739
-
1740
- // Restore context after compaction OR after /clear (session rotation)
1741
- // With DISABLE_COMPACT, /clear is the primary way to free context
1742
- if (!input || (input.source !== 'compact' && input.source !== 'clear')) return;
1743
-
1744
- const sessionId = input.session_id;
1745
- if (!sessionId) return;
1746
-
1747
- const { backend, type } = await resolveBackend();
1748
-
1749
- // Use smart retrieval (importance-ranked) when auto-optimize is on
1750
- let additionalContext;
1751
- if (AUTO_OPTIMIZE) {
1752
- const { text, accessedIds } = await retrieveContextSmart(backend, sessionId, RESTORE_BUDGET);
1753
- additionalContext = text;
1754
-
1755
- // Track which entries were actually restored (access pattern learning)
1756
- if (accessedIds.length > 0 && backend.markAccessed) {
1757
- try { backend.markAccessed(accessedIds); } catch { /* non-critical */ }
1758
- }
1759
-
1760
- if (accessedIds.length > 0) {
1761
- process.stderr.write(
1762
- `[ContextPersistence] Smart restore: ${accessedIds.length} turns (importance-ranked) via ${type}\n`
1763
- );
1764
- }
1765
- } else {
1766
- additionalContext = await retrieveContext(backend, sessionId, RESTORE_BUDGET);
1767
- }
1768
-
1769
- await backend.shutdown();
1770
-
1771
- if (!additionalContext) return;
1772
-
1773
- const output = {
1774
- hookSpecificOutput: {
1775
- hookEventName: 'SessionStart',
1776
- additionalContext,
1777
- },
1778
- };
1779
- process.stdout.write(JSON.stringify(output));
1780
- }
1781
-
1782
- // ============================================================================
1783
- // Proactive archiving on every user prompt (prevents context cliff)
1784
- // ============================================================================
1785
-
1786
- async function doUserPromptSubmit() {
1787
- const input = await readStdin(200);
1788
- if (!input) return;
1789
-
1790
- const { session_id: sessionId, transcript_path: transcriptPath } = input;
1791
- if (!transcriptPath || !sessionId) return;
1792
-
1793
- const messages = parseTranscript(transcriptPath);
1794
- if (messages.length === 0) return;
1795
-
1796
- const chunks = chunkTranscript(messages);
1797
- if (chunks.length === 0) return;
1798
-
1799
- const { backend, type } = await resolveBackend();
1800
-
1801
- // Only archive new turns (dedup handles the rest, but we can skip early
1802
- // by only processing the last N chunks since the previous archive)
1803
- const existingCount = backend.queryBySession
1804
- ? (await backend.queryBySession(NAMESPACE, sessionId)).length
1805
- : 0;
1806
-
1807
- // Skip if we've already archived most turns (within 2 turns tolerance)
1808
- const skipArchive = existingCount > 0 && chunks.length - existingCount <= 2;
1809
-
1810
- let archiveMsg = '';
1811
- if (!skipArchive) {
1812
- const result = await storeChunks(backend, chunks, sessionId, 'proactive');
1813
- if (result.stored > 0) {
1814
- const total = await backend.count(NAMESPACE);
1815
- archiveMsg = `[ContextPersistence] Proactively archived ${result.stored} turns (total: ${total}).`;
1816
- process.stderr.write(
1817
- `[ContextPersistence] Proactive archive: ${result.stored} new, ${result.deduped} deduped via ${type}. Total: ${total}\n`
1818
- );
1819
- }
1820
- }
1821
-
1822
- // Context Autopilot: estimate usage and report percentage
1823
- let autopilotMsg = '';
1824
- if (AUTOPILOT_ENABLED && transcriptPath) {
1825
- try {
1826
- const autopilot = await runAutopilot(transcriptPath, sessionId, backend, type);
1827
- autopilotMsg = autopilot.additionalContext;
1828
-
1829
- process.stderr.write(
1830
- `[ContextAutopilot] ${(autopilot.percentage * 100).toFixed(1)}% context used (~${formatTokens(autopilot.tokens)} tokens, ${autopilot.turns} turns, ${autopilot.method})\n`
1831
- );
1832
- } catch (err) {
1833
- process.stderr.write(`[ContextAutopilot] Error: ${err.message}\n`);
1834
- }
1835
- }
1836
-
1837
- await backend.shutdown();
1838
-
1839
- // Combine archive message and autopilot report
1840
- const additionalContext = [archiveMsg, autopilotMsg].filter(Boolean).join(' ');
1841
-
1842
- if (additionalContext) {
1843
- const output = {
1844
- hookSpecificOutput: {
1845
- hookEventName: 'UserPromptSubmit',
1846
- additionalContext,
1847
- },
1848
- };
1849
- process.stdout.write(JSON.stringify(output));
1850
- }
1851
- }
1852
-
1853
- async function doStatus() {
1854
- const { backend, type } = await resolveBackend();
1855
-
1856
- const total = await backend.count();
1857
- const archiveCount = await backend.count(NAMESPACE);
1858
- const namespaces = await backend.listNamespaces();
1859
- const sessions = await backend.listSessions(NAMESPACE);
1860
-
1861
- console.log('\n=== Context Persistence Archive Status ===\n');
1862
- const backendLabel = {
1863
- sqlite: ARCHIVE_DB_PATH,
1864
- ruvector: `${process.env.RUVECTOR_HOST || 'N/A'}:${process.env.RUVECTOR_PORT || '5432'}`,
1865
- agentdb: 'in-memory HNSW',
1866
- json: ARCHIVE_JSON_PATH,
1867
- };
1868
- console.log(` Backend: ${type} (${backendLabel[type] || type})`);
1869
- console.log(` Total: ${total} entries`);
1870
- console.log(` Transcripts: ${archiveCount} entries`);
1871
- console.log(` Namespaces: ${namespaces.join(', ') || 'none'}`);
1872
- console.log(` Budget: ${RESTORE_BUDGET} chars`);
1873
- console.log(` Sessions: ${sessions.length}`);
1874
- console.log(` Proactive: enabled (UserPromptSubmit hook)`);
1875
- console.log(` Auto-opt: ${AUTO_OPTIMIZE ? 'enabled' : 'disabled'} (importance ranking, pruning, sync)`);
1876
- console.log(` Retention: ${RETENTION_DAYS} days (prune never-accessed entries)`);
1877
- const rvConfig = getRuVectorConfig();
1878
- console.log(` RuVector: ${rvConfig ? `${rvConfig.host}:${rvConfig.port}/${rvConfig.database} (auto-sync enabled)` : 'not configured'}`);
1879
-
1880
- // Self-learning stats
1881
- if (type === 'sqlite' && backend.db) {
1882
- try {
1883
- const embCount = backend.db.prepare('SELECT COUNT(*) as cnt FROM transcript_entries WHERE embedding IS NOT NULL').get().cnt;
1884
- const avgConf = backend.db.prepare('SELECT AVG(confidence) as avg FROM transcript_entries WHERE namespace = ?').get(NAMESPACE)?.avg || 0;
1885
- const lowConf = backend.db.prepare('SELECT COUNT(*) as cnt FROM transcript_entries WHERE namespace = ? AND confidence < 0.3').get(NAMESPACE).cnt;
1886
- console.log('');
1887
- console.log(' --- Self-Learning ---');
1888
- console.log(` Embeddings: ${embCount}/${archiveCount} entries have vector embeddings`);
1889
- console.log(` Avg conf: ${(avgConf * 100).toFixed(1)}% (decay: -0.5%/hr, boost: +3%/access)`);
1890
- console.log(` Low conf: ${lowConf} entries below 30% (pruned at 15%)`);
1891
- console.log(` Semantic: ${embCount > 0 ? 'enabled (cross-session search)' : 'pending (embeddings generating)'}`);
1892
- } catch { /* stats are non-critical */ }
1893
- }
1894
-
1895
- // Autopilot status
1896
- console.log('');
1897
- console.log(' --- Context Autopilot ---');
1898
- console.log(` Enabled: ${AUTOPILOT_ENABLED}`);
1899
- console.log(` Window: ${formatTokens(CONTEXT_WINDOW_TOKENS)} tokens`);
1900
- console.log(` Warn at: ${(AUTOPILOT_WARN_PCT * 100).toFixed(0)}%`);
1901
- console.log(` Prune at: ${(AUTOPILOT_PRUNE_PCT * 100).toFixed(0)}%`);
1902
- console.log(` Compaction: LOSSLESS (archive before, restore after)`);
1903
-
1904
- const apState = loadAutopilotState();
1905
- if (apState.sessionId) {
1906
- const pct = apState.lastPercentage || 0;
1907
- const bar = buildProgressBar(pct);
1908
- console.log(` Current: ${bar} ${(pct * 100).toFixed(1)}% (~${formatTokens(apState.lastTokenEstimate)} tokens)`);
1909
- console.log(` Prune cycles: ${apState.pruneCount}`);
1910
- if (apState.history.length >= 2) {
1911
- const first = apState.history[0];
1912
- const last = apState.history[apState.history.length - 1];
1913
- const growthRate = (last.pct - first.pct) / apState.history.length;
1914
- if (growthRate > 0) {
1915
- const turnsLeft = Math.ceil((1.0 - pct) / growthRate);
1916
- console.log(` Est. runway: ~${turnsLeft} turns until prune threshold`);
1917
- }
1918
- }
1919
- }
1920
-
1921
- if (sessions.length > 0) {
1922
- console.log('\n Recent sessions:');
1923
- for (const s of sessions.slice(0, 10)) {
1924
- console.log(` - ${s.session_id}: ${s.cnt} turns`);
1925
- }
1926
- }
1927
-
1928
- console.log('');
1929
- await backend.shutdown();
1930
- }
1931
-
1932
- // ============================================================================
1933
- // Exports for testing
1934
- // ============================================================================
1935
-
1936
- export {
1937
- SQLiteBackend,
1938
- RuVectorBackend,
1939
- JsonFileBackend,
1940
- resolveBackend,
1941
- getRuVectorConfig,
1942
- createEmbedding,
1943
- createHashEmbedding,
1944
- getOnnxPipeline,
1945
- EMBEDDING_DIM,
1946
- hashContent,
1947
- parseTranscript,
1948
- extractTextContent,
1949
- extractToolCalls,
1950
- extractFilePaths,
1951
- chunkTranscript,
1952
- extractSummary,
1953
- buildEntry,
1954
- buildCompactInstructions,
1955
- computeImportance,
1956
- retrieveContextSmart,
1957
- autoOptimize,
1958
- crossSessionSearch,
1959
- storeChunks,
1960
- retrieveContext,
1961
- readStdin,
1962
- // Autopilot
1963
- estimateContextTokens,
1964
- loadAutopilotState,
1965
- saveAutopilotState,
1966
- runAutopilot,
1967
- buildProgressBar,
1968
- formatTokens,
1969
- buildAutopilotReport,
1970
- NAMESPACE,
1971
- ARCHIVE_DB_PATH,
1972
- ARCHIVE_JSON_PATH,
1973
- COMPACT_INSTRUCTION_BUDGET,
1974
- RETENTION_DAYS,
1975
- AUTO_OPTIMIZE,
1976
- AUTOPILOT_ENABLED,
1977
- CONTEXT_WINDOW_TOKENS,
1978
- AUTOPILOT_WARN_PCT,
1979
- AUTOPILOT_PRUNE_PCT,
1980
- };
1981
-
1982
- // ============================================================================
1983
- // Main
1984
- // ============================================================================
1985
-
1986
- const command = process.argv[2] || 'status';
1987
-
1988
- try {
1989
- switch (command) {
1990
- case 'pre-compact': await doPreCompact(); break;
1991
- case 'session-start': await doSessionStart(); break;
1992
- case 'user-prompt-submit': await doUserPromptSubmit(); break;
1993
- case 'status': await doStatus(); break;
1994
- default:
1995
- console.log('Usage: context-persistence-hook.mjs <pre-compact|session-start|user-prompt-submit|status>');
1996
- process.exit(1);
1997
- }
1998
- } catch (err) {
1999
- // Hooks must never crash Claude Code - fail silently
2000
- process.stderr.write(`[ContextPersistence] Error (non-critical): ${err.message}\n`);
2001
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Context Persistence Hook (ADR-051)
4
+ *
5
+ * Intercepts Claude Code's PreCompact, SessionStart, and UserPromptSubmit
6
+ * lifecycle events to persist conversation history in SQLite (primary),
7
+ * RuVector PostgreSQL (optional), or JSON (fallback), enabling "infinite
8
+ * context" across compaction boundaries.
9
+ *
10
+ * Backend priority:
11
+ * 1. better-sqlite3 (native, WAL mode, indexed queries, ACID transactions)
12
+ * 2. RuVector PostgreSQL (if RUVECTOR_* env vars set - TB-scale, GNN search)
13
+ * 3. AgentDB from @claude-flow/memory (HNSW vector search)
14
+ * 4. JsonFileBackend (zero dependencies, always works)
15
+ *
16
+ * Proactive archiving:
17
+ * - UserPromptSubmit hook archives on every prompt, BEFORE context fills up
18
+ * - PreCompact hook is a safety net that catches any remaining unarchived turns
19
+ * - SessionStart hook restores context after compaction
20
+ * - Together, compaction becomes invisible — no information is ever lost
21
+ *
22
+ * Usage:
23
+ * node context-persistence-hook.mjs pre-compact # PreCompact: archive transcript
24
+ * node context-persistence-hook.mjs session-start # SessionStart: restore context
25
+ * node context-persistence-hook.mjs user-prompt-submit # UserPromptSubmit: proactive archive
26
+ * node context-persistence-hook.mjs status # Show archive stats
27
+ */
28
+
29
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
30
+ import { createHash } from 'crypto';
31
+ import { join, dirname } from 'path';
32
+ import { fileURLToPath } from 'url';
33
+ import { createRequire } from 'module';
34
+
35
+ const __filename = fileURLToPath(import.meta.url);
36
+ const __dirname = dirname(__filename);
37
+ const PROJECT_ROOT = join(__dirname, '../..');
38
+ const DATA_DIR = join(PROJECT_ROOT, '.claude-flow', 'data');
39
+ const ARCHIVE_JSON_PATH = join(DATA_DIR, 'transcript-archive.json');
40
+ const ARCHIVE_DB_PATH = join(DATA_DIR, 'transcript-archive.db');
41
+
42
+ const NAMESPACE = 'transcript-archive';
43
+ const RESTORE_BUDGET = parseInt(process.env.CLAUDE_FLOW_COMPACT_RESTORE_BUDGET || '4000', 10);
44
+ const MAX_MESSAGES = 500;
45
+ const BLOCK_COMPACTION = process.env.CLAUDE_FLOW_BLOCK_COMPACTION === 'true';
46
+ const COMPACT_INSTRUCTION_BUDGET = parseInt(process.env.CLAUDE_FLOW_COMPACT_INSTRUCTION_BUDGET || '2000', 10);
47
+ const RETENTION_DAYS = parseInt(process.env.CLAUDE_FLOW_RETENTION_DAYS || '30', 10);
48
+ const AUTO_OPTIMIZE = process.env.CLAUDE_FLOW_AUTO_OPTIMIZE !== 'false'; // on by default
49
+
50
+ // ============================================================================
51
+ // Context Autopilot — prevent compaction by managing context size in real-time
52
+ // ============================================================================
53
+ const AUTOPILOT_ENABLED = process.env.CLAUDE_FLOW_CONTEXT_AUTOPILOT !== 'false'; // on by default
54
+ const CONTEXT_WINDOW_TOKENS = parseInt(process.env.CLAUDE_FLOW_CONTEXT_WINDOW || '200000', 10);
55
+ const AUTOPILOT_WARN_PCT = parseFloat(process.env.CLAUDE_FLOW_AUTOPILOT_WARN || '0.70');
56
+ const AUTOPILOT_PRUNE_PCT = parseFloat(process.env.CLAUDE_FLOW_AUTOPILOT_PRUNE || '0.85');
57
+ const AUTOPILOT_STATE_PATH = join(DATA_DIR, 'autopilot-state.json');
58
+
59
+ // Approximate tokens per character (Claude averages ~3.5 chars per token)
60
+ const CHARS_PER_TOKEN = 3.5;
61
+
62
+ const DEBUG = !!(process.env.RUFLO_DEBUG || process.env.DEBUG);
63
+
64
+ // ── Graceful shutdown (FIX 3) ───────────────────────────────────────────────
65
+ // The active backend is created mid-handler and closed at the end. SQLite holds
66
+ // a native handle and a WAL; if a SIGTERM/SIGINT arrives between creation and
67
+ // `backend.shutdown()`, that close is skipped — risking an unflushed WAL or a
68
+ // stale lock file. Track the active backend and flush it on signal before exit.
69
+ let activeBackend = null;
70
+ let shuttingDown = false;
71
+ function trackBackend(b) { activeBackend = b; return b; }
72
+ async function gracefulExit(signal) {
73
+ if (shuttingDown) return;
74
+ shuttingDown = true;
75
+ if (DEBUG) process.stderr.write(`[ContextPersistence] received ${signal}, flushing backend before exit\n`);
76
+ try {
77
+ if (activeBackend && typeof activeBackend.shutdown === 'function') await activeBackend.shutdown();
78
+ } catch { /* best effort — never block exit on cleanup */ }
79
+ process.exit(0);
80
+ }
81
+ process.on('SIGTERM', () => { gracefulExit('SIGTERM'); });
82
+ process.on('SIGINT', () => { gracefulExit('SIGINT'); });
83
+
84
+ // Ensure data dir
85
+ if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
86
+
87
+ // ============================================================================
88
+ // SQLite Backend (better-sqlite3 — synchronous, fast, WAL mode)
89
+ // ============================================================================
90
+
91
+ class SQLiteBackend {
92
+ constructor(dbPath) {
93
+ this.dbPath = dbPath;
94
+ this.db = null;
95
+ }
96
+
97
+ async initialize() {
98
+ const require = createRequire(import.meta.url);
99
+ const Database = require('better-sqlite3');
100
+ this.db = new Database(this.dbPath);
101
+
102
+ // Performance optimizations
103
+ this.db.pragma('journal_mode = WAL');
104
+ this.db.pragma('synchronous = NORMAL');
105
+ this.db.pragma('cache_size = 5000');
106
+ this.db.pragma('temp_store = MEMORY');
107
+
108
+ // Create schema
109
+ this.db.exec(`
110
+ CREATE TABLE IF NOT EXISTS transcript_entries (
111
+ id TEXT PRIMARY KEY,
112
+ key TEXT NOT NULL,
113
+ content TEXT NOT NULL,
114
+ type TEXT NOT NULL DEFAULT 'episodic',
115
+ namespace TEXT NOT NULL DEFAULT 'transcript-archive',
116
+ tags TEXT NOT NULL DEFAULT '[]',
117
+ metadata TEXT NOT NULL DEFAULT '{}',
118
+ access_level TEXT NOT NULL DEFAULT 'private',
119
+ created_at INTEGER NOT NULL,
120
+ updated_at INTEGER NOT NULL,
121
+ version INTEGER NOT NULL DEFAULT 1,
122
+ access_count INTEGER NOT NULL DEFAULT 0,
123
+ last_accessed_at INTEGER NOT NULL,
124
+ content_hash TEXT,
125
+ session_id TEXT,
126
+ chunk_index INTEGER,
127
+ summary TEXT
128
+ );
129
+
130
+ CREATE INDEX IF NOT EXISTS idx_te_namespace ON transcript_entries(namespace);
131
+ CREATE INDEX IF NOT EXISTS idx_te_session ON transcript_entries(session_id);
132
+ CREATE INDEX IF NOT EXISTS idx_te_hash ON transcript_entries(content_hash);
133
+ CREATE INDEX IF NOT EXISTS idx_te_chunk ON transcript_entries(session_id, chunk_index);
134
+ CREATE INDEX IF NOT EXISTS idx_te_created ON transcript_entries(created_at);
135
+ `);
136
+
137
+ // Schema migration: add confidence + embedding columns (self-learning support)
138
+ try {
139
+ this.db.exec(`ALTER TABLE transcript_entries ADD COLUMN confidence REAL NOT NULL DEFAULT 0.8`);
140
+ } catch { /* column already exists */ }
141
+ try {
142
+ this.db.exec(`ALTER TABLE transcript_entries ADD COLUMN embedding BLOB`);
143
+ } catch { /* column already exists */ }
144
+ try {
145
+ this.db.exec(`CREATE INDEX IF NOT EXISTS idx_te_confidence ON transcript_entries(confidence)`);
146
+ } catch { /* index already exists */ }
147
+
148
+ // Prepare statements for reuse
149
+ this._stmts = {
150
+ insert: this.db.prepare(`
151
+ INSERT OR IGNORE INTO transcript_entries
152
+ (id, key, content, type, namespace, tags, metadata, access_level,
153
+ created_at, updated_at, version, access_count, last_accessed_at,
154
+ content_hash, session_id, chunk_index, summary)
155
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
156
+ `),
157
+ queryByNamespace: this.db.prepare(
158
+ 'SELECT * FROM transcript_entries WHERE namespace = ? ORDER BY created_at DESC'
159
+ ),
160
+ queryBySession: this.db.prepare(
161
+ 'SELECT * FROM transcript_entries WHERE namespace = ? AND session_id = ? ORDER BY chunk_index DESC'
162
+ ),
163
+ countAll: this.db.prepare('SELECT COUNT(*) as cnt FROM transcript_entries'),
164
+ countByNamespace: this.db.prepare(
165
+ 'SELECT COUNT(*) as cnt FROM transcript_entries WHERE namespace = ?'
166
+ ),
167
+ hashExists: this.db.prepare(
168
+ 'SELECT 1 FROM transcript_entries WHERE content_hash = ? LIMIT 1'
169
+ ),
170
+ listNamespaces: this.db.prepare(
171
+ 'SELECT DISTINCT namespace FROM transcript_entries'
172
+ ),
173
+ listSessions: this.db.prepare(
174
+ 'SELECT session_id, COUNT(*) as cnt FROM transcript_entries WHERE namespace = ? GROUP BY session_id ORDER BY MAX(created_at) DESC'
175
+ ),
176
+ };
177
+
178
+ this._bulkInsert = this.db.transaction((entries) => {
179
+ for (const e of entries) {
180
+ this._stmts.insert.run(
181
+ e.id, e.key, e.content, e.type, e.namespace,
182
+ JSON.stringify(e.tags), JSON.stringify(e.metadata), e.accessLevel,
183
+ e.createdAt, e.updatedAt, e.version, e.accessCount, e.lastAccessedAt,
184
+ e.metadata?.contentHash || null,
185
+ e.metadata?.sessionId || null,
186
+ e.metadata?.chunkIndex ?? null,
187
+ e.metadata?.summary || null
188
+ );
189
+ }
190
+ });
191
+
192
+ // Optimization statements
193
+ this._stmts.markAccessed = this.db.prepare(
194
+ 'UPDATE transcript_entries SET access_count = access_count + 1, last_accessed_at = ? WHERE id = ?'
195
+ );
196
+ this._stmts.pruneStale = this.db.prepare(
197
+ 'DELETE FROM transcript_entries WHERE namespace = ? AND access_count = 0 AND created_at < ?'
198
+ );
199
+ this._stmts.queryByImportance = this.db.prepare(`
200
+ SELECT *, (
201
+ (CAST(access_count AS REAL) + 1) *
202
+ (1.0 / (1.0 + (? - created_at) / 86400000.0)) *
203
+ (CASE WHEN json_array_length(json_extract(metadata, '$.toolNames')) > 0 THEN 1.5 ELSE 1.0 END) *
204
+ (CASE WHEN json_array_length(json_extract(metadata, '$.filePaths')) > 0 THEN 1.3 ELSE 1.0 END)
205
+ ) AS importance_score
206
+ FROM transcript_entries
207
+ WHERE namespace = ? AND session_id = ?
208
+ ORDER BY importance_score DESC
209
+ `);
210
+ this._stmts.allForSync = this.db.prepare(
211
+ 'SELECT * FROM transcript_entries WHERE namespace = ? ORDER BY created_at ASC'
212
+ );
213
+ }
214
+
215
+ async store(entry) {
216
+ this._stmts.insert.run(
217
+ entry.id, entry.key, entry.content, entry.type, entry.namespace,
218
+ JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.accessLevel,
219
+ entry.createdAt, entry.updatedAt, entry.version, entry.accessCount, entry.lastAccessedAt,
220
+ entry.metadata?.contentHash || null,
221
+ entry.metadata?.sessionId || null,
222
+ entry.metadata?.chunkIndex ?? null,
223
+ entry.metadata?.summary || null
224
+ );
225
+ }
226
+
227
+ async bulkInsert(entries) {
228
+ this._bulkInsert(entries);
229
+ }
230
+
231
+ async query(opts) {
232
+ let rows;
233
+ if (opts?.namespace && opts?.sessionId) {
234
+ rows = this._stmts.queryBySession.all(opts.namespace, opts.sessionId);
235
+ } else if (opts?.namespace) {
236
+ rows = this._stmts.queryByNamespace.all(opts.namespace);
237
+ } else {
238
+ rows = this.db.prepare('SELECT * FROM transcript_entries ORDER BY created_at DESC').all();
239
+ }
240
+ return rows.map(r => this._rowToEntry(r));
241
+ }
242
+
243
+ async queryBySession(namespace, sessionId) {
244
+ const rows = this._stmts.queryBySession.all(namespace, sessionId);
245
+ return rows.map(r => this._rowToEntry(r));
246
+ }
247
+
248
+ hashExists(hash) {
249
+ return !!this._stmts.hashExists.get(hash);
250
+ }
251
+
252
+ async count(namespace) {
253
+ if (namespace) {
254
+ return this._stmts.countByNamespace.get(namespace).cnt;
255
+ }
256
+ return this._stmts.countAll.get().cnt;
257
+ }
258
+
259
+ async listNamespaces() {
260
+ return this._stmts.listNamespaces.all().map(r => r.namespace);
261
+ }
262
+
263
+ async listSessions(namespace) {
264
+ return this._stmts.listSessions.all(namespace || NAMESPACE);
265
+ }
266
+
267
+ markAccessed(ids) {
268
+ const now = Date.now();
269
+ const boostStmt = this.db.prepare(
270
+ 'UPDATE transcript_entries SET access_count = access_count + 1, last_accessed_at = ?, confidence = MIN(1.0, confidence + 0.03) WHERE id = ?'
271
+ );
272
+ for (const id of ids) {
273
+ boostStmt.run(now, id);
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Confidence decay: reduce confidence for entries not accessed recently.
279
+ * Decay rate: 0.5% per hour (matches LearningBridge default).
280
+ * Entries with confidence below 0.1 are floor-clamped.
281
+ */
282
+ decayConfidence(namespace, hoursElapsed = 1) {
283
+ const decayRate = 0.005 * hoursElapsed;
284
+ const result = this.db.prepare(
285
+ 'UPDATE transcript_entries SET confidence = MAX(0.1, confidence - ?) WHERE namespace = ? AND confidence > 0.1'
286
+ ).run(decayRate, namespace || NAMESPACE);
287
+ return result.changes;
288
+ }
289
+
290
+ /**
291
+ * Store embedding blob for an entry (Float32Array → Buffer; 384-dim ONNX today, 768-dim legacy hash blobs still readable).
292
+ */
293
+ storeEmbedding(id, embedding) {
294
+ const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
295
+ this.db.prepare('UPDATE transcript_entries SET embedding = ? WHERE id = ?').run(buf, id);
296
+ }
297
+
298
+ /**
299
+ * Cosine similarity search across all entries with embeddings.
300
+ * Handles both 384-dim (ONNX) and 768-dim (legacy hash) embeddings.
301
+ * Returns top-k entries ranked by similarity to the query embedding.
302
+ */
303
+ semanticSearch(queryEmbedding, k = 10, namespace) {
304
+ const rows = this.db.prepare(
305
+ 'SELECT id, embedding, summary, session_id, chunk_index, confidence, access_count FROM transcript_entries WHERE namespace = ? AND embedding IS NOT NULL'
306
+ ).all(namespace || NAMESPACE);
307
+
308
+ const queryDim = queryEmbedding.length;
309
+ const scored = [];
310
+ for (const row of rows) {
311
+ if (!row.embedding) continue;
312
+ const stored = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
313
+ // Only compare if dimensions match
314
+ if (stored.length !== queryDim) continue;
315
+ let dot = 0;
316
+ for (let i = 0; i < queryDim; i++) {
317
+ dot += queryEmbedding[i] * stored[i];
318
+ }
319
+ // Boost by confidence (self-learning signal)
320
+ const score = dot * (row.confidence || 0.8);
321
+ scored.push({ id: row.id, score, summary: row.summary, sessionId: row.session_id, chunkIndex: row.chunk_index, confidence: row.confidence, accessCount: row.access_count });
322
+ }
323
+
324
+ scored.sort((a, b) => b.score - a.score);
325
+ return scored.slice(0, k);
326
+ }
327
+
328
+ /**
329
+ * Smart pruning: prune by confidence instead of just age.
330
+ * Removes entries with confidence <= threshold AND access_count = 0.
331
+ */
332
+ pruneByConfidence(namespace, threshold = 0.2) {
333
+ const result = this.db.prepare(
334
+ 'DELETE FROM transcript_entries WHERE namespace = ? AND confidence <= ? AND access_count = 0'
335
+ ).run(namespace || NAMESPACE, threshold);
336
+ return result.changes;
337
+ }
338
+
339
+ pruneStale(namespace, maxAgeDays) {
340
+ const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
341
+ const result = this._stmts.pruneStale.run(namespace || NAMESPACE, cutoff);
342
+ return result.changes;
343
+ }
344
+
345
+ queryByImportance(namespace, sessionId) {
346
+ const now = Date.now();
347
+ const rows = this._stmts.queryByImportance.all(now, namespace, sessionId);
348
+ return rows.map(r => ({ ...this._rowToEntry(r), importanceScore: r.importance_score }));
349
+ }
350
+
351
+ allForSync(namespace) {
352
+ const rows = this._stmts.allForSync.all(namespace || NAMESPACE);
353
+ return rows.map(r => this._rowToEntry(r));
354
+ }
355
+
356
+ async shutdown() {
357
+ if (this.db) {
358
+ this.db.pragma('optimize');
359
+ this.db.close();
360
+ this.db = null;
361
+ }
362
+ }
363
+
364
+ _rowToEntry(row) {
365
+ return {
366
+ id: row.id,
367
+ key: row.key,
368
+ content: row.content,
369
+ type: row.type,
370
+ namespace: row.namespace,
371
+ tags: JSON.parse(row.tags),
372
+ metadata: JSON.parse(row.metadata),
373
+ accessLevel: row.access_level,
374
+ createdAt: row.created_at,
375
+ updatedAt: row.updated_at,
376
+ version: row.version,
377
+ accessCount: row.access_count,
378
+ lastAccessedAt: row.last_accessed_at,
379
+ references: [],
380
+ };
381
+ }
382
+ }
383
+
384
+ // ============================================================================
385
+ // JSON File Backend (fallback when better-sqlite3 unavailable)
386
+ // ============================================================================
387
+
388
+ class JsonFileBackend {
389
+ constructor(filePath) {
390
+ this.filePath = filePath;
391
+ this.entries = new Map();
392
+ }
393
+
394
+ async initialize() {
395
+ if (existsSync(this.filePath)) {
396
+ try {
397
+ const data = JSON.parse(readFileSync(this.filePath, 'utf-8'));
398
+ if (Array.isArray(data)) {
399
+ for (const entry of data) this.entries.set(entry.id, entry);
400
+ }
401
+ } catch { /* start fresh */ }
402
+ }
403
+ }
404
+
405
+ async store(entry) { this.entries.set(entry.id, entry); this._persist(); }
406
+
407
+ async bulkInsert(entries) {
408
+ for (const e of entries) this.entries.set(e.id, e);
409
+ this._persist();
410
+ }
411
+
412
+ async query(opts) {
413
+ let results = [...this.entries.values()];
414
+ if (opts?.namespace) results = results.filter(e => e.namespace === opts.namespace);
415
+ if (opts?.type) results = results.filter(e => e.type === opts.type);
416
+ if (opts?.limit) results = results.slice(0, opts.limit);
417
+ return results;
418
+ }
419
+
420
+ async queryBySession(namespace, sessionId) {
421
+ return [...this.entries.values()]
422
+ .filter(e => e.namespace === namespace && e.metadata?.sessionId === sessionId)
423
+ .sort((a, b) => (b.metadata?.chunkIndex ?? 0) - (a.metadata?.chunkIndex ?? 0));
424
+ }
425
+
426
+ hashExists(hash) {
427
+ for (const e of this.entries.values()) {
428
+ if (e.metadata?.contentHash === hash) return true;
429
+ }
430
+ return false;
431
+ }
432
+
433
+ async count(namespace) {
434
+ if (!namespace) return this.entries.size;
435
+ let n = 0;
436
+ for (const e of this.entries.values()) {
437
+ if (e.namespace === namespace) n++;
438
+ }
439
+ return n;
440
+ }
441
+
442
+ async listNamespaces() {
443
+ const ns = new Set();
444
+ for (const e of this.entries.values()) ns.add(e.namespace || 'default');
445
+ return [...ns];
446
+ }
447
+
448
+ async listSessions(namespace) {
449
+ const sessions = new Map();
450
+ for (const e of this.entries.values()) {
451
+ if (e.namespace === (namespace || NAMESPACE) && e.metadata?.sessionId) {
452
+ sessions.set(e.metadata.sessionId, (sessions.get(e.metadata.sessionId) || 0) + 1);
453
+ }
454
+ }
455
+ return [...sessions.entries()].map(([session_id, cnt]) => ({ session_id, cnt }));
456
+ }
457
+
458
+ async shutdown() { this._persist(); }
459
+
460
+ _persist() {
461
+ try {
462
+ writeFileSync(this.filePath, JSON.stringify([...this.entries.values()], null, 2), 'utf-8');
463
+ } catch { /* best effort */ }
464
+ }
465
+ }
466
+
467
+ // ============================================================================
468
+ // RuVector PostgreSQL Backend (optional, TB-scale, GNN-enhanced)
469
+ // ============================================================================
470
+
471
+ class RuVectorBackend {
472
+ constructor(config) {
473
+ this.config = config;
474
+ this.pool = null;
475
+ }
476
+
477
+ async initialize() {
478
+ const pg = await import('pg');
479
+ const Pool = pg.default?.Pool || pg.Pool;
480
+ this.pool = new Pool({
481
+ host: this.config.host,
482
+ port: this.config.port || 5432,
483
+ database: this.config.database,
484
+ user: this.config.user,
485
+ password: this.config.password,
486
+ ssl: this.config.ssl || false,
487
+ max: 3,
488
+ idleTimeoutMillis: 10000,
489
+ connectionTimeoutMillis: 3000,
490
+ application_name: 'claude-flow-context-persistence',
491
+ });
492
+
493
+ // Test connection and create schema
494
+ const client = await this.pool.connect();
495
+ try {
496
+ await client.query(`
497
+ CREATE TABLE IF NOT EXISTS transcript_entries (
498
+ id TEXT PRIMARY KEY,
499
+ key TEXT NOT NULL,
500
+ content TEXT NOT NULL,
501
+ type TEXT NOT NULL DEFAULT 'episodic',
502
+ namespace TEXT NOT NULL DEFAULT 'transcript-archive',
503
+ tags JSONB NOT NULL DEFAULT '[]',
504
+ metadata JSONB NOT NULL DEFAULT '{}',
505
+ access_level TEXT NOT NULL DEFAULT 'private',
506
+ created_at BIGINT NOT NULL,
507
+ updated_at BIGINT NOT NULL,
508
+ version INTEGER NOT NULL DEFAULT 1,
509
+ access_count INTEGER NOT NULL DEFAULT 0,
510
+ last_accessed_at BIGINT NOT NULL,
511
+ content_hash TEXT,
512
+ session_id TEXT,
513
+ chunk_index INTEGER,
514
+ summary TEXT,
515
+ embedding vector(768)
516
+ );
517
+
518
+ CREATE INDEX IF NOT EXISTS idx_te_namespace ON transcript_entries(namespace);
519
+ CREATE INDEX IF NOT EXISTS idx_te_session ON transcript_entries(session_id);
520
+ CREATE INDEX IF NOT EXISTS idx_te_hash ON transcript_entries(content_hash);
521
+ CREATE INDEX IF NOT EXISTS idx_te_chunk ON transcript_entries(session_id, chunk_index);
522
+ CREATE INDEX IF NOT EXISTS idx_te_created ON transcript_entries(created_at);
523
+ `);
524
+ } finally {
525
+ client.release();
526
+ }
527
+ }
528
+
529
+ async store(entry) {
530
+ const embeddingArr = entry._embedding
531
+ ? `[${Array.from(entry._embedding).join(',')}]`
532
+ : null;
533
+ await this.pool.query(
534
+ `INSERT INTO transcript_entries
535
+ (id, key, content, type, namespace, tags, metadata, access_level,
536
+ created_at, updated_at, version, access_count, last_accessed_at,
537
+ content_hash, session_id, chunk_index, summary, embedding)
538
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
539
+ ON CONFLICT (id) DO NOTHING`,
540
+ [
541
+ entry.id, entry.key, entry.content, entry.type, entry.namespace,
542
+ JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.accessLevel,
543
+ entry.createdAt, entry.updatedAt, entry.version, entry.accessCount, entry.lastAccessedAt,
544
+ entry.metadata?.contentHash || null,
545
+ entry.metadata?.sessionId || null,
546
+ entry.metadata?.chunkIndex ?? null,
547
+ entry.metadata?.summary || null,
548
+ embeddingArr,
549
+ ]
550
+ );
551
+ }
552
+
553
+ async bulkInsert(entries) {
554
+ const client = await this.pool.connect();
555
+ try {
556
+ await client.query('BEGIN');
557
+ for (const entry of entries) {
558
+ const embeddingArr = entry._embedding
559
+ ? `[${Array.from(entry._embedding).join(',')}]`
560
+ : null;
561
+ await client.query(
562
+ `INSERT INTO transcript_entries
563
+ (id, key, content, type, namespace, tags, metadata, access_level,
564
+ created_at, updated_at, version, access_count, last_accessed_at,
565
+ content_hash, session_id, chunk_index, summary, embedding)
566
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
567
+ ON CONFLICT (id) DO NOTHING`,
568
+ [
569
+ entry.id, entry.key, entry.content, entry.type, entry.namespace,
570
+ JSON.stringify(entry.tags), JSON.stringify(entry.metadata), entry.accessLevel,
571
+ entry.createdAt, entry.updatedAt, entry.version, entry.accessCount, entry.lastAccessedAt,
572
+ entry.metadata?.contentHash || null,
573
+ entry.metadata?.sessionId || null,
574
+ entry.metadata?.chunkIndex ?? null,
575
+ entry.metadata?.summary || null,
576
+ embeddingArr,
577
+ ]
578
+ );
579
+ }
580
+ await client.query('COMMIT');
581
+ } catch (err) {
582
+ await client.query('ROLLBACK');
583
+ throw err;
584
+ } finally {
585
+ client.release();
586
+ }
587
+ }
588
+
589
+ async query(opts) {
590
+ let sql = 'SELECT * FROM transcript_entries';
591
+ const params = [];
592
+ const clauses = [];
593
+ if (opts?.namespace) { params.push(opts.namespace); clauses.push(`namespace = $${params.length}`); }
594
+ if (clauses.length) sql += ' WHERE ' + clauses.join(' AND ');
595
+ sql += ' ORDER BY created_at DESC';
596
+ if (opts?.limit) { params.push(opts.limit); sql += ` LIMIT $${params.length}`; }
597
+ const { rows } = await this.pool.query(sql, params);
598
+ return rows.map(r => this._rowToEntry(r));
599
+ }
600
+
601
+ async queryBySession(namespace, sessionId) {
602
+ const { rows } = await this.pool.query(
603
+ 'SELECT * FROM transcript_entries WHERE namespace = $1 AND session_id = $2 ORDER BY chunk_index DESC',
604
+ [namespace, sessionId]
605
+ );
606
+ return rows.map(r => this._rowToEntry(r));
607
+ }
608
+
609
+ hashExists(hash) {
610
+ // Synchronous check not possible with pg — use a cached check
611
+ // The bulkInsert uses ON CONFLICT DO NOTHING for dedup at DB level
612
+ return false;
613
+ }
614
+
615
+ async hashExistsAsync(hash) {
616
+ const { rows } = await this.pool.query(
617
+ 'SELECT 1 FROM transcript_entries WHERE content_hash = $1 LIMIT 1',
618
+ [hash]
619
+ );
620
+ return rows.length > 0;
621
+ }
622
+
623
+ async count(namespace) {
624
+ const sql = namespace
625
+ ? 'SELECT COUNT(*) as cnt FROM transcript_entries WHERE namespace = $1'
626
+ : 'SELECT COUNT(*) as cnt FROM transcript_entries';
627
+ const params = namespace ? [namespace] : [];
628
+ const { rows } = await this.pool.query(sql, params);
629
+ return parseInt(rows[0].cnt, 10);
630
+ }
631
+
632
+ async listNamespaces() {
633
+ const { rows } = await this.pool.query('SELECT DISTINCT namespace FROM transcript_entries');
634
+ return rows.map(r => r.namespace);
635
+ }
636
+
637
+ async listSessions(namespace) {
638
+ const { rows } = await this.pool.query(
639
+ `SELECT session_id, COUNT(*) as cnt FROM transcript_entries
640
+ WHERE namespace = $1 GROUP BY session_id ORDER BY MAX(created_at) DESC`,
641
+ [namespace || NAMESPACE]
642
+ );
643
+ return rows.map(r => ({ session_id: r.session_id, cnt: parseInt(r.cnt, 10) }));
644
+ }
645
+
646
+ async markAccessed(ids) {
647
+ const now = Date.now();
648
+ for (const id of ids) {
649
+ await this.pool.query(
650
+ 'UPDATE transcript_entries SET access_count = access_count + 1, last_accessed_at = $1 WHERE id = $2',
651
+ [now, id]
652
+ );
653
+ }
654
+ }
655
+
656
+ async pruneStale(namespace, maxAgeDays) {
657
+ const cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000);
658
+ const { rowCount } = await this.pool.query(
659
+ 'DELETE FROM transcript_entries WHERE namespace = $1 AND access_count = 0 AND created_at < $2',
660
+ [namespace || NAMESPACE, cutoff]
661
+ );
662
+ return rowCount;
663
+ }
664
+
665
+ async queryByImportance(namespace, sessionId) {
666
+ const now = Date.now();
667
+ const { rows } = await this.pool.query(`
668
+ SELECT *, (
669
+ (CAST(access_count AS REAL) + 1) *
670
+ (1.0 / (1.0 + ($1 - created_at) / 86400000.0)) *
671
+ (CASE WHEN jsonb_array_length(metadata->'toolNames') > 0 THEN 1.5 ELSE 1.0 END) *
672
+ (CASE WHEN jsonb_array_length(metadata->'filePaths') > 0 THEN 1.3 ELSE 1.0 END)
673
+ ) AS importance_score
674
+ FROM transcript_entries
675
+ WHERE namespace = $2 AND session_id = $3
676
+ ORDER BY importance_score DESC
677
+ `, [now, namespace, sessionId]);
678
+ return rows.map(r => ({ ...this._rowToEntry(r), importanceScore: r.importance_score }));
679
+ }
680
+
681
+ async shutdown() {
682
+ if (this.pool) {
683
+ await this.pool.end();
684
+ this.pool = null;
685
+ }
686
+ }
687
+
688
+ _rowToEntry(row) {
689
+ return {
690
+ id: row.id,
691
+ key: row.key,
692
+ content: row.content,
693
+ type: row.type,
694
+ namespace: row.namespace,
695
+ tags: typeof row.tags === 'string' ? JSON.parse(row.tags) : row.tags,
696
+ metadata: typeof row.metadata === 'string' ? JSON.parse(row.metadata) : row.metadata,
697
+ accessLevel: row.access_level,
698
+ createdAt: parseInt(row.created_at, 10),
699
+ updatedAt: parseInt(row.updated_at, 10),
700
+ version: row.version,
701
+ accessCount: row.access_count,
702
+ lastAccessedAt: parseInt(row.last_accessed_at, 10),
703
+ references: [],
704
+ };
705
+ }
706
+ }
707
+
708
+ /**
709
+ * Parse RuVector config from environment variables.
710
+ * Returns null if required vars are not set.
711
+ */
712
+ function getRuVectorConfig() {
713
+ const host = process.env.RUVECTOR_HOST || process.env.PGHOST;
714
+ const database = process.env.RUVECTOR_DATABASE || process.env.PGDATABASE;
715
+ const user = process.env.RUVECTOR_USER || process.env.PGUSER;
716
+ const password = process.env.RUVECTOR_PASSWORD || process.env.PGPASSWORD;
717
+
718
+ if (!host || !database || !user) return null;
719
+
720
+ return {
721
+ host,
722
+ port: parseInt(process.env.RUVECTOR_PORT || process.env.PGPORT || '5432', 10),
723
+ database,
724
+ user,
725
+ password: password || '',
726
+ ssl: process.env.RUVECTOR_SSL === 'true',
727
+ };
728
+ }
729
+
730
+ // ============================================================================
731
+ // Backend resolution: SQLite > RuVector PostgreSQL > AgentDB > JSON
732
+ // ============================================================================
733
+
734
+ async function resolveBackend() {
735
+ // Tier 1: better-sqlite3 (native, fastest, local)
736
+ try {
737
+ const backend = new SQLiteBackend(ARCHIVE_DB_PATH);
738
+ await backend.initialize();
739
+ return { backend: trackBackend(backend), type: 'sqlite' };
740
+ } catch { /* fall through */ }
741
+
742
+ // Tier 2: RuVector PostgreSQL (TB-scale, vector search, GNN)
743
+ try {
744
+ const rvConfig = getRuVectorConfig();
745
+ if (rvConfig) {
746
+ const backend = new RuVectorBackend(rvConfig);
747
+ await backend.initialize();
748
+ return { backend: trackBackend(backend), type: 'ruvector' };
749
+ }
750
+ } catch { /* fall through */ }
751
+
752
+ // Tier 3: AgentDB from @claude-flow/memory (HNSW)
753
+ try {
754
+ const localDist = join(PROJECT_ROOT, 'v3/@claude-flow/memory/dist/index.js');
755
+ let memPkg = null;
756
+ if (existsSync(localDist)) {
757
+ memPkg = await import(`file://${localDist}`);
758
+ } else {
759
+ memPkg = await import('@claude-flow/memory');
760
+ }
761
+ if (memPkg?.AgentDBBackend) {
762
+ const backend = new memPkg.AgentDBBackend();
763
+ await backend.initialize();
764
+ return { backend: trackBackend(backend), type: 'agentdb' };
765
+ }
766
+ } catch { /* fall through */ }
767
+
768
+ // Tier 4: JSON file (always works)
769
+ const backend = new JsonFileBackend(ARCHIVE_JSON_PATH);
770
+ await backend.initialize();
771
+ return { backend: trackBackend(backend), type: 'json' };
772
+ }
773
+
774
+ // ============================================================================
775
+ // ONNX Embedding (384-dim, all-MiniLM-L6-v2 via @xenova/transformers)
776
+ // ============================================================================
777
+
778
+ const EMBEDDING_DIM = 384; // ONNX all-MiniLM-L6-v2 output dimension
779
+ let _onnxPipeline = null;
780
+ let _onnxFailed = false;
781
+
782
+ /**
783
+ * Initialize ONNX embedding pipeline (lazy, cached).
784
+ * Returns null if @xenova/transformers is not available.
785
+ */
786
+ async function getOnnxPipeline() {
787
+ if (_onnxFailed) return null;
788
+ if (_onnxPipeline) return _onnxPipeline;
789
+ try {
790
+ const { pipeline } = await import('@xenova/transformers');
791
+ _onnxPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
792
+ return _onnxPipeline;
793
+ } catch {
794
+ _onnxFailed = true;
795
+ return null;
796
+ }
797
+ }
798
+
799
+ /**
800
+ * Generate ONNX embedding (384-dim, high quality semantic vectors).
801
+ * Falls back to hash embedding if ONNX is unavailable.
802
+ */
803
+ async function createEmbedding(text) {
804
+ // Try ONNX first (384-dim, real semantic understanding)
805
+ const pipe = await getOnnxPipeline();
806
+ if (pipe) {
807
+ try {
808
+ const truncated = text.slice(0, 512); // MiniLM max ~512 tokens
809
+ const output = await pipe(truncated, { pooling: 'mean', normalize: true });
810
+ return { embedding: new Float32Array(output.data), dim: 384, method: 'onnx' };
811
+ } catch { /* fall through to hash */ }
812
+ }
813
+ // Fallback: hash embedding (384-dim to match ONNX dimension)
814
+ return { embedding: createHashEmbedding(text, 384), dim: 384, method: 'hash' };
815
+ }
816
+
817
+ // ============================================================================
818
+ // Hash embedding fallback (deterministic, sub-millisecond)
819
+ // ============================================================================
820
+
821
+ function createHashEmbedding(text, dimensions = 384) {
822
+ const embedding = new Float32Array(dimensions);
823
+ const normalized = text.toLowerCase().trim();
824
+ for (let i = 0; i < dimensions; i++) {
825
+ let hash = 0;
826
+ for (let j = 0; j < normalized.length; j++) {
827
+ hash = ((hash << 5) - hash + normalized.charCodeAt(j) * (i + 1)) | 0;
828
+ }
829
+ embedding[i] = (Math.sin(hash) + 1) / 2;
830
+ }
831
+ let norm = 0;
832
+ for (let i = 0; i < dimensions; i++) norm += embedding[i] * embedding[i];
833
+ norm = Math.sqrt(norm);
834
+ if (norm > 0) for (let i = 0; i < dimensions; i++) embedding[i] /= norm;
835
+ return embedding;
836
+ }
837
+
838
+ // ============================================================================
839
+ // Content hash for dedup
840
+ // ============================================================================
841
+
842
+ function hashContent(content) {
843
+ return createHash('sha256').update(content).digest('hex');
844
+ }
845
+
846
+ // ============================================================================
847
+ // Read stdin with timeout (hooks receive JSON input on stdin)
848
+ // ============================================================================
849
+
850
+ function readStdin(timeoutMs = 100) {
851
+ return new Promise((resolve) => {
852
+ let data = '';
853
+ const timer = setTimeout(() => {
854
+ process.stdin.removeAllListeners();
855
+ resolve(data ? JSON.parse(data) : null);
856
+ }, timeoutMs);
857
+
858
+ if (process.stdin.isTTY) {
859
+ clearTimeout(timer);
860
+ resolve(null);
861
+ return;
862
+ }
863
+
864
+ process.stdin.setEncoding('utf-8');
865
+ process.stdin.on('data', (chunk) => { data += chunk; });
866
+ process.stdin.on('end', () => {
867
+ clearTimeout(timer);
868
+ try { resolve(data ? JSON.parse(data) : null); }
869
+ catch { resolve(null); }
870
+ });
871
+ process.stdin.on('error', () => {
872
+ clearTimeout(timer);
873
+ resolve(null);
874
+ });
875
+ process.stdin.resume();
876
+ });
877
+ }
878
+
879
+ // ============================================================================
880
+ // Transcript parsing
881
+ // ============================================================================
882
+
883
+ function parseTranscript(transcriptPath) {
884
+ if (!existsSync(transcriptPath)) return [];
885
+ const content = readFileSync(transcriptPath, 'utf-8');
886
+ const lines = content.split('\n').filter(Boolean);
887
+ const messages = [];
888
+ for (const line of lines) {
889
+ try {
890
+ const parsed = JSON.parse(line);
891
+ // SDK transcript wraps messages: { type: "user"|"A", message: { role, content } }
892
+ // Unwrap to get the inner API message with role/content
893
+ if (parsed.message && parsed.message.role) {
894
+ messages.push(parsed.message);
895
+ } else if (parsed.role) {
896
+ // Already in API message format (e.g. from tests)
897
+ messages.push(parsed);
898
+ }
899
+ // Skip non-message entries (progress, file-history-snapshot, queue-operation)
900
+ } catch { /* skip malformed lines */ }
901
+ }
902
+ return messages;
903
+ }
904
+
905
+ // ============================================================================
906
+ // Extract text content from message content blocks
907
+ // ============================================================================
908
+
909
+ function extractTextContent(message) {
910
+ if (!message) return '';
911
+ if (typeof message.content === 'string') return message.content;
912
+ if (Array.isArray(message.content)) {
913
+ return message.content
914
+ .filter(b => b.type === 'text')
915
+ .map(b => b.text || '')
916
+ .join('\n');
917
+ }
918
+ if (typeof message.text === 'string') return message.text;
919
+ return '';
920
+ }
921
+
922
+ // ============================================================================
923
+ // Extract tool calls from assistant message
924
+ // ============================================================================
925
+
926
+ function extractToolCalls(message) {
927
+ if (!message || !Array.isArray(message.content)) return [];
928
+ return message.content
929
+ .filter(b => b.type === 'tool_use')
930
+ .map(b => ({
931
+ name: b.name || 'unknown',
932
+ input: b.input || {},
933
+ }));
934
+ }
935
+
936
+ // ============================================================================
937
+ // Extract file paths from tool calls
938
+ // ============================================================================
939
+
940
+ function extractFilePaths(toolCalls) {
941
+ const paths = new Set();
942
+ for (const tc of toolCalls) {
943
+ if (tc.input?.file_path) paths.add(tc.input.file_path);
944
+ if (tc.input?.path) paths.add(tc.input.path);
945
+ if (tc.input?.notebook_path) paths.add(tc.input.notebook_path);
946
+ }
947
+ return [...paths];
948
+ }
949
+
950
+ // ============================================================================
951
+ // Chunk transcript into conversation turns
952
+ // ============================================================================
953
+
954
+ function chunkTranscript(messages) {
955
+ const relevant = messages.filter(
956
+ m => m.role === 'user' || m.role === 'assistant'
957
+ );
958
+ const capped = relevant.slice(-MAX_MESSAGES);
959
+
960
+ const chunks = [];
961
+ let currentChunk = null;
962
+
963
+ for (const msg of capped) {
964
+ if (msg.role === 'user') {
965
+ const isSynthetic = Array.isArray(msg.content) &&
966
+ msg.content.every(b => b.type === 'tool_result');
967
+ if (isSynthetic && currentChunk) continue;
968
+ if (currentChunk) chunks.push(currentChunk);
969
+ currentChunk = {
970
+ userMessage: msg,
971
+ assistantMessage: null,
972
+ toolCalls: [],
973
+ turnIndex: chunks.length,
974
+ };
975
+ } else if (msg.role === 'assistant' && currentChunk) {
976
+ currentChunk.assistantMessage = msg;
977
+ currentChunk.toolCalls = extractToolCalls(msg);
978
+ }
979
+ }
980
+
981
+ if (currentChunk) chunks.push(currentChunk);
982
+ return chunks;
983
+ }
984
+
985
+ // ============================================================================
986
+ // Extract summary from chunk (no LLM, extractive only)
987
+ // ============================================================================
988
+
989
+ function extractSummary(chunk) {
990
+ const parts = [];
991
+
992
+ const userText = extractTextContent(chunk.userMessage);
993
+ const firstUserLine = userText.split('\n').find(l => l.trim()) || '';
994
+ if (firstUserLine) parts.push(firstUserLine.slice(0, 100));
995
+
996
+ const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
997
+ if (toolNames.length) parts.push('Tools: ' + toolNames.join(', '));
998
+
999
+ const filePaths = extractFilePaths(chunk.toolCalls);
1000
+ if (filePaths.length) {
1001
+ const shortPaths = filePaths.slice(0, 5).map(p => {
1002
+ const segs = p.split('/');
1003
+ return segs.length > 2 ? '.../' + segs.slice(-2).join('/') : p;
1004
+ });
1005
+ parts.push('Files: ' + shortPaths.join(', '));
1006
+ }
1007
+
1008
+ const assistantText = extractTextContent(chunk.assistantMessage);
1009
+ const assistantLines = assistantText.split('\n').filter(l => l.trim()).slice(0, 2);
1010
+ if (assistantLines.length) parts.push(assistantLines.join(' ').slice(0, 120));
1011
+
1012
+ return parts.join(' | ').slice(0, 300);
1013
+ }
1014
+
1015
+ // ============================================================================
1016
+ // Generate unique ID
1017
+ // ============================================================================
1018
+
1019
+ let idCounter = 0;
1020
+ function generateId() {
1021
+ return `ctx-${Date.now()}-${++idCounter}-${Math.random().toString(36).slice(2, 8)}`;
1022
+ }
1023
+
1024
+ // ============================================================================
1025
+ // Build MemoryEntry from chunk
1026
+ // ============================================================================
1027
+
1028
+ function buildEntry(chunk, sessionId, trigger, timestamp) {
1029
+ const userText = extractTextContent(chunk.userMessage);
1030
+ const assistantText = extractTextContent(chunk.assistantMessage);
1031
+ const fullContent = `User: ${userText}\n\nAssistant: ${assistantText}`;
1032
+ const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
1033
+ const filePaths = extractFilePaths(chunk.toolCalls);
1034
+ const summary = extractSummary(chunk);
1035
+ const contentHash = hashContent(fullContent);
1036
+
1037
+ const now = Date.now();
1038
+ return {
1039
+ id: generateId(),
1040
+ key: `transcript:${sessionId}:${chunk.turnIndex}:${timestamp}`,
1041
+ content: fullContent,
1042
+ type: 'episodic',
1043
+ namespace: NAMESPACE,
1044
+ tags: ['transcript', 'compaction', sessionId, ...toolNames],
1045
+ metadata: {
1046
+ sessionId,
1047
+ chunkIndex: chunk.turnIndex,
1048
+ trigger,
1049
+ timestamp,
1050
+ toolNames,
1051
+ filePaths,
1052
+ summary,
1053
+ contentHash,
1054
+ turnRange: [chunk.turnIndex, chunk.turnIndex],
1055
+ },
1056
+ accessLevel: 'private',
1057
+ createdAt: now,
1058
+ updatedAt: now,
1059
+ version: 1,
1060
+ references: [],
1061
+ accessCount: 0,
1062
+ lastAccessedAt: now,
1063
+ };
1064
+ }
1065
+
1066
+ // ============================================================================
1067
+ // Store chunks with dedup (uses indexed hash lookup for SQLite)
1068
+ // ============================================================================
1069
+
1070
+ async function storeChunks(backend, chunks, sessionId, trigger) {
1071
+ const timestamp = new Date().toISOString();
1072
+
1073
+ const entries = [];
1074
+ for (const chunk of chunks) {
1075
+ const entry = buildEntry(chunk, sessionId, trigger, timestamp);
1076
+ // Fast hash-based dedup (indexed lookup in SQLite, scan in JSON)
1077
+ if (!backend.hashExists(entry.metadata.contentHash)) {
1078
+ entries.push(entry);
1079
+ }
1080
+ }
1081
+
1082
+ if (entries.length > 0) {
1083
+ await backend.bulkInsert(entries);
1084
+ }
1085
+
1086
+ return { stored: entries.length, deduped: chunks.length - entries.length };
1087
+ }
1088
+
1089
+ // ============================================================================
1090
+ // Retrieve context for restoration (uses indexed session query for SQLite)
1091
+ // ============================================================================
1092
+
1093
+ async function retrieveContext(backend, sessionId, budget) {
1094
+ // Use optimized session query if available, otherwise filter manually
1095
+ const sessionEntries = backend.queryBySession
1096
+ ? await backend.queryBySession(NAMESPACE, sessionId)
1097
+ : (await backend.query({ namespace: NAMESPACE }))
1098
+ .filter(e => e.metadata?.sessionId === sessionId)
1099
+ .sort((a, b) => (b.metadata?.chunkIndex ?? 0) - (a.metadata?.chunkIndex ?? 0));
1100
+
1101
+ if (sessionEntries.length === 0) return '';
1102
+
1103
+ const lines = [];
1104
+ let charCount = 0;
1105
+ const header = `## Restored Context (from pre-compaction archive)\n\nPrevious conversation included ${sessionEntries.length} archived turns:\n\n`;
1106
+ charCount += header.length;
1107
+
1108
+ for (const entry of sessionEntries) {
1109
+ const meta = entry.metadata || {};
1110
+ const toolStr = meta.toolNames?.length ? ` Tools: ${meta.toolNames.join(', ')}.` : '';
1111
+ const fileStr = meta.filePaths?.length ? ` Files: ${meta.filePaths.slice(0, 3).join(', ')}.` : '';
1112
+ const line = `- [Turn ${meta.chunkIndex ?? '?'}] ${meta.summary || '(no summary)'}${toolStr}${fileStr}`;
1113
+
1114
+ if (charCount + line.length + 1 > budget) break;
1115
+ lines.push(line);
1116
+ charCount += line.length + 1;
1117
+ }
1118
+
1119
+ if (lines.length === 0) return '';
1120
+
1121
+ const footer = `\n\nFull archive: ${NAMESPACE} namespace in AgentDB (query with session ID: ${sessionId})`;
1122
+ return header + lines.join('\n') + footer;
1123
+ }
1124
+
1125
+ // ============================================================================
1126
+ // Build custom compact instructions (exit code 0 stdout)
1127
+ // Guides Claude on what to preserve during compaction summary
1128
+ // ============================================================================
1129
+
1130
+ function buildCompactInstructions(chunks, sessionId, archiveResult) {
1131
+ const parts = [];
1132
+
1133
+ parts.push('COMPACTION GUIDANCE (from context-persistence-hook):');
1134
+ parts.push('');
1135
+ parts.push(`All ${chunks.length} conversation turns have been archived to the transcript-archive database.`);
1136
+ parts.push(`Session: ${sessionId} | Stored: ${archiveResult.stored} new, ${archiveResult.deduped} deduped.`);
1137
+ parts.push('After compaction, archived context will be automatically restored via SessionStart hook.');
1138
+ parts.push('');
1139
+
1140
+ // Collect unique tools and files across all chunks for preservation hints
1141
+ const allTools = new Set();
1142
+ const allFiles = new Set();
1143
+ const decisions = [];
1144
+
1145
+ for (const chunk of chunks) {
1146
+ const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
1147
+ for (const t of toolNames) allTools.add(t);
1148
+ const filePaths = extractFilePaths(chunk.toolCalls);
1149
+ for (const f of filePaths) allFiles.add(f);
1150
+
1151
+ // Look for decision indicators in assistant text
1152
+ const assistantText = extractTextContent(chunk.assistantMessage);
1153
+ if (assistantText) {
1154
+ const lower = assistantText.toLowerCase();
1155
+ if (lower.includes('decided') || lower.includes('choosing') || lower.includes('approach')
1156
+ || lower.includes('instead of') || lower.includes('rather than')) {
1157
+ const firstLine = assistantText.split('\n').find(l => l.trim()) || '';
1158
+ if (firstLine.length > 10) decisions.push(firstLine.slice(0, 120));
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ parts.push('PRESERVE in compaction summary:');
1164
+
1165
+ if (allFiles.size > 0) {
1166
+ const fileList = [...allFiles].slice(0, 15).map(f => {
1167
+ const segs = f.split('/');
1168
+ return segs.length > 3 ? '.../' + segs.slice(-3).join('/') : f;
1169
+ });
1170
+ parts.push(`- Files modified/read: ${fileList.join(', ')}`);
1171
+ }
1172
+
1173
+ if (allTools.size > 0) {
1174
+ parts.push(`- Tools used: ${[...allTools].join(', ')}`);
1175
+ }
1176
+
1177
+ if (decisions.length > 0) {
1178
+ parts.push('- Key decisions:');
1179
+ for (const d of decisions.slice(0, 5)) {
1180
+ parts.push(` * ${d}`);
1181
+ }
1182
+ }
1183
+
1184
+ // Recent turns summary (most important context)
1185
+ const recentChunks = chunks.slice(-5);
1186
+ if (recentChunks.length > 0) {
1187
+ parts.push('');
1188
+ parts.push('MOST RECENT TURNS (prioritize preserving):');
1189
+ for (const chunk of recentChunks) {
1190
+ const userText = extractTextContent(chunk.userMessage);
1191
+ const firstLine = userText.split('\n').find(l => l.trim()) || '';
1192
+ const toolNames = [...new Set(chunk.toolCalls.map(tc => tc.name))];
1193
+ parts.push(`- [Turn ${chunk.turnIndex}] ${firstLine.slice(0, 80)}${toolNames.length ? ` (${toolNames.join(', ')})` : ''}`);
1194
+ }
1195
+ }
1196
+
1197
+ // Cap at budget
1198
+ let result = parts.join('\n');
1199
+ if (result.length > COMPACT_INSTRUCTION_BUDGET) {
1200
+ result = result.slice(0, COMPACT_INSTRUCTION_BUDGET - 3) + '...';
1201
+ }
1202
+ return result;
1203
+ }
1204
+
1205
+ // ============================================================================
1206
+ // Importance scoring for retrieval ranking
1207
+ // ============================================================================
1208
+
1209
+ function computeImportance(entry, now) {
1210
+ const meta = entry.metadata || {};
1211
+ const accessCount = entry.accessCount || 0;
1212
+ const createdAt = entry.createdAt || now;
1213
+ const ageMs = Math.max(1, now - createdAt);
1214
+ const ageDays = ageMs / 86400000;
1215
+
1216
+ // Recency: exponential decay, half-life of 7 days
1217
+ const recency = Math.exp(-0.693 * ageDays / 7);
1218
+
1219
+ // Frequency: log-scaled access count
1220
+ const frequency = Math.log2(accessCount + 1) + 1;
1221
+
1222
+ // Richness: tool calls and file paths indicate actionable context
1223
+ const toolCount = meta.toolNames?.length || 0;
1224
+ const fileCount = meta.filePaths?.length || 0;
1225
+ const richness = 1.0 + (toolCount > 0 ? 0.5 : 0) + (fileCount > 0 ? 0.3 : 0);
1226
+
1227
+ return recency * frequency * richness;
1228
+ }
1229
+
1230
+ // ============================================================================
1231
+ // Smart retrieval: importance-ranked instead of just recency
1232
+ // ============================================================================
1233
+
1234
+ async function retrieveContextSmart(backend, sessionId, budget) {
1235
+ let sessionEntries;
1236
+
1237
+ // Use importance-ranked query if backend supports it
1238
+ if (backend.queryByImportance) {
1239
+ try {
1240
+ sessionEntries = backend.queryByImportance(NAMESPACE, sessionId);
1241
+ } catch {
1242
+ // Fall back to standard query
1243
+ sessionEntries = null;
1244
+ }
1245
+ }
1246
+
1247
+ if (!sessionEntries) {
1248
+ // Fall back: fetch all, compute importance in JS
1249
+ const raw = backend.queryBySession
1250
+ ? await backend.queryBySession(NAMESPACE, sessionId)
1251
+ : (await backend.query({ namespace: NAMESPACE }))
1252
+ .filter(e => e.metadata?.sessionId === sessionId);
1253
+
1254
+ const now = Date.now();
1255
+ sessionEntries = raw
1256
+ .map(e => ({ ...e, importanceScore: computeImportance(e, now) }))
1257
+ .sort((a, b) => b.importanceScore - a.importanceScore);
1258
+ }
1259
+
1260
+ if (sessionEntries.length === 0) return { text: '', accessedIds: [] };
1261
+
1262
+ const lines = [];
1263
+ const accessedIds = [];
1264
+ let charCount = 0;
1265
+ const header = `## Restored Context (importance-ranked from archive)\n\nPrevious conversation: ${sessionEntries.length} archived turns, ranked by importance:\n\n`;
1266
+ charCount += header.length;
1267
+
1268
+ for (const entry of sessionEntries) {
1269
+ const meta = entry.metadata || {};
1270
+ const score = entry.importanceScore?.toFixed(2) || '?';
1271
+ const toolStr = meta.toolNames?.length ? ` Tools: ${meta.toolNames.join(', ')}.` : '';
1272
+ const fileStr = meta.filePaths?.length ? ` Files: ${meta.filePaths.slice(0, 3).join(', ')}.` : '';
1273
+ const line = `- [Turn ${meta.chunkIndex ?? '?'}, score:${score}] ${meta.summary || '(no summary)'}${toolStr}${fileStr}`;
1274
+
1275
+ if (charCount + line.length + 1 > budget) break;
1276
+ lines.push(line);
1277
+ accessedIds.push(entry.id);
1278
+ charCount += line.length + 1;
1279
+ }
1280
+
1281
+ if (lines.length === 0) return { text: '', accessedIds: [] };
1282
+
1283
+ // Cross-session semantic search: find related context from previous sessions
1284
+ let crossSessionText = '';
1285
+ if (backend.semanticSearch && sessionEntries.length > 0) {
1286
+ try {
1287
+ // Use the most recent turn's summary as the search query
1288
+ const recentSummary = sessionEntries[0]?.metadata?.summary || '';
1289
+ if (recentSummary) {
1290
+ const crossResults = await crossSessionSearch(backend, recentSummary, sessionId, 3);
1291
+ if (crossResults.length > 0) {
1292
+ const crossLines = crossResults.map(r =>
1293
+ `- [Session ${r.sessionId?.slice(0, 8)}..., turn ${r.chunkIndex ?? '?'}, conf:${(r.confidence || 0).toFixed(2)}] ${r.summary || '(no summary)'}`
1294
+ );
1295
+ crossSessionText = `\n\nRelated context from previous sessions:\n${crossLines.join('\n')}`;
1296
+ }
1297
+ }
1298
+ } catch { /* cross-session search is best-effort */ }
1299
+ }
1300
+
1301
+ const footer = `\n\nFull archive: ${NAMESPACE} namespace (session: ${sessionId}). ${sessionEntries.length - lines.length} additional turns available.`;
1302
+ return { text: header + lines.join('\n') + crossSessionText + footer, accessedIds };
1303
+ }
1304
+
1305
+ // ============================================================================
1306
+ // Auto-optimize: prune stale entries, run after archiving
1307
+ // ============================================================================
1308
+
1309
+ async function autoOptimize(backend, backendType) {
1310
+ if (!AUTO_OPTIMIZE) return { pruned: 0, synced: 0, decayed: 0, embedded: 0 };
1311
+
1312
+ let pruned = 0;
1313
+ let decayed = 0;
1314
+ let embedded = 0;
1315
+
1316
+ // Step 1: Confidence decay — reduce confidence for unaccessed entries
1317
+ if (backend.decayConfidence) {
1318
+ try {
1319
+ decayed = backend.decayConfidence(NAMESPACE, 1); // 1 hour worth of decay per optimize cycle
1320
+ } catch { /* non-critical */ }
1321
+ }
1322
+
1323
+ // Step 2: Smart pruning — remove low-confidence entries first
1324
+ if (backend.pruneByConfidence) {
1325
+ try {
1326
+ pruned += backend.pruneByConfidence(NAMESPACE, 0.15);
1327
+ } catch { /* non-critical */ }
1328
+ }
1329
+
1330
+ // Step 3: Age-based pruning as fallback
1331
+ if (backend.pruneStale) {
1332
+ try {
1333
+ pruned += backend.pruneStale(NAMESPACE, RETENTION_DAYS);
1334
+ } catch { /* non-critical */ }
1335
+ }
1336
+
1337
+ // Step 4: Generate ONNX embeddings (384-dim) for entries missing them
1338
+ if (backend.storeEmbedding) {
1339
+ try {
1340
+ const rows = backend.db?.prepare?.(
1341
+ 'SELECT id, content FROM transcript_entries WHERE namespace = ? AND embedding IS NULL LIMIT 20'
1342
+ )?.all(NAMESPACE);
1343
+ if (rows) {
1344
+ for (const row of rows) {
1345
+ const { embedding } = await createEmbedding(row.content);
1346
+ backend.storeEmbedding(row.id, embedding);
1347
+ embedded++;
1348
+ }
1349
+ }
1350
+ } catch { /* non-critical */ }
1351
+ }
1352
+
1353
+ // Step 5: Auto-sync to RuVector if available
1354
+ let synced = 0;
1355
+ if (backendType === 'sqlite' && backend.allForSync) {
1356
+ try {
1357
+ const rvConfig = getRuVectorConfig();
1358
+ if (rvConfig) {
1359
+ const rvBackend = new RuVectorBackend(rvConfig);
1360
+ await rvBackend.initialize();
1361
+
1362
+ const allEntries = backend.allForSync(NAMESPACE);
1363
+ if (allEntries.length > 0) {
1364
+ // Add hash embeddings for vector search in RuVector
1365
+ const entriesToSync = allEntries.map(e => ({
1366
+ ...e,
1367
+ _embedding: createHashEmbedding(e.content),
1368
+ }));
1369
+ await rvBackend.bulkInsert(entriesToSync);
1370
+ synced = entriesToSync.length;
1371
+ }
1372
+
1373
+ await rvBackend.shutdown();
1374
+ }
1375
+ } catch { /* RuVector sync is best-effort */ }
1376
+ }
1377
+
1378
+ return { pruned, synced, decayed, embedded };
1379
+ }
1380
+
1381
+ // ============================================================================
1382
+ // Cross-session semantic retrieval
1383
+ // ============================================================================
1384
+
1385
+ /**
1386
+ * Find relevant context from OTHER sessions using semantic similarity.
1387
+ * This enables "What did we discuss about auth?" across sessions.
1388
+ */
1389
+ async function crossSessionSearch(backend, queryText, currentSessionId, k = 5) {
1390
+ if (!backend.semanticSearch) return [];
1391
+ try {
1392
+ const { embedding: queryEmb } = await createEmbedding(queryText);
1393
+ const results = backend.semanticSearch(queryEmb, k * 2, NAMESPACE);
1394
+ // Filter out current session entries (we already have those)
1395
+ return results
1396
+ .filter(r => r.sessionId !== currentSessionId)
1397
+ .slice(0, k);
1398
+ } catch { return []; }
1399
+ }
1400
+
1401
+ // ============================================================================
1402
+ // Context Autopilot Engine
1403
+ // ============================================================================
1404
+
1405
+ /**
1406
+ * Estimate context token usage from transcript JSONL.
1407
+ *
1408
+ * Primary method: Read the most recent assistant message's `usage` field which
1409
+ * contains `input_tokens` + `cache_read_input_tokens` — this is the ACTUAL
1410
+ * context size as reported by the Claude API. This includes system prompt,
1411
+ * CLAUDE.md, tool definitions, all messages, and everything Claude sees.
1412
+ *
1413
+ * Fallback: Sum character lengths and divide by CHARS_PER_TOKEN.
1414
+ */
1415
+ function estimateContextTokens(transcriptPath) {
1416
+ if (!existsSync(transcriptPath)) return { tokens: 0, turns: 0, method: 'none' };
1417
+
1418
+ const content = readFileSync(transcriptPath, 'utf-8');
1419
+ const lines = content.split('\n').filter(Boolean);
1420
+
1421
+ // Track the most recent usage data (from the last assistant message)
1422
+ let lastInputTokens = 0;
1423
+ let lastCacheRead = 0;
1424
+ let lastCacheCreate = 0;
1425
+ let turns = 0;
1426
+ let lastPreTokens = 0;
1427
+ let totalChars = 0;
1428
+
1429
+ for (let i = 0; i < lines.length; i++) {
1430
+ try {
1431
+ const parsed = JSON.parse(lines[i]);
1432
+
1433
+ // Check for compact_boundary
1434
+ if (parsed.type === 'system' && parsed.subtype === 'compact_boundary') {
1435
+ lastPreTokens = parsed.compactMetadata?.preTokens
1436
+ || parsed.compact_metadata?.pre_tokens || 0;
1437
+ // Reset after compaction — new context starts here
1438
+ totalChars = 0;
1439
+ turns = 0;
1440
+ lastInputTokens = 0;
1441
+ lastCacheRead = 0;
1442
+ lastCacheCreate = 0;
1443
+ continue;
1444
+ }
1445
+
1446
+ // Extract ACTUAL token usage from assistant messages
1447
+ // The SDK transcript stores: { message: { role, content, usage: { input_tokens, cache_read_input_tokens, ... } } }
1448
+ const msg = parsed.message || parsed;
1449
+ const usage = msg.usage;
1450
+ if (usage && (msg.role === 'assistant' || parsed.type === 'assistant')) {
1451
+ const inputTokens = usage.input_tokens || 0;
1452
+ const cacheRead = usage.cache_read_input_tokens || 0;
1453
+ const cacheCreate = usage.cache_creation_input_tokens || 0;
1454
+
1455
+ // The total context sent to Claude = input_tokens + cache_read + cache_create
1456
+ // input_tokens: non-cached tokens actually processed
1457
+ // cache_read: tokens served from cache (still in context)
1458
+ // cache_create: tokens newly cached (still in context)
1459
+ const totalContext = inputTokens + cacheRead + cacheCreate;
1460
+
1461
+ if (totalContext > 0) {
1462
+ lastInputTokens = inputTokens;
1463
+ lastCacheRead = cacheRead;
1464
+ lastCacheCreate = cacheCreate;
1465
+ }
1466
+ }
1467
+
1468
+ // Count turns for display
1469
+ const role = msg.role || parsed.type;
1470
+ if (role === 'user') turns++;
1471
+
1472
+ // Char fallback accumulation
1473
+ if (role === 'user' || role === 'assistant') {
1474
+ const c = msg.content;
1475
+ if (typeof c === 'string') totalChars += c.length;
1476
+ else if (Array.isArray(c)) {
1477
+ for (const block of c) {
1478
+ if (block.text) totalChars += block.text.length;
1479
+ else if (block.input) totalChars += JSON.stringify(block.input).length;
1480
+ }
1481
+ }
1482
+ }
1483
+ } catch { /* skip */ }
1484
+ }
1485
+
1486
+ // Primary: use actual API usage data
1487
+ const actualTotal = lastInputTokens + lastCacheRead + lastCacheCreate;
1488
+ if (actualTotal > 0) {
1489
+ return {
1490
+ tokens: actualTotal,
1491
+ turns,
1492
+ method: 'api-usage',
1493
+ lastPreTokens,
1494
+ breakdown: {
1495
+ input: lastInputTokens,
1496
+ cacheRead: lastCacheRead,
1497
+ cacheCreate: lastCacheCreate,
1498
+ },
1499
+ };
1500
+ }
1501
+
1502
+ // Fallback: char-based estimate
1503
+ const estimatedTokens = Math.ceil(totalChars / CHARS_PER_TOKEN);
1504
+ if (lastPreTokens > 0) {
1505
+ const compactSummaryTokens = 3000;
1506
+ return {
1507
+ tokens: compactSummaryTokens + estimatedTokens,
1508
+ turns,
1509
+ method: 'post-compact-char-estimate',
1510
+ lastPreTokens,
1511
+ };
1512
+ }
1513
+
1514
+ return { tokens: estimatedTokens, turns, method: 'char-estimate' };
1515
+ }
1516
+
1517
+ /**
1518
+ * Load autopilot state (persisted across hook invocations).
1519
+ */
1520
+ function loadAutopilotState() {
1521
+ try {
1522
+ if (existsSync(AUTOPILOT_STATE_PATH)) {
1523
+ return JSON.parse(readFileSync(AUTOPILOT_STATE_PATH, 'utf-8'));
1524
+ }
1525
+ } catch { /* fresh state */ }
1526
+ return {
1527
+ sessionId: null,
1528
+ lastTokenEstimate: 0,
1529
+ lastPercentage: 0,
1530
+ pruneCount: 0,
1531
+ warningIssued: false,
1532
+ lastCheck: 0,
1533
+ history: [], // Track token growth over time
1534
+ };
1535
+ }
1536
+
1537
+ /**
1538
+ * Save autopilot state.
1539
+ */
1540
+ function saveAutopilotState(state) {
1541
+ try {
1542
+ writeFileSync(AUTOPILOT_STATE_PATH, JSON.stringify(state, null, 2), 'utf-8');
1543
+ } catch { /* best effort */ }
1544
+ }
1545
+
1546
+ /**
1547
+ * Build a context optimization report for additionalContext injection.
1548
+ */
1549
+ function buildAutopilotReport(percentage, tokens, windowSize, turns, state) {
1550
+ const bar = buildProgressBar(percentage);
1551
+ const status = percentage >= AUTOPILOT_PRUNE_PCT
1552
+ ? 'OPTIMIZING'
1553
+ : percentage >= AUTOPILOT_WARN_PCT
1554
+ ? 'WARNING'
1555
+ : 'OK';
1556
+
1557
+ const parts = [
1558
+ `[ContextAutopilot] ${bar} ${(percentage * 100).toFixed(1)}% context used`,
1559
+ `(~${formatTokens(tokens)}/${formatTokens(windowSize)} tokens, ${turns} turns)`,
1560
+ `Status: ${status}`,
1561
+ ];
1562
+
1563
+ if (state.pruneCount > 0) {
1564
+ parts.push(`| Optimizations: ${state.pruneCount} prune cycles`);
1565
+ }
1566
+
1567
+ // Add trend if we have history
1568
+ if (state.history.length >= 2) {
1569
+ const recent = state.history.slice(-3);
1570
+ const avgGrowth = recent.reduce((sum, h, i) => {
1571
+ if (i === 0) return 0;
1572
+ return sum + (h.pct - recent[i - 1].pct);
1573
+ }, 0) / (recent.length - 1);
1574
+
1575
+ if (avgGrowth > 0) {
1576
+ const turnsUntilFull = Math.ceil((1.0 - percentage) / avgGrowth);
1577
+ parts.push(`| ~${turnsUntilFull} turns until optimization needed`);
1578
+ }
1579
+ }
1580
+
1581
+ return parts.join(' ');
1582
+ }
1583
+
1584
+ /**
1585
+ * Visual progress bar for context usage.
1586
+ */
1587
+ function buildProgressBar(percentage) {
1588
+ const width = 20;
1589
+ const filled = Math.round(percentage * width);
1590
+ const empty = width - filled;
1591
+ const fillChar = percentage >= AUTOPILOT_PRUNE_PCT ? '!' : percentage >= AUTOPILOT_WARN_PCT ? '#' : '=';
1592
+ return `[${fillChar.repeat(filled)}${'-'.repeat(empty)}]`;
1593
+ }
1594
+
1595
+ /**
1596
+ * Format token count for display.
1597
+ */
1598
+ function formatTokens(n) {
1599
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1600
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
1601
+ return String(n);
1602
+ }
1603
+
1604
+ /**
1605
+ * Context Autopilot: run on every UserPromptSubmit.
1606
+ * Returns { additionalContext, shouldBlock } for the hook output.
1607
+ */
1608
+ async function runAutopilot(transcriptPath, sessionId, backend, backendType) {
1609
+ const state = loadAutopilotState();
1610
+
1611
+ // Reset state if session changed
1612
+ if (state.sessionId !== sessionId) {
1613
+ state.sessionId = sessionId;
1614
+ state.lastTokenEstimate = 0;
1615
+ state.lastPercentage = 0;
1616
+ state.pruneCount = 0;
1617
+ state.warningIssued = false;
1618
+ state.history = [];
1619
+ }
1620
+
1621
+ // Estimate current context usage
1622
+ const { tokens, turns, method, lastPreTokens } = estimateContextTokens(transcriptPath);
1623
+ const percentage = Math.min(tokens / CONTEXT_WINDOW_TOKENS, 1.0);
1624
+
1625
+ // Track history (keep last 50 data points)
1626
+ state.history.push({ ts: Date.now(), tokens, pct: percentage, turns });
1627
+ if (state.history.length > 50) state.history.shift();
1628
+
1629
+ state.lastTokenEstimate = tokens;
1630
+ state.lastPercentage = percentage;
1631
+ state.lastCheck = Date.now();
1632
+
1633
+ let optimizationMessage = '';
1634
+
1635
+ // Phase 1: Warning zone (70-85%) — advise concise responses
1636
+ if (percentage >= AUTOPILOT_WARN_PCT && percentage < AUTOPILOT_PRUNE_PCT) {
1637
+ if (!state.warningIssued) {
1638
+ state.warningIssued = true;
1639
+ optimizationMessage = ` | Context at ${(percentage * 100).toFixed(0)}%. Keep responses concise to extend session.`;
1640
+ }
1641
+ }
1642
+
1643
+ // Phase 2: Critical zone (85%+) — session rotation needed
1644
+ if (percentage >= AUTOPILOT_PRUNE_PCT) {
1645
+ state.pruneCount++;
1646
+
1647
+ // Prune stale entries from archive to free up storage
1648
+ if (backend.pruneStale) {
1649
+ try {
1650
+ const pruned = backend.pruneStale(NAMESPACE, Math.min(RETENTION_DAYS, 7));
1651
+ if (pruned > 0) {
1652
+ optimizationMessage += ` | Pruned ${pruned} stale archive entries.`;
1653
+ }
1654
+ } catch { /* non-critical */ }
1655
+ }
1656
+
1657
+ const turnsLeft = Math.max(0, Math.ceil((1.0 - percentage) / 0.03));
1658
+ optimizationMessage += ` | CRITICAL: ${(percentage * 100).toFixed(0)}% context used (~${turnsLeft} turns left). All ${turns} turns archived. Start a new session with /clear — context will be fully restored via SessionStart hook.`;
1659
+ }
1660
+
1661
+ const report = buildAutopilotReport(percentage, tokens, CONTEXT_WINDOW_TOKENS, turns, state);
1662
+ saveAutopilotState(state);
1663
+
1664
+ return {
1665
+ additionalContext: report + optimizationMessage,
1666
+ percentage,
1667
+ tokens,
1668
+ turns,
1669
+ method,
1670
+ state,
1671
+ };
1672
+ }
1673
+
1674
+ // ============================================================================
1675
+ // Commands
1676
+ // ============================================================================
1677
+
1678
+ async function doPreCompact() {
1679
+ const input = await readStdin(200);
1680
+ if (!input) return;
1681
+
1682
+ const { session_id: sessionId, transcript_path: transcriptPath, trigger } = input;
1683
+ if (!transcriptPath || !sessionId) return;
1684
+
1685
+ const messages = parseTranscript(transcriptPath);
1686
+ if (messages.length === 0) return;
1687
+
1688
+ const chunks = chunkTranscript(messages);
1689
+ if (chunks.length === 0) return;
1690
+
1691
+ const { backend, type } = await resolveBackend();
1692
+
1693
+ const archiveResult = await storeChunks(backend, chunks, sessionId, trigger || 'auto');
1694
+
1695
+ // Auto-optimize: prune stale entries + sync to RuVector if available
1696
+ const optimizeResult = await autoOptimize(backend, type);
1697
+
1698
+ const total = await backend.count(NAMESPACE);
1699
+ await backend.shutdown();
1700
+
1701
+ const optParts = [];
1702
+ if (optimizeResult.pruned > 0) optParts.push(`${optimizeResult.pruned} pruned`);
1703
+ if (optimizeResult.decayed > 0) optParts.push(`${optimizeResult.decayed} decayed`);
1704
+ if (optimizeResult.embedded > 0) optParts.push(`${optimizeResult.embedded} embedded`);
1705
+ if (optimizeResult.synced > 0) optParts.push(`${optimizeResult.synced} synced`);
1706
+ const optimizeMsg = optParts.length > 0 ? ` Optimized: ${optParts.join(', ')}.` : '';
1707
+ process.stderr.write(
1708
+ `[ContextPersistence] Archived ${archiveResult.stored} turns (${archiveResult.deduped} deduped) via ${type}. Total: ${total}.${optimizeMsg}\n`
1709
+ );
1710
+
1711
+ // Exit code 0: stdout is appended as custom compact instructions
1712
+ // This guides Claude on what to preserve in the compaction summary
1713
+ const instructions = buildCompactInstructions(chunks, sessionId, archiveResult);
1714
+ process.stdout.write(instructions);
1715
+
1716
+ // Context Autopilot: track state and log archival status
1717
+ // NOTE: Claude Code 2.0.76 executePreCompactHooks uses executeHooksOutsideREPL
1718
+ // which does NOT support exit code 2 blocking. Compaction always proceeds.
1719
+ // Our "infinite context" comes from archive + restore, not blocking.
1720
+ if (AUTOPILOT_ENABLED) {
1721
+ const state = loadAutopilotState();
1722
+ const pct = state.lastPercentage || 0;
1723
+ const bar = buildProgressBar(pct);
1724
+
1725
+ process.stderr.write(
1726
+ `[ContextAutopilot] ${bar} ${(pct * 100).toFixed(1)}% | ${trigger} compact — ${chunks.length} turns archived. Context will be restored after compaction.\n`
1727
+ );
1728
+
1729
+ // Reset autopilot state for post-compaction fresh start
1730
+ state.lastTokenEstimate = 0;
1731
+ state.lastPercentage = 0;
1732
+ state.warningIssued = false;
1733
+ saveAutopilotState(state);
1734
+ }
1735
+ }
1736
+
1737
+ async function doSessionStart() {
1738
+ const input = await readStdin(200);
1739
+
1740
+ // Restore context after compaction OR after /clear (session rotation)
1741
+ // With DISABLE_COMPACT, /clear is the primary way to free context
1742
+ if (!input || (input.source !== 'compact' && input.source !== 'clear')) return;
1743
+
1744
+ const sessionId = input.session_id;
1745
+ if (!sessionId) return;
1746
+
1747
+ const { backend, type } = await resolveBackend();
1748
+
1749
+ // Use smart retrieval (importance-ranked) when auto-optimize is on
1750
+ let additionalContext;
1751
+ if (AUTO_OPTIMIZE) {
1752
+ const { text, accessedIds } = await retrieveContextSmart(backend, sessionId, RESTORE_BUDGET);
1753
+ additionalContext = text;
1754
+
1755
+ // Track which entries were actually restored (access pattern learning)
1756
+ if (accessedIds.length > 0 && backend.markAccessed) {
1757
+ try { backend.markAccessed(accessedIds); } catch { /* non-critical */ }
1758
+ }
1759
+
1760
+ if (accessedIds.length > 0) {
1761
+ process.stderr.write(
1762
+ `[ContextPersistence] Smart restore: ${accessedIds.length} turns (importance-ranked) via ${type}\n`
1763
+ );
1764
+ }
1765
+ } else {
1766
+ additionalContext = await retrieveContext(backend, sessionId, RESTORE_BUDGET);
1767
+ }
1768
+
1769
+ await backend.shutdown();
1770
+
1771
+ if (!additionalContext) return;
1772
+
1773
+ const output = {
1774
+ hookSpecificOutput: {
1775
+ hookEventName: 'SessionStart',
1776
+ additionalContext,
1777
+ },
1778
+ };
1779
+ process.stdout.write(JSON.stringify(output));
1780
+ }
1781
+
1782
+ // ============================================================================
1783
+ // Proactive archiving on every user prompt (prevents context cliff)
1784
+ // ============================================================================
1785
+
1786
+ async function doUserPromptSubmit() {
1787
+ const input = await readStdin(200);
1788
+ if (!input) return;
1789
+
1790
+ const { session_id: sessionId, transcript_path: transcriptPath } = input;
1791
+ if (!transcriptPath || !sessionId) return;
1792
+
1793
+ const messages = parseTranscript(transcriptPath);
1794
+ if (messages.length === 0) return;
1795
+
1796
+ const chunks = chunkTranscript(messages);
1797
+ if (chunks.length === 0) return;
1798
+
1799
+ const { backend, type } = await resolveBackend();
1800
+
1801
+ // Only archive new turns (dedup handles the rest, but we can skip early
1802
+ // by only processing the last N chunks since the previous archive)
1803
+ const existingCount = backend.queryBySession
1804
+ ? (await backend.queryBySession(NAMESPACE, sessionId)).length
1805
+ : 0;
1806
+
1807
+ // Skip if we've already archived most turns (within 2 turns tolerance)
1808
+ const skipArchive = existingCount > 0 && chunks.length - existingCount <= 2;
1809
+
1810
+ let archiveMsg = '';
1811
+ if (!skipArchive) {
1812
+ const result = await storeChunks(backend, chunks, sessionId, 'proactive');
1813
+ if (result.stored > 0) {
1814
+ const total = await backend.count(NAMESPACE);
1815
+ archiveMsg = `[ContextPersistence] Proactively archived ${result.stored} turns (total: ${total}).`;
1816
+ process.stderr.write(
1817
+ `[ContextPersistence] Proactive archive: ${result.stored} new, ${result.deduped} deduped via ${type}. Total: ${total}\n`
1818
+ );
1819
+ }
1820
+ }
1821
+
1822
+ // Context Autopilot: estimate usage and report percentage
1823
+ let autopilotMsg = '';
1824
+ if (AUTOPILOT_ENABLED && transcriptPath) {
1825
+ try {
1826
+ const autopilot = await runAutopilot(transcriptPath, sessionId, backend, type);
1827
+ autopilotMsg = autopilot.additionalContext;
1828
+
1829
+ process.stderr.write(
1830
+ `[ContextAutopilot] ${(autopilot.percentage * 100).toFixed(1)}% context used (~${formatTokens(autopilot.tokens)} tokens, ${autopilot.turns} turns, ${autopilot.method})\n`
1831
+ );
1832
+ } catch (err) {
1833
+ process.stderr.write(`[ContextAutopilot] Error: ${err.message}\n`);
1834
+ }
1835
+ }
1836
+
1837
+ await backend.shutdown();
1838
+
1839
+ // Combine archive message and autopilot report
1840
+ const additionalContext = [archiveMsg, autopilotMsg].filter(Boolean).join(' ');
1841
+
1842
+ if (additionalContext) {
1843
+ const output = {
1844
+ hookSpecificOutput: {
1845
+ hookEventName: 'UserPromptSubmit',
1846
+ additionalContext,
1847
+ },
1848
+ };
1849
+ process.stdout.write(JSON.stringify(output));
1850
+ }
1851
+ }
1852
+
1853
+ async function doStatus() {
1854
+ const { backend, type } = await resolveBackend();
1855
+
1856
+ const total = await backend.count();
1857
+ const archiveCount = await backend.count(NAMESPACE);
1858
+ const namespaces = await backend.listNamespaces();
1859
+ const sessions = await backend.listSessions(NAMESPACE);
1860
+
1861
+ console.log('\n=== Context Persistence Archive Status ===\n');
1862
+ const backendLabel = {
1863
+ sqlite: ARCHIVE_DB_PATH,
1864
+ ruvector: `${process.env.RUVECTOR_HOST || 'N/A'}:${process.env.RUVECTOR_PORT || '5432'}`,
1865
+ agentdb: 'in-memory HNSW',
1866
+ json: ARCHIVE_JSON_PATH,
1867
+ };
1868
+ console.log(` Backend: ${type} (${backendLabel[type] || type})`);
1869
+ console.log(` Total: ${total} entries`);
1870
+ console.log(` Transcripts: ${archiveCount} entries`);
1871
+ console.log(` Namespaces: ${namespaces.join(', ') || 'none'}`);
1872
+ console.log(` Budget: ${RESTORE_BUDGET} chars`);
1873
+ console.log(` Sessions: ${sessions.length}`);
1874
+ console.log(` Proactive: enabled (UserPromptSubmit hook)`);
1875
+ console.log(` Auto-opt: ${AUTO_OPTIMIZE ? 'enabled' : 'disabled'} (importance ranking, pruning, sync)`);
1876
+ console.log(` Retention: ${RETENTION_DAYS} days (prune never-accessed entries)`);
1877
+ const rvConfig = getRuVectorConfig();
1878
+ console.log(` RuVector: ${rvConfig ? `${rvConfig.host}:${rvConfig.port}/${rvConfig.database} (auto-sync enabled)` : 'not configured'}`);
1879
+
1880
+ // Self-learning stats
1881
+ if (type === 'sqlite' && backend.db) {
1882
+ try {
1883
+ const embCount = backend.db.prepare('SELECT COUNT(*) as cnt FROM transcript_entries WHERE embedding IS NOT NULL').get().cnt;
1884
+ const avgConf = backend.db.prepare('SELECT AVG(confidence) as avg FROM transcript_entries WHERE namespace = ?').get(NAMESPACE)?.avg || 0;
1885
+ const lowConf = backend.db.prepare('SELECT COUNT(*) as cnt FROM transcript_entries WHERE namespace = ? AND confidence < 0.3').get(NAMESPACE).cnt;
1886
+ console.log('');
1887
+ console.log(' --- Self-Learning ---');
1888
+ console.log(` Embeddings: ${embCount}/${archiveCount} entries have vector embeddings`);
1889
+ console.log(` Avg conf: ${(avgConf * 100).toFixed(1)}% (decay: -0.5%/hr, boost: +3%/access)`);
1890
+ console.log(` Low conf: ${lowConf} entries below 30% (pruned at 15%)`);
1891
+ console.log(` Semantic: ${embCount > 0 ? 'enabled (cross-session search)' : 'pending (embeddings generating)'}`);
1892
+ } catch { /* stats are non-critical */ }
1893
+ }
1894
+
1895
+ // Autopilot status
1896
+ console.log('');
1897
+ console.log(' --- Context Autopilot ---');
1898
+ console.log(` Enabled: ${AUTOPILOT_ENABLED}`);
1899
+ console.log(` Window: ${formatTokens(CONTEXT_WINDOW_TOKENS)} tokens`);
1900
+ console.log(` Warn at: ${(AUTOPILOT_WARN_PCT * 100).toFixed(0)}%`);
1901
+ console.log(` Prune at: ${(AUTOPILOT_PRUNE_PCT * 100).toFixed(0)}%`);
1902
+ console.log(` Compaction: LOSSLESS (archive before, restore after)`);
1903
+
1904
+ const apState = loadAutopilotState();
1905
+ if (apState.sessionId) {
1906
+ const pct = apState.lastPercentage || 0;
1907
+ const bar = buildProgressBar(pct);
1908
+ console.log(` Current: ${bar} ${(pct * 100).toFixed(1)}% (~${formatTokens(apState.lastTokenEstimate)} tokens)`);
1909
+ console.log(` Prune cycles: ${apState.pruneCount}`);
1910
+ if (apState.history.length >= 2) {
1911
+ const first = apState.history[0];
1912
+ const last = apState.history[apState.history.length - 1];
1913
+ const growthRate = (last.pct - first.pct) / apState.history.length;
1914
+ if (growthRate > 0) {
1915
+ const turnsLeft = Math.ceil((1.0 - pct) / growthRate);
1916
+ console.log(` Est. runway: ~${turnsLeft} turns until prune threshold`);
1917
+ }
1918
+ }
1919
+ }
1920
+
1921
+ if (sessions.length > 0) {
1922
+ console.log('\n Recent sessions:');
1923
+ for (const s of sessions.slice(0, 10)) {
1924
+ console.log(` - ${s.session_id}: ${s.cnt} turns`);
1925
+ }
1926
+ }
1927
+
1928
+ console.log('');
1929
+ await backend.shutdown();
1930
+ }
1931
+
1932
+ // ============================================================================
1933
+ // Exports for testing
1934
+ // ============================================================================
1935
+
1936
+ export {
1937
+ SQLiteBackend,
1938
+ RuVectorBackend,
1939
+ JsonFileBackend,
1940
+ resolveBackend,
1941
+ getRuVectorConfig,
1942
+ createEmbedding,
1943
+ createHashEmbedding,
1944
+ getOnnxPipeline,
1945
+ EMBEDDING_DIM,
1946
+ hashContent,
1947
+ parseTranscript,
1948
+ extractTextContent,
1949
+ extractToolCalls,
1950
+ extractFilePaths,
1951
+ chunkTranscript,
1952
+ extractSummary,
1953
+ buildEntry,
1954
+ buildCompactInstructions,
1955
+ computeImportance,
1956
+ retrieveContextSmart,
1957
+ autoOptimize,
1958
+ crossSessionSearch,
1959
+ storeChunks,
1960
+ retrieveContext,
1961
+ readStdin,
1962
+ // Autopilot
1963
+ estimateContextTokens,
1964
+ loadAutopilotState,
1965
+ saveAutopilotState,
1966
+ runAutopilot,
1967
+ buildProgressBar,
1968
+ formatTokens,
1969
+ buildAutopilotReport,
1970
+ NAMESPACE,
1971
+ ARCHIVE_DB_PATH,
1972
+ ARCHIVE_JSON_PATH,
1973
+ COMPACT_INSTRUCTION_BUDGET,
1974
+ RETENTION_DAYS,
1975
+ AUTO_OPTIMIZE,
1976
+ AUTOPILOT_ENABLED,
1977
+ CONTEXT_WINDOW_TOKENS,
1978
+ AUTOPILOT_WARN_PCT,
1979
+ AUTOPILOT_PRUNE_PCT,
1980
+ };
1981
+
1982
+ // ============================================================================
1983
+ // Main
1984
+ // ============================================================================
1985
+
1986
+ const command = process.argv[2] || 'status';
1987
+
1988
+ try {
1989
+ switch (command) {
1990
+ case 'pre-compact': await doPreCompact(); break;
1991
+ case 'session-start': await doSessionStart(); break;
1992
+ case 'user-prompt-submit': await doUserPromptSubmit(); break;
1993
+ case 'status': await doStatus(); break;
1994
+ default:
1995
+ console.log('Usage: context-persistence-hook.mjs <pre-compact|session-start|user-prompt-submit|status>');
1996
+ process.exit(1);
1997
+ }
1998
+ } catch (err) {
1999
+ // Hooks must never crash Claude Code - fail silently
2000
+ process.stderr.write(`[ContextPersistence] Error (non-critical): ${err.message}\n`);
2001
+ }