@akira-tl/forgerelay 0.5.1 → 0.5.4

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.
@@ -0,0 +1,247 @@
1
+ export class ActivityQueryService {
2
+ turns;
3
+ audit;
4
+ outputs;
5
+ constructor(turns, audit, outputs) {
6
+ this.turns = turns;
7
+ this.audit = audit;
8
+ this.outputs = outputs;
9
+ }
10
+ beginTurn(conversationScopeId) {
11
+ const turn = this.turns.begin(conversationScopeId);
12
+ return this.snapshot(turn.turnId);
13
+ }
14
+ currentTurnId(conversationScopeId) {
15
+ return this.turns.current(conversationScopeId)?.turnId;
16
+ }
17
+ snapshot(turnId, knownRevision) {
18
+ this.requireTurn(turnId);
19
+ const revision = this.audit.turnRevision(turnId);
20
+ const activities = this.audit.listActivitiesByTurn(turnId).map(toSummary);
21
+ const state = aggregateState(activities);
22
+ const changed = knownRevision === undefined || knownRevision !== revision;
23
+ return {
24
+ turnId,
25
+ revision,
26
+ changed,
27
+ state,
28
+ activities: changed ? activities : [],
29
+ };
30
+ }
31
+ detail(turnId, activityId) {
32
+ this.requireTurn(turnId);
33
+ const record = this.audit.getActivity(activityId);
34
+ if (!record || record.turnId !== turnId) {
35
+ throw new Error(`Unknown Activity ${activityId} in Host Turn ${turnId}.`);
36
+ }
37
+ const activity = toSummary(record);
38
+ if (!activity.detailAvailable) {
39
+ throw new Error(`Activity ${activityId} is summary-complete and has no lazy detail.`);
40
+ }
41
+ return {
42
+ activity,
43
+ ...(record.request !== undefined ? { request: record.request } : {}),
44
+ ...(record.result !== undefined ? { result: record.result } : {}),
45
+ ...(record.error !== undefined ? { error: record.error } : {}),
46
+ };
47
+ }
48
+ bashOutput(turnId, outputId) {
49
+ this.requireTurn(turnId);
50
+ const output = this.outputs.read(outputId);
51
+ if (!output)
52
+ throw new Error(`Unknown Bash output: ${outputId}.`);
53
+ const activities = this.audit.listActivitiesByTurn(turnId);
54
+ const visible = activities.some((activity) => activity.activityId === output.activityId || activityOutputId(activity) === outputId);
55
+ if (!visible) {
56
+ throw new Error(`Bash output ${outputId} is not part of Host Turn ${turnId}.`);
57
+ }
58
+ return {
59
+ outputId: output.outputId,
60
+ activityId: output.activityId,
61
+ processId: output.processId,
62
+ command: output.command,
63
+ output: output.output,
64
+ status: output.status,
65
+ ...(output.exitCode !== undefined ? { exitCode: output.exitCode } : {}),
66
+ ...(output.signal !== undefined ? { signal: output.signal } : {}),
67
+ timedOut: output.timedOut,
68
+ startedAt: output.startedAt,
69
+ ...(output.finishedAt !== undefined ? { finishedAt: output.finishedAt } : {}),
70
+ };
71
+ }
72
+ requireTurn(turnId) {
73
+ if (!this.turns.get(turnId))
74
+ throw new Error(`Unknown Host Turn: ${turnId}.`);
75
+ }
76
+ }
77
+ function toSummary(record) {
78
+ const request = asRecord(record.request);
79
+ const result = asRecord(record.result);
80
+ const structured = asRecord(result?.structuredContent);
81
+ const directProcessId = numberField(result, "processId") ?? numberField(request, "processId");
82
+ const processId = numberField(structured, "processId") ?? directProcessId;
83
+ const outputId = stringField(structured, "outputId")
84
+ ?? stringField(result, "outputId")
85
+ ?? stringField(request, "outputId");
86
+ const command = stringField(request, "command") ?? stringField(request, "cmd");
87
+ const durationMs = numberField(structured, "wallTimeMs")
88
+ ?? numberField(result, "wallTimeMs")
89
+ ?? elapsedMs(record.startedAt, record.updatedAt, record.state);
90
+ const bashLike = record.tool === "bash" || record.tool === "exec_command" || record.tool === "bash_result";
91
+ return {
92
+ activityId: record.activityId,
93
+ tool: record.tool,
94
+ kind: activityKind(record.tool),
95
+ status: activityStatus(record.state),
96
+ state: record.state,
97
+ title: activityTitle(record.tool),
98
+ target: activityTarget(record, request, result, structured),
99
+ detailAvailable: record.tool !== "rename" && record.tool !== "delete",
100
+ ...(record.workspace.id ? { workspaceId: record.workspace.id } : {}),
101
+ ...(processId !== undefined ? { processId } : {}),
102
+ ...(outputId !== undefined ? { outputId } : {}),
103
+ ...(bashLike && command !== undefined ? { commandLength: command.length } : {}),
104
+ ...(bashLike ? { bashPhase: bashPhase(record.state) } : {}),
105
+ startedAt: record.startedAt,
106
+ ...(record.state !== "executing" ? { finishedAt: record.updatedAt } : {}),
107
+ ...(durationMs !== undefined ? { durationMs } : {}),
108
+ };
109
+ }
110
+ function activityKind(tool) {
111
+ if (tool === "read")
112
+ return "read";
113
+ if (tool === "write")
114
+ return "write";
115
+ if (tool === "edit" || tool === "apply_patch")
116
+ return "edit";
117
+ if (tool === "rename")
118
+ return "rename";
119
+ if (tool === "delete")
120
+ return "delete";
121
+ if (tool === "bash_result")
122
+ return "shell-result";
123
+ if (tool === "bash" || tool === "exec_command")
124
+ return "shell";
125
+ if (tool === "capability")
126
+ return "capability";
127
+ return "tool";
128
+ }
129
+ function activityTitle(tool) {
130
+ const titles = {
131
+ read: "Read",
132
+ write: "Write",
133
+ edit: "Edit",
134
+ apply_patch: "Edit",
135
+ rename: "Rename",
136
+ delete: "Delete",
137
+ bash: "Bash",
138
+ exec_command: "Command",
139
+ bash_result: "Bash result",
140
+ capability: "Capability",
141
+ };
142
+ return titles[tool] ?? tool;
143
+ }
144
+ function activityTarget(record, request, result, structured) {
145
+ if (record.tool === "bash" || record.tool === "exec_command")
146
+ return "Shell command";
147
+ if (record.tool === "bash_result") {
148
+ const processId = numberField(result, "processId") ?? numberField(request, "processId");
149
+ const exitCode = numberField(result, "exitCode");
150
+ const signal = stringField(result, "signal");
151
+ const timedOut = booleanField(result, "timedOut");
152
+ const outcome = timedOut
153
+ ? "timed out"
154
+ : signal
155
+ ? `signal ${signal}`
156
+ : exitCode !== undefined
157
+ ? `exit ${exitCode}`
158
+ : record.state === "failed"
159
+ ? "failed"
160
+ : "completed";
161
+ return `Process ${processId ?? "?"} · ${outcome}`;
162
+ }
163
+ if (record.tool === "capability") {
164
+ const name = stringField(request, "name") ?? "capability";
165
+ const action = stringField(request, "action") ?? "run";
166
+ return `${name} · ${action}`;
167
+ }
168
+ if (record.tool === "rename") {
169
+ const from = stringField(request, "path")
170
+ ?? stringField(request, "from")
171
+ ?? stringField(request, "source");
172
+ const to = stringField(request, "newPath")
173
+ ?? stringField(request, "to")
174
+ ?? stringField(request, "destination");
175
+ return [from, to].filter((value) => Boolean(value)).join(" → ") || "path";
176
+ }
177
+ const path = stringField(request, "path");
178
+ if (path)
179
+ return path;
180
+ if (record.tool === "apply_patch") {
181
+ const files = arrayField(structured, "files");
182
+ const first = asRecord(files?.[0]);
183
+ const firstPath = stringField(first, "path");
184
+ if (firstPath)
185
+ return files && files.length > 1 ? `${firstPath} +${files.length - 1}` : firstPath;
186
+ }
187
+ return record.tool;
188
+ }
189
+ function activityStatus(state) {
190
+ if (state === "executing")
191
+ return "working";
192
+ if (state === "failed" || state === "blocked")
193
+ return "error";
194
+ return "done";
195
+ }
196
+ function bashPhase(state) {
197
+ if (state === "executing")
198
+ return "executing";
199
+ if (state === "returned")
200
+ return "returned";
201
+ if (state === "failed" || state === "blocked")
202
+ return "error";
203
+ return "done";
204
+ }
205
+ function aggregateState(activities) {
206
+ if (activities.length === 0)
207
+ return "working";
208
+ if (activities.some((activity) => activity.status === "working"))
209
+ return "working";
210
+ if (activities.some((activity) => activity.status === "error"))
211
+ return "error";
212
+ return "done";
213
+ }
214
+ function activityOutputId(activity) {
215
+ const request = asRecord(activity.request);
216
+ const result = asRecord(activity.result);
217
+ const structured = asRecord(result?.structuredContent);
218
+ return stringField(request, "outputId")
219
+ ?? stringField(result, "outputId")
220
+ ?? stringField(structured, "outputId");
221
+ }
222
+ function asRecord(value) {
223
+ return typeof value === "object" && value !== null && !Array.isArray(value)
224
+ ? value
225
+ : undefined;
226
+ }
227
+ function stringField(record, key) {
228
+ return typeof record?.[key] === "string" ? record[key] : undefined;
229
+ }
230
+ function numberField(record, key) {
231
+ return typeof record?.[key] === "number" ? record[key] : undefined;
232
+ }
233
+ function booleanField(record, key) {
234
+ return typeof record?.[key] === "boolean" ? record[key] : undefined;
235
+ }
236
+ function arrayField(record, key) {
237
+ return Array.isArray(record?.[key]) ? record[key] : undefined;
238
+ }
239
+ function elapsedMs(startedAt, updatedAt, state) {
240
+ if (state === "executing")
241
+ return undefined;
242
+ const start = Date.parse(startedAt);
243
+ const end = Date.parse(updatedAt);
244
+ if (!Number.isFinite(start) || !Number.isFinite(end))
245
+ return undefined;
246
+ return Math.max(0, end - start);
247
+ }
@@ -39,6 +39,16 @@ const migrations = [
39
39
  name: "activity-audit",
40
40
  up: migrateActivityAudit,
41
41
  },
