@akira-tl/forgerelay 0.5.2 → 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 CHANGED
@@ -4,6 +4,24 @@ All notable ForgeRelay changes are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.4] - 2026-08-15
8
+
9
+ ### Fixed
10
+
11
+ - Closed all Activity query SQLite handles before temporary-state cleanup so the backend acceptance suite also passes on Windows, where open database files cannot be unlinked.
12
+
13
+ ## [0.5.3] - 2026-08-15
14
+
15
+ ### Added
16
+
17
+ - Added durable Host Turn records and a stable Activity query projection backed by ForgeRelay's local SQLite audit history, with revision-based lightweight snapshots that remain queryable after ForgeRelay restarts.
18
+ - Added the production backend query contract for future MCP App UI: model-visible `activity_panel` establishes a Host Turn, while app-only `activity_snapshot`, `activity_detail`, and `activity_output` data sources expose summaries, selected lazy detail, and complete Bash output separately.
19
+
20
+ ### Changed
21
+
22
+ - Activity snapshots now use an explicit summary whitelist: read bodies, write/edit patches, full Bash commands/output, and capability-heavy payloads stay out of normal snapshots; rename/delete include complete path targets and are summary-complete without detail requests.
23
+ - Late Bash completion records use the current Host Turn for delivery while preserving the original Bash Activity's immutable returned history and Workspace audit snapshot.
24
+
7
25
  ## [0.5.2] - 2026-08-15
8
26
 
9
27
  ### Added
