@opengsd/gsd-pi 1.3.0-dev.eed73bea → 1.4.0-dev.5fc22c6f

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 (550) hide show
  1. package/README.md +11 -11
  2. package/dist/cli.js +17 -4
  3. package/dist/headless-answers.js +1 -1
  4. package/dist/headless-ui.d.ts +1 -1
  5. package/dist/headless-ui.js +1 -1
  6. package/dist/headless.js +1 -1
  7. package/dist/onboarding.js +11 -0
  8. package/dist/resources/.managed-resources-content-hash +1 -1
  9. package/dist/resources/GSD-WORKFLOW.md +20 -25
  10. package/dist/resources/extensions/claude-code-cli/models.js +9 -0
  11. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +61 -50
  12. package/dist/resources/extensions/cursor-cli/index.js +16 -0
  13. package/dist/resources/extensions/cursor-cli/models.js +63 -0
  14. package/dist/resources/extensions/cursor-cli/package.json +11 -0
  15. package/dist/resources/extensions/cursor-cli/readiness.js +128 -0
  16. package/dist/resources/extensions/cursor-cli/stream-adapter.js +353 -0
  17. package/dist/resources/extensions/gsd/activity-log.js +4 -1
  18. package/dist/resources/extensions/gsd/artifact-verification.js +431 -0
  19. package/dist/resources/extensions/gsd/auto/session.js +11 -0
  20. package/dist/resources/extensions/gsd/auto/verification-retry-policy.js +1 -1
  21. package/dist/resources/extensions/gsd/auto-artifact-paths.js +28 -1
  22. package/dist/resources/extensions/gsd/auto-budget.js +5 -1
  23. package/dist/resources/extensions/gsd/auto-dispatch.js +32 -11
  24. package/dist/resources/extensions/gsd/auto-model-selection.js +36 -18
  25. package/dist/resources/extensions/gsd/auto-post-unit.js +1 -1
  26. package/dist/resources/extensions/gsd/auto-prompts.js +83 -25
  27. package/dist/resources/extensions/gsd/auto-recovery.js +9 -517
  28. package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -5
  29. package/dist/resources/extensions/gsd/auto-start.js +4 -3
  30. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -3
  31. package/dist/resources/extensions/gsd/auto-verification.js +5 -2
  32. package/dist/resources/extensions/gsd/auto-worktree.js +38 -11
  33. package/dist/resources/extensions/gsd/auto.js +32 -10
  34. package/dist/resources/extensions/gsd/bootstrap/core-session-tools.js +39 -0
  35. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +8 -4
  36. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  37. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +147 -23
  38. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +56 -36
  39. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +29 -5
  40. package/dist/resources/extensions/gsd/closeout-consistency-gate.js +3 -1
  41. package/dist/resources/extensions/gsd/codebase-generator.js +157 -19
  42. package/dist/resources/extensions/gsd/commands/handlers/auto.js +1 -1
  43. package/dist/resources/extensions/gsd/commands/handlers/core.js +40 -4
  44. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +154 -2
  45. package/dist/resources/extensions/gsd/commands-usage.js +1 -1
  46. package/dist/resources/extensions/gsd/commands-worktree.js +11 -2
  47. package/dist/resources/extensions/gsd/db/engine.js +4 -1
  48. package/dist/resources/extensions/gsd/db/queries.js +39 -0
  49. package/dist/resources/extensions/gsd/db-memory-fts-schema.js +34 -0
  50. package/dist/resources/extensions/gsd/db-workspace.js +55 -3
  51. package/dist/resources/extensions/gsd/delegation-policy.js +1 -1
  52. package/dist/resources/extensions/gsd/discussion-handoff.js +8 -3
  53. package/dist/resources/extensions/gsd/docs/preferences-reference.md +10 -4
  54. package/dist/resources/extensions/gsd/doctor-engine-checks.js +227 -5
  55. package/dist/resources/extensions/gsd/doctor-providers.js +38 -10
  56. package/dist/resources/extensions/gsd/doctor-runtime-checks.js +3 -0
  57. package/dist/resources/extensions/gsd/doctor.js +93 -3
  58. package/dist/resources/extensions/gsd/exec-sandbox.js +14 -4
  59. package/dist/resources/extensions/gsd/flat-phase-migration.js +119 -23
  60. package/dist/resources/extensions/gsd/git-service.js +61 -8
  61. package/dist/resources/extensions/gsd/gitignore.js +4 -1
  62. package/dist/resources/extensions/gsd/guidance.js +1 -0
  63. package/dist/resources/extensions/gsd/guided-flow-queue.js +20 -13
  64. package/dist/resources/extensions/gsd/guided-flow.js +40 -2
  65. package/dist/resources/extensions/gsd/json-persistence.js +20 -0
  66. package/dist/resources/extensions/gsd/key-manager.js +1 -0
  67. package/dist/resources/extensions/gsd/knowledge-backfill.js +18 -11
  68. package/dist/resources/extensions/gsd/layout-policy.js +3 -1
  69. package/dist/resources/extensions/gsd/markdown-renderer.js +27 -15
  70. package/dist/resources/extensions/gsd/memory-backfill.js +28 -21
  71. package/dist/resources/extensions/gsd/metrics.js +4 -4
  72. package/dist/resources/extensions/gsd/migrate-external.js +22 -5
  73. package/dist/resources/extensions/gsd/migration-auto-check.js +22 -0
  74. package/dist/resources/extensions/gsd/milestone-ids.js +32 -2
  75. package/dist/resources/extensions/gsd/orphan-stash-audit.js +92 -16
  76. package/dist/resources/extensions/gsd/preferences-diagnostics.js +24 -1
  77. package/dist/resources/extensions/gsd/preferences-models.js +6 -4
  78. package/dist/resources/extensions/gsd/preferences-validation.js +25 -2
  79. package/dist/resources/extensions/gsd/preferences.js +29 -2
  80. package/dist/resources/extensions/gsd/prompts/code-review.md +6 -4
  81. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  82. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -3
  83. package/dist/resources/extensions/gsd/redact-secrets.js +37 -0
  84. package/dist/resources/extensions/gsd/repository-registry.js +6 -1
  85. package/dist/resources/extensions/gsd/root-write-leak-guard.js +7 -1
  86. package/dist/resources/extensions/gsd/safe-fs.js +23 -2
  87. package/dist/resources/extensions/gsd/safety/destructive-guard.js +8 -1
  88. package/dist/resources/extensions/gsd/safety/evidence-collector.js +16 -1
  89. package/dist/resources/extensions/gsd/safety/file-change-validator.js +5 -2
  90. package/dist/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  91. package/dist/resources/extensions/gsd/skills/gsd-headless/references/commands.md +2 -1
  92. package/dist/resources/extensions/gsd/state/derive/cache.js +28 -0
  93. package/dist/resources/extensions/gsd/state/derive/db-open.js +39 -0
  94. package/dist/resources/extensions/gsd/state/derive/from-db.js +452 -0
  95. package/dist/resources/extensions/gsd/state/derive/index.js +75 -0
  96. package/dist/resources/extensions/gsd/state/derive/interrupted-work.js +21 -0
  97. package/dist/resources/extensions/gsd/state-reconciliation/drift/stale-render.js +27 -33
  98. package/dist/resources/extensions/gsd/state-reconciliation/index.js +48 -23
  99. package/dist/resources/extensions/gsd/state-reconciliation/registry.js +32 -28
  100. package/dist/resources/extensions/gsd/state.js +12 -611
  101. package/dist/resources/extensions/gsd/tools/complete-milestone.js +4 -2
  102. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -34
  103. package/dist/resources/extensions/gsd/tools/complete-task.js +8 -41
  104. package/dist/resources/extensions/gsd/tools/plan-slice.js +59 -44
  105. package/dist/resources/extensions/gsd/tools/plan-task.js +103 -12
  106. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +40 -0
  107. package/dist/resources/extensions/gsd/undo.js +43 -6
  108. package/dist/resources/extensions/gsd/unit-context-manifest.js +5 -4
  109. package/dist/resources/extensions/gsd/unit-registry.js +11 -3
  110. package/dist/resources/extensions/gsd/unit-runtime.js +21 -0
  111. package/dist/resources/extensions/gsd/verification-gate.js +7 -0
  112. package/dist/resources/extensions/gsd/workflow-manifest.js +126 -13
  113. package/dist/resources/extensions/gsd/workflow-projections.js +19 -14
  114. package/dist/resources/extensions/gsd/workspace.js +26 -8
  115. package/dist/resources/extensions/gsd/worktree-manager.js +140 -5
  116. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  117. package/dist/web/standalone/.next/BUILD_ID +1 -1
  118. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  119. package/dist/web/standalone/.next/build-manifest.json +3 -3
  120. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  121. package/dist/web/standalone/.next/react-loadable-manifest.json +9 -9
  122. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  123. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  124. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  128. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  129. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  130. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  132. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  133. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  134. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  135. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  136. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  137. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  138. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  139. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  140. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  141. package/dist/web/standalone/.next/server/app/index.html +1 -1
  142. package/dist/web/standalone/.next/server/app/index.rsc +2 -2
  143. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  144. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  145. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  146. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  147. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  148. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  149. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  150. package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
  151. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  152. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  153. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  154. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  155. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  156. package/dist/web/standalone/.next/static/chunks/{2659.b7b129ee6a769448.js → 2659.58e950899a9bb82f.js} +1 -1
  157. package/dist/web/standalone/.next/static/chunks/2772.a7c1fcc69a4685ef.js +1 -0
  158. package/dist/web/standalone/.next/static/chunks/{3616.3c60753b8ffcbd2e.js → 3616.61a2af74bb8833c8.js} +1 -1
  159. package/dist/web/standalone/.next/static/chunks/{4283.8e446784528ed9dc.js → 4283.d0d9e0a955e441cb.js} +1 -1
  160. package/dist/web/standalone/.next/static/chunks/{5826.a46ecdd1cfe8dabc.js → 5826.5421d66c72b9f34e.js} +1 -1
  161. package/dist/web/standalone/.next/static/chunks/{8785.481aa5869991b760.js → 8785.e29b3134cab1d153.js} +1 -1
  162. package/dist/web/standalone/.next/static/chunks/8937.640dc9c2aaa1dfad.js +10 -0
  163. package/dist/web/standalone/.next/static/chunks/app/{page-6644fc6ee8ca1247.js → page-72a856634ad14c10.js} +1 -1
  164. package/dist/web/standalone/.next/static/chunks/webpack-9c401904f87ded16.js +1 -0
  165. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  166. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  167. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  168. package/dist/wizard.d.ts +8 -0
  169. package/dist/wizard.js +8 -1
  170. package/integrations/hermes/__init__.py +31 -0
  171. package/integrations/hermes/tests/test_directory_loader.py +37 -0
  172. package/package.json +8 -8
  173. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts +1 -0
  174. package/packages/cloud-mcp-gateway/dist/auth-store.d.ts.map +1 -1
  175. package/packages/cloud-mcp-gateway/dist/auth-store.js +21 -1
  176. package/packages/cloud-mcp-gateway/dist/auth-store.js.map +1 -1
  177. package/packages/cloud-mcp-gateway/dist/auth-store.test.js +34 -0
  178. package/packages/cloud-mcp-gateway/dist/auth-store.test.js.map +1 -1
  179. package/packages/cloud-mcp-gateway/package.json +2 -2
  180. package/packages/contracts/package.json +1 -1
  181. package/packages/daemon/dist/cloud-runtime.d.ts +2 -0
  182. package/packages/daemon/dist/cloud-runtime.d.ts.map +1 -1
  183. package/packages/daemon/dist/cloud-runtime.js +25 -1
  184. package/packages/daemon/dist/cloud-runtime.js.map +1 -1
  185. package/packages/daemon/dist/cloud-runtime.test.js +51 -0
  186. package/packages/daemon/dist/cloud-runtime.test.js.map +1 -1
  187. package/packages/daemon/dist/message-batcher.d.ts +7 -0
  188. package/packages/daemon/dist/message-batcher.d.ts.map +1 -1
  189. package/packages/daemon/dist/message-batcher.js +13 -0
  190. package/packages/daemon/dist/message-batcher.js.map +1 -1
  191. package/packages/daemon/dist/message-batcher.test.js +48 -0
  192. package/packages/daemon/dist/message-batcher.test.js.map +1 -1
  193. package/packages/daemon/package.json +5 -5
  194. package/packages/gsd-agent-core/dist/agent-session.d.ts +1 -0
  195. package/packages/gsd-agent-core/dist/agent-session.d.ts.map +1 -1
  196. package/packages/gsd-agent-core/dist/agent-session.js +3 -0
  197. package/packages/gsd-agent-core/dist/agent-session.js.map +1 -1
  198. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts +13 -1
  199. package/packages/gsd-agent-core/dist/compaction/compaction.d.ts.map +1 -1
  200. package/packages/gsd-agent-core/dist/compaction/compaction.js +15 -1
  201. package/packages/gsd-agent-core/dist/compaction/compaction.js.map +1 -1
  202. package/packages/gsd-agent-core/dist/extension-ui-snapshot.d.ts +41 -0
  203. package/packages/gsd-agent-core/dist/extension-ui-snapshot.d.ts.map +1 -0
  204. package/packages/gsd-agent-core/dist/extension-ui-snapshot.js +62 -0
  205. package/packages/gsd-agent-core/dist/extension-ui-snapshot.js.map +1 -0
  206. package/packages/gsd-agent-core/dist/index.d.ts +2 -0
  207. package/packages/gsd-agent-core/dist/index.d.ts.map +1 -1
  208. package/packages/gsd-agent-core/dist/index.js +2 -0
  209. package/packages/gsd-agent-core/dist/index.js.map +1 -1
  210. package/packages/gsd-agent-core/dist/session/agent-session-events.js +1 -1
  211. package/packages/gsd-agent-core/dist/session/agent-session-events.js.map +1 -1
  212. package/packages/gsd-agent-core/dist/session/agent-session-host.d.ts +1 -0
  213. package/packages/gsd-agent-core/dist/session/agent-session-host.d.ts.map +1 -1
  214. package/packages/gsd-agent-core/dist/session/agent-session-host.js.map +1 -1
  215. package/packages/gsd-agent-core/dist/session/agent-session-prompt.d.ts +5 -0
  216. package/packages/gsd-agent-core/dist/session/agent-session-prompt.d.ts.map +1 -1
  217. package/packages/gsd-agent-core/dist/session/agent-session-prompt.js +60 -3
  218. package/packages/gsd-agent-core/dist/session/agent-session-prompt.js.map +1 -1
  219. package/packages/gsd-agent-core/dist/transcript-store.d.ts +58 -0
  220. package/packages/gsd-agent-core/dist/transcript-store.d.ts.map +1 -0
  221. package/packages/gsd-agent-core/dist/transcript-store.js +132 -0
  222. package/packages/gsd-agent-core/dist/transcript-store.js.map +1 -0
  223. package/packages/gsd-agent-core/package.json +5 -5
  224. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts +2 -0
  225. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  226. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js +25 -11
  227. package/packages/gsd-agent-modes/dist/modes/interactive/components/tool-execution.js.map +1 -1
  228. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller-latency.d.ts +4 -0
  229. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller-latency.d.ts.map +1 -0
  230. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller-latency.js +7 -0
  231. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller-latency.js.map +1 -0
  232. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts +3 -24
  233. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  234. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js +26 -841
  235. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  236. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-handoff-filter.d.ts +58 -0
  237. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-handoff-filter.d.ts.map +1 -0
  238. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-handoff-filter.js +312 -0
  239. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-handoff-filter.js.map +1 -0
  240. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-pinned-zone.d.ts +31 -0
  241. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-pinned-zone.d.ts.map +1 -0
  242. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-pinned-zone.js +130 -0
  243. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-pinned-zone.js.map +1 -0
  244. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.d.ts +15 -0
  245. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.d.ts.map +1 -0
  246. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js +259 -0
  247. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-segment-walker.js.map +1 -0
  248. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-tool-rollup.d.ts +13 -0
  249. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-tool-rollup.d.ts.map +1 -0
  250. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-tool-rollup.js +118 -0
  251. package/packages/gsd-agent-modes/dist/modes/interactive/controllers/chat-tool-rollup.js.map +1 -0
  252. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-state.d.ts +9 -0
  253. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  254. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-state.js +1 -1
  255. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  256. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-ui-state.d.ts +54 -0
  257. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-ui-state.d.ts.map +1 -0
  258. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-ui-state.js +20 -0
  259. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode-ui-state.js.map +1 -0
  260. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts +4 -1
  261. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  262. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js +9 -0
  263. package/packages/gsd-agent-modes/dist/modes/interactive/interactive-mode.js.map +1 -1
  264. package/packages/gsd-agent-modes/dist/modes/interactive/streaming-render-state.d.ts +56 -0
  265. package/packages/gsd-agent-modes/dist/modes/interactive/streaming-render-state.d.ts.map +1 -0
  266. package/packages/gsd-agent-modes/dist/modes/interactive/streaming-render-state.js +44 -0
  267. package/packages/gsd-agent-modes/dist/modes/interactive/streaming-render-state.js.map +1 -0
  268. package/packages/gsd-agent-modes/dist/modes/interactive/tui-transcript-tracker.d.ts +5 -0
  269. package/packages/gsd-agent-modes/dist/modes/interactive/tui-transcript-tracker.d.ts.map +1 -0
  270. package/packages/gsd-agent-modes/dist/modes/interactive/tui-transcript-tracker.js +77 -0
  271. package/packages/gsd-agent-modes/dist/modes/interactive/tui-transcript-tracker.js.map +1 -0
  272. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts +2 -0
  273. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  274. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js +24 -6
  275. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-client.js.map +1 -1
  276. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  277. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js +18 -0
  278. package/packages/gsd-agent-modes/dist/modes/rpc/rpc-mode.js.map +1 -1
  279. package/packages/gsd-agent-modes/package.json +7 -7
  280. package/packages/mcp-server/dist/workflow-tools.js +1 -1
  281. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  282. package/packages/mcp-server/package.json +4 -4
  283. package/packages/native/package.json +1 -1
  284. package/packages/pi-agent-core/dist/agent-loop.js +3 -2
  285. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  286. package/packages/pi-agent-core/package.json +1 -1
  287. package/packages/pi-ai/dist/env-api-keys.d.ts.map +1 -1
  288. package/packages/pi-ai/dist/env-api-keys.js +3 -0
  289. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  290. package/packages/pi-ai/dist/models.generated.d.ts +46 -0
  291. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  292. package/packages/pi-ai/dist/models.generated.js +38 -0
  293. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  294. package/packages/pi-ai/dist/types.d.ts +2 -2
  295. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  296. package/packages/pi-ai/dist/types.js.map +1 -1
  297. package/packages/pi-ai/package.json +1 -1
  298. package/packages/pi-coding-agent/README.md +3 -2
  299. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  300. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  301. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  302. package/packages/pi-coding-agent/dist/core/session-manager-context.d.ts +9 -0
  303. package/packages/pi-coding-agent/dist/core/session-manager-context.d.ts.map +1 -0
  304. package/packages/pi-coding-agent/dist/core/session-manager-context.js +94 -0
  305. package/packages/pi-coding-agent/dist/core/session-manager-context.js.map +1 -0
  306. package/packages/pi-coding-agent/dist/core/session-manager-list.d.ts +8 -0
  307. package/packages/pi-coding-agent/dist/core/session-manager-list.d.ts.map +1 -0
  308. package/packages/pi-coding-agent/dist/core/session-manager-list.js +244 -0
  309. package/packages/pi-coding-agent/dist/core/session-manager-list.js.map +1 -0
  310. package/packages/pi-coding-agent/dist/core/session-manager-migration.d.ts +12 -0
  311. package/packages/pi-coding-agent/dist/core/session-manager-migration.d.ts.map +1 -0
  312. package/packages/pi-coding-agent/dist/core/session-manager-migration.js +84 -0
  313. package/packages/pi-coding-agent/dist/core/session-manager-migration.js.map +1 -0
  314. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +135 -0
  315. package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -0
  316. package/packages/pi-coding-agent/dist/core/session-manager-types.js +2 -0
  317. package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -0
  318. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +6 -154
  319. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  320. package/packages/pi-coding-agent/dist/core/session-manager.js +22 -459
  321. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  322. package/packages/pi-coding-agent/dist/theme/theme-schema.d.ts +75 -75
  323. package/packages/pi-coding-agent/dist/theme/theme-schema.d.ts.map +1 -1
  324. package/packages/pi-coding-agent/dist/theme/theme-schema.js +1 -1
  325. package/packages/pi-coding-agent/dist/theme/theme-schema.js.map +1 -1
  326. package/packages/pi-coding-agent/dist/theme/theme.d.ts.map +1 -1
  327. package/packages/pi-coding-agent/dist/theme/theme.js +11 -7
  328. package/packages/pi-coding-agent/dist/theme/theme.js.map +1 -1
  329. package/packages/pi-coding-agent/package.json +7 -7
  330. package/packages/pi-tui/package.json +2 -2
  331. package/packages/rpc-client/dist/rpc-client.js +1 -1
  332. package/packages/rpc-client/dist/rpc-client.js.map +1 -1
  333. package/packages/rpc-client/package.json +2 -2
  334. package/pkg/dist/theme/theme-schema.d.ts +75 -75
  335. package/pkg/dist/theme/theme-schema.d.ts.map +1 -1
  336. package/pkg/dist/theme/theme-schema.js +1 -1
  337. package/pkg/dist/theme/theme-schema.js.map +1 -1
  338. package/pkg/dist/theme/theme.d.ts.map +1 -1
  339. package/pkg/dist/theme/theme.js +11 -7
  340. package/pkg/dist/theme/theme.js.map +1 -1
  341. package/pkg/package.json +1 -1
  342. package/scripts/link-workspace-packages.cjs +14 -4
  343. package/src/resources/GSD-WORKFLOW.md +20 -25
  344. package/src/resources/extensions/claude-code-cli/models.ts +9 -0
  345. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +67 -49
  346. package/src/resources/extensions/claude-code-cli/tests/index.test.ts +14 -0
  347. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +159 -0
  348. package/src/resources/extensions/cursor-cli/index.ts +18 -0
  349. package/src/resources/extensions/cursor-cli/models.ts +65 -0
  350. package/src/resources/extensions/cursor-cli/package.json +11 -0
  351. package/src/resources/extensions/cursor-cli/readiness.ts +143 -0
  352. package/src/resources/extensions/cursor-cli/stream-adapter.ts +440 -0
  353. package/src/resources/extensions/cursor-cli/tests/index.test.ts +54 -0
  354. package/src/resources/extensions/cursor-cli/tests/readiness.test.ts +29 -0
  355. package/src/resources/extensions/cursor-cli/tests/stream-adapter.test.ts +166 -0
  356. package/src/resources/extensions/gsd/activity-log.ts +4 -1
  357. package/src/resources/extensions/gsd/artifact-verification.ts +468 -0
  358. package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
  359. package/src/resources/extensions/gsd/auto/session.ts +14 -0
  360. package/src/resources/extensions/gsd/auto/verification-retry-policy.ts +3 -1
  361. package/src/resources/extensions/gsd/auto-artifact-paths.ts +47 -1
  362. package/src/resources/extensions/gsd/auto-budget.ts +12 -1
  363. package/src/resources/extensions/gsd/auto-dispatch.ts +34 -12
  364. package/src/resources/extensions/gsd/auto-model-selection.ts +49 -18
  365. package/src/resources/extensions/gsd/auto-post-unit.ts +1 -1
  366. package/src/resources/extensions/gsd/auto-prompts.ts +80 -25
  367. package/src/resources/extensions/gsd/auto-recovery.ts +13 -518
  368. package/src/resources/extensions/gsd/auto-runtime-state.ts +4 -5
  369. package/src/resources/extensions/gsd/auto-start.ts +4 -3
  370. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +7 -2
  371. package/src/resources/extensions/gsd/auto-verification.ts +5 -1
  372. package/src/resources/extensions/gsd/auto-worktree.ts +50 -11
  373. package/src/resources/extensions/gsd/auto.ts +36 -8
  374. package/src/resources/extensions/gsd/bootstrap/core-session-tools.ts +44 -0
  375. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +8 -4
  376. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  377. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +171 -23
  378. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +49 -1
  379. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +60 -37
  380. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +39 -5
  381. package/src/resources/extensions/gsd/closeout-consistency-gate.ts +2 -1
  382. package/src/resources/extensions/gsd/codebase-generator.ts +179 -18
  383. package/src/resources/extensions/gsd/commands/handlers/auto.ts +1 -1
  384. package/src/resources/extensions/gsd/commands/handlers/core.ts +39 -3
  385. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +136 -2
  386. package/src/resources/extensions/gsd/commands-usage.ts +1 -1
  387. package/src/resources/extensions/gsd/commands-worktree.ts +13 -2
  388. package/src/resources/extensions/gsd/db/engine.ts +8 -1
  389. package/src/resources/extensions/gsd/db/queries.ts +40 -0
  390. package/src/resources/extensions/gsd/db-memory-fts-schema.ts +45 -0
  391. package/src/resources/extensions/gsd/db-workspace.ts +61 -3
  392. package/src/resources/extensions/gsd/delegation-policy.ts +1 -1
  393. package/src/resources/extensions/gsd/discussion-handoff.ts +10 -2
  394. package/src/resources/extensions/gsd/docs/preferences-reference.md +10 -4
  395. package/src/resources/extensions/gsd/doctor-engine-checks.ts +275 -5
  396. package/src/resources/extensions/gsd/doctor-providers.ts +39 -10
  397. package/src/resources/extensions/gsd/doctor-runtime-checks.ts +3 -0
  398. package/src/resources/extensions/gsd/doctor-types.ts +8 -1
  399. package/src/resources/extensions/gsd/doctor.ts +95 -3
  400. package/src/resources/extensions/gsd/exec-sandbox.ts +13 -4
  401. package/src/resources/extensions/gsd/flat-phase-migration.ts +142 -22
  402. package/src/resources/extensions/gsd/git-service.ts +70 -9
  403. package/src/resources/extensions/gsd/gitignore.ts +4 -1
  404. package/src/resources/extensions/gsd/guidance.ts +2 -0
  405. package/src/resources/extensions/gsd/guided-flow-queue.ts +20 -13
  406. package/src/resources/extensions/gsd/guided-flow.ts +45 -2
  407. package/src/resources/extensions/gsd/json-persistence.ts +20 -0
  408. package/src/resources/extensions/gsd/key-manager.ts +1 -0
  409. package/src/resources/extensions/gsd/knowledge-backfill.ts +16 -10
  410. package/src/resources/extensions/gsd/layout-policy.ts +2 -1
  411. package/src/resources/extensions/gsd/markdown-renderer.ts +27 -17
  412. package/src/resources/extensions/gsd/memory-backfill.ts +26 -20
  413. package/src/resources/extensions/gsd/metrics.ts +4 -4
  414. package/src/resources/extensions/gsd/migrate-external.ts +17 -4
  415. package/src/resources/extensions/gsd/migration-auto-check.ts +23 -0
  416. package/src/resources/extensions/gsd/milestone-ids.ts +31 -2
  417. package/src/resources/extensions/gsd/model-router.ts +4 -5
  418. package/src/resources/extensions/gsd/orphan-stash-audit.ts +108 -20
  419. package/src/resources/extensions/gsd/preferences-diagnostics.ts +24 -0
  420. package/src/resources/extensions/gsd/preferences-models.ts +6 -3
  421. package/src/resources/extensions/gsd/preferences-validation.ts +28 -2
  422. package/src/resources/extensions/gsd/preferences.ts +38 -4
  423. package/src/resources/extensions/gsd/prompts/code-review.md +6 -4
  424. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -2
  425. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -3
  426. package/src/resources/extensions/gsd/redact-secrets.ts +40 -0
  427. package/src/resources/extensions/gsd/repository-registry.ts +7 -1
  428. package/src/resources/extensions/gsd/root-write-leak-guard.ts +13 -1
  429. package/src/resources/extensions/gsd/safe-fs.ts +22 -2
  430. package/src/resources/extensions/gsd/safety/destructive-guard.ts +9 -1
  431. package/src/resources/extensions/gsd/safety/evidence-collector.ts +16 -1
  432. package/src/resources/extensions/gsd/safety/file-change-validator.ts +8 -2
  433. package/src/resources/extensions/gsd/skills/gsd-headless/SKILL.md +7 -9
  434. package/src/resources/extensions/gsd/skills/gsd-headless/references/commands.md +2 -1
  435. package/src/resources/extensions/gsd/state/derive/cache.ts +46 -0
  436. package/src/resources/extensions/gsd/state/derive/db-open.ts +45 -0
  437. package/src/resources/extensions/gsd/state/derive/from-db.ts +561 -0
  438. package/src/resources/extensions/gsd/state/derive/index.ts +104 -0
  439. package/src/resources/extensions/gsd/state/derive/interrupted-work.ts +31 -0
  440. package/src/resources/extensions/gsd/state-reconciliation/drift/stale-render.ts +36 -30
  441. package/src/resources/extensions/gsd/state-reconciliation/index.ts +50 -24
  442. package/src/resources/extensions/gsd/state-reconciliation/registry.ts +43 -28
  443. package/src/resources/extensions/gsd/state.ts +32 -732
  444. package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +4 -3
  445. package/src/resources/extensions/gsd/tests/auto-artifact-paths.test.ts +98 -1
  446. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +14 -5
  447. package/src/resources/extensions/gsd/tests/auto-migrating-recovery.test.ts +18 -0
  448. package/src/resources/extensions/gsd/tests/auto-model-selection.test.ts +178 -0
  449. package/src/resources/extensions/gsd/tests/auto-pause-double-entry-guard.test.ts +36 -0
  450. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +182 -4
  451. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +35 -0
  452. package/src/resources/extensions/gsd/tests/auto-remote-session-lock-cleanup.test.ts +41 -0
  453. package/src/resources/extensions/gsd/tests/auto-retry-mcp-churn-fixes.test.ts +30 -0
  454. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +41 -0
  455. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +4 -4
  456. package/src/resources/extensions/gsd/tests/auto-start-bootstrap-await-3420.test.ts +3 -0
  457. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-harness-abort.test.ts +55 -0
  458. package/src/resources/extensions/gsd/tests/auto-warning-noise-regression.test.ts +8 -0
  459. package/src/resources/extensions/gsd/tests/batch-task-query.test.ts +85 -0
  460. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +29 -0
  461. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +365 -0
  462. package/src/resources/extensions/gsd/tests/commands-gsd-core.test.ts +1 -0
  463. package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +62 -1
  464. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +28 -0
  465. package/src/resources/extensions/gsd/tests/complete-slice-reopen-handoff.test.ts +69 -0
  466. package/src/resources/extensions/gsd/tests/db-memory-fts-schema.test.ts +58 -3
  467. package/src/resources/extensions/gsd/tests/decisions-projection-from-memories.test.ts +51 -0
  468. package/src/resources/extensions/gsd/tests/destructive-guard.test.ts +44 -0
  469. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +143 -0
  470. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +138 -1
  471. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +296 -2
  472. package/src/resources/extensions/gsd/tests/doctor-workspace.test.ts +207 -0
  473. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +16 -0
  474. package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +40 -0
  475. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +74 -2
  476. package/src/resources/extensions/gsd/tests/flat-rate-routing-guard.test.ts +87 -19
  477. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +89 -0
  478. package/src/resources/extensions/gsd/tests/gitignore-bg-shell-runtime.test.ts +4 -0
  479. package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +9 -0
  480. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +11 -2
  481. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +4 -2
  482. package/src/resources/extensions/gsd/tests/json-persistence-atomic.test.ts +47 -0
  483. package/src/resources/extensions/gsd/tests/knowledge-backfill-projection.test.ts +35 -0
  484. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +122 -0
  485. package/src/resources/extensions/gsd/tests/memory-store.test.ts +48 -0
  486. package/src/resources/extensions/gsd/tests/merge-closeout-consistency-gate.test.ts +31 -1
  487. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +6 -0
  488. package/src/resources/extensions/gsd/tests/metrics.test.ts +1 -0
  489. package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +57 -0
  490. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +70 -0
  491. package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +91 -7
  492. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +4 -1
  493. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +2 -2
  494. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +27 -0
  495. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +103 -4
  496. package/src/resources/extensions/gsd/tests/plan-task.test.ts +307 -1
  497. package/src/resources/extensions/gsd/tests/post-mutation-hook.test.ts +4 -2
  498. package/src/resources/extensions/gsd/tests/preferences.test.ts +150 -0
  499. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +142 -0
  500. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +50 -14
  501. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +58 -0
  502. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +8 -6
  503. package/src/resources/extensions/gsd/tests/recovery-verify-logs.test.ts +4 -4
  504. package/src/resources/extensions/gsd/tests/redact-secrets.test.ts +56 -0
  505. package/src/resources/extensions/gsd/tests/register-hooks-flat-phase-migration.test.ts +73 -0
  506. package/src/resources/extensions/gsd/tests/register-hooks-loop-guard-auto.test.ts +378 -0
  507. package/src/resources/extensions/gsd/tests/repository-registry.test.ts +41 -2
  508. package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +55 -0
  509. package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +20 -1
  510. package/src/resources/extensions/gsd/tests/runtime-invariant-modules.test.ts +15 -3
  511. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +105 -0
  512. package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +115 -0
  513. package/src/resources/extensions/gsd/tests/status-per-repo.test.ts +94 -0
  514. package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +35 -0
  515. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +203 -0
  516. package/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts +6 -3
  517. package/src/resources/extensions/gsd/tests/undo.test.ts +74 -0
  518. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +66 -18
  519. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  520. package/src/resources/extensions/gsd/tests/uok-gitops-turn-action.test.ts +90 -0
  521. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +53 -1
  522. package/src/resources/extensions/gsd/tests/verification-retry-policy.test.ts +69 -4
  523. package/src/resources/extensions/gsd/tests/workflow-manifest.test.ts +96 -15
  524. package/src/resources/extensions/gsd/tests/workflow-mcp-preflight.test.ts +1 -0
  525. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +8 -4
  526. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +214 -6
  527. package/src/resources/extensions/gsd/tests/workspace.test.ts +18 -4
  528. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +39 -0
  529. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +31 -1
  530. package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +32 -0
  531. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -2
  532. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -30
  533. package/src/resources/extensions/gsd/tools/complete-task.ts +7 -39
  534. package/src/resources/extensions/gsd/tools/plan-slice.ts +60 -47
  535. package/src/resources/extensions/gsd/tools/plan-task.ts +135 -12
  536. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +58 -0
  537. package/src/resources/extensions/gsd/undo.ts +47 -6
  538. package/src/resources/extensions/gsd/unit-context-manifest.ts +11 -5
  539. package/src/resources/extensions/gsd/unit-registry.ts +11 -3
  540. package/src/resources/extensions/gsd/unit-runtime.ts +42 -0
  541. package/src/resources/extensions/gsd/verification-gate.ts +8 -0
  542. package/src/resources/extensions/gsd/workflow-manifest.ts +141 -7
  543. package/src/resources/extensions/gsd/workflow-projections.ts +20 -14
  544. package/src/resources/extensions/gsd/workspace.ts +42 -8
  545. package/src/resources/extensions/gsd/worktree-manager.ts +183 -5
  546. package/dist/web/standalone/.next/static/chunks/2772.bfa657f49f955239.js +0 -1
  547. package/dist/web/standalone/.next/static/chunks/796.3976108148518f7d.js +0 -10
  548. package/dist/web/standalone/.next/static/chunks/webpack-7c1d97e39be2da11.js +0 -1
  549. /package/dist/web/standalone/.next/static/{SzEuqWX37DR9MEpEuQjP1 → kIEFsCryyGy6RzifULbJl}/_buildManifest.js +0 -0
  550. /package/dist/web/standalone/.next/static/{SzEuqWX37DR9MEpEuQjP1 → kIEFsCryyGy6RzifULbJl}/_ssgManifest.js +0 -0
