opensquid 0.5.441 → 0.5.447

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 (380) 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/runtime/bootstrap.d.ts.map +1 -1
  11. package/dist/runtime/bootstrap.js +2 -0
  12. package/dist/runtime/bootstrap.js.map +1 -1
  13. package/dist/runtime/handoff/render.d.ts +5 -4
  14. package/dist/runtime/handoff/render.d.ts.map +1 -1
  15. package/dist/runtime/handoff/render.js +7 -7
  16. package/dist/runtime/handoff/render.js.map +1 -1
  17. package/dist/runtime/hooks/active_task_mirror.js +0 -0
  18. package/dist/runtime/hooks/apply_patch.js +0 -0
  19. package/dist/runtime/hooks/dispatch.js +0 -0
  20. package/dist/runtime/hooks/hook_output.js +0 -0
  21. package/dist/runtime/hooks/memory_reconcile.js +0 -0
  22. package/dist/runtime/hooks/new_project_detect.js +0 -0
  23. package/dist/runtime/hooks/profession_resolver.js +0 -0
  24. package/dist/runtime/hooks/scope_intent.js +0 -0
  25. package/dist/runtime/hooks/session_id.js +0 -0
  26. package/dist/runtime/hooks/session_liveness.js +0 -0
  27. package/dist/runtime/hooks/stop_drive.js +0 -0
  28. package/dist/runtime/hooks/stop_stream.js +0 -0
  29. package/dist/runtime/hooks/subagent_guard.js +0 -0
  30. package/dist/runtime/hooks/transcript.js +0 -0
  31. package/dist/runtime/hooks/transcript_tasks.js +0 -0
  32. package/dist/runtime/ralph/orchestrator.d.ts.map +1 -1
  33. package/dist/runtime/ralph/orchestrator.js +2 -1
  34. package/dist/runtime/ralph/orchestrator.js.map +1 -1
  35. package/dist/setup/cli/limits_state.d.ts.map +1 -1
  36. package/dist/setup/cli/limits_state.js +6 -40
  37. package/dist/setup/cli/limits_state.js.map +1 -1
  38. package/dist/setup/cli/pack_walk.d.ts +32 -0
  39. package/dist/setup/cli/pack_walk.d.ts.map +1 -0
  40. package/dist/setup/cli/pack_walk.js +76 -0
  41. package/dist/setup/cli/pack_walk.js.map +1 -0
  42. package/dist/setup/cli/permissions_state.d.ts.map +1 -1
  43. package/dist/setup/cli/permissions_state.js +6 -37
  44. package/dist/setup/cli/permissions_state.js.map +1 -1
  45. package/dist/setup/cli/triggers_state.d.ts.map +1 -1
  46. package/dist/setup/cli/triggers_state.js +3 -29
  47. package/dist/setup/cli/triggers_state.js.map +1 -1
  48. package/dist/workgraph/events.d.ts.map +1 -1
  49. package/dist/workgraph/events.js +10 -0
  50. package/dist/workgraph/events.js.map +1 -1
  51. package/dist/workgraph/store.d.ts.map +1 -1
  52. package/dist/workgraph/store.js +5 -0
  53. package/dist/workgraph/store.js.map +1 -1
  54. package/dist/workgraph/types.d.ts +2 -1
  55. package/dist/workgraph/types.d.ts.map +1 -1
  56. package/docs/ARCHITECTURE.md +268 -0
  57. package/package.json +5 -3
  58. package/packs/builtin/coding-flow/skills/entry-and-handoffs/skill.yaml +13 -17
  59. package/dist/anti-drift/evaluator.d.ts +0 -88
  60. package/dist/anti-drift/evaluator.d.ts.map +0 -1
  61. package/dist/anti-drift/evaluator.js +0 -417
  62. package/dist/anti-drift/evaluator.js.map +0 -1
  63. package/dist/anti-drift/evaluator.test.js +0 -78
  64. package/dist/anti-drift/rules.d.ts +0 -80
  65. package/dist/anti-drift/rules.d.ts.map +0 -1
  66. package/dist/anti-drift/rules.js +0 -368
  67. package/dist/anti-drift/rules.js.map +0 -1
  68. package/dist/anti-drift/rules.test.js +0 -213
  69. package/dist/anti-drift/state.d.ts +0 -107
  70. package/dist/anti-drift/state.d.ts.map +0 -1
  71. package/dist/anti-drift/state.js +0 -177
  72. package/dist/anti-drift/state.js.map +0 -1
  73. package/dist/anti-drift/state.test.js +0 -120
  74. package/dist/chat/adapters/discord.d.ts +0 -41
  75. package/dist/chat/adapters/discord.d.ts.map +0 -1
  76. package/dist/chat/adapters/discord.js +0 -176
  77. package/dist/chat/adapters/discord.js.map +0 -1
  78. package/dist/chat/adapters/discord.test.js +0 -25
  79. package/dist/chat/adapters/slack.d.ts +0 -43
  80. package/dist/chat/adapters/slack.d.ts.map +0 -1
  81. package/dist/chat/adapters/slack.js +0 -172
  82. package/dist/chat/adapters/slack.js.map +0 -1
  83. package/dist/chat/adapters/slack.test.js +0 -30
  84. package/dist/chat/adapters/telegram.d.ts +0 -148
  85. package/dist/chat/adapters/telegram.d.ts.map +0 -1
  86. package/dist/chat/adapters/telegram.js +0 -498
  87. package/dist/chat/adapters/telegram.js.map +0 -1
  88. package/dist/chat/adapters/telegram.test.js +0 -94
  89. package/dist/chat/config.d.ts +0 -98
  90. package/dist/chat/config.d.ts.map +0 -1
  91. package/dist/chat/config.js +0 -185
  92. package/dist/chat/config.js.map +0 -1
  93. package/dist/chat/daemon/active-project.d.ts +0 -17
  94. package/dist/chat/daemon/active-project.d.ts.map +0 -1
  95. package/dist/chat/daemon/active-project.js +0 -23
  96. package/dist/chat/daemon/active-project.js.map +0 -1
  97. package/dist/chat/daemon/autospawn.d.ts +0 -40
  98. package/dist/chat/daemon/autospawn.d.ts.map +0 -1
  99. package/dist/chat/daemon/autospawn.js +0 -129
  100. package/dist/chat/daemon/autospawn.js.map +0 -1
  101. package/dist/chat/daemon/autospawn.test.js +0 -112
  102. package/dist/chat/daemon/cli.d.ts +0 -18
  103. package/dist/chat/daemon/cli.d.ts.map +0 -1
  104. package/dist/chat/daemon/cli.js +0 -71
  105. package/dist/chat/daemon/cli.js.map +0 -1
  106. package/dist/chat/daemon/collisions.js +0 -384
  107. package/dist/chat/daemon/health-check.d.ts +0 -69
  108. package/dist/chat/daemon/health-check.d.ts.map +0 -1
  109. package/dist/chat/daemon/health-check.js +0 -112
  110. package/dist/chat/daemon/health-check.js.map +0 -1
  111. package/dist/chat/daemon/inbox-read.d.ts +0 -35
  112. package/dist/chat/daemon/inbox-read.d.ts.map +0 -1
  113. package/dist/chat/daemon/inbox-read.js +0 -75
  114. package/dist/chat/daemon/inbox-read.js.map +0 -1
  115. package/dist/chat/daemon/inbox-read.test.js +0 -97
  116. package/dist/chat/daemon/inbox.d.ts +0 -63
  117. package/dist/chat/daemon/inbox.d.ts.map +0 -1
  118. package/dist/chat/daemon/inbox.js +0 -56
  119. package/dist/chat/daemon/inbox.js.map +0 -1
  120. package/dist/chat/daemon/inbox.test.js +0 -110
  121. package/dist/chat/daemon/lifecycle.d.ts +0 -71
  122. package/dist/chat/daemon/lifecycle.d.ts.map +0 -1
  123. package/dist/chat/daemon/lifecycle.js +0 -221
  124. package/dist/chat/daemon/lifecycle.js.map +0 -1
  125. package/dist/chat/daemon/lifecycle.test.js +0 -163
  126. package/dist/chat/daemon/protocol.d.ts +0 -107
  127. package/dist/chat/daemon/protocol.d.ts.map +0 -1
  128. package/dist/chat/daemon/protocol.js +0 -54
  129. package/dist/chat/daemon/protocol.js.map +0 -1
  130. package/dist/chat/daemon/routing.d.ts +0 -140
  131. package/dist/chat/daemon/routing.d.ts.map +0 -1
  132. package/dist/chat/daemon/routing.js +0 -198
  133. package/dist/chat/daemon/routing.js.map +0 -1
  134. package/dist/chat/daemon/routing.test.js +0 -259
  135. package/dist/chat/daemon/rpc-client.d.ts +0 -45
  136. package/dist/chat/daemon/rpc-client.d.ts.map +0 -1
  137. package/dist/chat/daemon/rpc-client.js +0 -133
  138. package/dist/chat/daemon/rpc-client.js.map +0 -1
  139. package/dist/chat/daemon/rpc-server.d.ts +0 -39
  140. package/dist/chat/daemon/rpc-server.d.ts.map +0 -1
  141. package/dist/chat/daemon/rpc-server.js +0 -385
  142. package/dist/chat/daemon/rpc-server.js.map +0 -1
  143. package/dist/chat/daemon/rpc.test.js +0 -177
  144. package/dist/chat/daemon/subscribers.js +0 -257
  145. package/dist/chat/daemon/worker.d.ts +0 -27
  146. package/dist/chat/daemon/worker.d.ts.map +0 -1
  147. package/dist/chat/daemon/worker.js +0 -313
  148. package/dist/chat/daemon/worker.js.map +0 -1
  149. package/dist/chat/daemon/workspace-topic.js +0 -324
  150. package/dist/chat/env-token.d.ts +0 -60
  151. package/dist/chat/env-token.d.ts.map +0 -1
  152. package/dist/chat/env-token.js +0 -137
  153. package/dist/chat/env-token.js.map +0 -1
  154. package/dist/chat/env-token.test.js +0 -160
  155. package/dist/chat/factory.d.ts +0 -30
  156. package/dist/chat/factory.d.ts.map +0 -1
  157. package/dist/chat/factory.js +0 -50
  158. package/dist/chat/factory.js.map +0 -1
  159. package/dist/chat/factory.test.js +0 -55
  160. package/dist/chat/gateway.d.ts +0 -176
  161. package/dist/chat/gateway.d.ts.map +0 -1
  162. package/dist/chat/gateway.js +0 -146
  163. package/dist/chat/gateway.js.map +0 -1
  164. package/dist/chat/gateway.test.js +0 -192
  165. package/dist/claude-md.d.ts +0 -39
  166. package/dist/claude-md.d.ts.map +0 -1
  167. package/dist/claude-md.js +0 -113
  168. package/dist/claude-md.js.map +0 -1
  169. package/dist/claude-md.test.js +0 -91
  170. package/dist/codex/activate.d.ts +0 -66
  171. package/dist/codex/activate.d.ts.map +0 -1
  172. package/dist/codex/activate.js +0 -329
  173. package/dist/codex/activate.js.map +0 -1
  174. package/dist/codex/activate.test.js +0 -229
  175. package/dist/codex/bundled-default/bundled-default.test.js +0 -161
  176. package/dist/codex/cli-publish.test.js +0 -133
  177. package/dist/codex/cli.d.ts +0 -35
  178. package/dist/codex/cli.d.ts.map +0 -1
  179. package/dist/codex/cli.js +0 -554
  180. package/dist/codex/cli.js.map +0 -1
  181. package/dist/codex/cli.test.js +0 -277
  182. package/dist/codex/import-skill-md.d.ts +0 -53
  183. package/dist/codex/import-skill-md.d.ts.map +0 -1
  184. package/dist/codex/import-skill-md.js +0 -236
  185. package/dist/codex/import-skill-md.js.map +0 -1
  186. package/dist/codex/import-skill-md.test.js +0 -225
  187. package/dist/codex/loader.d.ts +0 -27
  188. package/dist/codex/loader.d.ts.map +0 -1
  189. package/dist/codex/loader.js +0 -86
  190. package/dist/codex/loader.js.map +0 -1
  191. package/dist/codex/loader.test.js +0 -75
  192. package/dist/codex/parse.d.ts +0 -28
  193. package/dist/codex/parse.d.ts.map +0 -1
  194. package/dist/codex/parse.js +0 -309
  195. package/dist/codex/parse.js.map +0 -1
  196. package/dist/codex/parse.test.js +0 -241
  197. package/dist/codex/store.d.ts +0 -87
  198. package/dist/codex/store.d.ts.map +0 -1
  199. package/dist/codex/store.js +0 -205
  200. package/dist/codex/store.js.map +0 -1
  201. package/dist/codex/store.test.js +0 -242
  202. package/dist/codex/types.d.ts +0 -398
  203. package/dist/codex/types.d.ts.map +0 -1
  204. package/dist/codex/types.js +0 -21
  205. package/dist/codex/types.js.map +0 -1
  206. package/dist/config.d.ts +0 -53
  207. package/dist/config.d.ts.map +0 -1
  208. package/dist/config.js +0 -202
  209. package/dist/config.js.map +0 -1
  210. package/dist/config.test.js +0 -117
  211. package/dist/engine/cli.d.ts +0 -14
  212. package/dist/engine/cli.d.ts.map +0 -1
  213. package/dist/engine/cli.js +0 -171
  214. package/dist/engine/cli.js.map +0 -1
  215. package/dist/engine/client.d.ts +0 -219
  216. package/dist/engine/client.d.ts.map +0 -1
  217. package/dist/engine/client.js +0 -312
  218. package/dist/engine/client.js.map +0 -1
  219. package/dist/engine/config.d.ts +0 -62
  220. package/dist/engine/config.d.ts.map +0 -1
  221. package/dist/engine/config.js +0 -223
  222. package/dist/engine/config.js.map +0 -1
  223. package/dist/engine/index.d.ts +0 -17
  224. package/dist/engine/index.d.ts.map +0 -1
  225. package/dist/engine/index.js +0 -16
  226. package/dist/engine/index.js.map +0 -1
  227. package/dist/engine/resolver.d.ts +0 -62
  228. package/dist/engine/resolver.d.ts.map +0 -1
  229. package/dist/engine/resolver.js +0 -103
  230. package/dist/engine/resolver.js.map +0 -1
  231. package/dist/engine/singleton.d.ts +0 -95
  232. package/dist/engine/singleton.d.ts.map +0 -1
  233. package/dist/engine/singleton.js +0 -325
  234. package/dist/engine/singleton.js.map +0 -1
  235. package/dist/engine/types.d.ts +0 -402
  236. package/dist/engine/types.d.ts.map +0 -1
  237. package/dist/engine/types.js +0 -22
  238. package/dist/engine/types.js.map +0 -1
  239. package/dist/engine-binary-resolver.js +0 -110
  240. package/dist/engine-binary-resolver.test.js +0 -61
  241. package/dist/engine-cli.js +0 -60
  242. package/dist/engine-client.js +0 -301
  243. package/dist/engine-client.test.js +0 -118
  244. package/dist/functions/chain_state.d.ts +0 -51
  245. package/dist/functions/chain_state.d.ts.map +0 -1
  246. package/dist/functions/chain_state.js +0 -59
  247. package/dist/functions/chain_state.js.map +0 -1
  248. package/dist/hooks/drift-catalog.d.ts +0 -68
  249. package/dist/hooks/drift-catalog.d.ts.map +0 -1
  250. package/dist/hooks/drift-catalog.js +0 -184
  251. package/dist/hooks/drift-catalog.js.map +0 -1
  252. package/dist/hooks/drift-catalog.test.js +0 -154
  253. package/dist/hooks/drift-patterns.d.ts +0 -110
  254. package/dist/hooks/drift-patterns.d.ts.map +0 -1
  255. package/dist/hooks/drift-patterns.js +0 -289
  256. package/dist/hooks/drift-patterns.js.map +0 -1
  257. package/dist/hooks/drift-patterns.test.js +0 -325
  258. package/dist/hooks/engine-vocab-gate.d.ts +0 -108
  259. package/dist/hooks/engine-vocab-gate.d.ts.map +0 -1
  260. package/dist/hooks/engine-vocab-gate.js +0 -225
  261. package/dist/hooks/engine-vocab-gate.js.map +0 -1
  262. package/dist/hooks/engine-vocab-gate.test.js +0 -170
  263. package/dist/hooks/heartbeat.d.ts +0 -107
  264. package/dist/hooks/heartbeat.d.ts.map +0 -1
  265. package/dist/hooks/heartbeat.js +0 -316
  266. package/dist/hooks/heartbeat.js.map +0 -1
  267. package/dist/hooks/heartbeat.test.js +0 -393
  268. package/dist/hooks/honesty-ledger-session-scope.test.js +0 -100
  269. package/dist/hooks/honesty-ledger.d.ts +0 -123
  270. package/dist/hooks/honesty-ledger.d.ts.map +0 -1
  271. package/dist/hooks/honesty-ledger.js +0 -226
  272. package/dist/hooks/honesty-ledger.js.map +0 -1
  273. package/dist/hooks/honesty-ledger.test.js +0 -466
  274. package/dist/hooks/inline-report-check.d.ts +0 -63
  275. package/dist/hooks/inline-report-check.d.ts.map +0 -1
  276. package/dist/hooks/inline-report-check.js +0 -88
  277. package/dist/hooks/inline-report-check.js.map +0 -1
  278. package/dist/hooks/inline-report-check.test.js +0 -96
  279. package/dist/hooks/pre-tool-use.d.ts +0 -62
  280. package/dist/hooks/pre-tool-use.d.ts.map +0 -1
  281. package/dist/hooks/pre-tool-use.js +0 -342
  282. package/dist/hooks/pre-tool-use.js.map +0 -1
  283. package/dist/hooks/pre-tool-use.test.js +0 -134
  284. package/dist/hooks/session-end.d.ts +0 -15
  285. package/dist/hooks/session-end.d.ts.map +0 -1
  286. package/dist/hooks/session-end.js +0 -60
  287. package/dist/hooks/session-end.js.map +0 -1
  288. package/dist/hooks/session-end.test.js +0 -52
  289. package/dist/hooks/stop.d.ts +0 -35
  290. package/dist/hooks/stop.d.ts.map +0 -1
  291. package/dist/hooks/stop.js +0 -136
  292. package/dist/hooks/stop.js.map +0 -1
  293. package/dist/hooks/transcript-active-task.test.js +0 -342
  294. package/dist/hooks/transcript.d.ts +0 -26
  295. package/dist/hooks/transcript.d.ts.map +0 -1
  296. package/dist/hooks/transcript.js +0 -266
  297. package/dist/hooks/transcript.js.map +0 -1
  298. package/dist/hooks/transcript.test.js +0 -103
  299. package/dist/hooks/user-prompt-submit.d.ts +0 -74
  300. package/dist/hooks/user-prompt-submit.d.ts.map +0 -1
  301. package/dist/hooks/user-prompt-submit.js +0 -256
  302. package/dist/hooks/user-prompt-submit.js.map +0 -1
  303. package/dist/hooks/user-prompt-submit.test.js +0 -118
  304. package/dist/hooks/versioning-gate.d.ts +0 -101
  305. package/dist/hooks/versioning-gate.d.ts.map +0 -1
  306. package/dist/hooks/versioning-gate.js +0 -245
  307. package/dist/hooks/versioning-gate.js.map +0 -1
  308. package/dist/hooks/versioning-gate.test.js +0 -368
  309. package/dist/hooks/workflow-gate.d.ts +0 -64
  310. package/dist/hooks/workflow-gate.d.ts.map +0 -1
  311. package/dist/hooks/workflow-gate.js +0 -152
  312. package/dist/hooks/workflow-gate.js.map +0 -1
  313. package/dist/hooks/workflow-gate.test.js +0 -197
  314. package/dist/hooks-cli.d.ts +0 -25
  315. package/dist/hooks-cli.d.ts.map +0 -1
  316. package/dist/hooks-cli.js +0 -286
  317. package/dist/hooks-cli.js.map +0 -1
  318. package/dist/hooks-cli.test.js +0 -148
  319. package/dist/origin.d.ts +0 -16
  320. package/dist/origin.d.ts.map +0 -1
  321. package/dist/origin.js +0 -92
  322. package/dist/origin.js.map +0 -1
  323. package/dist/packs/seed_lessons_ingest.d.ts +0 -30
  324. package/dist/packs/seed_lessons_ingest.d.ts.map +0 -1
  325. package/dist/packs/seed_lessons_ingest.js +0 -107
  326. package/dist/packs/seed_lessons_ingest.js.map +0 -1
  327. package/dist/project-cli.d.ts +0 -7
  328. package/dist/project-cli.d.ts.map +0 -1
  329. package/dist/project-cli.js +0 -145
  330. package/dist/project-cli.js.map +0 -1
  331. package/dist/project.d.ts +0 -127
  332. package/dist/project.d.ts.map +0 -1
  333. package/dist/project.js +0 -281
  334. package/dist/project.js.map +0 -1
  335. package/dist/project.test.js +0 -287
  336. package/dist/rag/backends/loop_engine.d.ts +0 -61
  337. package/dist/rag/backends/loop_engine.d.ts.map +0 -1
  338. package/dist/rag/backends/loop_engine.js +0 -160
  339. package/dist/rag/backends/loop_engine.js.map +0 -1
  340. package/dist/recall.d.ts +0 -82
  341. package/dist/recall.d.ts.map +0 -1
  342. package/dist/recall.js +0 -81
  343. package/dist/recall.js.map +0 -1
  344. package/dist/runtime/agent_bridge/autospawn.d.ts +0 -131
  345. package/dist/runtime/agent_bridge/autospawn.d.ts.map +0 -1
  346. package/dist/runtime/agent_bridge/autospawn.js +0 -251
  347. package/dist/runtime/agent_bridge/autospawn.js.map +0 -1
  348. package/dist/runtime/chain_state.d.ts +0 -124
  349. package/dist/runtime/chain_state.d.ts.map +0 -1
  350. package/dist/runtime/chain_state.js +0 -189
  351. package/dist/runtime/chain_state.js.map +0 -1
  352. package/dist/runtime/hooks/permission_decision.d.ts +0 -34
  353. package/dist/runtime/hooks/permission_decision.d.ts.map +0 -1
  354. package/dist/runtime/hooks/permission_decision.js +0 -39
  355. package/dist/runtime/hooks/permission_decision.js.map +0 -1
  356. package/dist/runtime/workflow_fsm.d.ts +0 -21
  357. package/dist/runtime/workflow_fsm.d.ts.map +0 -1
  358. package/dist/runtime/workflow_fsm.js +0 -25
  359. package/dist/runtime/workflow_fsm.js.map +0 -1
  360. package/dist/runtime/workflow_map.d.ts +0 -26
  361. package/dist/runtime/workflow_map.d.ts.map +0 -1
  362. package/dist/runtime/workflow_map.js +0 -38
  363. package/dist/runtime/workflow_map.js.map +0 -1
  364. package/dist/scope.d.ts +0 -48
  365. package/dist/scope.d.ts.map +0 -1
  366. package/dist/scope.js +0 -111
  367. package/dist/scope.js.map +0 -1
  368. package/dist/setup/cli/topic_create_step.d.ts +0 -84
  369. package/dist/setup/cli/topic_create_step.d.ts.map +0 -1
  370. package/dist/setup/cli/topic_create_step.js +0 -213
  371. package/dist/setup/cli/topic_create_step.js.map +0 -1
  372. package/dist/system-export.d.ts +0 -65
  373. package/dist/system-export.d.ts.map +0 -1
  374. package/dist/system-export.js +0 -194
  375. package/dist/system-export.js.map +0 -1
  376. package/dist/utterance/classifier.d.ts +0 -53
  377. package/dist/utterance/classifier.d.ts.map +0 -1
  378. package/dist/utterance/classifier.js +0 -184
  379. package/dist/utterance/classifier.js.map +0 -1
  380. 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":"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"}
@@ -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
- }