@akira-tl/forgerelay 0.5.0 → 0.5.2

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
@@ -15,6 +15,9 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@mode
15
15
  import express from "express";
16
16
  import * as z from "zod/v4";
17
17
  import { applyPatch } from "./apply-patch.js";
18
+ import { ActivityAuditStore } from "./activity/audit-store.js";
19
+ import { BashOutputStore } from "./activity/bash-output-store.js";
20
+ import { ActivityLifecycle, } from "./activity/lifecycle.js";
18
21
  import { buildCapabilityFingerprint } from "./capabilities.js";
19
22
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
20
23
  import { deletePath, renamePath } from "./file-mutations.js";
@@ -466,6 +469,24 @@ async function readWorkspaceAppResource(config, requestedUri, transportSessionId
466
469
  throw error;
467
470
  }
468
471
  }
472
+ const PROCESS_RESPONSE_OUTPUT_LINES = 10;
473
+ function compactProcessOutput(output) {
474
+ if (!output)
475
+ return { output: "", truncated: false };
476
+ const trailingNewline = output.endsWith("\n");
477
+ const body = trailingNewline ? output.slice(0, -1) : output;
478
+ const lines = body.split("\n");
479
+ if (lines.length <= PROCESS_RESPONSE_OUTPUT_LINES)
480
+ return { output, truncated: false };
481
+ const compact = lines.slice(-PROCESS_RESPONSE_OUTPUT_LINES).join("\n");
482
+ return {
483
+ output: trailingNewline ? `${compact}\n` : compact,
484
+ truncated: true,
485
+ };
486
+ }
487
+ function outputIdNotice(outputId) {
488
+ return outputId ? `Full output ID: ${outputId}.` : "";
489
+ }
469
490
  function processResult(snapshot) {
470
491
  const status = snapshot.running
471
492
  ? `Process running with process ID ${snapshot.processId}.`
@@ -474,7 +495,8 @@ function processResult(snapshot) {
474
495
  : snapshot.signal
475
496
  ? `Process exited after signal ${snapshot.signal}.`
476
497
  : `Process exited with code ${snapshot.exitCode ?? "unknown"}.`;
477
- return snapshot.output ? `${snapshot.output.replace(/\n$/, "")}\n${status}` : status;
498
+ const compact = compactProcessOutput(snapshot.output).output.replace(/\n$/, "");
499
+ return [compact, status, outputIdNotice(snapshot.outputId)].filter(Boolean).join("\n");
478
500
  }
479
501
  function completedProcessResult(snapshot) {
480
502
  const status = snapshot.timedOut
@@ -483,12 +505,14 @@ function completedProcessResult(snapshot) {
483
505
  ? `Background process ${snapshot.processId} exited after signal ${snapshot.signal}.`
484
506
  : `Background process ${snapshot.processId} exited with code ${snapshot.exitCode ?? "unknown"}.`;
485
507
  const command = `Command: ${snapshot.command}`;
486
- const output = snapshot.output ? `\n${snapshot.output.replace(/\n$/, "")}` : "";
487
- return `${status}\n${command}${output}`;
508
+ const output = compactProcessOutput(snapshot.output).output.replace(/\n$/, "");
509
+ return [status, command, output, outputIdNotice(snapshot.outputId)].filter(Boolean).join("\n");
488
510
  }
489
- function attachCompletedProcessNotices(processSessions, workspaceId, result) {
511
+ function attachCompletedProcessNotices(processSessions, workspaceId, result, onCompleted) {
490
512
  if (result instanceof Error) {
491
513
  const completed = processSessions.takeCompleted(workspaceId);
514
+ for (const snapshot of completed)
515
+ onCompleted?.(snapshot);
492
516
  if (completed.length > 0) {
493
517
  result.message = [
494
518
  result.message,
@@ -511,6 +535,8 @@ function attachCompletedProcessNotices(processSessions, workspaceId, result) {
511
535
  : undefined
512
536
  : undefined;
513
537
  const completed = processSessions.takeCompleted(workspaceId, undefined, currentProcessId);
538
+ for (const snapshot of completed)
539
+ onCompleted?.(snapshot);
514
540
  if (completed.length === 0)
515
541
  return result;
516
542
  return {
@@ -525,6 +551,7 @@ function processOutputSchema() {
525
551
  return resultOutputSchema({
526
552
  processId: z.number().int().positive().optional().describe("Canonical process handle for bash(action=\"process\") or the active command adapter."),
527
553
  sessionId: z.number().int().positive().optional().describe("Deprecated alias of processId for compatibility."),
554
+ outputId: z.string().optional().describe("Stable local audit identifier for retrieving the complete original process output."),
528
555
  running: z.boolean(),
529
556
  exitCode: z.number().int().optional(),
530
557
  signal: z.string().optional(),
@@ -541,9 +568,10 @@ function readForgeRelayVersion() {
541
568
  return packageJson.version;
542
569
  }
543
570
  function processToolResponse(tool, workspaceId, snapshot, summary) {
571
+ const compact = compactProcessOutput(snapshot.output);
544
572
  const result = processResult(snapshot);
545
573
  const content = [textBlock(result)];
546
- const outputSummary = textSummary(snapshot.output ? [textBlock(snapshot.output)] : []);
574
+ const outputSummary = textSummary(compact.output ? [textBlock(compact.output)] : []);
547
575
  return {
548
576
  content,
549
577
  _meta: {
@@ -558,15 +586,111 @@ function processToolResponse(tool, workspaceId, snapshot, summary) {
558
586
  result,
559
587
  processId: snapshot.processId,
560
588
  sessionId: snapshot.sessionId,
589
+ outputId: snapshot.outputId,
561
590
  running: snapshot.running,
562
591
  exitCode: snapshot.exitCode,
563
592
  signal: snapshot.signal,
564
593
  timedOut: snapshot.timedOut,
565
594
  wallTimeMs: snapshot.wallTimeMs,
566
- outputTruncated: snapshot.outputTruncated,
595
+ outputTruncated: snapshot.outputTruncated || compact.truncated,
567
596
  },
568
597
  };
569
598
  }
599
+ function durableOutputResult(record) {
600
+ const status = record.status === "running"
601
+ ? `Process ${record.processId} is still running.`
602
+ : record.timedOut
603
+ ? `Process ${record.processId} timed out and was terminated.`
604
+ : record.signal
605
+ ? `Process ${record.processId} exited after signal ${record.signal}.`
606
+ : `Process ${record.processId} exited with code ${record.exitCode ?? "unknown"}.`;
607
+ return [record.output.replace(/\n$/, ""), status, `Full output ID: ${record.outputId}.`]
608
+ .filter(Boolean)
609
+ .join("\n");
610
+ }
611
+ function durableOutputResponse(tool, workspaceId, record) {
612
+ const result = durableOutputResult(record);
613
+ const content = [textBlock(result)];
614
+ const finishedAt = record.finishedAt ? Date.parse(record.finishedAt) : Date.now();
615
+ const startedAt = Date.parse(record.startedAt);
616
+ return {
617
+ content,
618
+ _meta: {
619
+ tool,
620
+ card: {
621
+ workspaceId,
622
+ summary: textSummary(record.output ? [textBlock(record.output)] : []),
623
+ payload: { content },
624
+ },
625
+ },
626
+ structuredContent: {
627
+ result,
628
+ processId: record.processId,
629
+ sessionId: record.processId,
630
+ outputId: record.outputId,
631
+ running: record.status === "running",
632
+ exitCode: record.exitCode,
633
+ signal: record.signal,
634
+ timedOut: record.timedOut,
635
+ wallTimeMs: Math.max(0, Number.isFinite(finishedAt - startedAt) ? finishedAt - startedAt : 0),
636
+ outputTruncated: false,
637
+ },
638
+ };
639
+ }
640
+ function markReturnedOutput(store, result) {
641
+ if (typeof result !== "object" || result === null)
642
+ return;
643
+ const structured = result.structuredContent;
644
+ if (typeof structured !== "object" || structured === null)
645
+ return;
646
+ const record = structured;
647
+ if (record.running === true && typeof record.outputId === "string") {
648
+ store.markReturned(record.outputId);
649
+ }
650
+ }
651
+ function readWorkspaceBashOutput(store, workspaceId, outputId) {
652
+ const record = store.read(outputId);
653
+ if (!record)
654
+ throw new Error(`Unknown Bash output: ${outputId}`);
655
+ if (record.workspaceId !== workspaceId) {
656
+ throw new Error(`Bash output ${outputId} does not belong to workspace ${workspaceId}.`);
657
+ }
658
+ return record;
659
+ }
660
+ function bashCompletionError(record) {
661
+ if (record.error)
662
+ return record.error;
663
+ if (record.timedOut)
664
+ return `Background process ${record.processId} timed out.`;
665
+ if (record.signal)
666
+ return `Background process ${record.processId} exited after signal ${record.signal}.`;
667
+ return `Background process ${record.processId} exited with code ${record.exitCode ?? "unknown"}.`;
668
+ }
669
+ function recordBashCompletion(lifecycle, store, outputId) {
670
+ if (!outputId)
671
+ return;
672
+ const completion = store.claimCompletion(outputId);
673
+ if (!completion)
674
+ return;
675
+ lifecycle.recordLinked({
676
+ sourceActivityId: completion.activityId,
677
+ tool: "bash_result",
678
+ request: {
679
+ processId: completion.processId,
680
+ outputId: completion.outputId,
681
+ },
682
+ result: {
683
+ processId: completion.processId,
684
+ outputId: completion.outputId,
685
+ exitCode: completion.exitCode,
686
+ signal: completion.signal,
687
+ timedOut: completion.timedOut,
688
+ },
689
+ outcome: completion.status === "failed"
690
+ ? { type: "failed", error: bashCompletionError(completion) }
691
+ : { type: "succeeded" },
692
+ });
693
+ }
570
694
  function workspaceHookInvocation(workspace) {
571
695
  return {
572
696
  workspaceId: workspace.id,
@@ -597,7 +721,77 @@ async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
597
721
  function toolResultIsError(result) {
598
722
  return typeof result === "object" && result !== null && result.isError === true;
599
723
  }
600
- function registerProcessTools(server, config, workspaces, processSessions, hooks) {
724
+ function workspaceActivitySnapshot(workspace) {
725
+ return {
726
+ id: workspace.id,
727
+ root: workspace.root,
728
+ mode: workspace.mode,
729
+ ...(workspace.sourceRoot ? { sourceRoot: workspace.sourceRoot } : {}),
730
+ ...(workspace.worktree?.branch ? { branch: workspace.worktree.branch } : {}),
731
+ ...(workspace.worktree?.targetBranch ? { targetBranch: workspace.worktree.targetBranch } : {}),
732
+ };
733
+ }
734
+ function activityFailureMessage(result) {
735
+ if (typeof result !== "object" || result === null)
736
+ return "Tool returned a failed result.";
737
+ const record = result;
738
+ if (Array.isArray(record.content)) {
739
+ const text = record.content
740
+ .map((entry) => {
741
+ if (typeof entry !== "object" || entry === null)
742
+ return "";
743
+ const value = entry.text;
744
+ return typeof value === "string" ? value : "";
745
+ })
746
+ .filter(Boolean)
747
+ .join("\n");
748
+ if (text)
749
+ return text;
750
+ }
751
+ if (typeof record.structuredContent === "object" && record.structuredContent !== null) {
752
+ const value = record.structuredContent.result;
753
+ if (typeof value === "string" && value)
754
+ return value;
755
+ }
756
+ return "Tool returned a failed result.";
757
+ }
758
+ function standardActivityOutcome(result) {
759
+ return toolResultIsError(result)
760
+ ? { type: "failed", error: activityFailureMessage(result) }
761
+ : { type: "succeeded" };
762
+ }
763
+ function processActivityOutcome(result) {
764
+ if (toolResultIsError(result))
765
+ return { type: "failed", error: activityFailureMessage(result) };
766
+ if (typeof result !== "object" || result === null)
767
+ return { type: "succeeded" };
768
+ const structured = result.structuredContent;
769
+ if (typeof structured !== "object" || structured === null)
770
+ return { type: "succeeded" };
771
+ const process = structured;
772
+ if (process.running === true)
773
+ return { type: "returned" };
774
+ if (process.timedOut === true ||
775
+ typeof process.signal === "string" ||
776
+ (typeof process.exitCode === "number" && process.exitCode !== 0)) {
777
+ return { type: "failed", error: activityFailureMessage(result) };
778
+ }
779
+ return { type: "succeeded" };
780
+ }
781
+ function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome) {
782
+ return lifecycle.run({
783
+ tool,
784
+ workspace: workspaceActivitySnapshot(workspace),
785
+ conversationScopeId: openAiConversationScopeId(requestMeta),
786
+ request,
787
+ operation,
788
+ outcome,
789
+ });
790
+ }
791
+ function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions) {
792
+ return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions));
793
+ }
794
+ function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore) {
601
795
  if (config.toolMode === "codex") {
602
796
  registerAppTool(server, "exec_command", {
603
797
  title: "Execute command",
@@ -643,72 +837,78 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
643
837
  }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
644
838
  const workspace = workspaces.getWorkspace(workspaceId);
645
839
  let undeliveredProcessId;
646
- try {
647
- const result = await runToolWithHooks(hooks, {
648
- signal: extra.signal,
649
- tool: "exec_command",
650
- invocation: workspaceHookInvocation(workspace),
651
- payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
652
- operation: async () => {
653
- const startedAt = performance.now();
654
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
655
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
656
- const snapshot = await processSessions.start({
657
- workspaceId,
658
- command: cmd,
659
- cwd,
660
- workspaceRoot: workspace.root,
661
- tty,
662
- columns,
663
- rows,
664
- yieldTimeMs,
665
- timeoutMs,
666
- maxOutputTokens,
667
- codexCi: true,
668
- signal: extra.signal,
669
- });
670
- undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
671
- logToolCall(config, {
672
- tool: "exec_command",
673
- ...workspaceLogContext(workspace, extra.sessionId),
674
- workingDirectory: workingDirectory ?? ".",
675
- command: cmd,
676
- commandLength: cmd.length,
677
- exitCode: snapshot.exitCode,
678
- running: snapshot.running,
679
- processId: snapshot.processId,
680
- success: snapshot.running || snapshot.exitCode === 0,
681
- durationMs: Math.round(performance.now() - startedAt),
682
- });
683
- return processToolResponse("exec_command", workspaceId, snapshot, {
684
- command: cmd,
685
- workingDirectory: workingDirectory ?? ".",
686
- running: snapshot.running,
687
- exitCode: snapshot.exitCode,
688
- wallTimeMs: snapshot.wallTimeMs,
689
- });
690
- },
691
- });
692
- extra.signal.throwIfAborted();
693
- return result;
694
- }
695
- catch (error) {
696
- if (undeliveredProcessId !== undefined) {
697
- processSessions.discardUndelivered(workspaceId, 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;
698
890
  }
699
- throw error;
700
- }
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;
701
900
  });
702
901
  }
703
902
  if (config.toolMode !== "codex")
704
903
  return;
705
904
  registerAppTool(server, "write_stdin", {
706
905
  title: "Write to process",
707
- description: "Poll or write characters to a running process returned by bash or exec_command. Omit chars or pass an empty string to poll. Waiting never kills the process; pass \\u0003 to explicitly send Ctrl-C.",
906
+ description: "Poll or write characters to a running process returned by exec_command, or retrieve complete durable process output by outputId. Omit chars or pass an empty string to poll. Waiting never kills the process; pass \\u0003 to explicitly send Ctrl-C.",
708
907
  inputSchema: {
709
908
  workspaceId: z.string().describe("Workspace identifier used to start the process."),
710
909
  processId: z.number().int().positive().optional().describe("Canonical process identifier returned by bash or exec_command."),
711
910
  sessionId: z.number().int().positive().optional().describe("Deprecated alias for processId. Retained for compatibility."),
911
+ outputId: z.string().optional().describe("Stable output identifier returned by exec_command. When supplied, retrieve the complete durable output instead of controlling a process."),
712
912
  chars: z.string().optional().describe("Characters to write. Omit or pass an empty string to poll."),
713
913
  columns: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this width."),
714
914
  rows: z.number().int().min(1).max(1_000).optional().describe("Resize a PTY to this height."),
@@ -730,8 +930,21 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
730
930
  outputSchema: processOutputSchema(),
731
931
  ...toolWidgetDescriptorMeta(config, "shell"),
732
932
  annotations: SHELL_TOOL_ANNOTATIONS,
733
- }, async ({ workspaceId, processId, sessionId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
933
+ }, async ({ workspaceId, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
734
934
  const workspace = workspaces.getWorkspace(workspaceId);
935
+ if (outputId !== undefined) {
936
+ if (processId !== undefined || sessionId !== undefined || chars !== undefined || columns !== undefined ||
937
+ rows !== undefined || yieldTimeMs !== undefined || maxOutputTokens !== undefined) {
938
+ throw new Error("write_stdin outputId lookup cannot be combined with process control fields.");
939
+ }
940
+ return runToolWithHooks(hooks, {
941
+ signal: extra.signal,
942
+ tool: "write_stdin",
943
+ invocation: workspaceHookInvocation(workspace),
944
+ payload: { outputId },
945
+ operation: async () => durableOutputResponse("write_stdin", workspaceId, readWorkspaceBashOutput(bashOutputStore, workspaceId, outputId)),
946
+ });
947
+ }
735
948
  const resolvedProcessId = resolveProcessId(processId, sessionId);
736
949
  return runToolWithHooks(hooks, {
737
950
  signal: extra.signal,
@@ -764,20 +977,24 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
764
977
  success: snapshot.running || snapshot.exitCode === 0,
765
978
  durationMs: Math.round(performance.now() - startedAt),
766
979
  });
767
- return processToolResponse("write_stdin", workspaceId, snapshot, {
980
+ const response = processToolResponse("write_stdin", workspaceId, snapshot, {
768
981
  processId: resolvedProcessId,
769
982
  charactersWritten: chars?.length ?? 0,
770
983
  running: snapshot.running,
771
984
  exitCode: snapshot.exitCode,
772
985
  wallTimeMs: snapshot.wallTimeMs,
773
986
  });
987
+ if (!snapshot.running) {
988
+ recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId);
989
+ }
990
+ return response;
774
991
  },
775
992
  });
776
993
  });
777
994
  }
778
- export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence) {
995
+ export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore) {
779
996
  const toolDescriptions = buildToolDescriptions(config);
780
- const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result));
997
+ const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
781
998
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
782
999
  const artifactDownloadAvailable = config.artifactsEnabled && isArtifactDownloadSupportedPlatform();
783
1000
  const reviewChangesAvailable = config.widgets === "changes";
@@ -1287,7 +1504,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1287
1504
  }, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
1288
1505
  const workspace = workspaces.getWorkspace(workspaceId);
1289
1506
  let changedPaths = [];
1290
- return runToolWithHooks(hooks, {
1507
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, name, action, arguments: capabilityArguments, file }, {
1291
1508
  signal: extra.signal,
1292
1509
  tool: toolNames.capability,
1293
1510
  invocation: workspaceHookInvocation(workspace),
@@ -1510,7 +1727,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1510
1727
  annotations: { readOnlyHint: true },
1511
1728
  }, async ({ workspaceId, ...input }, extra) => {
1512
1729
  const workspace = workspaces.getWorkspace(workspaceId);
1513
- return runToolWithHooks(hooks, {
1730
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1514
1731
  signal: extra.signal,
1515
1732
  tool: toolNames.read,
1516
1733
  invocation: workspaceHookInvocation(workspace),
@@ -1597,7 +1814,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1597
1814
  annotations: WRITE_TOOL_ANNOTATIONS,
1598
1815
  }, async ({ workspaceId, ...input }, extra) => {
1599
1816
  const workspace = workspaces.getWorkspace(workspaceId);
1600
- return runToolWithHooks(hooks, {
1817
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1601
1818
  signal: extra.signal,
1602
1819
  tool: toolNames.write,
1603
1820
  invocation: workspaceHookInvocation(workspace),
@@ -1681,7 +1898,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1681
1898
  annotations: EDIT_TOOL_ANNOTATIONS,
1682
1899
  }, async ({ workspaceId, ...input }, extra) => {
1683
1900
  const workspace = workspaces.getWorkspace(workspaceId);
1684
- return runToolWithHooks(hooks, {
1901
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1685
1902
  signal: extra.signal,
1686
1903
  tool: toolNames.edit,
1687
1904
  invocation: workspaceHookInvocation(workspace),
@@ -1758,7 +1975,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1758
1975
  annotations: EDIT_TOOL_ANNOTATIONS,
1759
1976
  }, async ({ workspaceId, path, newPath }, extra) => {
1760
1977
  const workspace = workspaces.getWorkspace(workspaceId);
1761
- return runToolWithHooks(hooks, {
1978
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, newPath }, {
1762
1979
  signal: extra.signal,
1763
1980
  tool: toolNames.rename,
1764
1981
  invocation: workspaceHookInvocation(workspace),
@@ -1831,7 +2048,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1831
2048
  annotations: EDIT_TOOL_ANNOTATIONS,
1832
2049
  }, async ({ workspaceId, path, recursive }, extra) => {
1833
2050
  const workspace = workspaces.getWorkspace(workspaceId);
1834
- return runToolWithHooks(hooks, {
2051
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, recursive }, {
1835
2052
  signal: extra.signal,
1836
2053
  tool: toolNames.delete,
1837
2054
  invocation: workspaceHookInvocation(workspace),
@@ -1912,7 +2129,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1912
2129
  annotations: EDIT_TOOL_ANNOTATIONS,
1913
2130
  }, async ({ workspaceId, patch }, extra) => {
1914
2131
  const workspace = workspaces.getWorkspace(workspaceId);
1915
- return runToolWithHooks(hooks, {
2132
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, patch }, {
1916
2133
  signal: extra.signal,
1917
2134
  tool: "apply_patch",
1918
2135
  invocation: workspaceHookInvocation(workspace),
@@ -1971,9 +2188,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1971
2188
  .string()
1972
2189
  .describe("Workspace identifier returned by open_workspace."),
1973
2190
  action: z
1974
- .enum(["run", "process"])
2191
+ .enum(["run", "process", "output"])
1975
2192
  .optional()
1976
- .describe("Defaults to run. Use process with a returned processId to poll, interact, resize, or interrupt a running command."),
2193
+ .describe("Defaults to run. Use process with a returned processId to poll/interact, or output with outputId to retrieve complete durable output."),
1977
2194
  command: z
1978
2195
  .string()
1979
2196
  .optional()
@@ -1984,6 +2201,10 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1984
2201
  .positive()
1985
2202
  .optional()
1986
2203
  .describe("Process identifier returned by a previous bash action=run call. Required for action=process."),
2204
+ outputId: z
2205
+ .string()
2206
+ .optional()
2207
+ .describe("Stable output identifier returned by a Bash run. Required for action=output."),
1987
2208
  input: z
1988
2209
  .string()
1989
2210
  .optional()
@@ -2039,79 +2260,113 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2039
2260
  outputSchema: processOutputSchema(),
2040
2261
  ...toolWidgetDescriptorMeta(config, "shell"),
2041
2262
  annotations: SHELL_TOOL_ANNOTATIONS,
2042
- }, async ({ workspaceId, action = "run", command, processId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
2263
+ }, async ({ workspaceId, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
2043
2264
  const workspace = workspaces.getWorkspace(workspaceId);
2044
2265
  if (action === "run") {
2045
2266
  if (!command)
2046
2267
  throw new Error("bash action=run requires command.");
2047
- if (processId !== undefined || input !== undefined || interrupt !== undefined) {
2048
- throw new Error("bash action=run does not accept processId, input, or interrupt.");
2268
+ if (processId !== undefined || outputId !== undefined || input !== undefined || interrupt !== undefined) {
2269
+ throw new Error("bash action=run does not accept processId, outputId, input, or interrupt.");
2049
2270
  }
2050
2271
  let undeliveredProcessId;
2051
- try {
2052
- const result = await runToolWithHooks(hooks, {
2053
- signal: extra.signal,
2054
- tool: toolNames.shell,
2055
- invocation: workspaceHookInvocation(workspace),
2056
- payload: {
2057
- action,
2058
- command,
2059
- workingDirectory: workingDirectory ?? ".",
2060
- },
2061
- isFailure: toolResultIsError,
2062
- operation: async () => {
2063
- const startedAt = performance.now();
2064
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
2065
- await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
2066
- const snapshot = await processSessions.start({
2067
- workspaceId,
2068
- command,
2069
- cwd,
2070
- workspaceRoot: workspace.root,
2071
- tty,
2072
- columns,
2073
- rows,
2074
- yieldTimeMs,
2075
- timeoutMs,
2076
- maxOutputTokens,
2077
- signal: extra.signal,
2078
- });
2079
- undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
2080
- logToolCall(config, {
2081
- tool: toolNames.shell,
2082
- ...workspaceLogContext(workspace, extra.sessionId),
2083
- workingDirectory: workingDirectory ?? ".",
2084
- command,
2085
- commandLength: command.length,
2086
- exitCode: snapshot.exitCode,
2087
- running: snapshot.running,
2088
- processId: snapshot.processId,
2089
- success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
2090
- durationMs: Math.round(performance.now() - startedAt),
2091
- });
2092
- const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
2272
+ const activityResult = await runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.shell, {
2273
+ workspaceId,
2274
+ action,
2275
+ command,
2276
+ tty,
2277
+ columns,
2278
+ rows,
2279
+ workingDirectory,
2280
+ yieldTimeMs,
2281
+ timeoutMs,
2282
+ 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: {
2093
2290
  action,
2094
2291
  command,
2095
2292
  workingDirectory: workingDirectory ?? ".",
2096
- running: snapshot.running,
2097
- exitCode: snapshot.exitCode,
2098
- wallTimeMs: snapshot.wallTimeMs,
2099
- });
2100
- return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
2101
- ? { ...response, isError: true }
2102
- : response;
2103
- },
2104
- });
2105
- extra.signal.throwIfAborted();
2106
- return result;
2107
- }
2108
- catch (error) {
2109
- if (undeliveredProcessId !== undefined) {
2110
- processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
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;
2111
2341
  }
2112
- throw error;
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;
2351
+ }
2352
+ if (action === "output") {
2353
+ if (!outputId)
2354
+ throw new Error("bash action=output requires outputId.");
2355
+ if (command !== undefined || processId !== undefined || input !== undefined || interrupt !== undefined ||
2356
+ tty !== undefined || columns !== undefined || rows !== undefined || workingDirectory !== undefined ||
2357
+ yieldTimeMs !== undefined || timeoutMs !== undefined || maxOutputTokens !== undefined) {
2358
+ throw new Error("bash action=output accepts only workspaceId and outputId.");
2113
2359
  }
2360
+ return runToolWithHooks(hooks, {
2361
+ signal: extra.signal,
2362
+ tool: toolNames.shell,
2363
+ invocation: workspaceHookInvocation(workspace),
2364
+ payload: { action, outputId },
2365
+ operation: async () => durableOutputResponse(toolNames.shell, workspaceId, readWorkspaceBashOutput(bashOutputStore, workspaceId, outputId)),
2366
+ });
2114
2367
  }
2368
+ if (outputId !== undefined)
2369
+ throw new Error("bash action=process does not accept outputId.");
2115
2370
  if (command !== undefined || workingDirectory !== undefined || tty !== undefined || timeoutMs !== undefined) {
2116
2371
  throw new Error("bash action=process does not accept command, workingDirectory, tty, or timeoutMs.");
2117
2372
  }
@@ -2154,7 +2409,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2154
2409
  success: snapshot.running || snapshot.exitCode === 0,
2155
2410
  durationMs: Math.round(performance.now() - startedAt),
2156
2411
  });
2157
- return processToolResponse(toolNames.shell, workspaceId, snapshot, {
2412
+ const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
2158
2413
  action,
2159
2414
  processId,
2160
2415
  inputLength: input?.length ?? 0,
@@ -2163,11 +2418,15 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2163
2418
  exitCode: snapshot.exitCode,
2164
2419
  wallTimeMs: snapshot.wallTimeMs,
2165
2420
  });
2421
+ if (!snapshot.running) {
2422
+ recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId);
2423
+ }
2424
+ return response;
2166
2425
  },
2167
2426
  });
2168
2427
  });
2169
2428
  }
2170
- registerProcessTools(server, config, workspaces, processSessions, hooks);
2429
+ registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle, bashOutputStore);
2171
2430
  return server;
2172
2431
  }
2173
2432
  export function createServer(config = loadConfig(), options = {}) {
@@ -2193,8 +2452,11 @@ export function createServer(config = loadConfig(), options = {}) {
2193
2452
  });
2194
2453
  const workspaceStore = createWorkspaceStore(config.stateDir);
2195
2454
  const workspaces = new WorkspaceRegistry(config, workspaceStore);
2455
+ const activityAuditStore = new ActivityAuditStore(config.stateDir);
2456
+ const activityLifecycle = new ActivityLifecycle(activityAuditStore);
2457
+ const bashOutputStore = new BashOutputStore(config.stateDir);
2196
2458
  const reviewCheckpoints = createReviewCheckpointManager();
2197
- const processSessions = new ProcessManager();
2459
+ const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
2198
2460
  const codeIntelligence = new CodeIntelligenceManager(config);
2199
2461
  const localAgentProviders = config.subagents
2200
2462
  ? getLocalAgentProviderAvailabilitySnapshot()
@@ -2379,7 +2641,7 @@ export function createServer(config = loadConfig(), options = {}) {
2379
2641
  });
2380
2642
  }
2381
2643
  };
2382
- const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence);
2644
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore);
2383
2645
  await server.connect(transport);
2384
2646
  }
2385
2647
  else {
@@ -2411,6 +2673,8 @@ export function createServer(config = loadConfig(), options = {}) {
2411
2673
  processSessions.shutdown();
2412
2674
  await codeIntelligence.shutdown();
2413
2675
  oauthProvider.close();
2676
+ bashOutputStore.close();
2677
+ activityAuditStore.close();
2414
2678
  workspaceStore.close?.();
2415
2679
  })();
2416
2680
  return closePromise;