@jungjaehoon/mama-core 2.4.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/README.md +76 -239
  2. package/db/migrations/030-case-first-memory-substrate.sql +13 -98
  3. package/db/migrations/034-add-connector-event-scope-columns.sql +9 -13
  4. package/db/migrations/039-add-connector-event-operator-ingest-seq.sql +9 -95
  5. package/db/migrations/062-refresh-connector-event-sequences.sql +9 -113
  6. package/db/migrations/063-refresh-legacy-connector-event-updates.sql +9 -32
  7. package/db/migrations/067-add-connector-event-source-entity-id.sql +9 -13
  8. package/db/migrations/069-create-registry-nodes.sql +52 -0
  9. package/db/migrations/070-record-identity.sql +35 -0
  10. package/db/migrations/071-service-operation-origins.sql +187 -0
  11. package/db/migrations/072-observation-versions.sql +29 -0
  12. package/db/migrations/073-registry-corrections.sql +45 -0
  13. package/db/migrations/074-work-graph-ref-kinds.sql +39 -0
  14. package/db/migrations/075-agent-judgments.sql +58 -0
  15. package/db/migrations/076-scoped-checkpoint-bindings.sql +15 -0
  16. package/db/migrations/077-legacy-record-kind.sql +6 -0
  17. package/db/migrations/078-source-ingest-commands.sql +18 -0
  18. package/db/migrations/079-twin-edge-relations.sql +43 -0
  19. package/db/migrations/080-restore-duration-days.sql +7 -0
  20. package/db/migrations/081-mailbox.sql +61 -0
  21. package/db/migrations/082-commitments-row-id.sql +12 -0
  22. package/db/migrations/083-observation-is-the-evidence.sql +49 -0
  23. package/db/migrations/084-scope-kind-is-not-the-cores-to-list.sql +34 -0
  24. package/db/migrations/085-a-migration-has-a-source.sql +37 -0
  25. package/db/migrations/086-the-mailbox-is-generic.sql +110 -0
  26. package/db/migrations/087-mailbox-payload.sql +5 -0
  27. package/db/migrations/088-native-input-journal.sql +14 -0
  28. package/db/migrations/089-native-turn-receipt-lookup.sql +3 -0
  29. package/db/migrations/090-model-run-native-inputs.sql +29 -0
  30. package/db/migrations/091-native-turn-results.sql +11 -0
  31. package/db/migrations/092-restore-decision-fts.sql +34 -0
  32. package/db/migrations/093-native-turn-primary-kind.sql +14 -0
  33. package/db/migrations/094-commitment-run-provenance.sql +9 -0
  34. package/db/migrations/095-model-run-native-input-identity.sql +39 -0
  35. package/db/migrations/096-drop-connector-event-index-fts.sql +9 -0
  36. package/db/migrations/097-native-input-json-validity.sql +37 -0
  37. package/db/migrations/098-workflow-memory-kind.sql +105 -0
  38. package/db/migrations/099-commitment-revision-graph.sql +2 -0
  39. package/dist/action-contracts.d.ts +212 -0
  40. package/dist/action-contracts.js +10 -0
  41. package/dist/api/catalog.d.ts +118 -0
  42. package/dist/api/catalog.js +1756 -0
  43. package/dist/api/dispatch.d.ts +76 -0
  44. package/dist/api/dispatch.js +372 -0
  45. package/dist/canonicalize.d.ts +2 -2
  46. package/dist/canonicalize.js +3 -3
  47. package/dist/client/client.d.ts +33 -0
  48. package/dist/client/client.js +123 -0
  49. package/dist/client/ipc.d.ts +81 -0
  50. package/dist/client/ipc.js +314 -0
  51. package/dist/db-adapter/base-adapter.d.ts +6 -66
  52. package/dist/db-adapter/base-adapter.js +6 -6
  53. package/dist/db-adapter/index.d.ts +5 -5
  54. package/dist/db-adapter/index.js +2 -5
  55. package/dist/db-adapter/node-sqlite-adapter.d.ts +85 -15
  56. package/dist/db-adapter/node-sqlite-adapter.js +1985 -570
  57. package/dist/db-manager.d.ts +53 -97
  58. package/dist/db-manager.js +121 -603
  59. package/dist/embedding/embedder.d.ts +132 -0
  60. package/dist/embedding/embedder.js +299 -0
  61. package/dist/identity/principal-repository.d.ts +2 -2
  62. package/dist/index.d.ts +43 -58
  63. package/dist/index.js +97 -145
  64. package/dist/{context-compile/channel-grant.d.ts → knowledge/access.d.ts} +12 -17
  65. package/dist/knowledge/access.js +663 -0
  66. package/dist/knowledge/case-errors.d.ts +31 -0
  67. package/dist/knowledge/case-errors.js +40 -0
  68. package/dist/{cases/search-rollup.d.ts → knowledge/case-search-rollup.d.ts} +5 -4
  69. package/dist/{cases/search-rollup.js → knowledge/case-search-rollup.js} +6 -8
  70. package/dist/{cases/store.d.ts → knowledge/case-store.d.ts} +3 -6
  71. package/dist/{cases/store.js → knowledge/case-store.js} +7 -42
  72. package/dist/{cases/timeline-range.d.ts → knowledge/case-timeline-range.d.ts} +3 -3
  73. package/dist/{cases/timeline-range.js → knowledge/case-timeline-range.js} +47 -25
  74. package/dist/{cases/types.d.ts → knowledge/case-types.d.ts} +8 -2
  75. package/dist/{cases/types.js → knowledge/case-types.js} +1 -1
  76. package/dist/knowledge/commitment-revision-migration.d.ts +4 -0
  77. package/dist/knowledge/commitment-revision-migration.js +50 -0
  78. package/dist/knowledge/commitments.d.ts +192 -0
  79. package/dist/knowledge/commitments.js +386 -0
  80. package/dist/knowledge/decision-edges.d.ts +70 -0
  81. package/dist/knowledge/decision-edges.js +123 -0
  82. package/dist/{agent-graph/types.d.ts → knowledge/graph-query.d.ts} +80 -70
  83. package/dist/knowledge/graph-query.js +1585 -0
  84. package/dist/knowledge/identity.d.ts +16 -0
  85. package/dist/knowledge/identity.js +22 -0
  86. package/dist/knowledge/index.d.ts +47 -0
  87. package/dist/knowledge/index.js +90 -0
  88. package/dist/knowledge/judgment-edge.d.ts +6 -0
  89. package/dist/knowledge/judgment-edge.js +22 -0
  90. package/dist/knowledge/judgments.d.ts +118 -0
  91. package/dist/knowledge/judgments.js +802 -0
  92. package/dist/knowledge/observations.d.ts +160 -0
  93. package/dist/knowledge/observations.js +449 -0
  94. package/dist/knowledge/question-type.d.ts +34 -0
  95. package/dist/knowledge/question-type.js +70 -0
  96. package/dist/{search → knowledge}/ranker-features.js +1 -1
  97. package/dist/{search → knowledge}/search-quality.d.ts +0 -1
  98. package/dist/knowledge/search.d.ts +38 -0
  99. package/dist/knowledge/search.js +91 -0
  100. package/dist/knowledge/source-ingest.d.ts +65 -0
  101. package/dist/knowledge/source-ingest.js +174 -0
  102. package/dist/{edges/types.d.ts → knowledge/twin-edge-types.d.ts} +31 -4
  103. package/dist/{edges/types.js → knowledge/twin-edge-types.js} +13 -2
  104. package/dist/knowledge/work-dates.d.ts +49 -0
  105. package/dist/knowledge/work-dates.js +120 -0
  106. package/dist/mama-api.d.ts +114 -778
  107. package/dist/mama-api.js +313 -2461
  108. package/dist/memory/api.d.ts +264 -24
  109. package/dist/memory/api.js +1633 -843
  110. package/dist/memory/bootstrap-builder.d.ts +2 -1
  111. package/dist/memory/bootstrap-builder.js +5 -5
  112. package/dist/memory/channel-summary-state-store.d.ts +3 -2
  113. package/dist/memory/channel-summary-state-store.js +53 -51
  114. package/dist/memory/channel-summary-store.d.ts +5 -3
  115. package/dist/memory/channel-summary-store.js +3 -8
  116. package/dist/memory/dashboard-read.d.ts +46 -0
  117. package/dist/memory/dashboard-read.js +94 -0
  118. package/dist/memory/event-store.d.ts +3 -3
  119. package/dist/memory/event-store.js +3 -10
  120. package/dist/memory/finding-store.d.ts +3 -2
  121. package/dist/memory/finding-store.js +2 -7
  122. package/dist/memory/graph-read.d.ts +52 -0
  123. package/dist/memory/graph-read.js +130 -0
  124. package/dist/memory/judgment-types.d.ts +394 -0
  125. package/dist/memory/judgment-types.js +16 -0
  126. package/dist/memory/provenance-audit.d.ts +5 -2
  127. package/dist/memory/provenance-audit.js +8 -14
  128. package/dist/memory/provenance-live.d.ts +141 -0
  129. package/dist/memory/provenance-live.js +409 -0
  130. package/dist/memory/provenance-query.d.ts +5 -4
  131. package/dist/memory/provenance-query.js +21 -29
  132. package/dist/memory/provenance-resolver.d.ts +191 -0
  133. package/dist/memory/provenance-resolver.js +155 -0
  134. package/dist/memory/provenance.d.ts +1 -11
  135. package/dist/memory/provenance.js +2 -33
  136. package/dist/memory/recall-sanitize.d.ts +55 -0
  137. package/dist/memory/recall-sanitize.js +144 -0
  138. package/dist/memory/secret-filter.d.ts +32 -0
  139. package/dist/memory/secret-filter.js +126 -0
  140. package/dist/memory/truth-store.d.ts +2 -1
  141. package/dist/memory/truth-store.js +1 -4
  142. package/dist/memory/types.d.ts +74 -24
  143. package/dist/memory/types.js +75 -5
  144. package/dist/memory/write-adapters.d.ts +98 -0
  145. package/dist/memory/write-adapters.js +242 -0
  146. package/dist/operations/owner-action-effects.d.ts +53 -0
  147. package/dist/operations/owner-action-effects.js +96 -0
  148. package/dist/provenance/source-ref.d.ts +1 -1
  149. package/dist/provenance/source-ref.js +0 -2
  150. package/dist/registry/corrections.d.ts +31 -0
  151. package/dist/registry/corrections.js +332 -0
  152. package/dist/registry/record-identity.d.ts +49 -0
  153. package/dist/registry/record-identity.js +133 -0
  154. package/dist/registry/store.d.ts +101 -0
  155. package/dist/registry/store.js +395 -0
  156. package/dist/registry/types.d.ts +28 -0
  157. package/dist/registry/types.js +5 -0
  158. package/dist/relevance-scorer.js +2 -2
  159. package/dist/runtime/agent-event-bus.d.ts +50 -0
  160. package/dist/runtime/agent-event-bus.js +101 -0
  161. package/dist/runtime/drivers/claude-cli-wrapper.d.ts +152 -0
  162. package/dist/runtime/drivers/claude-cli-wrapper.js +375 -0
  163. package/dist/runtime/drivers/claude-effort.d.ts +4 -0
  164. package/dist/runtime/drivers/claude-effort.js +16 -0
  165. package/dist/runtime/drivers/cli-arg-redaction.d.ts +2 -0
  166. package/dist/runtime/drivers/cli-arg-redaction.js +18 -0
  167. package/dist/runtime/drivers/cli-secret-redaction.d.ts +17 -0
  168. package/dist/runtime/drivers/cli-secret-redaction.js +126 -0
  169. package/dist/runtime/drivers/codex-app-server-process.d.ts +277 -0
  170. package/dist/runtime/drivers/codex-app-server-process.js +2342 -0
  171. package/dist/runtime/drivers/codex-auxiliary-tools.d.ts +17 -0
  172. package/dist/runtime/drivers/codex-auxiliary-tools.js +267 -0
  173. package/dist/runtime/drivers/codex-home.d.ts +20 -0
  174. package/dist/runtime/drivers/codex-home.js +674 -0
  175. package/dist/runtime/drivers/codex-thread-registry.d.ts +63 -0
  176. package/dist/runtime/drivers/codex-thread-registry.js +433 -0
  177. package/dist/runtime/drivers/persistent-cli-adapter.d.ts +160 -0
  178. package/dist/runtime/drivers/persistent-cli-adapter.js +396 -0
  179. package/dist/runtime/drivers/persistent-cli-process.d.ts +425 -0
  180. package/dist/runtime/drivers/persistent-cli-process.js +1529 -0
  181. package/dist/runtime/drivers/types.d.ts +456 -0
  182. package/dist/runtime/drivers/types.js +110 -0
  183. package/dist/runtime/mailbox.d.ts +223 -0
  184. package/dist/runtime/mailbox.js +582 -0
  185. package/dist/runtime/model-run-store.d.ts +28 -0
  186. package/dist/{model-runs/store.js → runtime/model-run-store.js} +81 -41
  187. package/dist/{model-runs/types.d.ts → runtime/model-run-types.d.ts} +33 -4
  188. package/dist/{model-runs/types.js → runtime/model-run-types.js} +1 -1
  189. package/dist/runtime/native-effect-observer.d.ts +22 -0
  190. package/dist/runtime/native-effect-observer.js +103 -0
  191. package/dist/runtime/native-input-journal.d.ts +46 -0
  192. package/dist/runtime/native-input-journal.js +308 -0
  193. package/dist/runtime/native-prompt.d.ts +118 -0
  194. package/dist/runtime/native-prompt.js +496 -0
  195. package/dist/runtime/native-session.d.ts +10 -0
  196. package/dist/{agent-graph/index.js → runtime/native-session.js} +9 -6
  197. package/dist/runtime/native-tool-trace-observer.d.ts +5 -0
  198. package/dist/runtime/native-tool-trace-observer.js +65 -0
  199. package/dist/runtime/native-turn.d.ts +272 -0
  200. package/dist/runtime/native-turn.js +1041 -0
  201. package/dist/runtime/operations.d.ts +236 -0
  202. package/dist/runtime/operations.js +565 -0
  203. package/dist/runtime/post-tool-handler.d.ts +74 -0
  204. package/dist/runtime/post-tool-handler.js +137 -0
  205. package/dist/runtime/prompt-layers.d.ts +126 -0
  206. package/dist/runtime/prompt-layers.js +239 -0
  207. package/dist/runtime/runtime-process.d.ts +102 -0
  208. package/dist/runtime/runtime-process.js +206 -0
  209. package/dist/runtime/runtime.d.ts +278 -0
  210. package/dist/runtime/runtime.js +563 -0
  211. package/dist/runtime/session-pool.d.ts +176 -0
  212. package/dist/runtime/session-pool.js +369 -0
  213. package/dist/runtime/stimulus-payload.d.ts +5 -0
  214. package/dist/runtime/stimulus-payload.js +50 -0
  215. package/dist/runtime/subagent-bridge.d.ts +24 -0
  216. package/dist/{agent-graph/types.js → runtime/subagent-bridge.js} +1 -1
  217. package/dist/runtime/text-completion.d.ts +22 -0
  218. package/dist/{agent-situation/types.js → runtime/text-completion.js} +1 -1
  219. package/dist/runtime/token-estimator.d.ts +25 -0
  220. package/dist/runtime/token-estimator.js +75 -0
  221. package/dist/runtime/tool-trace-store.d.ts +22 -0
  222. package/dist/{model-runs → runtime}/tool-trace-store.js +46 -27
  223. package/dist/runtime/trace-summary.d.ts +5 -0
  224. package/dist/runtime/trace-summary.js +74 -0
  225. package/dist/runtime/turn-text.d.ts +23 -0
  226. package/dist/runtime/turn-text.js +41 -0
  227. package/dist/storage/database.d.ts +67 -0
  228. package/dist/storage/database.js +158 -0
  229. package/package.json +20 -34
  230. package/dist/agent-graph/alias-write.d.ts +0 -3
  231. package/dist/agent-graph/alias-write.js +0 -234
  232. package/dist/agent-graph/entity-resolve.d.ts +0 -3
  233. package/dist/agent-graph/entity-resolve.js +0 -271
  234. package/dist/agent-graph/errors.d.ts +0 -4
  235. package/dist/agent-graph/errors.js +0 -11
  236. package/dist/agent-graph/graph-query.d.ts +0 -5
  237. package/dist/agent-graph/graph-query.js +0 -360
  238. package/dist/agent-graph/index.d.ts +0 -6
  239. package/dist/agent-situation/builder.d.ts +0 -8
  240. package/dist/agent-situation/builder.js +0 -239
  241. package/dist/agent-situation/cache-key.d.ts +0 -4
  242. package/dist/agent-situation/cache-key.js +0 -64
  243. package/dist/agent-situation/index.d.ts +0 -7
  244. package/dist/agent-situation/index.js +0 -28
  245. package/dist/agent-situation/packet-store.d.ts +0 -27
  246. package/dist/agent-situation/packet-store.js +0 -228
  247. package/dist/agent-situation/ranking-policy.d.ts +0 -18
  248. package/dist/agent-situation/ranking-policy.js +0 -75
  249. package/dist/agent-situation/source-readers.d.ts +0 -52
  250. package/dist/agent-situation/source-readers.js +0 -300
  251. package/dist/agent-situation/types.d.ts +0 -195
  252. package/dist/cases/wiki-page-index.d.ts +0 -37
  253. package/dist/cases/wiki-page-index.js +0 -201
  254. package/dist/config-loader.d.ts +0 -68
  255. package/dist/config-loader.js +0 -239
  256. package/dist/connectors/event-index.d.ts +0 -24
  257. package/dist/connectors/event-index.js +0 -245
  258. package/dist/connectors/raw-query.d.ts +0 -40
  259. package/dist/connectors/raw-query.js +0 -412
  260. package/dist/connectors/types.d.ts +0 -117
  261. package/dist/connectors/types.js +0 -3
  262. package/dist/context-compile/boundary-defaults.d.ts +0 -12
  263. package/dist/context-compile/boundary-defaults.js +0 -105
  264. package/dist/context-compile/channel-grant.js +0 -66
  265. package/dist/context-compile/compiler-policy.d.ts +0 -39
  266. package/dist/context-compile/compiler-policy.js +0 -147
  267. package/dist/context-compile/compiler.d.ts +0 -18
  268. package/dist/context-compile/compiler.js +0 -404
  269. package/dist/context-compile/index.d.ts +0 -10
  270. package/dist/context-compile/index.js +0 -26
  271. package/dist/context-compile/packet-store.d.ts +0 -8
  272. package/dist/context-compile/packet-store.js +0 -239
  273. package/dist/context-compile/ref.d.ts +0 -7
  274. package/dist/context-compile/ref.js +0 -102
  275. package/dist/context-compile/source-readers.d.ts +0 -70
  276. package/dist/context-compile/source-readers.js +0 -840
  277. package/dist/context-compile/types.d.ts +0 -155
  278. package/dist/context-compile/types.js +0 -11
  279. package/dist/context-compile/visibility.d.ts +0 -21
  280. package/dist/context-compile/visibility.js +0 -224
  281. package/dist/decision-tracker.d.ts +0 -206
  282. package/dist/decision-tracker.js +0 -481
  283. package/dist/edges/ref-validation.d.ts +0 -7
  284. package/dist/edges/ref-validation.js +0 -321
  285. package/dist/edges/store.d.ts +0 -8
  286. package/dist/edges/store.js +0 -129
  287. package/dist/embedding-client.d.ts +0 -28
  288. package/dist/embedding-client.js +0 -53
  289. package/dist/embedding-server/index.d.ts +0 -67
  290. package/dist/embedding-server/index.js +0 -405
  291. package/dist/embedding-server/mobile/auth.d.ts +0 -44
  292. package/dist/embedding-server/mobile/auth.js +0 -150
  293. package/dist/embedding-server/mobile/daemon.d.ts +0 -129
  294. package/dist/embedding-server/mobile/daemon.js +0 -313
  295. package/dist/embedding-server/mobile/output-parser.d.ts +0 -115
  296. package/dist/embedding-server/mobile/output-parser.js +0 -241
  297. package/dist/embedding-server/mobile/session-api.d.ts +0 -57
  298. package/dist/embedding-server/mobile/session-api.js +0 -261
  299. package/dist/embedding-server/mobile/session-manager.d.ts +0 -138
  300. package/dist/embedding-server/mobile/session-manager.js +0 -378
  301. package/dist/embedding-server/mobile/websocket-handler.d.ts +0 -137
  302. package/dist/embedding-server/mobile/websocket-handler.js +0 -486
  303. package/dist/embeddings.d.ts +0 -68
  304. package/dist/embeddings.js +0 -244
  305. package/dist/entities/audit-metrics.d.ts +0 -68
  306. package/dist/entities/audit-metrics.js +0 -124
  307. package/dist/entities/candidate-generator.d.ts +0 -8
  308. package/dist/entities/candidate-generator.js +0 -24
  309. package/dist/entities/entity-linked-decision-counts.d.ts +0 -2
  310. package/dist/entities/entity-linked-decision-counts.js +0 -29
  311. package/dist/entities/entity-list.d.ts +0 -22
  312. package/dist/entities/entity-list.js +0 -3
  313. package/dist/entities/entity-orphan-list.d.ts +0 -17
  314. package/dist/entities/entity-orphan-list.js +0 -3
  315. package/dist/entities/entity-search.d.ts +0 -20
  316. package/dist/entities/entity-search.js +0 -3
  317. package/dist/entities/errors.d.ts +0 -55
  318. package/dist/entities/errors.js +0 -100
  319. package/dist/entities/lineage-store.d.ts +0 -28
  320. package/dist/entities/lineage-store.js +0 -204
  321. package/dist/entities/normalization.d.ts +0 -14
  322. package/dist/entities/normalization.js +0 -63
  323. package/dist/entities/policy-store.d.ts +0 -8
  324. package/dist/entities/policy-store.js +0 -75
  325. package/dist/entities/policy-types.d.ts +0 -72
  326. package/dist/entities/policy-types.js +0 -59
  327. package/dist/entities/projection.d.ts +0 -8
  328. package/dist/entities/projection.js +0 -82
  329. package/dist/entities/provenance-query.d.ts +0 -32
  330. package/dist/entities/provenance-query.js +0 -3
  331. package/dist/entities/read-identity.d.ts +0 -31
  332. package/dist/entities/read-identity.js +0 -264
  333. package/dist/entities/recall-bridge.d.ts +0 -5
  334. package/dist/entities/recall-bridge.js +0 -171
  335. package/dist/entities/resolution-engine.d.ts +0 -8
  336. package/dist/entities/resolution-engine.js +0 -3
  337. package/dist/entities/rollback-preview.d.ts +0 -35
  338. package/dist/entities/rollback-preview.js +0 -3
  339. package/dist/entities/source-locator.d.ts +0 -3
  340. package/dist/entities/source-locator.js +0 -32
  341. package/dist/entities/store.d.ts +0 -69
  342. package/dist/entities/store.js +0 -544
  343. package/dist/entities/types.d.ts +0 -166
  344. package/dist/entities/types.js +0 -30
  345. package/dist/memory/extraction-prompt.d.ts +0 -4
  346. package/dist/memory/extraction-prompt.js +0 -89
  347. package/dist/memory/scope-backfill.d.ts +0 -14
  348. package/dist/memory/scope-backfill.js +0 -3
  349. package/dist/memory/scope-store.d.ts +0 -4
  350. package/dist/memory/scope-store.js +0 -9
  351. package/dist/memory-store.d.ts +0 -87
  352. package/dist/memory-store.js +0 -78
  353. package/dist/model-runs/store.d.ts +0 -13
  354. package/dist/model-runs/tool-trace-store.d.ts +0 -8
  355. package/dist/notification-manager.d.ts +0 -7
  356. package/dist/notification-manager.js +0 -12
  357. package/dist/outcome-tracker.d.ts +0 -151
  358. package/dist/outcome-tracker.js +0 -271
  359. package/dist/query-intent.d.ts +0 -23
  360. package/dist/query-intent.js +0 -114
  361. package/dist/runtime/trusted-provenance.d.ts +0 -3
  362. package/dist/runtime/trusted-provenance.js +0 -6
  363. package/dist/search/question-type.d.ts +0 -5
  364. package/dist/search/question-type.js +0 -41
  365. package/dist/test-utils.d.ts +0 -80
  366. package/dist/test-utils.js +0 -218
  367. package/dist/tier-validator.d.ts +0 -43
  368. package/dist/tier-validator.js +0 -159
  369. package/dist/time-formatter.d.ts +0 -25
  370. package/dist/time-formatter.js +0 -93
  371. /package/dist/{search → knowledge}/feedback-store.d.ts +0 -0
  372. /package/dist/{search → knowledge}/feedback-store.js +0 -0
  373. /package/dist/{search → knowledge}/ranker-features.d.ts +0 -0
  374. /package/dist/{search → knowledge}/ranker-rescore.d.ts +0 -0
  375. /package/dist/{search → knowledge}/ranker-rescore.js +0 -0
  376. /package/dist/{search → knowledge}/ranker-trainer.d.ts +0 -0
  377. /package/dist/{search → knowledge}/ranker-trainer.js +0 -0
  378. /package/dist/{search → knowledge}/search-quality.js +0 -0
