@akira-tl/forgerelay 0.8.7 → 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.
- package/CHANGELOG.md +18 -0
- package/README.md +10 -18
- package/capabilities/host-integration/GUIDE.md +2 -2
- package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
- package/dist/activity/history/bash-output-store.js +250 -0
- package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
- package/dist/activity/{query-service.js → history/query-service.js} +13 -7
- package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
- package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
- package/dist/activity/storage/history.js +12 -0
- package/dist/activity/storage/paths.js +56 -0
- package/dist/activity/storage/segmented-log.js +149 -0
- package/dist/cli/setup-support.js +116 -0
- package/dist/cli.js +18 -126
- package/dist/lsp/code-intelligence.js +1 -1
- package/dist/lsp/test-support/server-fixture.js +9 -9
- package/dist/mcp/filesystem/filesystem-tools.js +245 -0
- package/dist/mcp/hooks/command-runner.js +79 -0
- package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
- package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
- package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
- package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
- package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
- package/dist/mcp/panel/app.js +143 -0
- package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
- package/dist/mcp/process/runtime.js +257 -0
- package/dist/mcp/process/tools.js +346 -0
- package/dist/mcp/server/core/activity-support.js +70 -0
- package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
- package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +2 -2
- package/dist/mcp/server/core/capability-support.js +90 -0
- package/dist/mcp/server/core/schemas.js +216 -0
- package/dist/mcp/server/core/tool-support.js +207 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -0
- package/dist/mcp/server/transport/http-server.js +301 -0
- package/dist/mcp/server/transport/http-support.js +43 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +222 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/{config.js → runtime/config/config.js} +3 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
- package/dist/{db → runtime/state/db}/client.js +1 -1
- package/dist/{db → runtime/state/db}/migrations.js +34 -3
- package/dist/runtime/state/lock/file-lock.js +130 -0
- package/dist/runtime/testing/server-fixture.js +190 -0
- package/dist/server.js +139 -4192
- package/dist/subagents/providers/adapters/claude.js +86 -52
- package/dist/subagents/providers/adapters/codex.js +103 -39
- package/dist/subagents/providers/adapters/opencode.js +114 -84
- package/dist/subagents/providers/adapters/pi.js +59 -8
- package/dist/subagents/providers/availability.js +15 -30
- package/dist/subagents/providers/runtime/external-command.js +56 -0
- package/dist/subagents/sessions/capability.js +1 -1
- package/dist/subagents/sessions/execution.js +1 -1
- package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
- package/dist/subagents/sessions/store.js +1 -1
- package/dist/ui/.vite/manifest.json +342 -409
- package/dist/ui/activity-panel-app.html +2 -4
- package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
- package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
- package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
- package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
- package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
- package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
- package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
- package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
- package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
- package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
- package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
- package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
- package/dist/ui/assets/css-C1tjBtVN.js +1 -0
- package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
- package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
- package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
- package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
- package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
- package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
- package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
- package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
- package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
- package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
- package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
- package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
- package/dist/ui/assets/html-By4dNnM4.js +1 -0
- package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
- package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
- package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
- package/dist/ui/assets/java-DYRe2lWG.js +1 -0
- package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
- package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
- package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
- package/dist/ui/assets/json-dbltgiiA.js +1 -0
- package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
- package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
- package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
- package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
- package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
- package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
- package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
- package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
- package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
- package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
- package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
- package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
- package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
- package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
- package/dist/ui/assets/r-CcezTbLq.js +1 -0
- package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
- package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
- package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
- package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
- package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
- package/dist/ui/assets/scss-BQb-cp01.js +1 -0
- package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
- package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
- package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
- package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
- package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
- package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
- package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
- package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
- package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
- package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
- package/dist/ui/assets/tsx-CajzROyy.js +1 -0
- package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
- package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
- package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
- package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
- package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
- package/dist/ui/assets/xml-BI86-WbH.js +1 -0
- package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
- package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
- package/dist/workspaces/bootstrap.js +72 -0
- package/dist/workspaces/context.js +302 -0
- package/dist/workspaces/conversation-test-support.js +86 -0
- package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
- package/dist/workspaces/inventory.js +131 -0
- package/dist/workspaces/paths.js +55 -0
- package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
- package/dist/workspaces/relay/result-support.js +111 -0
- package/dist/workspaces/relay/tests/test-support.js +203 -0
- package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
- package/dist/workspaces/relay/types.js +1 -0
- package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
- package/dist/workspaces/resources/skills.js +285 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +457 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +69 -991
- package/docs/chatgpt-coding-workflow.md +3 -11
- package/docs/configuration.md +12 -25
- package/docs/gotchas.md +0 -30
- package/docs/roadmap.md +6 -10
- package/docs/security.md +1 -5
- package/docs/setup.md +0 -11
- package/package.json +6 -8
- package/scripts/ci/architecture.mjs +109 -0
- package/scripts/ci/verify.mjs +5 -0
- package/scripts/debug/accept/bootstrap.mjs +326 -0
- package/scripts/debug/accept/harness.mjs +76 -0
- package/scripts/debug/accept/support.mjs +432 -0
- package/scripts/debug/accept.mjs +43 -806
- package/scripts/debug/relay-accept/support.mjs +135 -0
- package/scripts/debug/relay-accept.mjs +26 -115
- package/scripts/debug/traffic/support.mjs +167 -0
- package/scripts/debug/traffic/traffic-audit.mjs +4 -161
- package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
- package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
- package/scripts/wiki/sync.mjs +21 -10
- package/dist/activity/bash-output-store.js +0 -127
- package/dist/pi-tools.js +0 -110
- package/dist/skills.js +0 -127
- package/dist/ui/assets/activity-panel-app-CUAN6zyW.js +0 -2
- package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
- package/dist/ui/assets/c-IBzBpxAO.js +0 -1
- package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
- package/dist/ui/assets/css-DEfus9J5.js +0 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
- package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
- package/dist/ui/assets/heavy-payload-CgzrutLm.js +0 -4
- package/dist/ui/assets/html-D-C6YZk4.js +0 -1
- package/dist/ui/assets/java-ppHvycj5.js +0 -1
- package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
- package/dist/ui/assets/json-Cw91r8h1.js +0 -1
- package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
- package/dist/ui/assets/r-DfFga4si.js +0 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
- package/dist/ui/assets/review-payload-BrLbezbq.js +0 -1
- package/dist/ui/assets/scrollbar-CbhpdW05.js +0 -287
- package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
- package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
- package/dist/ui/assets/workspace-app-Bhj96tsR.js +0 -1
- package/dist/ui/assets/workspace-app-CxwJuZyS.js +0 -111
- package/dist/ui/assets/workspace-app-D6UR0AFl.js +0 -5
- package/dist/ui/assets/workspace-app-ldjBmCJR.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +0 -1
- package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
- package/dist/ui/workspace-app.html +0 -18
- package/dist/ui/workspace-lifecycle-app.html +0 -18
- package/docs/agent-profile-schema.md +0 -114
- package/docs/artifact-exchange.md +0 -65
- package/docs/versioning.md +0 -205
- /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
- /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +0 -0
- /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
- /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
- /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
- /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
- /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
- /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
- /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
- /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
- /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
- /package/dist/{oauth → mcp/oauth}/router.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
- /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
- /package/dist/{process-platform.js → mcp/process/process-platform.js} +0 -0
- /package/dist/{process-sessions.js → mcp/process/process-sessions.js} +0 -0
- /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
- /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
- /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
- /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
- /package/dist/{db → runtime/state/db}/schema.js +0 -0
- /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
- /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
- /package/dist/{git.js → workspaces/git/git.js} +0 -0
- /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
- /package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +0 -0
- /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
- /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
- /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ 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
|
+
|
|
19
|
+
## [0.8.8] - 2026-09-02
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Restored the full lightweight Workspace overview across fresh MCP connections and brought rich Read/Edit/Write Activity details back behind lazy per-Activity loading, without reintroducing heavy default polling payloads.
|
|
24
|
+
|
|
7
25
|
## [0.8.7] - 2026-09-02
|
|
8
26
|
|
|
9
27
|
### Changed
|
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
|
|
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
|
-
|
|
252
|
-
|
|
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/
|
|
255
|
+
See [Agent Profile Schema](docs/agents/profile-schema.md) for the profile format.
|
|
255
256
|
|
|
256
|
-
## Configuration
|
|
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/
|
|
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
|
|
28
|
+
ForgeRelay 使用 single-user Owner-password OAuth approval flow。owner 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
|
|
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 "
|
|
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
|
|
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:
|
|
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:
|
|
216
|
-
error:
|
|
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
|
-
...(
|
|
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:
|
|
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:
|
|
332
|
+
...(payload?.result !== undefined ? { result: payload.result } : {}),
|
|
260
333
|
};
|
|
261
334
|
case "failed":
|
|
262
|
-
if (!
|
|
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:
|
|
268
|
-
error:
|
|
340
|
+
...(payload.result !== undefined ? { result: payload.result } : {}),
|
|
341
|
+
error: payload.error,
|
|
269
342
|
};
|
|
270
343
|
case "blocked":
|
|
271
|
-
if (!
|
|
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:
|
|
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 "
|
|
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
|
}
|