@akira-tl/forgerelay 0.6.1 → 0.7.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +38 -0
  3. package/capabilities/subagents/GUIDE.md +100 -40
  4. package/dist/activity/lifecycle.js +1 -1
  5. package/dist/activity/mcp-query-tools.js +1 -0
  6. package/dist/activity/query-service.js +1 -0
  7. package/dist/capabilities.js +1 -1
  8. package/dist/capability-registry.js +34 -0
  9. package/dist/cli.js +80 -165
  10. package/dist/composite-activity.js +155 -0
  11. package/dist/composite-workspaces.js +197 -0
  12. package/dist/db/migrations.js +14 -0
  13. package/dist/db/schema.js +6 -0
  14. package/dist/remote-workspace-relay.js +16 -0
  15. package/dist/server.js +698 -172
  16. package/dist/{local-agent-targets.js → subagents/cli-target.js} +6 -6
  17. package/dist/{local-agent-profiles.js → subagents/profiles.js} +13 -5
  18. package/dist/subagents/providers/adapters/acp.js +148 -0
  19. package/dist/subagents/providers/adapters/claude.js +75 -0
  20. package/dist/{local-agent-runtime.js → subagents/providers/adapters/codex.js} +10 -4
  21. package/dist/subagents/providers/adapters/opencode.js +137 -0
  22. package/dist/subagents/providers/adapters/pi.js +232 -0
  23. package/dist/{local-agent-availability.js → subagents/providers/availability.js} +24 -10
  24. package/dist/subagents/providers/continuation.js +11 -0
  25. package/dist/subagents/providers/contract.js +1 -0
  26. package/dist/subagents/providers/registry.js +26 -0
  27. package/dist/subagents/providers/shared.js +40 -0
  28. package/dist/subagents/sessions/capability.js +214 -0
  29. package/dist/subagents/sessions/delivery-mailbox.js +115 -0
  30. package/dist/subagents/sessions/execution.js +171 -0
  31. package/dist/subagents/sessions/manager.js +107 -0
  32. package/dist/subagents/sessions/mcp/audit.js +85 -0
  33. package/dist/subagents/sessions/mcp/runtime.js +19 -0
  34. package/dist/{local-agent-store.js → subagents/sessions/store.js} +75 -39
  35. package/dist/ui/.vite/manifest.json +33 -33
  36. package/dist/ui/activity-panel-app.html +3 -3
  37. package/dist/ui/assets/{activity-panel-app-CjZVvVNc.js → activity-panel-app-E1ju2dqI.js} +1 -1
  38. package/dist/ui/assets/{heavy-payload-vGgBRvNX.js → heavy-payload-CeW-n9w5.js} +1 -1
  39. package/dist/ui/assets/{review-payload-4erWKckt.js → review-payload-B9CO298v.js} +1 -1
  40. package/dist/ui/assets/{scrollbar-CaOPzUJd.js → scrollbar-C2twAENW.js} +1 -1
  41. package/dist/ui/assets/workspace-app-BztEvZIC.js +5 -0
  42. package/dist/ui/assets/{workspace-app-DkAiSl_0.js → workspace-app-CwbJnb_w.js} +1 -1
  43. package/dist/ui/assets/workspace-app-YnUST8IP.css +1 -0
  44. package/dist/ui/assets/workspace-app-rKuhdae8.js +1 -0
  45. package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +1 -0
  46. package/dist/ui/workspace-app.html +4 -4
  47. package/dist/ui/workspace-lifecycle-app.html +4 -4
  48. package/dist/workspaces.js +3 -3
  49. package/docs/chatgpt-coding-workflow.md +2 -9
  50. package/docs/configuration.md +23 -0
  51. package/docs/debugging.md +7 -0
  52. package/docs/roadmap.md +42 -7
  53. package/package.json +2 -2
  54. package/scripts/debug/runtime.mjs +23 -1
  55. package/scripts/debug/runtime.test.mjs +14 -2
  56. package/scripts/debug/serve.mjs +4 -4
  57. package/scripts/release/release-gate.test.mjs +2 -2
  58. package/dist/local-agent-adapters.js +0 -653
  59. package/dist/ui/assets/workspace-app-CcrHAUIn.css +0 -1
  60. package/dist/ui/assets/workspace-app-DJmkPYJC.js +0 -1
  61. package/dist/ui/assets/workspace-app-QyauBrJX.js +0 -5
  62. package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +0 -1
  63. /package/dist/{local-agent-path.js → subagents/providers/path.js} +0 -0