@@ -4,10 +4,12 @@ export class CloudRuntime {
4
4
  cloud;
5
5
  executor;
6
6
  logger;
7
+ static MAX_OUTBOX = 200;
7
8
  socket;
8
9
  heartbeat;
9
10
  reconnect;
10
11
  inFlight = new Map();
12
+ outbox = [];
11
13
  stopped = false;
12
14
  constructor(cloud, executor, logger) {
13
15
  this.cloud = cloud;
@@ -27,6 +29,7 @@ export class CloudRuntime {
27
29
  clearInterval(this.heartbeat);
28
30
  this.heartbeat = undefined;
29
31
  this.inFlight.clear();
32
+ this.outbox = [];
30
33
  const socket = this.socket;
31
34
  this.socket = undefined;
32
35
  socket?.close();
@@ -83,7 +86,19 @@ export class CloudRuntime {
83
86
  if (socket !== this.socket)
84
87
  return;
85
88
  this.logger.info("cloud runtime connected", { gateway_url: this.cloud.gateway_url, runtime_id: this.cloud.runtime_id });
89
+ // Re-advertise projects (async: the hello is sent on a later microtask), then
90
+ // drain any messages buffered while disconnected. tool_results route by
91
+ // requestId on the authenticated connection, so drain order vs the hello is
92
+ // not significant.
86
93
  void this.advertiseProjects();
94
+ const pending = this.outbox;
95
+ this.outbox = [];
96
+ for (const text of pending) {
97
+ if (this.socket?.readyState === WebSocket.OPEN)
98
+ this.socket.send(text);
99
+ else
100
+ this.outbox.push(text);
101
+ }
87
102
  if (this.heartbeat)
88
103
  clearInterval(this.heartbeat);
89
104
  this.heartbeat = setInterval(() => this.send({ type: "heartbeat", at: Date.now() }), 30_000);
@@ -178,8 +193,17 @@ export class CloudRuntime {
178
193
  }
179
194
  }
180
195
  send(message) {
196
+ const text = JSON.stringify(message);
181
197
  if (this.socket?.readyState === WebSocket.OPEN) {
182
- this.socket.send(JSON.stringify(message));
198
+ this.socket.send(text);
199
+ return;
200
+ }
201
+ // Buffer while disconnected; flushed on reconnect in handleSocketOpen. Bounded
202
+ // so a long outage cannot grow memory without limit — a stale heartbeat is
203
+ // worth less than a fresh tool_result, so drop oldest first.
204
+ this.outbox.push(text);
205
+ if (this.outbox.length > CloudRuntime.MAX_OUTBOX) {
206
+ this.outbox.shift();
183
207
  }
184
208
  }
185
209
  }
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-runtime.js","sourceRoot":"","sources":["../src/cloud-runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import WebSocket from \"ws\";\nimport type { Logger } from \"./logger.js\";\nimport type { DaemonConfig } from \"./types.js\";\nimport type { LocalToolExecutor } from \"./local-tool-executor.js\";\nimport { createGatewayLookup, parseCloudGatewayUrl, validateGatewayNetworkTarget } from \"./cloud-config.js\";\n\ninterface GatewayMessage {\n type: string;\n requestId?: string;\n toolName?: string;\n args?: Record<string, unknown>;\n projectAlias?: string;\n}\n\nexport class CloudRuntime {\n private socket: WebSocket | undefined;\n private heartbeat: ReturnType<typeof setInterval> | undefined;\n private reconnect: ReturnType<typeof setTimeout> | undefined;\n private readonly inFlight = new Map<string, GatewayMessage>();\n private stopped = false;\n\n constructor(\n private readonly cloud: NonNullable<DaemonConfig[\"cloud\"]>,\n private readonly executor: LocalToolExecutor,\n private readonly logger: Logger,\n ) {}\n\n start(): void {\n this.stopped = false;\n this.connect();\n }\n\n stop(): void {\n this.stopped = true;\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = undefined;\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = undefined;\n this.inFlight.clear();\n const socket = this.socket;\n this.socket = undefined;\n socket?.close();\n }\n\n private connect(): void {\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = undefined;\n if (!this.cloud.device_token || !this.cloud.runtime_id) {\n this.logger.warn(\"cloud runtime skipped — missing device token or runtime id\");\n return;\n }\n const gatewayUrl = parseCloudGatewayUrl(this.cloud.gateway_url);\n try {\n validateGatewayNetworkTarget(gatewayUrl);\n } catch (err) {\n this.logger.warn(\"cloud runtime skipped unsafe gateway URL\", {\n error: err instanceof Error ? err.message : String(err),\n });\n return;\n }\n const url = new URL(\"/runtime/connect\", gatewayUrl);\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n const socket = new WebSocket(url, {\n headers: { Authorization: `Bearer ${this.cloud.device_token}` },\n lookup: createGatewayLookup(gatewayUrl),\n });\n const previousSocket = this.socket;\n this.socket = socket;\n if (previousSocket) {\n // Detach the old socket's handlers before closing so its listeners don't\n // linger on a socket we've already replaced (handlers also guard on\n // identity, but this releases them eagerly for GC).\n previousSocket.removeAllListeners();\n if (previousSocket.readyState !== WebSocket.CLOSING && previousSocket.readyState !== WebSocket.CLOSED) {\n previousSocket.close();\n }\n }\n\n socket.on(\"open\", () => {\n this.handleSocketOpen(socket);\n });\n socket.on(\"message\", (data) => {\n void this.handleSocketMessage(socket, data.toString(\"utf8\"));\n });\n socket.on(\"close\", () => {\n this.handleSocketClose(socket);\n });\n socket.on(\"error\", (err) => {\n this.handleSocketError(socket, err);\n });\n }\n\n private handleSocketOpen(socket: WebSocket): void {\n if (socket !== this.socket) return;\n this.logger.info(\"cloud runtime connected\", { gateway_url: this.cloud.gateway_url, runtime_id: this.cloud.runtime_id });\n void this.advertiseProjects();\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = setInterval(() => this.send({ type: \"heartbeat\", at: Date.now() }), 30_000);\n }\n\n private async handleSocketMessage(socket: WebSocket, text: string): Promise<void> {\n if (socket !== this.socket) return;\n await this.handleMessage(text);\n }\n\n private handleSocketClose(socket: WebSocket): void {\n if (socket !== this.socket) return;\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = undefined;\n this.socket = undefined;\n if (!this.stopped) {\n this.logger.warn(\"cloud runtime disconnected; reconnecting\");\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = setTimeout(() => this.connect(), 5_000);\n }\n }\n\n private handleSocketError(socket: WebSocket, err: Error): void {\n if (socket !== this.socket) return;\n this.logger.warn(\"cloud runtime socket error\", { error: err.message });\n }\n\n private async advertiseProjects(): Promise<void> {\n const projects = await this.executor.advertisedProjects();\n this.send({\n type: \"hello\",\n runtimeId: this.cloud.runtime_id,\n runtimeName: this.cloud.runtime_name,\n projects,\n });\n }\n\n private async handleMessage(text: string): Promise<void> {\n let message: GatewayMessage;\n try {\n message = JSON.parse(text) as GatewayMessage;\n } catch {\n return;\n }\n if (message.type === \"cancel\" && message.requestId) {\n void this.cancelInFlight(message.requestId);\n return;\n }\n if (message.type !== \"tool_call\" || !message.requestId || !message.toolName) return;\n this.inFlight.set(message.requestId, message);\n try {\n const result = await this.executor.execute(message.toolName, message.args ?? {}, message.projectAlias);\n if (!this.inFlight.has(message.requestId)) return;\n this.send({ type: \"tool_result\", requestId: message.requestId, result });\n } catch (err) {\n if (!this.inFlight.has(message.requestId)) return;\n this.send({\n type: \"tool_result\",\n requestId: message.requestId,\n error: err instanceof Error ? err.message : String(err),\n });\n } finally {\n this.inFlight.delete(message.requestId);\n }\n }\n\n private async cancelInFlight(requestId: string): Promise<void> {\n const pending = this.inFlight.get(requestId);\n if (!pending) return;\n this.inFlight.delete(requestId);\n try {\n if (typeof pending.args?.sessionId === \"string\") {\n await this.executor.execute(\"gsd_cancel\", { sessionId: pending.args.sessionId }, pending.projectAlias);\n return;\n }\n const projectDir = typeof pending.args?.projectDir === \"string\" ? pending.args.projectDir : pending.projectAlias;\n if (projectDir) {\n await this.executor.execute(\"gsd_cancel\", { projectDir });\n }\n } catch (err) {\n this.logger.warn(\"cloud runtime cancel failed\", {\n requestId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n private send(message: unknown): void {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(JSON.stringify(message));\n }\n }\n}\n"]}
1
+ {"version":3,"file":"cloud-runtime.js","sourceRoot":"","sources":["../src/cloud-runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import WebSocket from \"ws\";\nimport type { Logger } from \"./logger.js\";\nimport type { DaemonConfig } from \"./types.js\";\nimport type { LocalToolExecutor } from \"./local-tool-executor.js\";\nimport { createGatewayLookup, parseCloudGatewayUrl, validateGatewayNetworkTarget } from \"./cloud-config.js\";\n\ninterface GatewayMessage {\n type: string;\n requestId?: string;\n toolName?: string;\n args?: Record<string, unknown>;\n projectAlias?: string;\n}\n\nexport class CloudRuntime {\n private static readonly MAX_OUTBOX = 200;\n private socket: WebSocket | undefined;\n private heartbeat: ReturnType<typeof setInterval> | undefined;\n private reconnect: ReturnType<typeof setTimeout> | undefined;\n private readonly inFlight = new Map<string, GatewayMessage>();\n private outbox: string[] = [];\n private stopped = false;\n\n constructor(\n private readonly cloud: NonNullable<DaemonConfig[\"cloud\"]>,\n private readonly executor: LocalToolExecutor,\n private readonly logger: Logger,\n ) {}\n\n start(): void {\n this.stopped = false;\n this.connect();\n }\n\n stop(): void {\n this.stopped = true;\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = undefined;\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = undefined;\n this.inFlight.clear();\n this.outbox = [];\n const socket = this.socket;\n this.socket = undefined;\n socket?.close();\n }\n\n private connect(): void {\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = undefined;\n if (!this.cloud.device_token || !this.cloud.runtime_id) {\n this.logger.warn(\"cloud runtime skipped — missing device token or runtime id\");\n return;\n }\n const gatewayUrl = parseCloudGatewayUrl(this.cloud.gateway_url);\n try {\n validateGatewayNetworkTarget(gatewayUrl);\n } catch (err) {\n this.logger.warn(\"cloud runtime skipped unsafe gateway URL\", {\n error: err instanceof Error ? err.message : String(err),\n });\n return;\n }\n const url = new URL(\"/runtime/connect\", gatewayUrl);\n url.protocol = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n const socket = new WebSocket(url, {\n headers: { Authorization: `Bearer ${this.cloud.device_token}` },\n lookup: createGatewayLookup(gatewayUrl),\n });\n const previousSocket = this.socket;\n this.socket = socket;\n if (previousSocket) {\n // Detach the old socket's handlers before closing so its listeners don't\n // linger on a socket we've already replaced (handlers also guard on\n // identity, but this releases them eagerly for GC).\n previousSocket.removeAllListeners();\n if (previousSocket.readyState !== WebSocket.CLOSING && previousSocket.readyState !== WebSocket.CLOSED) {\n previousSocket.close();\n }\n }\n\n socket.on(\"open\", () => {\n this.handleSocketOpen(socket);\n });\n socket.on(\"message\", (data) => {\n void this.handleSocketMessage(socket, data.toString(\"utf8\"));\n });\n socket.on(\"close\", () => {\n this.handleSocketClose(socket);\n });\n socket.on(\"error\", (err) => {\n this.handleSocketError(socket, err);\n });\n }\n\n private handleSocketOpen(socket: WebSocket): void {\n if (socket !== this.socket) return;\n this.logger.info(\"cloud runtime connected\", { gateway_url: this.cloud.gateway_url, runtime_id: this.cloud.runtime_id });\n // Re-advertise projects (async: the hello is sent on a later microtask), then\n // drain any messages buffered while disconnected. tool_results route by\n // requestId on the authenticated connection, so drain order vs the hello is\n // not significant.\n void this.advertiseProjects();\n const pending = this.outbox;\n this.outbox = [];\n for (const text of pending) {\n if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(text);\n else this.outbox.push(text);\n }\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = setInterval(() => this.send({ type: \"heartbeat\", at: Date.now() }), 30_000);\n }\n\n private async handleSocketMessage(socket: WebSocket, text: string): Promise<void> {\n if (socket !== this.socket) return;\n await this.handleMessage(text);\n }\n\n private handleSocketClose(socket: WebSocket): void {\n if (socket !== this.socket) return;\n if (this.heartbeat) clearInterval(this.heartbeat);\n this.heartbeat = undefined;\n this.socket = undefined;\n if (!this.stopped) {\n this.logger.warn(\"cloud runtime disconnected; reconnecting\");\n if (this.reconnect) clearTimeout(this.reconnect);\n this.reconnect = setTimeout(() => this.connect(), 5_000);\n }\n }\n\n private handleSocketError(socket: WebSocket, err: Error): void {\n if (socket !== this.socket) return;\n this.logger.warn(\"cloud runtime socket error\", { error: err.message });\n }\n\n private async advertiseProjects(): Promise<void> {\n const projects = await this.executor.advertisedProjects();\n this.send({\n type: \"hello\",\n runtimeId: this.cloud.runtime_id,\n runtimeName: this.cloud.runtime_name,\n projects,\n });\n }\n\n private async handleMessage(text: string): Promise<void> {\n let message: GatewayMessage;\n try {\n message = JSON.parse(text) as GatewayMessage;\n } catch {\n return;\n }\n if (message.type === \"cancel\" && message.requestId) {\n void this.cancelInFlight(message.requestId);\n return;\n }\n if (message.type !== \"tool_call\" || !message.requestId || !message.toolName) return;\n this.inFlight.set(message.requestId, message);\n try {\n const result = await this.executor.execute(message.toolName, message.args ?? {}, message.projectAlias);\n if (!this.inFlight.has(message.requestId)) return;\n this.send({ type: \"tool_result\", requestId: message.requestId, result });\n } catch (err) {\n if (!this.inFlight.has(message.requestId)) return;\n this.send({\n type: \"tool_result\",\n requestId: message.requestId,\n error: err instanceof Error ? err.message : String(err),\n });\n } finally {\n this.inFlight.delete(message.requestId);\n }\n }\n\n private async cancelInFlight(requestId: string): Promise<void> {\n const pending = this.inFlight.get(requestId);\n if (!pending) return;\n this.inFlight.delete(requestId);\n try {\n if (typeof pending.args?.sessionId === \"string\") {\n await this.executor.execute(\"gsd_cancel\", { sessionId: pending.args.sessionId }, pending.projectAlias);\n return;\n }\n const projectDir = typeof pending.args?.projectDir === \"string\" ? pending.args.projectDir : pending.projectAlias;\n if (projectDir) {\n await this.executor.execute(\"gsd_cancel\", { projectDir });\n }\n } catch (err) {\n this.logger.warn(\"cloud runtime cancel failed\", {\n requestId,\n error: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n private send(message: unknown): void {\n const text = JSON.stringify(message);\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.socket.send(text);\n return;\n }\n // Buffer while disconnected; flushed on reconnect in handleSocketOpen. Bounded\n // so a long outage cannot grow memory without limit — a stale heartbeat is\n // worth less than a fresh tool_result, so drop oldest first.\n this.outbox.push(text);\n if (this.outbox.length > CloudRuntime.MAX_OUTBOX) {\n this.outbox.shift();\n }\n }\n}\n"]}
@@ -4,6 +4,57 @@ import { CloudRuntime } from "./cloud-runtime.js";
4
4
  function makeRuntime() {
5
5
  return new CloudRuntime({ gateway_url: "ws://127.0.0.1:1", device_token: "fixture", runtime_id: "runtime" }, { execute: async () => ({}), advertisedProjects: async () => [] }, { info: () => undefined, warn: () => undefined, error: () => undefined, debug: () => undefined });
6
6
  }
