@akira-tl/forgerelay 0.8.8 → 0.8.9

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 (246) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +10 -18
  3. package/capabilities/host-integration/GUIDE.md +2 -2
  4. package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
  5. package/dist/activity/history/bash-output-store.js +250 -0
  6. package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
  7. package/dist/activity/{query-service.js → history/query-service.js} +13 -7
  8. package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
  9. package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
  10. package/dist/activity/storage/history.js +12 -0
  11. package/dist/activity/storage/paths.js +56 -0
  12. package/dist/activity/storage/segmented-log.js +149 -0
  13. package/dist/cli/setup-support.js +116 -0
  14. package/dist/cli.js +18 -126
  15. package/dist/lsp/code-intelligence.js +1 -1
  16. package/dist/lsp/test-support/server-fixture.js +9 -9
  17. package/dist/mcp/filesystem/filesystem-tools.js +245 -0
  18. package/dist/mcp/hooks/command-runner.js +79 -0
  19. package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
  20. package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
  21. package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
  22. package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
  23. package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
  24. package/dist/mcp/panel/app.js +143 -0
  25. package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
  26. package/dist/mcp/process/runtime.js +257 -0
  27. package/dist/mcp/process/tools.js +346 -0
  28. package/dist/mcp/server/core/activity-support.js +70 -0
  29. package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
  30. package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +2 -2
  31. package/dist/mcp/server/core/capability-support.js +90 -0
  32. package/dist/mcp/server/core/schemas.js +216 -0
  33. package/dist/mcp/server/core/tool-support.js +207 -0
  34. package/dist/mcp/server/core/version.js +9 -0
  35. package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
  36. package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -0
  37. package/dist/mcp/server/transport/http-server.js +301 -0
  38. package/dist/mcp/server/transport/http-support.js +43 -0
  39. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +222 -0
  40. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
  41. package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
  42. package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
  43. package/dist/{config.js → runtime/config/config.js} +3 -3
  44. package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
  45. package/dist/{db → runtime/state/db}/client.js +1 -1
  46. package/dist/{db → runtime/state/db}/migrations.js +34 -3
  47. package/dist/runtime/state/lock/file-lock.js +130 -0
  48. package/dist/runtime/testing/server-fixture.js +190 -0
  49. package/dist/server.js +116 -4202
  50. package/dist/subagents/providers/adapters/claude.js +86 -52
  51. package/dist/subagents/providers/adapters/codex.js +103 -39
  52. package/dist/subagents/providers/adapters/opencode.js +114 -84
  53. package/dist/subagents/providers/adapters/pi.js +59 -8
  54. package/dist/subagents/providers/availability.js +15 -30
  55. package/dist/subagents/providers/runtime/external-command.js +56 -0
  56. package/dist/subagents/sessions/capability.js +1 -1
  57. package/dist/subagents/sessions/execution.js +1 -1
  58. package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
  59. package/dist/subagents/sessions/store.js +1 -1
  60. package/dist/ui/.vite/manifest.json +342 -410
  61. package/dist/ui/activity-panel-app.html +2 -4
  62. package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
  63. package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
  64. package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
  65. package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
  66. package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
  67. package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
  68. package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
  69. package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
  70. package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
  71. package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
  72. package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
  73. package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
  74. package/dist/ui/assets/css-C1tjBtVN.js +1 -0
  75. package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
  76. package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
  77. package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
  78. package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
  79. package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
  80. package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
  81. package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
  82. package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
  83. package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
  84. package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
  85. package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
  86. package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
  87. package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
  88. package/dist/ui/assets/html-By4dNnM4.js +1 -0
  89. package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
  90. package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
  91. package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
  92. package/dist/ui/assets/java-DYRe2lWG.js +1 -0
  93. package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
  94. package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
  95. package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
  96. package/dist/ui/assets/json-dbltgiiA.js +1 -0
  97. package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
  98. package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
  99. package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
  100. package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
  101. package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
  102. package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
  103. package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
  104. package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
  105. package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
  106. package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
  107. package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
  108. package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
  109. package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
  110. package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
  111. package/dist/ui/assets/r-CcezTbLq.js +1 -0
  112. package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
  113. package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
  114. package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
  115. package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
  116. package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
  117. package/dist/ui/assets/scss-BQb-cp01.js +1 -0
  118. package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
  119. package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
  120. package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
  121. package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
  122. package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
  123. package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
  124. package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
  125. package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
  126. package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
  127. package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
  128. package/dist/ui/assets/tsx-CajzROyy.js +1 -0
  129. package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
  130. package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
  131. package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
  132. package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
  133. package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
  134. package/dist/ui/assets/xml-BI86-WbH.js +1 -0
  135. package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
  136. package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
  137. package/dist/workspaces/bootstrap.js +72 -0
  138. package/dist/workspaces/context.js +302 -0
  139. package/dist/workspaces/conversation-test-support.js +86 -0
  140. package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
  141. package/dist/workspaces/inventory.js +131 -0
  142. package/dist/workspaces/paths.js +55 -0
  143. package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
  144. package/dist/workspaces/relay/result-support.js +111 -0
  145. package/dist/workspaces/relay/tests/test-support.js +203 -0
  146. package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
  147. package/dist/workspaces/relay/types.js +1 -0
  148. package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
  149. package/dist/workspaces/resources/skills.js +285 -0
  150. package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
  151. package/dist/workspaces/sessions.js +457 -0
  152. package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
  153. package/dist/workspaces.js +68 -995
  154. package/docs/chatgpt-coding-workflow.md +3 -11
  155. package/docs/configuration.md +12 -25
  156. package/docs/gotchas.md +0 -30
  157. package/docs/roadmap.md +6 -10
  158. package/docs/security.md +1 -5
  159. package/docs/setup.md +0 -11
  160. package/package.json +6 -8
  161. package/scripts/ci/architecture.mjs +109 -0
  162. package/scripts/ci/verify.mjs +5 -0
  163. package/scripts/debug/accept/bootstrap.mjs +326 -0
  164. package/scripts/debug/accept/harness.mjs +76 -0
  165. package/scripts/debug/accept/support.mjs +432 -0
  166. package/scripts/debug/accept.mjs +43 -806
  167. package/scripts/debug/relay-accept/support.mjs +135 -0
  168. package/scripts/debug/relay-accept.mjs +26 -115
  169. package/scripts/debug/traffic/support.mjs +167 -0
  170. package/scripts/debug/traffic/traffic-audit.mjs +4 -161
  171. package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
  172. package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
  173. package/scripts/wiki/sync.mjs +21 -10
  174. package/dist/activity/bash-output-store.js +0 -127
  175. package/dist/pi-tools.js +0 -110
  176. package/dist/skills.js +0 -127
  177. package/dist/ui/assets/activity-panel-app-7AckaxIR.js +0 -2
  178. package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
  179. package/dist/ui/assets/c-IBzBpxAO.js +0 -1
  180. package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
  181. package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
  182. package/dist/ui/assets/css-DEfus9J5.js +0 -1
  183. package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
  184. package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
  185. package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
  186. package/dist/ui/assets/heavy-payload-Bol1_mcs.js +0 -4
  187. package/dist/ui/assets/html-D-C6YZk4.js +0 -1
  188. package/dist/ui/assets/java-ppHvycj5.js +0 -1
  189. package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
  190. package/dist/ui/assets/json-Cw91r8h1.js +0 -1
  191. package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
  192. package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
  193. package/dist/ui/assets/r-DfFga4si.js +0 -1
  194. package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
  195. package/dist/ui/assets/review-payload-BqJp0c5e.js +0 -1
  196. package/dist/ui/assets/scrollbar-CvE-I-jG.js +0 -287
  197. package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
  198. package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
  199. package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
  200. package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
  201. package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
  202. package/dist/ui/assets/workspace-app-BLW7p6IZ.js +0 -114
  203. package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
  204. package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
  205. package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
  206. package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.js +0 -1
  207. package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
  208. package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
  209. package/dist/ui/workspace-app.html +0 -18
  210. package/dist/ui/workspace-lifecycle-app.html +0 -18
  211. package/docs/agent-profile-schema.md +0 -114
  212. package/docs/artifact-exchange.md +0 -65
  213. package/docs/versioning.md +0 -205
  214. /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
  215. /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +0 -0
  216. /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
  217. /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
  218. /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
  219. /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
  220. /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
  221. /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
  222. /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
  223. /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
  224. /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
  225. /package/dist/{oauth → mcp/oauth}/router.js +0 -0
  226. /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
  227. /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
  228. /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
  229. /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
  230. /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
  231. /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
  232. /package/dist/{process-platform.js → mcp/process/process-platform.js} +0 -0
  233. /package/dist/{process-sessions.js → mcp/process/process-sessions.js} +0 -0
  234. /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
  235. /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
  236. /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
  237. /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
  238. /package/dist/{db → runtime/state/db}/schema.js +0 -0
  239. /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
  240. /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
  241. /package/dist/{git.js → workspaces/git/git.js} +0 -0
  242. /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
  243. /package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +0 -0
  244. /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
  245. /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
  246. /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.8.9] - 2026-09-02
