@akira-tl/forgerelay 0.5.2 → 0.5.5

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/dist/server.js CHANGED
@@ -17,7 +17,10 @@ import * as z from "zod/v4";
17
17
  import { applyPatch } from "./apply-patch.js";
18
18
  import { ActivityAuditStore } from "./activity/audit-store.js";
19
19
  import { BashOutputStore } from "./activity/bash-output-store.js";
20
+ import { HostTurnStore } from "./activity/host-turn-store.js";
21
+ import { registerActivityQueryTools } from "./activity/mcp-query-tools.js";
20
22
  import { ActivityLifecycle, } from "./activity/lifecycle.js";
23
+ import { ActivityQueryService } from "./activity/query-service.js";
21
24
  import { buildCapabilityFingerprint } from "./capabilities.js";
22
25
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
23
26
  import { deletePath, renamePath } from "./file-mutations.js";
@@ -33,6 +36,10 @@ import { createOpenAIIncomingArtifactAdapter, IncomingArtifactAdapterRegistry, }
33
36
  import { logEvent, requestIp, requestPath, commandPreview, transportSessionIdPrefix, workspaceLogLabel, } from "./logger.js";
34
37
  import { editFileTool, readFileTool, writeFileTool, } from "./pi-tools.js";
35
38
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
39
+ import { BatchExecutor } from "./operations/batch/executor.js";
40
+ import { executeBulkRead } from "./operations/bulk-read.js";
41
+ import { NativeBulkMutationExecutor } from "./operations/native-bulk-mutations.js";
42
+ import { createCoreOperationExecutor, } from "./operations/core-operation-executor.js";
36
43
  import { McpTransportRegistry, } from "./mcp-sessions.js";
37
44
  import { ProcessManager, resolveProcessId, } from "./process-sessions.js";
38
45
  import { createReviewCheckpointManager } from "./review-checkpoints.js";
@@ -168,6 +175,7 @@ const capabilityCatalogOutputSchema = z.object({
168
175
  description: z.string(),
169
176
  available: z.boolean(),
170
177
  unavailableReason: z.string().optional(),
178
+ batchPolicy: z.enum(["parallel", "serial", "unsupported"]),
171
179
  guide: capabilityCatalogGuideOutputSchema,
172
180
  });
173
181
  const capabilityErrorOutputSchema = z.object({
@@ -721,6 +729,53 @@ async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
721
729
  function toolResultIsError(result) {
722
730
  return typeof result === "object" && result !== null && result.isError === true;
723
731
  }
732
+ function toolResultText(result) {
733
+ if (typeof result !== "object" || result === null)
734
+ return String(result ?? "");
735
+ const record = result;
736
+ if (Array.isArray(record.content)) {
737
+ const text = record.content
738
+ .map((entry) => {
739
+ if (typeof entry !== "object" || entry === null)
740
+ return "";
741
+ const value = entry.text;
742
+ return typeof value === "string" ? value : "";
743
+ })
744
+ .filter(Boolean)
745
+ .join("\n");
746
+ if (text)
747
+ return text;
748
+ }
749
+ if (typeof record.structuredContent === "object" && record.structuredContent !== null) {
750
+ const value = record.structuredContent.result;
751
+ if (typeof value === "string")
752
+ return value;
753
+ }
754
+ return "";
755
+ }
756
+ function toolResultContent(result) {
757
+ if (typeof result !== "object" || result === null)
758
+ return [];
759
+ const content = result.content;
760
+ return Array.isArray(content) ? content : [];
761
+ }
762
+ function toolResultAgentsFiles(result) {
763
+ if (typeof result !== "object" || result === null)
764
+ return [];
765
+ const structured = result.structuredContent;
766
+ if (typeof structured !== "object" || structured === null)
767
+ return [];
768
+ const agentsFiles = structured.agentsFiles;
769
+ if (!Array.isArray(agentsFiles))
770
+ return [];
771
+ return agentsFiles.flatMap((entry) => {
772
+ if (typeof entry !== "object" || entry === null)
773
+ return [];
774
+ const path = entry.path;
775
+ const content = entry.content;
776
+ return typeof path === "string" && typeof content === "string" ? [{ path, content }] : [];
777
+ });
778
+ }
724
779
  function workspaceActivitySnapshot(workspace) {
725
780
  return {
726
781
  id: workspace.id,
@@ -778,7 +833,13 @@ function processActivityOutcome(result) {
778
833
  }
779
834
  return { type: "succeeded" };
780
835
  }
781
- function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome) {
836
+ function activityRelationFor(context) {
837
+ return {
838
+ ...(context.parentActivityId ? { parentActivityId: context.parentActivityId } : {}),
839
+ ...(context.turnId ? { turnId: context.turnId } : {}),
840
+ };
841
+ }
842
+ function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome, relation = {}) {
782
843
  return lifecycle.run({
783
844
  tool,
784
845
  workspace: workspaceActivitySnapshot(workspace),
@@ -786,12 +847,13 @@ function runActivityTool(lifecycle, workspace, requestMeta, tool, request, opera
786
847
  request,
787
848
  operation,
788
849
  outcome,
850
+ ...relation,
789
851
  });
790
852
  }
791
- function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions) {
792
- return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions));
853
+ function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions, relation = {}) {
854
+ return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
793
855
  }
794
- function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore) {
856
+ function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, shellRun) {
795
857
  if (config.toolMode === "codex") {
796
858
  registerAppTool(server, "exec_command", {
797
859
  title: "Execute command",
@@ -834,70 +896,22 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
834
896
  outputSchema: processOutputSchema(),
835
897
  ...toolWidgetDescriptorMeta(config, "shell"),
836
898
  annotations: SHELL_TOOL_ANNOTATIONS,
837
- }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
838
- const workspace = workspaces.getWorkspace(workspaceId);
839
- let undeliveredProcessId;
840
- const activityResult = await runActivityTool(activityLifecycle, workspace, extra._meta, "exec_command", { workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, async (activityContext) => {
841
- try {
842
- const result = await runToolWithHooks(hooks, {
843
- signal: extra.signal,
844
- tool: "exec_command",
845
- invocation: workspaceHookInvocation(workspace),
846
- payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
847
- operation: async () => {
848
- const startedAt = performance.now();
849
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
850
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
851
- const snapshot = await processSessions.start({
852
- workspaceId,
853
- command: cmd,
854
- cwd,
855
- workspaceRoot: workspace.root,
856
- tty,
857
- columns,
858
- rows,
859
- yieldTimeMs,
860
- timeoutMs,
861
- maxOutputTokens,
862
- codexCi: true,
863
- signal: extra.signal,
864
- audit: activityContext,
865
- });
866
- undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
867
- logToolCall(config, {
868
- tool: "exec_command",
869
- ...workspaceLogContext(workspace, extra.sessionId),
870
- workingDirectory: workingDirectory ?? ".",
871
- command: cmd,
872
- commandLength: cmd.length,
873
- exitCode: snapshot.exitCode,
874
- running: snapshot.running,
875
- processId: snapshot.processId,
876
- success: snapshot.running || snapshot.exitCode === 0,
877
- durationMs: Math.round(performance.now() - startedAt),
878
- });
879
- return processToolResponse("exec_command", workspaceId, snapshot, {
880
- command: cmd,
881
- workingDirectory: workingDirectory ?? ".",
882
- running: snapshot.running,
883
- exitCode: snapshot.exitCode,
884
- wallTimeMs: snapshot.wallTimeMs,
885
- });
886
- },
887
- });
888
- extra.signal.throwIfAborted();
889
- return result;
890
- }
891
- catch (error) {
892
- if (undeliveredProcessId !== undefined) {
893
- processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
894
- }
895
- throw error;
896
- }
897
- }, processActivityOutcome);
898
- markReturnedOutput(bashOutputStore, activityResult);
899
- return activityResult;
900
- });
899
+ }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => shellRun({
900
+ workspaceId,
901
+ command: cmd,
902
+ surface: "exec_command",
903
+ tty,
904
+ columns,
905
+ rows,
906
+ workingDirectory,
907
+ yieldTimeMs,
908
+ timeoutMs,
909
+ maxOutputTokens,
910
+ }, {
911
+ requestMeta: extra._meta,
912
+ signal: extra.signal,
913
+ sessionId: extra.sessionId,
914
+ }));
901
915
  }
