@akira-tl/forgerelay 0.5.0 → 0.5.1

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 CHANGED
@@ -4,6 +4,16 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.1] - 2026-08-14
8
+
9
+ ### Added
10
+
11
+ - Routed ForgeRelay's top-level work operations through one persistent Activity lifecycle: read, write, edit, rename, delete, capability, Bash, and Codex-compatible execution/patch operations now record durable started/succeeded/failed/blocked/returned facts without relying on UI inference. Bash/exec process-control follow-ups remain part of the existing semantic operation instead of creating duplicate top-level Activities.
12
+
13
+ ### Fixed
14
+
15
+ - Activity auditing now treats a shell process as `returned` only after its `processId` can actually be delivered to the Host; Host cancellation during post-tool delivery protection records a failed Activity and discards the undelivered process instead of leaving a false returned history entry.
16
+
7
17
  ## [0.5.0] - 2026-08-14
8
18
 
9
19
  ### Added
@@ -66,6 +66,11 @@ export class ActivityAuditStore {
66
66
  result = event.result;
67
67
  error = undefined;
68
68
  break;
69
+ case "returned":
70
+ state = "returned";
71
+ result = event.result;
72
+ error = undefined;
73
+ break;
69
74
  case "failed":
70
75
  state = "failed";
71
76
  result = event.result;
@@ -177,6 +182,12 @@ function rowToEvent(row) {
177
182
  type: "succeeded",
178
183
  result: parseJson(row.result_json),
179
184
  };
185
+ case "returned":
186
+ return {
187
+ ...base,
188
+ type: "returned",
189
+ result: parseJson(row.result_json),
190
+ };
180
191
  case "failed":
181
192
  if (!row.error)
182
193
  throw new Error(`Activity audit failed event ${row.id} is missing an error.`);
@@ -0,0 +1,111 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { HookExecutionError } from "../hooks.js";
3
+ export class ActivityLifecycle {
4
+ auditStore;
5
+ activityId;
6
+ turnId;
7
+ constructor(auditStore, options = {}) {
8
+ this.auditStore = auditStore;
9
+ this.activityId = options.activityId ?? newActivityId;
10
+ this.turnId = options.turnId ?? newTurnId;
11
+ }
12
+ async run(options) {
13
+ const activityId = options.activityId ?? this.activityId();
14
+ const turnId = options.turnId ?? this.turnId();
15
+ const request = normalizeAuditValue(options.request);
16
+ this.auditStore.append({
17
+ type: "started",
18
+ activityId,
19
+ turnId,
20
+ ...(options.conversationScopeId ? { conversationScopeId: options.conversationScopeId } : {}),
21
+ tool: options.tool,
22
+ workspace: options.workspace,
23
+ ...(request !== undefined ? { request } : {}),
24
+ });
25
+ try {
26
+ const result = await options.operation();
27
+ const normalizedResult = normalizeAuditValue(result);
28
+ const outcome = options.outcome?.(result) ?? { type: "succeeded" };
29
+ switch (outcome.type) {
30
+ case "succeeded":
31
+ case "returned":
32
+ this.auditStore.append({
33
+ type: outcome.type,
34
+ activityId,
35
+ ...(normalizedResult !== undefined ? { result: normalizedResult } : {}),
36
+ });
37
+ break;
38
+ case "failed":
39
+ this.auditStore.append({
40
+ type: "failed",
41
+ activityId,
42
+ ...(normalizedResult !== undefined ? { result: normalizedResult } : {}),
43
+ error: outcome.error,
44
+ });
45
+ break;
46
+ }
47
+ return result;
48
+ }
49
+ catch (error) {
50
+ const message = error instanceof Error ? error.message : String(error);
51
+ this.auditStore.append(error instanceof HookExecutionError && error.event === "BeforeTool"
52
+ ? { type: "blocked", activityId, error: message }
53
+ : { type: "failed", activityId, error: message });
54
+ throw error;
55
+ }
56
+ }
57
+ }
58
+ function newActivityId() {
59
+ return `act_${randomUUID().replaceAll("-", "")}`;
60
+ }
61
+ function newTurnId() {
62
+ return `turn_${randomUUID().replaceAll("-", "")}`;
63
+ }
64
+ export function normalizeAuditValue(value) {
65
+ return normalizeAuditValueInternal(value, new WeakSet());
66
+ }
67
+ function normalizeAuditValueInternal(value, seen) {
68
+ if (value === undefined || typeof value === "function" || typeof value === "symbol")
69
+ return undefined;
70
+ if (value === null || typeof value === "string" || typeof value === "boolean")
71
+ return value;
72
+ if (typeof value === "number")
73
+ return Number.isFinite(value) ? value : String(value);
74
+ if (typeof value === "bigint")
75
+ return value.toString();
76
+ if (value instanceof Date)
77
+ return value.toISOString();
78
+ if (value instanceof Error) {
79
+ return {
80
+ name: value.name,
81
+ message: value.message,
82
+ };
83
+ }
84
+ if (value instanceof Uint8Array) {
85
+ return {
86
+ type: "bytes",
87
+ encoding: "base64",
88
+ data: Buffer.from(value).toString("base64"),
89
+ };
90
+ }
91
+ if (Array.isArray(value)) {
92
+ return value.map((entry) => normalizeAuditValueInternal(entry, seen) ?? null);
93
+ }
94
+ if (typeof value !== "object")
95
+ return String(value);
96
+ if (seen.has(value))
97
+ return "[Circular]";
98
+ seen.add(value);
99
+ try {
100
+ const normalized = {};
101
+ for (const [key, entry] of Object.entries(value)) {
102
+ const next = normalizeAuditValueInternal(entry, seen);
103
+ if (next !== undefined)
104
+ normalized[key] = next;
105
+ }
106
+ return normalized;
107
+ }
108
+ finally {
109
+ seen.delete(value);
110
+ }
111
+ }
@@ -4,6 +4,8 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
6
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
7
+ import { ActivityAuditStore } from "../../activity/audit-store.js";
8
+ import { ActivityLifecycle } from "../../activity/lifecycle.js";
7
9
  import { loadConfig } from "../../config.js";
8
10
  import { createReviewCheckpointManager } from "../../review-checkpoints.js";
9
11
  import { ProcessManager } from "../../process-sessions.js";
@@ -33,8 +35,10 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
33
35
  const store = new SqliteWorkspaceStore(stateDir);
34
36
  const workspaces = new WorkspaceRegistry(config, store);
35
37
  const processSessions = new ProcessManager();
38
+ const auditStore = new ActivityAuditStore(stateDir);
39
+ const activityLifecycle = new ActivityLifecycle(auditStore);
36
40
  const codeIntelligence = new CodeIntelligenceManager(config, options.codeIntelligenceOptions);
37
- const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence);
41
+ const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle);
38
42
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
39
43
  const client = new Client({ name: "forgerelay-code-intelligence-test-client", version: "1.0.0" });