8
+
9
+ ### Changed
10
+
11
+ - Removed the remaining DevSpace-era runtime compatibility identifiers and migration aliases while retaining upstream MIT attribution in `LICENSE`, `NOTICE.md`, and release provenance checks.
12
+ - Removed bundled Codex, Claude, OpenCode, and Pi executor packages; subagent providers now invoke user-installed external CLIs, while ForgeRelay keeps only protocol/runtime infrastructure it owns.
13
+ - Upgraded `better-sqlite3` to 13.0.3 and replaced Pi-owned file/Skill helpers with ForgeRelay implementations.
14
+
15
+ ### Fixed
16
+
17
+ - Eliminated the install-time `prebuild-install` and `node-domexception` deprecation chains without dependency overrides.
18
+
7
19
  ## [0.8.8] - 2026-09-02
8
20
 
9
21
  ### Fixed
package/README.md CHANGED
@@ -231,7 +231,8 @@ forgerelay hooks list --project /path/to/project
231
231
 
232
232
  ForgeRelay can delegate work to local coding runtimes through user-defined
233
233
  profiles. The current adapter layer supports Codex, Claude, OpenCode, Pi,
234
- Cursor, and Copilot where the corresponding local integration is available.
234
+ Cursor, and Copilot when the corresponding CLI is installed on the server.
235
+ ForgeRelay does not install or bundle those coding runtimes.
235
236
 
236
237
  Profiles can live in:
237
238
 
@@ -248,12 +249,12 @@ forgerelay agents run <profile-or-provider-or-id> "<prompt>"
248
249
  forgerelay agents show <id>