902
916
  if (config.toolMode !== "codex")
903
917
  return;
@@ -992,14 +1006,33 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
992
1006
  });
993
1007
  });
994
1008
  }
995
- export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore) {
1009
+ export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries) {
996
1010
  const toolDescriptions = buildToolDescriptions(config);
997
1011
  const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
998
1012
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
999
1013
  const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
1000
1014
  const reviewChangesAvailable = config.widgets === "changes";
1015
+ let batchExecutor;
1016
+ const batchExecuteAvailable = config.toolMode !== "codex";
1001
1017
  const capabilityRegistry = createCapabilityRegistry({
1002
1018
  inspectHooks: (workspaceRoot) => checkHookConfiguration(workspaceRoot, config.hooks),
1019
+ batchExecute: {
1020
+ available: batchExecuteAvailable,
1021
+ unavailableReason: batchExecuteAvailable
1022
+ ? undefined
1023
+ : "batch.execute is unavailable in Codex tool mode because v0.5.5 core batch tasks use the regular Read/Write/Edit/Bash operation surface.",
1024
+ run: async (input, context, options) => {
1025
+ if (!batchExecutor)
1026
+ throw new Error("Batch executor is not initialized.");
1027
+ return {
1028
+ value: await batchExecutor.run(context.workspaceId, input, {
1029
+ requestMeta: options.requestMeta,
1030
+ signal: options.signal,
1031
+ sessionId: options.sessionId,
1032
+ }),
1033
+ };
1034
+ },
1035
+ },
1003
1036
  codeIntelligence: {
1004
1037
  available: true,
1005
1038
  run: async (input, context, options) => {
@@ -1071,6 +1104,510 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1071
1104
  },
1072
1105
  },
1073
1106
  });
1107
+ const coreOperations = createCoreOperationExecutor({
1108
+ read: async (input, context) => {
1109
+ const { workspaceId, ...readInput } = input;
1110
+ const workspace = workspaces.getWorkspace(workspaceId);
1111
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1112
+ signal: context.signal,
1113
+ tool: toolNames.read,
1114
+ invocation: workspaceHookInvocation(workspace),
1115
+ payload: { path: readInput.path, offset: readInput.offset, limit: readInput.limit },
1116
+ isFailure: toolResultIsError,
1117
+ operation: async () => {
1118
+ const startedAt = performance.now();
1119
+ const readPath = workspaces.resolveReadPath(workspace, readInput.path);
1120
+ const discoveredInstructions = (await workspaces.discoverPathInstructions(workspace, readPath.absolutePath)).filter((file) => file.path !== readPath.absolutePath);
1121
+ const response = await readFileTool({ ...readInput, path: readPath.absolutePath }, {
1122
+ cwd: workspace.root,
1123
+ root: workspace.root,
1124
+ readRoots: readPath.readRoots,
1125
+ });
1126
+ if (response.isError) {
1127
+ logFailedToolResponse(config, {
1128
+ tool: toolNames.read,
1129
+ ...workspaceLogContext(workspace, context.sessionId),
1130
+ path: readInput.path,
1131
+ }, response.content, startedAt);
1132
+ return response;
1133
+ }
1134
+ workspaces.markReadPathLoaded(workspace, readPath);
1135
+ const discoveredInstructionContent = discoveredInstructions.length > 0
1136
+ ? textBlock(formatDiscoveredWorkspaceInstructions(discoveredInstructions, workspace.root))
1137
+ : undefined;
1138
+ const content = discoveredInstructionContent
1139
+ ? [discoveredInstructionContent, ...response.content]
1140
+ : response.content;
1141
+ const summary = {
1142
+ ...textSummary(response.content),
1143
+ offset: readInput.offset ?? 1,
1144
+ limited: readInput.limit !== undefined,
1145
+ };
1146
+ logToolCall(config, {
1147
+ tool: toolNames.read,
1148
+ ...workspaceLogContext(workspace, context.sessionId),
1149
+ path: readInput.path,
1150
+ success: true,
1151
+ durationMs: Math.round(performance.now() - startedAt),
1152
+ });
1153
+ return {
1154
+ ...response,
1155
+ content,
1156
+ _meta: {
1157
+ tool: toolNames.read,
1158
+ card: {
1159
+ workspaceId,
1160
+ path: readInput.path,
1161
+ summary,
1162
+ payload: { content: response.content },
1163
+ },
1164
+ },
1165
+ structuredContent: {
1166
+ result: contentText(content),
1167
+ ...(discoveredInstructions.length > 0
1168
+ ? {
1169
+ agentsFiles: discoveredInstructions.map((file) => ({
1170
+ path: formatAgentsPath(file.path, workspace.root),
1171
+ content: file.content,
1172
+ })),
1173
+ }
1174
+ : {}),
1175
+ },
1176
+ };
1177
+ },
1178
+ }, activityRelationFor(context));
1179
+ },
1180
+ write: async (input, context) => {
1181
+ const { workspaceId, ...writeInput } = input;
1182
+ const workspace = workspaces.getWorkspace(workspaceId);
1183
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1184
+ signal: context.signal,
1185
+ tool: toolNames.write,
1186
+ invocation: workspaceHookInvocation(workspace),
1187
+ payload: { path: writeInput.path },
1188
+ isFailure: toolResultIsError,
1189
+ changedPaths: (result) => toolResultIsError(result) ? [] : [writeInput.path],
1190
+ operation: async () => {
1191
+ const startedAt = performance.now();
1192
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [writeInput.path]);
1193
+ const response = await writeFileTool(writeInput, {
1194
+ cwd: workspace.root,
1195
+ root: workspace.root,
1196
+ fileRoots: workspaces.fileToolRoots(workspace),
1197
+ });
1198
+ if (response.isError) {
1199
+ logFailedToolResponse(config, {
1200
+ tool: toolNames.write,
1201
+ ...workspaceLogContext(workspace, context.sessionId),
1202
+ path: writeInput.path,
1203
+ }, response.content, startedAt);
1204
+ return response;
1205
+ }
1206
+ const patch = newFilePatch(writeInput.path, writeInput.content);
1207
+ const stats = countDiffStats(patch);
1208
+ const summary = {
1209
+ ...stats,
1210
+ lines: contentLineCount(writeInput.content),
1211
+ characters: writeInput.content.length,
1212
+ };
1213
+ logToolCall(config, {
1214
+ tool: toolNames.write,
1215
+ ...workspaceLogContext(workspace, context.sessionId),
1216
+ path: writeInput.path,
1217
+ success: true,
1218
+ durationMs: Math.round(performance.now() - startedAt),
1219
+ });
1220
+ return {
1221
+ ...response,
1222
+ _meta: {
1223
+ tool: toolNames.write,
1224
+ card: {
1225
+ workspaceId,
1226
+ path: writeInput.path,
1227
+ summary,
1228
+ payload: {
1229
+ content: response.content,
1230
+ patch,
1231
+ },
1232
+ },
1233
+ },
1234
+ structuredContent: {
1235
+ result: contentText(response.content),
1236
+ },
1237
+ };
1238
+ },
1239
+ }, activityRelationFor(context));
1240
+ },
1241
+ edit: async (input, context) => {
1242
+ const { workspaceId, ...editInput } = input;
1243
+ const workspace = workspaces.getWorkspace(workspaceId);
1244
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1245
+ signal: context.signal,
1246
+ tool: toolNames.edit,
1247
+ invocation: workspaceHookInvocation(workspace),
1248
+ payload: { path: editInput.path, editCount: editInput.edits.length },
1249
+ isFailure: toolResultIsError,
1250
+ changedPaths: (result) => toolResultIsError(result) ? [] : [editInput.path],
1251
+ operation: async () => {
1252
+ const startedAt = performance.now();
1253
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [editInput.path]);
1254
+ const response = await editFileTool(editInput, {
1255
+ cwd: workspace.root,
1256
+ root: workspace.root,
1257
+ fileRoots: workspaces.fileToolRoots(workspace),
1258
+ });
1259
+ if (response.isError) {
1260
+ logFailedToolResponse(config, {
1261
+ tool: toolNames.edit,
1262
+ ...workspaceLogContext(workspace, context.sessionId),
1263
+ path: editInput.path,
1264
+ }, response.content, startedAt);
1265
+ return response;
1266
+ }
1267
+ const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
1268
+ const summary = {
1269
+ ...stats,
1270
+ editCount: editInput.edits.length,
1271
+ };
1272
+ const editResultText = `Edited ${editInput.path} (+${stats.additions} -${stats.removals}).`;
1273
+ const editContent = [textBlock(editResultText)];
1274
+ logToolCall(config, {
1275
+ tool: toolNames.edit,
1276
+ ...workspaceLogContext(workspace, context.sessionId),
1277
+ path: editInput.path,
1278
+ success: true,
1279
+ durationMs: Math.round(performance.now() - startedAt),
1280
+ });
1281
+ return {
1282
+ content: editContent,
1283
+ _meta: {
1284
+ tool: toolNames.edit,
1285
+ card: {
1286
+ workspaceId,
1287
+ path: editInput.path,
1288
+ summary,
1289
+ payload: {
1290
+ diff: response.details?.diff,
1291
+ patch: response.details?.patch,
1292
+ },
1293
+ },
1294
+ },
1295
+ structuredContent: {
1296
+ status: "applied",
1297
+ result: contentText(editContent),
1298
+ },
1299
+ };
1300
+ },
1301
+ }, activityRelationFor(context));
1302
+ },
1303
+ rename: async (input, context) => {
1304
+ const { workspaceId, path, newPath } = input;
1305
+ const workspace = workspaces.getWorkspace(workspaceId);
1306
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1307
+ signal: context.signal,
1308
+ tool: toolNames.rename,
1309
+ invocation: workspaceHookInvocation(workspace),
1310
+ payload: { path, newPath, paths: [path, newPath] },
1311
+ changedPaths: () => [path, newPath],
1312
+ operation: async () => {
1313
+ const startedAt = performance.now();
1314
+ try {
1315
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path, newPath]);
1316
+ await renamePath({ path, newPath }, {
1317
+ cwd: workspace.root,
1318
+ allowedRoots: workspaces.fileToolRoots(workspace),
1319
+ });
1320
+ const result = `Renamed ${path} to ${newPath}.`;
1321
+ const content = [textBlock(result)];
1322
+ logToolCall(config, {
1323
+ tool: toolNames.rename,
1324
+ ...workspaceLogContext(workspace, context.sessionId),
1325
+ path: `${path} -> ${newPath}`,
1326
+ success: true,
1327
+ durationMs: Math.round(performance.now() - startedAt),
1328
+ });
1329
+ return {
1330
+ content,
1331
+ _meta: {
1332
+ tool: toolNames.rename,
1333
+ card: {
1334
+ workspaceId,
1335
+ path: newPath,
1336
+ summary: { previousPath: path },
1337
+ payload: { content },
1338
+ },
1339
+ },
1340
+ structuredContent: {
1341
+ result,
1342
+ status: "renamed",
1343
+ path,
1344
+ newPath,
1345
+ },
1346
+ };
1347
+ }
1348
+ catch (error) {
1349
+ logToolCall(config, {
1350
+ tool: toolNames.rename,
1351
+ ...workspaceLogContext(workspace, context.sessionId),
1352
+ path: `${path} -> ${newPath}`,
1353
+ success: false,
1354
+ durationMs: Math.round(performance.now() - startedAt),
1355
+ error: error instanceof Error ? error.message : String(error),
1356
+ });
1357
+ throw error;
1358
+ }
1359
+ },
1360
+ }, activityRelationFor(context));
1361
+ },
1362
+ delete: async (input, context) => {
1363
+ const { workspaceId, path, recursive } = input;
1364
+ const workspace = workspaces.getWorkspace(workspaceId);
1365
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, input, {
1366
+ signal: context.signal,
1367
+ tool: toolNames.delete,
1368
+ invocation: workspaceHookInvocation(workspace),
1369
+ payload: { path, recursive: recursive ?? false },
1370
+ changedPaths: () => [path],
1371
+ operation: async () => {
1372
+ const startedAt = performance.now();
1373
+ try {
1374
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path]);
1375
+ const deleted = await deletePath({ path, recursive }, {
1376
+ cwd: workspace.root,
1377
+ allowedRoots: workspaces.fileToolRoots(workspace),
1378
+ });
1379
+ const result = `Deleted ${path}${deleted.recursive ? " recursively" : ""}.`;
1380
+ const content = [textBlock(result)];
1381
+ logToolCall(config, {
1382
+ tool: toolNames.delete,
1383
+ ...workspaceLogContext(workspace, context.sessionId),
1384
+ path,
1385
+ success: true,
1386
+ durationMs: Math.round(performance.now() - startedAt),
1387
+ });
1388
+ return {
1389
+ content,
1390
+ _meta: {
1391
+ tool: toolNames.delete,
1392
+ card: {
1393
+ workspaceId,
1394
+ path,
1395
+ summary: { recursive: deleted.recursive },
1396
+ payload: { content },
1397
+ },
1398
+ },
1399
+ structuredContent: {
1400
+ result,
1401
+ status: "deleted",
1402
+ path,
1403
+ recursive: deleted.recursive,
1404
+ },
1405
+ };
1406
+ }
1407
+ catch (error) {
1408
+ logToolCall(config, {
1409
+ tool: toolNames.delete,
1410
+ ...workspaceLogContext(workspace, context.sessionId),
1411
+ path,
1412
+ success: false,
1413
+ durationMs: Math.round(performance.now() - startedAt),
1414
+ error: error instanceof Error ? error.message : String(error),
1415
+ });
1416
+ throw error;
1417
+ }
1418
+ },
1419
+ }, activityRelationFor(context));
1420
+ },
1421
+ shellRun: async (input, context) => {
1422
+ const { workspaceId, command, surface, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, } = input;
1423
+ const workspace = workspaces.getWorkspace(workspaceId);
1424
+ const activityRequest = surface === "exec_command"
1425
+ ? {
1426
+ workspaceId,
1427
+ cmd: command,
1428
+ tty,
1429
+ columns,
1430
+ rows,
1431
+ workingDirectory,
1432
+ yieldTimeMs,
1433
+ timeoutMs,
1434
+ maxOutputTokens,
1435
+ }
1436
+ : {
1437
+ workspaceId,
1438
+ action: "run",
1439
+ command,
1440
+ tty,
1441
+ columns,
1442
+ rows,
1443
+ workingDirectory,
1444
+ yieldTimeMs,
1445
+ timeoutMs,
1446
+ maxOutputTokens,
1447
+ };
1448
+ let undeliveredProcessId;
1449
+ const activityResult = await runActivityTool(activityLifecycle, workspace, context.requestMeta, surface, activityRequest, async (activityContext) => {
1450
+ try {
1451
+ const result = await runToolWithHooks(hooks, {
1452
+ signal: context.signal,
1453
+ tool: surface,
1454
+ invocation: workspaceHookInvocation(workspace),
1455
+ payload: surface === "exec_command"
1456
+ ? { command, workingDirectory: workingDirectory ?? "." }
1457
+ : { action: "run", command, workingDirectory: workingDirectory ?? "." },
1458
+ ...(surface === "bash" ? { isFailure: toolResultIsError } : {}),
1459
+ operation: async () => {
1460
+ const startedAt = performance.now();
1461
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1462
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
1463
+ const snapshot = await processSessions.start({
1464
+ workspaceId,
1465
+ command,
1466
+ cwd,
1467
+ workspaceRoot: workspace.root,
1468
+ tty,
1469
+ columns,
1470
+ rows,
1471
+ yieldTimeMs,
1472
+ timeoutMs,
1473
+ maxOutputTokens,
1474
+ ...(surface === "exec_command" ? { codexCi: true } : {}),
1475
+ signal: context.signal,
1476
+ audit: activityContext,
1477
+ });
1478
+ undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
1479
+ logToolCall(config, {
1480
+ tool: surface,
1481
+ ...workspaceLogContext(workspace, context.sessionId),
1482
+ workingDirectory: workingDirectory ?? ".",
1483
+ command,
1484
+ commandLength: command.length,
1485
+ exitCode: snapshot.exitCode,
1486
+ running: snapshot.running,
1487
+ processId: snapshot.processId,
1488
+ success: surface === "exec_command"
1489
+ ? snapshot.running || snapshot.exitCode === 0
1490
+ : snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
1491
+ durationMs: Math.round(performance.now() - startedAt),
1492
+ });
1493
+ const response = processToolResponse(surface, workspaceId, snapshot, {
1494
+ ...(surface === "bash" ? { action: "run" } : {}),
1495
+ command,
1496
+ workingDirectory: workingDirectory ?? ".",
1497
+ running: snapshot.running,
1498
+ exitCode: snapshot.exitCode,
1499
+ wallTimeMs: snapshot.wallTimeMs,
1500
+ });
1501
+ return surface === "bash" && !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
1502
+ ? { ...response, isError: true }
1503
+ : response;
1504
+ },
1505
+ });
1506
+ context.signal?.throwIfAborted();
1507
+ return result;
1508
+ }
1509
+ catch (error) {
1510
+ if (undeliveredProcessId !== undefined) {
1511
+ processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
1512
+ }
1513
+ throw error;
1514
+ }
1515
+ }, processActivityOutcome, activityRelationFor(context));
1516
+ markReturnedOutput(bashOutputStore, activityResult);
1517
+ return activityResult;
1518
+ },
1519
+ capabilityRun: async (input, context) => {
1520
+ const { workspaceId, name, arguments: capabilityArguments, file } = input;
1521
+ const workspace = workspaces.getWorkspace(workspaceId);
1522
+ let changedPaths = [];
1523
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, context.requestMeta, { workspaceId, name, action: "run", arguments: capabilityArguments, file }, {
1524
+ signal: context.signal,
1525
+ tool: toolNames.capability,
1526
+ invocation: workspaceHookInvocation(workspace),
1527
+ payload: { name, action: "run" },
1528
+ isFailure: toolResultIsError,
1529
+ changedPaths: () => changedPaths,
1530
+ operation: async () => {
1531
+ const startedAt = performance.now();
1532
+ try {
1533
+ const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
1534
+ nativeFile: file,
1535
+ signal: context.signal,
1536
+ requestMeta: context.requestMeta,
1537
+ sessionId: context.sessionId,
1538
+ batch: context.batch,
1539
+ });
1540
+ changedPaths = execution.changedPaths ?? [];
1541
+ const result = {
1542
+ content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
1543
+ ...(execution.card
1544
+ ? {
1545
+ _meta: {
1546
+ tool: toolNames.capability,
1547
+ card: {
1548
+ workspaceId,
1549
+ capabilityName: name,
1550
+ summary: execution.card.summary ?? {},
1551
+ files: execution.card.files,
1552
+ payload: execution.card.payload ?? {},
1553
+ },
1554
+ },
1555
+ }
1556
+ : {}),
1557
+ structuredContent: { name, action: "run", result: execution.value },
1558
+ };
1559
+ logToolCall(config, {
1560
+ tool: toolNames.capability,
1561
+ ...workspaceLogContext(workspace, context.sessionId),
1562
+ capability: name,
1563
+ action: "run",
1564
+ success: true,
1565
+ durationMs: Math.round(performance.now() - startedAt),
1566
+ });
1567
+ return result;
1568
+ }
1569
+ catch (error) {
1570
+ const capabilityError = error instanceof CapabilityError
1571
+ ? error
1572
+ : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
1573
+ const result = {
1574
+ content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
1575
+ structuredContent: {
1576
+ name,
1577
+ action: "run",
1578
+ error: { code: capabilityError.code, message: capabilityError.message },
1579
+ },
1580
+ isError: true,
1581
+ };
1582
+ logFailedToolResponse(config, {
1583
+ tool: toolNames.capability,
1584
+ ...workspaceLogContext(workspace, context.sessionId),
1585
+ capability: name,
1586
+ action: "run",
1587
+ }, result.content, startedAt);
1588
+ return result;
1589
+ }
1590
+ },
1591
+ }, activityRelationFor(context));
1592
+ },
1593
+ });
1594
+ batchExecutor = new BatchExecutor({
1595
+ lifecycle: activityLifecycle,
1596
+ workspaces,
1597
+ coreOperations,
1598
+ resultIsError: toolResultIsError,
1599
+ capabilityBatchPolicy: (name) => capabilityRegistry.batchPolicy(name),
1600
+ shellSurface: "bash",
1601
+ });
1602
+ const nativeBulkMutations = new NativeBulkMutationExecutor({
1603
+ lifecycle: activityLifecycle,
1604
+ workspaces,
1605
+ coreOperations,
1606
+ preflightInstructions: (workspace, paths) => assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, paths),
1607
+ resultIsError: toolResultIsError,
1608
+ resultText: toolResultText,
1609
+ resultContent: toolResultContent,
1610
+ });
1074
1611
  const server = new McpServer({
1075
1612
  name: "forgerelay",
1076
1613
  title: "ForgeRelay",
@@ -1465,6 +2002,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1465
2002
  },
1466
2003
  }, hookReports));