42
+ {
43
+ version: 9,
44
+ name: "bash-output-audit",
45
+ up: migrateBashOutputAudit,
46
+ },
47
+ {
48
+ version: 10,
49
+ name: "activity-host-turns",
50
+ up: migrateActivityHostTurns,
51
+ },
42
52
  ];
43
53
  export function migrateDatabase(sqlite) {
44
54
  const migrate = sqlite.transaction(() => {
@@ -248,6 +258,64 @@ function migrateActivityAudit(sqlite) {
248
258
  on activity_audit_events(created_at);
249
259
  `);
250
260
  }
261
+ function migrateBashOutputAudit(sqlite) {
262
+ sqlite.exec(`
263
+ create table if not exists bash_output_streams (
264
+ id text primary key,
265
+ activity_id text not null,
266
+ turn_id text not null,
267
+ conversation_scope_id text,
268
+ process_id integer not null,
269
+ workspace_id text not null,
270
+ workspace_root text not null,
271
+ command text not null,
272
+ tty integer not null default 0,
273
+ status text not null default 'running',
274
+ exit_code integer,
275
+ signal text,
276
+ timed_out integer not null default 0,
277
+ error text,
278
+ returned integer not null default 0,
279
+ completion_claimed_at text,
280
+ started_at text not null,
281
+ finished_at text
282
+ );
283
+
284
+ create index if not exists bash_output_streams_activity_idx
285
+ on bash_output_streams(activity_id);
286
+
287
+ create index if not exists bash_output_streams_workspace_idx
288
+ on bash_output_streams(workspace_id, started_at);
289
+
290
+ create table if not exists bash_output_chunks (
291
+ output_id text not null,
292
+ sequence integer not null,
293
+ channel text not null,
294
+ data blob not null,
295
+ created_at text not null,
296
+ primary key (output_id, sequence),
297
+ foreign key (output_id) references bash_output_streams(id) on delete cascade
298
+ );
299
+
300
+ create index if not exists bash_output_chunks_output_idx
301
+ on bash_output_chunks(output_id, sequence);
302
+ `);
303
+ }
304
+ function migrateActivityHostTurns(sqlite) {
305
+ sqlite.exec(`
306
+ create table if not exists activity_host_turns (
307
+ turn_id text primary key,
308
+ conversation_scope_id text,
309
+ created_at text not null
310
+ );
311
+
312
+ create index if not exists activity_host_turns_conversation_idx
313
+ on activity_host_turns(conversation_scope_id, created_at desc);
314
+
315
+ create index if not exists activity_host_turns_created_idx
316
+ on activity_host_turns(created_at desc);
317
+ `);
318
+ }
251
319
  function addColumnIfMissing(sqlite, table, column, definition) {
252
320
  const columns = sqlite.prepare(`pragma table_info(${table})`).all();
253
321
  if (columns.some((existingColumn) => existingColumn.name === column))
package/dist/db/schema.js CHANGED
@@ -1,4 +1,4 @@
1
- import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
1
+ import { blob, index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
2
2
  export const workspaceSessions = sqliteTable("workspace_sessions", {
3
3
  id: text("id").primaryKey(),
4
4
  root: text("root").notNull(),
@@ -97,6 +97,49 @@ export const activityAuditEvents = sqliteTable("activity_audit_events", {
97
97
  index("activity_audit_events_turn_idx").on(table.turnId, table.createdAt),
98
98
  index("activity_audit_events_created_idx").on(table.createdAt),
99
99
  ]);
100
+ export const activityHostTurns = sqliteTable("activity_host_turns", {
101
+ turnId: text("turn_id").primaryKey(),
102
+ conversationScopeId: text("conversation_scope_id"),
103
+ createdAt: text("created_at").notNull(),
104
+ }, (table) => [
105
+ index("activity_host_turns_conversation_idx").on(table.conversationScopeId, table.createdAt),
106
+ index("activity_host_turns_created_idx").on(table.createdAt),
107
+ ]);
108
+ export const bashOutputStreams = sqliteTable("bash_output_streams", {
109
+ id: text("id").primaryKey(),
110
+ activityId: text("activity_id").notNull(),
111
+ turnId: text("turn_id").notNull(),
112
+ conversationScopeId: text("conversation_scope_id"),
113
+ processId: integer("process_id").notNull(),
114
+ workspaceId: text("workspace_id").notNull(),
115
+ workspaceRoot: text("workspace_root").notNull(),
116
+ command: text("command").notNull(),
117
+ tty: integer("tty", { mode: "boolean" }).notNull().default(false),
118
+ status: text("status").notNull().default("running"),
119
+ exitCode: integer("exit_code"),
120
+ signal: text("signal"),
121
+ timedOut: integer("timed_out", { mode: "boolean" }).notNull().default(false),
122
+ error: text("error"),
123
+ returned: integer("returned", { mode: "boolean" }).notNull().default(false),
124
+ completionClaimedAt: text("completion_claimed_at"),
125
+ startedAt: text("started_at").notNull(),
126
+ finishedAt: text("finished_at"),
127
+ }, (table) => [
128
+ index("bash_output_streams_activity_idx").on(table.activityId),
129
+ index("bash_output_streams_workspace_idx").on(table.workspaceId, table.startedAt),
130
+ ]);
131
+ export const bashOutputChunks = sqliteTable("bash_output_chunks", {
132
+ outputId: text("output_id")
133
+ .notNull()
134
+ .references(() => bashOutputStreams.id, { onDelete: "cascade" }),
135
+ sequence: integer("sequence").notNull(),
136
+ channel: text("channel").notNull(),
137
+ data: blob("data", { mode: "buffer" }).notNull(),
138
+ createdAt: text("created_at").notNull(),
139
+ }, (table) => [
140
+ primaryKey({ columns: [table.outputId, table.sequence] }),
141
+ index("bash_output_chunks_output_idx").on(table.outputId, table.sequence),
142
+ ]);
100
143
  export const localAgentSessions = sqliteTable("local_agent_sessions", {
101
144
  id: text("id").primaryKey(),
102
145
  workspaceId: text("workspace_id"),
@@ -5,7 +5,10 @@ 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
7
  import { ActivityAuditStore } from "../../activity/audit-store.js";
8
+ import { BashOutputStore } from "../../activity/bash-output-store.js";
9
+ import { HostTurnStore } from "../../activity/host-turn-store.js";
8
10
  import { ActivityLifecycle } from "../../activity/lifecycle.js";
11
+ import { ActivityQueryService } from "../../activity/query-service.js";
9
12
  import { loadConfig } from "../../config.js";
10
13
  import { createReviewCheckpointManager } from "../../review-checkpoints.js";
11
14
  import { ProcessManager } from "../../process-sessions.js";
@@ -34,11 +37,16 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
34
37
  });
35
38
  const store = new SqliteWorkspaceStore(stateDir);
36
39
  const workspaces = new WorkspaceRegistry(config, store);
37
- const processSessions = new ProcessManager();
38
40
  const auditStore = new ActivityAuditStore(stateDir);
39
- const activityLifecycle = new ActivityLifecycle(auditStore);
41
+ const bashOutputStore = new BashOutputStore(stateDir);
42
+ const hostTurnStore = new HostTurnStore(stateDir);
43
+ const activityQueries = new ActivityQueryService(hostTurnStore, auditStore, bashOutputStore);
44
+ const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
45
+ const activityLifecycle = new ActivityLifecycle(auditStore, {
46
+ turnIdForConversation: (conversationScopeId) => activityQueries.currentTurnId(conversationScopeId),
47
+ });
40
48
  const codeIntelligence = new CodeIntelligenceManager(config, options.codeIntelligenceOptions);
41
- const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle);
49
+ const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
42
50
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
43
51
  const client = new Client({ name: "forgerelay-code-intelligence-test-client", version: "1.0.0" });
44
52
  await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
@@ -51,6 +59,8 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
51
59
  await server.close();
52
60
  await codeIntelligence.shutdown();
53
61
  processSessions.shutdown();
62
+ hostTurnStore.close();
63
+ bashOutputStore.close();
54
64
  auditStore.close();
55
65
  store.close();
56
66
  };
@@ -209,6 +209,7 @@ export class ProcessManager {
209
209
  completedProcessTtlMs;
210
210
  maxStartYieldMs;
211
211
  monotonicNow;
212
+ outputAudit;
212
213
  nextProcessId = 1;
213
214
  constructor(options = {}) {
214
215
  this.maxBufferCharacters = options.maxBufferCharacters ?? DEFAULT_BUFFER_CHARACTERS;
@@ -225,6 +226,7 @@ export class ProcessManager {
225
226
  ?? COMPLETED_PROCESS_TTL_MS;
226
227
  this.maxStartYieldMs = options.maxStartYieldMs ?? MAX_START_YIELD_MS;
227
228
  this.monotonicNow = options.monotonicNow ?? (() => performance.now());
229
+ this.outputAudit = options.outputAudit;
228
230
  }
229
231
  async start(input) {
230
232
  input.signal?.throwIfAborted();
@@ -241,6 +243,10 @@ export class ProcessManager {
241
243
  this.startPipe(processEntry, input);
242
244
  }
243
245
  catch (error) {
246
+ this.finishAudit(processEntry, {
247
+ timedOut: false,
248
+ error: error instanceof Error ? error.message : String(error),
249
+ });
244
250
  this.processes.delete(processEntry.id);
245
251
  throw error;
246
252
  }
@@ -349,8 +355,14 @@ export class ProcessManager {
349
355
  shutdown() {
350
356
  for (const processEntry of this.processes.values()) {
351
357
  this.clearProcessTimers(processEntry);
352
- if (processEntry.running)
358
+ if (processEntry.running) {
359
+ this.finishAudit(processEntry, {
360
+ signal: "shutdown",
361
+ timedOut: false,
362
+ error: "ForgeRelay shut down while the process was still running.",
363
+ });
353
364
  processEntry.process?.kill("SIGTERM");
365
+ }
354
366
  }
355
367
  this.processes.clear();
356
368
  this.completedByWorkspace.clear();
@@ -389,10 +401,25 @@ export class ProcessManager {
389
401
  const exitPromise = new Promise((resolve) => {
390
402
  resolveExit = resolve;
391
403
  });
404
+ const id = this.nextProcessId++;
405
+ const outputId = this.outputAudit && input.audit
406
+ ? this.outputAudit.begin({
407
+ activityId: input.audit.activityId,
408
+ turnId: input.audit.turnId,
409
+ ...(input.audit.conversationScopeId ? { conversationScopeId: input.audit.conversationScopeId } : {}),
410
+ processId: id,
411
+ workspaceId: input.workspaceId,
412
+ workspaceRoot: input.workspaceRoot ?? input.cwd,
413
+ command: input.command,
414
+ tty: input.tty === true,
415
+ })
416
+ : undefined;
392
417
  return {
393
- id: this.nextProcessId++,
418
+ id,
394
419
  workspaceId: input.workspaceId,
395
420
  command: input.command,
421
+ ...(outputId ? { outputId } : {}),
422
+ auditFinished: false,
396
423
  startedAtMonotonic: this.monotonicNow(),
397
424
  columns: terminalSize(input.columns, DEFAULT_COLUMNS),
398
425
  rows: terminalSize(input.rows, DEFAULT_ROWS),
@@ -426,9 +453,9 @@ export class ProcessManager {
426
453
  kill: (signal = "SIGTERM") => terminateProcessTree(child, signal, detached),
427
454
  resize: input.tty ? () => undefined : undefined,
428
455
  };
429
- child.stdout.on("data", (data) => this.append(processEntry, data.toString("utf8")));
430
- child.stderr.on("data", (data) => this.append(processEntry, data.toString("utf8")));
431
- child.on("error", (error) => this.append(processEntry, `${error.message}\n`));
456
+ child.stdout.on("data", (data) => this.append(processEntry, "stdout", data));
457
+ child.stderr.on("data", (data) => this.append(processEntry, "stderr", data));
458
+ child.on("error", (error) => this.append(processEntry, "process", `${error.message}\n`));
432
459
  child.on("close", (code, signal) => this.finish(processEntry, code ?? undefined, signal ?? undefined));
433
460
  }
434
461
  async startPty(processEntry, input) {
@@ -462,7 +489,7 @@ export class ProcessManager {
462
489
  kill: (signal) => pty.kill(signal),
463
490
  resize: (columns, rows) => pty.resize(columns, rows),
464
491
  };
465
- pty.onData((data) => this.append(processEntry, data));
492
+ pty.onData((data) => this.append(processEntry, "pty", data));
466
493
  pty.onExit(({ exitCode, signal }) => {
467
494
  this.finish(processEntry, exitCode, signal === 0 ? undefined : String(signal));
468
495
  });
@@ -475,6 +502,11 @@ export class ProcessManager {
475
502
  processEntry.signal = signal;
476
503
  processEntry.finishedAtMonotonic = this.monotonicNow();
477
504
  processEntry.process = undefined;
505
+ this.finishAudit(processEntry, {
506
+ ...(exitCode !== undefined ? { exitCode } : {}),
507
+ ...(signal ? { signal } : {}),
508
+ timedOut: processEntry.timedOut,
509
+ });
478
510
  if (processEntry.executionTimeoutTimer)
479
511
  clearTimeout(processEntry.executionTimeoutTimer);
480
512
  if (processEntry.forceKillTimer)
@@ -512,8 +544,17 @@ export class ProcessManager {
512
544
  processEntry.cleanupTimer = setTimeout(() => this.removeProcess(processEntry.id), remainingMs);
513
545
  processEntry.cleanupTimer.unref();
514
546
  }
515
- append(processEntry, output) {
516
- processEntry.buffer.append(output);
547
+ append(processEntry, channel, output) {
548
+ if (processEntry.outputId && !processEntry.auditFinished) {
549
+ this.outputAudit?.append(processEntry.outputId, channel, output);
550
+ }
551
+ processEntry.buffer.append(typeof output === "string" ? output : Buffer.from(output).toString("utf8"));
552
+ }
553
+ finishAudit(processEntry, input) {
554
+ if (!processEntry.outputId || processEntry.auditFinished)
555
+ return;
556
+ processEntry.auditFinished = true;
557
+ this.outputAudit?.finish(processEntry.outputId, input);
517
558
  }
518
559
  consume(processEntry, maxOutputTokens) {
519
560
  const limit = boundedInteger(maxOutputTokens, DEFAULT_MAX_OUTPUT_TOKENS, 100_000);
@@ -526,6 +567,7 @@ export class ProcessManager {
526
567
  return {
527
568
  processId,
528
569
  sessionId: processId,
570
+ ...(processEntry.outputId ? { outputId: processEntry.outputId } : {}),
529
571
  output: buffered.output,
530
572
  outputTruncated: processEntry.outputWasTruncated,
531
573
  running: processEntry.running,