249
250
  ```
250
251
 
251
- A first-class MCP subagent interface is planned so a parent agent can use the
252
- same runtime without going through the CLI.
252
+ MCP hosts delegate through the `subagent.session` Capability Gateway; the CLI
253
+ remains useful for local diagnostics and manual session inspection.
253
254
 
254
- See [Agent Profile Schema](docs/agent-profile-schema.md) for the profile format.
255
+ See [Agent Profile Schema](docs/agents/profile-schema.md) for the profile format.
255
256
 
256
- ## Configuration and upgrades from DevSpace
257
+ ## Configuration
257
258
 
258
259
  Configuration uses the `FORGERELAY_*` prefix. For example:
259
260
 
@@ -263,15 +264,6 @@ FORGERELAY_PUBLIC_BASE_URL="https://forge.example.com" \
263
264
  forgerelay serve
264
265
  ```
265
266
 
266
- The rename-era automatic compatibility window has ended. `DEVSPACE_*`
267
- environment variables are ignored, ForgeRelay no longer auto-selects
268
- `~/.devspace`, and project `.devspace/agents` profiles are not discovered.
269
-
270
- If an older installation still has data under DevSpace-named directories,
271
- migrate it explicitly or point the canonical ForgeRelay settings at those paths
272
- while moving the data. Persisted internal identifiers that would otherwise
273
- orphan existing state remain compatible and are not product-facing configuration.
274
-
275
267
  See [Configuration Reference](docs/configuration.md) for all supported options.
276
268
 
277
269
  ## Security model
@@ -341,7 +333,7 @@ after all platforms succeed. `npm run release:verify` remains available only whe
341
333
  cloud failure needs local reproduction against the pinned Node runtime; it is not a
342
334
  release prerequisite.
343
335
 
344
- See [Versioning and Release Management](docs/versioning.md) for the bootstrap and
336
+ See [Versioning and Release Management](docs/reference/versioning.md) for the bootstrap and
345
337
  Trusted Publishing setup.
346
338
 
347
339
  ## Documentation
@@ -351,12 +343,12 @@ Trusted Publishing setup.
351
343
  - [Local Debugging and 7677 Acceptance](docs/debugging.md)
352
344
  - [ChatGPT Coding Workflow](docs/chatgpt-coding-workflow.md)
353
345
  - [Configuration Reference](docs/configuration.md)
354
- - [Agent Profile Schema](docs/agent-profile-schema.md)
355
- - [Native File Download](docs/artifact-exchange.md)
346
+ - [Agent Profile Schema](docs/agents/profile-schema.md)
347
+ - [Native File Download](docs/reference/artifact-exchange.md)
356
348
  - [Security Model](docs/security.md)
357
349
  - [Troubleshooting](docs/gotchas.md)
358
350
  - [Roadmap](docs/roadmap.md)
359
- - [Versioning and Release Management](docs/versioning.md)
351
+ - [Versioning and Release Management](docs/reference/versioning.md)
360
352
  - [GitHub Wiki Maintenance](docs/maintenance/wiki.md)
361
353
  - [Changelog](CHANGELOG.md)
362
354
  - [Attribution Notice](NOTICE.md)
@@ -25,7 +25,7 @@ ForgeRelay 本地监听一个 HTTP origin,MCP endpoint 是 `/mcp`。当 Host
25
25
 
26
26
  ## OAuth owner flow
27
27
 
28
- ForgeRelay 使用 single-user Owner-password OAuth approval flow。新安装的 owner secret 通常保存在 `~/.forgerelay/auth.json`;迁移安装可能继续使用旧 `.devspace` 配置目录。不要把 owner token、refresh token 或 `auth.json` 内容放进 Agent 输出、项目文件或日志。
28
+ ForgeRelay 使用 single-user Owner-password OAuth approval flowowner secret 通常保存在 `~/.forgerelay/auth.json`。不要把 owner token、refresh token 或 `auth.json` 内容放进 Agent 输出、项目文件或日志。
29
29
 
30
30
  Host 可通过这些 metadata endpoint 发现授权配置:
31
31
 
@@ -34,7 +34,7 @@ Host 可通过这些 metadata endpoint 发现授权配置:
34
34
  /.well-known/oauth-authorization-server