1467
2004
  });
2005
+ registerActivityQueryTools(server, activityQueries);
1468
2006
  registerAppTool(server, toolNames.capability, {
1469
2007
  title: "Use optional capability",
1470
2008
  description: "Describe or run one optional ForgeRelay capability advertised by open_workspace. Use describe when the capability contract is unfamiliar, then read its advertised guide if needed. Run dispatches only explicitly registered capabilities; it cannot invoke arbitrary shell commands, URLs, or methods.",
@@ -1502,60 +2040,82 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1502
2040
  openWorldHint: true,
1503
2041
  },
1504
2042
  }, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
2043
+ if (action === "run" && name === "batch.execute") {
2044
+ const workspace = workspaces.getWorkspace(workspaceId);
2045
+ const startedAt = performance.now();
2046
+ try {
2047
+ const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), {
2048
+ nativeFile: file,
2049
+ signal: extra.signal,
2050
+ requestMeta: extra._meta,
2051
+ sessionId: extra.sessionId,
2052
+ });
2053
+ const result = {
2054
+ content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
2055
+ structuredContent: { name, action, result: execution.value },
2056
+ };
2057
+ logToolCall(config, {
2058
+ tool: toolNames.capability,
2059
+ ...workspaceLogContext(workspace, extra.sessionId),
2060
+ capability: name,
2061
+ action,
2062
+ success: true,
2063
+ durationMs: Math.round(performance.now() - startedAt),
2064
+ });
2065
+ return result;
2066
+ }
2067
+ catch (error) {
2068
+ if (extra.signal.aborted)
2069
+ throw error;
2070
+ const capabilityError = error instanceof CapabilityError
2071
+ ? error
2072
+ : new CapabilityError("execution_failed", error instanceof Error ? error.message : String(error));
2073
+ const result = {
2074
+ content: [textBlock(`${capabilityError.code}: ${capabilityError.message}`)],
2075
+ structuredContent: {
2076
+ name,
2077
+ action,
2078
+ error: { code: capabilityError.code, message: capabilityError.message },
2079
+ },
2080
+ isError: true,
2081
+ };
2082
+ logFailedToolResponse(config, {
2083
+ tool: toolNames.capability,
2084
+ ...workspaceLogContext(workspace, extra.sessionId),
2085
+ capability: name,
2086
+ action,
2087
+ }, result.content, startedAt);
2088
+ return result;
2089
+ }
2090
+ }
2091
+ if (action === "run") {
2092
+ return coreOperations.capabilityRun({ workspaceId, name, arguments: capabilityArguments, file }, {
2093
+ requestMeta: extra._meta,
2094
+ signal: extra.signal,
2095
+ sessionId: extra.sessionId,
2096
+ });
2097
+ }
1505
2098
  const workspace = workspaces.getWorkspace(workspaceId);
