@akira-tl/forgerelay 0.8.8 → 0.8.10
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 +32 -0
- package/README.md +10 -18
- package/capabilities/code-intelligence/GUIDE.md +7 -3
- package/capabilities/host-integration/GUIDE.md +2 -2
- package/capabilities/subagents/GUIDE.md +9 -0
- package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +6 -0
- 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/init.js +180 -0
- package/dist/cli/setup-support.js +181 -0
- package/dist/cli.js +23 -216
- package/dist/lsp/code-intelligence.js +18 -4
- package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
- package/dist/lsp/runtime/managed-language-servers.js +172 -0
- package/dist/lsp/runtime/manager.js +37 -2
- package/dist/lsp/runtime/process-launch.js +3 -0
- package/dist/lsp/test-support/server-fixture.js +14 -11
- 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/{process-platform.js → mcp/process/process-platform.js} +21 -9
- package/dist/{process-sessions.js → mcp/process/process-sessions.js} +3 -3
- 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} +9 -4
- 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 +210 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +455 -0
- package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +524 -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 +242 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +555 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/{config.js → runtime/config/config.js} +4 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -63
- 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 +133 -4189
- 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 -410
- 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 +337 -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/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +30 -3
- 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/resource-monitor.js +377 -0
- package/dist/workspaces/resources/skills.js +283 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +460 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +77 -993
- package/docs/chatgpt-coding-workflow.md +7 -14
- package/docs/configuration.md +26 -34
- 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 -9
- 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/release/publish.mjs +12 -9
- package/scripts/release/release-gate.test.mjs +3 -1
- package/scripts/release/release-version.test.mjs +13 -1
- package/scripts/release-parity.mjs +1 -1
- package/scripts/release-proof.mjs +15 -0
- package/scripts/release-proof.test.mjs +27 -1
- package/scripts/release-version.mjs +16 -1
- 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-7AckaxIR.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-Bol1_mcs.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-BqJp0c5e.js +0 -1
- package/dist/ui/assets/scrollbar-CvE-I-jG.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-BLW7p6IZ.js +0 -114
- package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
- package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
- package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.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/skills/subagent-delegation/SKILL.md +0 -132
- /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/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/{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/{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
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { isRemoteMcpUnauthorized, refreshRemoteAuthentication, } from "./remote-auth.js";
|
|
6
|
-
import { RemoteMcpConnectionPool } from "./remote-mcp-connection-pool.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const ROUTE_LOCK_STALE_MS = 30_000;
|
|
12
|
-
const ROUTE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
5
|
+
import { isRemoteMcpUnauthorized, refreshRemoteAuthentication, } from "./auth/remote-auth.js";
|
|
6
|
+
import { RemoteMcpConnectionPool } from "./transport/remote-mcp-connection-pool.js";
|
|
7
|
+
import { withFileLock } from "../../runtime/state/lock/file-lock.js";
|
|
8
|
+
import { withRemoteServiceEndpoint } from "./transport/remote-transport.js";
|
|
9
|
+
import { loadForgeRelayFiles, writeForgeRelayRemote, } from "../../runtime/config/user-config.js";
|
|
10
|
+
import { assertRemoteToolSucceeded, stringField, copyStringField, copyBooleanField, copyNumberField, safeTaskSummary, toolResultText, remapToolResultWorkspaceId, replaceExactWorkspaceId, sanitizedRemoteError, errorMessage } from "./result-support.js";
|
|
13
11
|
export class RemoteWorkspaceRelay {
|
|
14
12
|
routes = new Map();
|
|
15
13
|
turnRoutes = new Map();
|
|
@@ -109,7 +107,7 @@ export class RemoteWorkspaceRelay {
|
|
|
109
107
|
throw new Error("Remote open_workspace response did not include a valid workspace mode.");
|
|
110
108
|
}
|
|
111
109
|
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : undefined;
|
|
112
|
-
const route = this.findOrCreateRoute({
|
|
110
|
+
const route = await this.findOrCreateRoute({
|
|
113
111
|
remoteInstanceId: resolved.remote.instanceId,
|
|
114
112
|
remoteWorkspaceId,
|
|
115
113
|
root,
|
|
@@ -167,7 +165,7 @@ export class RemoteWorkspaceRelay {
|
|
|
167
165
|
? structured.mode
|
|
168
166
|
: route.mode;
|
|
169
167
|
const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : route.sourceRoot;
|
|
170
|
-
this.findOrCreateRoute({
|
|
168
|
+
await this.findOrCreateRoute({
|
|
171
169
|
remoteInstanceId: route.remoteInstanceId,
|
|
172
170
|
remoteWorkspaceId: route.remoteWorkspaceId,
|
|
173
171
|
root,
|
|
@@ -194,6 +192,9 @@ export class RemoteWorkspaceRelay {
|
|
|
194
192
|
async bash(gatewayWorkspaceId, input, conversationScopeId) {
|
|
195
193
|
return this.callWorkspaceTool(gatewayWorkspaceId, "bash", input, conversationScopeId);
|
|
196
194
|
}
|
|
195
|
+
async workspaceInstruction(gatewayWorkspaceId, path, conversationScopeId) {
|
|
196
|
+
return this.callWorkspaceTool(gatewayWorkspaceId, "workspace_instruction", { path }, conversationScopeId);
|
|
197
|
+
}
|
|
197
198
|
async execCommand(gatewayWorkspaceId, input, conversationScopeId) {
|
|
198
199
|
return this.callWorkspaceTool(gatewayWorkspaceId, "exec_command", input, conversationScopeId);
|
|
199
200
|
}
|
|
@@ -296,7 +297,7 @@ export class RemoteWorkspaceRelay {
|
|
|
296
297
|
const remoteStructured = result.structuredContent;
|
|
297
298
|
if (action === "delete") {
|
|
298
299
|
this.routes.delete(gatewayWorkspaceId);
|
|
299
|
-
this.deletePersistedRoute(gatewayWorkspaceId);
|
|
300
|
+
await this.deletePersistedRoute(gatewayWorkspaceId);
|
|
300
301
|
}
|
|
301
302
|
for (const [turnId, routedWorkspaceId] of this.turnRoutes) {
|
|
302
303
|
if (routedWorkspaceId === gatewayWorkspaceId)
|
|
@@ -356,35 +357,33 @@ export class RemoteWorkspaceRelay {
|
|
|
356
357
|
this.routes.set(workspaceId, route);
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
|
-
findOrCreateRoute(input) {
|
|
360
|
-
let selected;
|
|
360
|
+
async findOrCreateRoute(input) {
|
|
361
361
|
mkdirSync(this.routeStateDir, { recursive: true });
|
|
362
|
-
this.
|
|
362
|
+
const selected = await withFileLock(`${this.routeStatePath}.lock`, () => {
|
|
363
363
|
const routes = this.readRoutesFromDisk();
|
|
364
364
|
const existing = [...routes.values()]
|
|
365
365
|
.filter((route) => route.remoteInstanceId === input.remoteInstanceId &&
|
|
366
366
|
route.remoteWorkspaceId === input.remoteWorkspaceId)
|
|
367
367
|
.sort((left, right) => left.gatewayWorkspaceId.localeCompare(right.gatewayWorkspaceId))[0];
|
|
368
|
-
|
|
368
|
+
const route = {
|
|
369
369
|
gatewayWorkspaceId: existing?.gatewayWorkspaceId ?? this.allocateGatewayWorkspaceId(routes),
|
|
370
370
|
...input,
|
|
371
371
|
};
|
|
372
|
-
routes.set(
|
|
372
|
+
routes.set(route.gatewayWorkspaceId, route);
|
|
373
373
|
this.writeRoutesToDisk(routes);
|
|
374
|
+
return route;
|
|
374
375
|
});
|
|
375
|
-
if (!selected)
|
|
376
|
-
throw new Error("Failed to persist relayed Workspace route.");
|
|
377
376
|
this.routes.set(selected.gatewayWorkspaceId, selected);
|
|
378
377
|
return selected;
|
|
379
378
|
}
|
|
380
|
-
deletePersistedRoute(workspaceId) {
|
|
381
|
-
this.updatePersistedRoutes((routes) => {
|
|
379
|
+
async deletePersistedRoute(workspaceId) {
|
|
380
|
+
await this.updatePersistedRoutes((routes) => {
|
|
382
381
|
routes.delete(workspaceId);
|
|
383
382
|
});
|
|
384
383
|
}
|
|
385
|
-
updatePersistedRoutes(update) {
|
|
384
|
+
async updatePersistedRoutes(update) {
|
|
386
385
|
mkdirSync(this.routeStateDir, { recursive: true });
|
|
387
|
-
this.
|
|
386
|
+
await withFileLock(`${this.routeStatePath}.lock`, () => {
|
|
388
387
|
const routes = this.readRoutesFromDisk();
|
|
389
388
|
update(routes);
|
|
390
389
|
this.writeRoutesToDisk(routes);
|
|
@@ -418,43 +417,6 @@ export class RemoteWorkspaceRelay {
|
|
|
418
417
|
rmSync(tempPath, { force: true });
|
|
419
418
|
}
|
|
420
419
|
}
|
|
421
|
-
withRouteFileLock(operation) {
|
|
422
|
-
const lockPath = `${this.routeStatePath}.lock`;
|
|
423
|
-
const deadline = Date.now() + ROUTE_LOCK_TIMEOUT_MS;
|
|
424
|
-
for (;;) {
|
|
425
|
-
try {
|
|
426
|
-
const fd = openSync(lockPath, "wx", 0o600);
|
|
427
|
-
closeSync(fd);
|
|
428
|
-
break;
|
|
429
|
-
}
|
|
430
|
-
catch (error) {
|
|
431
|
-
const code = error.code;
|
|
432
|
-
if (code !== "EEXIST")
|
|
433
|
-
throw error;
|
|
434
|
-
try {
|
|
435
|
-
if (Date.now() - statSync(lockPath).mtimeMs > ROUTE_LOCK_STALE_MS) {
|
|
436
|
-
rmSync(lockPath, { force: true });
|
|
437
|
-
continue;
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
catch (statError) {
|
|
441
|
-
if (statError.code === "ENOENT")
|
|
442
|
-
continue;
|
|
443
|
-
throw statError;
|
|
444
|
-
}
|
|
445
|
-
if (Date.now() >= deadline) {
|
|
446
|
-
throw new Error(`Timed out waiting for relayed workspace route lock: ${lockPath}`);
|
|
447
|
-
}
|
|
448
|
-
Atomics.wait(ROUTE_LOCK_SLEEP, 0, 0, ROUTE_LOCK_RETRY_MS);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
try {
|
|
452
|
-
return operation();
|
|
453
|
-
}
|
|
454
|
-
finally {
|
|
455
|
-
rmSync(lockPath, { force: true });
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
420
|
remoteByAlias(aliasInput) {
|
|
459
421
|
const alias = aliasInput.trim();
|
|
460
422
|
if (!alias)
|
|
@@ -522,7 +484,7 @@ export class RemoteWorkspaceRelay {
|
|
|
522
484
|
}
|
|
523
485
|
async refreshRemote(alias, remote, endpoint) {
|
|
524
486
|
const refreshed = await refreshRemoteAuthentication(remote, endpoint);
|
|
525
|
-
writeForgeRelayRemote(alias, refreshed, this.authEnv);
|
|
487
|
+
await writeForgeRelayRemote(alias, refreshed, this.authEnv);
|
|
526
488
|
return refreshed;
|
|
527
489
|
}
|
|
528
490
|
allocateGatewayWorkspaceId(routes = this.routes) {
|
|
@@ -533,114 +495,3 @@ export class RemoteWorkspaceRelay {
|
|
|
533
495
|
return workspaceId;
|
|
534
496
|
}
|
|
535
497
|
}
|
|
536
|
-
function assertRemoteToolSucceeded(alias, tool, result) {
|
|
537
|
-
if (result.isError !== true)
|
|
538
|
-
return;
|
|
539
|
-
throw new Error(`Remote ForgeRelay ${alias} ${tool} failed: ${toolResultText(result)}`);
|
|
540
|
-
}
|
|
541
|
-
function stringField(structured, field, label) {
|
|
542
|
-
const value = structured?.[field];
|
|
543
|
-
if (typeof value !== "string" || !value) {
|
|
544
|
-
throw new Error(`${label} did not include ${field}.`);
|
|
545
|
-
}
|
|
546
|
-
return value;
|
|
547
|
-
}
|
|
548
|
-
function copyStringField(source, target, field) {
|
|
549
|
-
const value = source[field];
|
|
550
|
-
if (typeof value === "string")
|
|
551
|
-
target[field] = value;
|
|
552
|
-
}
|
|
553
|
-
function copyBooleanField(source, target, field) {
|
|
554
|
-
const value = source[field];
|
|
555
|
-
if (typeof value === "boolean")
|
|
556
|
-
target[field] = value;
|
|
557
|
-
}
|
|
558
|
-
function copyNumberField(source, target, field) {
|
|
559
|
-
const value = source[field];
|
|
560
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 0)
|
|
561
|
-
target[field] = value;
|
|
562
|
-
}
|
|
563
|
-
function safeTaskSummary(value) {
|
|
564
|
-
if (!value || typeof value !== "object")
|
|
565
|
-
return undefined;
|
|
566
|
-
const summary = value;
|
|
567
|
-
if (summary.level !== "summary" ||
|
|
568
|
-
summary.version !== 1 ||
|
|
569
|
-
typeof summary.revision !== "number" ||
|
|
570
|
-
!Number.isInteger(summary.revision) ||
|
|
571
|
-
summary.revision < 0 ||
|
|
572
|
-
!Array.isArray(summary.lists)) {
|
|
573
|
-
return undefined;
|
|
574
|
-
}
|
|
575
|
-
const lists = [];
|
|
576
|
-
for (const value of summary.lists) {
|
|
577
|
-
if (!value || typeof value !== "object")
|
|
578
|
-
return undefined;
|
|
579
|
-
const list = value;
|
|
580
|
-
if (typeof list.id !== "string" ||
|
|
581
|
-
typeof list.name !== "string" ||
|
|
582
|
-
(list.state !== "active" && list.state !== "archived") ||
|
|
583
|
-
typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
|
|
584
|
-
typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
|
|
585
|
-
typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
|
|
586
|
-
return undefined;
|
|
587
|
-
}
|
|
588
|
-
lists.push({
|
|
589
|
-
id: list.id,
|
|
590
|
-
name: list.name,
|
|
591
|
-
state: list.state,
|
|
592
|
-
revision: list.revision,
|
|
593
|
-
taskCount: list.taskCount,
|
|
594
|
-
unfinishedTaskCount: list.unfinishedTaskCount,
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
return {
|
|
598
|
-
level: "summary",
|
|
599
|
-
version: 1,
|
|
600
|
-
revision: summary.revision,
|
|
601
|
-
lists,
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
function toolResultText(result) {
|
|
605
|
-
return (result.content ?? [])
|
|
606
|
-
.filter((entry) => entry.type === "text")
|
|
607
|
-
.map((entry) => entry.text)
|
|
608
|
-
.join("\n") || "remote tool returned an error";
|
|
609
|
-
}
|
|
610
|
-
function remapToolResultWorkspaceId(result, remoteWorkspaceId, gatewayWorkspaceId) {
|
|
611
|
-
return {
|
|
612
|
-
...result,
|
|
613
|
-
content: (result.content ?? []).map((entry) => entry.type === "text"
|
|
614
|
-
? { ...entry, text: entry.text.split(remoteWorkspaceId).join(gatewayWorkspaceId) }
|
|
615
|
-
: entry),
|
|
616
|
-
...(result._meta
|
|
617
|
-
? { _meta: replaceExactWorkspaceId(result._meta, remoteWorkspaceId, gatewayWorkspaceId) }
|
|
618
|
-
: {}),
|
|
619
|
-
...(result.structuredContent
|
|
620
|
-
? {
|
|
621
|
-
structuredContent: replaceExactWorkspaceId(result.structuredContent, remoteWorkspaceId, gatewayWorkspaceId),
|
|
622
|
-
}
|
|
623
|
-
: {}),
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
function replaceExactWorkspaceId(value, from, to) {
|
|
627
|
-
if (typeof value === "string")
|
|
628
|
-
return value.split(from).join(to);
|
|
629
|
-
if (Array.isArray(value))
|
|
630
|
-
return value.map((entry) => replaceExactWorkspaceId(entry, from, to));
|
|
631
|
-
if (!value || typeof value !== "object")
|
|
632
|
-
return value;
|
|
633
|
-
return Object.fromEntries(Object.entries(value)
|
|
634
|
-
.map(([key, entry]) => [key, replaceExactWorkspaceId(entry, from, to)]));
|
|
635
|
-
}
|
|
636
|
-
function sanitizedRemoteError(error, remoteWorkspaceId, gatewayWorkspaceId) {
|
|
637
|
-
let message = errorMessage(error);
|
|
638
|
-
if (remoteWorkspaceId && gatewayWorkspaceId) {
|
|
639
|
-
message = message.split(remoteWorkspaceId).join(gatewayWorkspaceId);
|
|
640
|
-
}
|
|
641
|
-
message = message.replace(/(^|[^A-Za-z0-9_])ws_[0-9a-f]{10}(?=$|[^A-Za-z0-9_])/g, "$1[remote-workspace]");
|
|
642
|
-
return new Error(message, { cause: error });
|
|
643
|
-
}
|
|
644
|
-
function errorMessage(error) {
|
|
645
|
-
return error instanceof Error ? error.message : String(error);
|
|
646
|
-
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, unwatchFile, watchFile } from "node:fs";
|
|
2
|
+
import { relative, resolve, sep } from "node:path";
|
|
3
|
+
import { createTwoFilesPatch } from "diff";
|
|
4
|
+
import { skillSummaryFromContent } from "./skills.js";
|
|
5
|
+
const WATCH_INTERVAL_MS = 750;
|
|
6
|
+
const MAX_HISTORY = 128;
|
|
7
|
+
const MAX_DELIVERY_CHARACTERS = 32 * 1024;
|
|
8
|
+
const MAX_PATCH_CHARACTERS = 8 * 1024;
|
|
9
|
+
const sharedWatches = new Map();
|
|
10
|
+
let monitorSequence = 0;
|
|
11
|
+
/**
|
|
12
|
+
* Tracks already-advertised Workspace instruction and Skill files. The OS-level
|
|
13
|
+
* stat watcher is process-global and canonical-path keyed, so two Workspaces or
|
|
14
|
+
* conversations that refer to the same physical file still create only one
|
|
15
|
+
* underlying watcher. Resource changes are delivered as bounded deltas.
|
|
16
|
+
*/
|
|
17
|
+
export class WorkspaceResourceMonitor {
|
|
18
|
+
id = `monitor-${++monitorSequence}`;
|
|
19
|
+
states = new Map();
|
|
20
|
+
trackWorkspace(input) {
|
|
21
|
+
const state = this.states.get(input.workspaceId) ?? {
|
|
22
|
+
root: input.root,
|
|
23
|
+
revision: 0,
|
|
24
|
+
tracked: new Map(),
|
|
25
|
+
subscriptions: new Map(),
|
|
26
|
+
changes: [],
|
|
27
|
+
deliveredRevisionByScope: new Map(),
|
|
28
|
+
};
|
|
29
|
+
state.root = input.root;
|
|
30
|
+
this.states.set(input.workspaceId, state);
|
|
31
|
+
const desired = new Map();
|
|
32
|
+
for (const path of input.availableInstructions) {
|
|
33
|
+
this.addDesired(desired, path, {
|
|
34
|
+
kind: "instruction-available",
|
|
35
|
+
displayPath: displayWorkspacePath(path, input.root),
|
|
36
|
+
exposeSkillBody: false,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
for (const path of input.loadedInstructions) {
|
|
40
|
+
this.addDesired(desired, path, {
|
|
41
|
+
kind: "instruction-loaded",
|
|
42
|
+
displayPath: displayWorkspacePath(path, input.root),
|
|
43
|
+
exposeSkillBody: false,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
for (const skill of input.skills) {
|
|
47
|
+
this.addDesired(desired, skill.filePath, {
|
|
48
|
+
kind: "skill",
|
|
49
|
+
displayPath: `skills://${encodeURIComponent(skill.name)}`,
|
|
50
|
+
skillName: skill.name,
|
|
51
|
+
exposeSkillBody: skill.activated,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
for (const [key, stop] of state.tracked) {
|
|
55
|
+
if (desired.has(key))
|
|
56
|
+
continue;
|
|
57
|
+
stop();
|
|
58
|
+
state.tracked.delete(key);
|
|
59
|
+
state.subscriptions.delete(key);
|
|
60
|
+
}
|
|
61
|
+
for (const [key, item] of desired) {
|
|
62
|
+
state.subscriptions.set(key, item.subscription);
|
|
63
|
+
if (state.tracked.has(key)) {
|
|
64
|
+
synchronizeSharedWatch(key);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const subscriberId = `${this.id}:${input.workspaceId}:${key}`;
|
|
68
|
+
const stop = subscribeSharedWatch(item.path, subscriberId, (oldContent, newContent) => {
|
|
69
|
+
this.recordChange(input.workspaceId, key, oldContent, newContent);
|
|
70
|
+
});
|
|
71
|
+
state.tracked.set(key, stop);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
trackLoadedInstruction(workspaceId, root, path) {
|
|
75
|
+
const state = this.states.get(workspaceId);
|
|
76
|
+
if (!state)
|
|
77
|
+
return;
|
|
78
|
+
const key = canonicalWatchKey(path);
|
|
79
|
+
state.subscriptions.set(key, {
|
|
80
|
+
kind: "instruction-loaded",
|
|
81
|
+
displayPath: displayWorkspacePath(path, root),
|
|
82
|
+
exposeSkillBody: false,
|
|
83
|
+
});
|
|
84
|
+
if (state.tracked.has(key))
|
|
85
|
+
return;
|
|
86
|
+
const subscriberId = `${this.id}:${workspaceId}:${key}`;
|
|
87
|
+
state.tracked.set(key, subscribeSharedWatch(path, subscriberId, (oldContent, newContent) => {
|
|
88
|
+
this.recordChange(workspaceId, key, oldContent, newContent);
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
markSkillActivated(workspaceId, skillPath) {
|
|
92
|
+
const state = this.states.get(workspaceId);
|
|
93
|
+
if (!state)
|
|
94
|
+
return;
|
|
95
|
+
const subscription = state.subscriptions.get(canonicalWatchKey(skillPath));
|
|
96
|
+
if (subscription?.kind === "skill")
|
|
97
|
+
subscription.exposeSkillBody = true;
|
|
98
|
+
}
|
|
99
|
+
acknowledge(workspaceId, conversationScopeId) {
|
|
100
|
+
if (!conversationScopeId)
|
|
101
|
+
return;
|
|
102
|
+
const state = this.states.get(workspaceId);
|
|
103
|
+
if (!state)
|
|
104
|
+
return;
|
|
105
|
+
state.deliveredRevisionByScope.set(conversationScopeId, state.revision);
|
|
106
|
+
this.pruneDeliveredHistory(state);
|
|
107
|
+
}
|
|
108
|
+
claim(workspaceId, conversationScopeId) {
|
|
109
|
+
if (!conversationScopeId)
|
|
110
|
+
return undefined;
|
|
111
|
+
const state = this.states.get(workspaceId);
|
|
112
|
+
if (!state)
|
|
113
|
+
return undefined;
|
|
114
|
+
// File watcher delivery is advisory and can lag behind a tool call. Refresh
|
|
115
|
+
// tracked files synchronously before deciding whether this conversation has
|
|
116
|
+
// new context so rapid consecutive writes cannot be skipped.
|
|
117
|
+
for (const key of state.tracked.keys())
|
|
118
|
+
synchronizeSharedWatch(key);
|
|
119
|
+
const deliveredRevision = state.deliveredRevisionByScope.get(conversationScopeId) ?? state.revision;
|
|
120
|
+
const changes = state.changes.filter((change) => change.revision > deliveredRevision);
|
|
121
|
+
state.deliveredRevisionByScope.set(conversationScopeId, state.revision);
|
|
122
|
+
if (changes.length === 0)
|
|
123
|
+
return undefined;
|
|
124
|
+
const grouped = coalesceChanges(changes);
|
|
125
|
+
const sections = [];
|
|
126
|
+
const coveredComponents = new Set();
|
|
127
|
+
for (const change of grouped) {
|
|
128
|
+
const formatted = formatResourceChange(change);
|
|
129
|
+
if (!formatted)
|
|
130
|
+
continue;
|
|
131
|
+
sections.push(formatted.text);
|
|
132
|
+
for (const component of formatted.coveredComponents)
|
|
133
|
+
coveredComponents.add(component);
|
|
134
|
+
}
|
|
135
|
+
this.pruneDeliveredHistory(state);
|
|
136
|
+
if (sections.length === 0)
|
|
137
|
+
return undefined;
|
|
138
|
+
const header = "Workspace context changed after this Workspace was opened. Apply only these deltas; unchanged instructions and Skill metadata remain active:";
|
|
139
|
+
const joined = [header, ...sections].join("\n\n");
|
|
140
|
+
const text = joined.length <= MAX_DELIVERY_CHARACTERS
|
|
141
|
+
? joined
|
|
142
|
+
: `${joined.slice(0, MAX_DELIVERY_CHARACTERS)}\n\n[Additional Workspace context deltas were truncated; reopen with context=\"auto\" to refresh metadata.]`;
|
|
143
|
+
return {
|
|
144
|
+
text,
|
|
145
|
+
coveredComponents: [...coveredComponents],
|
|
146
|
+
revision: state.revision,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
isCurrentForScope(workspaceId, conversationScopeId) {
|
|
150
|
+
if (!conversationScopeId)
|
|
151
|
+
return false;
|
|
152
|
+
const state = this.states.get(workspaceId);
|
|
153
|
+
if (!state)
|
|
154
|
+
return false;
|
|
155
|
+
return state.deliveredRevisionByScope.get(conversationScopeId) === state.revision;
|
|
156
|
+
}
|
|
157
|
+
forgetWorkspace(workspaceId) {
|
|
158
|
+
const state = this.states.get(workspaceId);
|
|
159
|
+
if (!state)
|
|
160
|
+
return;
|
|
161
|
+
for (const stop of state.tracked.values())
|
|
162
|
+
stop();
|
|
163
|
+
this.states.delete(workspaceId);
|
|
164
|
+
}
|
|
165
|
+
pruneWorkspaces(activeWorkspaceIds) {
|
|
166
|
+
const active = new Set(activeWorkspaceIds);
|
|
167
|
+
for (const workspaceId of this.states.keys()) {
|
|
168
|
+
if (!active.has(workspaceId))
|
|
169
|
+
this.forgetWorkspace(workspaceId);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
get watchedPhysicalFiles() {
|
|
173
|
+
return sharedWatches.size;
|
|
174
|
+
}
|
|
175
|
+
addDesired(desired, path, subscription) {
|
|
176
|
+
const key = canonicalWatchKey(path);
|
|
177
|
+
const current = desired.get(key);
|
|
178
|
+
if (current?.subscription.kind === "instruction-loaded")
|
|
179
|
+
return;
|
|
180
|
+
desired.set(key, { path, subscription });
|
|
181
|
+
}
|
|
182
|
+
recordChange(workspaceId, key, oldContent, newContent) {
|
|
183
|
+
if (oldContent === newContent)
|
|
184
|
+
return;
|
|
185
|
+
const state = this.states.get(workspaceId);
|
|
186
|
+
const subscription = state?.subscriptions.get(key);
|
|
187
|
+
if (!state || !subscription)
|
|
188
|
+
return;
|
|
189
|
+
state.revision += 1;
|
|
190
|
+
state.changes.push({
|
|
191
|
+
...subscription,
|
|
192
|
+
revision: state.revision,
|
|
193
|
+
watchKey: key,
|
|
194
|
+
oldContent,
|
|
195
|
+
newContent,
|
|
196
|
+
});
|
|
197
|
+
if (state.changes.length > MAX_HISTORY) {
|
|
198
|
+
state.changes.splice(0, state.changes.length - MAX_HISTORY);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
pruneDeliveredHistory(state) {
|
|
202
|
+
if (state.changes.length === 0 || state.deliveredRevisionByScope.size === 0)
|
|
203
|
+
return;
|
|
204
|
+
const floor = Math.min(...state.deliveredRevisionByScope.values());
|
|
205
|
+
while (state.changes[0] && state.changes[0].revision <= floor)
|
|
206
|
+
state.changes.shift();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function subscribeSharedWatch(inputPath, subscriberId, subscriber) {
|
|
210
|
+
const key = canonicalWatchKey(inputPath);
|
|
211
|
+
let entry = sharedWatches.get(key);
|
|
212
|
+
if (!entry) {
|
|
213
|
+
const watchPath = resolve(inputPath);
|
|
214
|
+
const listener = () => synchronizeSharedWatch(key);
|
|
215
|
+
entry = {
|
|
216
|
+
key,
|
|
217
|
+
watchPath,
|
|
218
|
+
content: readOptionalFile(watchPath),
|
|
219
|
+
subscribers: new Map(),
|
|
220
|
+
listener,
|
|
221
|
+
};
|
|
222
|
+
sharedWatches.set(key, entry);
|
|
223
|
+
watchFile(watchPath, { interval: WATCH_INTERVAL_MS, persistent: false }, listener);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
synchronizeSharedWatch(key);
|
|
227
|
+
}
|
|
228
|
+
entry.subscribers.set(subscriberId, subscriber);
|
|
229
|
+
return () => {
|
|
230
|
+
const current = sharedWatches.get(key);
|
|
231
|
+
if (!current)
|
|
232
|
+
return;
|
|
233
|
+
current.subscribers.delete(subscriberId);
|
|
234
|
+
if (current.subscribers.size > 0)
|
|
235
|
+
return;
|
|
236
|
+
unwatchFile(current.watchPath, current.listener);
|
|
237
|
+
sharedWatches.delete(key);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function synchronizeSharedWatch(key) {
|
|
241
|
+
const entry = sharedWatches.get(key);
|
|
242
|
+
if (!entry)
|
|
243
|
+
return;
|
|
244
|
+
const next = readOptionalFile(entry.watchPath);
|
|
245
|
+
if (next === entry.content)
|
|
246
|
+
return;
|
|
247
|
+
const previous = entry.content;
|
|
248
|
+
entry.content = next;
|
|
249
|
+
for (const subscriber of entry.subscribers.values())
|
|
250
|
+
subscriber(previous, next);
|
|
251
|
+
}
|
|
252
|
+
function canonicalWatchKey(path) {
|
|
253
|
+
const absolute = resolve(path);
|
|
254
|
+
try {
|
|
255
|
+
return realpathSync(absolute);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return absolute;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function readOptionalFile(path) {
|
|
262
|
+
try {
|
|
263
|
+
return readFileSync(path, "utf8");
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function displayWorkspacePath(path, workspaceRoot) {
|
|
273
|
+
const relationship = relative(workspaceRoot, path);
|
|
274
|
+
if (!relationship || relationship === ".." || relationship.startsWith(`..${sep}`))
|
|
275
|
+
return resolve(path);
|
|
276
|
+
return relationship.split(sep).join("/");
|
|
277
|
+
}
|
|
278
|
+
function coalesceChanges(changes) {
|
|
279
|
+
const grouped = new Map();
|
|
280
|
+
for (const change of changes) {
|
|
281
|
+
const existing = grouped.get(change.watchKey);
|
|
282
|
+
if (!existing) {
|
|
283
|
+
grouped.set(change.watchKey, { ...change });
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
grouped.set(change.watchKey, {
|
|
287
|
+
...change,
|
|
288
|
+
oldContent: existing.oldContent,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return [...grouped.values()].sort((left, right) => left.revision - right.revision);
|
|
292
|
+
}
|
|
293
|
+
function formatResourceChange(change) {
|
|
294
|
+
if (change.oldContent === change.newContent)
|
|
295
|
+
return undefined;
|
|
296
|
+
if (change.kind === "instruction-available") {
|
|
297
|
+
return {
|
|
298
|
+
text: `Nested Workspace instruction changed: ${change.displayPath}. Its body remains lazy; read it before working under that directory.`,
|
|
299
|
+
coveredComponents: [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (change.kind === "instruction-loaded") {
|
|
303
|
+
return {
|
|
304
|
+
text: [
|
|
305
|
+
`Workspace instruction delta: ${change.displayPath}`,
|
|
306
|
+
boundedPatch(change.displayPath, change.oldContent, change.newContent),
|
|
307
|
+
].join("\n"),
|
|
308
|
+
coveredComponents: ["agentsFiles"],
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (change.exposeSkillBody) {
|
|
312
|
+
return {
|
|
313
|
+
text: [
|
|
314
|
+
`Active Skill delta: ${change.displayPath}`,
|
|
315
|
+
boundedPatch(change.displayPath, change.oldContent, change.newContent),
|
|
316
|
+
].join("\n"),
|
|
317
|
+
coveredComponents: skillMetadataChanged(change) ? ["skills"] : [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const metadata = formatSkillMetadataDelta(change);
|
|
321
|
+
return {
|
|
322
|
+
text: metadata ?? `Skill content changed: ${change.displayPath}. The Skill body was not injected because it was not active when it changed; reload it before next use.`,
|
|
323
|
+
coveredComponents: metadata ? ["skills"] : [],
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function skillMetadataChanged(change) {
|
|
327
|
+
try {
|
|
328
|
+
const oldSummary = change.oldContent === undefined
|
|
329
|
+
? undefined
|
|
330
|
+
: skillSummaryFromContent(change.oldContent, change.watchKey);
|
|
331
|
+
const newSummary = change.newContent === undefined
|
|
332
|
+
? undefined
|
|
333
|
+
: skillSummaryFromContent(change.newContent, change.watchKey);
|
|
334
|
+
return JSON.stringify(oldSummary) !== JSON.stringify(newSummary);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function formatSkillMetadataDelta(change) {
|
|
341
|
+
try {
|
|
342
|
+
const oldSummary = change.oldContent === undefined
|
|
343
|
+
? undefined
|
|
344
|
+
: skillSummaryFromContent(change.oldContent, change.watchKey);
|
|
345
|
+
const newSummary = change.newContent === undefined
|
|
346
|
+
? undefined
|
|
347
|
+
: skillSummaryFromContent(change.newContent, change.watchKey);
|
|
348
|
+
if (JSON.stringify(oldSummary) === JSON.stringify(newSummary))
|
|
349
|
+
return undefined;
|
|
350
|
+
if (!oldSummary) {
|
|
351
|
+
return `Skill metadata added: ${change.displayPath}\n+ name: ${newSummary?.name ?? change.skillName ?? "unknown"}\n+ description: ${newSummary?.description ?? ""}\n+ disable-model-invocation: ${newSummary?.disableModelInvocation === true}`;
|
|
352
|
+
}
|
|
353
|
+
if (!newSummary)
|
|
354
|
+
return `Skill removed: ${change.displayPath}`;
|
|
355
|
+
const lines = [`Skill metadata delta: ${change.displayPath}`];
|
|
356
|
+
for (const field of ["name", "description", "disableModelInvocation"]) {
|
|
357
|
+
if (oldSummary[field] === newSummary[field])
|
|
358
|
+
continue;
|
|
359
|
+
const label = field === "disableModelInvocation" ? "disable-model-invocation" : field;
|
|
360
|
+
lines.push(`- ${label}: ${String(oldSummary[field])}`);
|
|
361
|
+
lines.push(`+ ${label}: ${String(newSummary[field])}`);
|
|
362
|
+
}
|
|
363
|
+
return lines.join("\n");
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return `Skill metadata became unreadable or changed format: ${change.displayPath}. Reload it before next use.`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function boundedPatch(path, oldContent, newContent) {
|
|
370
|
+
const patch = createTwoFilesPatch(path, path, oldContent ?? "", newContent ?? "", oldContent === undefined ? "missing" : "before", newContent === undefined ? "missing" : "after", { context: 1 });
|
|
371
|
+
const lines = patch.split("\n");
|
|
372
|
+
const hunk = lines.slice(Math.max(0, lines.findIndex((line) => line.startsWith("@@"))));
|
|
373
|
+
const compact = hunk.join("\n").trimEnd();
|
|
374
|
+
if (compact.length <= MAX_PATCH_CHARACTERS)
|
|
375
|
+
return compact;
|
|
376
|
+
return `${compact.slice(0, MAX_PATCH_CHARACTERS)}\n[delta truncated]`;
|
|
377
|
+
}
|