opensquid 0.5.441 → 0.5.449

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 (391) hide show
  1. package/README.md +1 -0
  2. package/dist/functions/arm_scope.d.ts +27 -0
  3. package/dist/functions/arm_scope.d.ts.map +1 -0
  4. package/dist/functions/arm_scope.js +52 -0
  5. package/dist/functions/arm_scope.js.map +1 -0
  6. package/dist/functions/index.d.ts +1 -0
  7. package/dist/functions/index.d.ts.map +1 -1
  8. package/dist/functions/index.js +1 -0
  9. package/dist/functions/index.js.map +1 -1
  10. package/dist/functions/recall_pre_inject.d.ts.map +1 -1
  11. package/dist/functions/recall_pre_inject.js +12 -0
  12. package/dist/functions/recall_pre_inject.js.map +1 -1
  13. package/dist/rag/store_git.d.ts +23 -0
  14. package/dist/rag/store_git.d.ts.map +1 -0
  15. package/dist/rag/store_git.js +57 -0
  16. package/dist/rag/store_git.js.map +1 -0
  17. package/dist/runtime/bootstrap.d.ts.map +1 -1
  18. package/dist/runtime/bootstrap.js +2 -0
  19. package/dist/runtime/bootstrap.js.map +1 -1
  20. package/dist/runtime/handoff/render.d.ts +5 -4
  21. package/dist/runtime/handoff/render.d.ts.map +1 -1
  22. package/dist/runtime/handoff/render.js +7 -7
  23. package/dist/runtime/handoff/render.js.map +1 -1
  24. package/dist/runtime/hooks/active_task_mirror.js +0 -0
  25. package/dist/runtime/hooks/apply_patch.js +0 -0
  26. package/dist/runtime/hooks/dispatch.js +0 -0
  27. package/dist/runtime/hooks/hook_output.js +0 -0
  28. package/dist/runtime/hooks/memory_reconcile.js +0 -0
  29. package/dist/runtime/hooks/new_project_detect.js +0 -0
  30. package/dist/runtime/hooks/profession_resolver.js +0 -0
  31. package/dist/runtime/hooks/scope_intent.js +0 -0
  32. package/dist/runtime/hooks/session-end.js +11 -0
  33. package/dist/runtime/hooks/session-end.js.map +1 -1
  34. package/dist/runtime/hooks/session_id.js +0 -0
  35. package/dist/runtime/hooks/session_liveness.js +0 -0
  36. package/dist/runtime/hooks/stop_drive.js +0 -0
  37. package/dist/runtime/hooks/stop_stream.js +0 -0
  38. package/dist/runtime/hooks/subagent_guard.js +0 -0
  39. package/dist/runtime/hooks/transcript.js +0 -0
  40. package/dist/runtime/hooks/transcript_tasks.js +0 -0
  41. package/dist/runtime/ralph/orchestrator.d.ts.map +1 -1
  42. package/dist/runtime/ralph/orchestrator.js +2 -1
  43. package/dist/runtime/ralph/orchestrator.js.map +1 -1
  44. package/dist/setup/cli/limits_state.d.ts.map +1 -1
  45. package/dist/setup/cli/limits_state.js +6 -40
  46. package/dist/setup/cli/limits_state.js.map +1 -1
  47. package/dist/setup/cli/pack_walk.d.ts +32 -0
  48. package/dist/setup/cli/pack_walk.d.ts.map +1 -0
  49. package/dist/setup/cli/pack_walk.js +76 -0
  50. package/dist/setup/cli/pack_walk.js.map +1 -0
  51. package/dist/setup/cli/permissions_state.d.ts.map +1 -1
  52. package/dist/setup/cli/permissions_state.js +6 -37
  53. package/dist/setup/cli/permissions_state.js.map +1 -1
  54. package/dist/setup/cli/triggers_state.d.ts.map +1 -1
  55. package/dist/setup/cli/triggers_state.js +3 -29
  56. package/dist/setup/cli/triggers_state.js.map +1 -1
  57. package/dist/workgraph/events.d.ts.map +1 -1
  58. package/dist/workgraph/events.js +10 -0
  59. package/dist/workgraph/events.js.map +1 -1
  60. package/dist/workgraph/store.d.ts.map +1 -1
  61. package/dist/workgraph/store.js +5 -0
  62. package/dist/workgraph/store.js.map +1 -1
  63. package/dist/workgraph/types.d.ts +2 -1
  64. package/dist/workgraph/types.d.ts.map +1 -1
  65. package/docs/ARCHITECTURE.md +268 -0
  66. package/docs/pack-runtime.md +15 -12
  67. package/docs/skill-grammar-guide.md +4 -4
  68. package/package.json +5 -3
  69. package/packs/builtin/coding-flow/skills/entry-and-handoffs/skill.yaml +13 -17
  70. package/dist/anti-drift/evaluator.d.ts +0 -88
  71. package/dist/anti-drift/evaluator.d.ts.map +0 -1
  72. package/dist/anti-drift/evaluator.js +0 -417
  73. package/dist/anti-drift/evaluator.js.map +0 -1
  74. package/dist/anti-drift/evaluator.test.js +0 -78
  75. package/dist/anti-drift/rules.d.ts +0 -80
  76. package/dist/anti-drift/rules.d.ts.map +0 -1
  77. package/dist/anti-drift/rules.js +0 -368
  78. package/dist/anti-drift/rules.js.map +0 -1
  79. package/dist/anti-drift/rules.test.js +0 -213
  80. package/dist/anti-drift/state.d.ts +0 -107
  81. package/dist/anti-drift/state.d.ts.map +0 -1
  82. package/dist/anti-drift/state.js +0 -177
  83. package/dist/anti-drift/state.js.map +0 -1
  84. package/dist/anti-drift/state.test.js +0 -120
  85. package/dist/chat/adapters/discord.d.ts +0 -41
  86. package/dist/chat/adapters/discord.d.ts.map +0 -1
  87. package/dist/chat/adapters/discord.js +0 -176
  88. package/dist/chat/adapters/discord.js.map +0 -1
  89. package/dist/chat/adapters/discord.test.js +0 -25
  90. package/dist/chat/adapters/slack.d.ts +0 -43
  91. package/dist/chat/adapters/slack.d.ts.map +0 -1
  92. package/dist/chat/adapters/slack.js +0 -172
  93. package/dist/chat/adapters/slack.js.map +0 -1
  94. package/dist/chat/adapters/slack.test.js +0 -30
  95. package/dist/chat/adapters/telegram.d.ts +0 -148
  96. package/dist/chat/adapters/telegram.d.ts.map +0 -1
  97. package/dist/chat/adapters/telegram.js +0 -498
  98. package/dist/chat/adapters/telegram.js.map +0 -1
  99. package/dist/chat/adapters/telegram.test.js +0 -94
  100. package/dist/chat/config.d.ts +0 -98
  101. package/dist/chat/config.d.ts.map +0 -1
  102. package/dist/chat/config.js +0 -185
  103. package/dist/chat/config.js.map +0 -1
  104. package/dist/chat/daemon/active-project.d.ts +0 -17
  105. package/dist/chat/daemon/active-project.d.ts.map +0 -1
  106. package/dist/chat/daemon/active-project.js +0 -23
  107. package/dist/chat/daemon/active-project.js.map +0 -1
  108. package/dist/chat/daemon/autospawn.d.ts +0 -40
  109. package/dist/chat/daemon/autospawn.d.ts.map +0 -1
  110. package/dist/chat/daemon/autospawn.js +0 -129
  111. package/dist/chat/daemon/autospawn.js.map +0 -1
  112. package/dist/chat/daemon/autospawn.test.js +0 -112
  113. package/dist/chat/daemon/cli.d.ts +0 -18
  114. package/dist/chat/daemon/cli.d.ts.map +0 -1
  115. package/dist/chat/daemon/cli.js +0 -71
  116. package/dist/chat/daemon/cli.js.map +0 -1
  117. package/dist/chat/daemon/collisions.js +0 -384
  118. package/dist/chat/daemon/health-check.d.ts +0 -69
  119. package/dist/chat/daemon/health-check.d.ts.map +0 -1
  120. package/dist/chat/daemon/health-check.js +0 -112
  121. package/dist/chat/daemon/health-check.js.map +0 -1
  122. package/dist/chat/daemon/inbox-read.d.ts +0 -35
  123. package/dist/chat/daemon/inbox-read.d.ts.map +0 -1
  124. package/dist/chat/daemon/inbox-read.js +0 -75
  125. package/dist/chat/daemon/inbox-read.js.map +0 -1
  126. package/dist/chat/daemon/inbox-read.test.js +0 -97
  127. package/dist/chat/daemon/inbox.d.ts +0 -63
  128. package/dist/chat/daemon/inbox.d.ts.map +0 -1
  129. package/dist/chat/daemon/inbox.js +0 -56
  130. package/dist/chat/daemon/inbox.js.map +0 -1
  131. package/dist/chat/daemon/inbox.test.js +0 -110
  132. package/dist/chat/daemon/lifecycle.d.ts +0 -71
  133. package/dist/chat/daemon/lifecycle.d.ts.map +0 -1
  134. package/dist/chat/daemon/lifecycle.js +0 -221
  135. package/dist/chat/daemon/lifecycle.js.map +0 -1
  136. package/dist/chat/daemon/lifecycle.test.js +0 -163
  137. package/dist/chat/daemon/protocol.d.ts +0 -107
  138. package/dist/chat/daemon/protocol.d.ts.map +0 -1
  139. package/dist/chat/daemon/protocol.js +0 -54
  140. package/dist/chat/daemon/protocol.js.map +0 -1
  141. package/dist/chat/daemon/routing.d.ts +0 -140
  142. package/dist/chat/daemon/routing.d.ts.map +0 -1
  143. package/dist/chat/daemon/routing.js +0 -198
  144. package/dist/chat/daemon/routing.js.map +0 -1
  145. package/dist/chat/daemon/routing.test.js +0 -259
  146. package/dist/chat/daemon/rpc-client.d.ts +0 -45
  147. package/dist/chat/daemon/rpc-client.d.ts.map +0 -1
  148. package/dist/chat/daemon/rpc-client.js +0 -133
  149. package/dist/chat/daemon/rpc-client.js.map +0 -1
  150. package/dist/chat/daemon/rpc-server.d.ts +0 -39
  151. package/dist/chat/daemon/rpc-server.d.ts.map +0 -1
  152. package/dist/chat/daemon/rpc-server.js +0 -385
  153. package/dist/chat/daemon/rpc-server.js.map +0 -1
  154. package/dist/chat/daemon/rpc.test.js +0 -177
  155. package/dist/chat/daemon/subscribers.js +0 -257
  156. package/dist/chat/daemon/worker.d.ts +0 -27
  157. package/dist/chat/daemon/worker.d.ts.map +0 -1
  158. package/dist/chat/daemon/worker.js +0 -313
  159. package/dist/chat/daemon/worker.js.map +0 -1
  160. package/dist/chat/daemon/workspace-topic.js +0 -324
  161. package/dist/chat/env-token.d.ts +0 -60
  162. package/dist/chat/env-token.d.ts.map +0 -1
  163. package/dist/chat/env-token.js +0 -137
  164. package/dist/chat/env-token.js.map +0 -1
  165. package/dist/chat/env-token.test.js +0 -160
  166. package/dist/chat/factory.d.ts +0 -30
  167. package/dist/chat/factory.d.ts.map +0 -1
  168. package/dist/chat/factory.js +0 -50
  169. package/dist/chat/factory.js.map +0 -1
  170. package/dist/chat/factory.test.js +0 -55
  171. package/dist/chat/gateway.d.ts +0 -176
  172. package/dist/chat/gateway.d.ts.map +0 -1
  173. package/dist/chat/gateway.js +0 -146
  174. package/dist/chat/gateway.js.map +0 -1
  175. package/dist/chat/gateway.test.js +0 -192
  176. package/dist/claude-md.d.ts +0 -39
  177. package/dist/claude-md.d.ts.map +0 -1
  178. package/dist/claude-md.js +0 -113
  179. package/dist/claude-md.js.map +0 -1
  180. package/dist/claude-md.test.js +0 -91
  181. package/dist/codex/activate.d.ts +0 -66
  182. package/dist/codex/activate.d.ts.map +0 -1
  183. package/dist/codex/activate.js +0 -329
  184. package/dist/codex/activate.js.map +0 -1
  185. package/dist/codex/activate.test.js +0 -229
  186. package/dist/codex/bundled-default/bundled-default.test.js +0 -161
  187. package/dist/codex/cli-publish.test.js +0 -133
  188. package/dist/codex/cli.d.ts +0 -35
  189. package/dist/codex/cli.d.ts.map +0 -1
  190. package/dist/codex/cli.js +0 -554
  191. package/dist/codex/cli.js.map +0 -1
  192. package/dist/codex/cli.test.js +0 -277
  193. package/dist/codex/import-skill-md.d.ts +0 -53
  194. package/dist/codex/import-skill-md.d.ts.map +0 -1
  195. package/dist/codex/import-skill-md.js +0 -236
  196. package/dist/codex/import-skill-md.js.map +0 -1
  197. package/dist/codex/import-skill-md.test.js +0 -225
  198. package/dist/codex/loader.d.ts +0 -27
  199. package/dist/codex/loader.d.ts.map +0 -1
  200. package/dist/codex/loader.js +0 -86
  201. package/dist/codex/loader.js.map +0 -1
  202. package/dist/codex/loader.test.js +0 -75
  203. package/dist/codex/parse.d.ts +0 -28
  204. package/dist/codex/parse.d.ts.map +0 -1
  205. package/dist/codex/parse.js +0 -309
  206. package/dist/codex/parse.js.map +0 -1
  207. package/dist/codex/parse.test.js +0 -241
  208. package/dist/codex/store.d.ts +0 -87
  209. package/dist/codex/store.d.ts.map +0 -1
  210. package/dist/codex/store.js +0 -205
  211. package/dist/codex/store.js.map +0 -1
  212. package/dist/codex/store.test.js +0 -242
  213. package/dist/codex/types.d.ts +0 -398
  214. package/dist/codex/types.d.ts.map +0 -1
  215. package/dist/codex/types.js +0 -21
  216. package/dist/codex/types.js.map +0 -1
  217. package/dist/config.d.ts +0 -53
  218. package/dist/config.d.ts.map +0 -1
  219. package/dist/config.js +0 -202
  220. package/dist/config.js.map +0 -1
  221. package/dist/config.test.js +0 -117
  222. package/dist/engine/cli.d.ts +0 -14
  223. package/dist/engine/cli.d.ts.map +0 -1
  224. package/dist/engine/cli.js +0 -171
  225. package/dist/engine/cli.js.map +0 -1
  226. package/dist/engine/client.d.ts +0 -219
  227. package/dist/engine/client.d.ts.map +0 -1
  228. package/dist/engine/client.js +0 -312
  229. package/dist/engine/client.js.map +0 -1
  230. package/dist/engine/config.d.ts +0 -62
  231. package/dist/engine/config.d.ts.map +0 -1
  232. package/dist/engine/config.js +0 -223
  233. package/dist/engine/config.js.map +0 -1
  234. package/dist/engine/index.d.ts +0 -17
  235. package/dist/engine/index.d.ts.map +0 -1
  236. package/dist/engine/index.js +0 -16
  237. package/dist/engine/index.js.map +0 -1
  238. package/dist/engine/resolver.d.ts +0 -62
  239. package/dist/engine/resolver.d.ts.map +0 -1
  240. package/dist/engine/resolver.js +0 -103
  241. package/dist/engine/resolver.js.map +0 -1
  242. package/dist/engine/singleton.d.ts +0 -95
  243. package/dist/engine/singleton.d.ts.map +0 -1
  244. package/dist/engine/singleton.js +0 -325
  245. package/dist/engine/singleton.js.map +0 -1
  246. package/dist/engine/types.d.ts +0 -402
  247. package/dist/engine/types.d.ts.map +0 -1
  248. package/dist/engine/types.js +0 -22
  249. package/dist/engine/types.js.map +0 -1
  250. package/dist/engine-binary-resolver.js +0 -110
  251. package/dist/engine-binary-resolver.test.js +0 -61
  252. package/dist/engine-cli.js +0 -60
  253. package/dist/engine-client.js +0 -301
  254. package/dist/engine-client.test.js +0 -118
  255. package/dist/functions/chain_state.d.ts +0 -51
  256. package/dist/functions/chain_state.d.ts.map +0 -1
  257. package/dist/functions/chain_state.js +0 -59
  258. package/dist/functions/chain_state.js.map +0 -1
  259. package/dist/hooks/drift-catalog.d.ts +0 -68
  260. package/dist/hooks/drift-catalog.d.ts.map +0 -1
  261. package/dist/hooks/drift-catalog.js +0 -184
  262. package/dist/hooks/drift-catalog.js.map +0 -1
  263. package/dist/hooks/drift-catalog.test.js +0 -154
  264. package/dist/hooks/drift-patterns.d.ts +0 -110
  265. package/dist/hooks/drift-patterns.d.ts.map +0 -1
  266. package/dist/hooks/drift-patterns.js +0 -289
  267. package/dist/hooks/drift-patterns.js.map +0 -1
  268. package/dist/hooks/drift-patterns.test.js +0 -325
  269. package/dist/hooks/engine-vocab-gate.d.ts +0 -108
  270. package/dist/hooks/engine-vocab-gate.d.ts.map +0 -1
  271. package/dist/hooks/engine-vocab-gate.js +0 -225
  272. package/dist/hooks/engine-vocab-gate.js.map +0 -1
  273. package/dist/hooks/engine-vocab-gate.test.js +0 -170
  274. package/dist/hooks/heartbeat.d.ts +0 -107
  275. package/dist/hooks/heartbeat.d.ts.map +0 -1
  276. package/dist/hooks/heartbeat.js +0 -316
  277. package/dist/hooks/heartbeat.js.map +0 -1
  278. package/dist/hooks/heartbeat.test.js +0 -393
  279. package/dist/hooks/honesty-ledger-session-scope.test.js +0 -100
  280. package/dist/hooks/honesty-ledger.d.ts +0 -123
  281. package/dist/hooks/honesty-ledger.d.ts.map +0 -1
  282. package/dist/hooks/honesty-ledger.js +0 -226
  283. package/dist/hooks/honesty-ledger.js.map +0 -1
  284. package/dist/hooks/honesty-ledger.test.js +0 -466
  285. package/dist/hooks/inline-report-check.d.ts +0 -63
  286. package/dist/hooks/inline-report-check.d.ts.map +0 -1
  287. package/dist/hooks/inline-report-check.js +0 -88
  288. package/dist/hooks/inline-report-check.js.map +0 -1
  289. package/dist/hooks/inline-report-check.test.js +0 -96
  290. package/dist/hooks/pre-tool-use.d.ts +0 -62
  291. package/dist/hooks/pre-tool-use.d.ts.map +0 -1
  292. package/dist/hooks/pre-tool-use.js +0 -342
  293. package/dist/hooks/pre-tool-use.js.map +0 -1
  294. package/dist/hooks/pre-tool-use.test.js +0 -134
  295. package/dist/hooks/session-end.d.ts +0 -15
  296. package/dist/hooks/session-end.d.ts.map +0 -1
  297. package/dist/hooks/session-end.js +0 -60
  298. package/dist/hooks/session-end.js.map +0 -1
  299. package/dist/hooks/session-end.test.js +0 -52
  300. package/dist/hooks/stop.d.ts +0 -35
  301. package/dist/hooks/stop.d.ts.map +0 -1
  302. package/dist/hooks/stop.js +0 -136
  303. package/dist/hooks/stop.js.map +0 -1
  304. package/dist/hooks/transcript-active-task.test.js +0 -342
  305. package/dist/hooks/transcript.d.ts +0 -26
  306. package/dist/hooks/transcript.d.ts.map +0 -1
  307. package/dist/hooks/transcript.js +0 -266
  308. package/dist/hooks/transcript.js.map +0 -1
  309. package/dist/hooks/transcript.test.js +0 -103
  310. package/dist/hooks/user-prompt-submit.d.ts +0 -74
  311. package/dist/hooks/user-prompt-submit.d.ts.map +0 -1
  312. package/dist/hooks/user-prompt-submit.js +0 -256
  313. package/dist/hooks/user-prompt-submit.js.map +0 -1
  314. package/dist/hooks/user-prompt-submit.test.js +0 -118
  315. package/dist/hooks/versioning-gate.d.ts +0 -101
  316. package/dist/hooks/versioning-gate.d.ts.map +0 -1
  317. package/dist/hooks/versioning-gate.js +0 -245
  318. package/dist/hooks/versioning-gate.js.map +0 -1
  319. package/dist/hooks/versioning-gate.test.js +0 -368
  320. package/dist/hooks/workflow-gate.d.ts +0 -64
  321. package/dist/hooks/workflow-gate.d.ts.map +0 -1
  322. package/dist/hooks/workflow-gate.js +0 -152
  323. package/dist/hooks/workflow-gate.js.map +0 -1
  324. package/dist/hooks/workflow-gate.test.js +0 -197
  325. package/dist/hooks-cli.d.ts +0 -25
  326. package/dist/hooks-cli.d.ts.map +0 -1
  327. package/dist/hooks-cli.js +0 -286
  328. package/dist/hooks-cli.js.map +0 -1
  329. package/dist/hooks-cli.test.js +0 -148
  330. package/dist/origin.d.ts +0 -16
  331. package/dist/origin.d.ts.map +0 -1
  332. package/dist/origin.js +0 -92
  333. package/dist/origin.js.map +0 -1
  334. package/dist/packs/seed_lessons_ingest.d.ts +0 -30
  335. package/dist/packs/seed_lessons_ingest.d.ts.map +0 -1
  336. package/dist/packs/seed_lessons_ingest.js +0 -107
  337. package/dist/packs/seed_lessons_ingest.js.map +0 -1
  338. package/dist/project-cli.d.ts +0 -7
  339. package/dist/project-cli.d.ts.map +0 -1
  340. package/dist/project-cli.js +0 -145
  341. package/dist/project-cli.js.map +0 -1
  342. package/dist/project.d.ts +0 -127
  343. package/dist/project.d.ts.map +0 -1
  344. package/dist/project.js +0 -281
  345. package/dist/project.js.map +0 -1
  346. package/dist/project.test.js +0 -287
  347. package/dist/rag/backends/loop_engine.d.ts +0 -61
  348. package/dist/rag/backends/loop_engine.d.ts.map +0 -1
  349. package/dist/rag/backends/loop_engine.js +0 -160
  350. package/dist/rag/backends/loop_engine.js.map +0 -1
  351. package/dist/recall.d.ts +0 -82
  352. package/dist/recall.d.ts.map +0 -1
  353. package/dist/recall.js +0 -81
  354. package/dist/recall.js.map +0 -1
  355. package/dist/runtime/agent_bridge/autospawn.d.ts +0 -131
  356. package/dist/runtime/agent_bridge/autospawn.d.ts.map +0 -1
  357. package/dist/runtime/agent_bridge/autospawn.js +0 -251
  358. package/dist/runtime/agent_bridge/autospawn.js.map +0 -1
  359. package/dist/runtime/chain_state.d.ts +0 -124
  360. package/dist/runtime/chain_state.d.ts.map +0 -1
  361. package/dist/runtime/chain_state.js +0 -189
  362. package/dist/runtime/chain_state.js.map +0 -1
  363. package/dist/runtime/hooks/permission_decision.d.ts +0 -34
  364. package/dist/runtime/hooks/permission_decision.d.ts.map +0 -1
  365. package/dist/runtime/hooks/permission_decision.js +0 -39
  366. package/dist/runtime/hooks/permission_decision.js.map +0 -1
  367. package/dist/runtime/workflow_fsm.d.ts +0 -21
  368. package/dist/runtime/workflow_fsm.d.ts.map +0 -1
  369. package/dist/runtime/workflow_fsm.js +0 -25
  370. package/dist/runtime/workflow_fsm.js.map +0 -1
  371. package/dist/runtime/workflow_map.d.ts +0 -26
  372. package/dist/runtime/workflow_map.d.ts.map +0 -1
  373. package/dist/runtime/workflow_map.js +0 -38
  374. package/dist/runtime/workflow_map.js.map +0 -1
  375. package/dist/scope.d.ts +0 -48
  376. package/dist/scope.d.ts.map +0 -1
  377. package/dist/scope.js +0 -111
  378. package/dist/scope.js.map +0 -1
  379. package/dist/setup/cli/topic_create_step.d.ts +0 -84
  380. package/dist/setup/cli/topic_create_step.d.ts.map +0 -1
  381. package/dist/setup/cli/topic_create_step.js +0 -213
  382. package/dist/setup/cli/topic_create_step.js.map +0 -1
  383. package/dist/system-export.d.ts +0 -65
  384. package/dist/system-export.d.ts.map +0 -1
  385. package/dist/system-export.js +0 -194
  386. package/dist/system-export.js.map +0 -1
  387. package/dist/utterance/classifier.d.ts +0 -53
  388. package/dist/utterance/classifier.d.ts.map +0 -1
  389. package/dist/utterance/classifier.js +0 -184
  390. package/dist/utterance/classifier.js.map +0 -1
  391. package/dist/utterance/classifier.test.js +0 -147
