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