35
35
  ```
36
36
 
37
- 默认 redirect hosts 包含 `chatgpt.com`、`localhost`、`127.0.0.1`。其他 MCP client 需要显式加入 `FORGERELAY_OAUTH_ALLOWED_REDIRECT_HOSTS`。OAuth scope 默认仍使用 legacy-compatible internal identifier `devspace`,这是协议兼容值,不是产品名回退。
37
+ 默认 redirect hosts 包含 `chatgpt.com`、`localhost`、`127.0.0.1`。其他 MCP client 需要显式加入 `FORGERELAY_OAUTH_ALLOWED_REDIRECT_HOSTS`。OAuth scope 默认使用 `forgerelay`。
38
38
 
39
39
  Owner password 不被接受时先用 `forgerelay doctor` 确认实际 auth/config 路径;只有用户明确要重建配置时才使用 `forgerelay init --force`。
40
40
 
@@ -1,11 +1,18 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { openDatabase } from "../db/client.js";
2
+ import { openDatabase } from "../../runtime/state/db/client.js";
3
+ import { SegmentedLogStore } from "../storage/segmented-log.js";
4
+ import { activityEventLogPrefix } from "../storage/paths.js";
3
5
  export class ActivityAuditStore {
4
6
  database;
5
7
  now;
8
+ stateDir;
9
+ payloads;
6
10
  constructor(stateDir, options = {}) {
7
11
  this.database = openDatabase(stateDir);
8
12
  this.now = options.now ?? (() => new Date());
13
+ this.stateDir = stateDir;
14
+ this.payloads = new SegmentedLogStore(stateDir);
15
+ this.migrateLegacyPayloads();
9
16
  }
10
17
  append(input) {
11
18
  return this.database.sqlite.transaction(() => {
@@ -30,7 +37,14 @@ export class ActivityAuditStore {
30
37
  const sequence = existing.length + 1;
31
38
  const id = `evt_${randomUUID().replaceAll("-", "")}`;
32
39
  const createdAt = this.now().toISOString();
33
- const row = eventInputToRow(input, { id, sequence, createdAt });
40
+ const workspace = input.type === "started"
41
+ ? input.workspace
42
+ : workspaceFromStartedRow(existing[0]);
43
+ const payload = eventPayload(input);
44
+ const payloadRef = payload === undefined
45
+ ? undefined
46
+ : this.payloads.append(activityEventLogPrefix(this.stateDir, workspace), Buffer.from(JSON.stringify(payload), "utf8"));
47
+ const row = eventInputToRow(input, { id, sequence, createdAt }, payloadRef);
34
48
  this.database.sqlite.prepare(`insert into activity_audit_events (
35
49
  id,
36
50
  activity_id,
@@ -49,13 +63,16 @@ export class ActivityAuditStore {
49
63
  request_json,
50
64
  result_json,
51
65
  error,
66
+ payload_file,
67
+ payload_offset,
68
+ payload_length,
52
69
  created_at
53
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.activity_id, row.sequence, row.event_type, row.turn_id, row.parent_activity_id, row.conversation_scope_id, row.tool, row.workspace_id, row.workspace_root, row.workspace_mode, row.workspace_source_root, row.workspace_branch, row.workspace_target_branch, row.request_json, row.result_json, row.error, row.created_at);
54
- return rowToEvent(row);
70
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.activity_id, row.sequence, row.event_type, row.turn_id, row.parent_activity_id, row.conversation_scope_id, row.tool, row.workspace_id, row.workspace_root, row.workspace_mode, row.workspace_source_root, row.workspace_branch, row.workspace_target_branch, row.request_json, row.result_json, row.error, row.payload_file, row.payload_offset, row.payload_length, row.created_at);
71
+ return this.rowToEvent(row);
55
72
  })();
56
73
  }
57
74
  listEvents(activityId) {
58
- return this.readRows(activityId).map(rowToEvent);
75
+ return this.readRows(activityId).map((row) => this.rowToEvent(row));
59
76
  }
60
77
  listActivitiesByTurn(turnId) {
61
78
  const rows = this.database.sqlite.prepare(`select activity_id from activity_audit_events
@@ -164,6 +181,13 @@ export class ActivityAuditStore {
164
181
  updatedAt,
165
182
  };
166
183
  }
184
+ deleteWorkspace(workspaceId) {
185
+ this.database.sqlite.prepare(`delete from activity_audit_events
186
+ where activity_id in (
187
+ select activity_id from activity_audit_events
188
+ where event_type = 'started' and workspace_id = ?
189
+ )`).run(workspaceId);
190
+ }
167
191
  close() {
168
192
  this.database.close();
169
193
  }
@@ -172,8 +196,51 @@ export class ActivityAuditStore {
172
196
  where activity_id = ?
173
197
  order by sequence asc`).all(activityId);
174
198
  }
199
+ rowToEvent(row) {
200
+ return rowToEvent(row, this.readPayload(row));
201
+ }
202
+ readPayload(row) {
203
+ if (row.payload_file !== null && row.payload_offset !== null && row.payload_length !== null) {
204
+ return JSON.parse(this.payloads.read({
205
+ prefix: row.payload_file,
206
+ offset: row.payload_offset,
207
+ length: row.payload_length,
208
+ }).toString("utf8"));
209
+ }
210
+ return legacyPayload(row);
211
+ }
212
+ migrateLegacyPayloads() {
213
+ const rows = this.database.sqlite.prepare(`select * from activity_audit_events
214
+ where payload_file is null
215
+ and (request_json is not null or result_json is not null or error is not null)
216
+ order by rowid asc`);
217
+ const workspaceByActivity = new Map();
218
+ const update = this.database.sqlite.prepare(`update activity_audit_events
219
+ set payload_file = ?, payload_offset = ?, payload_length = ?,
220
+ request_json = null, result_json = null, error = null
221
+ where id = ?`);
222
+ for (const row of rows.iterate()) {
223
+ const payload = legacyPayload(row);
224
+ if (payload === undefined)
225
+ continue;
226
+ let workspace = workspaceByActivity.get(row.activity_id);
227
+ if (!workspace) {
228
+ const started = row.event_type === "started"
229
+ ? row
230
+ : this.database.sqlite.prepare(`select * from activity_audit_events
231
+ where activity_id = ? and event_type = 'started'
232
+ order by sequence asc limit 1`).get(row.activity_id);
233
+ if (!started)
234
+ continue;
235
+ workspace = workspaceFromStartedRow(started);
236
+ workspaceByActivity.set(row.activity_id, workspace);
237
+ }
238
+ const reference = this.payloads.append(activityEventLogPrefix(this.stateDir, workspace), Buffer.from(JSON.stringify(payload), "utf8"));
239
+ update.run(reference.prefix, reference.offset, reference.length, row.id);
240
+ }
241
+ }
175
242
  }
