discoclaw 0.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 (393) hide show
  1. package/.context/README.md +42 -0
  2. package/.context/architecture.md +58 -0
  3. package/.context/bot-setup.md +24 -0
  4. package/.context/dev.md +230 -0
  5. package/.context/discord.md +144 -0
  6. package/.context/memory.md +257 -0
  7. package/.context/ops.md +59 -0
  8. package/.context/pa-safety.md +47 -0
  9. package/.context/pa.md +118 -0
  10. package/.context/project.md +43 -0
  11. package/.context/runtime.md +253 -0
  12. package/.context/tasks.md +71 -0
  13. package/.context/tools.md +75 -0
  14. package/.env.example +88 -0
  15. package/.env.example.full +378 -0
  16. package/LICENSE +21 -0
  17. package/README.md +220 -0
  18. package/dist/beads/auto-tag.js +2 -0
  19. package/dist/beads/auto-tag.test.js +62 -0
  20. package/dist/beads/bd-cli.js +9 -0
  21. package/dist/beads/bd-cli.test.js +495 -0
  22. package/dist/beads/bead-hooks-cli.js +149 -0
  23. package/dist/beads/bead-sync-cli.js +5 -0
  24. package/dist/beads/bead-sync-cli.test.js +72 -0
  25. package/dist/beads/bead-sync-coordinator.js +4 -0
  26. package/dist/beads/bead-sync-coordinator.test.js +239 -0
  27. package/dist/beads/bead-sync-watcher.js +2 -0
  28. package/dist/beads/bead-sync-watcher.test.js +96 -0
  29. package/dist/beads/bead-sync.js +7 -0
  30. package/dist/beads/bead-sync.test.js +876 -0
  31. package/dist/beads/bead-thread-cache.js +8 -0
  32. package/dist/beads/bead-thread-cache.test.js +91 -0
  33. package/dist/beads/discord-sync.js +18 -0
  34. package/dist/beads/discord-sync.test.js +782 -0
  35. package/dist/beads/find-bead-by-thread.test.js +36 -0
  36. package/dist/beads/forum-guard.js +2 -0
  37. package/dist/beads/forum-guard.test.js +204 -0
  38. package/dist/beads/initialize.js +3 -0
  39. package/dist/beads/initialize.test.js +304 -0
  40. package/dist/beads/types.js +10 -0
  41. package/dist/cli/daemon-installer.js +225 -0
  42. package/dist/cli/daemon-installer.test.js +289 -0
  43. package/dist/cli/index.js +42 -0
  44. package/dist/cli/init-wizard.js +374 -0
  45. package/dist/cli/init-wizard.test.js +191 -0
  46. package/dist/config.js +385 -0
  47. package/dist/config.test.js +589 -0
  48. package/dist/cron/auto-tag.js +100 -0
  49. package/dist/cron/auto-tag.test.js +91 -0
  50. package/dist/cron/cadence.js +74 -0
  51. package/dist/cron/cadence.test.js +53 -0
  52. package/dist/cron/cron-sync-coordinator.js +66 -0
  53. package/dist/cron/cron-sync-coordinator.test.js +118 -0
  54. package/dist/cron/cron-sync.js +165 -0
  55. package/dist/cron/cron-sync.test.js +228 -0
  56. package/dist/cron/cron-tag-map-watcher.js +128 -0
  57. package/dist/cron/cron-tag-map-watcher.test.js +155 -0
  58. package/dist/cron/default-timezone.js +23 -0
  59. package/dist/cron/default-timezone.test.js +30 -0
  60. package/dist/cron/discord-sync.js +205 -0
  61. package/dist/cron/discord-sync.test.js +353 -0
  62. package/dist/cron/executor.js +303 -0
  63. package/dist/cron/executor.test.js +614 -0
  64. package/dist/cron/forum-sync.js +347 -0
  65. package/dist/cron/forum-sync.test.js +539 -0
  66. package/dist/cron/job-lock.js +164 -0
  67. package/dist/cron/job-lock.test.js +178 -0
  68. package/dist/cron/parser.js +68 -0
  69. package/dist/cron/parser.test.js +115 -0
  70. package/dist/cron/run-control.js +24 -0
  71. package/dist/cron/run-control.test.js +27 -0
  72. package/dist/cron/run-stats.js +265 -0
  73. package/dist/cron/run-stats.test.js +160 -0
  74. package/dist/cron/scheduler.js +97 -0
  75. package/dist/cron/scheduler.test.js +112 -0
  76. package/dist/cron/tag-map.js +47 -0
  77. package/dist/cron/tag-map.test.js +64 -0
  78. package/dist/cron/types.js +1 -0
  79. package/dist/discoclaw-plan-format.test.js +137 -0
  80. package/dist/discoclaw-recipe-format.test.js +137 -0
  81. package/dist/discord/abort-registry.js +70 -0
  82. package/dist/discord/action-categories.js +36 -0
  83. package/dist/discord/action-types.js +1 -0
  84. package/dist/discord/action-utils.js +58 -0
  85. package/dist/discord/action-utils.test.js +58 -0
  86. package/dist/discord/actions-beads.js +1 -0
  87. package/dist/discord/actions-beads.test.js +372 -0
  88. package/dist/discord/actions-bot-profile.js +107 -0
  89. package/dist/discord/actions-bot-profile.test.js +138 -0
  90. package/dist/discord/actions-channels.js +427 -0
  91. package/dist/discord/actions-channels.test.js +697 -0
  92. package/dist/discord/actions-config.js +173 -0
  93. package/dist/discord/actions-config.test.js +322 -0
  94. package/dist/discord/actions-crons.js +586 -0
  95. package/dist/discord/actions-crons.test.js +499 -0
  96. package/dist/discord/actions-defer.js +60 -0
  97. package/dist/discord/actions-defer.test.js +134 -0
  98. package/dist/discord/actions-forge.js +134 -0
  99. package/dist/discord/actions-forge.test.js +206 -0
  100. package/dist/discord/actions-guild.js +301 -0
  101. package/dist/discord/actions-guild.test.js +386 -0
  102. package/dist/discord/actions-memory.js +106 -0
  103. package/dist/discord/actions-memory.test.js +248 -0
  104. package/dist/discord/actions-messaging.js +401 -0
  105. package/dist/discord/actions-messaging.test.js +738 -0
  106. package/dist/discord/actions-moderation.js +65 -0
  107. package/dist/discord/actions-moderation.test.js +88 -0
  108. package/dist/discord/actions-plan.js +445 -0
  109. package/dist/discord/actions-plan.test.js +610 -0
  110. package/dist/discord/actions-poll.js +38 -0
  111. package/dist/discord/actions-poll.test.js +93 -0
  112. package/dist/discord/actions-tasks.js +3 -0
  113. package/dist/discord/actions-tasks.test.js +418 -0
  114. package/dist/discord/actions.js +600 -0
  115. package/dist/discord/actions.test.js +522 -0
  116. package/dist/discord/allowed-mentions.js +3 -0
  117. package/dist/discord/allowed-mentions.test.js +17 -0
  118. package/dist/discord/allowlist.js +29 -0
  119. package/dist/discord/allowlist.test.js +24 -0
  120. package/dist/discord/audit-handler.js +191 -0
  121. package/dist/discord/audit-handler.test.js +361 -0
  122. package/dist/discord/bot.js +141 -0
  123. package/dist/discord/channel-context.js +181 -0
  124. package/dist/discord/defer-scheduler.js +45 -0
  125. package/dist/discord/destructive-confirmation.js +128 -0
  126. package/dist/discord/destructive-confirmation.test.js +49 -0
  127. package/dist/discord/discord-plan-auto-implement.test.js +18 -0
  128. package/dist/discord/durable-memory.js +145 -0
  129. package/dist/discord/durable-memory.test.js +281 -0
  130. package/dist/discord/durable-write-queue.js +4 -0
  131. package/dist/discord/file-download.js +308 -0
  132. package/dist/discord/file-download.test.js +303 -0
  133. package/dist/discord/forge-audit-verdict.js +140 -0
  134. package/dist/discord/forge-auto-implement.js +80 -0
  135. package/dist/discord/forge-auto-implement.test.js +110 -0
  136. package/dist/discord/forge-commands.js +698 -0
  137. package/dist/discord/forge-commands.test.js +1606 -0
  138. package/dist/discord/forge-plan-registry.js +68 -0
  139. package/dist/discord/forge-plan-registry.test.js +127 -0
  140. package/dist/discord/forum-count-sync.js +130 -0
  141. package/dist/discord/forum-count-sync.test.js +200 -0
  142. package/dist/discord/health-command.js +98 -0
  143. package/dist/discord/health-command.test.js +195 -0
  144. package/dist/discord/help-command.js +22 -0
  145. package/dist/discord/help-command.test.js +49 -0
  146. package/dist/discord/image-download.js +201 -0
  147. package/dist/discord/image-download.test.js +499 -0
  148. package/dist/discord/inflight-replies.js +228 -0
  149. package/dist/discord/inflight-replies.test.js +295 -0
  150. package/dist/discord/json-extract.js +110 -0
  151. package/dist/discord/keyed-queue.js +22 -0
  152. package/dist/discord/memory-commands.js +85 -0
  153. package/dist/discord/memory-commands.test.js +159 -0
  154. package/dist/discord/memory-timing.integration.test.js +159 -0
  155. package/dist/discord/message-coordinator.js +2347 -0
  156. package/dist/discord/message-coordinator.onboarding.test.js +183 -0
  157. package/dist/discord/message-coordinator.plan-run.test.js +264 -0
  158. package/dist/discord/message-history.js +53 -0
  159. package/dist/discord/message-history.test.js +95 -0
  160. package/dist/discord/models-command.js +59 -0
  161. package/dist/discord/models-command.test.js +150 -0
  162. package/dist/discord/nickname.test.js +76 -0
  163. package/dist/discord/onboarding-completion.js +55 -0
  164. package/dist/discord/onboarding-completion.test.js +176 -0
  165. package/dist/discord/output-common.js +178 -0
  166. package/dist/discord/output-common.test.js +198 -0
  167. package/dist/discord/output-utils.js +156 -0
  168. package/dist/discord/parse-identity-name.test.js +129 -0
  169. package/dist/discord/plan-commands.js +612 -0
  170. package/dist/discord/plan-commands.test.js +1622 -0
  171. package/dist/discord/plan-manager.js +1491 -0
  172. package/dist/discord/plan-manager.test.js +2380 -0
  173. package/dist/discord/plan-parser.js +110 -0
  174. package/dist/discord/plan-parser.test.js +63 -0
  175. package/dist/discord/plan-run-phase-start.js +20 -0
  176. package/dist/discord/plan-run-phase-start.test.js +29 -0
  177. package/dist/discord/platform-message.js +45 -0
  178. package/dist/discord/platform-message.test.js +110 -0
  179. package/dist/discord/prompt-common.js +240 -0
  180. package/dist/discord/prompt-common.test.js +423 -0
  181. package/dist/discord/reaction-handler.js +691 -0
  182. package/dist/discord/reaction-handler.test.js +1574 -0
  183. package/dist/discord/reaction-prompts.js +118 -0
  184. package/dist/discord/reaction-prompts.test.js +253 -0
  185. package/dist/discord/reply-reference.js +66 -0
  186. package/dist/discord/reply-reference.test.js +125 -0
  187. package/dist/discord/restart-command.js +143 -0
  188. package/dist/discord/restart-command.test.js +196 -0
  189. package/dist/discord/runtime-utils.js +43 -0
  190. package/dist/discord/runtime-utils.test.js +112 -0
  191. package/dist/discord/session-key.js +7 -0
  192. package/dist/discord/session-key.test.js +13 -0
  193. package/dist/discord/shortterm-memory.js +166 -0
  194. package/dist/discord/shortterm-memory.test.js +345 -0
  195. package/dist/discord/shutdown-context.js +122 -0
  196. package/dist/discord/shutdown-context.test.js +279 -0
  197. package/dist/discord/startup-profile.test.js +214 -0
  198. package/dist/discord/status-channel.js +190 -0
  199. package/dist/discord/status-channel.test.js +282 -0
  200. package/dist/discord/status-command.js +206 -0
  201. package/dist/discord/status-command.test.js +341 -0
  202. package/dist/discord/streaming-progress.js +107 -0
  203. package/dist/discord/streaming-progress.test.js +93 -0
  204. package/dist/discord/summarizer.js +89 -0
  205. package/dist/discord/summarizer.test.js +245 -0
  206. package/dist/discord/system-bootstrap.js +396 -0
  207. package/dist/discord/system-bootstrap.test.js +724 -0
  208. package/dist/discord/thread-context.js +169 -0
  209. package/dist/discord/thread-context.test.js +386 -0
  210. package/dist/discord/tool-aware-queue.js +116 -0
  211. package/dist/discord/tool-aware-queue.test.js +180 -0
  212. package/dist/discord/update-command.js +127 -0
  213. package/dist/discord/update-command.test.js +275 -0
  214. package/dist/discord/user-errors.js +40 -0
  215. package/dist/discord/user-errors.test.js +31 -0
  216. package/dist/discord/user-turn-to-durable.js +111 -0
  217. package/dist/discord/user-turn-to-durable.test.js +273 -0
  218. package/dist/discord-followup.test.js +677 -0
  219. package/dist/discord.channel-context.test.js +95 -0
  220. package/dist/discord.fail-closed.test.js +199 -0
  221. package/dist/discord.health-command.integration.test.js +140 -0
  222. package/dist/discord.js +190 -0
  223. package/dist/discord.prompt-context.test.js +1431 -0
  224. package/dist/discord.render.test.js +621 -0
  225. package/dist/discord.status-wiring.test.js +187 -0
  226. package/dist/engine/claudeCli.js +137 -0
  227. package/dist/engine/types.js +1 -0
  228. package/dist/group-queue.js +25 -0
  229. package/dist/health/credential-check.js +175 -0
  230. package/dist/health/credential-check.test.js +401 -0
  231. package/dist/health/startup-healing.js +139 -0
  232. package/dist/health/startup-healing.test.js +298 -0
  233. package/dist/identity.js +36 -0
  234. package/dist/index.js +1378 -0
  235. package/dist/logging/logger-like.js +1 -0
  236. package/dist/observability/memory-sampler.js +51 -0
  237. package/dist/observability/memory-sampler.test.js +93 -0
  238. package/dist/observability/metrics.js +88 -0
  239. package/dist/observability/metrics.test.js +42 -0
  240. package/dist/onboarding/onboarding-flow.js +246 -0
  241. package/dist/onboarding/onboarding-flow.test.js +238 -0
  242. package/dist/onboarding/onboarding-writer.js +102 -0
  243. package/dist/onboarding/onboarding-writer.test.js +143 -0
  244. package/dist/pidlock.js +187 -0
  245. package/dist/pidlock.test.js +128 -0
  246. package/dist/pipeline/engine.js +206 -0
  247. package/dist/pipeline/engine.test.js +771 -0
  248. package/dist/root-policy.js +21 -0
  249. package/dist/root-policy.test.js +55 -0
  250. package/dist/runtime/claude-code-cli.js +35 -0
  251. package/dist/runtime/claude-code-cli.test.js +1199 -0
  252. package/dist/runtime/cli-adapter.js +584 -0
  253. package/dist/runtime/cli-output-parsers.js +108 -0
  254. package/dist/runtime/cli-shared.js +96 -0
  255. package/dist/runtime/cli-shared.test.js +104 -0
  256. package/dist/runtime/cli-strategy.js +6 -0
  257. package/dist/runtime/codex-cli.js +16 -0
  258. package/dist/runtime/codex-cli.test.js +862 -0
  259. package/dist/runtime/concurrency-limit.js +80 -0
  260. package/dist/runtime/concurrency-limit.test.js +137 -0
  261. package/dist/runtime/gemini-cli.js +16 -0
  262. package/dist/runtime/gemini-cli.test.js +413 -0
  263. package/dist/runtime/long-running-process.js +415 -0
  264. package/dist/runtime/long-running-process.test.js +318 -0
  265. package/dist/runtime/model-smoke-helpers.js +160 -0
  266. package/dist/runtime/model-smoke.test.js +194 -0
  267. package/dist/runtime/model-tiers.js +33 -0
  268. package/dist/runtime/model-tiers.test.js +65 -0
  269. package/dist/runtime/openai-auth.js +151 -0
  270. package/dist/runtime/openai-auth.test.js +361 -0
  271. package/dist/runtime/openai-compat.js +178 -0
  272. package/dist/runtime/openai-compat.test.js +449 -0
  273. package/dist/runtime/process-pool.js +93 -0
  274. package/dist/runtime/process-pool.test.js +148 -0
  275. package/dist/runtime/registry.js +15 -0
  276. package/dist/runtime/registry.test.js +47 -0
  277. package/dist/runtime/session-scanner.js +186 -0
  278. package/dist/runtime/session-scanner.test.js +257 -0
  279. package/dist/runtime/strategies/claude-strategy.js +193 -0
  280. package/dist/runtime/strategies/codex-strategy.js +161 -0
  281. package/dist/runtime/strategies/gemini-strategy.js +64 -0
  282. package/dist/runtime/strategies/template-strategy.js +85 -0
  283. package/dist/runtime/tool-capabilities.js +27 -0
  284. package/dist/runtime/tool-capabilities.test.js +24 -0
  285. package/dist/runtime/tool-labels.js +48 -0
  286. package/dist/runtime/types.js +2 -0
  287. package/dist/sessionManager.js +47 -0
  288. package/dist/sessions.js +18 -0
  289. package/dist/tasks/architecture-contract.js +33 -0
  290. package/dist/tasks/architecture-contract.test.js +90 -0
  291. package/dist/tasks/auto-tag.js +50 -0
  292. package/dist/tasks/auto-tag.test.js +64 -0
  293. package/dist/tasks/bd-cli.js +164 -0
  294. package/dist/tasks/bd-cli.test.js +359 -0
  295. package/dist/tasks/bead-sync.js +1 -0
  296. package/dist/tasks/context-summary.js +27 -0
  297. package/dist/tasks/discord-sync.js +3 -0
  298. package/dist/tasks/discord-sync.test.js +685 -0
  299. package/dist/tasks/discord-types.js +4 -0
  300. package/dist/tasks/find-task-by-thread.test.js +36 -0
  301. package/dist/tasks/forum-guard.js +81 -0
  302. package/dist/tasks/forum-guard.test.js +192 -0
  303. package/dist/tasks/initialize.js +77 -0
  304. package/dist/tasks/initialize.test.js +263 -0
  305. package/dist/tasks/logger-types.js +1 -0
  306. package/dist/tasks/metrics-types.js +3 -0
  307. package/dist/tasks/migrate.js +33 -0
  308. package/dist/tasks/migrate.test.js +156 -0
  309. package/dist/tasks/path-defaults.js +67 -0
  310. package/dist/tasks/path-defaults.test.js +73 -0
  311. package/dist/tasks/runtime-types.js +1 -0
  312. package/dist/tasks/service.js +33 -0
  313. package/dist/tasks/service.test.js +51 -0
  314. package/dist/tasks/store.js +238 -0
  315. package/dist/tasks/store.test.js +417 -0
  316. package/dist/tasks/sync-context.js +1 -0
  317. package/dist/tasks/sync-contract.js +24 -0
  318. package/dist/tasks/sync-contract.test.js +25 -0
  319. package/dist/tasks/sync-coordinator-metrics.js +41 -0
  320. package/dist/tasks/sync-coordinator-retries.js +71 -0
  321. package/dist/tasks/sync-coordinator.js +96 -0
  322. package/dist/tasks/sync-coordinator.test.js +501 -0
  323. package/dist/tasks/sync-types.js +1 -0
  324. package/dist/tasks/sync-watcher.js +27 -0
  325. package/dist/tasks/sync-watcher.test.js +92 -0
  326. package/dist/tasks/tag-map.js +36 -0
  327. package/dist/tasks/tag-map.test.js +54 -0
  328. package/dist/tasks/task-action-contract.js +16 -0
  329. package/dist/tasks/task-action-contract.test.js +16 -0
  330. package/dist/tasks/task-action-executor.js +18 -0
  331. package/dist/tasks/task-action-executor.test.js +420 -0
  332. package/dist/tasks/task-action-mutation-helpers.js +17 -0
  333. package/dist/tasks/task-action-mutations.js +151 -0
  334. package/dist/tasks/task-action-prompt.js +62 -0
  335. package/dist/tasks/task-action-read-ops.js +73 -0
  336. package/dist/tasks/task-action-runner-types.js +1 -0
  337. package/dist/tasks/task-action-thread-sync.js +82 -0
  338. package/dist/tasks/task-actions.js +3 -0
  339. package/dist/tasks/task-cli.js +227 -0
  340. package/dist/tasks/task-context.js +1 -0
  341. package/dist/tasks/task-lifecycle.js +46 -0
  342. package/dist/tasks/task-lifecycle.test.js +35 -0
  343. package/dist/tasks/task-sync-apply-plan.js +95 -0
  344. package/dist/tasks/task-sync-apply-types.js +12 -0
  345. package/dist/tasks/task-sync-apply.js +319 -0
  346. package/dist/tasks/task-sync-cli.js +89 -0
  347. package/dist/tasks/task-sync-cli.test.js +70 -0
  348. package/dist/tasks/task-sync-engine.js +88 -0
  349. package/dist/tasks/task-sync-engine.test.js +934 -0
  350. package/dist/tasks/task-sync-phase-apply.js +171 -0
  351. package/dist/tasks/task-sync-pipeline.js +2 -0
  352. package/dist/tasks/task-sync-pipeline.test.js +265 -0
  353. package/dist/tasks/task-sync-reconcile-plan.js +182 -0
  354. package/dist/tasks/task-sync-reconcile.js +144 -0
  355. package/dist/tasks/task-sync.js +56 -0
  356. package/dist/tasks/task-sync.test.js +86 -0
  357. package/dist/tasks/thread-cache.js +42 -0
  358. package/dist/tasks/thread-cache.test.js +89 -0
  359. package/dist/tasks/thread-contracts.test.js +711 -0
  360. package/dist/tasks/thread-forum-ops.js +68 -0
  361. package/dist/tasks/thread-helpers.js +86 -0
  362. package/dist/tasks/thread-helpers.test.js +33 -0
  363. package/dist/tasks/thread-lifecycle-ops.js +144 -0
  364. package/dist/tasks/thread-ops-shared.js +21 -0
  365. package/dist/tasks/thread-ops.js +2 -0
  366. package/dist/tasks/types.js +20 -0
  367. package/dist/tasks/types.test.js +60 -0
  368. package/dist/test-setup.js +11 -0
  369. package/dist/test-setup.test.js +42 -0
  370. package/dist/transport/types.js +1 -0
  371. package/dist/validate.js +41 -0
  372. package/dist/validate.test.js +94 -0
  373. package/dist/version.js +15 -0
  374. package/dist/version.test.js +31 -0
  375. package/dist/webhook/server.js +199 -0
  376. package/dist/webhook/server.test.js +460 -0
  377. package/dist/workspace-bootstrap.js +135 -0
  378. package/dist/workspace-bootstrap.test.js +514 -0
  379. package/dist/workspace-permissions.js +134 -0
  380. package/dist/workspace-permissions.test.js +181 -0
  381. package/package.json +74 -0
  382. package/scripts/cron/cron-tag-map.json +9 -0
  383. package/scripts/tasks/tag-map.json +10 -0
  384. package/systemd/discoclaw.service +19 -0
  385. package/templates/recipes/integration.discoclaw-recipe.md +171 -0
  386. package/templates/workspace/AGENTS.md +217 -0
  387. package/templates/workspace/BOOTSTRAP.md +1 -0
  388. package/templates/workspace/HEARTBEAT.md +10 -0
  389. package/templates/workspace/IDENTITY.md +16 -0
  390. package/templates/workspace/MEMORY.md +24 -0
  391. package/templates/workspace/SOUL.md +52 -0
  392. package/templates/workspace/TOOLS.md +304 -0
  393. package/templates/workspace/USER.md +37 -0