1506
- let changedPaths = [];
1507
2099
  return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, name, action, arguments: capabilityArguments, file }, {
1508
2100
  signal: extra.signal,
1509
2101
  tool: toolNames.capability,
1510
2102
  invocation: workspaceHookInvocation(workspace),
1511
2103
  payload: { name, action },
1512
2104
  isFailure: toolResultIsError,
1513
- changedPaths: () => changedPaths,
1514
2105
  operation: async () => {
1515
2106
  const startedAt = performance.now();
1516
2107
  try {
1517
- if (action === "describe") {
1518
- const capability = capabilityRegistry.describe(name, capabilityContextFor(workspace));
1519
- const result = {
1520
- content: [textBlock([
1521
- `${capability.name}: ${capability.description}`,
1522
- `Available: ${capability.available}`,
1523
- `Guide: ${capability.guide.path}`,
1524
- capability.guide.readBeforeFirstUse
1525
- ? "Read the guide before first use when this contract is unfamiliar."
1526
- : undefined,
1527
- ].filter(Boolean).join("\n"))],
1528
- structuredContent: { name, action, capability },
1529
- };
1530
- logToolCall(config, {
1531
- tool: toolNames.capability,
1532
- ...workspaceLogContext(workspace),
1533
- capability: name,
1534
- action,
1535
- success: true,
1536
- durationMs: Math.round(performance.now() - startedAt),
1537
- });
1538
- return result;
1539
- }
1540
- const execution = await capabilityRegistry.run(name, capabilityArguments ?? {}, capabilityContextFor(workspace), { nativeFile: file, signal: extra.signal });
1541
- changedPaths = execution.changedPaths ?? [];
2108
+ const capability = capabilityRegistry.describe(name, capabilityContextFor(workspace));
1542
2109
  const result = {
1543
- content: [textBlock(`Capability ${name} completed.\n${JSON.stringify(execution.value, null, 2)}`)],
1544
- ...(execution.card
1545
- ? {
1546
- _meta: {
1547
- tool: toolNames.capability,
1548
- card: {
1549
- workspaceId,
1550
- capabilityName: name,
1551
- summary: execution.card.summary ?? {},
1552
- files: execution.card.files,
1553
- payload: execution.card.payload ?? {},
1554
- },
1555
- },
1556
- }
1557
- : {}),
1558
- structuredContent: { name, action, result: execution.value },
2110
+ content: [textBlock([
2111
+ `${capability.name}: ${capability.description}`,
2112
+ `Available: ${capability.available}`,
2113
+ `Guide: ${capability.guide.path}`,
2114
+ capability.guide.readBeforeFirstUse
2115
+ ? "Read the guide before first use when this contract is unfamiliar."
2116
+ : undefined,
2117
+ ].filter(Boolean).join("\n"))],
2118
+ structuredContent: { name, action, capability },
1559
2119
  };
1560
2120
  logToolCall(config, {
1561
2121
  tool: toolNames.capability,
@@ -1704,9 +2264,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1704
2264
  .describe("Workspace identifier returned by open_workspace."),
1705
2265
  path: z
1706
2266
  .string()
2267
+ .optional()
1707
2268
  .describe(config.skillsEnabled
1708
- ? "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill or capability-guide path from open_workspace, including a ~/... home-relative path."
1709
- : "File path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised capability-guide path from open_workspace."),
2269
+ ? "One file path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised skill or capability-guide path from open_workspace, including a ~/... home-relative path. Use exactly one of path or paths."
2270
+ : "One file path to read, relative to the workspace root or absolute inside the OS temp directory. May also be an advertised capability-guide path from open_workspace. Use exactly one of path or paths."),
2271
+ paths: z
2272
+ .array(z.string())
2273
+ .min(1)
2274
+ .max(100)
2275
+ .optional()
2276
+ .describe("Multiple file paths to read in one call. Uses the same offset/limit for every file. Use exactly one of path or paths."),
1710
2277
  offset: z
1711
2278
  .number()
1712
2279
  .int()
@@ -1722,79 +2289,81 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1722
2289
  },
1723
2290
  outputSchema: resultOutputSchema({
1724
2291
  agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
2292
+ results: z.array(z.object({
2293
+ path: z.string(),
2294
+ status: z.enum(["done", "error"]),
2295
+ result: z.string(),
2296
+ })).optional(),
2297
+ files: z.number().int().nonnegative().optional(),
2298
+ failed: z.number().int().nonnegative().optional(),
1725
2299
  }),
1726
2300
  ...toolWidgetDescriptorMeta(config, "read"),
1727
2301
  annotations: { readOnlyHint: true },
1728
- }, async ({ workspaceId, ...input }, extra) => {
2302
+ }, async ({ workspaceId, path, paths, offset, limit }, extra) => {
2303
+ if ((path === undefined) === (paths === undefined)) {
2304
+ throw new Error("read requires exactly one of path or paths.");
2305
+ }
2306
+ if (path !== undefined) {
2307
+ return coreOperations.read({ workspaceId, path, offset, limit }, {
2308
+ requestMeta: extra._meta,
2309
+ signal: extra.signal,
2310
+ sessionId: extra.sessionId,
2311
+ });
2312
+ }
1729
2313
  const workspace = workspaces.getWorkspace(workspaceId);
1730
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1731
- signal: extra.signal,
1732
- tool: toolNames.read,
1733
- invocation: workspaceHookInvocation(workspace),
1734
- payload: { path: input.path, offset: input.offset, limit: input.limit },
1735
- isFailure: toolResultIsError,
1736
- operation: async () => {
1737
- const startedAt = performance.now();
1738
- const readPath = workspaces.resolveReadPath(workspace, input.path);
1739
- const discoveredInstructions = (await workspaces.discoverPathInstructions(workspace, readPath.absolutePath)).filter((file) => file.path !== readPath.absolutePath);
1740
- const response = await readFileTool({ ...input, path: readPath.absolutePath }, {
1741
- cwd: workspace.root,
1742
- root: workspace.root,
1743
- readRoots: readPath.readRoots,
1744
- });
1745
- if (response.isError) {
1746
- logFailedToolResponse(config, {
1747
- tool: toolNames.read,
1748
- ...workspaceLogContext(workspace, extra.sessionId),
1749
- path: input.path,
1750
- }, response.content, startedAt);
1751
- return response;
1752
- }
1753
- workspaces.markReadPathLoaded(workspace, readPath);
1754
- const discoveredInstructionContent = discoveredInstructions.length > 0
1755
- ? textBlock(formatDiscoveredWorkspaceInstructions(discoveredInstructions, workspace.root))
1756
- : undefined;
1757
- const content = discoveredInstructionContent
1758
- ? [discoveredInstructionContent, ...response.content]
1759
- : response.content;
1760
- const summary = {
1761
- ...textSummary(response.content),
1762
- offset: input.offset ?? 1,
1763
- limited: input.limit !== undefined,
1764
- };
1765
- logToolCall(config, {
1766
- tool: toolNames.read,
1767
- ...workspaceLogContext(workspace, extra.sessionId),
1768
- path: input.path,
1769
- success: true,
1770
- durationMs: Math.round(performance.now() - startedAt),
1771
- });
1772
- return {
1773
- ...response,
1774
- content,
1775
- _meta: {
1776
- tool: toolNames.read,
1777
- card: {
1778
- workspaceId,
1779
- path: input.path,
1780
- summary,
1781
- payload: { content: response.content },
1782
- },
1783
- },
1784
- structuredContent: {
1785
- result: contentText(content),
1786
- ...(discoveredInstructions.length > 0
1787
- ? {
1788
- agentsFiles: discoveredInstructions.map((file) => ({
1789
- path: formatAgentsPath(file.path, workspace.root),
1790
- content: file.content,
1791
- })),
1792
- }
1793
- : {}),
1794
- },
1795
- };
1796
- },
1797
- });
2314
+ let response;
2315
+ await runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.read, { workspaceId, paths, offset, limit }, async (parentContext) => {
2316
+ const execution = await executeBulkRead({
2317
+ paths: paths,
2318
+ signal: extra.signal,
2319
+ run: (childPath) => coreOperations.read({ workspaceId, path: childPath, offset, limit }, {
2320
+ requestMeta: extra._meta,
2321
+ signal: extra.signal,
2322
+ sessionId: extra.sessionId,
2323
+ parentActivityId: parentContext.activityId,
2324
+ turnId: parentContext.turnId,
2325
+ }),
2326
+ isError: toolResultIsError,
2327
+ resultText: toolResultText,
2328
+ });
2329
+ const content = execution.children.flatMap((child) => [
2330
+ textBlock(`--- ${child.path} · ${child.status} ---`),
2331
+ ...(child.response
2332
+ ? toolResultContent(child.response)
2333
+ : [textBlock(child.result)]),
2334
+ ]);
2335
+ const seenAgentPaths = new Set();
2336
+ const agentsFiles = execution.children.flatMap((child) => child.response ? toolResultAgentsFiles(child.response) : []).filter((file) => {
2337
+ if (seenAgentPaths.has(file.path))
2338
+ return false;
2339
+ seenAgentPaths.add(file.path);
2340
+ return true;
2341
+ });
2342
+ response = {
2343
+ content,
2344
+ structuredContent: {
2345
+ result: contentText(content),
2346
+ results: execution.children.map(({ path: childPath, status, result }) => ({
2347
+ path: childPath,
2348
+ status,
2349
+ result,
2350
+ })),
2351
+ files: execution.children.length,
2352
+ failed: execution.failed,
2353
+ ...(agentsFiles.length > 0 ? { agentsFiles } : {}),
2354
+ },
2355
+ };
2356
+ return {
2357
+ childCount: execution.children.length,
2358
+ succeeded: execution.succeeded,
2359
+ failed: execution.failed,
2360
+ };
2361
+ }, (summary) => summary.failed > 0
2362
+ ? { type: "failed", error: `${summary.failed} of ${summary.childCount} child Reads failed.` }
2363
+ : { type: "succeeded" });
2364
+ if (!response)
2365
+ throw new Error("Bulk Read completed without a response.");
2366
+ return response;
1798
2367
  });
