@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.
- package/CHANGELOG.md +29 -0
- package/dist/activity/audit-store.js +18 -0
- package/dist/activity/bash-output-store.js +127 -0
- package/dist/activity/host-turn-store.js +46 -0
- package/dist/activity/lifecycle.js +62 -31
- package/dist/activity/mcp-query-tools.js +129 -0
- package/dist/activity/process-output-audit.js +1 -0
- package/dist/activity/query-service.js +247 -0
- package/dist/db/migrations.js +68 -0
- package/dist/db/schema.js +44 -1
- package/dist/lsp/test-support/server-fixture.js +13 -3
- package/dist/process-sessions.js +50 -8
- package/dist/server.js +209 -26
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/db/migrations.js
CHANGED
|
@@ -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
|
|
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
|
};
|
package/dist/process-sessions.js
CHANGED
|
@@ -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
|
|
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,
|
|
430
|
-
child.stderr.on("data", (data) => this.append(processEntry,
|
|
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.
|
|
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,
|