40
44
  await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
@@ -47,6 +51,7 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
47
51
  await server.close();
48
52
  await codeIntelligence.shutdown();
49
53
  processSessions.shutdown();
54
+ auditStore.close();
50
55
  store.close();
51
56
  };
52
57
  t.after(async () => {
package/dist/server.js CHANGED
@@ -15,6 +15,8 @@ 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 { ActivityLifecycle } from "./activity/lifecycle.js";
18
20
  import { buildCapabilityFingerprint } from "./capabilities.js";
19
21
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
20
22
  import { deletePath, renamePath } from "./file-mutations.js";
@@ -597,7 +599,77 @@ async function reviewWorkspaceChanges(reviewCheckpoints, workspace) {
597
599
  function toolResultIsError(result) {
598
600
  return typeof result === "object" && result !== null && result.isError === true;
599
601
  }
600
- function registerProcessTools(server, config, workspaces, processSessions, hooks) {
602
+ function workspaceActivitySnapshot(workspace) {
603
+ return {
604
+ id: workspace.id,
605
+ root: workspace.root,
606
+ mode: workspace.mode,
607
+ ...(workspace.sourceRoot ? { sourceRoot: workspace.sourceRoot } : {}),
608
+ ...(workspace.worktree?.branch ? { branch: workspace.worktree.branch } : {}),
609
+ ...(workspace.worktree?.targetBranch ? { targetBranch: workspace.worktree.targetBranch } : {}),
610
+ };
611
+ }
612
+ function activityFailureMessage(result) {
613
+ if (typeof result !== "object" || result === null)
614
+ return "Tool returned a failed result.";
615
+ const record = result;
616
+ if (Array.isArray(record.content)) {
617
+ const text = record.content
618
+ .map((entry) => {
619
+ if (typeof entry !== "object" || entry === null)
620
+ return "";
621
+ const value = entry.text;
622
+ return typeof value === "string" ? value : "";
623
+ })
624
+ .filter(Boolean)
625
+ .join("\n");
626
+ if (text)
627
+ return text;
628
+ }
629
+ if (typeof record.structuredContent === "object" && record.structuredContent !== null) {
630
+ const value = record.structuredContent.result;
631
+ if (typeof value === "string" && value)
632
+ return value;
633
+ }
634
+ return "Tool returned a failed result.";
635
+ }
636
+ function standardActivityOutcome(result) {
637
+ return toolResultIsError(result)
638
+ ? { type: "failed", error: activityFailureMessage(result) }
639
+ : { type: "succeeded" };
640
+ }
641
+ function processActivityOutcome(result) {
642
+ if (toolResultIsError(result))
643
+ return { type: "failed", error: activityFailureMessage(result) };
644
+ if (typeof result !== "object" || result === null)
645
+ return { type: "succeeded" };
646
+ const structured = result.structuredContent;
647
+ if (typeof structured !== "object" || structured === null)
648
+ return { type: "succeeded" };
649
+ const process = structured;
650
+ if (process.running === true)
651
+ return { type: "returned" };
652
+ if (process.timedOut === true ||
653
+ typeof process.signal === "string" ||
654
+ (typeof process.exitCode === "number" && process.exitCode !== 0)) {
655
+ return { type: "failed", error: activityFailureMessage(result) };
656
+ }
657
+ return { type: "succeeded" };
658
+ }
659
+ function runActivityTool(lifecycle, workspace, requestMeta, tool, request, operation, outcome = standardActivityOutcome) {
660
+ return lifecycle.run({
661
+ tool,
662
+ workspace: workspaceActivitySnapshot(workspace),
663
+ conversationScopeId: openAiConversationScopeId(requestMeta),
664
+ request,
665
+ operation,
666
+ outcome,
667
+ });
668
+ }
669
+ function runActivityToolWithHooks(lifecycle, hooks, workspace, requestMeta, request, hookOptions) {
670
+ return runActivityTool(lifecycle, workspace, requestMeta, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions));
671
+ }
672
+ function registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle) {
601
673
  if (config.toolMode === "codex") {
602
674
  registerAppTool(server, "exec_command", {
603
675
  title: "Execute command",
@@ -643,61 +715,63 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
643
715
  }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
644
716
  const workspace = workspaces.getWorkspace(workspaceId);
645
717
  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);
718
+ return runActivityTool(activityLifecycle, workspace, extra._meta, "exec_command", { workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, async () => {
719
+ try {
720
+ const result = await runToolWithHooks(hooks, {
721
+ signal: extra.signal,
722
+ tool: "exec_command",
723
+ invocation: workspaceHookInvocation(workspace),
724
+ payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
725
+ operation: async () => {
726
+ const startedAt = performance.now();
727
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
728
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
729
+ const snapshot = await processSessions.start({
730
+ workspaceId,
731
+ command: cmd,
732
+ cwd,
733
+ workspaceRoot: workspace.root,
734
+ tty,
735
+ columns,
736
+ rows,
737
+ yieldTimeMs,
738
+ timeoutMs,
739
+ maxOutputTokens,
740
+ codexCi: true,
741
+ signal: extra.signal,
742
+ });
743
+ undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
744
+ logToolCall(config, {
745
+ tool: "exec_command",
746
+ ...workspaceLogContext(workspace, extra.sessionId),
747
+ workingDirectory: workingDirectory ?? ".",
748
+ command: cmd,
749
+ commandLength: cmd.length,
750
+ exitCode: snapshot.exitCode,
751
+ running: snapshot.running,
752
+ processId: snapshot.processId,
753
+ success: snapshot.running || snapshot.exitCode === 0,
754
+ durationMs: Math.round(performance.now() - startedAt),
755
+ });
756
+ return processToolResponse("exec_command", workspaceId, snapshot, {
757
+ command: cmd,
758
+ workingDirectory: workingDirectory ?? ".",
759
+ running: snapshot.running,
760
+ exitCode: snapshot.exitCode,
761
+ wallTimeMs: snapshot.wallTimeMs,
762
+ });
763
+ },
764
+ });
765
+ extra.signal.throwIfAborted();
766
+ return result;
698
767
  }
699
- throw error;
700
- }
768
+ catch (error) {
769
+ if (undeliveredProcessId !== undefined) {
770
+ processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
771
+ }
772
+ throw error;
773
+ }
774
+ }, processActivityOutcome);
701
775
  });
702
776
  }
703
777
  if (config.toolMode !== "codex")
@@ -775,7 +849,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
775
849
  });
