@akira-tl/forgerelay 0.3.5 → 0.3.7
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 +29 -0
- package/capabilities/shell-processes/GUIDE.md +2 -2
- package/dist/db/migrations.js +19 -0
- package/dist/db/schema.js +9 -0
- package/dist/logger.js +16 -0
- package/dist/mcp-sessions.js +30 -0
- package/dist/oauth-provider.js +9 -0
- package/dist/process-sessions.js +98 -16
- package/dist/review-checkpoints.js +36 -1
- package/dist/server.js +234 -26
- package/dist/workspace-store.js +148 -20
- package/dist/workspaces.js +339 -111
- package/docs/chatgpt-coding-workflow.md +75 -12
- package/docs/configuration.md +47 -9
- package/docs/roadmap.md +23 -1
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +60 -3
package/dist/server.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { access, realpath } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
+
import { resolve } from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
8
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
@@ -42,6 +43,7 @@ import { formatLocalAgentProviderAvailabilitySummary, getLocalAgentProviderAvail
|
|
|
42
43
|
// transport. Bound stale transport-session retention so abandoned transports do
|
|
43
44
|
// not accumulate for the life of the process.
|
|
44
45
|
const MCP_TRANSPORT_IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
|
|
46
|
+
const MAX_MCP_TRANSPORT_SESSIONS = 64;
|
|
45
47
|
const FORGERELAY_VERSION = readForgeRelayVersion();
|
|
46
48
|
const MCP_TRANSPORT_CLEANUP_INTERVAL_MS = 5 * 60 * 1_000;
|
|
47
49
|
const WRITE_TOOL_ANNOTATIONS = {
|
|
@@ -92,6 +94,30 @@ function workspaceLogContext(workspace, _transportSessionId) {
|
|
|
92
94
|
workspace: workspaceLogLabel(workspace.root, workspace.id),
|
|
93
95
|
};
|
|
94
96
|
}
|
|
97
|
+
function formatDiscoveredWorkspaceInstructions(files, workspaceRoot) {
|
|
98
|
+
return [
|
|
99
|
+
"Workspace instructions discovered for this path. Apply them to follow-up work under their directories:",
|
|
100
|
+
...files.flatMap((file) => [
|
|
101
|
+
`--- ${formatAgentsPath(file.path, workspaceRoot)} ---`,
|
|
102
|
+
file.content.trimEnd(),
|
|
103
|
+
]),
|
|
104
|
+
].join("\n");
|
|
105
|
+
}
|
|
106
|
+
async function assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, paths) {
|
|
107
|
+
const discovered = new Map();
|
|
108
|
+
for (const path of paths) {
|
|
109
|
+
const absolutePath = resolve(workspace.root, path);
|
|
110
|
+
for (const file of await workspaces.discoverPathInstructions(workspace, absolutePath)) {
|
|
111
|
+
discovered.set(file.path, file);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (discovered.size === 0)
|
|
115
|
+
return;
|
|
116
|
+
throw new Error([
|
|
117
|
+
formatDiscoveredWorkspaceInstructions([...discovered.values()], workspace.root),
|
|
118
|
+
"Apply these instructions, then retry this tool call. No mutation or command was executed.",
|
|
119
|
+
].join("\n"));
|
|
120
|
+
}
|
|
95
121
|
function formatVisibleAgent(agent) {
|
|
96
122
|
const model = agent.model ? `, model ${agent.model}` : "";
|
|
97
123
|
const thinking = agent.thinking ? `, thinking ${agent.thinking}` : "";
|
|
@@ -164,6 +190,36 @@ const workspaceLocalAgentProviderOutputSchema = z.object({
|
|
|
164
190
|
const workspaceAvailableAgentsFileOutputSchema = z.object({
|
|
165
191
|
path: z.string(),
|
|
166
192
|
});
|
|
193
|
+
const workspaceInventoryEntryOutputSchema = z.object({
|
|
194
|
+
label: z.string(),
|
|
195
|
+
workspaceId: z.string(),
|
|
196
|
+
root: z.string(),
|
|
197
|
+
status: z.string(),
|
|
198
|
+
state: z.enum(["active", "stale", "invalid", "closed"]),
|
|
199
|
+
mode: z.enum(["checkout", "worktree"]),
|
|
200
|
+
sourceRoot: z.string().optional(),
|
|
201
|
+
branch: z.string().optional(),
|
|
202
|
+
targetBranch: z.string().optional(),
|
|
203
|
+
managed: z.boolean(),
|
|
204
|
+
createdAt: z.string(),
|
|
205
|
+
lastUsedAt: z.string(),
|
|
206
|
+
idleMs: z.number().nonnegative(),
|
|
207
|
+
rootValid: z.boolean(),
|
|
208
|
+
current: z.boolean(),
|
|
209
|
+
});
|
|
210
|
+
const workspaceInventorySummaryOutputSchema = z.object({
|
|
211
|
+
total: z.number().int().nonnegative(),
|
|
212
|
+
matching: z.number().int().nonnegative(),
|
|
213
|
+
active: z.number().int().nonnegative(),
|
|
214
|
+
stale: z.number().int().nonnegative(),
|
|
215
|
+
invalid: z.number().int().nonnegative(),
|
|
216
|
+
closed: z.number().int().nonnegative(),
|
|
217
|
+
});
|
|
218
|
+
const workspaceInventoryPageOutputSchema = z.object({
|
|
219
|
+
offset: z.number().int().nonnegative(),
|
|
220
|
+
limit: z.number().int().positive(),
|
|
221
|
+
hasMore: z.boolean(),
|
|
222
|
+
});
|
|
167
223
|
const reviewFileOutputSchema = z.object({
|
|
168
224
|
path: z.string(),
|
|
169
225
|
previousPath: z.string().optional(),
|
|
@@ -578,6 +634,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
|
|
|
578
634
|
operation: async () => {
|
|
579
635
|
const startedAt = performance.now();
|
|
580
636
|
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
|
|
637
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
|
|
581
638
|
const snapshot = await processSessions.start({
|
|
582
639
|
workspaceId,
|
|
583
640
|
command: cmd,
|
|
@@ -775,20 +832,24 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
775
832
|
}, async (uri, _variables, extra) => readWorkspaceAppResource(config, uri.toString(), extra.sessionId));
|
|
776
833
|
registerAppTool(server, "open_workspace", {
|
|
777
834
|
title: "Open workspace",
|
|
778
|
-
description: "Open or resume a local coding workspace. Reuse the returned workspaceId for later calls. Default to checkout; use mode=\"worktree\" only when the user explicitly requests isolated or parallel Git work. Every call returns
|
|
835
|
+
description: "Open or resume a local coding workspace. Reuse the returned workspaceId for later calls. Default to checkout; use mode=\"worktree\" only when the user explicitly requests isolated or parallel Git work. Every call returns lightweight workspace metadata; bootstrap context is delivered automatically only when needed and can be explicitly suppressed or refreshed.",
|
|
779
836
|
inputSchema: {
|
|
837
|
+
action: z
|
|
838
|
+
.enum(["open", "list"])
|
|
839
|
+
.optional()
|
|
840
|
+
.describe("Defaults to open. Use list only when you need to inspect or choose logical workspaces before resuming or cleaning them up."),
|
|
780
841
|
path: z
|
|
781
842
|
.string()
|
|
782
843
|
.optional()
|
|
783
|
-
.describe("Project path to open. Required unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
|
|
844
|
+
.describe("Project path to open. Required for action=open unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
|
|
784
845
|
workspaceId: z
|
|
785
846
|
.string()
|
|
786
847
|
.optional()
|
|
787
|
-
.describe("
|
|
848
|
+
.describe("For action=open, an existing logical workspace ID to resume in this conversation. For action=list, filters inventory to one workspace ID."),
|
|
788
849
|
mode: z
|
|
789
850
|
.enum(["checkout", "worktree"])
|
|
790
851
|
.optional()
|
|
791
|
-
.describe("
|
|
852
|
+
.describe("For action=open, defaults to checkout and uses the actual directory unless worktree isolation is explicitly requested. For action=list, filters by workspace mode."),
|
|
792
853
|
baseRef: z
|
|
793
854
|
.string()
|
|
794
855
|
.optional()
|
|
@@ -801,11 +862,45 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
801
862
|
.boolean()
|
|
802
863
|
.optional()
|
|
803
864
|
.describe("When true, allocate a fresh logical workspaceId for the same physical checkout or worktree and bind this conversation to it. Use only after the user explicitly requests a new logical workspace."),
|
|
865
|
+
context: z
|
|
866
|
+
.enum(["auto", "full", "none"])
|
|
867
|
+
.optional()
|
|
868
|
+
.describe("Bootstrap context policy for action=open. auto (default) sends full project context only when this conversation has not received the current context fingerprint; full forces a refresh; none opens/resumes without returning the full bootstrap context."),
|
|
869
|
+
root: z
|
|
870
|
+
.string()
|
|
871
|
+
.optional()
|
|
872
|
+
.describe("For action=list, filter by canonical workspace root or source root."),
|
|
873
|
+
status: z
|
|
874
|
+
.string()
|
|
875
|
+
.optional()
|
|
876
|
+
.describe("For action=list, filter by persisted workspace status such as active or closed."),
|
|
877
|
+
state: z
|
|
878
|
+
.enum(["active", "stale", "invalid", "closed"])
|
|
879
|
+
.optional()
|
|
880
|
+
.describe("For action=list, filter by derived lifecycle state."),
|
|
881
|
+
staleOnly: z
|
|
882
|
+
.boolean()
|
|
883
|
+
.optional()
|
|
884
|
+
.describe("For action=list, return only active logical workspaces idle for more than two days."),
|
|
885
|
+
offset: z
|
|
886
|
+
.number()
|
|
887
|
+
.int()
|
|
888
|
+
.nonnegative()
|
|
889
|
+
.optional()
|
|
890
|
+
.describe("For action=list, zero-based inventory offset. Defaults to 0."),
|
|
891
|
+
limit: z
|
|
892
|
+
.number()
|
|
893
|
+
.int()
|
|
894
|
+
.min(1)
|
|
895
|
+
.max(100)
|
|
896
|
+
.optional()
|
|
897
|
+
.describe("For action=list, maximum records to return. Defaults to 50; maximum 100."),
|
|
804
898
|
},
|
|
805
899
|
outputSchema: {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
900
|
+
action: z.enum(["open", "list"]),
|
|
901
|
+
workspaceId: z.string().optional(),
|
|
902
|
+
root: z.string().optional(),
|
|
903
|
+
mode: z.enum(["checkout", "worktree"]).optional(),
|
|
809
904
|
sourceRoot: z.string().optional(),
|
|
810
905
|
worktree: z
|
|
811
906
|
.object({
|
|
@@ -828,7 +923,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
828
923
|
targetBranch: z.string().optional(),
|
|
829
924
|
managed: z.boolean(),
|
|
830
925
|
current: z.boolean(),
|
|
831
|
-
})),
|
|
926
|
+
})).optional(),
|
|
832
927
|
staleWorkspaces: z.array(z.object({
|
|
833
928
|
workspaceId: z.string(),
|
|
834
929
|
root: z.string(),
|
|
@@ -838,9 +933,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
838
933
|
branch: z.string().optional(),
|
|
839
934
|
targetBranch: z.string().optional(),
|
|
840
935
|
managed: z.boolean(),
|
|
841
|
-
})),
|
|
842
|
-
capabilityFingerprint: capabilityFingerprintOutputSchema,
|
|
843
|
-
|
|
936
|
+
})).optional(),
|
|
937
|
+
capabilityFingerprint: capabilityFingerprintOutputSchema.optional(),
|
|
938
|
+
contextFingerprint: z.string().optional(),
|
|
939
|
+
capabilityCatalog: z.array(capabilityCatalogOutputSchema).optional(),
|
|
844
940
|
capabilityGuides: z.array(capabilityGuideOutputSchema).optional(),
|
|
845
941
|
agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
|
|
846
942
|
availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
|
|
@@ -848,6 +944,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
848
944
|
agentProviders: z.array(workspaceLocalAgentProviderOutputSchema).optional(),
|
|
849
945
|
agents: z.array(workspaceLocalAgentOutputSchema).optional(),
|
|
850
946
|
skillDiagnostics: z.array(z.unknown()).optional(),
|
|
947
|
+
workspaces: z.array(workspaceInventoryEntryOutputSchema).optional(),
|
|
948
|
+
summary: workspaceInventorySummaryOutputSchema.optional(),
|
|
949
|
+
page: workspaceInventoryPageOutputSchema.optional(),
|
|
851
950
|
instruction: z.string(),
|
|
852
951
|
},
|
|
853
952
|
...toolWidgetDescriptorMeta(config, "workspace"),
|
|
@@ -857,11 +956,62 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
857
956
|
idempotentHint: false,
|
|
858
957
|
openWorldHint: false,
|
|
859
958
|
},
|
|
860
|
-
}, async ({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace }, { _meta, sessionId }) => {
|
|
959
|
+
}, async ({ action = "open", path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context, root, status, state, staleOnly, offset, limit, }, { _meta, sessionId }) => {
|
|
861
960
|
const startedAt = performance.now();
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
961
|
+
const conversationScopeId = openAiConversationScopeId(_meta);
|
|
962
|
+
const protectedWorkspaceIds = processSessions.activeWorkspaceIds();
|
|
963
|
+
if (action === "list") {
|
|
964
|
+
if (path !== undefined || baseRef !== undefined || newWorktree !== undefined ||
|
|
965
|
+
newWorkspace !== undefined || context !== undefined) {
|
|
966
|
+
throw new Error("open_workspace action=list does not accept path, baseRef, newWorktree, newWorkspace, or context. Use root/workspaceId/mode/status/state/staleOnly for inventory filters.");
|
|
967
|
+
}
|
|
968
|
+
const inventory = await workspaces.listWorkspaces({ workspaceId, mode, root, status, state, staleOnly, offset, limit }, { conversationScopeId, protectedWorkspaceIds });
|
|
969
|
+
const nextOffset = inventory.page.offset + inventory.page.limit;
|
|
970
|
+
const instruction = [
|
|
971
|
+
"Resume a selected workspaceId with open_workspace(action=\"open\", workspaceId=...).",
|
|
972
|
+
"Use close_workspace only after the user chooses cleanup; never close inventory entries automatically.",
|
|
973
|
+
inventory.page.hasMore
|
|
974
|
+
? `More matching workspaces are available; continue with offset=${nextOffset}.`
|
|
975
|
+
: undefined,
|
|
976
|
+
].filter(Boolean).join(" ");
|
|
977
|
+
const result = [
|
|
978
|
+
`Logical workspace inventory: ${inventory.summary.matching} matching of ${inventory.summary.total} stored records.`,
|
|
979
|
+
`States: active=${inventory.summary.active}, stale=${inventory.summary.stale}, invalid=${inventory.summary.invalid}, closed=${inventory.summary.closed}.`,
|
|
980
|
+
...inventory.workspaces.map((entry) => [
|
|
981
|
+
entry.label,
|
|
982
|
+
`state=${entry.state}`,
|
|
983
|
+
`status=${entry.status}`,
|
|
984
|
+
`mode=${entry.mode}`,
|
|
985
|
+
entry.managed ? "managed" : undefined,
|
|
986
|
+
entry.current ? "current" : undefined,
|
|
987
|
+
`root=${entry.root}`,
|
|
988
|
+
`last-used=${entry.lastUsedAt}`,
|
|
989
|
+
].filter(Boolean).join(" ")),
|
|
990
|
+
instruction,
|
|
991
|
+
].join("\n");
|
|
992
|
+
logToolCall(config, {
|
|
993
|
+
tool: "open_workspace",
|
|
994
|
+
action: "list",
|
|
995
|
+
path: root,
|
|
996
|
+
success: true,
|
|
997
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
998
|
+
});
|
|
999
|
+
return {
|
|
1000
|
+
content: [textBlock(result)],
|
|
1001
|
+
structuredContent: {
|
|
1002
|
+
action: "list",
|
|
1003
|
+
...inventory,
|
|
1004
|
+
instruction,
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
if (root !== undefined || status !== undefined || state !== undefined ||
|
|
1009
|
+
staleOnly !== undefined || offset !== undefined || limit !== undefined) {
|
|
1010
|
+
throw new Error("open_workspace inventory filters root, status, state, staleOnly, offset, and limit are only valid with action=list.");
|
|
1011
|
+
}
|
|
1012
|
+
const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, contextFingerprint, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace, context }, {
|
|
1013
|
+
conversationScopeId,
|
|
1014
|
+
protectedWorkspaceIds,
|
|
865
1015
|
});
|
|
866
1016
|
const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
|
|
867
1017
|
const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
|
|
@@ -911,20 +1061,26 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
911
1061
|
const visibleAgents = includeBootstrapContext ? cardAgents : [];
|
|
912
1062
|
const loadedAgentsFiles = includeBootstrapContext ? cardAgentsFiles : [];
|
|
913
1063
|
const availableAgentsFileOutputs = includeBootstrapContext ? cardAvailableAgentsFiles : [];
|
|
1064
|
+
const workspaceContextInstruction = "For later open_workspace calls, context=\"auto\" avoids repeating unchanged bootstrap context; use context=\"none\" when only the workspace handle/metadata is needed, or context=\"full\" to force a refresh.";
|
|
1065
|
+
const workspaceManagementInstruction = "When you need to continue an earlier logical workspace or organize workspace state, use open_workspace(action=\"list\") to inspect candidates, then resume a selected workspaceId or ask the user before close_workspace cleanup.";
|
|
914
1066
|
const cardInstruction = config.skillsEnabled
|
|
915
|
-
?
|
|
916
|
-
:
|
|
1067
|
+
? `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches an available skill or capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`
|
|
1068
|
+
: `Use this workspaceId in all subsequent tool calls for this project. Follow loaded agentsFiles instructions. Read an availableAgentsFiles path before working under it. When a task matches a capability guide, read its advertised path before proceeding. ${workspaceContextInstruction} ${workspaceManagementInstruction}`;
|
|
917
1069
|
const instruction = workspaceReused
|
|
918
1070
|
? includeBootstrapContext
|
|
919
1071
|
? [
|
|
920
1072
|
`Workspace already exists as ${workspace.id} for this directory.`,
|
|
921
1073
|
"Reuse this workspaceId for subsequent tool calls.",
|
|
922
1074
|
"The complete project context is included because it has not yet been provided in this conversation or host context.",
|
|
1075
|
+
workspaceContextInstruction,
|
|
1076
|
+
workspaceManagementInstruction,
|
|
923
1077
|
].join("\n\n")
|
|
924
1078
|
: [
|
|
925
1079
|
`Workspace already open as ${workspace.id}.`,
|
|
926
1080
|
"Reuse this workspaceId for subsequent tool calls. This is the same directory previously opened in this conversation.",
|
|
927
1081
|
"Continue following the project instructions, nested instruction files, skills, capability guides, agent profiles, and diagnostics previously provided for this workspace. They remain active and are not repeated here.",
|
|
1082
|
+
workspaceContextInstruction,
|
|
1083
|
+
workspaceManagementInstruction,
|
|
928
1084
|
].join("\n\n")
|
|
929
1085
|
: workspace.mode === "worktree"
|
|
930
1086
|
? "Use this workspaceId for subsequent tool calls. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for this isolated worktree."
|
|
@@ -998,6 +1154,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
998
1154
|
worktrees: knownWorktrees,
|
|
999
1155
|
staleWorkspaces,
|
|
1000
1156
|
capabilityFingerprint,
|
|
1157
|
+
contextFingerprint,
|
|
1001
1158
|
capabilityCatalog,
|
|
1002
1159
|
agentsFiles: cardAgentsFiles,
|
|
1003
1160
|
availableAgentsFiles: cardAvailableAgentsFiles,
|
|
@@ -1017,6 +1174,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1017
1174
|
},
|
|
1018
1175
|
},
|
|
1019
1176
|
structuredContent: {
|
|
1177
|
+
action: "open",
|
|
1020
1178
|
workspaceId: workspace.id,
|
|
1021
1179
|
root: workspace.root,
|
|
1022
1180
|
mode: workspace.mode,
|
|
@@ -1025,6 +1183,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1025
1183
|
worktrees: knownWorktrees,
|
|
1026
1184
|
staleWorkspaces,
|
|
1027
1185
|
capabilityFingerprint,
|
|
1186
|
+
contextFingerprint,
|
|
1028
1187
|
capabilityCatalog,
|
|
1029
1188
|
...(includeBootstrapContext
|
|
1030
1189
|
? {
|
|
@@ -1205,14 +1364,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1205
1364
|
if (!commitMessage) {
|
|
1206
1365
|
throw new Error(`Managed-worktree-backed workspace ${workspaceId} requires commitMessage when closing.`);
|
|
1207
1366
|
}
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1367
|
+
const physicalWorkspaceIds = workspaces.workspaceIdsForPhysicalWorkspace(workspace);
|
|
1368
|
+
const busyWorkspaceIds = physicalWorkspaceIds
|
|
1210
1369
|
.filter((id) => processSessions.activeWorkspaceIds().has(id));
|
|
1211
1370
|
if (busyWorkspaceIds.length > 0) {
|
|
1212
1371
|
throw new Error(`Cannot close this worktree-backed workspace while logical workspace processes are still running or awaiting completion delivery: ${busyWorkspaceIds.join(", ")}.`);
|
|
1213
1372
|
}
|
|
1214
1373
|
const startedAt = performance.now();
|
|
1215
1374
|
const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
|
|
1375
|
+
await Promise.all(physicalWorkspaceIds.map((id) => reviewCheckpoints.releaseWorkspace(id)));
|
|
1216
1376
|
const result = [
|
|
1217
1377
|
`Closed managed-worktree-backed workspace ${workspaceId}.`,
|
|
1218
1378
|
`Merged ${closed.branch} into ${closed.targetBranch} by fast-forward.`,
|
|
@@ -1252,6 +1412,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1252
1412
|
throw new Error(`Workspace ${workspaceId} still owns a running process or an unconsumed process completion. Poll or consume it before closing this workspace.`);
|
|
1253
1413
|
}
|
|
1254
1414
|
workspaces.closeWorkspace(workspaceId);
|
|
1415
|
+
await reviewCheckpoints.releaseWorkspace(workspaceId);
|
|
1255
1416
|
const result = `Closed checkout-backed workspace ${workspaceId}. Physical project files were not removed.`;
|
|
1256
1417
|
return {
|
|
1257
1418
|
content: [textBlock(result)],
|
|
@@ -1285,7 +1446,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1285
1446
|
.optional()
|
|
1286
1447
|
.describe("Maximum number of lines to read."),
|
|
1287
1448
|
},
|
|
1288
|
-
outputSchema: resultOutputSchema(
|
|
1449
|
+
outputSchema: resultOutputSchema({
|
|
1450
|
+
agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
|
|
1451
|
+
}),
|
|
1289
1452
|
...toolWidgetDescriptorMeta(config, "read"),
|
|
1290
1453
|
annotations: { readOnlyHint: true },
|
|
1291
1454
|
}, async ({ workspaceId, ...input }, extra) => {
|
|
@@ -1298,6 +1461,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1298
1461
|
operation: async () => {
|
|
1299
1462
|
const startedAt = performance.now();
|
|
1300
1463
|
const readPath = workspaces.resolveReadPath(workspace, input.path);
|
|
1464
|
+
const discoveredInstructions = (await workspaces.discoverPathInstructions(workspace, readPath.absolutePath)).filter((file) => file.path !== readPath.absolutePath);
|
|
1301
1465
|
const response = await readFileTool({ ...input, path: readPath.absolutePath }, {
|
|
1302
1466
|
cwd: workspace.root,
|
|
1303
1467
|
root: workspace.root,
|
|
@@ -1312,6 +1476,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1312
1476
|
return response;
|
|
1313
1477
|
}
|
|
1314
1478
|
workspaces.markReadPathLoaded(workspace, readPath);
|
|
1479
|
+
const discoveredInstructionContent = discoveredInstructions.length > 0
|
|
1480
|
+
? textBlock(formatDiscoveredWorkspaceInstructions(discoveredInstructions, workspace.root))
|
|
1481
|
+
: undefined;
|
|
1482
|
+
const content = discoveredInstructionContent
|
|
1483
|
+
? [discoveredInstructionContent, ...response.content]
|
|
1484
|
+
: response.content;
|
|
1315
1485
|
const summary = {
|
|
1316
1486
|
...textSummary(response.content),
|
|
1317
1487
|
offset: input.offset ?? 1,
|
|
@@ -1326,6 +1496,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1326
1496
|
});
|
|
1327
1497
|
return {
|
|
1328
1498
|
...response,
|
|
1499
|
+
content,
|
|
1329
1500
|
_meta: {
|
|
1330
1501
|
tool: toolNames.read,
|
|
1331
1502
|
card: {
|
|
@@ -1336,7 +1507,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1336
1507
|
},
|
|
1337
1508
|
},
|
|
1338
1509
|
structuredContent: {
|
|
1339
|
-
result: contentText(
|
|
1510
|
+
result: contentText(content),
|
|
1511
|
+
...(discoveredInstructions.length > 0
|
|
1512
|
+
? {
|
|
1513
|
+
agentsFiles: discoveredInstructions.map((file) => ({
|
|
1514
|
+
path: formatAgentsPath(file.path, workspace.root),
|
|
1515
|
+
content: file.content,
|
|
1516
|
+
})),
|
|
1517
|
+
}
|
|
1518
|
+
: {}),
|
|
1340
1519
|
},
|
|
1341
1520
|
};
|
|
1342
1521
|
},
|
|
@@ -1368,6 +1547,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1368
1547
|
changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
|
|
1369
1548
|
operation: async () => {
|
|
1370
1549
|
const startedAt = performance.now();
|
|
1550
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [input.path]);
|
|
1371
1551
|
const response = await writeFileTool(input, {
|
|
1372
1552
|
cwd: workspace.root,
|
|
1373
1553
|
root: workspace.root,
|
|
@@ -1450,6 +1630,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1450
1630
|
changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
|
|
1451
1631
|
operation: async () => {
|
|
1452
1632
|
const startedAt = performance.now();
|
|
1633
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [input.path]);
|
|
1453
1634
|
const response = await editFileTool(input, {
|
|
1454
1635
|
cwd: workspace.root,
|
|
1455
1636
|
root: workspace.root,
|
|
@@ -1525,6 +1706,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1525
1706
|
operation: async () => {
|
|
1526
1707
|
const startedAt = performance.now();
|
|
1527
1708
|
try {
|
|
1709
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path, newPath]);
|
|
1528
1710
|
await renamePath({ path, newPath }, {
|
|
1529
1711
|
cwd: workspace.root,
|
|
1530
1712
|
allowedRoots: workspaces.fileToolRoots(workspace),
|
|
@@ -1596,6 +1778,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1596
1778
|
operation: async () => {
|
|
1597
1779
|
const startedAt = performance.now();
|
|
1598
1780
|
try {
|
|
1781
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path]);
|
|
1599
1782
|
const deleted = await deletePath({ path, recursive }, {
|
|
1600
1783
|
cwd: workspace.root,
|
|
1601
1784
|
allowedRoots: workspaces.fileToolRoots(workspace),
|
|
@@ -1806,6 +1989,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
1806
1989
|
operation: async () => {
|
|
1807
1990
|
const startedAt = performance.now();
|
|
1808
1991
|
const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
|
|
1992
|
+
await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
|
|
1809
1993
|
const snapshot = await processSessions.start({
|
|
1810
1994
|
workspaceId,
|
|
1811
1995
|
command,
|
|
@@ -1909,7 +2093,9 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1909
2093
|
host: config.host,
|
|
1910
2094
|
...(allowedHosts ? { allowedHosts } : {}),
|
|
1911
2095
|
});
|
|
1912
|
-
const transports = new McpTransportRegistry(
|
|
2096
|
+
const transports = new McpTransportRegistry({
|
|
2097
|
+
maxTransports: MAX_MCP_TRANSPORT_SESSIONS,
|
|
2098
|
+
});
|
|
1913
2099
|
const mcpUrl = new URL("/mcp", config.publicBaseUrl);
|
|
1914
2100
|
const resourceServerUrl = resourceUrlFromServerUrl(mcpUrl);
|
|
1915
2101
|
const oauthProvider = new SingleUserOAuthProvider(config.oauth, mcpUrl, config.stateDir);
|
|
@@ -1939,7 +2125,7 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1939
2125
|
continue;
|
|
1940
2126
|
}
|
|
1941
2127
|
closedCount += 1;
|
|
1942
|
-
if (reason
|
|
2128
|
+
if (reason !== "server_shutdown") {
|
|
1943
2129
|
logEvent(config.logging, "debug", "mcp_transport_session_closed", {
|
|
1944
2130
|
reason,
|
|
1945
2131
|
transportSessionIdPrefix: transportSessionIdPrefix(result.transportSessionId),
|
|
@@ -1953,12 +2139,31 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
1953
2139
|
});
|
|
1954
2140
|
}
|
|
1955
2141
|
};
|
|
2142
|
+
const logRuntimeResources = () => {
|
|
2143
|
+
const memory = process.memoryUsage();
|
|
2144
|
+
const processStats = processSessions.stats();
|
|
2145
|
+
logEvent(config.logging, "debug", "runtime_resources", {
|
|
2146
|
+
rssBytes: memory.rss,
|
|
2147
|
+
heapUsedBytes: memory.heapUsed,
|
|
2148
|
+
heapTotalBytes: memory.heapTotal,
|
|
2149
|
+
externalBytes: memory.external,
|
|
2150
|
+
arrayBuffersBytes: memory.arrayBuffers,
|
|
2151
|
+
mcpTransports: transports.size,
|
|
2152
|
+
processesTotal: processStats.total,
|
|
2153
|
+
processesRunning: processStats.running,
|
|
2154
|
+
processesCompleted: processStats.completed,
|
|
2155
|
+
cachedWorkspaces: workspaces.cachedWorkspaceCount,
|
|
2156
|
+
reviewStates: reviewCheckpoints.stateCount,
|
|
2157
|
+
});
|
|
2158
|
+
};
|
|
1956
2159
|
const transportCleanupTimer = setInterval(() => {
|
|
1957
2160
|
void transports
|
|
1958
2161
|
.closeIdle(MCP_TRANSPORT_IDLE_TIMEOUT_MS)
|
|
1959
|
-
.then((results) => logTransportCloseResults("idle_timeout", results))
|
|
2162
|
+
.then((results) => logTransportCloseResults("idle_timeout", results))
|
|
2163
|
+
.finally(logRuntimeResources);
|
|
1960
2164
|
}, MCP_TRANSPORT_CLEANUP_INTERVAL_MS);
|
|
1961
2165
|
transportCleanupTimer.unref();
|
|
2166
|
+
logRuntimeResources();
|
|
1962
2167
|
if (config.logging.trustProxy) {
|
|
1963
2168
|
app.set("trust proxy", 1);
|
|
1964
2169
|
}
|
|
@@ -2050,8 +2255,11 @@ export function createServer(config = loadConfig(), options = {}) {
|
|
|
2050
2255
|
transport = new StreamableHTTPServerTransport({
|
|
2051
2256
|
sessionIdGenerator: () => randomUUID(),
|
|
2052
2257
|
onsessioninitialized: (newTransportSessionId) => {
|
|
2053
|
-
if (transport)
|
|
2054
|
-
transports
|
|
2258
|
+
if (transport) {
|
|
2259
|
+
void transports
|
|
2260
|
+
.register(newTransportSessionId, transport)
|
|
2261
|
+
.then((results) => logTransportCloseResults("capacity_limit", results));
|
|
2262
|
+
}
|
|
2055
2263
|
logEvent(config.logging, "debug", "mcp_transport_session_created", {
|
|
2056
2264
|
requestId,
|
|
2057
2265
|
transportSessionIdPrefix: transportSessionIdPrefix(newTransportSessionId),
|