@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/CHANGELOG.md +21 -0
- package/dist/activity/audit-store.js +11 -0
- package/dist/activity/bash-output-store.js +127 -0
- package/dist/activity/lifecycle.js +138 -0
- package/dist/activity/process-output-audit.js +1 -0
- package/dist/db/migrations.js +48 -0
- package/dist/db/schema.js +36 -1
- package/dist/lsp/test-support/server-fixture.js +10 -2
- package/dist/process-sessions.js +50 -8
- package/dist/server.js +404 -140
- package/package.json +2 -2
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
|
-
|
|
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
|
|
487
|
-
return
|
|
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(
|
|
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
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
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
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|