776
850
  });
777
851
  }
778
- export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence) {
852
+ export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle) {
779
853
  const toolDescriptions = buildToolDescriptions(config);
780
854
  const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result));
781
855
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
@@ -1287,7 +1361,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1287
1361
  }, async ({ workspaceId, name, action, arguments: capabilityArguments, file }, extra) => {
1288
1362
  const workspace = workspaces.getWorkspace(workspaceId);
1289
1363
  let changedPaths = [];
1290
- return runToolWithHooks(hooks, {
1364
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, name, action, arguments: capabilityArguments, file }, {
1291
1365
  signal: extra.signal,
1292
1366
  tool: toolNames.capability,
1293
1367
  invocation: workspaceHookInvocation(workspace),
@@ -1510,7 +1584,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1510
1584
  annotations: { readOnlyHint: true },
1511
1585
  }, async ({ workspaceId, ...input }, extra) => {
1512
1586
  const workspace = workspaces.getWorkspace(workspaceId);
1513
- return runToolWithHooks(hooks, {
1587
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1514
1588
  signal: extra.signal,
1515
1589
  tool: toolNames.read,
1516
1590
  invocation: workspaceHookInvocation(workspace),
@@ -1597,7 +1671,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1597
1671
  annotations: WRITE_TOOL_ANNOTATIONS,
1598
1672
  }, async ({ workspaceId, ...input }, extra) => {
1599
1673
  const workspace = workspaces.getWorkspace(workspaceId);
1600
- return runToolWithHooks(hooks, {
1674
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1601
1675
  signal: extra.signal,
1602
1676
  tool: toolNames.write,
1603
1677
  invocation: workspaceHookInvocation(workspace),
@@ -1681,7 +1755,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1681
1755
  annotations: EDIT_TOOL_ANNOTATIONS,
1682
1756
  }, async ({ workspaceId, ...input }, extra) => {
1683
1757
  const workspace = workspaces.getWorkspace(workspaceId);
1684
- return runToolWithHooks(hooks, {
1758
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, ...input }, {
1685
1759
  signal: extra.signal,
1686
1760
  tool: toolNames.edit,
1687
1761
  invocation: workspaceHookInvocation(workspace),
@@ -1758,7 +1832,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1758
1832
  annotations: EDIT_TOOL_ANNOTATIONS,
1759
1833
  }, async ({ workspaceId, path, newPath }, extra) => {
1760
1834
  const workspace = workspaces.getWorkspace(workspaceId);
1761
- return runToolWithHooks(hooks, {
1835
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, newPath }, {
1762
1836
  signal: extra.signal,
1763
1837
  tool: toolNames.rename,
1764
1838
  invocation: workspaceHookInvocation(workspace),
@@ -1831,7 +1905,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1831
1905
  annotations: EDIT_TOOL_ANNOTATIONS,
1832
1906
  }, async ({ workspaceId, path, recursive }, extra) => {
1833
1907
  const workspace = workspaces.getWorkspace(workspaceId);
1834
- return runToolWithHooks(hooks, {
1908
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, path, recursive }, {
1835
1909
  signal: extra.signal,
1836
1910
  tool: toolNames.delete,
1837
1911
  invocation: workspaceHookInvocation(workspace),
@@ -1912,7 +1986,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1912
1986
  annotations: EDIT_TOOL_ANNOTATIONS,
1913
1987
  }, async ({ workspaceId, patch }, extra) => {
1914
1988
  const workspace = workspaces.getWorkspace(workspaceId);
1915
- return runToolWithHooks(hooks, {
1989
+ return runActivityToolWithHooks(activityLifecycle, hooks, workspace, extra._meta, { workspaceId, patch }, {
1916
1990
  signal: extra.signal,
1917
1991
  tool: "apply_patch",
1918
1992
  invocation: workspaceHookInvocation(workspace),
@@ -2048,69 +2122,82 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2048
2122
  throw new Error("bash action=run does not accept processId, input, or interrupt.");
2049
2123
  }
2050
2124
  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, {
2125
+ return runActivityTool(activityLifecycle, workspace, extra._meta, toolNames.shell, {
2126
+ workspaceId,
2127
+ action,
2128
+ command,
2129
+ tty,
2130
+ columns,
2131
+ rows,
2132
+ workingDirectory,
2133
+ yieldTimeMs,
2134
+ timeoutMs,
2135
+ maxOutputTokens,
2136
+ }, async () => {
2137
+ try {
2138
+ const result = await runToolWithHooks(hooks, {
2139
+ signal: extra.signal,
2140
+ tool: toolNames.shell,
2141
+ invocation: workspaceHookInvocation(workspace),
2142
+ payload: {
2093
2143
  action,
2094
2144
  command,
2095
2145
  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);
2146
+ },
2147
+ isFailure: toolResultIsError,
2148
+ operation: async () => {
2149
+ const startedAt = performance.now();
2150
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
2151
+ await assertWorkspaceInstructionsLoadedBeforeSideEffect(workspaces, workspace, [cwd]);
2152
+ const snapshot = await processSessions.start({
2153
+ workspaceId,
2154
+ command,
2155
+ cwd,
2156
+ workspaceRoot: workspace.root,
2157
+ tty,
2158
+ columns,
2159
+ rows,
2160
+ yieldTimeMs,
2161
+ timeoutMs,
2162
+ maxOutputTokens,
2163
+ signal: extra.signal,
2164
+ });
2165
+ undeliveredProcessId = snapshot.running ? snapshot.processId : undefined;
2166
+ logToolCall(config, {
2167
+ tool: toolNames.shell,
2168
+ ...workspaceLogContext(workspace, extra.sessionId),
2169
+ workingDirectory: workingDirectory ?? ".",
2170
+ command,
2171
+ commandLength: command.length,
2172
+ exitCode: snapshot.exitCode,
2173
+ running: snapshot.running,
2174
+ processId: snapshot.processId,
2175
+ success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
2176
+ durationMs: Math.round(performance.now() - startedAt),
2177
+ });
2178
+ const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
2179
+ action,
2180
+ command,
2181
+ workingDirectory: workingDirectory ?? ".",
2182
+ running: snapshot.running,
2183
+ exitCode: snapshot.exitCode,
2184
+ wallTimeMs: snapshot.wallTimeMs,
2185
+ });
2186
+ return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
2187
+ ? { ...response, isError: true }
2188
+ : response;
2189
+ },
2190
+ });
2191
+ extra.signal.throwIfAborted();
2192
+ return result;
2111
2193
  }
2112
- throw error;
2113
- }
2194
+ catch (error) {
2195
+ if (undeliveredProcessId !== undefined) {
2196
+ processSessions.discardUndelivered(workspaceId, undeliveredProcessId);
2197
+ }
2198
+ throw error;
2199
+ }
2200
+ }, processActivityOutcome);
2114
2201
  }
2115
2202
  if (command !== undefined || workingDirectory !== undefined || tty !== undefined || timeoutMs !== undefined) {
2116
2203
  throw new Error("bash action=process does not accept command, workingDirectory, tty, or timeoutMs.");
@@ -2167,7 +2254,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
2167
2254
  });
2168
2255
  });
2169
2256
  }
2170
- registerProcessTools(server, config, workspaces, processSessions, hooks);
2257
+ registerProcessTools(server, config, workspaces, processSessions, hooks, activityLifecycle);
2171
2258
  return server;
2172
2259
  }
2173
2260
  export function createServer(config = loadConfig(), options = {}) {
@@ -2193,6 +2280,8 @@ export function createServer(config = loadConfig(), options = {}) {
2193
2280
  });
2194
2281
  const workspaceStore = createWorkspaceStore(config.stateDir);
2195
2282
  const workspaces = new WorkspaceRegistry(config, workspaceStore);
2283
+ const activityAuditStore = new ActivityAuditStore(config.stateDir);
2284
+ const activityLifecycle = new ActivityLifecycle(activityAuditStore);
2196
2285
  const reviewCheckpoints = createReviewCheckpointManager();
2197
2286
  const processSessions = new ProcessManager();
2198
2287
  const codeIntelligence = new CodeIntelligenceManager(config);
@@ -2379,7 +2468,7 @@ export function createServer(config = loadConfig(), options = {}) {
2379
2468
  });
2380
2469
  }
2381
2470
  };
2382
- const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence);
2471
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle);
2383
2472
  await server.connect(transport);
2384
2473
  }
2385
2474
  else {
@@ -2411,6 +2500,7 @@ export function createServer(config = loadConfig(), options = {}) {
2411
2500
  processSessions.shutdown();
2412
2501
  await codeIntelligence.shutdown();
2413
2502
  oauthProvider.close();
2503
+ activityAuditStore.close();
2414
2504
  workspaceStore.close?.();
2415
2505
  })();
2416
2506
  return closePromise;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -44,7 +44,7 @@
44
44
  "release:parity": "node scripts/release-parity.mjs",
45
45
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
46
46
  "start": "node dist/cli.js serve",
47
- "test": "node --test scripts/release-proof.test.mjs && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/activity/audit-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
47
+ "test": "node --test scripts/release-proof.test.mjs && tsx src/config.test.ts && tsx src/lsp/language-server-config.test.ts && tsx src/lsp/normalization/hover.test.ts && tsx src/lsp/references.server.test.ts && tsx src/lsp/operations/document-symbols.server.test.ts && tsx src/lsp/operations/workspace-symbols.server.test.ts && tsx src/lsp/operations/diagnostics-push.server.test.ts && tsx src/lsp/operations/diagnostics-pull.server.test.ts && tsx src/lsp/operations/request-hardening.server.test.ts && tsx src/lsp/operations/recovery.server.test.ts && tsx src/lsp/operations/lifecycle.server.test.ts && tsx src/lsp/runtime/semantic-requests.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/activity/audit-store.test.ts && tsx src/activity/lifecycle.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/lsp/code-intelligence.server.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
48
48
  "typecheck": "tsc -p tsconfig.json --noEmit",
49
49
  "release:check": "node scripts/release-version.mjs check",
50
50
  "release:tag-check": "node scripts/release-version.mjs tag",