@@ -0,0 +1,115 @@
1
+ import { mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ const MAX_DELIVERY_TEXT_BYTES = 64 * 1024;
4
+ const MAX_DELIVERIES_PER_RESPONSE = 4;
5
+ export class SubagentDeliveryMailbox {
6
+ directory;
7
+ constructor(stateDir) {
8
+ this.directory = join(stateDir, "subagent-delivery");
9
+ }
10
+ write(input) {
11
+ const response = boundText(input.finalResponse);
12
+ const error = boundText(input.error);
13
+ const delivery = {
14
+ sessionId: input.sessionId,
15
+ runId: input.runId,
16
+ workspaceId: input.workspaceId,
17
+ ...(input.activityId ? { activityId: input.activityId } : {}),
18
+ provider: input.provider,
19
+ outcome: input.outcome,
20
+ ...(response.text !== undefined ? { finalResponse: response.text } : {}),
21
+ ...(error.text !== undefined ? { error: error.text } : {}),
22
+ truncated: response.truncated || error.truncated,
23
+ createdAt: new Date().toISOString(),
24
+ };
25
+ mkdirSync(this.directory, { recursive: true, mode: 0o700 });
26
+ const path = this.pathFor(delivery.sessionId);
27
+ const temporary = `${path}.${process.pid}.tmp`;
28
+ writeFileSync(temporary, `${JSON.stringify(delivery)}\n`, { mode: 0o600 });
29
+ rmSync(path, { force: true });
30
+ renameSync(temporary, path);
31
+ return delivery;
32
+ }
33
+ claimWorkspace(workspaceId, excludeRunId) {
34
+ return this.claim((delivery) => delivery.workspaceId === workspaceId && delivery.runId !== excludeRunId);
35
+ }
36
+ claimSession(workspaceId, sessionId) {
37
+ return this.claim((delivery) => delivery.workspaceId === workspaceId && delivery.sessionId === sessionId);
38
+ }
39
+ hasSession(sessionId) {
40
+ return this.files().includes(`${sessionId}.json`);
41
+ }
42
+ claim(predicate) {
43
+ const deliveries = [];
44
+ for (const file of this.files()) {
45
+ if (deliveries.length >= MAX_DELIVERIES_PER_RESPONSE)
46
+ break;
47
+ const path = join(this.directory, file);
48
+ const delivery = readDelivery(path);
49
+ if (!delivery || !predicate(delivery))
50
+ continue;
51
+ rmSync(path, { force: true });
52
+ deliveries.push(delivery);
53
+ }
54
+ return deliveries.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
55
+ }
56
+ files() {
57
+ try {
58
+ return readdirSync(this.directory)
59
+ .filter((file) => /^agt_[a-z0-9]+\.json$/i.test(file))
60
+ .sort();
61
+ }
62
+ catch (error) {
63
+ if (error.code === "ENOENT")
64
+ return [];
65
+ throw error;
66
+ }
67
+ }
68
+ pathFor(sessionId) {
69
+ if (!/^agt_[a-z0-9]+$/i.test(sessionId))
70
+ throw new Error(`Invalid Subagent Session id: ${sessionId}`);
71
+ return join(this.directory, `${sessionId}.json`);
72
+ }
73
+ }
74
+ function readDelivery(path) {
75
+ try {
76
+ const value = JSON.parse(readFileSync(path, "utf8"));
77
+ if (typeof value.sessionId !== "string" ||
78
+ typeof value.runId !== "string" ||
79
+ typeof value.workspaceId !== "string" ||
80
+ typeof value.provider !== "string" ||
81
+ !isOutcome(value.outcome) ||
82
+ typeof value.createdAt !== "string") {
83
+ return undefined;
84
+ }
85
+ return {
86
+ sessionId: value.sessionId,
87
+ runId: value.runId,
88
+ workspaceId: value.workspaceId,
89
+ ...(typeof value.activityId === "string" ? { activityId: value.activityId } : {}),
90
+ provider: value.provider,
91
+ outcome: value.outcome,
92
+ ...(typeof value.finalResponse === "string" ? { finalResponse: value.finalResponse } : {}),
93
+ ...(typeof value.error === "string" ? { error: value.error } : {}),
94
+ truncated: value.truncated === true,
95
+ createdAt: value.createdAt,
96
+ };
97
+ }
98
+ catch {
99
+ return undefined;
100
+ }
101
+ }
102
+ function boundText(value) {
103
+ if (value === undefined)
104
+ return { truncated: false };
105
+ const bytes = Buffer.from(value, "utf8");
106
+ if (bytes.length <= MAX_DELIVERY_TEXT_BYTES)
107
+ return { text: value, truncated: false };
108
+ return {
109
+ text: bytes.subarray(0, MAX_DELIVERY_TEXT_BYTES).toString("utf8"),
110
+ truncated: true,
111
+ };
112
+ }
113
+ function isOutcome(value) {
114
+ return value === "succeeded" || value === "failed" || value === "cancelled" || value === "interrupted";
115
+ }
@@ -0,0 +1,171 @@
1
+ import { HookRunner } from "../../hooks.js";
2
+ import { isSubagentProvider, loadSubagentProfiles, } from "../profiles.js";
3
+ import { runSubagentProvider } from "../providers/registry.js";
4
+ import { SubagentDeliveryMailbox } from "./delivery-mailbox.js";
5
+ import { createSubagentSessionStore, } from "./store.js";
6
+ export async function executeSubagentRun(config, input, providerRunner = runSubagentProvider) {
7
+ const store = createSubagentSessionStore(config);
8
+ const mailbox = new SubagentDeliveryMailbox(config.stateDir);
9
+ try {
10
+ const record = store.get(input.sessionId);
11
+ if (!record)
12
+ throw new Error(`Unknown subagent id: ${input.sessionId}`);
13
+ if (record.activeRun?.id !== input.runId) {
14
+ throw new Error(`Subagent Run ${input.runId} is not active for Session ${record.id}.`);
15
+ }
16
+ const hooks = new HookRunner(config.hooks, config.logging);
17
+ const hookInvocation = {
18
+ workspaceId: record.workspaceId,
19
+ workspaceRoot: record.workspaceRoot,
20
+ payload: {
21
+ agentId: record.id,
22
+ sessionId: record.id,
23
+ runId: input.runId,
24
+ profile: record.profileName,
25
+ provider: record.provider,
26
+ model: record.model,
27
+ thinking: record.thinking,
28
+ },
29
+ };
30
+ await hooks.run("SubagentStart", hookInvocation);
31
+ try {
32
+ const result = await runSessionProvider(config, record, input.prompt, providerRunner);
33
+ await hooks.run("SubagentStop", {
34
+ ...hookInvocation,
35
+ payload: {
36
+ ...hookInvocation.payload,
37
+ status: "succeeded",
38
+ providerSessionId: result.providerSessionId,
39
+ },
40
+ });
41
+ const finishedAt = new Date().toISOString();
42
+ store.update(record.id, {
43
+ providerSessionId: result.providerSessionId ?? undefined,
44
+ status: "idle",
45
+ activeRun: undefined,
46
+ latestRun: {
47
+ id: input.runId,
48
+ status: "succeeded",
49
+ finishedAt,
50
+ },
51
+ });
52
+ if (record.workspaceId) {
53
+ mailbox.write({
54
+ sessionId: record.id,
55
+ runId: input.runId,
56
+ workspaceId: record.workspaceId,
57
+ ...(input.activityId ? { activityId: input.activityId } : {}),
58
+ provider: record.provider,
59
+ outcome: "succeeded",
60
+ finalResponse: result.finalResponse,
61
+ });
62
+ }
63
+ return completion(record, input, "succeeded");
64
+ }
65
+ catch (error) {
66
+ const message = error instanceof Error ? error.message : String(error);
67
+ await hooks.run("SubagentStop", {
68
+ ...hookInvocation,
69
+ payload: {
70
+ ...hookInvocation.payload,
71
+ status: "failed",
72
+ },
73
+ });
74
+ const finishedAt = new Date().toISOString();
75
+ store.update(record.id, {
76
+ status: "idle",
77
+ activeRun: undefined,
78
+ latestRun: {
79
+ id: input.runId,
80
+ status: "failed",
81
+ finishedAt,
82
+ },
83
+ });
84
+ if (record.workspaceId) {
85
+ mailbox.write({
86
+ sessionId: record.id,
87
+ runId: input.runId,
88
+ workspaceId: record.workspaceId,
89
+ ...(input.activityId ? { activityId: input.activityId } : {}),
90
+ provider: record.provider,
91
+ outcome: "failed",
92
+ error: message,
93
+ });
94
+ }
95
+ return completion(record, input, "failed", message);
96
+ }
97
+ }
98
+ finally {
99
+ store.close();
100
+ }
101
+ }
102
+ export async function executeSubagentSession(config, sessionId, prompt) {
103
+ const store = createSubagentSessionStore(config);
104
+ try {
105
+ const record = store.get(sessionId);
106
+ if (!record)
107
+ throw new Error(`Unknown subagent id: ${sessionId}`);
108
+ if (!record.activeRun)
109
+ throw new Error(`Subagent Session ${sessionId} has no active Run.`);
110
+ return executeSubagentRun(config, {
111
+ sessionId,
112
+ runId: record.activeRun.id,
113
+ ...(record.activeRun.activityId ? { activityId: record.activeRun.activityId } : {}),
114
+ prompt,
115
+ });
116
+ }
117
+ finally {
118
+ store.close();
119
+ }
120
+ }
121
+ async function runSessionProvider(config, session, prompt, providerRunner) {
122
+ if (!isSubagentProvider(session.provider)) {
123
+ throw new Error(`Unknown subagent provider for Session ${session.id}: ${session.provider}`);
124
+ }
125
+ if (session.providerSessionId) {
126
+ return providerRunner(session.provider, {
127
+ prompt,
128
+ workspace: session.workspaceRoot,
129
+ providerSessionId: session.providerSessionId,
130
+ writeMode: "allowed",
131
+ model: session.model,
132
+ thinking: session.thinking,
133
+ });
134
+ }
135
+ if (session.profileName === session.provider) {
136
+ return providerRunner(session.provider, {
137
+ prompt,
138
+ workspace: session.workspaceRoot,
139
+ writeMode: "allowed",
140
+ model: session.model,
141
+ thinking: session.thinking,
142
+ });
143
+ }
144
+ const profiles = await loadSubagentProfiles(config, session.workspaceRoot);
145
+ const profile = profiles.find((candidate) => candidate.name === session.profileName);
146
+ if (!profile)
147
+ throw new Error(`Subagent profile not found: ${session.profileName}`);
148
+ return runSubagentProfile(profile, session, prompt, providerRunner);
149
+ }
150
+ async function runSubagentProfile(profile, session, prompt, providerRunner) {
151
+ const body = profile.body.trim();
152
+ const firstPrompt = body ? `${body}\n\nTask:\n${prompt}` : prompt;
153
+ return providerRunner(session.provider, {
154
+ prompt: firstPrompt,
155
+ workspace: session.workspaceRoot,
156
+ writeMode: "allowed",
157
+ model: session.model,
158
+ thinking: session.thinking,
159
+ });
160
+ }
161
+ function completion(session, input, outcome, error) {
162
+ return {
163
+ sessionId: session.id,
164
+ runId: input.runId,
165
+ workspaceId: session.workspaceId,
166
+ activityId: input.activityId,
167
+ provider: session.provider,
168
+ outcome,
169
+ ...(error ? { error } : {}),
170
+ };
171
+ }
@@ -0,0 +1,107 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { formatAvailableSubagentTargets, resolveSubagentTarget, } from "../cli-target.js";
3
+ import { isSubagentProvider, loadSubagentProfiles, } from "../profiles.js";
4
+ import { assertSubagentProviderAvailable } from "../providers/availability.js";
5
+ import { subagentProviderContinuationSupported } from "../providers/continuation.js";
6
+ import { createSubagentSessionStore, } from "./store.js";
7
+ export class SubagentSessionError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ this.name = "SubagentSessionError";
13
+ }
14
+ }
15
+ export class SubagentSessionManager {
16
+ config;
17
+ launcher;
18
+ store;
19
+ constructor(config, launcher) {
20
+ this.config = config;
21
+ this.launcher = launcher;
22
+ this.store = createSubagentSessionStore(config);
23
+ }
24
+ list(scope = {}) {
25
+ return this.store.list(scope);
26
+ }
27
+ get(idOrPrefix, scope) {
28
+ return scope ? this.store.getInScope(idOrPrefix, scope) : this.store.get(idOrPrefix);
29
+ }
30
+ async start(input) {
31
+ const profiles = await loadSubagentProfiles(this.config, input.workspaceRoot);
32
+ const target = resolveSubagentTarget(input.target, profiles, input.model, input.thinking);
33
+ if (!target) {
34
+ throw new Error(`Unknown subagent profile or provider: ${input.target}. Available ${formatAvailableSubagentTargets(profiles)}`);
35
+ }
36
+ assertSubagentProviderAvailable(target.provider);
37
+ const runId = newRunId();
38
+ const startedAt = new Date().toISOString();
39
+ const session = this.store.create({
40
+ workspaceId: input.workspaceId,
41
+ workspaceRoot: input.workspaceRoot,
42
+ profileName: target.name,
43
+ provider: target.provider,
44
+ model: target.model,
45
+ thinking: target.thinking,
46
+ activeRun: {
47
+ id: runId,
48
+ ...(input.activityId ? { activityId: input.activityId } : {}),
49
+ startedAt,
50
+ },
51
+ });
52
+ const run = session.activeRun;
53
+ if (!run)
54
+ throw new Error(`Subagent Session ${session.id} did not create an active Run.`);
55
+ this.launcher.launch({
56
+ sessionId: session.id,
57
+ runId,
58
+ ...(input.activityId ? { activityId: input.activityId } : {}),
59
+ prompt: input.prompt,
60
+ });
61
+ return { session, run };
62
+ }
63
+ resume(input, scope = {}) {
64
+ const existing = this.store.getInScope(input.sessionId, scope);
65
+ if (!existing) {
66
+ throw new SubagentSessionError("subagent.session_not_found", `Unknown Subagent Session in this Workspace: ${input.sessionId}`);
67
+ }
68
+ if (existing.activeRun) {
69
+ throw new SubagentSessionError("subagent.busy", `Subagent Session ${existing.id} already has active Run ${existing.activeRun.id}.`);
70
+ }
71
+ if (!isSubagentProvider(existing.provider)) {
72
+ throw new Error(`Unknown subagent provider for existing session: ${existing.provider}`);
73
+ }
74
+ if (!subagentProviderContinuationSupported(existing.provider)) {
75
+ throw new SubagentSessionError("subagent.continuation_unsupported", `${existing.provider} does not support true Subagent Session continuation.`);
76
+ }
77
+ if (!existing.providerSessionId) {
78
+ throw new SubagentSessionError("subagent.continuation_unavailable", `Subagent Session ${existing.id} has no provider continuation identity.`);
79
+ }
80
+ assertSubagentProviderAvailable(existing.provider);
81
+ const runId = newRunId();
82
+ const startedAt = new Date().toISOString();
83
+ const run = {
84
+ id: runId,
85
+ status: "running",
86
+ ...(input.activityId ? { activityId: input.activityId } : {}),
87
+ startedAt,
88
+ };
89
+ const session = this.store.update(existing.id, {
90
+ status: "running",
91
+ activeRun: run,
92
+ });
93
+ this.launcher.launch({
94
+ sessionId: session.id,
95
+ runId,
96
+ ...(input.activityId ? { activityId: input.activityId } : {}),
97
+ prompt: input.prompt,
98
+ });
99
+ return { session, run };
100
+ }
101
+ close() {
102
+ this.store.close();
103
+ }
104
+ }
105
+ function newRunId() {
106
+ return `run_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
107
+ }
@@ -0,0 +1,85 @@
1
+ export function capabilityActivityAuditRequest(input) {
2
+ if (input.name !== "subagent.session") {
3
+ return {
4
+ workspaceId: input.workspaceId,
5
+ name: input.name,
6
+ action: "run",
7
+ arguments: input.arguments,
8
+ file: input.file,
9
+ };
10
+ }
11
+ const argumentsValue = isAuditRecord(input.arguments) ? input.arguments : {};
12
+ return {
13
+ workspaceId: input.workspaceId,
14
+ name: input.name,
15
+ action: "run",
16
+ arguments: {
17
+ operation: argumentsValue.operation,
18
+ ...(typeof argumentsValue.target === "string" ? { target: argumentsValue.target } : {}),
19
+ ...(typeof argumentsValue.sessionId === "string" ? { sessionId: argumentsValue.sessionId } : {}),
20
+ ...(typeof argumentsValue.model === "string" ? { model: argumentsValue.model } : {}),
21
+ ...(typeof argumentsValue.thinking === "string" ? { thinking: argumentsValue.thinking } : {}),
22
+ ...(typeof argumentsValue.prompt === "string" ? { promptLength: argumentsValue.prompt.length } : {}),
23
+ },
24
+ };
25
+ }
26
+ export function capabilityActivityAuditResult(name, result) {
27
+ if (name !== "subagent.session" || !isAuditRecord(result))
28
+ return result;
29
+ const structuredContent = isAuditRecord(result.structuredContent) ? result.structuredContent : undefined;
30
+ const capabilityResult = structuredContent && isAuditRecord(structuredContent.result)
31
+ ? structuredContent.result
32
+ : undefined;
33
+ const error = structuredContent && isAuditRecord(structuredContent.error)
34
+ ? structuredContent.error
35
+ : undefined;
36
+ return {
37
+ name,
38
+ action: "run",
39
+ ...(capabilityResult ? { result: summarizeSubagentCapabilityResult(capabilityResult) } : {}),
40
+ ...(error
41
+ ? {
42
+ error: {
43
+ code: error.code,
44
+ message: error.message,
45
+ },
46
+ }
47
+ : {}),
48
+ };
49
+ }
50
+ function isAuditRecord(value) {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+ function summarizeSubagentCapabilityResult(result) {
54
+ const summary = { operation: result.operation };
55
+ const session = isAuditRecord(result.session) ? result.session : undefined;
56
+ if (session) {
57
+ summary.session = {
58
+ id: session.id,
59
+ status: session.status,
60
+ profileName: session.profileName,
61
+ provider: session.provider,
62
+ model: session.model,
63
+ thinking: session.thinking,
64
+ };
65
+ }
66
+ for (const key of ["run", "activeRun", "latestRun"]) {
67
+ const run = isAuditRecord(result[key]) ? result[key] : undefined;
68
+ if (run)
69
+ summary[key] = { id: run.id, status: run.status };
70
+ }
71
+ if (Array.isArray(result.sessions)) {
72
+ summary.sessions = result.sessions.flatMap((entry) => {
73
+ if (!isAuditRecord(entry))
74
+ return [];
75
+ return [{
76
+ id: entry.id,
77
+ status: entry.status,
78
+ profileName: entry.profileName,
79
+ provider: entry.provider,
80
+ activeRunId: entry.activeRunId,
81
+ }];
82
+ });
83
+ }
84
+ return summary;
85
+ }
@@ -0,0 +1,19 @@
1
+ import { SubagentSessionCapability } from "../capability.js";
2
+ export function createSubagentMcpRuntime(config, activityLifecycle, options = {}) {
3
+ const capability = config.subagents
4
+ ? new SubagentSessionCapability(config, activityLifecycle, {
5
+ providerRunner: options.subagentProviderRunner,
6
+ })
7
+ : undefined;
8
+ return {
9
+ registryDependencies: capability
10
+ ? {
11
+ subagentSession: {
12
+ available: true,
13
+ run: (input, context, runOptions) => capability.run(input, context, runOptions),
14
+ },
15
+ }
16
+ : {},
17
+ decorateResult: (workspaceId, result) => capability?.decorateResult(workspaceId, result) ?? result,
18
+ };
19
+ }
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { resolve } from "node:path";
3
- import { openDatabase } from "./db/client.js";
4
- export class LocalAgentStore {
3
+ import { openDatabase } from "../../db/client.js";
4
+ export class SubagentSessionStore {
5
5
  database;
6
6
  constructor(stateDir) {
7
7
  this.database = openDatabase(stateDir);
@@ -27,7 +27,7 @@ export class LocalAgentStore {
27
27
  .prepare("select * from local_agent_sessions order by updated_at desc")
28
28
  .all();
29
29
  }
30
- return rows.map(rowToLocalAgentRecord);
30
+ return rows.map(rowToSubagentSession);
31
31
  }
32
32
  create(input) {
33
33
  const now = new Date().toISOString();
@@ -39,7 +39,17 @@ export class LocalAgentStore {
39
39
  provider: input.provider,
40
40
  model: input.model,
41
41
  thinking: input.thinking,
42
- status: "starting",
42
+ status: input.activeRun ? "running" : "idle",
43
+ ...(input.activeRun
44
+ ? {
45
+ activeRun: {
46
+ id: input.activeRun.id,
47
+ status: "running",
48
+ ...(input.activeRun.activityId ? { activityId: input.activeRun.activityId } : {}),
49
+ startedAt: input.activeRun.startedAt,
50
+ },
51
+ }
52
+ : {}),
43
53
  createdAt: now,
44
54
  updatedAt: now,
45
55
  };
@@ -52,11 +62,21 @@ export class LocalAgentStore {
52
62
  provider,
53
63
  model,
54
64
  thinking,
65
+ provider_session_id,
55
66
  status,
67
+ active_run_id,
68
+ active_activity_id,
69
+ active_run_started_at,
70
+ latest_run_id,
71
+ latest_run_outcome,
72
+ latest_run_finished_at,
73
+ latest_response,
74
+ error,
75
+ hook_reports_json,
56
76
  created_at,
57
77
  updated_at
58
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
59
- .run(record.id, record.workspaceId ?? null, record.workspaceRoot, record.profileName, record.provider, record.model ?? null, record.thinking ?? null, record.status, record.createdAt, record.updatedAt);
78
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, null, null, null, ?, ?)`)
79
+ .run(record.id, record.workspaceId ?? null, record.workspaceRoot, record.profileName, record.provider, record.model ?? null, record.thinking ?? null, null, record.status, record.activeRun?.id ?? null, record.activeRun?.activityId ?? null, record.activeRun?.startedAt ?? null, null, null, null, record.createdAt, record.updatedAt);
60
80
  return record;