1799
2368
  if (config.toolMode !== "codex") {
1800
2369
  registerAppTool(server, toolNames.write, {
@@ -1812,66 +2381,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1812
2381
  outputSchema: resultOutputSchema(),
1813
2382
  ...toolWidgetDescriptorMeta(config, "write"),
1814
2383
  annotations: WRITE_TOOL_ANNOTATIONS,
1815
- }, async ({ workspaceId, ...input }, extra) => {
1816
- const workspace = workspaces.getWorkspace(workspaceId);
1817
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1818
- signal: extra.signal,
1819
- tool: toolNames.write,
1820
- invocation: workspaceHookInvocation(workspace),
1821
- payload: { path: input.path },
1822
- isFailure: toolResultIsError,
1823
- changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
1824
- operation: async () => {
1825
- const startedAt = performance.now();
1826
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [input.path]);
1827
- const response = await writeFileTool(input, {
1828
- cwd: workspace.root,
1829
- root: workspace.root,
1830
- fileRoots: workspaces.fileToolRoots(workspace),
1831
- });
1832
- if (response.isError) {
1833
- logFailedToolResponse(config, {
1834
- tool: toolNames.write,
1835
- ...workspaceLogContext(workspace, extra.sessionId),
1836
- path: input.path,
1837
- }, response.content, startedAt);
1838
- return response;
1839
- }
1840
- const patch = newFilePatch(input.path, input.content);
1841
- const stats = countDiffStats(patch);
1842
- const summary = {
1843
- ...stats,
1844
- lines: contentLineCount(input.content),
1845
- characters: input.content.length,
1846
- };
1847
- logToolCall(config, {
1848
- tool: toolNames.write,
1849
- ...workspaceLogContext(workspace, extra.sessionId),
1850
- path: input.path,
1851
- success: true,
1852
- durationMs: Math.round(performance.now() - startedAt),
1853
- });
1854
- return {
1855
- ...response,
1856
- _meta: {
1857
- tool: toolNames.write,
1858
- card: {
1859
- workspaceId,
1860
- path: input.path,
1861
- summary,
1862
- payload: {
1863
- content: response.content,
1864
- patch,
1865
- },
1866
- },
1867
- },
1868
- structuredContent: {
1869
- result: contentText(response.content),
1870
- },
1871
- };
1872
- },
1873
- });
1874
- });
2384
+ }, async ({ workspaceId, ...input }, extra) => coreOperations.write({ workspaceId, ...input }, {
2385
+ requestMeta: extra._meta,
2386
+ signal: extra.signal,
2387
+ sessionId: extra.sessionId,
2388
+ }));
1875
2389
  registerAppTool(server, toolNames.edit, {
1876
2390
  title: "Edit file",
1877
2391
  description: toolDescriptions.edit,
@@ -1881,7 +2395,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1881
2395
  .describe("Workspace identifier returned by open_workspace."),
1882
2396
  path: z
1883
2397
  .string()
1884
- .describe("File path to edit, relative to the workspace root or absolute inside the OS temp directory."),
2398
+ .optional()
2399
+ .describe("One file path to edit. Use exactly one of path or paths."),
2400
+ paths: z
2401
+ .array(z.string())
2402
+ .min(1)
2403
+ .max(100)
2404
+ .optional()
2405
+ .describe("Multiple file paths to edit with the same edits. Use exactly one of path or paths."),
1885
2406
  edits: z
1886
2407
  .array(z.object({
1887
2408
  oldText: z
@@ -1892,69 +2413,34 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1892
2413
  .min(1),
1893
2414
  },
1894
2415
  outputSchema: resultOutputSchema({
1895
- status: z.literal("applied"),
2416
+ status: z.enum(["applied", "partial"]),
2417
+ results: z.array(z.object({
2418
+ path: z.string(),
2419
+ status: z.enum(["done", "error", "unexecuted"]),
2420
+ result: z.string().optional(),
2421
+ })).optional(),
2422
+ files: z.number().int().nonnegative().optional(),
2423
+ completed: z.number().int().nonnegative().optional(),
2424
+ failed: z.number().int().nonnegative().optional(),
2425
+ unexecuted: z.number().int().nonnegative().optional(),
1896
2426
  }),
1897
2427
  ...toolWidgetDescriptorMeta(config, "edit"),
1898
2428
  annotations: EDIT_TOOL_ANNOTATIONS,
1899
- }, async ({ workspaceId, ...input }, extra) => {
1900
- const workspace = workspaces.getWorkspace(workspaceId);
1901
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
2429
+ }, async ({ workspaceId, path, paths, edits }, extra) => {
2430
+ if ((path === undefined) === (paths === undefined)) {
2431
+ throw new Error("edit requires exactly one of path or paths.");
2432
+ }
2433
+ if (path !== undefined) {
2434
+ return coreOperations.edit({ workspaceId, path, edits }, {
2435
+ requestMeta: extra._meta,
2436
+ signal: extra.signal,
2437
+ sessionId: extra.sessionId,
2438
+ });
2439
+ }
2440
+ return nativeBulkMutations.edit({ workspaceId, paths: paths, edits }, {
2441
+ requestMeta: extra._meta,
1902
2442
  signal: extra.signal,
1903
- tool: toolNames.edit,
1904
- invocation: workspaceHookInvocation(workspace),
1905
- payload: { path: input.path, editCount: input.edits.length },
1906
- isFailure: toolResultIsError,
1907
- changedPaths: (result) => toolResultIsError(result) ? [] : [input.path],
1908
- operation: async () => {
1909
- const startedAt = performance.now();
1910
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [input.path]);
1911
- const response = await editFileTool(input, {
1912
- cwd: workspace.root,
1913
- root: workspace.root,
1914
- fileRoots: workspaces.fileToolRoots(workspace),
1915
- });
1916
- if (response.isError) {
1917
- logFailedToolResponse(config, {
1918
- tool: toolNames.edit,
1919
- ...workspaceLogContext(workspace, extra.sessionId),
1920
- path: input.path,
1921
- }, response.content, startedAt);
1922
- return response;
1923
- }
1924
- const stats = countDiffStats(response.details?.patch ?? response.details?.diff);
1925
- const summary = {
1926
- ...stats,
1927
- editCount: input.edits.length,
1928
- };
1929
- const editResultText = `Edited ${input.path} (+${stats.additions} -${stats.removals}).`;
1930
- const editContent = [textBlock(editResultText)];
1931
- logToolCall(config, {
1932
- tool: toolNames.edit,
1933
- ...workspaceLogContext(workspace, extra.sessionId),
1934
- path: input.path,
1935
- success: true,
1936
- durationMs: Math.round(performance.now() - startedAt),
1937
- });
1938
- return {
1939
- content: editContent,
1940
- _meta: {
1941
- tool: toolNames.edit,
1942
- card: {
1943
- workspaceId,
1944
- path: input.path,
1945
- summary,
1946
- payload: {
1947
- diff: response.details?.diff,
1948
- patch: response.details?.patch,
1949
- },
1950
- },
1951
- },
1952
- structuredContent: {
1953
- status: "applied",
1954
- result: contentText(editContent),
1955
- },
1956
- };
1957
- },
2443
+ sessionId: extra.sessionId,
1958
2444
  });
1959
2445
  });
1960
2446
  }