@@ -47,6 +47,24 @@ export class ActivityAuditStore {
47
47
  listEvents(activityId) {
48
48
  return this.readRows(activityId).map(rowToEvent);
49
49
  }
50
+ listActivitiesByTurn(turnId) {
51
+ const rows = this.database.sqlite.prepare(`select activity_id from activity_audit_events
52
+ where event_type = 'started' and turn_id = ?
53
+ order by rowid asc`).all(turnId);
54
+ return rows.flatMap(({ activity_id }) => {
55
+ const activity = this.getActivity(activity_id);
56
+ return activity ? [activity] : [];
57
+ });
58
+ }
59
+ turnRevision(turnId) {
60
+ const row = this.database.sqlite.prepare(`select count(*) as revision
61
+ from activity_audit_events
62
+ where activity_id in (
63
+ select activity_id from activity_audit_events
64
+ where event_type = 'started' and turn_id = ?
65
+ )`).get(turnId);
66
+ return row.revision;
67
+ }
50
68
  getActivity(activityId) {
51
69
  const events = this.listEvents(activityId);
52
70
  const started = events[0];
@@ -0,0 +1,46 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { openDatabase } from "../db/client.js";
3
+ export class HostTurnStore {
4
+ database;
5
+ now;
6
+ nextTurnId;
7
+ constructor(stateDir, options = {}) {
8
+ this.database = openDatabase(stateDir);
9
+ this.now = options.now ?? (() => new Date());
10
+ this.nextTurnId = options.turnId ?? (() => `turn_${randomUUID().replaceAll("-", "")}`);
11
+ }
12
+ begin(conversationScopeId) {
13
+ const turnId = this.nextTurnId();
14
+ const createdAt = this.now().toISOString();
15
+ this.database.sqlite.prepare(`insert into activity_host_turns (turn_id, conversation_scope_id, created_at)
16
+ values (?, ?, ?)`).run(turnId, conversationScopeId ?? null, createdAt);
17
+ return {
18
+ turnId,
19
+ ...(conversationScopeId ? { conversationScopeId } : {}),
20
+ createdAt,
21
+ };
22
+ }
23
+ get(turnId) {
24
+ const row = this.database.sqlite.prepare("select * from activity_host_turns where turn_id = ?").get(turnId);
25
+ return row ? rowToTurn(row) : undefined;
26
+ }
27
+ current(conversationScopeId) {
28
+ if (!conversationScopeId)
29
+ return undefined;
30
+ const row = this.database.sqlite.prepare(`select * from activity_host_turns
31
+ where conversation_scope_id = ?
32
+ order by rowid desc
33
+ limit 1`).get(conversationScopeId);
34
+ return row ? rowToTurn(row) : undefined;
35
+ }
36
+ close() {
37
+ this.database.close();
38
+ }
39
+ }
40
+ function rowToTurn(row) {
41
+ return {
42
+ turnId: row.turn_id,
43
+ ...(row.conversation_scope_id ? { conversationScopeId: row.conversation_scope_id } : {}),
44
+ createdAt: row.created_at,
45
+ };
46
+ }
@@ -4,10 +4,12 @@ export class ActivityLifecycle {
4
4
  auditStore;
5
5
  activityId;
6
6
  turnId;
7
+ turnIdForConversation;
7
8
  constructor(auditStore, options = {}) {
8
9
  this.auditStore = auditStore;
9
10
  this.activityId = options.activityId ?? newActivityId;
10
11
  this.turnId = options.turnId ?? newTurnId;
12
+ this.turnIdForConversation = options.turnIdForConversation;
11
13
  }
12
14
  record(options) {
13
15
  const context = this.start(options);
@@ -43,7 +45,9 @@ export class ActivityLifecycle {
43
45
  }
44
46
  start(options) {
45
47
  const activityId = options.activityId ?? this.activityId();
46
- const turnId = options.turnId ?? this.turnId();
48
+ const turnId = options.turnId
49
+ ?? this.turnIdForConversation?.(options.conversationScopeId)
50
+ ?? this.turnId();
47
51
  const request = normalizeAuditValue(options.request);
48
52
  this.auditStore.append({
49
53
  type: "started",
@@ -0,0 +1,129 @@
1
+ import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
2
+ import * as z from "zod/v4";
3
+ import { openAiConversationScopeId } from "../request-meta.js";
4
+ const READ_ONLY_ANNOTATIONS = {
5
+ readOnlyHint: true,
6
+ destructiveHint: false,
7
+ idempotentHint: true,
8
+ openWorldHint: false,
9
+ };
10
+ const activitySummarySchema = z.object({
11
+ activityId: z.string(),
12
+ tool: z.string(),
13
+ kind: z.string(),
14
+ status: z.enum(["working", "done", "error"]),
15
+ state: z.enum(["executing", "returned", "done", "failed", "blocked"]),
16
+ title: z.string(),
17
+ target: z.string(),
18
+ detailAvailable: z.boolean(),
19
+ workspaceId: z.string().optional(),
20
+ processId: z.number().int().positive().optional(),
21
+ outputId: z.string().optional(),
22
+ commandLength: z.number().int().nonnegative().optional(),
23
+ bashPhase: z.enum(["executing", "returned", "done", "error"]).optional(),
24
+ startedAt: z.string(),
25
+ finishedAt: z.string().optional(),
26
+ durationMs: z.number().nonnegative().optional(),
27
+ });
28
+ const snapshotOutputSchema = {
29
+ turnId: z.string(),
30
+ revision: z.number().int().nonnegative(),
31
+ changed: z.boolean(),
32
+ state: z.enum(["working", "done", "error"]),
33
+ activities: z.array(activitySummarySchema),
34
+ };
35
+ export function registerActivityQueryTools(server, queries) {
36
+ registerAppTool(server, "activity_panel", {
37
+ title: "Begin Activity Panel",
38
+ description: "Begin one ForgeRelay Host Turn lifecycle for subsequent project work. This orchestration call does not read or modify project files. Call it once before the first ForgeRelay work operation in a Host Turn that performs project work.",
39
+ inputSchema: {},
40
+ outputSchema: snapshotOutputSchema,
41
+ _meta: { ui: { visibility: ["model", "app"] } },
42
+ annotations: {
43
+ ...READ_ONLY_ANNOTATIONS,
44
+ idempotentHint: false,
45
+ },
46
+ }, async (_input, extra) => {
47
+ const snapshot = queries.beginTurn(openAiConversationScopeId(extra._meta));
48
+ return {
49
+ content: [{
50
+ type: "text",
51
+ text: `Started ForgeRelay Activity Panel Host Turn ${snapshot.turnId}.`,
52
+ }],
53
+ structuredContent: { ...snapshot },
54
+ };
55
+ });
56
+ registerAppTool(server, "activity_snapshot", {
57
+ title: "Read Activity snapshot",
58
+ description: "App-only data source for lightweight Activity summaries in one durable Host Turn.",
59
+ inputSchema: {
60
+ turnId: z.string(),
61
+ knownRevision: z.number().int().nonnegative().optional(),
62
+ },
63
+ outputSchema: snapshotOutputSchema,
64
+ _meta: { ui: { visibility: ["app"] } },
65
+ annotations: READ_ONLY_ANNOTATIONS,
66
+ }, async ({ turnId, knownRevision }) => {
67
+ const snapshot = queries.snapshot(turnId, knownRevision);
68
+ return {
69
+ content: [{
70
+ type: "text",
71
+ text: snapshot.changed
72
+ ? `Activity snapshot ${turnId} revision ${snapshot.revision}.`
73
+ : `Activity snapshot ${turnId} unchanged at revision ${snapshot.revision}.`,
74
+ }],
75
+ structuredContent: { ...snapshot },
76
+ };
77
+ });
78
+ registerAppTool(server, "activity_detail", {
79
+ title: "Read Activity detail",
80
+ description: "App-only lazy data source for one selected expandable Activity.",
81
+ inputSchema: {
82
+ turnId: z.string(),
83
+ activityId: z.string(),
84
+ },
85
+ outputSchema: {
86
+ activity: activitySummarySchema,
87
+ request: z.unknown().optional(),
88
+ result: z.unknown().optional(),
89
+ error: z.string().optional(),
90
+ },
91
+ _meta: { ui: { visibility: ["app"] } },
92
+ annotations: READ_ONLY_ANNOTATIONS,
93
+ }, async ({ turnId, activityId }) => {
94
+ const detail = queries.detail(turnId, activityId);
95
+ return {
96
+ content: [{ type: "text", text: `Activity detail ${activityId}.` }],
97
+ structuredContent: { ...detail },
98
+ };
99
+ });
100
+ registerAppTool(server, "activity_output", {
101
+ title: "Read Bash output",
102
+ description: "App-only lazy data source for complete durable Bash command/output by stable outputId.",
103
+ inputSchema: {
104
+ turnId: z.string(),
105
+ outputId: z.string(),
106
+ },
107
+ outputSchema: {
108
+ outputId: z.string(),
109
+ activityId: z.string(),
110
+ processId: z.number().int().positive(),
111
+ command: z.string(),
112
+ output: z.string(),
113
+ status: z.enum(["running", "done", "failed"]),
114
+ exitCode: z.number().int().optional(),
115
+ signal: z.string().optional(),
116
+ timedOut: z.boolean(),
117
+ startedAt: z.string(),
118
+ finishedAt: z.string().optional(),
119
+ },
120
+ _meta: { ui: { visibility: ["app"] } },
121
+ annotations: READ_ONLY_ANNOTATIONS,
122
+ }, async ({ turnId, outputId }) => {
123
+ const output = queries.bashOutput(turnId, outputId);
124
+ return {
125
+ content: [{ type: "text", text: `Bash output ${outputId}.` }],
126
+ structuredContent: { ...output },
127
+ };
128
+ });
129
+ }
@@ -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
+ }
@@ -44,6 +44,11 @@ const migrations = [
44
44
  name: "bash-output-audit",
45
45
  up: migrateBashOutputAudit,
46
46
  },
47
+ {
48
+ version: 10,
49
+ name: "activity-host-turns",
50
+ up: migrateActivityHostTurns,
51
+ },
47
52
  ];
48
53
  export function migrateDatabase(sqlite) {
49
54
  const migrate = sqlite.transaction(() => {
@@ -296,6 +301,21 @@ function migrateBashOutputAudit(sqlite) {
296
301
  on bash_output_chunks(output_id, sequence);
297
302
  `);
298
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
+ }
299
319
  function addColumnIfMissing(sqlite, table, column, definition) {
300
320
  const columns = sqlite.prepare(`pragma table_info(${table})`).all();
301
321
  if (columns.some((existingColumn) => existingColumn.name === column))
package/dist/db/schema.js CHANGED
@@ -97,6 +97,14 @@ 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
+ ]);
100
108
  export const bashOutputStreams = sqliteTable("bash_output_streams", {
101
109
  id: text("id").primaryKey(),
102
110
  activityId: text("activity_id").notNull(),
@@ -6,7 +6,9 @@ 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
8
  import { BashOutputStore } from "../../activity/bash-output-store.js";
9
+ import { HostTurnStore } from "../../activity/host-turn-store.js";
9
10
  import { ActivityLifecycle } from "../../activity/lifecycle.js";
11
+ import { ActivityQueryService } from "../../activity/query-service.js";
10
12
  import { loadConfig } from "../../config.js";
11
13
  import { createReviewCheckpointManager } from "../../review-checkpoints.js";
12
14
  import { ProcessManager } from "../../process-sessions.js";
@@ -37,10 +39,14 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
37
39
  const workspaces = new WorkspaceRegistry(config, store);
38
40
  const auditStore = new ActivityAuditStore(stateDir);
39
41
  const bashOutputStore = new BashOutputStore(stateDir);
42
+ const hostTurnStore = new HostTurnStore(stateDir);
43
+ const activityQueries = new ActivityQueryService(hostTurnStore, auditStore, bashOutputStore);
40
44
  const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
41
- const activityLifecycle = new ActivityLifecycle(auditStore);
45
+ const activityLifecycle = new ActivityLifecycle(auditStore, {
46
+ turnIdForConversation: (conversationScopeId) => activityQueries.currentTurnId(conversationScopeId),
47
+ });
42
48
  const codeIntelligence = new CodeIntelligenceManager(config, options.codeIntelligenceOptions);
43
- const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore);
49
+ const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
44
50
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
45
51
  const client = new Client({ name: "forgerelay-code-intelligence-test-client", version: "1.0.0" });
46
52
  await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
@@ -53,6 +59,7 @@ export async function createCodeIntelligenceServerFixture(t, options = {}) {
53
59
  await server.close();
54
60
  await codeIntelligence.shutdown();
55
61
  processSessions.shutdown();
62
+ hostTurnStore.close();
56
63
  bashOutputStore.close();
57
64
  auditStore.close();
58
65
  store.close();
package/dist/server.js CHANGED
@@ -17,7 +17,10 @@ import * as z from "zod/v4";
17
17
  import { applyPatch } from "./apply-patch.js";
18
18
  import { ActivityAuditStore } from "./activity/audit-store.js";
19
19
  import { BashOutputStore } from "./activity/bash-output-store.js";
20
+ import { HostTurnStore } from "./activity/host-turn-store.js";
21
+ import { registerActivityQueryTools } from "./activity/mcp-query-tools.js";
20
22
  import { ActivityLifecycle, } from "./activity/lifecycle.js";
23
+ import { ActivityQueryService } from "./activity/query-service.js";
21
24
  import { buildCapabilityFingerprint } from "./capabilities.js";
22
25
  import { CapabilityError, createCapabilityRegistry, } from "./capability-registry.js";
23
26
  import { deletePath, renamePath } from "./file-mutations.js";
@@ -992,7 +995,7 @@ function registerProcessTools(server, config, workspaces, processSessions, hooks
992
995
  });
993
996
  });
994
997
  }
995
- export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore) {
998
+ export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries) {
996
999
  const toolDescriptions = buildToolDescriptions(config);
997
1000
  const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result, (snapshot) => recordBashCompletion(activityLifecycle, bashOutputStore, snapshot.outputId)));
998
1001
  const incomingArtifactRegistry = new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
@@ -1465,6 +1468,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1465
1468
  },
1466
1469
  }, hookReports));
1467
1470
  });
1471
+ registerActivityQueryTools(server, activityQueries);
1468
1472
  registerAppTool(server, toolNames.capability, {
1469
1473
  title: "Use optional capability",
1470
1474
  description: "Describe or run one optional ForgeRelay capability advertised by open_workspace. Use describe when the capability contract is unfamiliar, then read its advertised guide if needed. Run dispatches only explicitly registered capabilities; it cannot invoke arbitrary shell commands, URLs, or methods.",
@@ -2453,8 +2457,12 @@ export function createServer(config = loadConfig(), options = {}) {
2453
2457
  const workspaceStore = createWorkspaceStore(config.stateDir);
2454
2458
  const workspaces = new WorkspaceRegistry(config, workspaceStore);
2455
2459
  const activityAuditStore = new ActivityAuditStore(config.stateDir);
2456
- const activityLifecycle = new ActivityLifecycle(activityAuditStore);
2457
2460
  const bashOutputStore = new BashOutputStore(config.stateDir);
2461
+ const hostTurnStore = new HostTurnStore(config.stateDir);
2462
+ const activityQueries = new ActivityQueryService(hostTurnStore, activityAuditStore, bashOutputStore);
2463
+ const activityLifecycle = new ActivityLifecycle(activityAuditStore, {
2464
+ turnIdForConversation: (conversationScopeId) => activityQueries.currentTurnId(conversationScopeId),
2465
+ });
2458
2466
  const reviewCheckpoints = createReviewCheckpointManager();
2459
2467
  const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
2460
2468
  const codeIntelligence = new CodeIntelligenceManager(config);
@@ -2641,7 +2649,7 @@ export function createServer(config = loadConfig(), options = {}) {
2641
2649
  });
2642
2650
  }
2643
2651
  };
2644
- const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore);
2652
+ const server = createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters, codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
2645
2653
  await server.connect(transport);
2646
2654
  }
2647
2655
  else {
@@ -2673,6 +2681,7 @@ export function createServer(config = loadConfig(), options = {}) {
2673
2681
  processSessions.shutdown();
2674
2682
  await codeIntelligence.shutdown();
2675
2683
  oauthProvider.close();
2684
+ hostTurnStore.close();
2676
2685
  bashOutputStore.close();
2677
2686
  activityAuditStore.close();
2678
2687
  workspaceStore.close?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
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/activity/bash-output-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",
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/bash-output-store.test.ts && tsx src/activity/lifecycle.test.ts && tsx src/activity/query-service.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",