61
81
  }
62
82
  get(idOrPrefix) {
@@ -66,13 +86,23 @@ export class LocalAgentStore {
66
86
  limit 1`)
67
87
  .get(idOrPrefix, idOrPrefix);
68
88
  if (exact)
69
- return rowToLocalAgentRecord(exact);
89
+ return rowToSubagentSession(exact);
70
90
  const matches = this.database.sqlite
71
91
  .prepare(`select * from local_agent_sessions
72
92
  where id like ? escape '\\' or provider_session_id like ? escape '\\'
73
93
  order by updated_at desc`)
74
94
  .all(`${escapeLike(idOrPrefix)}%`, `${escapeLike(idOrPrefix)}%`);
75
- return matches.length === 1 ? rowToLocalAgentRecord(matches[0]) : undefined;
95
+ return matches.length === 1 ? rowToSubagentSession(matches[0]) : undefined;
96
+ }
97
+ getInScope(idOrPrefix, scope) {
98
+ const session = this.get(idOrPrefix);
99
+ if (!session)
100
+ return undefined;
101
+ if (scope.workspaceId && session.workspaceId !== scope.workspaceId)
102
+ return undefined;
103
+ if (scope.workspaceRoot && session.workspaceRoot !== resolve(scope.workspaceRoot))
104
+ return undefined;
105
+ return session;
76
106
  }
77
107
  update(id, patch) {
78
108
  const current = this.getById(id);
@@ -93,12 +123,18 @@ export class LocalAgentStore {
93
123
  thinking = ?,
94
124
  provider_session_id = ?,
95
125
  status = ?,
96
- latest_response = ?,
97
- error = ?,
98
- hook_reports_json = ?,
126
+ active_run_id = ?,
127
+ active_activity_id = ?,
128
+ active_run_started_at = ?,
129
+ latest_run_id = ?,
130
+ latest_run_outcome = ?,
131
+ latest_run_finished_at = ?,
132
+ latest_response = null,
133
+ error = null,
134
+ hook_reports_json = null,
99
135
  updated_at = ?
100
136
  where id = ?`)
101
- .run(updated.workspaceId ?? null, resolve(updated.workspaceRoot), updated.profileName, updated.provider, updated.model ?? null, updated.thinking ?? null, updated.providerSessionId ?? null, updated.status, updated.latestResponse ?? null, updated.error ?? null, updated.hookReports ? JSON.stringify(updated.hookReports) : null, updated.updatedAt, updated.id);
137
+ .run(updated.workspaceId ?? null, resolve(updated.workspaceRoot), updated.profileName, updated.provider, updated.model ?? null, updated.thinking ?? null, updated.providerSessionId ?? null, updated.status, updated.activeRun?.id ?? null, updated.activeRun?.activityId ?? null, updated.activeRun?.startedAt ?? null, updated.latestRun?.id ?? null, updated.latestRun && updated.latestRun.status !== "running" ? updated.latestRun.status : null, updated.latestRun?.finishedAt ?? null, updated.updatedAt, updated.id);
102
138
  return updated;
103
139
  }