@@ -1,313 +0,0 @@
1
- /**
2
- * Chat-daemon worker entrypoint (v0.7.1 Phase A).
3
- *
4
- * Spawned as a detached child by `lifecycle.startDaemon()`. Owns the
5
- * single long-poll connection per chat platform. The MCP server side
6
- * stays out of the polling business entirely — outbound RPC (Phase B)
7
- * and inbox tailing (Phase C) replace the in-process gateway.
8
- *
9
- * Lifecycle inside the worker:
10
- * 1. Write our PID to ~/.opensquid/chat-daemon.pid
11
- * 2. Build the chat gateway from ~/.opensquid/config.json
12
- * 3. Start every configured adapter (their long-poll loops run as
13
- * side effects of start())
14
- * 4. Install SIGTERM / SIGINT handlers that stop the gateway and
15
- * remove the pidfile before exit
16
- * 5. Park on process.stdin (which is /dev/null in detached mode)
17
- * so the event loop stays alive
18
- *
19
- * Crash behavior: any unhandled exception from gateway.start() prints
20
- * to the (parent-redirected) log file and exits non-zero. The pidfile
21
- * is cleaned up in the SIGTERM handler — if we crash before installing
22
- * it, the pidfile may linger, and the next `status` call will report
23
- * `stale_pid` (lifecycle.startDaemon cleans up stale pidfiles before
24
- * spawning).
25
- */
26
- import { promises as fs } from "node:fs";
27
- import { buildChatGateway } from "../factory.js";
28
- import { appendToInbox } from "./inbox.js";
29
- import { daemonPaths } from "./lifecycle.js";
30
- import { buildRoutingIndex, loadAllProjectChatRouting } from "./routing.js";
31
- import { RpcServer } from "./rpc-server.js";
32
- let gateway = null;
33
- let rpcServer = null;
34
- let routingIndex = new Map();
35
- let routingPollTimer = null;
36
- let pidFile = null;
37
- let shuttingDown = false;
38
- export async function runDaemonWorker(dataRoot) {
39
- const paths = daemonPaths(dataRoot);
40
- pidFile = paths.pidFile;
41
- // Write pidfile FIRST so a status check after spawn sees the worker
42
- // promptly. Truncate-write is the right semantic — any previous
43
- // pidfile is stale by definition (we already verified no live daemon
44
- // existed in lifecycle.startDaemon).
45
- await fs.writeFile(pidFile, `${process.pid}\n`, "utf8");
46
- log(`[chat-daemon] worker booted pid=${process.pid} cwd=${process.cwd()}`);
47
- // Build + start the gateway. If config is empty, no adapters
48
- // activate and the daemon parks idle — useful for testing the
49
- // lifecycle without configuring a real bot token.
50
- try {
51
- // 0.7.5 (#148): log which source each platform's token came from
52
- // (env / env-file / config-json) so operators can debug "which
53
- // bot is this daemon actually using" without exposing the secret.
54
- try {
55
- const { loadChatConfigWithSources } = await import("../config.js");
56
- const { sources } = await loadChatConfigWithSources(dataRoot);
57
- const lines = [];
58
- if (sources.telegram)
59
- lines.push(`telegram=${sources.telegram}`);
60
- if (sources.discord)
61
- lines.push(`discord=${sources.discord}`);
62
- if (sources.slack_bot)
63
- lines.push(`slack_bot=${sources.slack_bot}`);
64
- if (sources.slack_app)
65
- lines.push(`slack_app=${sources.slack_app}`);
66
- if (lines.length) {
67
- log(`[chat-daemon] token sources: ${lines.join(" ")}${sources.env_file_path ? ` (env-file: ${sources.env_file_path})` : ""}`);
68
- }
69
- }
70
- catch (logErr) {
71
- log(`[chat-daemon] could not log token sources (non-fatal): ${logErr instanceof Error ? logErr.message : logErr}`);
72
- }
73
- const built = await buildChatGateway({ dataRoot });
74
- gateway = built.gateway;
75
- log(`[chat-daemon] activating platforms: ${built.activated.join(",") || "(none)"}`);
76
- if (built.issues.length) {
77
- for (const i of built.issues) {
78
- log(`[chat-daemon] config issue ${i.platform}.${i.field}: ${i.problem}`);
79
- }
80
- }
81
- // Phase C: load per-project chat-routing.json files and build the
82
- // chat_id → project_uuid index BEFORE attaching the inbound
83
- // handler. The handler uses this index to route messages to
84
- // per-project inboxes.
85
- routingIndex = await rebuildRoutingIndex(dataRoot);
86
- log(`[chat-daemon] routing index built: ${routingIndex.size} inbound channels mapped`);
87
- gateway.onMessage(async (msg) => {
88
- // v0.5.94 (WAB.2 Part A / TG.1 (a)): DM-first routing.
89
- //
90
- // Key precedence:
91
- // 1. DM key (`telegram:dm:<user_id>`) when the message is a DM —
92
- // defined as `msg.channel === "telegram:" + msg.senderId`.
93
- // The Telegram adapter formats DM chats as `telegram:<chat_id>`
94
- // where chat_id === from.id, so the equality check is the
95
- // canonical Telegram private-chat indicator. This also rejects
96
- // group-message-from-self spoofs (group chat.id is negative
97
- // for supergroups; senderId is positive; they cannot collide).
98
- // 2. Topic-specific key (`<channel>:<threadId>`) when the message
99
- // is in a forum topic.
100
- // 3. Channel-only key (`<channel>`).
101
- //
102
- // Strict topic whitelist preserved per TG.1 (d) — a message in a
103
- // topic NOT listed by any project's `inbound_topic_ids` will not
104
- // fall back to the chat-only key (because `collectInboundChannels`
105
- // does not emit the chat-only key when `inbound_topic_ids` is set
106
- // on that project). Such messages orphan, which is the documented
107
- // security-correct default.
108
- const isDm = msg.platform === 'telegram' && msg.channel === `telegram:${msg.senderId}`;
109
- const dmKey = isDm ? `telegram:dm:${msg.senderId}` : null;
110
- const topicKey = msg.threadId ? `${msg.channel}:${msg.threadId}` : null;
111
- const projectUuid = (dmKey ? routingIndex.get(dmKey) : undefined) ??
112
- (topicKey ? routingIndex.get(topicKey) : undefined) ??
113
- routingIndex.get(msg.channel) ??
114
- null;
115
- try {
116
- const r = await appendToInbox(msg, projectUuid, dataRoot);
117
- log(`[chat-daemon] inbox ← ${msg.channel} (${msg.text.slice(0, 60).replace(/\n/g, " ")}…) → ${r.destination}${r.project_uuid ? "/" + r.project_uuid : ""}`);
118
- }
119
- catch (err) {
120
- log(`[chat-daemon] inbox append failed for ${msg.channel}: ${err instanceof Error ? err.message : err}`);
121
- }
122
- // TPS.6 patch 2 (v0.5.126) — broadcast to long-lived UDS
123
- // subscribers. The JSONL file write above is the durable record;
124
- // this push is the low-latency delivery. We broadcast on a
125
- // single key — the most-specific one available: topic-suffixed
126
- // `<channel>:<threadId>` if present, else bare `<channel>`.
127
- // This mirrors the routing-index key semantics from
128
- // `collectInboundChannels`: a project that registers only
129
- // `chat_id:thread_id` doesn't want to receive messages from
130
- // other topics in the same supergroup. Wildcard subscribers
131
- // (chat_ids=[]) see every message regardless of key shape.
132
- // Fire-and-forget — subscriber write failures are handled by
133
- // the registry's own socket lifecycle hooks.
134
- if (rpcServer !== null) {
135
- const broadcastKey = msg.threadId
136
- ? `${msg.channel}:${msg.threadId}`
137
- : msg.channel;
138
- const notif = {
139
- jsonrpc: "2.0",
140
- method: "inbound_message",
141
- params: {
142
- delivery_id: `del-${Date.now().toString()}-${Math.random().toString(36).slice(2, 10)}`,
143
- message_id: msg.id,
144
- platform: msg.platform,
145
- channel: msg.channel,
146
- ...(msg.threadId !== undefined ? { thread_id: msg.threadId } : {}),
147
- sender: msg.sender,
148
- sender_id: msg.senderId,
149
- text: msg.text,
150
- received_at: msg.receivedAt.toISOString(),
151
- mentions_bot: msg.mentionsBot,
152
- },
153
- };
154
- rpcServer.subscribers.broadcast(broadcastKey, notif);
155
- }
156
- });
157
- await gateway.start();
158
- log(`[chat-daemon] gateway start complete`);
159
- // v0.5.89 (TG.2) — startup reachability check against each
160
- // unique inbound chat_id. Catches kicked-from-supergroup (403),
161
- // stale chat_id (400), and network errors immediately at startup.
162
- // Best-effort: failures don't block daemon startup, just log
163
- // warnings so operators can see what's wrong without inspecting
164
- // hours of empty log noise. The check is Telegram-only for now;
165
- // Discord/Slack equivalents land in a follow-up if needed.
166
- try {
167
- const { verifyTelegramChats, formatReachabilityLine } = await import("./health-check.js");
168
- const { loadChatConfig } = await import("../config.js");
169
- const chatConfig = await loadChatConfig(dataRoot);
170
- const tgToken = chatConfig.telegram?.bot_token;
171
- // Collect unique chat_ids referenced across all projects' Telegram
172
- // routing (skip dm: and topic-suffixed keys — getChat takes only
173
- // the chat_id, no topic).
174
- const chatIds = new Set();
175
- const configs = await loadAllProjectChatRouting(dataRoot);
176
- for (const cfg of configs.values()) {
177
- for (const id of cfg.telegram?.inbound_chat_ids ?? [])
178
- chatIds.add(id);
179
- }
180
- if (chatIds.size > 0 && tgToken) {
181
- const results = await verifyTelegramChats(tgToken, [...chatIds]);
182
- for (const r of results)
183
- log(formatReachabilityLine(r));
184
- }
185
- }
186
- catch (err) {
187
- log(`[chat-daemon] chat-reachability check skipped (non-fatal): ${err instanceof Error ? err.message : err}`);
188
- }
189
- // RPC server listens for outbound send() calls from per-project
190
- // MCP servers. Starting it AFTER gateway.start() means clients
191
- // that connect successfully are guaranteed a fully-warmed gateway.
192
- rpcServer = new RpcServer({ gateway, dataRoot, version: "v0.7.1-phase-c" });
193
- await rpcServer.listen();
194
- // Poll the routing files every 30s so operators can edit a
195
- // chat-routing.json and have the daemon pick it up without a full
196
- // restart. Polling is the most portable option (fs.watch behavior
197
- // varies across macOS/Linux/Windows + recursive support).
198
- routingPollTimer = setInterval(() => {
199
- void (async () => {
200
- try {
201
- const next = await rebuildRoutingIndex(dataRoot);
202
- if (!sameIndex(routingIndex, next)) {
203
- routingIndex = next;
204
- log(`[chat-daemon] routing reload: ${routingIndex.size} inbound channels mapped`);
205
- }
206
- }
207
- catch (err) {
208
- log(`[chat-daemon] routing reload failed (non-fatal): ${err instanceof Error ? err.message : err}`);
209
- }
210
- })();
211
- }, 30_000);
212
- log(`[chat-daemon] rpc server listening; entering park loop`);
213
- }
214
- catch (err) {
215
- log(`[chat-daemon] FATAL: gateway start failed: ${err instanceof Error ? err.stack : err}`);
216
- await cleanup();
217
- process.exit(1);
218
- }
219
- // Signal handlers. SIGTERM = graceful, SIGINT = also graceful (for
220
- // manual `kill` during dev). Each calls cleanup() exactly once.
221
- process.on("SIGTERM", () => void shutdown("SIGTERM"));
222
- process.on("SIGINT", () => void shutdown("SIGINT"));
223
- // Park forever. process.stdin.resume() does NOT work here because
224
- // the parent spawned us with `stdio: ['ignore', ...]` — there's no
225
- // FD 0 to poll. An unresolved Promise alone won't hold the event
226
- // loop either; Node exits when nothing's scheduled. The reliable
227
- // pattern is a long-interval no-op timer (~12 days per tick); the
228
- // tick is a microsecond of CPU and easily survives clock jitter.
229
- // Signal handlers are independently registered above and still fire.
230
- setInterval(() => {
231
- /* keep-alive heartbeat */
232
- }, 1 << 30);
233
- // TypeScript demands a return path even though we never reach here.
234
- return await new Promise(() => {
235
- /* never resolves; held alive by the heartbeat interval */
236
- });
237
- }
238
- async function shutdown(signal) {
239
- if (shuttingDown)
240
- return;
241
- shuttingDown = true;
242
- log(`[chat-daemon] ${signal} received, shutting down...`);
243
- if (routingPollTimer)
244
- clearInterval(routingPollTimer);
245
- // TPS.6 patch 2 (v0.5.126) — tell subscribers we're going away
246
- // BEFORE closing the RPC server. The shutdown notification gives
247
- // MCP bridges a clean signal to back off their reconnect loop
248
- // instead of treating the disconnect as transient and immediately
249
- // retrying.
250
- try {
251
- if (rpcServer)
252
- rpcServer.subscribers.shutdown(signal);
253
- }
254
- catch (err) {
255
- log(`[chat-daemon] subscriber shutdown notify error (non-fatal): ${err instanceof Error ? err.message : err}`);
256
- }
257
- try {
258
- if (rpcServer)
259
- await rpcServer.close();
260
- }
261
- catch (err) {
262
- log(`[chat-daemon] rpc close error (non-fatal): ${err instanceof Error ? err.message : err}`);
263
- }
264
- try {
265
- if (gateway)
266
- await gateway.shutdown();
267
- }
268
- catch (err) {
269
- log(`[chat-daemon] gateway.shutdown error (non-fatal): ${err instanceof Error ? err.message : err}`);
270
- }
271
- await cleanup();
272
- log(`[chat-daemon] clean exit`);
273
- process.exit(0);
274
- }
275
- async function cleanup() {
276
- if (pidFile) {
277
- try {
278
- await fs.unlink(pidFile);
279
- }
280
- catch {
281
- /* race-tolerant */
282
- }
283
- }
284
- }
285
- function log(line) {
286
- // stdio is already redirected to the log file by the parent's spawn
287
- // options; plain console.log lands in the right place.
288
- process.stdout.write(`${new Date().toISOString()} ${line}\n`);
289
- }
290
- async function rebuildRoutingIndex(dataRoot) {
291
- const cfgs = await loadAllProjectChatRouting(dataRoot);
292
- const { recordCollision } = await import("./collisions.js");
293
- return buildRoutingIndex(cfgs, (info) => {
294
- log(`[chat-daemon] routing collision: ${info.channel_key} existing=${info.existing_uuid} newcomer=${info.newcomer_uuid} (latter wins)`);
295
- // Fire-and-forget: persist + notify happen async; the routing
296
- // rebuild must not wait. recordCollision swallows its own errors
297
- // (stderr-logged) so this `void` is intentional.
298
- void recordCollision({
299
- info,
300
- dataRoot,
301
- ...(gateway !== null ? { gateway } : {}),
302
- });
303
- });
304
- }
305
- function sameIndex(a, b) {
306
- if (a.size !== b.size)
307
- return false;
308
- for (const [k, v] of a) {
309
- if (b.get(k) !== v)
310
- return false;
311
- }
312
- return true;
313
- }
@@ -1 +0,0 @@
1
- {"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src.legacy/chat/daemon/worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAqB,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AAC/F,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,IAAI,OAAO,GAAuB,IAAI,CAAC;AACvC,IAAI,SAAS,GAAqB,IAAI,CAAC;AACvC,IAAI,YAAY,GAAiB,IAAI,GAAG,EAAE,CAAC;AAC3C,IAAI,gBAAgB,GAA0B,IAAI,CAAC;AACnD,IAAI,OAAO,GAAkB,IAAI,CAAC;AAClC,IAAI,YAAY,GAAG,KAAK,CAAC;AAEzB,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAiB;IACrD,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACpC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAExB,oEAAoE;IACpE,gEAAgE;IAChE,qEAAqE;IACrE,qCAAqC;IACrC,MAAM,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,OAAO,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;IAExD,GAAG,CAAC,mCAAmC,OAAO,CAAC,GAAG,QAAQ,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAE3E,6DAA6D;IAC7D,8DAA8D;IAC9D,kDAAkD;IAClD,IAAI,CAAC;QACH,iEAAiE;QACjE,+DAA+D;QAC/D,kEAAkE;QAClE,IAAI,CAAC;YACH,MAAM,EAAE,yBAAyB,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;YACnE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,yBAAyB,CAAC,QAAQ,CAAC,CAAC;YAC9D,MAAM,KAAK,GAAa,EAAE,CAAC;YAC3B,IAAI,OAAO,CAAC,QAAQ;gBAAE,KAAK,CAAC,IAAI,CAAC,YAAY,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACjE,IAAI,OAAO,CAAC,OAAO;gBAAE,KAAK,CAAC,IAAI,CAAC,WAAW,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;YAC9D,IAAI,OAAO,CAAC,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,aAAa,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;YACpE,IAAI,OAAO,CAAC,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,aAAa,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;YACpE,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBACjB,GAAG,CACD,gCAAgC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,eAAe,OAAO,CAAC,aAAa,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CACzH,CAAC;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,MAAM,EAAE,CAAC;YAChB,GAAG,CACD,0DAA0D,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,CAC9G,CAAC;QACJ,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QACnD,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QACxB,GAAG,CAAC,uCAAuC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,QAAQ,EAAE,CAAC,CAAC;QACpF,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACxB,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;gBAC7B,GAAG,CAAC,8BAA8B,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;QACD,kEAAkE;QAClE,4DAA4D;QAC5D,4DAA4D;QAC5D,uBAAuB;QACvB,YAAY,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QACnD,GAAG,CAAC,sCAAsC,YAAY,CAAC,IAAI,0BAA0B,CAAC,CAAC;QACvF,OAAO,CAAC,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC9B,uDAAuD;YACvD,EAAE;YACF,kBAAkB;YAClB,mEAAmE;YACnE,gEAAgE;YAChE,qEAAqE;YACrE,+DAA+D;YAC/D,oEAAoE;YACpE,iEAAiE;YACjE,oEAAoE;YACpE,oEAAoE;YACpE,4BAA4B;YAC5B,uCAAuC;YACvC,EAAE;YACF,iEAAiE;YACjE,iEAAiE;YACjE,mEAAmE;YACnE,kEAAkE;YAClE,kEAAkE;YAClE,4BAA4B;YAC5B,MAAM,IAAI,GACR,GAAG,CAAC,QAAQ,KAAK,UAAU,IAAI,GAAG,CAAC,OAAO,KAAK,YAAY,GAAG,CAAC,QAAQ,EAAE,CAAC;YAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,eAAe,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YAC1D,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;YACxE,MAAM,WAAW,GACf,CAAC,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC7C,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;gBACnD,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC;gBAC7B,IAAI,CAAC;YACP,IAAI,CAAC;gBACH,MAAM,CAAC,GAAG,MAAM,aAAa,CAAC,GAAG,EAAE,WAAW,EAAE,QAAQ,CAAC,CAAC;gBAC1D,GAAG,CACD,yBAAyB,GAAG,CAAC,OAAO,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,EAAE,CACvJ,CAAC;YACJ,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,GAAG,CACD,yCAAyC,GAAG,CAAC,OAAO,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CACpG,CAAC;YACJ,CAAC;QACH,CAAC,CAAC,CAAC;QACH,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACtB,GAAG,CAAC,sCAAsC,CAAC,CAAC;QAE5C,2DAA2D;QAC3D,gEAAgE;QAChE,kEAAkE;QAClE,6DAA6D;QAC7D,gEAAgE;QAChE,gEAAgE;QAChE,2DAA2D;QAC3D,IAAI,CAAC;YACH,MAAM,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,GAAG,MAAM,MAAM,CAClE,mBAAmB,CACpB,CAAC;YACF,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;YACxD,MAAM,UAAU,GAAG,MAAM,cAAc,CAAC,QAAQ,CAAC,CAAC;YAClD,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC;YAC/C,mEAAmE;YACnE,iEAAiE;YACjE,0BAA0B;YAC1B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;YAClC,MAAM,OAAO,GAAG,MAAM,yBAAyB,CAAC,QAAQ,CAAC,CAAC;YAC1D,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,QAAQ,EAAE,gBAAgB,IAAI,EAAE;oBAAE,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACzE,CAAC;YACD,IAAI,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,OAAO,EAAE,CAAC;gBAChC,MAAM,OAAO,GAAG,MAAM,mBAAmB,CAAC,OAAO,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;gBACjE,KAAK,MAAM,CAAC,IAAI,OAAO;oBAAE,GAAG,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAC,CAAC;YAC1D,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,GAAG,CACD,8DAA8D,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CACzG,CAAC;QACJ,CAAC;QAED,gEAAgE;QAChE,+DAA+D;QAC/D,mEAAmE;QACnE,SAAS,GAAG,IAAI,SAAS,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAC5E,MAAM,SAAS,CAAC,MAAM,EAAE,CAAC;QACzB,2DAA2D;QAC3D,kEAAkE;QAClE,kEAAkE;QAClE,0DAA0D;QAC1D,gBAAgB,GAAG,WAAW,CAAC,GAAG,EAAE;YAClC,KAAK,CAAC,KAAK,IAAI,EAAE;gBACf,IAAI,CAAC;oBACH,MAAM,IAAI,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;oBACjD,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,CAAC,EAAE,CAAC;wBACnC,YAAY,GAAG,IAAI,CAAC;wBACpB,GAAG,CAAC,iCAAiC,YAAY,CAAC,IAAI,0BAA0B,CAAC,CAAC;oBACpF,CAAC;gBACH,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,GAAG,CACD,oDAAoD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAC/F,CAAC;gBACJ,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,EAAE,MAAM,CAAC,CAAC;QACX,GAAG,CAAC,wDAAwD,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,8CAA8C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;QAC5F,MAAM,OAAO,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,mEAAmE;IACnE,gEAAgE;IAChE,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACtD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEpD,kEAAkE;IAClE,mEAAmE;IACnE,iEAAiE;IACjE,iEAAiE;IACjE,kEAAkE;IAClE,iEAAiE;IACjE,qEAAqE;IACrE,WAAW,CAAC,GAAG,EAAE;QACf,0BAA0B;IAC5B,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IAEZ,oEAAoE;IACpE,OAAO,MAAM,IAAI,OAAO,CAAQ,GAAG,EAAE;QACnC,0DAA0D;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,MAAc;IACpC,IAAI,YAAY;QAAE,OAAO;IACzB,YAAY,GAAG,IAAI,CAAC;IACpB,GAAG,CAAC,iBAAiB,MAAM,6BAA6B,CAAC,CAAC;IAC1D,IAAI,gBAAgB;QAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACtD,IAAI,CAAC;QACH,IAAI,SAAS;YAAE,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;IACzC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CAAC,8CAA8C,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC;IAChG,CAAC;IACD,IAAI,CAAC;QACH,IAAI,OAAO;YAAE,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,GAAG,CACD,qDAAqD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAChG,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,EAAE,CAAC;IAChB,GAAG,CAAC,0BAA0B,CAAC,CAAC;IAChC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,OAAO;IACpB,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,mBAAmB;QACrB,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,GAAG,CAAC,IAAY;IACvB,oEAAoE;IACpE,uDAAuD;IACvD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,IAAI,IAAI,CAAC,CAAC;AAChE,CAAC;AAED,KAAK,UAAU,mBAAmB,CAAC,QAAiB;IAClD,MAAM,IAAI,GAAG,MAAM,yBAAyB,CAAC,QAAQ,CAAC,CAAC;IACvD,OAAO,iBAAiB,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,+BAA+B,GAAG,EAAE,CAAC,CAAC,CAAC;AACrF,CAAC;AAED,SAAS,SAAS,CAAC,CAAe,EAAE,CAAe;IACjD,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACpC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IACnC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -1,324 +0,0 @@
1
- /**
2
- * workspace-topic.ts — workspace → forum-topic binding primitive
3
- * (TPS.3 / v0.5.120+).
4
- *
5
- * Solves: given a workspace (cwd-resolved uuid + path) and a target
6
- * supergroup chat_id, ensure the workspace has a bound forum topic +
7
- * persist the binding to its `chat-routing.json`. Idempotent.
8
- *
9
- * Used by:
10
- * - TPS.4 — `opensquid setup chat` wizard step (mode: "wizard")
11
- * - TPS.6 — daemon auto-boot on MCP subscribe (mode: "auto-boot")
12
- *
13
- * Concurrency: protected by `proper-lockfile` on the per-project
14
- * `chat-routing.json` so two concurrent invocations for the same
15
- * workspace can't race-create two topics. The lock window covers
16
- * (load existing config) → (call createTopic if missing) → (write
17
- * updated config). Lock retries are bounded; if a stale lock from a
18
- * crashed prior run blocks acquisition, an `LOCKED` error is thrown
19
- * upward — callers (wizard, auto-boot) surface this to the user
20
- * rather than papering over it.
21
- *
22
- * Error propagation: this module does NOT swallow errors. RPC failures
23
- * (bot not admin, network), parse errors, and lock failures all
24
- * propagate. Callers decide how to surface them (TPS.4 prints to the
25
- * wizard, TPS.6 logs + falls back to general topic).
26
- *
27
- * Rebuild path: same as adapters/telegram.ts — see that file's header
28
- * for the ad-hoc tsc invocation. `pnpm build` does NOT recompile this
29
- * file; the chat-daemon worker loads dist/chat/daemon/workspace-topic.js
30
- * at runtime.
31
- */
32
- import { promises as fs } from "node:fs";
33
- import { createRequire } from "node:module";
34
- import * as path from "node:path";
35
- import * as lockfile from "proper-lockfile";
36
- // Need synchronous require() to construct the rpc-client lazily without
37
- // making resolveOrCreateTopic's signature async-on-import. ESM Node 20+
38
- // exposes createRequire for exactly this case.
39
- const requireCJS = createRequire(import.meta.url);
40
- import { loadAllProjectChatRouting, loadProjectChatRouting, projectChatRoutingPath, } from "./routing.js";
41
- // ---------------------------------------------------------------------
42
- // Main entrypoint
43
- // ---------------------------------------------------------------------
44
- export async function resolveOrCreateTopic(args) {
45
- const routingPath = projectChatRoutingPath(args.workspaceUuid, args.dataRoot);
46
- // Lockfile lives next to the routing file; proper-lockfile handles
47
- // both lock acquisition and the necessary parent-dir creation logic
48
- // as long as the target exists. Ensure the dir exists first.
49
- await fs.mkdir(path.dirname(routingPath), { recursive: true });
50
- // proper-lockfile requires the target file to exist; touch it
51
- // (empty config) if it doesn't, so the lock can be acquired
52
- // regardless of whether the workspace has ever had routing set up.
53
- await ensureRoutingFileExists(routingPath);
54
- // Retry tuning rationale (TPS.3 pre-research): typical createTopic
55
- // round-trip is 200-600ms (UDS + HTTPS to api.telegram.org). 8
56
- // retries with 1.5× backoff at 50ms-800ms gives ~2.4s headroom —
57
- // plenty for one contender to finish while another waits. Stale is
58
- // proper-lockfile's default (10s); not setting it explicitly.
59
- const release = await lockfile.lock(routingPath, {
60
- retries: { retries: 8, factor: 1.5, minTimeout: 50, maxTimeout: 800 },
61
- });
62
- try {
63
- const existing = await loadProjectChatRouting(args.workspaceUuid, args.dataRoot);
64
- assertAutoBoundInvariant(existing, routingPath);
65
- const bound = existing?.telegram?.auto_bound;
66
- if (bound && Number.isFinite(bound.topic_id) && bound.topic_id > 0) {
67
- // Idempotent — already bound, return existing.
68
- // Sanity check: if the auto_bound.workspace_uuid disagrees with
69
- // the outer uuid (the directory name), log on stderr but trust
70
- // the outer uuid as authoritative.
71
- if (bound.workspace_uuid !== args.workspaceUuid) {
72
- process.stderr.write(`[workspace-topic] auto_bound.workspace_uuid (${bound.workspace_uuid}) ≠ outer uuid (${args.workspaceUuid}) for ${routingPath}; using existing binding\n`);
73
- }
74
- return { topicId: bound.topic_id, topicName: bound.topic_name, created: false };
75
- }
76
- const name = deriveTopicName(args.workspacePath, args.workspaceUuid);
77
- const client = args.rpcClient ?? defaultRpcClient(args.dataRoot);
78
- const created = await client.createTopic({
79
- platform: "telegram",
80
- chat_id: args.chatId,
81
- name,
82
- });
83
- const nextAutoBound = {
84
- workspace_path: args.workspacePath,
85
- workspace_uuid: args.workspaceUuid,
86
- topic_id: created.message_thread_id,
87
- topic_name: created.name,
88
- created_at: new Date().toISOString(),
89
- created_by: args.mode,
90
- };
91
- const merged = {
92
- ...(existing ?? {}),
93
- telegram: {
94
- ...(existing?.telegram ?? {}),
95
- // Persist both auto_bound metadata + the actual routing field
96
- // (inbound_topic_ids) so the routing index picks up the new
97
- // binding on its next ~30s hot-reload without separate writes.
98
- inbound_topic_ids: mergeTopicIds(existing?.telegram?.inbound_topic_ids, created.message_thread_id),
99
- inbound_chat_ids: mergeChatIds(existing?.telegram?.inbound_chat_ids, args.chatId),
100
- auto_bound: nextAutoBound,
101
- },
102
- };
103
- try {
104
- await persistRoutingAtomic(routingPath, merged);
105
- }
106
- catch (persistErr) {
107
- // TPS.3 pre-research, choice #6 partial-failure compensation:
108
- // createTopic SUCCEEDED but persist FAILED — Telegram has a real
109
- // topic we cannot reference. Log it to a recovery file so the
110
- // user can clean up (delete the orphan topic manually) instead
111
- // of accumulating ghost topics on every retry. Don't try to
112
- // rollback (delete the topic) here — that requires a second
113
- // RPC call that could also fail, compounding the problem.
114
- // The user-facing surface lives in TPS.5 collision channel.
115
- await recordOrphanTopic(args.dataRoot, {
116
- chat_id: args.chatId,
117
- topic_id: created.message_thread_id,
118
- topic_name: created.name,
119
- workspace_uuid: args.workspaceUuid,
120
- workspace_path: args.workspacePath,
121
- mode: args.mode,
122
- persist_error: persistErr instanceof Error ? persistErr.message : String(persistErr),
123
- occurred_at: new Date().toISOString(),
124
- });
125
- throw persistErr;
126
- }
127
- return {
128
- topicId: created.message_thread_id,
129
- topicName: created.name,
130
- created: true,
131
- };
132
- }
133
- finally {
134
- await release();
135
- }
136
- }
137
- // ---------------------------------------------------------------------
138
- // Helpers (exported for unit tests)
139
- // ---------------------------------------------------------------------
140
- /**
141
- * Derive a deterministic, human-readable topic name from the workspace
142
- * path + uuid. The basename of the path is the most user-recognisable
143
- * part; the uuid prefix disambiguates two workspaces with the same
144
- * basename. Examples:
145
- *
146
- * deriveTopicName("/Users/slee/projects/loop", "da96385b-...") =
147
- * "loop · da96385b"
148
- * deriveTopicName("/", "abc12345-...") = "root · abc12345"
149
- */
150
- export function deriveTopicName(workspacePath, workspaceUuid) {
151
- const basenameRaw = path.basename(workspacePath) || "root";
152
- // Telegram limit per [aiogram docs](https://docs.aiogram.dev/en/latest/api/methods/create_forum_topic.html)
153
- // is 1-128 chars. Cap basename at 48 to leave headroom for the
154
- // " · 12345678" suffix (11 chars) — total max output ~59 chars.
155
- // 48 is conservative: Telegram client truncates topic-list display
156
- // at ~30-35 chars anyway. Pre-research verdict #4.
157
- const basename = basenameRaw.length > 48 ? `${basenameRaw.slice(0, 45)}...` : basenameRaw;
158
- const uuidShort = workspaceUuid.slice(0, 8);
159
- return `${basename} · ${uuidShort}`;
160
- }
161
- /**
162
- * Merge a single new topic_id into an optional existing array. Avoids
163
- * duplicates while preserving order (existing first, new last).
164
- */
165
- export function mergeTopicIds(existing, newId) {
166
- if (!existing || existing.length === 0)
167
- return [newId];
168
- if (existing.includes(newId))
169
- return existing;
170
- return [...existing, newId];
171
- }
172
- /**
173
- * Same as mergeTopicIds for chat_ids (strings).
174
- */
175
- export function mergeChatIds(existing, newId) {
176
- if (!existing || existing.length === 0)
177
- return [newId];
178
- if (existing.includes(newId))
179
- return existing;
180
- return [...existing, newId];
181
- }
182
- /**
183
- * Clear an existing auto_bound block (TPS.7 stale-topic lifecycle).
184
- * Leaves `inbound_topic_ids` alone — caller decides whether to also
185
- * scrub those (typically yes, since the stale topic_id no longer
186
- * exists). Returns true if a binding was cleared, false if none.
187
- */
188
- export async function clearBinding(args) {
189
- const routingPath = projectChatRoutingPath(args.workspaceUuid, args.dataRoot);
190
- await fs.mkdir(path.dirname(routingPath), { recursive: true });
191
- await ensureRoutingFileExists(routingPath);
192
- const release = await lockfile.lock(routingPath, {
193
- retries: { retries: 8, factor: 1.5, minTimeout: 50, maxTimeout: 800 },
194
- stale: 10_000,
195
- });
196
- try {
197
- const existing = await loadProjectChatRouting(args.workspaceUuid, args.dataRoot);
198
- if (!existing?.telegram?.auto_bound)
199
- return false;
200
- const staleTopicId = existing.telegram.auto_bound.topic_id;
201
- const next = {
202
- ...existing,
203
- telegram: {
204
- ...existing.telegram,
205
- inbound_topic_ids: (existing.telegram.inbound_topic_ids ?? []).filter((t) => t !== staleTopicId),
206
- auto_bound: undefined,
207
- },
208
- };
209
- // Drop the auto_bound key entirely (don't leave `auto_bound: undefined`
210
- // in the JSON output).
211
- if (next.telegram)
212
- delete next.telegram.auto_bound;
213
- if (next.telegram?.inbound_topic_ids?.length === 0)
214
- delete next.telegram.inbound_topic_ids;
215
- await persistRoutingAtomic(routingPath, next);
216
- return true;
217
- }
218
- finally {
219
- await release();
220
- }
221
- }
222
- /**
223
- * Find the workspace whose `auto_bound` block claims the given
224
- * (chat_id, topic_id) pair. Used by TPS.7 stale-topic recovery to
225
- * locate which workspace owned a now-stale binding so we can
226
- * `clearBinding` for the right uuid.
227
- *
228
- * Returns the workspace uuid on match, or null when:
229
- * - no project has an auto_bound block at all
230
- * - no auto_bound block matches BOTH chat_id and topic_id
231
- * - the matching project's routing config was already cleared by a
232
- * concurrent recovery (race-safe)
233
- *
234
- * Match rule: `auto_bound.topic_id === topicId` AND
235
- * `inbound_chat_ids.includes(chatId)`.
236
- * The chat_id check guards against the (rare) case of identical topic_id
237
- * numbers across two different supergroups; without it we could clear
238
- * the wrong workspace's binding.
239
- *
240
- * Lock-free read: this just scans on-disk routing configs. The actual
241
- * mutation (`clearBinding`) is lockfile-protected on the per-project
242
- * routing file, so two concurrent recoveries racing to clear the same
243
- * binding serialize cleanly — first wins, second sees no binding to
244
- * clear and returns false.
245
- */
246
- export async function findOwnerOfBinding(args) {
247
- const all = await loadAllProjectChatRouting(args.dataRoot);
248
- for (const [uuid, cfg] of all) {
249
- const bound = cfg.telegram?.auto_bound;
250
- if (!bound)
251
- continue;
252
- if (bound.topic_id !== args.topicId)
253
- continue;
254
- const inboundChats = cfg.telegram?.inbound_chat_ids ?? [];
255
- if (!inboundChats.includes(args.chatId))
256
- continue;
257
- return uuid;
258
- }
259
- return null;
260
- }
261
- // ---------------------------------------------------------------------
262
- // Internal
263
- // ---------------------------------------------------------------------
264
- async function ensureRoutingFileExists(routingPath) {
265
- try {
266
- await fs.access(routingPath);
267
- }
268
- catch {
269
- await fs.writeFile(routingPath, "{}\n", { flag: "wx" }).catch((err) => {
270
- // EEXIST is fine — another process touched it in the race window.
271
- if (err.code !== "EEXIST")
272
- throw err;
273
- });
274
- }
275
- }
276
- async function persistRoutingAtomic(routingPath, cfg) {
277
- // Write to a sibling tmp + rename for atomicity (rename(2) is atomic
278
- // on the same filesystem). Avoids partial-write reads from the daemon's
279
- // 30s reload loop.
280
- const tmp = `${routingPath}.${process.pid}.tmp`;
281
- await fs.writeFile(tmp, JSON.stringify(cfg, null, 2) + "\n", "utf8");
282
- await fs.rename(tmp, routingPath);
283
- }
284
- function defaultRpcClient(dataRoot) {
285
- // No cache: DaemonClient construction is cheap (just stores config)
286
- // and caching it across calls broke tests that switch OPENSQUID_HOME
287
- // per test. Construct fresh; pay the ~no-op cost. Pre-research
288
- // verdict #7.
289
- const { DaemonClient } = requireCJS("./rpc-client.js");
290
- return new DaemonClient(dataRoot ? { dataRoot } : {});
291
- }
292
- /**
293
- * Invariant check: if `auto_bound.topic_id` is set, it MUST appear in
294
- * `inbound_topic_ids`. Pre-research verdict #9: log a warning on
295
- * mismatch but do NOT auto-repair (preserves user-edited intent).
296
- */
297
- function assertAutoBoundInvariant(cfg, routingPath) {
298
- const bound = cfg?.telegram?.auto_bound;
299
- if (!bound)
300
- return;
301
- const inboundTopics = cfg?.telegram?.inbound_topic_ids ?? [];
302
- if (!inboundTopics.includes(bound.topic_id)) {
303
- process.stderr.write(`[workspace-topic] invariant warning: auto_bound.topic_id=${bound.topic_id} not in inbound_topic_ids=${JSON.stringify(inboundTopics)} for ${routingPath}; not auto-repairing\n`);
304
- }
305
- }
306
- async function recordOrphanTopic(dataRoot, record) {
307
- // Pre-research verdict #6: log orphans to a recovery file so the
308
- // user can clean up (delete the topic manually via Telegram client
309
- // or via a future TPS.7 cleanup tool) instead of accumulating
310
- // ghost topics on every retry. Doesn't try to delete the topic
311
- // (that's a separate RPC call that could ALSO fail, compounding).
312
- const root = dataRoot ?? process.env.OPENSQUID_HOME;
313
- if (!root)
314
- return; // best-effort: nowhere to write
315
- const recoveryPath = path.join(root, "orphan-topics.jsonl");
316
- try {
317
- await fs.mkdir(path.dirname(recoveryPath), { recursive: true });
318
- await fs.appendFile(recoveryPath, JSON.stringify(record) + "\n", "utf8");
319
- }
320
- catch {
321
- // If even the recovery write fails, give up silently. The original
322
- // persist error will propagate; that's the load-bearing surface.
323
- }
324
- }