@@ -0,0 +1,415 @@
1
+ import { execa } from 'execa';
2
+ import { MAX_IMAGES_PER_INVOCATION } from './types.js';
3
+ import { tryParseJsonLine, cliExecaEnv } from './cli-shared.js';
4
+ import { extractTextFromUnknownEvent, extractResultText, extractImageFromUnknownEvent, extractResultContentBlocks, imageDedupeKey, stripToolUseBlocks, } from './cli-output-parsers.js';
5
+ /**
6
+ * Manages a single long-running Claude Code subprocess using `--input-format stream-json`.
7
+ * Prompts are sent via stdin as NDJSON; responses stream back on stdout.
8
+ */
9
+ export class LongRunningProcess {
10
+ subprocess = null;
11
+ _state = 'starting';
12
+ opts;
13
+ hangTimer = null;
14
+ idleTimer = null;
15
+ killAfterTimer = null;
16
+ cleanupCalled = false;
17
+ turnActive = false;
18
+ turnEnded = false;
19
+ stdoutOnData = null;
20
+ // For active turn: the queue + notify mechanism (same pattern as one-shot).
21
+ turnQueue = [];
22
+ turnNotify = null;
23
+ stdoutBuffer = '';
24
+ // Track accumulated text for the current turn.
25
+ turnMerged = '';
26
+ turnResultText = '';
27
+ turnInToolUse = false;
28
+ turnSeenImages = new Set();
29
+ turnImageCount = 0;
30
+ /** Called when this process is added to / removed from an external tracking set. */
31
+ onCleanup;
32
+ constructor(opts) {
33
+ this.opts = {
34
+ hangTimeoutMs: 60_000,
35
+ idleTimeoutMs: 300_000,
36
+ ...opts,
37
+ };
38
+ }
39
+ get state() {
40
+ return this._state;
41
+ }
42
+ get isAlive() {
43
+ return this._state === 'idle' || this._state === 'busy';
44
+ }
45
+ /**
46
+ * Spawn the Claude Code subprocess. Must be called once after construction.
47
+ * Returns false if spawn fails.
48
+ */
49
+ spawn() {
50
+ const args = [
51
+ '-p',
52
+ '--input-format', 'stream-json',
53
+ '--output-format', 'stream-json',
54
+ '--include-partial-messages',
55
+ '--model', this.opts.model,
56
+ ];
57
+ if (this.opts.dangerouslySkipPermissions) {
58
+ args.push('--dangerously-skip-permissions');
59
+ }
60
+ if (this.opts.strictMcpConfig) {
61
+ args.push('--strict-mcp-config');
62
+ }
63
+ if (this.opts.fallbackModel) {
64
+ args.push('--fallback-model', this.opts.fallbackModel);
65
+ }
66
+ if (this.opts.maxBudgetUsd != null) {
67
+ args.push('--max-budget-usd', String(this.opts.maxBudgetUsd));
68
+ }
69
+ if (this.opts.appendSystemPrompt) {
70
+ args.push('--append-system-prompt', this.opts.appendSystemPrompt);
71
+ }
72
+ if (this.opts.verbose) {
73
+ args.push('--verbose');
74
+ }
75
+ if (this.opts.tools) {
76
+ if (this.opts.tools.length > 0) {
77
+ args.push('--tools', this.opts.tools.join(','));
78
+ }
79
+ else {
80
+ args.push('--tools=');
81
+ }
82
+ }
83
+ if (this.opts.addDirs) {
84
+ for (const dir of this.opts.addDirs) {
85
+ args.push('--add-dir', dir);
86
+ }
87
+ }
88
+ this.opts.log?.debug({ args }, 'long-running: spawning');
89
+ try {
90
+ this.subprocess = execa(this.opts.claudeBin, args, {
91
+ cwd: this.opts.cwd,
92
+ reject: false,
93
+ stdin: 'pipe',
94
+ stdout: 'pipe',
95
+ stderr: 'pipe',
96
+ env: cliExecaEnv(),
97
+ });
98
+ }
99
+ catch (err) {
100
+ this.opts.log?.info({ err }, 'long-running: spawn failed');
101
+ this._state = 'dead';
102
+ return false;
103
+ }
104
+ // Handle process exit.
105
+ this.subprocess.then(() => {
106
+ this.handleExit();
107
+ }).catch(() => {
108
+ this.handleExit();
109
+ });
110
+ this._state = 'idle';
111
+ this.startIdleTimer();
112
+ return true;
113
+ }
114
+ /**
115
+ * Send a user turn to the long-running process and yield EngineEvents.
116
+ * Caller must ensure state is `idle` before calling.
117
+ */
118
+ async *sendTurn(prompt, images) {
119
+ if (this._state !== 'idle') {
120
+ yield { type: 'error', message: `long-running: cannot send turn in state ${this._state}` };
121
+ yield { type: 'done' };
122
+ return;
123
+ }
124
+ this._state = 'busy';
125
+ this.clearIdleTimer();
126
+ this.turnActive = true;
127
+ this.turnEnded = false;
128
+ // Reset per-turn state.
129
+ this.turnQueue = [];
130
+ this.turnNotify = null;
131
+ this.turnMerged = '';
132
+ this.turnResultText = '';
133
+ this.turnInToolUse = false;
134
+ this.turnSeenImages = new Set();
135
+ this.turnImageCount = 0;
136
+ this.stdoutBuffer = '';
137
+ // Wire up stdout parsing for this turn.
138
+ const onData = (chunk) => {
139
+ this.resetHangTimer();
140
+ this.parseStdoutChunk(String(chunk));
141
+ };
142
+ this.stdoutOnData = onData;
143
+ this.subprocess.stdout.on('data', onData);
144
+ // Start hang detection.
145
+ this.startHangTimer();
146
+ // Write the user message to stdin (Claude CLI stream-json expects API-shaped messages).
147
+ // When images are present, build a content-block array; otherwise plain string.
148
+ const content = images && images.length > 0
149
+ ? [
150
+ { type: 'text', text: prompt },
151
+ ...images.map((img) => ({
152
+ type: 'image',
153
+ source: { type: 'base64', media_type: img.mediaType, data: img.base64 },
154
+ })),
155
+ ]
156
+ : prompt;
157
+ const msg = JSON.stringify({ type: 'user', message: { role: 'user', content } }) + '\n';
158
+ try {
159
+ this.subprocess.stdin.write(msg);
160
+ }
161
+ catch (err) {
162
+ // Treat as a fatal termination for this turn: unblock the consumer.
163
+ this.terminate({
164
+ reason: 'stdin_write_failed',
165
+ signal: 'SIGKILL',
166
+ emitTurnError: true,
167
+ errorMessage: `long-running: stdin write failed: ${err}`,
168
+ });
169
+ // Drain the events we just enqueued.
170
+ while (this.turnQueue.length > 0) {
171
+ yield this.turnQueue.shift();
172
+ }
173
+ return;
174
+ }
175
+ // Yield events as they arrive.
176
+ try {
177
+ let done = false;
178
+ while (!done) {
179
+ if (this.turnQueue.length === 0) {
180
+ await new Promise((resolve) => {
181
+ this.turnNotify = resolve;
182
+ });
183
+ }
184
+ while (this.turnQueue.length > 0) {
185
+ const evt = this.turnQueue.shift();
186
+ yield evt;
187
+ if (evt.type === 'done') {
188
+ done = true;
189
+ break;
190
+ }
191
+ }
192
+ }
193
+ }
194
+ finally {
195
+ if (this.stdoutOnData) {
196
+ this.subprocess?.stdout?.off('data', this.stdoutOnData);
197
+ this.stdoutOnData = null;
198
+ }
199
+ this.clearHangTimer();
200
+ this.turnActive = false;
201
+ if (this._state === 'busy') {
202
+ this._state = 'idle';
203
+ this.startIdleTimer();
204
+ }
205
+ }
206
+ }
207
+ /** Gracefully kill the subprocess. */
208
+ kill() {
209
+ this.terminate({
210
+ reason: 'kill',
211
+ signal: 'SIGTERM',
212
+ forceKillAfterMs: 5000,
213
+ emitTurnError: true,
214
+ // Avoid triggering one-shot fallback heuristics ("long-running:" / "hang detected").
215
+ errorMessage: 'multi-turn: terminated',
216
+ });
217
+ }
218
+ /** Force-kill the subprocess. */
219
+ forceKill() {
220
+ this.terminate({
221
+ reason: 'force_kill',
222
+ signal: 'SIGKILL',
223
+ emitTurnError: true,
224
+ // Avoid triggering one-shot fallback heuristics ("long-running:" / "hang detected").
225
+ errorMessage: 'multi-turn: terminated',
226
+ });
227
+ }
228
+ /** Get the underlying subprocess for external tracking (e.g. activeSubprocesses set). */
229
+ getSubprocess() {
230
+ return this.subprocess;
231
+ }
232
+ // --- Internal ---
233
+ pushEvent(evt) {
234
+ this.turnQueue.push(evt);
235
+ if (this.turnNotify) {
236
+ const n = this.turnNotify;
237
+ this.turnNotify = null;
238
+ n();
239
+ }
240
+ }
241
+ parseStdoutChunk(s) {
242
+ this.stdoutBuffer += s;
243
+ const lines = this.stdoutBuffer.split(/\r?\n/);
244
+ this.stdoutBuffer = lines.pop() ?? '';
245
+ for (const line of lines) {
246
+ const trimmed = line.trim();
247
+ if (!trimmed)
248
+ continue;
249
+ const evt = tryParseJsonLine(trimmed);
250
+ if (!evt)
251
+ continue;
252
+ const anyEvt = evt;
253
+ // Detect end-of-turn: a `result` event signals Claude finished this turn.
254
+ if (anyEvt.type === 'result') {
255
+ const rt = extractResultText(evt);
256
+ if (rt)
257
+ this.turnResultText = rt;
258
+ // Extract images from result content block arrays.
259
+ const blocks = extractResultContentBlocks(evt);
260
+ if (blocks) {
261
+ if (blocks.text)
262
+ this.turnResultText = blocks.text;
263
+ for (const img of blocks.images) {
264
+ this.pushImageIfNew(img);
265
+ }
266
+ }
267
+ this.finalizeTurn();
268
+ return;
269
+ }
270
+ // Extract streaming text.
271
+ const text = extractTextFromUnknownEvent(evt);
272
+ if (text) {
273
+ this.turnMerged += text;
274
+ const hasToolOpen = text.includes('<tool_use>') || text.includes('<tool_calls>') || text.includes('<tool_call>') || text.includes('<tool_results>') || text.includes('<tool_result>');
275
+ const hasToolClose = text.includes('</tool_use>') || text.includes('</tool_calls>') || text.includes('</tool_call>') || text.includes('</tool_results>') || text.includes('</tool_result>');
276
+ if (hasToolOpen)
277
+ this.turnInToolUse = true;
278
+ if (!this.turnInToolUse)
279
+ this.pushEvent({ type: 'text_delta', text });
280
+ if (hasToolClose)
281
+ this.turnInToolUse = false;
282
+ }
283
+ else {
284
+ // Try extracting a single image from streaming content blocks.
285
+ const img = extractImageFromUnknownEvent(evt);
286
+ if (img)
287
+ this.pushImageIfNew(img);
288
+ }
289
+ }
290
+ }
291
+ finalizeTurn() {
292
+ const raw = this.turnResultText.trim() || (this.turnMerged.trim() ? this.turnMerged.trimEnd() : '');
293
+ const final = stripToolUseBlocks(raw);
294
+ if (final)
295
+ this.pushEvent({ type: 'text_final', text: final });
296
+ this.pushDoneOnce();
297
+ }
298
+ handleExit() {
299
+ const hadActiveTurn = this.turnActive && !this.turnEnded;
300
+ this._state = 'dead';
301
+ this.clearHangTimer();
302
+ this.clearIdleTimer();
303
+ this.clearKillAfterTimer();
304
+ if (hadActiveTurn) {
305
+ this.pushEvent({ type: 'error', message: 'long-running: process exited unexpectedly' });
306
+ this.pushDoneOnce();
307
+ }
308
+ this.cleanupOnce();
309
+ }
310
+ startHangTimer() {
311
+ this.clearHangTimer();
312
+ this.hangTimer = setTimeout(() => {
313
+ this.opts.log?.info('long-running: hang detected, killing process');
314
+ this.terminate({
315
+ reason: 'hang',
316
+ signal: 'SIGKILL',
317
+ emitTurnError: true,
318
+ errorMessage: 'multi-turn: hang detected',
319
+ });
320
+ }, this.opts.hangTimeoutMs);
321
+ }
322
+ resetHangTimer() {
323
+ if (this._state !== 'busy')
324
+ return;
325
+ this.startHangTimer();
326
+ }
327
+ clearHangTimer() {
328
+ if (this.hangTimer) {
329
+ clearTimeout(this.hangTimer);
330
+ this.hangTimer = null;
331
+ }
332
+ }
333
+ startIdleTimer() {
334
+ this.clearIdleTimer();
335
+ this.idleTimer = setTimeout(() => {
336
+ this.opts.log?.info('long-running: idle timeout, killing process');
337
+ // Idle kill is not a "turn failure" and should not affect consumers.
338
+ this.terminate({
339
+ reason: 'idle_timeout',
340
+ signal: 'SIGTERM',
341
+ forceKillAfterMs: 5000,
342
+ emitTurnError: false,
343
+ });
344
+ }, this.opts.idleTimeoutMs);
345
+ }
346
+ clearIdleTimer() {
347
+ if (this.idleTimer) {
348
+ clearTimeout(this.idleTimer);
349
+ this.idleTimer = null;
350
+ }
351
+ }
352
+ clearKillAfterTimer() {
353
+ if (this.killAfterTimer) {
354
+ clearTimeout(this.killAfterTimer);
355
+ this.killAfterTimer = null;
356
+ }
357
+ }
358
+ cleanupOnce() {
359
+ if (this.cleanupCalled)
360
+ return;
361
+ this.cleanupCalled = true;
362
+ this.onCleanup?.();
363
+ }
364
+ pushImageIfNew(img) {
365
+ if (this.turnImageCount >= MAX_IMAGES_PER_INVOCATION)
366
+ return;
367
+ const key = imageDedupeKey(img);
368
+ if (this.turnSeenImages.has(key))
369
+ return;
370
+ this.turnSeenImages.add(key);
371
+ this.turnImageCount++;
372
+ this.pushEvent({ type: 'image_data', image: img });
373
+ }
374
+ pushDoneOnce() {
375
+ if (this.turnEnded)
376
+ return;
377
+ this.turnEnded = true;
378
+ this.pushEvent({ type: 'done' });
379
+ }
380
+ terminate(opts) {
381
+ // Idempotent: once dead and the active turn is ended, there's nothing left to do.
382
+ if (this._state === 'dead' && (!this.turnActive || this.turnEnded)) {
383
+ this.cleanupOnce();
384
+ return;
385
+ }
386
+ this.clearHangTimer();
387
+ this.clearIdleTimer();
388
+ this.clearKillAfterTimer();
389
+ if (this.stdoutOnData) {
390
+ this.subprocess?.stdout?.off('data', this.stdoutOnData);
391
+ this.stdoutOnData = null;
392
+ }
393
+ this._state = 'dead';
394
+ // If a consumer is blocked waiting for events, guarantee we unblock it.
395
+ if (this.turnActive && !this.turnEnded) {
396
+ if (opts.emitTurnError && opts.errorMessage) {
397
+ this.pushEvent({ type: 'error', message: opts.errorMessage });
398
+ }
399
+ this.pushDoneOnce();
400
+ }
401
+ try {
402
+ this.subprocess?.kill(opts.signal);
403
+ }
404
+ catch { /* ignore */ }
405
+ if (opts.signal === 'SIGTERM' && opts.forceKillAfterMs && opts.forceKillAfterMs > 0) {
406
+ this.killAfterTimer = setTimeout(() => {
407
+ try {
408
+ this.subprocess?.kill('SIGKILL');
409
+ }
410
+ catch { /* ignore */ }
411
+ }, opts.forceKillAfterMs);
412
+ }
413
+ this.cleanupOnce();
414
+ }
415
+ }