@@ -1973,135 +2459,56 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1973
2459
  }),
1974
2460
  ...toolWidgetDescriptorMeta(config, "edit"),
1975
2461
  annotations: EDIT_TOOL_ANNOTATIONS,
1976
- }, async ({ workspaceId, path, newPath }, extra) => {
1977
- const workspace = workspaces.getWorkspace(workspaceId);
1978
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, newPath }, {
1979
- signal: extra.signal,
1980
- tool: toolNames.rename,
1981
- invocation: workspaceHookInvocation(workspace),
1982
- payload: { path, newPath, paths: [path, newPath] },
1983
- changedPaths: () => [path, newPath],
1984
- operation: async () => {
1985
- const startedAt = performance.now();
1986
- try {
1987
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path, newPath]);
1988
- await renamePath({ path, newPath }, {
1989
- cwd: workspace.root,
1990
- allowedRoots: workspaces.fileToolRoots(workspace),
1991
- });
1992
- const result = `Renamed ${path} to ${newPath}.`;
1993
- const content = [textBlock(result)];
1994
- logToolCall(config, {
1995
- tool: toolNames.rename,
1996
- ...workspaceLogContext(workspace, extra.sessionId),
1997
- path: `${path} -> ${newPath}`,
1998
- success: true,
1999
- durationMs: Math.round(performance.now() - startedAt),
2000
- });
2001
- return {
2002
- content,
2003
- _meta: {
2004
- tool: toolNames.rename,
2005
- card: {
2006
- workspaceId,
2007
- path: newPath,
2008
- summary: { previousPath: path },
2009
- payload: { content },
2010
- },
2011
- },
2012
- structuredContent: {
2013
- result,
2014
- status: "renamed",
2015
- path,
2016
- newPath,
2017
- },
2018
- };
2019
- }
2020
- catch (error) {
2021
- logToolCall(config, {
2022
- tool: toolNames.rename,
2023
- ...workspaceLogContext(workspace, extra.sessionId),
2024
- path: `${path} -> ${newPath}`,
2025
- success: false,
2026
- durationMs: Math.round(performance.now() - startedAt),
2027
- error: error instanceof Error ? error.message : String(error),
2028
- });
2029
- throw error;
2030
- }
2031
- },
2032
- });
2033
- });
2462
+ }, async ({ workspaceId, path, newPath }, extra) => coreOperations.rename({ workspaceId, path, newPath }, {
2463
+ requestMeta: extra._meta,
2464
+ signal: extra.signal,
2465
+ sessionId: extra.sessionId,
2466
+ }));
2034
2467
  registerAppTool(server, toolNames.delete, {
2035
2468
  title: "Delete path",
2036
2469
  description: toolDescriptions.delete,
2037
2470
  inputSchema: {
2038
2471
  workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
2039
- path: z.string().describe("File or directory path relative to the workspace root, or absolute inside the OS temp directory."),
2040
- recursive: z.boolean().optional().describe("Delete a non-empty directory tree. Defaults to false."),
2472
+ path: z.string().optional().describe("One file or directory path to delete. Use exactly one of path or paths."),
2473
+ paths: z
2474
+ .array(z.string())
2475
+ .min(1)
2476
+ .max(100)
2477
+ .optional()
2478
+ .describe("Multiple paths to delete in one call. Use exactly one of path or paths."),
2479
+ recursive: z.boolean().optional().describe("Delete non-empty directory trees. Defaults to false and applies to every bulk target."),
2041
2480
  },
2042
2481
  outputSchema: resultOutputSchema({
2043
- status: z.literal("deleted"),
2044
- path: z.string(),
2045
- recursive: z.boolean(),
2482
+ status: z.enum(["deleted", "partial"]),
2483
+ path: z.string().optional(),
2484
+ recursive: z.boolean().optional(),
2485
+ results: z.array(z.object({
2486
+ path: z.string(),
2487
+ status: z.enum(["done", "error", "unexecuted"]),
2488
+ result: z.string().optional(),
2489
+ })).optional(),
2490
+ paths: z.number().int().nonnegative().optional(),
2491
+ completed: z.number().int().nonnegative().optional(),
2492
+ failed: z.number().int().nonnegative().optional(),
2493
+ unexecuted: z.number().int().nonnegative().optional(),
2046
2494
  }),
2047
2495
  ...toolWidgetDescriptorMeta(config, "edit"),
2048
2496
  annotations: EDIT_TOOL_ANNOTATIONS,
2049
- }, async ({ workspaceId, path, recursive }, extra) => {
2050
- const workspace = workspaces.getWorkspace(workspaceId);
2051
- return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, recursive }, {
2497
+ }, async ({ workspaceId, path, paths, recursive }, extra) => {
2498
+ if ((path === undefined) === (paths === undefined)) {
2499
+ throw new Error("delete requires exactly one of path or paths.");
2500
+ }
2501
+ if (path !== undefined) {
2502
+ return coreOperations.delete({ workspaceId, path, recursive }, {
2503
+ requestMeta: extra._meta,
2504
+ signal: extra.signal,
2505
+ sessionId: extra.sessionId,
2506
+ });
2507
+ }
2508
+ return nativeBulkMutations.delete({ workspaceId, paths: paths, recursive }, {
2509
+ requestMeta: extra._meta,
2052
2510
  signal: extra.signal,
2053
- tool: toolNames.delete,
2054
- invocation: workspaceHookInvocation(workspace),
2055
- payload: { path, recursive: recursive ?? false },
2056
- changedPaths: () => [path],
2057
- operation: async () => {
2058
- const startedAt = performance.now();
2059
- try {
2060
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [path]);
2061
- const deleted = await deletePath({ path, recursive }, {
2062
- cwd: workspace.root,
2063
- allowedRoots: workspaces.fileToolRoots(workspace),
2064
- });
2065
- const result = `Deleted ${path}${deleted.recursive ? " recursively" : ""}.`;
2066
- const content = [textBlock(result)];
2067
- logToolCall(config, {
2068
- tool: toolNames.delete,
2069
- ...workspaceLogContext(workspace, extra.sessionId),
2070
- path,
2071
- success: true,
2072
- durationMs: Math.round(performance.now() - startedAt),
2073
- });
2074
- return {
2075
- content,
2076
- _meta: {
2077
- tool: toolNames.delete,
2078
- card: {
2079
- workspaceId,
2080
- path,
2081
- summary: { recursive: deleted.recursive },
2082
- payload: { content },
2083
- },
2084
- },
2085
- structuredContent: {
2086
- result,
2087
- status: "deleted",
2088
- path,
2089
- recursive: deleted.recursive,
2090
- },
2091
- };
2092
- }
2093
- catch (error) {
2094
- logToolCall(config, {
2095
- tool: toolNames.delete,
2096
- ...workspaceLogContext(workspace, extra.sessionId),
2097
- path,
2098
- success: false,
2099
- durationMs: Math.round(performance.now() - startedAt),
2100
- error: error instanceof Error ? error.message : String(error),
2101
- });
2102
- throw error;
2103
- }
2104
- },
2511
+ sessionId: extra.sessionId,
2105
2512
  });
2106
2513
  });