176
- function eventInputToRow(input, identity) {
243
+ function eventInputToRow(input, identity, payloadRef) {
177
244
  if (input.type === "started") {
178
245
  return {
179
246
  id: identity.id,
@@ -190,9 +257,12 @@ function eventInputToRow(input, identity) {
190
257
  workspace_source_root: input.workspace.sourceRoot ?? null,
191
258
  workspace_branch: input.workspace.branch ?? null,
192
259
  workspace_target_branch: input.workspace.targetBranch ?? null,
193
- request_json: serializeJson(input.request),
260
+ request_json: null,
194
261
  result_json: null,
195
262
  error: null,
263
+ payload_file: payloadRef?.prefix ?? null,
264
+ payload_offset: payloadRef?.offset ?? null,
265
+ payload_length: payloadRef?.length ?? null,
196
266
  created_at: identity.createdAt,
197
267
  };
198
268
  }
@@ -212,12 +282,15 @@ function eventInputToRow(input, identity) {
212
282
  workspace_branch: null,
213
283
  workspace_target_branch: null,
214
284
  request_json: null,
215
- result_json: "result" in input ? serializeJson(input.result) : null,
216
- error: "error" in input ? input.error : null,
285
+ result_json: null,
286
+ error: null,
287
+ payload_file: payloadRef?.prefix ?? null,
288
+ payload_offset: payloadRef?.offset ?? null,
289
+ payload_length: payloadRef?.length ?? null,
217
290
  created_at: identity.createdAt,
218
291
  };
219
292
  }
220
- function rowToEvent(row) {
293
+ function rowToEvent(row, payload) {
221
294
  const base = {
222
295
  id: row.id,
223
296
  activityId: row.activity_id,
@@ -244,47 +317,82 @@ function rowToEvent(row) {
244
317
  ...(row.workspace_branch ? { branch: row.workspace_branch } : {}),
245
318
  ...(row.workspace_target_branch ? { targetBranch: row.workspace_target_branch } : {}),
246
319
  },
247
- ...(row.request_json !== null ? { request: parseJson(row.request_json) } : {}),
320
+ ...(payload?.request !== undefined ? { request: payload.request } : {}),
248
321
  };
249
322
  case "succeeded":
250
323
  return {
251
324
  ...base,
252
325
  type: "succeeded",
253
- result: parseJson(row.result_json),
326
+ ...(payload?.result !== undefined ? { result: payload.result } : {}),
254
327
  };
255
328
  case "returned":
256
329
  return {
257
330
  ...base,
258
331
  type: "returned",
259
- result: parseJson(row.result_json),
332
+ ...(payload?.result !== undefined ? { result: payload.result } : {}),
260
333
  };
261
334
  case "failed":
262
- if (!row.error)
335
+ if (!payload?.error)
263
336
  throw new Error(`Activity audit failed event ${row.id} is missing an error.`);
264
337
  return {
265
338
  ...base,
266
339
  type: "failed",
267
- result: parseJson(row.result_json),
268
- error: row.error,
340
+ ...(payload.result !== undefined ? { result: payload.result } : {}),
341
+ error: payload.error,
269
342
  };
270
343
  case "blocked":
271
- if (!row.error)
344
+ if (!payload?.error)
272
345
  throw new Error(`Activity audit blocked event ${row.id} is missing an error.`);
273
346
  return {
274
347
  ...base,
275
348
  type: "blocked",
276
- error: row.error,
349
+ error: payload.error,
277
350
  };
278
351
  default:
279
352
  throw new Error(`Unknown Activity audit event type: ${row.event_type}`);
280
353
  }
281
354
  }
355
+ function eventPayload(input) {
356
+ if (input.type === "started") {
357
+ return input.request === undefined ? undefined : { request: input.request };
358
+ }
359
+ if (input.type === "blocked")
360
+ return { error: input.error };
361
+ if (input.type === "failed") {
362
+ return {
363
+ ...(input.result !== undefined ? { result: input.result } : {}),
364
+ error: input.error,
365
+ };
366
+ }
367
+ return input.result === undefined ? undefined : { result: input.result };
368
+ }
369
+ function legacyPayload(row) {
370
+ const request = parseJson(row.request_json);
371
+ const result = parseJson(row.result_json);
372
+ if (request === undefined && result === undefined && row.error === null)
373
+ return undefined;
374
+ return {
375
+ ...(request !== undefined ? { request } : {}),
376
+ ...(result !== undefined ? { result } : {}),
377
+ ...(row.error !== null ? { error: row.error } : {}),
378
+ };
379
+ }
380
+ function workspaceFromStartedRow(row) {
381
+ if (!row.workspace_root || !isWorkspaceMode(row.workspace_mode)) {
382
+ throw new Error(`Activity audit start event ${row.id} is missing Workspace context.`);
383
+ }
384
+ return {
385
+ ...(row.workspace_id ? { id: row.workspace_id } : {}),
386
+ root: row.workspace_root,
387
+ mode: row.workspace_mode,
388
+ ...(row.workspace_source_root ? { sourceRoot: row.workspace_source_root } : {}),
389
+ ...(row.workspace_branch ? { branch: row.workspace_branch } : {}),
390
+ ...(row.workspace_target_branch ? { targetBranch: row.workspace_target_branch } : {}),
391
+ };
392
+ }
282
393
  function isWorkspaceMode(value) {
283
394
  return value === "checkout" || value === "worktree";
284
395
  }
285
- function serializeJson(value) {
286
- return value === undefined ? null : JSON.stringify(value);
287
- }
288
396
  function parseJson(value) {
289
397
  return value === null ? undefined : JSON.parse(value);
290
398
  }
@@ -0,0 +1,250 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { openDatabase } from "../../runtime/state/db/client.js";
3
+ import { SegmentedLogStore } from "../storage/segmented-log.js";
4
+ import { bashOutputLogPrefix, bashOutputMetadataPrefix, stateRelativePath } from "../storage/paths.js";
5
+ const DEFAULT_FLUSH_BYTES = 256 * 1024;
6
+ const DEFAULT_FLUSH_INTERVAL_MS = 500;
7
+ export class BashOutputStore {
8
+ database;
9
+ now;
10
+ nextOutputId;
11
+ stateDir;
12
+ logs;
13
+ pending = new Map();
14
+ flushBytes;
15
+ flushTimer;
16
+ constructor(stateDir, options = {}) {
17
+ this.database = openDatabase(stateDir);
18
+ this.now = options.now ?? (() => new Date());
19
+ this.nextOutputId = options.outputId ?? (() => `out_${randomUUID().replaceAll("-", "")}`);
20
+ this.stateDir = stateDir;
21
+ this.logs = new SegmentedLogStore(stateDir);
22
+ this.flushBytes = positiveInteger(options.flushBytes, DEFAULT_FLUSH_BYTES, "Bash output flushBytes");
23
+ const flushIntervalMs = positiveInteger(options.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS, "Bash output flushIntervalMs");
24
+ this.migrateLegacyStreams();
25
+ this.migrateLegacyMetadata();
26
+ this.flushTimer = setInterval(() => this.flushPending(), flushIntervalMs);
27
+ this.flushTimer.unref();
28
+ }
29
+ begin(input) {
30
+ const outputId = this.nextOutputId();
31
+ const startedAt = this.now().toISOString();
32
+ const workspace = { id: input.workspaceId, root: input.workspaceRoot, mode: "checkout" };
33
+ const logPrefix = stateRelativePath(this.stateDir, bashOutputLogPrefix(this.stateDir, workspace, outputId));
34
+ const commandRef = this.logs.append(bashOutputMetadataPrefix(this.stateDir, workspace, outputId, "command"), Buffer.from(input.command, "utf8"));
35
+ this.database.sqlite.prepare(`insert into bash_output_streams (
36
+ id, activity_id, turn_id, conversation_scope_id, process_id,
37
+ workspace_id, workspace_root, command, tty, status, timed_out, started_at,
38
+ log_file, output_bytes, command_file, command_offset, command_length
39
+ ) values (?, ?, ?, ?, ?, ?, ?, '', ?, 'running', 0, ?, ?, 0, ?, ?, ?)`).run(outputId, input.activityId, input.turnId, input.conversationScopeId ?? null, input.processId, input.workspaceId, input.workspaceRoot, input.tty ? 1 : 0, startedAt, logPrefix, commandRef.prefix, commandRef.offset, commandRef.length);
40
+ return outputId;
41
+ }
42
+ append(outputId, _channel, data) {
43
+ const bytes = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
44
+ if (bytes.length === 0)
45
+ return;
46
+ const pending = this.pending.get(outputId) ?? { buffers: [], bytes: 0 };
47
+ pending.buffers.push(bytes);
48
+ pending.bytes += bytes.length;
49
+ this.pending.set(outputId, pending);
50
+ if (pending.bytes >= this.flushBytes)
51
+ this.flushOutput(outputId);
52
+ }
53
+ markReturned(outputId) {
54
+ this.flushOutput(outputId);
55
+ this.database.sqlite.prepare("update bash_output_streams set returned = 1 where id = ?").run(outputId);
56
+ }
57
+ claimCompletion(outputId) {
58
+ this.flushOutput(outputId);
59
+ const claimedAt = this.now().toISOString();
60
+ const claimed = this.database.sqlite.prepare(`update bash_output_streams
61
+ set completion_claimed_at = ?
62
+ where id = ? and returned = 1 and status != 'running' and completion_claimed_at is null`).run(claimedAt, outputId);
63
+ return claimed.changes === 1 ? this.readMetadata(outputId) : undefined;
64
+ }
65
+ finish(outputId, input) {
66
+ this.flushOutput(outputId);
67
+ const row = this.requireStream(outputId);
68
+ const status = input.error || input.timedOut || input.signal || (input.exitCode !== undefined && input.exitCode !== 0)
69
+ ? "failed"
70
+ : "done";
71
+ const errorRef = input.error
72
+ ? this.logs.append(bashOutputMetadataPrefix(this.stateDir, { id: row.workspace_id, root: row.workspace_root, mode: "checkout" }, outputId, "error"), Buffer.from(input.error, "utf8"))
73
+ : undefined;
74
+ this.database.sqlite.prepare(`update bash_output_streams
75
+ set status = ?, exit_code = ?, signal = ?, timed_out = ?, error = null, finished_at = ?,
76
+ error_file = ?, error_offset = ?, error_length = ?
77
+ where id = ?`).run(status, input.exitCode ?? null, input.signal ?? null, input.timedOut ? 1 : 0, this.now().toISOString(), errorRef?.prefix ?? null, errorRef?.offset ?? null, errorRef?.length ?? null, outputId);
78
+ }
79
+ read(outputId) {
80
+ const metadata = this.readMetadata(outputId);
81
+ if (!metadata)
82
+ return undefined;
83
+ const row = this.requireStream(outputId);
84
+ const prefix = this.ensureFileBacked(row);
85
+ const output = this.logs.readRange(prefix, 0, metadata.outputBytes).toString("utf8");
86
+ return { ...metadata, output };
87
+ }
88
+ readSince(outputId, cursor = 0) {
89
+ const metadata = this.readMetadata(outputId);
90
+ if (!metadata)
91
+ return undefined;
92
+ if (!Number.isSafeInteger(cursor) || cursor < 0) {
93
+ throw new Error(`Bash output cursor must be a non-negative integer: ${cursor}.`);
94
+ }
95
+ const start = Math.min(cursor, metadata.outputBytes);
96
+ const row = this.requireStream(outputId);
97
+ const prefix = this.ensureFileBacked(row);
98
+ const output = this.logs.readRange(prefix, start, metadata.outputBytes).toString("utf8");
99
+ return { ...metadata, output, cursor: metadata.outputBytes };
100
+ }
101
+ readMetadata(outputId) {
102
+ this.flushOutput(outputId);
103
+ const row = this.readStream(outputId);
104
+ if (!row)
105
+ return undefined;
106
+ this.ensureFileBacked(row);
107
+ const refreshed = this.requireStream(outputId);
108
+ return this.rowToMetadata(refreshed);
109
+ }
110
+ deleteWorkspace(workspaceId) {
111
+ const rows = this.database.sqlite.prepare("select id from bash_output_streams where workspace_id = ?").all(workspaceId);
112
+ for (const row of rows) {
113
+ this.flushOutput(row.id);
114
+ this.pending.delete(row.id);
115
+ }
116
+ this.database.sqlite.prepare("delete from bash_output_streams where workspace_id = ?").run(workspaceId);
117
+ }
118
+ close() {
119
+ clearInterval(this.flushTimer);
120
+ this.flushPending();
121
+ this.database.close();
122
+ }
123
+ flushPending() {
124
+ for (const outputId of [...this.pending.keys()])
125
+ this.flushOutput(outputId);
126
+ }
127
+ flushOutput(outputId) {
128
+ const pending = this.pending.get(outputId);
129
+ if (!pending || pending.bytes === 0)
130
+ return;
131
+ const row = this.requireStream(outputId);
132
+ const prefix = this.ensureFileBacked(row);
133
+ const onDiskBytes = this.logs.logicalLength(prefix);
134
+ if (onDiskBytes < row.output_bytes) {
135
+ throw new Error(`Bash output ${outputId} metadata references ${row.output_bytes} bytes but only ${onDiskBytes} are on disk.`);
136
+ }
137
+ const data = Buffer.concat(pending.buffers, pending.bytes);
138
+ // File first, index second. If SQLite updating fails after the write, the
139
+ // next access reconciles output_bytes from the append-only log rather than
140
+ // re-emitting the already-written payload.
141
+ this.logs.appendAt(prefix, onDiskBytes, data);
142
+ this.pending.delete(outputId);
143
+ this.database.sqlite.prepare("update bash_output_streams set output_bytes = ? where id = ?").run(onDiskBytes + data.length, outputId);
144
+ }
145
+ ensureFileBacked(row) {
146
+ let prefix = row.log_file;
147
+ if (!prefix) {
148
+ prefix = stateRelativePath(this.stateDir, bashOutputLogPrefix(this.stateDir, { id: row.workspace_id, root: row.workspace_root, mode: "checkout" }, row.id));
149
+ let logicalOffset = this.logs.logicalLength(prefix);
150
+ const legacyRows = this.database.sqlite.prepare(`select data from bash_output_chunks
151
+ where output_id = ?
152
+ order by sequence asc`);
153
+ for (const legacy of legacyRows.iterate(row.id)) {
154
+ this.logs.appendAt(prefix, logicalOffset, legacy.data);
155
+ logicalOffset += legacy.data.length;
156
+ }
157
+ this.database.sqlite.transaction(() => {
158
+ this.database.sqlite.prepare("update bash_output_streams set log_file = ?, output_bytes = ? where id = ?").run(prefix, logicalOffset, row.id);
159
+ this.database.sqlite.prepare("delete from bash_output_chunks where output_id = ?").run(row.id);
160
+ }).immediate();
161
+ row.log_file = prefix;
162
+ row.output_bytes = logicalOffset;
163
+ return prefix;
164
+ }
165
+ const onDiskBytes = this.logs.logicalLength(prefix);
166
+ if (onDiskBytes < row.output_bytes) {
167
+ throw new Error(`Bash output ${row.id} index is ahead of its segmented log (${row.output_bytes} > ${onDiskBytes}).`);
168
+ }
169
+ if (onDiskBytes > row.output_bytes) {
170
+ this.database.sqlite.prepare("update bash_output_streams set output_bytes = ? where id = ?").run(onDiskBytes, row.id);
171
+ row.output_bytes = onDiskBytes;
172
+ }
173
+ return prefix;
174
+ }
175
+ migrateLegacyStreams() {
176
+ const rows = this.database.sqlite.prepare("select * from bash_output_streams where log_file is null order by started_at asc");
177
+ for (const row of rows.iterate()) {
178
+ this.ensureFileBacked(row);
179
+ }
180
+ }
181
+ migrateLegacyMetadata() {
182
+ const rows = this.database.sqlite.prepare(`select * from bash_output_streams
183
+ where command_file is null or (error is not null and error_file is null)
184
+ order by started_at asc`);
185
+ const update = this.database.sqlite.prepare(`update bash_output_streams
186
+ set command = '', error = null,
187
+ command_file = ?, command_offset = ?, command_length = ?,
188
+ error_file = ?, error_offset = ?, error_length = ?
189
+ where id = ?`);
190
+ for (const row of rows.iterate()) {
191
+ const workspace = { id: row.workspace_id, root: row.workspace_root, mode: "checkout" };
192
+ const commandRef = row.command_file
193
+ ? undefined
194
+ : this.logs.append(bashOutputMetadataPrefix(this.stateDir, workspace, row.id, "command"), Buffer.from(row.command, "utf8"));
195
+ const errorRef = row.error && !row.error_file
196
+ ? this.logs.append(bashOutputMetadataPrefix(this.stateDir, workspace, row.id, "error"), Buffer.from(row.error, "utf8"))
197
+ : undefined;
198
+ update.run(commandRef?.prefix ?? row.command_file, commandRef?.offset ?? row.command_offset, commandRef?.length ?? row.command_length, errorRef?.prefix ?? row.error_file, errorRef?.offset ?? row.error_offset, errorRef?.length ?? row.error_length, row.id);
199
+ }
200
+ }
201
+ rowToMetadata(stream) {
202
+ const command = this.readMetadataField(stream.command_file, stream.command_offset, stream.command_length, stream.command);
203
+ const error = this.readMetadataField(stream.error_file, stream.error_offset, stream.error_length, stream.error ?? undefined);
204
+ return {
205
+ outputId: stream.id,
206
+ activityId: stream.activity_id,
207
+ turnId: stream.turn_id,
208
+ ...(stream.conversation_scope_id ? { conversationScopeId: stream.conversation_scope_id } : {}),
209
+ processId: stream.process_id,
210
+ workspaceId: stream.workspace_id,
211
+ workspaceRoot: stream.workspace_root,
212
+ command,
213
+ tty: stream.tty === 1,
214
+ status: isBashOutputStatus(stream.status) ? stream.status : "failed",
215
+ ...(stream.exit_code !== null ? { exitCode: stream.exit_code } : {}),
216
+ ...(stream.signal ? { signal: stream.signal } : {}),
217
+ timedOut: stream.timed_out === 1,
218
+ ...(error ? { error } : {}),
219
+ returned: stream.returned === 1,
220
+ outputBytes: stream.output_bytes,
221
+ startedAt: stream.started_at,
222
+ ...(stream.finished_at ? { finishedAt: stream.finished_at } : {}),
223
+ };
224
+ }
225
+ readMetadataField(file, offset, length, fallback) {
226
+ if (file !== null && offset !== null && length !== null) {
227
+ return this.logs.read({ prefix: file, offset, length }).toString("utf8");
228
+ }
229
+ return fallback ?? "";
230
+ }
231
+ readStream(outputId) {
232
+ return this.database.sqlite.prepare("select * from bash_output_streams where id = ?").get(outputId);
233
+ }
234
+ requireStream(outputId) {
235
+ const row = this.readStream(outputId);
236
+ if (!row)
237
+ throw new Error(`Unknown Bash output: ${outputId}.`);
238
+ return row;
239
+ }
240
+ }
241
+ function isBashOutputStatus(value) {
242
+ return value === "running" || value === "done" || value === "failed";
243
+ }
244
+ function positiveInteger(value, fallback, label) {
245
+ const resolved = value ?? fallback;
246
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
247
+ throw new Error(`${label} must be a positive integer.`);
248
+ }
249
+ return resolved;
250
+ }
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { openDatabase } from "../db/client.js";
2
+ import { openDatabase } from "../../runtime/state/db/client.js";
3
3
  export class HostTurnStore {
4
4
  database;
5
5
  now;
@@ -34,6 +34,9 @@ export class HostTurnStore {
34
34
  limit 1`).get(conversationScopeId, workspaceId);
35
35
  return row ? rowToTurn(row) : undefined;
36
36
  }
37
+ deleteWorkspace(workspaceId) {
38
+ this.database.sqlite.prepare("delete from activity_host_turns where workspace_id = ?").run(workspaceId);
39
+ }
37
40
  close() {
38
41
  this.database.close();
39
42
  }
@@ -1,3 +1,4 @@
1
+ import { deleteWorkspaceActivityHistory } from "../storage/history.js";
1
2
  export class ActivityQueryService {
2
3
  turns;
3
4
  audit;
@@ -7,6 +8,15 @@ export class ActivityQueryService {
7
8
  this.audit = audit;
8
9
  this.outputs = outputs;
9
10
  }
11
+ deleteWorkspaceHistory(stateDir, workspaceId) {
12
+ deleteWorkspaceActivityHistory({
13
+ stateDir,
14
+ workspaceId,
15
+ audit: this.audit,
16
+ outputs: this.outputs,
17
+ turns: this.turns,
18
+ });
19
+ }
10
20
  beginTurn(conversationScopeId, workspaceId) {
11
21
  const turn = this.turns.begin(conversationScopeId, workspaceId);
12
22
  return this.state(turn.turnId);
@@ -73,7 +83,7 @@ export class ActivityQueryService {
73
83
  }
74
84
  bashOutput(turnId, outputId, cursor = 0) {
75
85
  this.requireTurn(turnId);
76
- const output = this.outputs.read(outputId);
86
+ const output = this.outputs.readSince(outputId, cursor);
77
87
  if (!output)
78
88
  throw new Error(`Unknown Bash output: ${outputId}.`);
79
89
  const activities = this.audit.listActivitiesByTurn(turnId);
@@ -81,17 +91,13 @@ export class ActivityQueryService {
81
91
  if (!visible) {
82
92
  throw new Error(`Bash output ${outputId} is not part of Host Turn ${turnId}.`);
83
93
  }
84
- const latestCursor = output.chunks.at(-1)?.sequence ?? 0;
85
94
  return {
86
95
  outputId: output.outputId,
87
96
  activityId: output.activityId,
88
97
  processId: output.processId,
89
98
  command: output.command,
90
- output: output.chunks
91
- .filter((chunk) => chunk.sequence > cursor)
92
- .map((chunk) => chunk.data)
93
- .join(""),
94
- cursor: latestCursor,
99
+ output: output.output,
100
+ cursor: output.cursor,
95
101
  status: output.status,
96
102
  ...(output.exitCode !== undefined ? { exitCode: output.exitCode } : {}),
97
103
  ...(output.signal !== undefined ? { signal: output.signal } : {}),