7
+ function fakeSocket(readyState = WebSocket.OPEN) {
8
+ const sent = [];
9
+ return { readyState, sent, send: (t) => sent.push(t), close: () => undefined };
10
+ }
11
+ test("cloud runtime buffers sends while disconnected and flushes them on reconnect (FIFO)", () => {
12
+ const runtime = makeRuntime();
13
+ const internals = runtime;
14
+ try {
15
+ internals.socket = undefined; // disconnected
16
+ internals.send({ type: "tool_result", requestId: "a" });
17
+ internals.send({ type: "tool_result", requestId: "b" });
18
+ assert.equal(internals.outbox.length, 2, "messages buffered, not dropped");
19
+ const socket = fakeSocket();
20
+ internals.socket = socket;
21
+ internals.handleSocketOpen(socket); // triggers advertise (send) + drain
22
+ // advertiseProjects sends a hello first, then the two buffered results in order.
23
+ const results = socket.sent.filter((t) => t.includes("tool_result"));
24
+ assert.deepEqual(results.map((t) => JSON.parse(t).requestId), ["a", "b"]);
25
+ assert.equal(internals.outbox.length, 0, "outbox drained");
26
+ }
27
+ finally {
28
+ runtime.stop();
29
+ }
30
+ });
31
+ test("cloud runtime caps the outbox and drops oldest", () => {
32
+ const runtime = makeRuntime();
33
+ const internals = runtime;
34
+ try {
35
+ internals.socket = undefined;
36
+ for (let i = 0; i < 210; i += 1)
37
+ internals.send({ type: "tool_result", requestId: `r${i}` });
38
+ assert.equal(internals.outbox.length, 200, "bounded at MAX_OUTBOX");
39
+ // Oldest dropped: r0..r9 gone, newest retained.
40
+ const first = JSON.parse(internals.outbox[0]);
41
+ const last = JSON.parse(internals.outbox[internals.outbox.length - 1]);
42
+ assert.equal(first.requestId, "r10");
43
+ assert.equal(last.requestId, "r209");
44
+ }
45
+ finally {
46
+ runtime.stop();
47
+ }
48
+ });
49
+ test("cloud runtime stop() clears the outbox", () => {
50
+ const runtime = makeRuntime();
51
+ const internals = runtime;
52
+ internals.socket = undefined;
53
+ internals.send({ type: "tool_result", requestId: "x" });
54
+ assert.equal(internals.outbox.length, 1);
55
+ runtime.stop();
56
+ assert.equal(internals.outbox.length, 0);
57
+ });
7
58
  test("cloud runtime ignores stale socket close events after replacement", () => {
8
59
  const runtime = makeRuntime();
9
60
  const staleSocket = {};
@@ -1 +1 @@
1
- {"version":3,"file":"cloud-runtime.test.js","sourceRoot":"","sources":["../src/cloud-runtime.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,SAAS,WAAW;IAClB,OAAO,IAAI,YAAY,CACrB,EAAE,WAAW,EAAE,kBAAkB,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,EACnF,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,kBAAkB,EAAE,KAAK,IAAI,EAAE,CAAC,EAAE,EAAW,EAC1E,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,EAAW,CAC1G,CAAC;AACJ,CAAC;AAED,IAAI,CAAC,mEAAmE,EAAE,GAAG,EAAE;IAC7E,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,MAAM,WAAW,GAAG,EAAE,CAAC;IACvB,MAAM,YAAY,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,SAAS,EAAE,CAAC;IAChD,MAAM,SAAS,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IACvD,IAAI,CAAC;QACH,MAAM,CAAC,MAAM,CAAC,OAAoF,EAAE;YAClG,MAAM,EAAE,YAAY;YACpB,SAAS;SACV,CAAC,CAAC;QAEF,OAAuE,CAAC,iBAAiB,CAAC,WAAW,CAAC,CAAC;QAExG,MAAM,KAAK,GAAG,OAIb,CAAC;QACF,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACzC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC3C,CAAC;YAAS,CAAC;QACT,aAAa,CAAC,SAAS,CAAC,CAAC;QACzB,OAAO,CAAC,IAAI,EAAE,CAAC;IACjB,CAAC;AACH,CAAC,CAAC,CAAC","sourcesContent":["import { test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { CloudRuntime } from \"./cloud-runtime.js\";\n\nfunction makeRuntime(): CloudRuntime {\n return new CloudRuntime(\n { gateway_url: \"ws://127.0.0.1:1\", device_token: \"fixture\", runtime_id: \"runtime\" },\n { execute: async () => ({}), advertisedProjects: async () => [] } as never,\n { info: () => undefined, warn: () => undefined, error: () => undefined, debug: () => undefined } as never,\n );\n}\n\ntest(\"cloud runtime ignores stale socket close events after replacement\", () => {\n const runtime = makeRuntime();\n const staleSocket = {};\n const activeSocket = { close: () => undefined };\n const heartbeat = setInterval(() => undefined, 30_000);\n try {\n Object.assign(runtime as unknown as { socket: unknown; heartbeat: ReturnType<typeof setInterval> }, {\n socket: activeSocket,\n heartbeat,\n });\n\n (runtime as unknown as { handleSocketClose: (socket: unknown) => void }).handleSocketClose(staleSocket);\n\n const state = runtime as unknown as {\n socket: unknown;\n heartbeat: ReturnType<typeof setInterval> | undefined;\n reconnect: ReturnType<typeof setTimeout> | undefined;\n };\n assert.equal(state.socket, activeSocket);\n assert.equal(state.heartbeat, heartbeat);\n assert.equal(state.reconnect, undefined);\n } finally {\n clearInterval(heartbeat);\n runtime.stop();\n }\n});\n"]}
1
+ {"version":3,"file":"cloud-runtime.test.js","sourceRoot":"","sources":["../src/cloud-runtime.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { CloudRuntime } from \"./cloud-runtime.js\";\n\nfunction makeRuntime(): CloudRuntime {\n return new CloudRuntime(\n { gateway_url: \"ws://127.0.0.1:1\", device_token: \"fixture\", runtime_id: \"runtime\" },\n { execute: async () => ({}), advertisedProjects: async () => [] } as never,\n { info: () => undefined, warn: () => undefined, error: () => undefined, debug: () => undefined } as never,\n );\n}\n\ntype FakeSocket = { readyState: number; sent: string[]; send: (t: string) => void; close: () => void };\nfunction fakeSocket(readyState = WebSocket.OPEN): FakeSocket {\n const sent: string[] = [];\n return { readyState, sent, send: (t: string) => sent.push(t), close: () => undefined };\n}\ntype RuntimeInternals = {\n socket: FakeSocket | undefined;\n outbox: string[];\n send: (message: unknown) => void;\n handleSocketOpen: (socket: unknown) => void;\n};\n\ntest(\"cloud runtime buffers sends while disconnected and flushes them on reconnect (FIFO)\", () => {\n const runtime = makeRuntime();\n const internals = runtime as unknown as RuntimeInternals;\n try {\n internals.socket = undefined; // disconnected\n internals.send({ type: \"tool_result\", requestId: \"a\" });\n internals.send({ type: \"tool_result\", requestId: \"b\" });\n assert.equal(internals.outbox.length, 2, \"messages buffered, not dropped\");\n\n const socket = fakeSocket();\n internals.socket = socket;\n internals.handleSocketOpen(socket); // triggers advertise (send) + drain\n // advertiseProjects sends a hello first, then the two buffered results in order.\n const results = socket.sent.filter((t) => t.includes(\"tool_result\"));\n assert.deepEqual(\n results.map((t) => (JSON.parse(t) as { requestId: string }).requestId),\n [\"a\", \"b\"],\n );\n assert.equal(internals.outbox.length, 0, \"outbox drained\");\n } finally {\n runtime.stop();\n }\n});\n\ntest(\"cloud runtime caps the outbox and drops oldest\", () => {\n const runtime = makeRuntime();\n const internals = runtime as unknown as RuntimeInternals;\n try {\n internals.socket = undefined;\n for (let i = 0; i < 210; i += 1) internals.send({ type: \"tool_result\", requestId: `r${i}` });\n assert.equal(internals.outbox.length, 200, \"bounded at MAX_OUTBOX\");\n // Oldest dropped: r0..r9 gone, newest retained.\n const first = JSON.parse(internals.outbox[0]) as { requestId: string };\n const last = JSON.parse(internals.outbox[internals.outbox.length - 1]) as { requestId: string };\n assert.equal(first.requestId, \"r10\");\n assert.equal(last.requestId, \"r209\");\n } finally {\n runtime.stop();\n }\n});\n\ntest(\"cloud runtime stop() clears the outbox\", () => {\n const runtime = makeRuntime();\n const internals = runtime as unknown as RuntimeInternals;\n internals.socket = undefined;\n internals.send({ type: \"tool_result\", requestId: \"x\" });\n assert.equal(internals.outbox.length, 1);\n runtime.stop();\n assert.equal(internals.outbox.length, 0);\n});\n\ntest(\"cloud runtime ignores stale socket close events after replacement\", () => {\n const runtime = makeRuntime();\n const staleSocket = {};\n const activeSocket = { close: () => undefined };\n const heartbeat = setInterval(() => undefined, 30_000);\n try {\n Object.assign(runtime as unknown as { socket: unknown; heartbeat: ReturnType<typeof setInterval> }, {\n socket: activeSocket,\n heartbeat,\n });\n\n (runtime as unknown as { handleSocketClose: (socket: unknown) => void }).handleSocketClose(staleSocket);\n\n const state = runtime as unknown as {\n socket: unknown;\n heartbeat: ReturnType<typeof setInterval> | undefined;\n reconnect: ReturnType<typeof setTimeout> | undefined;\n };\n assert.equal(state.socket, activeSocket);\n assert.equal(state.heartbeat, heartbeat);\n assert.equal(state.reconnect, undefined);\n } finally {\n clearInterval(heartbeat);\n runtime.stop();\n }\n});\n"]}
@@ -30,16 +30,23 @@ export interface BatcherOptions {
30
30
  flushIntervalMs?: number;
31
31
  /** Max events before triggering an immediate capacity flush. Default: 4 */
32
32
  maxBatchSize?: number;
33
+ /**
34
+ * Hard cap on buffered events. When exceeded (e.g. Discord API is slow/down and
35
+ * flushes stall), the oldest events are dropped to bound memory. Default: 1000.
36
+ */
37
+ maxBufferSize?: number;
33
38
  }
34
39
  export declare class MessageBatcher {
35
40
  private readonly send;
36
41
  private readonly logger;
37
42
  private readonly flushIntervalMs;
38
43
  private readonly maxBatchSize;
44
+ private readonly maxBufferSize;
39
45
  private buffer;
40
46
  private timer;
41
47
  private flushing;
42
48
  private destroyed;
49
+ private saturated;
43
50
  constructor(send: SendFn, logger?: BatcherLogger, options?: BatcherOptions);
44
51
  /** Start the periodic flush timer. */
45
52
  start(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"message-batcher.d.ts","sourceRoot":"","sources":["../src/message-batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAMjD,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,UAAU,EAAE,OAAO,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEpE,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAgBD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IAEtC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc;IAW1E,sCAAsC;IACtC,KAAK,IAAI,IAAI;IAYb,sDAAsD;IACtD,IAAI,IAAI,IAAI;IAQZ,oEAAoE;IAC9D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI;IAQxC;;;OAGG;IACG,gBAAgB,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhE,wEAAwE;IACxE,IAAI,OAAO,IAAI,MAAM,CAEpB;IAMD;;;;OAIG;YACW,KAAK;IAanB;;;;;;;OAOG;YACW,MAAM;CA8CrB"}
1
+ {"version":3,"file":"message-batcher.d.ts","sourceRoot":"","sources":["../src/message-batcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAMjD,sFAAsF;AACtF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,UAAU,EAAE,OAAO,EAAE,CAAC;CACvB;AAED,4DAA4D;AAC5D,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAEpE,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACxD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC1D;AAED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC7B,0DAA0D;IAC1D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAgBD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,KAAK,CAA+C;IAC5D,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAS;gBAEd,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,cAAc;IAY1E,sCAAsC;IACtC,KAAK,IAAI,IAAI;IAYb,sDAAsD;IACtD,IAAI,IAAI,IAAI;IAQZ,oEAAoE;IAC9D,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ9B;;;OAGG;IACH,OAAO,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI;IAiBxC;;;OAGG;IACG,gBAAgB,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAQhE,wEAAwE;IACxE,IAAI,OAAO,IAAI,MAAM,CAEpB;IAMD;;;;OAIG;YACW,KAAK;IAcnB;;;;;;;OAOG;YACW,MAAM;CA8CrB"}
@@ -25,15 +25,18 @@ export class MessageBatcher {
25
25
  logger;
26
26
  flushIntervalMs;
27
27
  maxBatchSize;
28
+ maxBufferSize;
28
29
  buffer = [];
29
30
  timer = null;
30
31
  flushing = false;
31
32
  destroyed = false;
33
+ saturated = false;
32
34
  constructor(send, logger, options) {
33
35
  this.send = send;
34
36
  this.logger = logger ?? noopLogger;
35
37
  this.flushIntervalMs = options?.flushIntervalMs ?? 1500;
36
38
  this.maxBatchSize = options?.maxBatchSize ?? 4;
39
+ this.maxBufferSize = options?.maxBufferSize ?? 1000;
37
40
  }
38
41
  // -----------------------------------------------------------------------
39
42
  // Public API
@@ -75,6 +78,15 @@ export class MessageBatcher {
75
78
  enqueue(formatted) {
76
79
  if (this.destroyed)
77
80
  return;
81
+ if (this.buffer.length >= this.maxBufferSize) {
82
+ // Discord is not draining fast enough; drop oldest to bound memory. Blockers
83
+ // use enqueueImmediate() and never sit in this buffer, so they're unaffected.
84
+ this.buffer.shift();
85
+ if (!this.saturated) {
86
+ this.saturated = true;
87
+ this.logger.warn('Batcher buffer saturated; dropping oldest events', { maxBufferSize: this.maxBufferSize });
88
+ }
89
+ }
78
90
  this.buffer.push(formatted);
79
91
  if (this.buffer.length >= this.maxBatchSize) {
80
92
  void this.flush();
@@ -111,6 +123,7 @@ export class MessageBatcher {
111
123
  return; // prevent re-entrant flush
112
124
  this.flushing = true;
113
125
  const batch = this.buffer.splice(0); // take all
126
+ this.saturated = false; // buffer drained; allow a fresh saturation warning later
114
127
  try {
115
128
  await this.doSend(batch);
116
129
  }
@@ -1 +1 @@
1
- {"version":3,"file":"message-batcher.js","sourceRoot":"","sources":["../src/message-batcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * message-batcher.ts — Rate-limit-aware message batcher for Discord.\n *\n * Accumulates FormattedEvent payloads and flushes them to a Discord channel\n * respecting the 5 msg/5s rate limit. Supports:\n * - Timer-based periodic flush (default 1.5s)\n * - Capacity-based flush when buffer hits maxBatchSize\n * - Immediate priority flush for blockers (bypasses batching)\n * - Combining multiple embeds into a single send() call\n * - Error isolation: send() failures are logged, never crash the batcher\n */\n\nimport type { FormattedEvent } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Payload passed to the send callback — matches Discord TextChannel.send() shape. */\nexport interface SendPayload {\n content: string;\n embeds: unknown[];\n components: unknown[];\n}\n\n/** Send callback abstraction. Returns void or a promise. */\nexport type SendFn = (payload: SendPayload) => Promise<void> | void;\n\n/** Logger interface — just needs error/warn/debug. */\nexport interface BatcherLogger {\n error(msg: string, data?: Record<string, unknown>): void;\n warn(msg: string, data?: Record<string, unknown>): void;\n debug(msg: string, data?: Record<string, unknown>): void;\n}\n\n/** MessageBatcher configuration options. */\nexport interface BatcherOptions {\n /** Interval between timed flushes in ms. Default: 1500 */\n flushIntervalMs?: number;\n /** Max events before triggering an immediate capacity flush. Default: 4 */\n maxBatchSize?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Default no-op logger\n// ---------------------------------------------------------------------------\n\nconst noopLogger: BatcherLogger = {\n error() {},\n warn() {},\n debug() {},\n};\n\n// ---------------------------------------------------------------------------\n// MessageBatcher\n// ---------------------------------------------------------------------------\n\nexport class MessageBatcher {\n private readonly send: SendFn;\n private readonly logger: BatcherLogger;\n private readonly flushIntervalMs: number;\n private readonly maxBatchSize: number;\n\n private buffer: FormattedEvent[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private destroyed = false;\n\n constructor(send: SendFn, logger?: BatcherLogger, options?: BatcherOptions) {\n this.send = send;\n this.logger = logger ?? noopLogger;\n this.flushIntervalMs = options?.flushIntervalMs ?? 1500;\n this.maxBatchSize = options?.maxBatchSize ?? 4;\n }\n\n // -----------------------------------------------------------------------\n // Public API\n // -----------------------------------------------------------------------\n\n /** Start the periodic flush timer. */\n start(): void {\n if (this.timer) return; // already running\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushIntervalMs);\n // Don't hold the process open for the timer\n if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {\n this.timer.unref();\n }\n this.logger.debug('Batcher started', { flushIntervalMs: this.flushIntervalMs });\n }\n\n /** Stop the periodic flush timer without flushing. */\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n this.logger.debug('Batcher stopped');\n }\n\n /** Flush remaining buffer and stop. Safe to call multiple times. */\n async destroy(): Promise<void> {\n if (this.destroyed) return;\n this.destroyed = true;\n this.stop();\n await this.flush();\n this.logger.debug('Batcher destroyed');\n }\n\n /**\n * Enqueue a formatted event for batched sending.\n * Triggers an immediate capacity flush if buffer reaches maxBatchSize.\n */\n enqueue(formatted: FormattedEvent): void {\n if (this.destroyed) return;\n this.buffer.push(formatted);\n if (this.buffer.length >= this.maxBatchSize) {\n void this.flush();\n }\n }\n\n /**\n * Immediately send a high-priority event (e.g. blocker).\n * Flushes any pending buffer first, then sends the priority event alone.\n */\n async enqueueImmediate(formatted: FormattedEvent): Promise<void> {\n if (this.destroyed) return;\n // Flush pending buffer first so ordering is preserved\n await this.flush();\n // Send the priority event immediately, alone\n await this.doSend([formatted]);\n }\n\n /** Current number of events in the buffer (for testing/diagnostics). */\n get pending(): number {\n return this.buffer.length;\n }\n\n // -----------------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------------\n\n /**\n * Flush the current buffer as a single Discord message.\n * Multiple embeds are combined into one send() call (Discord supports up to 10).\n * No-op if buffer is empty.\n */\n private async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n if (this.flushing) return; // prevent re-entrant flush\n\n this.flushing = true;\n const batch = this.buffer.splice(0); // take all\n try {\n await this.doSend(batch);\n } finally {\n this.flushing = false;\n }\n }\n\n /**\n * Build a SendPayload from a batch of FormattedEvents and invoke the send callback.\n * Catches and logs errors — never throws.\n *\n * For batched messages (2+ events), we send content-only to avoid duplication\n * between content text and embed descriptions, and to stay under Discord's\n * 10-embed limit. Single-event sends include the embed for rich formatting.\n */\n private async doSend(batch: FormattedEvent[]): Promise<void> {\n if (batch.length === 0) return;\n\n // Combine content lines\n const content = batch.map((e) => e.content).join('\\n');\n\n // For single events, include the embed for rich formatting.\n // For batches, skip embeds — the content lines are self-descriptive and\n // embeds would duplicate the information + risk hitting Discord's 10-embed cap.\n const embeds: unknown[] = [];\n if (batch.length === 1 && batch[0].embed) {\n embeds.push(batch[0].embed);\n }\n\n // Collect all component rows (only from the last event with components —\n // Discord only supports one set of components per message)\n let components: unknown[] = [];\n for (const e of batch) {\n if (e.components && e.components.length > 0) {\n components = e.components;\n }\n }\n\n const payload: SendPayload = { content, embeds, components };\n\n try {\n await this.send(payload);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error('Batcher send failed', { error: message, batchSize: batch.length });\n\n // Retry once after a short delay\n try {\n await new Promise((r) => setTimeout(r, 1000));\n await this.send(payload);\n this.logger.debug('Batcher retry succeeded');\n } catch (retryErr) {\n const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);\n this.logger.warn('Batcher retry also failed, dropping batch', {\n error: retryMessage,\n batchSize: batch.length,\n });\n // Drop the batch — don't re-enqueue to prevent infinite loops\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"message-batcher.js","sourceRoot":"","sources":["../src/message-batcher.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * message-batcher.ts — Rate-limit-aware message batcher for Discord.\n *\n * Accumulates FormattedEvent payloads and flushes them to a Discord channel\n * respecting the 5 msg/5s rate limit. Supports:\n * - Timer-based periodic flush (default 1.5s)\n * - Capacity-based flush when buffer hits maxBatchSize\n * - Immediate priority flush for blockers (bypasses batching)\n * - Combining multiple embeds into a single send() call\n * - Error isolation: send() failures are logged, never crash the batcher\n */\n\nimport type { FormattedEvent } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Payload passed to the send callback — matches Discord TextChannel.send() shape. */\nexport interface SendPayload {\n content: string;\n embeds: unknown[];\n components: unknown[];\n}\n\n/** Send callback abstraction. Returns void or a promise. */\nexport type SendFn = (payload: SendPayload) => Promise<void> | void;\n\n/** Logger interface — just needs error/warn/debug. */\nexport interface BatcherLogger {\n error(msg: string, data?: Record<string, unknown>): void;\n warn(msg: string, data?: Record<string, unknown>): void;\n debug(msg: string, data?: Record<string, unknown>): void;\n}\n\n/** MessageBatcher configuration options. */\nexport interface BatcherOptions {\n /** Interval between timed flushes in ms. Default: 1500 */\n flushIntervalMs?: number;\n /** Max events before triggering an immediate capacity flush. Default: 4 */\n maxBatchSize?: number;\n /**\n * Hard cap on buffered events. When exceeded (e.g. Discord API is slow/down and\n * flushes stall), the oldest events are dropped to bound memory. Default: 1000.\n */\n maxBufferSize?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Default no-op logger\n// ---------------------------------------------------------------------------\n\nconst noopLogger: BatcherLogger = {\n error() {},\n warn() {},\n debug() {},\n};\n\n// ---------------------------------------------------------------------------\n// MessageBatcher\n// ---------------------------------------------------------------------------\n\nexport class MessageBatcher {\n private readonly send: SendFn;\n private readonly logger: BatcherLogger;\n private readonly flushIntervalMs: number;\n private readonly maxBatchSize: number;\n private readonly maxBufferSize: number;\n\n private buffer: FormattedEvent[] = [];\n private timer: ReturnType<typeof setInterval> | null = null;\n private flushing = false;\n private destroyed = false;\n private saturated = false;\n\n constructor(send: SendFn, logger?: BatcherLogger, options?: BatcherOptions) {\n this.send = send;\n this.logger = logger ?? noopLogger;\n this.flushIntervalMs = options?.flushIntervalMs ?? 1500;\n this.maxBatchSize = options?.maxBatchSize ?? 4;\n this.maxBufferSize = options?.maxBufferSize ?? 1000;\n }\n\n // -----------------------------------------------------------------------\n // Public API\n // -----------------------------------------------------------------------\n\n /** Start the periodic flush timer. */\n start(): void {\n if (this.timer) return; // already running\n this.timer = setInterval(() => {\n void this.flush();\n }, this.flushIntervalMs);\n // Don't hold the process open for the timer\n if (this.timer && typeof this.timer === 'object' && 'unref' in this.timer) {\n this.timer.unref();\n }\n this.logger.debug('Batcher started', { flushIntervalMs: this.flushIntervalMs });\n }\n\n /** Stop the periodic flush timer without flushing. */\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n this.logger.debug('Batcher stopped');\n }\n\n /** Flush remaining buffer and stop. Safe to call multiple times. */\n async destroy(): Promise<void> {\n if (this.destroyed) return;\n this.destroyed = true;\n this.stop();\n await this.flush();\n this.logger.debug('Batcher destroyed');\n }\n\n /**\n * Enqueue a formatted event for batched sending.\n * Triggers an immediate capacity flush if buffer reaches maxBatchSize.\n */\n enqueue(formatted: FormattedEvent): void {\n if (this.destroyed) return;\n if (this.buffer.length >= this.maxBufferSize) {\n // Discord is not draining fast enough; drop oldest to bound memory. Blockers\n // use enqueueImmediate() and never sit in this buffer, so they're unaffected.\n this.buffer.shift();\n if (!this.saturated) {\n this.saturated = true;\n this.logger.warn('Batcher buffer saturated; dropping oldest events', { maxBufferSize: this.maxBufferSize });\n }\n }\n this.buffer.push(formatted);\n if (this.buffer.length >= this.maxBatchSize) {\n void this.flush();\n }\n }\n\n /**\n * Immediately send a high-priority event (e.g. blocker).\n * Flushes any pending buffer first, then sends the priority event alone.\n */\n async enqueueImmediate(formatted: FormattedEvent): Promise<void> {\n if (this.destroyed) return;\n // Flush pending buffer first so ordering is preserved\n await this.flush();\n // Send the priority event immediately, alone\n await this.doSend([formatted]);\n }\n\n /** Current number of events in the buffer (for testing/diagnostics). */\n get pending(): number {\n return this.buffer.length;\n }\n\n // -----------------------------------------------------------------------\n // Internal\n // -----------------------------------------------------------------------\n\n /**\n * Flush the current buffer as a single Discord message.\n * Multiple embeds are combined into one send() call (Discord supports up to 10).\n * No-op if buffer is empty.\n */\n private async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n if (this.flushing) return; // prevent re-entrant flush\n\n this.flushing = true;\n const batch = this.buffer.splice(0); // take all\n this.saturated = false; // buffer drained; allow a fresh saturation warning later\n try {\n await this.doSend(batch);\n } finally {\n this.flushing = false;\n }\n }\n\n /**\n * Build a SendPayload from a batch of FormattedEvents and invoke the send callback.\n * Catches and logs errors — never throws.\n *\n * For batched messages (2+ events), we send content-only to avoid duplication\n * between content text and embed descriptions, and to stay under Discord's\n * 10-embed limit. Single-event sends include the embed for rich formatting.\n */\n private async doSend(batch: FormattedEvent[]): Promise<void> {\n if (batch.length === 0) return;\n\n // Combine content lines\n const content = batch.map((e) => e.content).join('\\n');\n\n // For single events, include the embed for rich formatting.\n // For batches, skip embeds — the content lines are self-descriptive and\n // embeds would duplicate the information + risk hitting Discord's 10-embed cap.\n const embeds: unknown[] = [];\n if (batch.length === 1 && batch[0].embed) {\n embeds.push(batch[0].embed);\n }\n\n // Collect all component rows (only from the last event with components —\n // Discord only supports one set of components per message)\n let components: unknown[] = [];\n for (const e of batch) {\n if (e.components && e.components.length > 0) {\n components = e.components;\n }\n }\n\n const payload: SendPayload = { content, embeds, components };\n\n try {\n await this.send(payload);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n this.logger.error('Batcher send failed', { error: message, batchSize: batch.length });\n\n // Retry once after a short delay\n try {\n await new Promise((r) => setTimeout(r, 1000));\n await this.send(payload);\n this.logger.debug('Batcher retry succeeded');\n } catch (retryErr) {\n const retryMessage = retryErr instanceof Error ? retryErr.message : String(retryErr);\n this.logger.warn('Batcher retry also failed, dropping batch', {\n error: retryMessage,\n batchSize: batch.length,\n });\n // Drop the batch — don't re-enqueue to prevent infinite loops\n }\n }\n }\n}\n"]}
@@ -238,5 +238,53 @@ describe('MessageBatcher', () => {
238
238
  await batcher.destroy();
239
239
  });
240
240
  });
241
+ describe('buffer cap (maxBufferSize)', () => {
242
+ it('caps the buffer at maxBufferSize, dropping oldest', async () => {
243
+ const { fn, calls } = createSend();
244
+ // maxBatchSize high so capacity flush never fires; small maxBufferSize.
245
+ const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, maxBufferSize: 3, flushIntervalMs: 60_000 });
246
+ for (let i = 0; i < 10; i++)
247
+ batcher.enqueue(fakeEvent(`e${i}`));
248
+ assert.equal(batcher.pending, 3);
249
+ await batcher.destroy();
250
+ // Only the newest three survived; e0..e6 were dropped.
251
+ assert.equal(calls.length, 1);
252
+ assert.equal(calls[0].content, 'e7\ne8\ne9');
253
+ });
254
+ it('warns once per saturation, not once per dropped event', async () => {
255
+ const { fn } = createSend();
256
+ const { logger, warns } = createLogger();
257
+ const batcher = new MessageBatcher(fn, logger, { maxBatchSize: 100, maxBufferSize: 3, flushIntervalMs: 60_000 });
258
+ for (let i = 0; i < 10; i++)
259
+ batcher.enqueue(fakeEvent(`e${i}`));
260
+ assert.equal(warns.filter((w) => w.includes('saturated')).length, 1);
261
+ await batcher.destroy();
262
+ });
263
+ it('allows a fresh saturation warning after the buffer drains', async () => {
264
+ const { fn } = createSend();
265
+ const { logger, warns } = createLogger();
266
+ const batcher = new MessageBatcher(fn, logger, { maxBatchSize: 100, maxBufferSize: 3, flushIntervalMs: 60_000 });
267
+ for (let i = 0; i < 10; i++)
268
+ batcher.enqueue(fakeEvent(`a${i}`));
269
+ // enqueueImmediate flushes the buffer first, draining it and resetting saturation.
270
+ await batcher.enqueueImmediate(fakeEvent('drain'));
271
+ assert.equal(batcher.pending, 0);
272
+ for (let i = 0; i < 10; i++)
273
+ batcher.enqueue(fakeEvent(`b${i}`));
274
+ assert.equal(warns.filter((w) => w.includes('saturated')).length, 2);
275
+ await batcher.destroy();
276
+ });
277
+ it('does not drop or warn under default options', async () => {
278
+ const { fn, calls } = createSend();
279
+ const { logger, warns } = createLogger();
280
+ const batcher = new MessageBatcher(fn, logger, { flushIntervalMs: 60_000 }); // default cap 1000
281
+ batcher.enqueue(fakeEvent('1'));
282
+ batcher.enqueue(fakeEvent('2'));
283
+ assert.equal(batcher.pending, 2);
284
+ assert.equal(warns.filter((w) => w.includes('saturated')).length, 0);
285
+ await batcher.destroy();
286
+ assert.equal(calls[0].content, '1\n2');
287
+ });
288
+ });
241
289
  });
242
290
  //# sourceMappingURL=message-batcher.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"message-batcher.test.js","sourceRoot":"","sources":["../src/message-batcher.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, beforeEach, afterEach, mock } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { MessageBatcher } from './message-batcher.js';\nimport type { SendPayload, BatcherLogger } from './message-batcher.js';\nimport type { FormattedEvent } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Create a minimal FormattedEvent for testing. */\nfunction fakeEvent(content: string, hasEmbed = false): FormattedEvent {\n const fe: FormattedEvent = { content };\n if (hasEmbed) {\n // Minimal mock embed — just needs to be truthy and pass through\n fe.embed = { data: { title: content } } as any;\n }\n return fe;\n}\n\n/** Create a tracking send function. */\nfunction createSend() {\n const calls: SendPayload[] = [];\n const fn = mock.fn(async (payload: SendPayload) => {\n calls.push(payload);\n });\n return { fn, calls };\n}\n\n/** Create a logger that captures error/warn calls. */\nfunction createLogger() {\n const errors: string[] = [];\n const warns: string[] = [];\n const debugs: string[] = [];\n const logger: BatcherLogger = {\n error(msg: string) { errors.push(msg); },\n warn(msg: string) { warns.push(msg); },\n debug(msg: string) { debugs.push(msg); },\n };\n return { logger, errors, warns, debugs };\n}\n\n// ---------------------------------------------------------------------------\n// Tests\n// ---------------------------------------------------------------------------\n\ndescribe('MessageBatcher', () => {\n describe('enqueue + capacity flush', () => {\n it('flushes when buffer reaches maxBatchSize', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 3, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('a'));\n batcher.enqueue(fakeEvent('b'));\n assert.equal(calls.length, 0, 'should not flush yet');\n\n batcher.enqueue(fakeEvent('c')); // hits capacity\n // flush is async — give it a tick\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1, 'should have flushed once');\n assert.equal(calls[0].content, 'a\\nb\\nc');\n assert.equal(batcher.pending, 0);\n\n await batcher.destroy();\n });\n\n it('skips embeds for batched messages (only content)', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('a', true));\n batcher.enqueue(fakeEvent('b', true)); // triggers flush\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].embeds.length, 0, 'batched sends skip embeds to avoid duplication');\n assert.equal(calls[0].content, 'a\\nb');\n\n await batcher.destroy();\n });\n });\n\n describe('enqueueImmediate', () => {\n it('flushes pending buffer then sends immediately', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 10, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('buffered-1'));\n batcher.enqueue(fakeEvent('buffered-2'));\n\n await batcher.enqueueImmediate(fakeEvent('blocker!'));\n\n // First call: the pending buffer flush\n // Second call: the immediate event\n assert.equal(calls.length, 2, 'should have two send calls');\n assert.equal(calls[0].content, 'buffered-1\\nbuffered-2');\n assert.equal(calls[1].content, 'blocker!');\n assert.equal(batcher.pending, 0);\n\n await batcher.destroy();\n });\n\n it('sends immediately when buffer is empty', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 10, flushIntervalMs: 60_000 });\n\n await batcher.enqueueImmediate(fakeEvent('urgent'));\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, 'urgent');\n\n await batcher.destroy();\n });\n });\n\n describe('timer-based flush', () => {\n it('flushes on interval', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 50 });\n batcher.start();\n\n batcher.enqueue(fakeEvent('timed-1'));\n batcher.enqueue(fakeEvent('timed-2'));\n\n // Wait longer than flushIntervalMs\n await new Promise((r) => setTimeout(r, 120));\n\n assert.ok(calls.length >= 1, 'timer should have triggered at least one flush');\n assert.equal(calls[0].content, 'timed-1\\ntimed-2');\n assert.equal(batcher.pending, 0);\n\n await batcher.destroy();\n });\n\n it('stop prevents further timer flushes', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 30 });\n batcher.start();\n batcher.stop();\n\n batcher.enqueue(fakeEvent('orphan'));\n await new Promise((r) => setTimeout(r, 80));\n\n assert.equal(calls.length, 0, 'no flush after stop');\n // Cleanup without triggering flush timer\n batcher.stop(); // idempotent\n // Manually drain for cleanup\n await batcher.destroy();\n });\n });\n\n describe('destroy', () => {\n it('flushes remaining buffer on destroy', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('leftover-1'));\n batcher.enqueue(fakeEvent('leftover-2'));\n\n await batcher.destroy();\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, 'leftover-1\\nleftover-2');\n });\n\n it('is idempotent — second destroy is no-op', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('once'));\n await batcher.destroy();\n await batcher.destroy(); // second call\n\n assert.equal(calls.length, 1, 'only flushed once');\n });\n\n it('enqueue after destroy is silently ignored', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n await batcher.destroy();\n\n batcher.enqueue(fakeEvent('post-destroy'));\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 0, 'no sends after destroy');\n });\n });\n\n describe('empty buffer', () => {\n it('flush of empty buffer is no-op', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n batcher.start();\n\n // Force a timer tick with an empty buffer\n await new Promise((r) => setTimeout(r, 10));\n await batcher.destroy();\n\n // Only the destroy-triggered flush, which should also be a no-op\n assert.equal(calls.length, 0, 'no sends for empty buffer');\n });\n });\n\n describe('single-item flush', () => {\n it('handles a single item in buffer at destroy', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('solo'));\n await batcher.destroy();\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, 'solo');\n assert.equal(calls[0].embeds.length, 0);\n assert.equal(calls[0].components.length, 0);\n });\n });\n\n describe('error handling', () => {\n it('logs error and continues when send throws', async () => {\n let attempt = 0;\n const sendFn = async () => {\n attempt++;\n throw new Error('Discord rate limit');\n };\n const { logger, errors, warns } = createLogger();\n const batcher = new MessageBatcher(sendFn, logger, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('x'));\n batcher.enqueue(fakeEvent('y')); // triggers flush\n // Wait for flush + retry\n await new Promise((r) => setTimeout(r, 1500));\n\n assert.ok(errors.length >= 1, 'should have logged an error');\n assert.ok(warns.length >= 1, 'should have logged a warning on retry failure');\n assert.equal(batcher.pending, 0, 'buffer cleared even on error');\n\n // Batcher should still be alive — enqueue more\n batcher.enqueue(fakeEvent('after-error'));\n assert.equal(batcher.pending, 1, 'can still enqueue after error');\n\n await batcher.destroy();\n });\n\n it('succeeds on retry if first attempt fails', async () => {\n let attempt = 0;\n const calls: SendPayload[] = [];\n const sendFn = async (payload: SendPayload) => {\n attempt++;\n if (attempt === 1) throw new Error('transient');\n calls.push(payload);\n };\n const { logger, errors } = createLogger();\n const batcher = new MessageBatcher(sendFn, logger, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('retry-me'));\n batcher.enqueue(fakeEvent('retry-too'));\n // Wait for flush + retry delay\n await new Promise((r) => setTimeout(r, 1500));\n\n assert.equal(errors.length, 1, 'logged one error on first attempt');\n assert.equal(calls.length, 1, 'retry succeeded');\n assert.equal(calls[0].content, 'retry-me\\nretry-too');\n\n await batcher.destroy();\n });\n });\n\n describe('buffer at exactly capacity', () => {\n it('flushes at exactly maxBatchSize', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 4, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('1'));\n batcher.enqueue(fakeEvent('2'));\n batcher.enqueue(fakeEvent('3'));\n assert.equal(calls.length, 0, 'not flushed at 3/4');\n\n batcher.enqueue(fakeEvent('4')); // exactly at capacity\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, '1\\n2\\n3\\n4');\n\n await batcher.destroy();\n });\n });\n\n describe('components handling', () => {\n it('uses components from the last event that has them', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 3, flushIntervalMs: 60_000 });\n\n const fakeRow = { type: 'ActionRow', components: [] };\n batcher.enqueue(fakeEvent('no-components'));\n batcher.enqueue({ content: 'with-components', components: [fakeRow] } as any);\n batcher.enqueue(fakeEvent('also-no-components')); // triggers flush\n\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1);\n assert.deepEqual(calls[0].components, [fakeRow]);\n\n await batcher.destroy();\n });\n });\n});\n"]}
1
+ {"version":3,"file":"message-batcher.test.js","sourceRoot":"","sources":["../src/message-batcher.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, beforeEach, afterEach, mock } from 'node:test';\nimport assert from 'node:assert/strict';\nimport { MessageBatcher } from './message-batcher.js';\nimport type { SendPayload, BatcherLogger } from './message-batcher.js';\nimport type { FormattedEvent } from './types.js';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Create a minimal FormattedEvent for testing. */\nfunction fakeEvent(content: string, hasEmbed = false): FormattedEvent {\n const fe: FormattedEvent = { content };\n if (hasEmbed) {\n // Minimal mock embed — just needs to be truthy and pass through\n fe.embed = { data: { title: content } } as any;\n }\n return fe;\n}\n\n/** Create a tracking send function. */\nfunction createSend() {\n const calls: SendPayload[] = [];\n const fn = mock.fn(async (payload: SendPayload) => {\n calls.push(payload);\n });\n return { fn, calls };\n}\n\n/** Create a logger that captures error/warn calls. */\nfunction createLogger() {\n const errors: string[] = [];\n const warns: string[] = [];\n const debugs: string[] = [];\n const logger: BatcherLogger = {\n error(msg: string) { errors.push(msg); },\n warn(msg: string) { warns.push(msg); },\n debug(msg: string) { debugs.push(msg); },\n };\n return { logger, errors, warns, debugs };\n}\n\n// ---------------------------------------------------------------------------\n// Tests\n// ---------------------------------------------------------------------------\n\ndescribe('MessageBatcher', () => {\n describe('enqueue + capacity flush', () => {\n it('flushes when buffer reaches maxBatchSize', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 3, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('a'));\n batcher.enqueue(fakeEvent('b'));\n assert.equal(calls.length, 0, 'should not flush yet');\n\n batcher.enqueue(fakeEvent('c')); // hits capacity\n // flush is async — give it a tick\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1, 'should have flushed once');\n assert.equal(calls[0].content, 'a\\nb\\nc');\n assert.equal(batcher.pending, 0);\n\n await batcher.destroy();\n });\n\n it('skips embeds for batched messages (only content)', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('a', true));\n batcher.enqueue(fakeEvent('b', true)); // triggers flush\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].embeds.length, 0, 'batched sends skip embeds to avoid duplication');\n assert.equal(calls[0].content, 'a\\nb');\n\n await batcher.destroy();\n });\n });\n\n describe('enqueueImmediate', () => {\n it('flushes pending buffer then sends immediately', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 10, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('buffered-1'));\n batcher.enqueue(fakeEvent('buffered-2'));\n\n await batcher.enqueueImmediate(fakeEvent('blocker!'));\n\n // First call: the pending buffer flush\n // Second call: the immediate event\n assert.equal(calls.length, 2, 'should have two send calls');\n assert.equal(calls[0].content, 'buffered-1\\nbuffered-2');\n assert.equal(calls[1].content, 'blocker!');\n assert.equal(batcher.pending, 0);\n\n await batcher.destroy();\n });\n\n it('sends immediately when buffer is empty', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 10, flushIntervalMs: 60_000 });\n\n await batcher.enqueueImmediate(fakeEvent('urgent'));\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, 'urgent');\n\n await batcher.destroy();\n });\n });\n\n describe('timer-based flush', () => {\n it('flushes on interval', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 50 });\n batcher.start();\n\n batcher.enqueue(fakeEvent('timed-1'));\n batcher.enqueue(fakeEvent('timed-2'));\n\n // Wait longer than flushIntervalMs\n await new Promise((r) => setTimeout(r, 120));\n\n assert.ok(calls.length >= 1, 'timer should have triggered at least one flush');\n assert.equal(calls[0].content, 'timed-1\\ntimed-2');\n assert.equal(batcher.pending, 0);\n\n await batcher.destroy();\n });\n\n it('stop prevents further timer flushes', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 30 });\n batcher.start();\n batcher.stop();\n\n batcher.enqueue(fakeEvent('orphan'));\n await new Promise((r) => setTimeout(r, 80));\n\n assert.equal(calls.length, 0, 'no flush after stop');\n // Cleanup without triggering flush timer\n batcher.stop(); // idempotent\n // Manually drain for cleanup\n await batcher.destroy();\n });\n });\n\n describe('destroy', () => {\n it('flushes remaining buffer on destroy', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('leftover-1'));\n batcher.enqueue(fakeEvent('leftover-2'));\n\n await batcher.destroy();\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, 'leftover-1\\nleftover-2');\n });\n\n it('is idempotent — second destroy is no-op', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('once'));\n await batcher.destroy();\n await batcher.destroy(); // second call\n\n assert.equal(calls.length, 1, 'only flushed once');\n });\n\n it('enqueue after destroy is silently ignored', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n await batcher.destroy();\n\n batcher.enqueue(fakeEvent('post-destroy'));\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 0, 'no sends after destroy');\n });\n });\n\n describe('empty buffer', () => {\n it('flush of empty buffer is no-op', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n batcher.start();\n\n // Force a timer tick with an empty buffer\n await new Promise((r) => setTimeout(r, 10));\n await batcher.destroy();\n\n // Only the destroy-triggered flush, which should also be a no-op\n assert.equal(calls.length, 0, 'no sends for empty buffer');\n });\n });\n\n describe('single-item flush', () => {\n it('handles a single item in buffer at destroy', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('solo'));\n await batcher.destroy();\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, 'solo');\n assert.equal(calls[0].embeds.length, 0);\n assert.equal(calls[0].components.length, 0);\n });\n });\n\n describe('error handling', () => {\n it('logs error and continues when send throws', async () => {\n let attempt = 0;\n const sendFn = async () => {\n attempt++;\n throw new Error('Discord rate limit');\n };\n const { logger, errors, warns } = createLogger();\n const batcher = new MessageBatcher(sendFn, logger, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('x'));\n batcher.enqueue(fakeEvent('y')); // triggers flush\n // Wait for flush + retry\n await new Promise((r) => setTimeout(r, 1500));\n\n assert.ok(errors.length >= 1, 'should have logged an error');\n assert.ok(warns.length >= 1, 'should have logged a warning on retry failure');\n assert.equal(batcher.pending, 0, 'buffer cleared even on error');\n\n // Batcher should still be alive — enqueue more\n batcher.enqueue(fakeEvent('after-error'));\n assert.equal(batcher.pending, 1, 'can still enqueue after error');\n\n await batcher.destroy();\n });\n\n it('succeeds on retry if first attempt fails', async () => {\n let attempt = 0;\n const calls: SendPayload[] = [];\n const sendFn = async (payload: SendPayload) => {\n attempt++;\n if (attempt === 1) throw new Error('transient');\n calls.push(payload);\n };\n const { logger, errors } = createLogger();\n const batcher = new MessageBatcher(sendFn, logger, { maxBatchSize: 2, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('retry-me'));\n batcher.enqueue(fakeEvent('retry-too'));\n // Wait for flush + retry delay\n await new Promise((r) => setTimeout(r, 1500));\n\n assert.equal(errors.length, 1, 'logged one error on first attempt');\n assert.equal(calls.length, 1, 'retry succeeded');\n assert.equal(calls[0].content, 'retry-me\\nretry-too');\n\n await batcher.destroy();\n });\n });\n\n describe('buffer at exactly capacity', () => {\n it('flushes at exactly maxBatchSize', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 4, flushIntervalMs: 60_000 });\n\n batcher.enqueue(fakeEvent('1'));\n batcher.enqueue(fakeEvent('2'));\n batcher.enqueue(fakeEvent('3'));\n assert.equal(calls.length, 0, 'not flushed at 3/4');\n\n batcher.enqueue(fakeEvent('4')); // exactly at capacity\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, '1\\n2\\n3\\n4');\n\n await batcher.destroy();\n });\n });\n\n describe('components handling', () => {\n it('uses components from the last event that has them', async () => {\n const { fn, calls } = createSend();\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 3, flushIntervalMs: 60_000 });\n\n const fakeRow = { type: 'ActionRow', components: [] };\n batcher.enqueue(fakeEvent('no-components'));\n batcher.enqueue({ content: 'with-components', components: [fakeRow] } as any);\n batcher.enqueue(fakeEvent('also-no-components')); // triggers flush\n\n await new Promise((r) => setTimeout(r, 10));\n\n assert.equal(calls.length, 1);\n assert.deepEqual(calls[0].components, [fakeRow]);\n\n await batcher.destroy();\n });\n });\n\n describe('buffer cap (maxBufferSize)', () => {\n it('caps the buffer at maxBufferSize, dropping oldest', async () => {\n const { fn, calls } = createSend();\n // maxBatchSize high so capacity flush never fires; small maxBufferSize.\n const batcher = new MessageBatcher(fn, undefined, { maxBatchSize: 100, maxBufferSize: 3, flushIntervalMs: 60_000 });\n for (let i = 0; i < 10; i++) batcher.enqueue(fakeEvent(`e${i}`));\n assert.equal(batcher.pending, 3);\n await batcher.destroy();\n // Only the newest three survived; e0..e6 were dropped.\n assert.equal(calls.length, 1);\n assert.equal(calls[0].content, 'e7\\ne8\\ne9');\n });\n\n it('warns once per saturation, not once per dropped event', async () => {\n const { fn } = createSend();\n const { logger, warns } = createLogger();\n const batcher = new MessageBatcher(fn, logger, { maxBatchSize: 100, maxBufferSize: 3, flushIntervalMs: 60_000 });\n for (let i = 0; i < 10; i++) batcher.enqueue(fakeEvent(`e${i}`));\n assert.equal(warns.filter((w) => w.includes('saturated')).length, 1);\n await batcher.destroy();\n });\n\n it('allows a fresh saturation warning after the buffer drains', async () => {\n const { fn } = createSend();\n const { logger, warns } = createLogger();\n const batcher = new MessageBatcher(fn, logger, { maxBatchSize: 100, maxBufferSize: 3, flushIntervalMs: 60_000 });\n for (let i = 0; i < 10; i++) batcher.enqueue(fakeEvent(`a${i}`));\n // enqueueImmediate flushes the buffer first, draining it and resetting saturation.\n await batcher.enqueueImmediate(fakeEvent('drain'));\n assert.equal(batcher.pending, 0);\n for (let i = 0; i < 10; i++) batcher.enqueue(fakeEvent(`b${i}`));\n assert.equal(warns.filter((w) => w.includes('saturated')).length, 2);\n await batcher.destroy();\n });\n\n it('does not drop or warn under default options', async () => {\n const { fn, calls } = createSend();\n const { logger, warns } = createLogger();\n const batcher = new MessageBatcher(fn, logger, { flushIntervalMs: 60_000 }); // default cap 1000\n batcher.enqueue(fakeEvent('1'));\n batcher.enqueue(fakeEvent('2'));\n assert.equal(batcher.pending, 2);\n assert.equal(warns.filter((w) => w.includes('saturated')).length, 0);\n await batcher.destroy();\n assert.equal(calls[0].content, '1\\n2');\n });\n });\n});\n"]}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengsd/daemon",
3
- "version": "1.3.0-dev.eed73bea",
3
+ "version": "1.4.0-dev.5fc22c6f",
4
4
  "description": "GSD daemon — background process for project monitoring and Discord integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,13 +27,13 @@
27
27
  },
28
28
  "scripts": {
29
29
  "build": "node ../../scripts/clean-package-dist.cjs && tsc",
30
- "test": "node --test dist/daemon.test.js dist/cloud-config.test.js dist/cloud-runtime.test.js dist/local-tool-executor.test.js dist/mcp-cli.test.js"
30
+ "test": "node --test dist/daemon.test.js dist/cloud-config.test.js dist/cloud-runtime.test.js dist/local-tool-executor.test.js dist/mcp-cli.test.js dist/message-batcher.test.js"
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/sdk": "^0.52.0",
34
- "@opengsd/contracts": "^1.3.0-dev.eed73bea",
35
- "@opengsd/mcp-server": "^1.3.0-dev.eed73bea",
36
- "@opengsd/rpc-client": "^1.3.0-dev.eed73bea",
34
+ "@opengsd/contracts": "^1.4.0-dev.5fc22c6f",
35
+ "@opengsd/mcp-server": "^1.4.0-dev.5fc22c6f",
36
+ "@opengsd/rpc-client": "^1.4.0-dev.5fc22c6f",
37
37
  "discord.js": "^14.25.1",
38
38
  "ws": "^8.20.0",
39
39
  "yaml": "^2.8.0",
@@ -123,6 +123,7 @@ export declare class AgentSession implements AgentSessionHost {
123
123
  disconnectFromAgent(): void;
124
124
  reconnectToAgent(): void;
125
125
  isRetryableError(message: AssistantMessage): boolean;
126
+ canPrepareRetry(message: AssistantMessage): boolean;
126
127
  runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void>;
127
128
  handlePostAgentRun(): Promise<boolean>;
128
129
  flushPendingBashMessages(): void;