104
140
  close() {
@@ -108,13 +144,29 @@ export class LocalAgentStore {
108
144
  const row = this.database.sqlite
109
145
  .prepare("select * from local_agent_sessions where id = ?")
110
146
  .get(id);
111
- return row ? rowToLocalAgentRecord(row) : undefined;
147
+ return row ? rowToSubagentSession(row) : undefined;
112
148
  }
113
149
  }
114
- export function createLocalAgentStore(config) {
115
- return new LocalAgentStore(config.stateDir);
150
+ export function createSubagentSessionStore(config) {
151
+ return new SubagentSessionStore(config.stateDir);
116
152
  }
117
- function rowToLocalAgentRecord(row) {
153
+ function rowToSubagentSession(row) {
154
+ const activeRun = row.active_run_id
155
+ ? {
156
+ id: row.active_run_id,
157
+ status: "running",
158
+ ...(row.active_activity_id ? { activityId: row.active_activity_id } : {}),
159
+ ...(row.active_run_started_at ? { startedAt: row.active_run_started_at } : {}),
160
+ }
161
+ : undefined;
162
+ const latestOutcome = readOutcome(row.latest_run_outcome);
163
+ const latestRun = row.latest_run_id && latestOutcome
164
+ ? {
165
+ id: row.latest_run_id,
166
+ status: latestOutcome,
167
+ ...(row.latest_run_finished_at ? { finishedAt: row.latest_run_finished_at } : {}),
168
+ }
169
+ : undefined;
118
170
  return {
119
171
  id: row.id,
120
172
  workspaceId: row.workspace_id ?? undefined,
@@ -124,34 +176,18 @@ function rowToLocalAgentRecord(row) {
124
176
  model: row.model ?? undefined,
125
177
  thinking: row.thinking ?? undefined,
126
178
  providerSessionId: row.provider_session_id ?? undefined,
127
- status: readStatus(row.status),
128
- latestResponse: row.latest_response ?? undefined,
129
- error: row.error ?? undefined,
130
- hookReports: parseHookReports(row.hook_reports_json),
179
+ status: activeRun || row.status === "starting" || row.status === "running" ? "running" : "idle",
180
+ ...(activeRun ? { activeRun } : {}),
181
+ ...(latestRun ? { latestRun } : {}),
131
182
  createdAt: row.created_at,
132
183
  updatedAt: row.updated_at,
133
184
  };
134
185
  }
135
- function parseHookReports(value) {
136
- if (!value)
137
- return undefined;
138
- try {
139
- const parsed = JSON.parse(value);
140
- return Array.isArray(parsed) ? parsed : undefined;
141
- }
142
- catch {
143
- return undefined;
144
- }
145
- }
146
- function readStatus(status) {
147
- if (status === "starting" ||
148
- status === "running" ||
149
- status === "idle" ||
150
- status === "error" ||
151
- status === "stopped") {
152
- return status;
186
+ function readOutcome(value) {
187
+ if (value === "succeeded" || value === "failed" || value === "cancelled" || value === "interrupted") {
188
+ return value;
153
189
  }
154
- return "error";
190
+ return undefined;
155
191
  }
156
192
  function escapeLike(value) {
157
193
  return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");