2107
2514
  if (config.toolMode === "codex") {
@@ -2268,11 +2675,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2268
2675
  if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
2269
2676
  throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
2270
2677
  }
2271
- let undeliveredProcessId;
2272
- const activityResult = await runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.shell, {
2678
+ return coreOperations.shellRun({
2273
2679
  workspaceId,
2274
- action,
2275
2680
  command,
2681
+ surface: "bash",
2276
2682
  tty,
2277
2683
  columns,
2278
2684
  rows,
@@ -2280,74 +2686,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2280
2686
  yieldTimeMs,
2281
2687
  timeoutMs,
2282
2688
  maxOutputTokens,
2283
- }, async (activityContext) => {
2284
- try {
2285
- const result = await runToolWithHooks(hooks, {
2286
- signal: extra.signal,
2287
- tool: toolNames.shell,
2288
- invocation: workspaceHookInvocation(workspace),
2289
- payload: {
2290
- action,
2291
- command,
2292
- workingDirectory: workingDirectory ?? ".",
2293
- },
2294
- isFailure: toolResultIsError,
2295
- operation: async () => {
2296
- const startedAt = performance.now();
2297
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
2298
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
2299
- const snapshot = await processSessions.start({
2300
- workspaceId,
2301
- command,
2302
- cwd,
2303
- workspaceRoot: workspace.root,
2304
- tty,
2305
- columns,
2306
- rows,
2307
- yieldTimeMs,
2308
- timeoutMs,
2309
- maxOutputTokens,
2310
- signal: extra.signal,
2311
- audit: activityContext,
2312
- });
2313
- undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
2314
- logToolCall(config, {
2315
- tool: toolNames.shell,
2316
- ...workspaceLogContext(workspace, extra.sessionId),
2317
- workingDirectory: workingDirectory ?? ".",
2318
- command,
2319
- commandLength: command.length,
2320
- exitCode: snapshot.exitCode,
2321
- running: snapshot.running,
2322
- processId: snapshot.processId,
2323
- success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
2324
- durationMs: Math.round(performance.now() - startedAt),
2325
- });
2326
- const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
2327
- action,
2328
- command,
2329
- workingDirectory: workingDirectory ?? ".",
2330
- running: snapshot.running,
2331
- exitCode: snapshot.exitCode,
2332
- wallTimeMs: snapshot.wallTimeMs,
2333
- });
2334
- return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
2335
- ? { ...response, isError: true }
2336
- : response;
2337
- },
2338
- });
2339
- extra.signal.throwIfAborted();
2340
- return result;
2341
- }
2342
- catch (error) {
2343
- if (undeliveredProcessId !== undefined) {
2344
- processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
2345
- }
2346
- throw error;
2347
- }
2348
- }, processActivityOutcome);
2349
- markReturnedOutput(bashOutputStore, activityResult);
2350
- return activityResult;
2689
+ }, {
2690
+ requestMeta: extra._meta,
2691
+ signal: extra.signal,
2692
+ sessionId: extra.sessionId,
2693
+ });
2351
2694
  }