@@ -0,0 +1,1529 @@
1
+ "use strict";
2
+ /**
3
+ * Persistent Claude CLI Process - Keeps Claude alive for multi-turn conversations
4
+ *
5
+ * WHY THIS EXISTS:
6
+ * - Previous approach spawned new Claude CLI process for each message
7
+ * - Each spawn required ~20K system prompt to be sent every time
8
+ * - Result: Slow responses (16-30 seconds per message)
9
+ *
10
+ * NEW ARCHITECTURE:
11
+ * - Keep Claude process alive using stream-json input/output
12
+ * - Send messages via stdin, receive responses via stdout
13
+ * - Session memory preserved in Claude's context
14
+ * - System prompt sent only once at process start
15
+ *
16
+ * STREAM-JSON PROTOCOL:
17
+ * Input (stdin):
18
+ * User message: {"type":"user","message":{"role":"user","content":"..."}}
19
+ * Tool result: {"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"xxx","content":"...","is_error":false}]}}
20
+ *
21
+ * Output (stdout):
22
+ * Init: {"type":"system","subtype":"init",...}
23
+ * Assistant: {"type":"assistant","message":{...}}
24
+ * Tool use: Content block with type="tool_use" in assistant message
25
+ * Result: {"type":"result","subtype":"success",...}
26
+ */
27
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ var desc = Object.getOwnPropertyDescriptor(m, k);
30
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
31
+ desc = { enumerable: true, get: function() { return m[k]; } };
32
+ }
33
+ Object.defineProperty(o, k2, desc);
34
+ }) : (function(o, m, k, k2) {
35
+ if (k2 === undefined) k2 = k;
36
+ o[k2] = m[k];
37
+ }));
38
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
39
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
40
+ }) : function(o, v) {
41
+ o["default"] = v;
42
+ });
43
+ var __importStar = (this && this.__importStar) || (function () {
44
+ var ownKeys = function(o) {
45
+ ownKeys = Object.getOwnPropertyNames || function (o) {
46
+ var ar = [];
47
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
48
+ return ar;
49
+ };
50
+ return ownKeys(o);
51
+ };
52
+ return function (mod) {
53
+ if (mod && mod.__esModule) return mod;
54
+ var result = {};
55
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
56
+ __setModuleDefault(result, mod);
57
+ return result;
58
+ };
59
+ })();
60
+ Object.defineProperty(exports, "__esModule", { value: true });
61
+ exports.PersistentProcessPool = exports.PersistentClaudeProcess = void 0;
62
+ exports.formatClaudeArgsForLog = formatClaudeArgsForLog;
63
+ const child_process_1 = require("child_process");
64
+ const crypto_1 = require("crypto");
65
+ const path_1 = require("path");
66
+ const fs_1 = require("fs");
67
+ const events_1 = require("events");
68
+ const types_js_1 = require("./types.js");
69
+ const debugLogger = __importStar(require("@jungjaehoon/mama-core/debug-logger"));
70
+ const cli_secret_redaction_js_1 = require("./cli-secret-redaction.js");
71
+ const cli_arg_redaction_js_1 = require("./cli-arg-redaction.js");
72
+ const claude_effort_js_1 = require("./claude-effort.js");
73
+ const { DebugLogger } = debugLogger;
74
+ const persistentLogger = new DebugLogger('PersistentCLI');
75
+ const poolLogger = new DebugLogger('ProcessPool');
76
+ /**
77
+ * Regex to strip lone Unicode surrogates that cause API 400 errors.
78
+ * Matches high surrogates not followed by a low surrogate, and
79
+ * low surrogates not preceded by a high surrogate.
80
+ */
81
+ // eslint-disable-next-line no-control-regex
82
+ const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
83
+ /** The native Claude Code delegation tool. `run_in_background: true` is the delegating shape. */
84
+ const BACKGROUND_AGENT_TOOL = 'Agent';
85
+ /** Bound on the child's final text carried to the host wake. Mirrors Codex's limit. */
86
+ const MAX_SUBAGENT_FINAL_TEXT_CHARS = 4_000;
87
+ /**
88
+ * How long a `task_notification` waits for the CLI's own follow-up turn before the host
89
+ * takes the wake itself. Measured: `init` follows the notification immediately.
90
+ */
91
+ const AUTONOMOUS_TURN_GRACE_MS = 5_000;
92
+ /** Identity, not history: finished children are dropped, live ones are few. */
93
+ const MAX_TRACKED_BACKGROUND_AGENTS = 50;
94
+ const MAX_STREAM_TOOL_RESULT_CHARS = 64 * 1024;
95
+ function stableJson(value) {
96
+ if (value === null || typeof value !== 'object') {
97
+ const serialized = JSON.stringify(value);
98
+ return serialized === undefined ? 'undefined' : serialized;
99
+ }
100
+ if (Array.isArray(value)) {
101
+ return `[${value.map((item) => stableJson(item)).join(',')}]`;
102
+ }
103
+ const record = value;
104
+ return `{${Object.keys(record)
105
+ .sort()
106
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
107
+ .join(',')}}`;
108
+ }
109
+ function normalizeStreamToolResultContent(content) {
110
+ const normalized = typeof content === 'string'
111
+ ? content
112
+ : Array.isArray(content)
113
+ ? content
114
+ .filter((block) => block.type === 'text' && typeof block.text === 'string')
115
+ .map((block) => block.text)
116
+ .join('\n')
117
+ : '';
118
+ return normalized.slice(0, MAX_STREAM_TOOL_RESULT_CHARS);
119
+ }
120
+ /**
121
+ * PersistentClaudeProcess - Manages a single long-lived Claude CLI process
122
+ *
123
+ * Features:
124
+ * - Keeps Claude process alive for multiple messages
125
+ * - Uses stream-json for bidirectional communication
126
+ * - Handles tool execution via Gateway Tools
127
+ * - Auto-restarts on process death
128
+ */
129
+ class PersistentClaudeProcess extends events_1.EventEmitter {
130
+ process = null;
131
+ options;
132
+ state = 'dead';
133
+ outputBuffer = '';
134
+ currentCallbacks = null;
135
+ /**
136
+ * The most recent request that offered a follow-up sink. A CLI turn that no stdin request
137
+ * and no tracked child explains (a grandchild's task notification, measured 2026-09-10)
138
+ * still answers SOMEONE: the last request that asked.
139
+ */
140
+ lastFollowUp;
141
+ currentResolve = null;
142
+ currentReject = null;
143
+ currentInputUuid = null;
144
+ inputAcknowledged = false;
145
+ requestTimeoutHandle = null;
146
+ toolUseBlocks = [];
147
+ promptToolExchanges = new Map();
148
+ completedToolExchanges = [];
149
+ awaitingToolResults = false;
150
+ pendingToolUseStartedAt = null;
151
+ accumulatedText = '';
152
+ startPromise = null;
153
+ onTokenUsage;
154
+ /** Live + just-finished native background Agents, keyed by their `Agent` tool_use id. */
155
+ backgroundAgents = new Map();
156
+ /**
157
+ * True once a turn's `result` resolved while a background child was still running. From
158
+ * then until the next stdin request, unattributed assistant/user events on the stream
159
+ * belong to that child, not to a parent turn that no longer exists.
160
+ */
161
+ parentTurnEnded = false;
162
+ /** Set between a `task_notification` and the CLI's own follow-up turn. */
163
+ pendingNotification = null;
164
+ /** The CLI-initiated turn currently running with no stdin request behind it. */
165
+ autonomousTurn = null;
166
+ /**
167
+ * Resolve the effective request timeout in ms.
168
+ * 0 = unlimited (no timeout). Returns the configured or default value.
169
+ */
170
+ _getRequestTimeoutMs() {
171
+ if (this.options.requestTimeout !== undefined && this.options.requestTimeout !== null) {
172
+ return Math.max(0, this.options.requestTimeout);
173
+ }
174
+ return Math.max(0, this.options.timeouts?.requestMs ?? 120_000);
175
+ }
176
+ constructor(options) {
177
+ super();
178
+ this.options = options;
179
+ this.onTokenUsage = options.onTokenUsage;
180
+ // Register default error handler to prevent Node crash if no listeners attached
181
+ this.on('error', (err) => {
182
+ console.error('[PersistentCLI] Unhandled error event:', err);
183
+ });
184
+ }
185
+ /**
186
+ * Start the Claude CLI process
187
+ *
188
+ * Note: The CLI only emits the 'init' event after receiving the first user message.
189
+ * So we don't wait for init here - we just start the process and let it run.
190
+ * The first sendMessage() call will handle init as part of its response flow.
191
+ */
192
+ async start() {
193
+ // Serialize concurrent start() calls — if already starting, wait for that to finish
194
+ if (this.startPromise) {
195
+ return this.startPromise;
196
+ }
197
+ if (this.state !== 'dead') {
198
+ persistentLogger.info(`[PersistentCLI] Process already in state: ${this.state}`);
199
+ return;
200
+ }
201
+ this.startPromise = this.doStart();
202
+ try {
203
+ await this.startPromise;
204
+ }
205
+ finally {
206
+ this.startPromise = null;
207
+ }
208
+ }
209
+ async doStart() {
210
+ this.state = 'starting';
211
+ persistentLogger.info(`[PersistentCLI] Starting process for session: ${this.options.sessionId}`);
212
+ const args = this.buildArgs();
213
+ persistentLogger.info(`[PersistentCLI] Spawning: claude ${formatClaudeArgsForLog(args).join(' ')}`);
214
+ // Clean environment: Remove conflicting MAMA_* variables before merging
215
+ const cleanEnv = { ...(this.options.processEnv ?? process.env) };
216
+ const processOptionsEnv = { ...(this.options.env || {}) };
217
+ if (this.options.env) {
218
+ // If we're setting MAMA_DISABLE_HOOKS, remove MAMA_HOOK_FEATURES
219
+ if ('MAMA_DISABLE_HOOKS' in this.options.env) {
220
+ delete cleanEnv.MAMA_HOOK_FEATURES;
221
+ }
222
+ // If we're setting MAMA_HOOK_FEATURES, remove MAMA_DISABLE_HOOKS
223
+ if ('MAMA_HOOK_FEATURES' in this.options.env) {
224
+ delete cleanEnv.MAMA_DISABLE_HOOKS;
225
+ }
226
+ }
227
+ // ============================================================
228
+ // ⚠️ MAMA OS AGENT ISOLATION — DO NOT MODIFY ⚠️
229
+ // ============================================================
230
+ // MAMA OS agents must operate only within the .mama scope, not globally.
231
+ //
232
+ // WHY: Claude Code CLI traverses upward from cwd to find CLAUDE.md.
233
+ // cwd = the user's home → ~/CLAUDE.md gets injected (their personal config leaks
234
+ // into the agent)
235
+ // cwd = <host workspace> + git boundary → traversal stops here
236
+ //
237
+ // HOW: Create <host workspace>/.git/HEAD so Claude Code treats it as a git repo root.
238
+ // This prevents Claude Code from searching for CLAUDE.md above this directory.
239
+ //
240
+ // FILE ACCESS: cwd only restricts CLAUDE.md discovery. If --dangerously-skip-permissions
241
+ // is enabled, the agent can still access all files on the system.
242
+ //
243
+ // Pointing cwd back at the user's home, or removing the git boundary, will cause
244
+ // ~/CLAUDE.md + global plugins to be re-injected every turn, wasting tokens.
245
+ // (This driver does not know where that home is, and must not: the workspace is
246
+ // the caller's to state.)
247
+ // ============================================================
248
+ const workspaceDir = this.options.workspaceDir;
249
+ if (!(0, fs_1.existsSync)(workspaceDir)) {
250
+ (0, fs_1.mkdirSync)(workspaceDir, { recursive: true });
251
+ }
252
+ const gitDir = (0, path_1.join)(workspaceDir, '.git');
253
+ if (!(0, fs_1.existsSync)(gitDir)) {
254
+ (0, fs_1.mkdirSync)(gitDir, { recursive: true });
255
+ }
256
+ const headFile = (0, path_1.join)(gitDir, 'HEAD');
257
+ if (!(0, fs_1.existsSync)(headFile)) {
258
+ (0, fs_1.writeFileSync)(headFile, 'ref: refs/heads/main\n');
259
+ }
260
+ const stderrRedactor = new cli_secret_redaction_js_1.SecretRedactingStream((0, cli_secret_redaction_js_1.claudeConfiguredSecrets)(this.options.mcpConfigPath, {
261
+ ...(this.options.processEnv ?? process.env),
262
+ ...this.options.env,
263
+ }), (safe) => {
264
+ if (safe.trim())
265
+ console.error(`[PersistentCLI:stderr] ${safe.trim()}`);
266
+ });
267
+ this.process = (0, child_process_1.spawn)('claude', args, {
268
+ stdio: ['pipe', 'pipe', 'pipe'],
269
+ // ⚠️ NEVER spawn in the user's home directory — it breaks agent isolation.
270
+ cwd: workspaceDir,
271
+ env: {
272
+ ...cleanEnv,
273
+ ...processOptionsEnv,
274
+ },
275
+ });
276
+ // Set up event handlers
277
+ this.process.stdout?.on('data', (chunk) => this.handleStdout(chunk));
278
+ this.process.stderr?.on('data', (chunk) => stderrRedactor.write(chunk));
279
+ this.process.stderr?.on('end', () => stderrRedactor.end());
280
+ this.process.on('close', (code) => {
281
+ stderrRedactor.end();
282
+ this.handleClose(code);
283
+ });
284
+ this.process.on('error', (error) => this.handleError(error));
285
+ // Don't wait for init - CLI only emits it after first user message
286
+ // Just wait a brief moment for the process to stabilize
287
+ await new Promise((resolve) => setTimeout(resolve, 500));
288
+ // Guard: Only set idle if still in starting state AND process is alive
289
+ // handleClose could have been called during the setTimeout above (→ state='dead')
290
+ // Note: TS can't track async state mutations from event handlers, so cast is needed
291
+ const currentState = this.state;
292
+ const pid = this.process?.pid;
293
+ if (currentState === 'starting' && pid && !this.process?.killed) {
294
+ this.state = 'idle';
295
+ persistentLogger.info(`[PersistentCLI] Process started and waiting for first message`);
296
+ }
297
+ else {
298
+ this.state = 'dead';
299
+ throw new Error('Process failed to start');
300
+ }
301
+ }
302
+ /**
303
+ * Build CLI arguments for stream-json mode
304
+ */
305
+ buildArgs() {
306
+ const args = [
307
+ '--print',
308
+ '--verbose',
309
+ '--input-format',
310
+ 'stream-json',
311
+ '--output-format',
312
+ 'stream-json',
313
+ '--replay-user-messages',
314
+ '--session-id',
315
+ this.options.sessionId,
316
+ // ============================================================
317
+ // ⚠️ BLOCK GLOBAL SETTINGS — DO NOT REMOVE ⚠️
318
+ // ============================================================
319
+ // Excluding 'user' from --setting-sources prevents loading ~/.claude/settings.json.
320
+ // That file contains enabledPlugins, so including 'user' causes global plugins
321
+ // (superpowers, bmad, etc.) to be injected every turn.
322
+ // --plugin-dir alone is NOT sufficient (it's additive, not an override).
323
+ // ============================================================
324
+ '--setting-sources',
325
+ 'project,local',
326
+ ];
327
+ if (this.options.model) {
328
+ args.push('--model', this.options.model);
329
+ }
330
+ if (this.options.systemPrompt) {
331
+ args.push('--system-prompt', this.options.systemPrompt);
332
+ }
333
+ // Hybrid mode: MCP + Gateway can both be enabled
334
+ if (this.options.mcpConfigPath) {
335
+ args.push('--mcp-config', this.options.mcpConfigPath);
336
+ args.push('--strict-mcp-config');
337
+ persistentLogger.info('MCP enabled:', this.options.mcpConfigPath);
338
+ }
339
+ if (this.options.useGatewayTools) {
340
+ persistentLogger.info('Gateway Tools mode enabled');
341
+ }
342
+ args.push(...(0, claude_effort_js_1.claudeEffortArgs)(this.options.model, this.options.effort));
343
+ if (this.options.permissionMode) {
344
+ args.push('--permission-mode', this.options.permissionMode);
345
+ }
346
+ if (this.options.dangerouslySkipPermissions) {
347
+ args.push('--dangerously-skip-permissions');
348
+ }
349
+ // Override built-in tool set (e.g. --tools "" to disable all)
350
+ if (this.options.tools !== undefined) {
351
+ args.push('--tools', this.options.tools);
352
+ }
353
+ // ============================================================
354
+ // ⚠️ PLUGIN ISOLATION — DO NOT REMOVE ⚠️
355
+ // ============================================================
356
+ // Points --plugin-dir to an empty directory so Claude Code cannot load
357
+ // global plugins. MAMA already includes everything needed via --system-prompt,
358
+ // so loading plugins would cause duplicate injection of the same content.
359
+ //
360
+ // Removing this causes skills/CLAUDE.md to be re-injected as system-reminder
361
+ // every turn, wasting thousands of tokens.
362
+ // ============================================================
363
+ const pluginDir = this.options.pluginDir ?? (0, path_1.join)(this.options.workspaceDir, '.empty-plugins');
364
+ if (!(0, fs_1.existsSync)(pluginDir)) {
365
+ (0, fs_1.mkdirSync)(pluginDir, { recursive: true });
366
+ }
367
+ args.push('--plugin-dir', pluginDir);
368
+ // Structural tool enforcement via CLI flags
369
+ if (this.options.allowedTools?.length) {
370
+ args.push('--allowedTools', ...this.options.allowedTools);
371
+ }
372
+ if (this.options.disallowedTools?.length) {
373
+ args.push('--disallowedTools', ...this.options.disallowedTools);
374
+ }
375
+ return args;
376
+ }
377
+ /**
378
+ * Send a user message to Claude
379
+ */
380
+ async sendMessage(content, callbacks, nativeInputId) {
381
+ if (nativeInputId !== undefined &&
382
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(nativeInputId)) {
383
+ throw new Error('Native input ID must be a UUID');
384
+ }
385
+ if (this.state === 'dead') {
386
+ await this.start();
387
+ }
388
+ // If another caller is starting, wait for it to finish
389
+ if (this.startPromise) {
390
+ await this.startPromise;
391
+ }
392
+ // Prevent concurrent requests during active processing
393
+ if (this.state === 'busy') {
394
+ throw new Error('Process is busy with another request');
395
+ }
396
+ if (this.state === 'starting' || this.state === 'dead') {
397
+ throw new Error(`Process is not ready (state: ${this.state})`);
398
+ }
399
+ this.state = 'busy';
400
+ this.awaitingToolResults = false;
401
+ this.pendingToolUseStartedAt = null;
402
+ this.currentCallbacks = callbacks || null;
403
+ this.currentInputUuid = nativeInputId ?? (0, crypto_1.randomUUID)();
404
+ this.inputAcknowledged = false;
405
+ this.toolUseBlocks = [];
406
+ this.promptToolExchanges.clear();
407
+ this.completedToolExchanges = [];
408
+ this.accumulatedText = '';
409
+ // A live child keeps running, but this turn's own events are the parent's again.
410
+ this.parentTurnEnded = false;
411
+ return new Promise((resolve, reject) => {
412
+ this.currentResolve = resolve;
413
+ this.currentReject = reject;
414
+ // Set request timeout (0 = unlimited, skip timeout entirely)
415
+ const timeoutMs = this._getRequestTimeoutMs();
416
+ if (timeoutMs > 0) {
417
+ this.requestTimeoutHandle = setTimeout(() => {
418
+ this.handleTimeout();
419
+ }, timeoutMs);
420
+ }
421
+ // Strip lone surrogates to prevent API 400 errors
422
+ const safeContent = content.replace(LONE_SURROGATE_RE, '');
423
+ const message = {
424
+ type: 'user',
425
+ uuid: this.currentInputUuid,
426
+ session_id: this.options.sessionId,
427
+ parent_tool_use_id: null,
428
+ message: {
429
+ role: 'user',
430
+ content: safeContent,
431
+ },
432
+ };
433
+ const jsonLine = JSON.stringify(message) + '\n';
434
+ persistentLogger.info(`[PersistentCLI] Sending message (${content.length} chars)`);
435
+ if (!this.process?.stdin?.writable) {
436
+ this.handleError(new Error('Process stdin not writable'));
437
+ return;
438
+ }
439
+ try {
440
+ callbacks?.onInputDispatch?.({
441
+ backend: 'claude',
442
+ sessionId: this.options.sessionId,
443
+ inputId: this.currentInputUuid,
444
+ });
445
+ }
446
+ catch (error) {
447
+ this.handleError(error instanceof Error ? error : new Error(String(error)));
448
+ return;
449
+ }
450
+ this.process.stdin.write(jsonLine, (err) => {
451
+ if (err) {
452
+ this.handleError(err);
453
+ }
454
+ });
455
+ });
456
+ }
457
+ /**
458
+ * Send a tool result back to Claude
459
+ */
460
+ async sendToolResult(toolUseId, result, isError = false, callbacks) {
461
+ return this.sendToolResults([{ tool_use_id: toolUseId, content: result, is_error: isError }], callbacks);
462
+ }
463
+ /**
464
+ * Send multiple tool results back to Claude in a single message.
465
+ * Required when Claude requests multiple tools in one turn.
466
+ */
467
+ async sendToolResults(results, callbacks) {
468
+ if (this.state === 'dead') {
469
+ throw new Error('Cannot send tool result: process is dead');
470
+ }
471
+ if (this.state !== 'idle') {
472
+ throw new Error(`Cannot send tool result in state: ${this.state}`);
473
+ }
474
+ this.state = 'busy';
475
+ this.awaitingToolResults = false;
476
+ this.pendingToolUseStartedAt = null;
477
+ this.currentCallbacks = callbacks || null;
478
+ this.toolUseBlocks = [];
479
+ this.promptToolExchanges.clear();
480
+ this.completedToolExchanges = [];
481
+ this.accumulatedText = '';
482
+ // A live child keeps running, but this turn's own events are the parent's again.
483
+ this.parentTurnEnded = false;
484
+ return new Promise((resolve, reject) => {
485
+ this.currentResolve = resolve;
486
+ this.currentReject = reject;
487
+ // Set request timeout (0 = unlimited, skip timeout entirely)
488
+ const timeoutMs = this._getRequestTimeoutMs();
489
+ if (timeoutMs > 0) {
490
+ this.requestTimeoutHandle = setTimeout(() => {
491
+ this.handleTimeout();
492
+ }, timeoutMs);
493
+ }
494
+ // Strip lone surrogates from tool results to prevent API 400 errors
495
+ const message = {
496
+ type: 'user',
497
+ message: {
498
+ role: 'user',
499
+ content: results.map((r) => ({
500
+ type: 'tool_result',
501
+ tool_use_id: r.tool_use_id,
502
+ content: r.content.replace(LONE_SURROGATE_RE, ''),
503
+ is_error: r.is_error,
504
+ })),
505
+ },
506
+ };
507
+ const jsonLine = JSON.stringify(message) + '\n';
508
+ persistentLogger.info(`[PersistentCLI] Sending ${results.length} tool_result(s): ${results.map((r) => r.tool_use_id).join(', ')}`);
509
+ if (!this.process?.stdin?.writable) {
510
+ this.handleError(new Error('Process stdin not writable'));
511
+ return;
512
+ }
513
+ this.process.stdin.write(jsonLine, (err) => {
514
+ if (err) {
515
+ this.handleError(err);
516
+ }
517
+ });
518
+ });
519
+ }
520
+ /**
521
+ * Handle stdout data
522
+ */
523
+ handleStdout(chunk) {
524
+ this.outputBuffer += chunk.toString();
525
+ // Process complete lines
526
+ const lines = this.outputBuffer.split('\n');
527
+ this.outputBuffer = lines.pop() || ''; // Keep incomplete line in buffer
528
+ for (const line of lines) {
529
+ if (!line.trim())
530
+ continue;
531
+ try {
532
+ const event = JSON.parse(line);
533
+ this.processEvent(event);
534
+ }
535
+ catch {
536
+ console.warn(`[PersistentCLI] Failed to parse JSON: ${line.substring(0, 100)}...`);
537
+ }
538
+ }
539
+ // Try to parse buffer as complete JSON (handles case where line doesn't end with newline)
540
+ // This is needed because Claude CLI may not flush a trailing newline when waiting for stdin
541
+ if (this.outputBuffer.trim()) {
542
+ try {
543
+ const event = JSON.parse(this.outputBuffer);
544
+ this.processEvent(event);
545
+ this.outputBuffer = ''; // Clear buffer after successful parse
546
+ }
547
+ catch {
548
+ // Not complete JSON yet, wait for more data
549
+ }
550
+ }
551
+ }
552
+ /**
553
+ * Process a parsed event from stdout
554
+ */
555
+ processEvent(event) {
556
+ switch (event.type) {
557
+ case 'system':
558
+ if (event.subtype === 'init') {
559
+ // A notification followed by `init` with no stdin request behind it is the CLI
560
+ // opening its OWN turn for the finished child. It must be surfaced BEFORE its
561
+ // assistant text arrives, so the host can bind it to a run.
562
+ if (this.pendingNotification && this.currentResolve === null) {
563
+ this.beginAutonomousTurn();
564
+ }
565
+ persistentLogger.info(`[PersistentCLI] Received init event`);
566
+ // Init event received (logged for debugging)
567
+ this.emit('init', event);
568
+ }
569
+ else if (event.subtype === 'hook_response') {
570
+ // Hook responses - could extract context if needed
571
+ persistentLogger.info(`[PersistentCLI] Hook response received`);
572
+ }
573
+ else if (event.subtype === 'task_started') {
574
+ this.recordBackgroundTaskStarted(event);
575
+ }
576
+ else if (event.subtype === 'task_notification') {
577
+ this.recordBackgroundTaskNotification(event);
578
+ }
579
+ break;
580
+ case 'assistant':
581
+ if (this.autonomousTurn) {
582
+ // The CLI's own follow-up turn: its text is that turn's answer, never this
583
+ // process's next stdin result, and its tool calls are not parent tool uses.
584
+ for (const block of Array.isArray(event.message?.content) ? event.message.content : []) {
585
+ if (block.type === 'text') {
586
+ this.autonomousTurn.text += block.text || '';
587
+ }
588
+ else if (block.type === 'tool_use') {
589
+ persistentLogger.info(`[PersistentCLI] autonomous turn tool use: ${block.name ?? 'unknown'}`);
590
+ }
591
+ }
592
+ break;
593
+ }
594
+ {
595
+ const child = this.childAgentFor(event);
596
+ if (child) {
597
+ this.absorbChildAssistant(child, event);
598
+ break;
599
+ }
600
+ }
601
+ // Process assistant message content
602
+ if (Array.isArray(event.message?.content)) {
603
+ for (const block of event.message.content) {
604
+ if (block.type === 'text') {
605
+ this.accumulatedText += block.text || '';
606
+ this.currentCallbacks?.onDelta?.(block.text || '');
607
+ }
608
+ else if (block.type === 'tool_use') {
609
+ const toolName = block.name ?? 'unknown';
610
+ const toolUse = {
611
+ type: 'tool_use',
612
+ id: block.id || `tool_${(0, crypto_1.randomUUID)()}`,
613
+ name: toolName,
614
+ input: block.input || {},
615
+ };
616
+ if (!this.recordToolUse(toolUse)) {
617
+ return;
618
+ }
619
+ }
620
+ }
621
+ }
622
+ break;
623
+ case 'user':
624
+ if (this.currentInputUuid !== null &&
625
+ event.uuid === this.currentInputUuid &&
626
+ event.session_id === this.options.sessionId &&
627
+ !event.parent_tool_use_id &&
628
+ event.message?.role === 'user' &&
629
+ (!event.origin || event.origin.kind === 'human')) {
630
+ if (!this.inputAcknowledged) {
631
+ this.inputAcknowledged = true;
632
+ const receipt = {
633
+ backend: 'claude',
634
+ sessionId: event.session_id,
635
+ inputId: this.currentInputUuid,
636
+ };
637
+ try {
638
+ this.currentCallbacks?.onAccepted?.(receipt);
639
+ }
640
+ catch (error) {
641
+ this.handleError(new types_js_1.NativeInputUncertainError('Native receipt observer failed after acceptance', receipt, error));
642
+ this.stop();
643
+ }
644
+ }
645
+ break;
646
+ }
647
+ if (this.autonomousTurn) {
648
+ break;
649
+ }
650
+ {
651
+ const child = this.childAgentFor(event);
652
+ if (child) {
653
+ for (const block of Array.isArray(event.message?.content)
654
+ ? event.message.content
655
+ : []) {
656
+ if (block.type === 'tool_result' && block.tool_use_id) {
657
+ const tool = child.nativeItems.get(block.tool_use_id);
658
+ if (tool && !tool.completed) {
659
+ tool.completed = true;
660
+ child.onToolComplete?.(tool.name, block.tool_use_id, block.is_error === true);
661
+ }
662
+ }
663
+ }
664
+ break;
665
+ }
666
+ }
667
+ if (Array.isArray(event.message?.content)) {
668
+ for (const block of event.message.content) {
669
+ if (block.type === 'tool_result' && !this.recordToolResult(block)) {
670
+ return;
671
+ }
672
+ }
673
+ }
674
+ break;
675
+ case 'result':
676
+ // A CLI-initiated turn's result belongs to THAT turn. It must never resolve a
677
+ // stdin request (its own, or a later one) and must not clear its timeout.
678
+ if (this.autonomousTurn) {
679
+ this.finishAutonomousTurn(event);
680
+ break;
681
+ }
682
+ if (this.currentResolve === null && this.toolUseBlocks.length === 0) {
683
+ // A finished CLI turn nobody requested and no tracked child explains (e.g. a
684
+ // grandchild's task notification). A turn still waiting on host tool results is
685
+ // not finished and keeps the normal path; a finished one is an answer for the
686
+ // last request that asked.
687
+ const text = event.result || '';
688
+ persistentLogger.info(`[PersistentCLI] unrequested CLI turn ended (${event.duration_ms ?? 0}ms); ` +
689
+ (this.lastFollowUp
690
+ ? 'handing its text to the last request'
691
+ : 'no request to hand it to'));
692
+ this.lastFollowUp?.({
693
+ agentThreadId: 'untracked',
694
+ agentPath: 'cli-turn',
695
+ itemId: 'untracked',
696
+ text,
697
+ isError: event.subtype !== 'success',
698
+ });
699
+ break;
700
+ }
701
+ if (this.currentInputUuid && this.currentCallbacks?.onAccepted && !this.inputAcknowledged) {
702
+ this.handleError(new types_js_1.NativeInputUncertainError('Native input acknowledgement was not observed', {
703
+ backend: 'claude',
704
+ sessionId: this.options.sessionId,
705
+ inputId: this.currentInputUuid,
706
+ }));
707
+ this.stop();
708
+ break;
709
+ }
710
+ // Request complete
711
+ this.clearRequestTimeout();
712
+ if (event.subtype === 'success') {
713
+ const unresolvedToolUses = [...this.toolUseBlocks];
714
+ const completedToolExchanges = [...this.completedToolExchanges];
715
+ const hasToolUse = unresolvedToolUses.length > 0;
716
+ const result = {
717
+ response: event.result || this.accumulatedText,
718
+ session_id: event.session_id || this.options.sessionId,
719
+ cost_usd: event.total_cost_usd,
720
+ duration_ms: event.duration_ms,
721
+ usage: {
722
+ input_tokens: event.usage?.input_tokens || 0,
723
+ output_tokens: event.usage?.output_tokens || 0,
724
+ cache_creation_input_tokens: event.usage?.cache_creation_input_tokens,
725
+ cache_read_input_tokens: event.usage?.cache_read_input_tokens,
726
+ },
727
+ toolUseBlocks: hasToolUse ? unresolvedToolUses : undefined,
728
+ hasToolUse,
729
+ completedToolExchanges,
730
+ };
731
+ // Record token usage
732
+ if (this.onTokenUsage) {
733
+ try {
734
+ this.onTokenUsage({
735
+ channel_key: this.options.channelKey || 'cli',
736
+ agent_id: this.options.agentId || undefined,
737
+ input_tokens: event.usage?.input_tokens || 0,
738
+ output_tokens: event.usage?.output_tokens || 0,
739
+ cache_read_tokens: event.usage?.cache_read_input_tokens || 0,
740
+ cost_usd: event.total_cost_usd || undefined,
741
+ });
742
+ }
743
+ catch {
744
+ /* ignore recording errors */
745
+ }
746
+ }
747
+ persistentLogger.info(`[PersistentCLI] Request complete (${event.duration_ms}ms, ${unresolvedToolUses.length} unresolved tools, ${completedToolExchanges.length} completed tools)`);
748
+ this.currentCallbacks?.onFinal?.({
749
+ content: result.response,
750
+ toolUseBlocks: unresolvedToolUses,
751
+ });
752
+ this.state = 'idle';
753
+ this.awaitingToolResults = hasToolUse;
754
+ this.pendingToolUseStartedAt = hasToolUse ? Date.now() : null;
755
+ // The parent turn is over but a background child is still running: from here on
756
+ // the stream is the child's until the next stdin request.
757
+ this.parentTurnEnded = this.hasLiveBackgroundAgents();
758
+ this.currentResolve?.(result);
759
+ this.resetRequestState();
760
+ this.emit('idle'); // F7: Trigger message queue drain (after resolve/cleanup)
761
+ }
762
+ else if (event.is_error) {
763
+ const error = new Error(event.error || 'Unknown error');
764
+ this.currentCallbacks?.onError?.(error);
765
+ this.state = 'idle';
766
+ this.awaitingToolResults = false;
767
+ this.pendingToolUseStartedAt = null;
768
+ this.currentReject?.(error);
769
+ this.resetRequestState();
770
+ this.emit('idle'); // F7: Trigger message queue drain (after reject/cleanup)
771
+ }
772
+ break;
773
+ case 'error': {
774
+ this.clearRequestTimeout();
775
+ const error = new Error(event.error || 'Unknown error');
776
+ this.currentCallbacks?.onError?.(error);
777
+ this.state = 'idle';
778
+ this.awaitingToolResults = false;
779
+ this.pendingToolUseStartedAt = null;
780
+ this.currentReject?.(error);
781
+ this.resetRequestState();
782
+ this.emit('idle'); // F7: Trigger message queue drain (after reject/cleanup)
783
+ break;
784
+ }
785
+ }
786
+ }
787
+ recordToolUse(toolUse) {
788
+ const fingerprint = stableJson({ name: toolUse.name, input: toolUse.input });
789
+ const current = this.promptToolExchanges.get(toolUse.id);
790
+ if (current) {
791
+ if (current.toolResult) {
792
+ this.failToolStreamProtocol(`Completed tool id was reused: ${toolUse.id}`);
793
+ return false;
794
+ }
795
+ if (current.toolUseFingerprint !== fingerprint) {
796
+ this.failToolStreamProtocol(`Conflicting tool_use payload for id: ${toolUse.id}`);
797
+ return false;
798
+ }
799
+ return true;
800
+ }
801
+ this.promptToolExchanges.set(toolUse.id, {
802
+ toolUse,
803
+ toolUseFingerprint: fingerprint,
804
+ });
805
+ this.toolUseBlocks.push(toolUse);
806
+ // Callback-only metadata pairs overlapping native calls by provider identity;
807
+ // preserve the original tool input and exchange fingerprint unchanged.
808
+ this.currentCallbacks?.onToolUse?.(toolUse.name, {
809
+ ...toolUse.input,
810
+ nativeToolUseId: toolUse.id,
811
+ });
812
+ persistentLogger.info(`[PersistentCLI] Tool use: ${toolUse.name}`);
813
+ // Every Agent spawn is tracked from here; its LAUNCH RESULT decides whether it is a
814
+ // background child ("Async agent launched") or a synchronous one (dropped on result).
815
+ // Measured 2026-09-10 21:06 KST: the CLI launched async without run_in_background, the
816
+ // flag-gated tracker missed it, and the child lost the run context mid-flight.
817
+ if (toolUse.name === BACKGROUND_AGENT_TOOL) {
818
+ this.trackBackgroundAgent(toolUse);
819
+ }
820
+ return true;
821
+ }
822
+ // ─── Native background Agent observation ───────────────────────────────
823
+ //
824
+ // The CLI gives one stream. A `run_in_background: true` Agent tool_use, the
825
+ // "Async agent launched" tool_result that names the child, `system/task_*` events, the
826
+ // child's own tool calls, its final text, `task_notification`, and - measured - a whole
827
+ // turn the CLI opens by itself all arrive on it. These helpers separate those without
828
+ // inventing anything the stream does not say.
829
+ trackBackgroundAgent(toolUse) {
830
+ const description = toolUse.input?.description;
831
+ const state = {
832
+ itemId: toolUse.id,
833
+ agentPath: typeof description === 'string' && description.trim() ? description : toolUse.id,
834
+ agentId: null,
835
+ taskId: null,
836
+ spawnObserved: false,
837
+ startFired: false,
838
+ notificationSeen: false,
839
+ completed: false,
840
+ finalText: '',
841
+ onToolUse: this.currentCallbacks?.onToolUse,
842
+ onToolComplete: this.currentCallbacks?.onToolComplete,
843
+ nativeItems: new Map(),
844
+ onSubagentStart: this.currentCallbacks?.onSubagentStart,
845
+ onFollowUp: this.currentCallbacks?.onFollowUp,
846
+ };
847
+ this.backgroundAgents.set(toolUse.id, state);
848
+ this.pruneBackgroundAgents();
849
+ }
850
+ pruneBackgroundAgents() {
851
+ if (this.backgroundAgents.size <= MAX_TRACKED_BACKGROUND_AGENTS)
852
+ return;
853
+ for (const [id, state] of this.backgroundAgents) {
854
+ if (this.backgroundAgents.size <= MAX_TRACKED_BACKGROUND_AGENTS)
855
+ break;
856
+ if (state.completed)
857
+ this.backgroundAgents.delete(id);
858
+ }
859
+ }
860
+ /** The newest tracked agent matching a predicate; `system` task events carry no item id. */
861
+ latestBackgroundAgent(match) {
862
+ let found = null;
863
+ for (const state of this.backgroundAgents.values()) {
864
+ if (match(state))
865
+ found = state;
866
+ }
867
+ return found;
868
+ }
869
+ taskIdentityOf(event) {
870
+ return (event.task_id ?? event.taskId ?? event.task?.id ?? event.agent_id ?? event.agentId ?? null);
871
+ }
872
+ recordBackgroundTaskStarted(event) {
873
+ const state = this.latestBackgroundAgent((candidate) => !candidate.spawnObserved);
874
+ if (!state) {
875
+ console.warn('[PersistentCLI] task_started with no tracked background Agent tool_use');
876
+ return;
877
+ }
878
+ state.spawnObserved = true;
879
+ state.taskId = this.taskIdentityOf(event) ?? state.taskId;
880
+ // The spawn is not reported here: `task_started` precedes the launch tool_result, and
881
+ // that result is what names the child. The task id stays as the fallback identity.
882
+ }
883
+ /** Lift the child's id out of the "Async agent launched" tool_result text. */
884
+ recordBackgroundLaunchResult(state, content) {
885
+ if (!/async agent launched/i.test(content)) {
886
+ // A synchronous child: its result IS its answer, inside the parent turn. Nothing to hold.
887
+ this.backgroundAgents.delete(state.itemId);
888
+ return;
889
+ }
890
+ const match = /agentId["'\s:=]+([A-Za-z0-9_-]{4,})/.exec(content);
891
+ if (match) {
892
+ state.agentId = match[1];
893
+ }
894
+ else if (!state.taskId) {
895
+ console.warn(`[PersistentCLI] background Agent launch result carried no agentId (item=${state.itemId})`);
896
+ }
897
+ this.fireSubagentStart(state);
898
+ }
899
+ fireSubagentStart(state) {
900
+ if (state.startFired)
901
+ return;
902
+ const agentThreadId = state.agentId ?? state.taskId;
903
+ if (!agentThreadId)
904
+ return;
905
+ state.startFired = true;
906
+ try {
907
+ state.onSubagentStart?.({
908
+ agentThreadId,
909
+ agentPath: state.agentPath,
910
+ itemId: state.itemId,
911
+ });
912
+ }
913
+ catch (error) {
914
+ console.error(`[PersistentCLI] onSubagentStart callback failed: ${error instanceof Error ? error.message : String(error)}`);
915
+ }
916
+ const emitted = {
917
+ kind: 'started',
918
+ agentThreadId,
919
+ agentPath: state.agentPath,
920
+ itemId: state.itemId,
921
+ wakeRequired: false,
922
+ };
923
+ this.emit('subagent', emitted);
924
+ }
925
+ /**
926
+ * Which background child an assistant/user event belongs to, or null for the parent.
927
+ *
928
+ * `parent_tool_use_id` is authoritative when the CLI stamps it. Without it the only
929
+ * honest signal is position: the parent turn already ended and exactly one child is
930
+ * live. Two live children and no stamp is ambiguous, so the event stays with the parent
931
+ * and says so rather than being silently attributed.
932
+ */
933
+ childAgentFor(event) {
934
+ const stamped = event.parent_tool_use_id;
935
+ if (typeof stamped === 'string' && stamped) {
936
+ return this.backgroundAgents.get(stamped) ?? null;
937
+ }
938
+ if (!this.parentTurnEnded)
939
+ return null;
940
+ const live = [...this.backgroundAgents.values()].filter((state) => !state.completed);
941
+ if (live.length === 1)
942
+ return live[0];
943
+ if (live.length > 1) {
944
+ console.warn(`[PersistentCLI] ${live.length} live background agents and no parent_tool_use_id; ` +
945
+ 'event left with the parent');
946
+ }
947
+ return null;
948
+ }
949
+ absorbChildAssistant(child, event) {
950
+ for (const block of Array.isArray(event.message?.content) ? event.message.content : []) {
951
+ if (block.type === 'text') {
952
+ // Only the child's LAST answer matters to the wake, but the stream does not mark
953
+ // it, so text accumulates bounded, like the Codex child's final_answer buffer.
954
+ child.finalText = (child.finalText + (block.text || '')).slice(0, MAX_SUBAGENT_FINAL_TEXT_CHARS);
955
+ }
956
+ else if (block.type === 'tool_use') {
957
+ // Observed, never counted as a parent tool use awaiting a host result. The call
958
+ // itself already reached the MCP server under the parent's context key.
959
+ if (!block.id || child.nativeItems.has(block.id))
960
+ continue;
961
+ child.nativeItems.set(block.id, { name: block.name ?? 'unknown', completed: false });
962
+ child.onToolUse?.(block.name ?? 'unknown', {
963
+ ...(block.input ?? {}),
964
+ nativeToolUseId: block.id,
965
+ subagentItemId: child.itemId,
966
+ });
967
+ }
968
+ }
969
+ }
970
+ recordBackgroundTaskNotification(event) {
971
+ const identity = this.taskIdentityOf(event);
972
+ const state = (identity
973
+ ? this.latestBackgroundAgent((candidate) => candidate.agentId === identity || candidate.taskId === identity)
974
+ : null) ?? this.latestBackgroundAgent((candidate) => !candidate.completed);
975
+ if (!state) {
976
+ console.warn('[PersistentCLI] task_notification with no tracked background Agent');
977
+ return;
978
+ }
979
+ if (state.notificationSeen)
980
+ return;
981
+ state.notificationSeen = true;
982
+ // The CLI may answer the notification itself. Give it that window; if it does not, the
983
+ // host takes the wake so a finished child is never dropped.
984
+ const timer = setTimeout(() => {
985
+ this.pendingNotification = null;
986
+ this.completeBackgroundAgent(state, true);
987
+ }, AUTONOMOUS_TURN_GRACE_MS);
988
+ timer.unref?.();
989
+ this.pendingNotification = { itemId: state.itemId, timer };
990
+ }
991
+ beginAutonomousTurn() {
992
+ const pending = this.pendingNotification;
993
+ if (!pending)
994
+ return;
995
+ clearTimeout(pending.timer);
996
+ this.pendingNotification = null;
997
+ const state = this.backgroundAgents.get(pending.itemId);
998
+ if (!state)
999
+ return;
1000
+ this.autonomousTurn = { state, text: '' };
1001
+ const event = {
1002
+ agentThreadId: state.agentId ?? state.taskId ?? state.itemId,
1003
+ agentPath: state.agentPath,
1004
+ itemId: state.itemId,
1005
+ };
1006
+ persistentLogger.info(`[PersistentCLI] autonomous turn opened for finished child ${event.agentThreadId}`);
1007
+ this.emit('autonomousTurn', event);
1008
+ }
1009
+ finishAutonomousTurn(event) {
1010
+ const active = this.autonomousTurn;
1011
+ if (!active)
1012
+ return;
1013
+ this.autonomousTurn = null;
1014
+ const state = active.state;
1015
+ const resultEvent = {
1016
+ agentThreadId: state.agentId ?? state.taskId ?? state.itemId,
1017
+ agentPath: state.agentPath,
1018
+ itemId: state.itemId,
1019
+ text: event.result || active.text,
1020
+ isError: event.is_error === true || event.subtype !== 'success',
1021
+ };
1022
+ // The CLI's own turn IS the owner reacting, so the host must not wake it again.
1023
+ this.completeBackgroundAgent(state, false);
1024
+ this.emit('autonomousTurnResult', resultEvent);
1025
+ // The answer belongs to the request that spawned the child: hand it back there.
1026
+ (state.onFollowUp ?? this.lastFollowUp)?.(resultEvent);
1027
+ }
1028
+ completeBackgroundAgent(state, wakeRequired, status = 'completed') {
1029
+ if (state.completed)
1030
+ return;
1031
+ state.completed = true;
1032
+ for (const [callId, item] of state.nativeItems) {
1033
+ if (item.completed)
1034
+ continue;
1035
+ item.completed = true;
1036
+ try {
1037
+ state.onToolComplete?.(item.name, callId, true, 'unknown');
1038
+ }
1039
+ catch {
1040
+ console.warn('[PersistentCLI] native observation failed');
1041
+ }
1042
+ }
1043
+ const emitted = {
1044
+ kind: 'completed',
1045
+ agentThreadId: state.agentId ?? state.taskId ?? state.itemId,
1046
+ agentPath: state.agentPath,
1047
+ itemId: state.itemId,
1048
+ // The stream reports a notification, not an exit code; a child cut off by a dead CLI
1049
+ // is `unknown`, never success.
1050
+ status,
1051
+ finalText: state.finalText,
1052
+ wakeRequired,
1053
+ };
1054
+ if (!this.hasLiveBackgroundAgents()) {
1055
+ this.parentTurnEnded = false;
1056
+ }
1057
+ this.emit('subagent', emitted);
1058
+ }
1059
+ /** Whether a spawned background child has not been reported finished yet. */
1060
+ hasLiveBackgroundAgents() {
1061
+ for (const state of this.backgroundAgents.values()) {
1062
+ if (!state.completed)
1063
+ return true;
1064
+ }
1065
+ return false;
1066
+ }
1067
+ recordToolResult(block) {
1068
+ const toolUseId = block.tool_use_id;
1069
+ if (!toolUseId) {
1070
+ this.failToolStreamProtocol('tool_result is missing tool_use_id');
1071
+ return false;
1072
+ }
1073
+ const current = this.promptToolExchanges.get(toolUseId);
1074
+ if (!current) {
1075
+ this.failToolStreamProtocol(`tool_result arrived before tool_use: ${toolUseId}`);
1076
+ return false;
1077
+ }
1078
+ const toolResult = {
1079
+ type: 'tool_result',
1080
+ tool_use_id: toolUseId,
1081
+ content: normalizeStreamToolResultContent(block.content),
1082
+ is_error: block.is_error === true,
1083
+ };
1084
+ const fingerprint = stableJson({
1085
+ content: toolResult.content,
1086
+ is_error: toolResult.is_error,
1087
+ });
1088
+ if (current.toolResult) {
1089
+ if (current.toolResultFingerprint !== fingerprint) {
1090
+ this.failToolStreamProtocol(`Conflicting tool_result payload for id: ${toolUseId}`);
1091
+ return false;
1092
+ }
1093
+ return true;
1094
+ }
1095
+ current.toolResult = toolResult;
1096
+ current.toolResultFingerprint = fingerprint;
1097
+ this.toolUseBlocks = this.toolUseBlocks.filter((toolUse) => toolUse.id !== toolUseId);
1098
+ this.completedToolExchanges.push({ toolUse: current.toolUse, toolResult });
1099
+ this.currentCallbacks?.onToolComplete?.(current.toolUse.name, toolUseId, toolResult.is_error === true);
1100
+ const backgroundAgent = this.backgroundAgents.get(toolUseId);
1101
+ if (backgroundAgent) {
1102
+ // "Async agent launched successfully…" - the one event that reliably names the child.
1103
+ this.recordBackgroundLaunchResult(backgroundAgent, toolResult.content);
1104
+ }
1105
+ return true;
1106
+ }
1107
+ failToolStreamProtocol(message) {
1108
+ const error = new types_js_1.ClaudeToolStreamProtocolError(message);
1109
+ this.currentCallbacks?.onError?.(error);
1110
+ this.handleError(error);
1111
+ }
1112
+ /**
1113
+ * Handle process close
1114
+ */
1115
+ handleClose(code) {
1116
+ persistentLogger.info(`[PersistentCLI] Process closed with code ${code}`);
1117
+ this.state = 'dead';
1118
+ this.process = null;
1119
+ this.awaitingToolResults = false;
1120
+ this.pendingToolUseStartedAt = null;
1121
+ // A child cannot outlive the CLI that ran it. Report the ones still open as unknown
1122
+ // rather than leaving the host waiting for a notification that can never arrive.
1123
+ if (this.pendingNotification) {
1124
+ clearTimeout(this.pendingNotification.timer);
1125
+ this.pendingNotification = null;
1126
+ }
1127
+ this.autonomousTurn = null;
1128
+ for (const state of [...this.backgroundAgents.values()]) {
1129
+ if (!state.completed) {
1130
+ this.completeBackgroundAgent(state, true, 'unknown');
1131
+ }
1132
+ }
1133
+ // Reject any pending request
1134
+ if (this.currentReject) {
1135
+ this.currentReject(new Error(`Process exited with code ${code}`));
1136
+ this.resetRequestState();
1137
+ }
1138
+ this.emit('close', code);
1139
+ }
1140
+ /**
1141
+ * Handle process error
1142
+ */
1143
+ handleError(error) {
1144
+ console.error(`[PersistentCLI] Process error:`, error.message);
1145
+ if (this.currentReject) {
1146
+ this.currentReject(error);
1147
+ this.resetRequestState();
1148
+ }
1149
+ // Transition to idle so subsequent requests aren't blocked
1150
+ if (this.state === 'busy') {
1151
+ this.state = 'idle';
1152
+ this.awaitingToolResults = false;
1153
+ this.pendingToolUseStartedAt = null;
1154
+ this.emit('idle');
1155
+ }
1156
+ this.emit('error', error);
1157
+ }
1158
+ /**
1159
+ * Handle request timeout
1160
+ */
1161
+ handleTimeout() {
1162
+ console.error(`[PersistentCLI] Request timeout — killing process to prevent zombie`);
1163
+ if (this.currentReject) {
1164
+ this.currentReject(new Error('Request timeout'));
1165
+ this.resetRequestState();
1166
+ }
1167
+ // ⚠️ Timed-out processes MUST be killed.
1168
+ // Setting state to 'idle' without killing leaves zombie processes consuming memory.
1169
+ // When SessionPool creates new sessions without cleaning up old processes,
1170
+ // Claude processes accumulate and exhaust system memory.
1171
+ if (this.process && !this.process.killed) {
1172
+ // `killed` turns true as soon as SIGTERM is sent, so escalation must watch the exit itself.
1173
+ const child = this.process;
1174
+ let exited = false;
1175
+ const markExited = () => {
1176
+ exited = true;
1177
+ };
1178
+ if (typeof child.once === 'function') {
1179
+ child.once('exit', markExited);
1180
+ child.once('close', markExited);
1181
+ }
1182
+ child.kill('SIGTERM');
1183
+ const forceKillTimer = setTimeout(() => {
1184
+ if (!exited)
1185
+ child.kill('SIGKILL');
1186
+ }, 3000);
1187
+ forceKillTimer.unref?.();
1188
+ }
1189
+ this.state = 'dead';
1190
+ this.awaitingToolResults = false;
1191
+ this.pendingToolUseStartedAt = null;
1192
+ this.emit('idle'); // F7: Trigger message queue drain (after cleanup)
1193
+ }
1194
+ /**
1195
+ * Clear request timeout
1196
+ */
1197
+ clearRequestTimeout() {
1198
+ if (this.requestTimeoutHandle) {
1199
+ clearTimeout(this.requestTimeoutHandle);
1200
+ this.requestTimeoutHandle = null;
1201
+ }
1202
+ }
1203
+ /**
1204
+ * Reset request state
1205
+ */
1206
+ resetRequestState() {
1207
+ this.clearRequestTimeout();
1208
+ // The request is over, but the runner may still answer it later (its own follow-up turn):
1209
+ // keep the sink the request offered.
1210
+ if (this.currentCallbacks?.onFollowUp)
1211
+ this.lastFollowUp = this.currentCallbacks.onFollowUp;
1212
+ this.currentCallbacks = null;
1213
+ this.currentResolve = null;
1214
+ this.currentReject = null;
1215
+ this.currentInputUuid = null;
1216
+ this.inputAcknowledged = false;
1217
+ this.toolUseBlocks = [];
1218
+ this.promptToolExchanges.clear();
1219
+ this.completedToolExchanges = [];
1220
+ this.accumulatedText = '';
1221
+ }
1222
+ /**
1223
+ * Stop the process
1224
+ */
1225
+ stop() {
1226
+ persistentLogger.info(`[PersistentCLI] Stopping process`);
1227
+ // Reject any pending request BEFORE resetting state
1228
+ // This ensures promises are resolved even if process is already dead
1229
+ if (this.currentReject) {
1230
+ this.currentReject(new Error('Process stopped by user'));
1231
+ }
1232
+ if (this.process) {
1233
+ const child = this.process;
1234
+ let exited = false;
1235
+ const markExited = () => {
1236
+ exited = true;
1237
+ };
1238
+ if (typeof child.once === 'function') {
1239
+ child.once('exit', markExited);
1240
+ child.once('close', markExited);
1241
+ }
1242
+ this.clearRequestTimeout();
1243
+ child.stdin?.end();
1244
+ child.kill('SIGTERM');
1245
+ const forceKillTimer = setTimeout(() => {
1246
+ if (!exited) {
1247
+ child.kill('SIGKILL');
1248
+ }
1249
+ }, 3000);
1250
+ forceKillTimer.unref();
1251
+ this.process = null;
1252
+ }
1253
+ this.state = 'dead';
1254
+ this.awaitingToolResults = false;
1255
+ this.pendingToolUseStartedAt = null;
1256
+ this.resetRequestState();
1257
+ }
1258
+ /**
1259
+ * Check if process is alive
1260
+ */
1261
+ isAlive() {
1262
+ return this.state !== 'dead';
1263
+ }
1264
+ /**
1265
+ * Check if process is ready for new messages
1266
+ */
1267
+ isReady() {
1268
+ return this.state === 'idle';
1269
+ }
1270
+ /**
1271
+ * True while Claude has requested tools and the host has not sent tool_result yet.
1272
+ * The process is technically idle during host-side tool execution, but it must not
1273
+ * be reclaimed because the next prompt depends on this live Claude context.
1274
+ */
1275
+ hasPendingToolUse() {
1276
+ return this.awaitingToolResults;
1277
+ }
1278
+ getPendingToolUseStartedAt() {
1279
+ return this.pendingToolUseStartedAt;
1280
+ }
1281
+ /**
1282
+ * Get current state
1283
+ */
1284
+ getState() {
1285
+ return this.state;
1286
+ }
1287
+ /**
1288
+ * Get session ID
1289
+ */
1290
+ getSessionId() {
1291
+ return this.options.sessionId;
1292
+ }
1293
+ }
1294
+ exports.PersistentClaudeProcess = PersistentClaudeProcess;
1295
+ function formatClaudeArgsForLog(args) {
1296
+ return (0, cli_arg_redaction_js_1.formatCliArgsForLog)(args);
1297
+ }
1298
+ /**
1299
+ * PersistentProcessPool - Manages multiple persistent Claude processes
1300
+ *
1301
+ * Features:
1302
+ * - One process per channel/session
1303
+ * - Automatic process lifecycle management
1304
+ * - Process reuse for multi-turn conversations
1305
+ */
1306
+ class PersistentProcessPool {
1307
+ processes = new Map();
1308
+ defaultOptions;
1309
+ idleTimeoutMs;
1310
+ cleanupIntervalMs;
1311
+ pendingToolUseTimeoutMs;
1312
+ cleanupTimer = null;
1313
+ constructor(defaultOptions) {
1314
+ this.defaultOptions = defaultOptions;
1315
+ this.idleTimeoutMs = this.resolveIdleTimeoutMs(defaultOptions);
1316
+ this.cleanupIntervalMs = this.resolveCleanupIntervalMs(defaultOptions);
1317
+ this.pendingToolUseTimeoutMs = this.resolvePendingToolUseTimeoutMs(defaultOptions);
1318
+ this.startCleanupTimer();
1319
+ }
1320
+ resolveIdleTimeoutMs(defaultOptions) {
1321
+ if (defaultOptions.idleTimeoutMs !== undefined) {
1322
+ return Math.max(0, defaultOptions.idleTimeoutMs);
1323
+ }
1324
+ return Math.max(0, defaultOptions.timeouts?.idleMs ?? defaultOptions.timeouts?.sessionMs ?? 1_800_000);
1325
+ }
1326
+ resolveCleanupIntervalMs(defaultOptions) {
1327
+ if (defaultOptions.cleanupIntervalMs !== undefined) {
1328
+ return Math.max(0, defaultOptions.cleanupIntervalMs);
1329
+ }
1330
+ return Math.max(0, defaultOptions.timeouts?.cleanupMs ?? 300_000);
1331
+ }
1332
+ resolvePendingToolUseTimeoutMs(defaultOptions) {
1333
+ if (defaultOptions.pendingToolUseTimeoutMs !== undefined) {
1334
+ return Math.max(0, defaultOptions.pendingToolUseTimeoutMs);
1335
+ }
1336
+ return Math.max(0, defaultOptions.timeouts?.pendingToolMs ?? Math.max(this.idleTimeoutMs * 4, 1_800_000));
1337
+ }
1338
+ startCleanupTimer() {
1339
+ if (this.cleanupTimer || this.cleanupIntervalMs === 0) {
1340
+ return;
1341
+ }
1342
+ this.cleanupTimer = setInterval(() => {
1343
+ this.cleanupIdleProcesses();
1344
+ }, this.cleanupIntervalMs);
1345
+ this.cleanupTimer.unref();
1346
+ }
1347
+ /**
1348
+ * Get or create a process for a channel
1349
+ */
1350
+ async getProcess(channelKey, options) {
1351
+ return (await this.getProcessWithStatus(channelKey, options)).process;
1352
+ }
1353
+ /**
1354
+ * Get or create a process for a channel and report whether this call created it.
1355
+ */
1356
+ async getProcessWithStatus(channelKey, options) {
1357
+ this.startCleanupTimer();
1358
+ const now = Date.now();
1359
+ const entry = this.processes.get(channelKey);
1360
+ let process = entry?.process;
1361
+ let created = false;
1362
+ if (!process || !process.isAlive()) {
1363
+ // Create new process
1364
+ const mergedOptions = {
1365
+ sessionId: (0, crypto_1.randomUUID)(),
1366
+ ...this.defaultOptions,
1367
+ ...options,
1368
+ // A per-call override may omit it; the pool's own workspace is the floor.
1369
+ workspaceDir: options?.workspaceDir ?? this.defaultOptions.workspaceDir,
1370
+ };
1371
+ poolLogger.info(`Creating new process for channel: ${channelKey}`);
1372
+ const createdProcess = new PersistentClaudeProcess(mergedOptions);
1373
+ process = createdProcess;
1374
+ // Handle process errors - prevent unhandled 'error' event crash
1375
+ const removeIfCurrent = () => {
1376
+ const current = this.processes.get(channelKey);
1377
+ if (current?.process === createdProcess) {
1378
+ this.processes.delete(channelKey);
1379
+ }
1380
+ };
1381
+ const touchIfCurrent = () => {
1382
+ const current = this.processes.get(channelKey);
1383
+ if (!current || current.process !== createdProcess) {
1384
+ return;
1385
+ }
1386
+ current.lastUsedAt = Date.now();
1387
+ if (createdProcess.hasPendingToolUse()) {
1388
+ current.pendingToolUseSince =
1389
+ createdProcess.getPendingToolUseStartedAt() ?? current.lastUsedAt;
1390
+ }
1391
+ else {
1392
+ current.pendingToolUseSince = undefined;
1393
+ }
1394
+ };
1395
+ createdProcess.on('error', (err) => {
1396
+ poolLogger.error(`Process error for ${channelKey}:`, err);
1397
+ // An errored generation is no longer reusable. Stop it before dropping
1398
+ // the pool reference so it cannot survive as an untracked child.
1399
+ createdProcess.stop();
1400
+ removeIfCurrent();
1401
+ });
1402
+ // Handle process death - remove from pool
1403
+ createdProcess.on('close', () => {
1404
+ poolLogger.info(`Process for ${channelKey} closed, removing from pool`);
1405
+ removeIfCurrent();
1406
+ });
1407
+ createdProcess.on('idle', touchIfCurrent);
1408
+ this.processes.set(channelKey, {
1409
+ process: createdProcess,
1410
+ lastUsedAt: now,
1411
+ policyFingerprint: mergedOptions.policyFingerprint,
1412
+ });
1413
+ await createdProcess.start();
1414
+ created = true;
1415
+ }
1416
+ else if (entry) {
1417
+ entry.lastUsedAt = now;
1418
+ }
1419
+ return { process, created };
1420
+ }
1421
+ /**
1422
+ * Stop idle ready processes that have outlived the configured idle timeout.
1423
+ */
1424
+ cleanupIdleProcesses(now = Date.now()) {
1425
+ let cleaned = 0;
1426
+ for (const [key, entry] of this.processes) {
1427
+ const process = entry.process;
1428
+ if (!process.isAlive()) {
1429
+ this.processes.delete(key);
1430
+ cleaned++;
1431
+ continue;
1432
+ }
1433
+ if (process.hasPendingToolUse()) {
1434
+ entry.pendingToolUseSince =
1435
+ process.getPendingToolUseStartedAt() ?? entry.pendingToolUseSince ?? now;
1436
+ if (this.pendingToolUseTimeoutMs === 0 ||
1437
+ now - entry.pendingToolUseSince <= this.pendingToolUseTimeoutMs) {
1438
+ continue;
1439
+ }
1440
+ poolLogger.warn(`Stopping process with expired pending tool result wait for: ${key}`);
1441
+ process.stop();
1442
+ this.processes.delete(key);
1443
+ cleaned++;
1444
+ continue;
1445
+ }
1446
+ entry.pendingToolUseSince = undefined;
1447
+ if (this.idleTimeoutMs > 0 &&
1448
+ process.isReady() &&
1449
+ now - entry.lastUsedAt > this.idleTimeoutMs) {
1450
+ poolLogger.info(`Stopping idle process for: ${key}`);
1451
+ process.stop();
1452
+ this.processes.delete(key);
1453
+ cleaned++;
1454
+ }
1455
+ }
1456
+ return cleaned;
1457
+ }
1458
+ /**
1459
+ * Stop a specific process
1460
+ */
1461
+ stopProcess(channelKey) {
1462
+ const entry = this.processes.get(channelKey);
1463
+ if (entry) {
1464
+ entry.process.stop();
1465
+ this.processes.delete(channelKey);
1466
+ }
1467
+ }
1468
+ getSessionPolicyStatus(channelKey, policyFingerprint) {
1469
+ const entry = this.processes.get(channelKey);
1470
+ if (!entry?.process.isAlive()) {
1471
+ return 'missing';
1472
+ }
1473
+ return entry.policyFingerprint === policyFingerprint ? 'compatible' : 'mismatch';
1474
+ }
1475
+ /**
1476
+ * Stop the exact process generation acquired by the caller. Only remove the pool entry when it
1477
+ * still points at that generation, so stale cleanup cannot stop or evict a replacement. The
1478
+ * expected process may already be absent after its error listener ran; it still must be stopped
1479
+ * or the child would remain alive outside pool lifecycle management.
1480
+ */
1481
+ retireProcess(channelKey, expectedProcess) {
1482
+ const entry = this.processes.get(channelKey);
1483
+ const removedCurrentGeneration = entry?.process === expectedProcess;
1484
+ if (removedCurrentGeneration) {
1485
+ this.processes.delete(channelKey);
1486
+ }
1487
+ expectedProcess.stop();
1488
+ return removedCurrentGeneration;
1489
+ }
1490
+ /**
1491
+ * Stop all processes
1492
+ */
1493
+ stopAll() {
1494
+ for (const [key, entry] of this.processes) {
1495
+ poolLogger.info(`Stopping process for: ${key}`);
1496
+ entry.process.stop();
1497
+ }
1498
+ this.processes.clear();
1499
+ if (this.cleanupTimer) {
1500
+ clearInterval(this.cleanupTimer);
1501
+ this.cleanupTimer = null;
1502
+ }
1503
+ }
1504
+ /**
1505
+ * Get number of active processes
1506
+ */
1507
+ getActiveCount() {
1508
+ return this.processes.size;
1509
+ }
1510
+ /**
1511
+ * Get all channel keys with active processes
1512
+ */
1513
+ getActiveChannels() {
1514
+ return Array.from(this.processes.keys());
1515
+ }
1516
+ /**
1517
+ * Get states of all active processes
1518
+ * @returns Map of channelKey → ProcessState
1519
+ */
1520
+ getProcessStates() {
1521
+ const states = new Map();
1522
+ for (const [key, entry] of this.processes) {
1523
+ states.set(key, entry.process.getState());
1524
+ }
1525
+ return states;
1526
+ }
1527
+ }
1528
+ exports.PersistentProcessPool = PersistentProcessPool;
1529
+ //# sourceMappingURL=persistent-cli-process.js.map