2352
2695
  if (action === "output") {
2353
2696
  if (!outputId)
@@ -2426,7 +2769,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2426
2769
  });
2427
2770
  });
2428
2771
  }
2429
- registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore);
2772
+ registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore, (input, context) => coreOperations.shellRun(input, context));
2430
2773
  return server;
2431
2774
  }
2432
2775
  export function createServer(config = loadConfig(), options = {}) {
@@ -2453,8 +2796,12 @@ export function createServer(config = loadConfig(), options = {}) {
2453
2796
  const workspaceStore = createWorkspaceStore(config.stateDir);
2454
2797
  const workspaces = new WorkspaceRegistry(config, workspaceStore);
2455
2798
  const activityAuditStore = new ActivityAuditStore(config.stateDir);
2456
- const activityLifecycle = new ActivityLifecycle(activityAuditStore);
2457
2799
  const bashOutputStore = new BashOutputStore(config.stateDir);
2800
+ const hostTurnStore = new HostTurnStore(config.stateDir);
2801
+ const activityQueries = new ActivityQueryService(hostTurnStore, activityAuditStore, bashOutputStore);
2802
+ const activityLifecycle = new ActivityLifecycle(activityAuditStore, {
2803
+ turnIdForConversation: (conversationScopeId) => activityQueries.currentTurnId(conversationScopeId),
2804
+ });
2458
2805
  const reviewCheckpoints = createReviewCheckpointManager();
2459
2806
  const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
2460
2807
  const codeIntelligence = new CodeIntelligenceManager(config);
@@ -2641,7 +2988,7 @@ export function createServer(config = loadConfig(), options = {}) {
2641
2988
  });
2642
2989
  }
2643
2990
  };
2644
- const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore);
2991
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
2645
2992
  await server.connect(transport);
2646
2993
  }
2647
2994
  else {
@@ -2673,6 +3020,7 @@ export function createServer(config = loadConfig(), options = {}) {
2673
3020
  processSessions.shutdown();
2674
3021
  await codeIntelligence.shutdown();
2675
3022
  oauthProvider.close();
3023
+ hostTurnStore.close();
2676
3024
  bashOutputStore.close();
2677
3025
  activityAuditStore.close();
2678
3026
  workspaceStore.close?.();