@akira-tl/forgerelay 0.7.1 → 0.7.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 +22 -0
- package/capabilities/subagents/GUIDE.md +13 -3
- package/dist/capability-registry.js +8 -0
- package/dist/cli.js +3 -1
- package/dist/db/migrations.js +10 -0
- package/dist/db/schema.js +2 -0
- package/dist/server.js +1 -0
- package/dist/subagents/providers/adapters/acp.js +3 -1
- package/dist/subagents/providers/adapters/claude.js +38 -31
- package/dist/subagents/providers/adapters/codex.js +1 -1
- package/dist/subagents/providers/adapters/opencode.js +41 -14
- package/dist/subagents/providers/adapters/pi.js +22 -8
- package/dist/subagents/providers/shared.js +26 -0
- package/dist/subagents/sessions/capability.js +110 -5
- package/dist/subagents/sessions/delivery-mailbox.js +3 -0
- package/dist/subagents/sessions/execution.js +56 -59
- package/dist/subagents/sessions/manager.js +50 -6
- package/dist/subagents/sessions/mcp/runtime.js +1 -0
- package/dist/subagents/sessions/store.js +23 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.7.4] - 2026-08-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Subagent Run restart reconciliation now preserves live owners and marks stale active Runs `interrupted` without replaying the old prompt.
|
|
12
|
+
- First-class Subagent Sessions now work through Workspace Relay and Composite explicit-member routing while keeping Session state on the Execution ForgeRelay.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Composite Capability result remapping now preserves the declared output schema during relayed Subagent operations.
|
|
17
|
+
|
|
18
|
+
## [0.7.2] - 2026-08-30
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `subagent.session stop/delete` 提供真实 Run 取消和显式 ForgeRelay Session 清理;取消后可继续 resume,active Session 不能直接 delete。
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- `SubagentStart` / `SubagentStop` 按 Run 触发,只携带有界身份与状态元数据;不传 prompt、response 或 provider session ID。
|
|
27
|
+
- `delete` 只清理 ForgeRelay coordination/mailbox,provider-native conversation history 保持不变。
|
|
28
|
+
|
|
7
29
|
## [0.7.1] - 2026-08-30
|
|
8
30
|
|
|
9
31
|
### Added
|
|
@@ -13,12 +13,14 @@ name = subagent.session
|
|
|
13
13
|
action = run
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
当前支持六个 operation:
|
|
17
17
|
|
|
18
18
|
- `start`:创建 Subagent Session,并立即启动第一个 Subagent Run;
|
|
19
19
|
- `resume`:在支持真实 continuation 的 provider 上继续现有 Session;
|
|
20
20
|
- `status`:读取当前 Workspace 中一个 Session 的协调状态;
|
|
21
|
-
- `list`:列出当前 Workspace 拥有的 Session
|
|
21
|
+
- `list`:列出当前 Workspace 拥有的 Session 摘要;
|
|
22
|
+
- `stop`:取消当前 active Run,但保留 Session;
|
|
23
|
+
- `delete`:显式删除 idle Session 的 ForgeRelay coordination state。
|
|
22
24
|
|
|
23
25
|
`subagent.session` 不支持 `batch.execute`。
|
|
24
26
|
|
|
@@ -82,7 +84,13 @@ action = run
|
|
|
82
84
|
|
|
83
85
|
Session 受实际 Execution Workspace 所有权约束;Session ID 不是跨 Workspace 的访问凭证。`status` / `resume` 都只能访问当前实际 Execution Workspace 拥有的 Session,`list` 也只返回当前 Workspace 的紧凑摘要。
|
|
84
86
|
|
|
85
|
-
Session 返回中会给出 `continuationSupported` 与 `resumable`;`open_workspace` 的 provider metadata 也会明确给出 `continuationSupported
|
|
87
|
+
Session 返回中会给出 `continuationSupported` 与 `resumable`;`open_workspace` 的 provider metadata 也会明确给出 `continuationSupported`。不要用新的 Core MCP tool 绕过 Capability Gateway。
|
|
88
|
+
|
|
89
|
+
## stop / delete
|
|
90
|
+
|
|
91
|
+
`stop` 只取消当前 active Run,并等待 provider execution 真正观察取消后才返回。取消完成后 Run 状态为 `cancelled`,Session 回到 `idle`;如果已有 provider continuation identity,之后仍可 `resume`。对 idle Session 调用 `stop` 是幂等的。
|
|
92
|
+
|
|
93
|
+
`delete` 只允许 idle Session。它只删除 ForgeRelay 的 Session mapping 和尚未领取的 delivery mailbox,不删除或修改 Claude Code、Codex、OpenCode、Pi、Cursor/Copilot 等 provider-native conversation/session history。ForgeRelay 不自动清理 Subagent Session;只有显式 `delete` 才删除协调状态。
|
|
86
94
|
|
|
87
95
|
## 后台完成与结果交付
|
|
88
96
|
|
|
@@ -95,6 +103,8 @@ Run 完成后:
|
|
|
95
103
|
- 成功领取后 mailbox 条目立即删除,同一个 completion 不重复交付;
|
|
96
104
|
- 未领取 completion 可以跨正常 ForgeRelay 进程重启保留。
|
|
97
105
|
|
|
106
|
+
ForgeRelay 只为 active Run 持久化最小 execution-owner 元数据(owner identity / PID),不保存 delegated prompt。后续 Session 操作会先做 restart reconciliation:能够证明 owner 仍存活的 Run 保持 `running`;无法证明仍有执行 owner 的 Run 一次性转为 `interrupted`,Session 回到 `idle`。ForgeRelay 绝不自动 replay 被中断 Run 的旧 prompt;如果 provider continuation 仍有效,由 Host 显式 `resume` 新 prompt。
|
|
107
|
+
|
|
98
108
|
如果需要等待结果,使用 `status` 进行有节制的后续查询;不要高频短轮询。
|
|
99
109
|
|
|
100
110
|
## 数据所有权
|
|
@@ -147,6 +147,14 @@ export function createCapabilityRegistry(dependencies) {
|
|
|
147
147
|
operation: z.literal("status"),
|
|
148
148
|
sessionId: z.string().min(1),
|
|
149
149
|
}).strict(),
|
|
150
|
+
z.object({
|
|
151
|
+
operation: z.literal("stop"),
|
|
152
|
+
sessionId: z.string().min(1),
|
|
153
|
+
}).strict(),
|
|
154
|
+
z.object({
|
|
155
|
+
operation: z.literal("delete"),
|
|
156
|
+
sessionId: z.string().min(1),
|
|
157
|
+
}).strict(),
|
|
150
158
|
z.object({ operation: z.literal("list") }).strict(),
|
|
151
159
|
]);
|
|
152
160
|
const codeIntelligenceInput = z.discriminatedUnion("operation", [
|
package/dist/cli.js
CHANGED
|
@@ -580,7 +580,8 @@ function createCliSubagentSessionManager(config = loadConfig()) {
|
|
|
580
580
|
return new SubagentSessionManager(config, {
|
|
581
581
|
launch(request) {
|
|
582
582
|
const promptFile = writeSubagentPromptFile(request.prompt);
|
|
583
|
-
spawnSubagentWorker(request.sessionId, promptFile);
|
|
583
|
+
const pid = spawnSubagentWorker(request.sessionId, promptFile);
|
|
584
|
+
return pid === undefined ? undefined : { id: `subagent-worker-${request.runId}`, pid };
|
|
584
585
|
},
|
|
585
586
|
});
|
|
586
587
|
}
|
|
@@ -599,6 +600,7 @@ function spawnSubagentWorker(sessionId, promptFile) {
|
|
|
599
600
|
env: process.env,
|
|
600
601
|
});
|
|
601
602
|
child.unref();
|
|
603
|
+
return child.pid;
|
|
602
604
|
}
|
|
603
605
|
function writeSubagentPromptFile(prompt) {
|
|
604
606
|
const directory = mkdtempSync(join(tmpdir(), "forgerelay-agent-prompt-"));
|
package/dist/db/migrations.js
CHANGED
|
@@ -69,6 +69,11 @@ const migrations = [
|
|
|
69
69
|
name: "subagent-session-coordination",
|
|
70
70
|
up: migrateSubagentSessionCoordination,
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
version: 15,
|
|
74
|
+
name: "subagent-run-ownership",
|
|
75
|
+
up: migrateSubagentRunOwnership,
|
|
76
|
+
},
|
|
72
77
|
];
|
|
73
78
|
export function migrateDatabase(sqlite) {
|
|
74
79
|
const migrate = sqlite.transaction(() => {
|
|
@@ -358,6 +363,11 @@ function migrateSubagentSessionCoordination(sqlite) {
|
|
|
358
363
|
addColumnIfMissing(sqlite, "local_agent_sessions", "latest_run_outcome", "text");
|
|
359
364
|
addColumnIfMissing(sqlite, "local_agent_sessions", "latest_run_finished_at", "text");
|
|
360
365
|
}
|
|
366
|
+
function migrateSubagentRunOwnership(sqlite) {
|
|
367
|
+
migrateSubagentSessionCoordination(sqlite);
|
|
368
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "active_owner_id", "text");
|
|
369
|
+
addColumnIfMissing(sqlite, "local_agent_sessions", "active_owner_pid", "integer");
|
|
370
|
+
}
|
|
361
371
|
function migrateActivityHostTurnWorkspace(sqlite) {
|
|
362
372
|
migrateActivityHostTurns(sqlite);
|
|
363
373
|
addColumnIfMissing(sqlite, "activity_host_turns", "workspace_id", "text");
|
package/dist/db/schema.js
CHANGED
|
@@ -155,6 +155,8 @@ export const localAgentSessions = sqliteTable("local_agent_sessions", {
|
|
|
155
155
|
activeRunId: text("active_run_id"),
|
|
156
156
|
activeActivityId: text("active_activity_id"),
|
|
157
157
|
activeRunStartedAt: text("active_run_started_at"),
|
|
158
|
+
activeOwnerId: text("active_owner_id"),
|
|
159
|
+
activeOwnerPid: integer("active_owner_pid"),
|
|
158
160
|
latestRunId: text("latest_run_id"),
|
|
159
161
|
latestRunOutcome: text("latest_run_outcome"),
|
|
160
162
|
latestRunFinishedAt: text("latest_run_finished_at"),
|
package/dist/server.js
CHANGED
|
@@ -2764,6 +2764,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
|
|
|
2764
2764
|
outputSchema: {
|
|
2765
2765
|
name: z.string(),
|
|
2766
2766
|
action: z.enum(["describe", "run"]),
|
|
2767
|
+
member: z.string().optional(),
|
|
2767
2768
|
capability: z.unknown().optional(),
|
|
2768
2769
|
result: z.unknown().optional(),
|
|
2769
2770
|
error: capabilityErrorOutputSchema.optional(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { Readable, Writable } from "node:stream";
|
|
3
|
-
import { asRecord, assertPipedChild, directString, errorMessage, readArray, } from "../shared.js";
|
|
3
|
+
import { asRecord, assertPipedChild, directString, errorMessage, readArray, terminateChildOnAbort, } from "../shared.js";
|
|
4
4
|
const ACP_COMMANDS = {
|
|
5
5
|
cursor: ["cursor-agent", "acp"],
|
|
6
6
|
copilot: ["copilot", "--acp"],
|
|
@@ -24,6 +24,7 @@ export class AcpSubagentAdapter {
|
|
|
24
24
|
windowsHide: true,
|
|
25
25
|
});
|
|
26
26
|
assertPipedChild(child);
|
|
27
|
+
const detachAbort = terminateChildOnAbort(child, input.signal);
|
|
27
28
|
let stderr = "";
|
|
28
29
|
child.stderr.on("data", (chunk) => {
|
|
29
30
|
stderr += chunk.toString("utf8");
|
|
@@ -80,6 +81,7 @@ export class AcpSubagentAdapter {
|
|
|
80
81
|
throw new Error(`${this.provider} ACP run failed: ${errorMessage(error)}${stderr ? `\n${stderr.trim()}` : ""}`);
|
|
81
82
|
}
|
|
82
83
|
finally {
|
|
84
|
+
detachAbort();
|
|
83
85
|
child.kill();
|
|
84
86
|
}
|
|
85
87
|
}
|
|
@@ -1,42 +1,49 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { directString, requireFinalResponse } from "../shared.js";
|
|
2
|
+
import { directString, linkedAbortController, requireFinalResponse } from "../shared.js";
|
|
3
3
|
export class ClaudeSubagentAdapter {
|
|
4
4
|
provider = "claude";
|
|
5
5
|
async run(input) {
|
|
6
6
|
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
7
7
|
const claudeExecutable = process.env.CLAUDE_COMMAND ?? resolveExecutable("claude");
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
8
|
+
const linkedAbort = linkedAbortController(input.signal);
|
|
9
|
+
try {
|
|
10
|
+
const messages = query({
|
|
11
|
+
prompt: input.prompt,
|
|
12
|
+
options: {
|
|
13
|
+
cwd: input.workspace,
|
|
14
|
+
model: input.model,
|
|
15
|
+
...(input.thinking ? { thinking: { type: "adaptive" }, effort: input.thinking } : {}),
|
|
16
|
+
resume: input.providerSessionId,
|
|
17
|
+
permissionMode: "bypassPermissions",
|
|
18
|
+
allowDangerouslySkipPermissions: true,
|
|
19
|
+
env: claudeCommandEnvironment(process.env),
|
|
20
|
+
...(linkedAbort.controller ? { abortController: linkedAbort.controller } : {}),
|
|
21
|
+
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
let providerSessionId = input.providerSessionId ?? null;
|
|
25
|
+
let finalResponse = "";
|
|
26
|
+
for await (const message of messages) {
|
|
27
|
+
const record = message;
|
|
28
|
+
if (typeof record.session_id === "string")
|
|
29
|
+
providerSessionId = record.session_id;
|
|
30
|
+
if (record.type === "result" && typeof record.result === "string") {
|
|
31
|
+
const resultError = claudeResultError(record);
|
|
32
|
+
if (resultError)
|
|
33
|
+
throw new Error(resultError);
|
|
34
|
+
finalResponse = record.result;
|
|
35
|
+
}
|
|
32
36
|
}
|
|
37
|
+
finalResponse = requireFinalResponse("Claude", finalResponse);
|
|
38
|
+
return {
|
|
39
|
+
provider: this.provider,
|
|
40
|
+
providerSessionId,
|
|
41
|
+
finalResponse,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
linkedAbort.dispose();
|
|
33
46
|
}
|
|
34
|
-
finalResponse = requireFinalResponse("Claude", finalResponse);
|
|
35
|
-
return {
|
|
36
|
-
provider: this.provider,
|
|
37
|
-
providerSessionId,
|
|
38
|
-
finalResponse,
|
|
39
|
-
};
|
|
40
47
|
}
|
|
41
48
|
}
|
|
42
49
|
function claudeResultError(record) {
|
|
@@ -29,7 +29,7 @@ export class CodexSdkSubagentRuntime {
|
|
|
29
29
|
const thread = input.providerSessionId
|
|
30
30
|
? this.codex.resumeThread(input.providerSessionId, options)
|
|
31
31
|
: this.codex.startThread(options);
|
|
32
|
-
const turn = await thread.run(input.prompt);
|
|
32
|
+
const turn = await thread.run(input.prompt, { signal: input.signal });
|
|
33
33
|
return {
|
|
34
34
|
provider: this.provider,
|
|
35
35
|
providerSessionId: thread.id,
|
|
@@ -6,15 +6,31 @@ export class OpencodeSubagentAdapter {
|
|
|
6
6
|
const { client, server } = await createOpencode();
|
|
7
7
|
try {
|
|
8
8
|
const sessionId = input.providerSessionId ?? await createOpencodeSession(client, input);
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const finalResponse = requireFinalResponse("OpenCode", extractOpenCodeFinalResponse(messages) || extractOpenCodeFinalResponse(promptResult));
|
|
13
|
-
return {
|
|
14
|
-
provider: this.provider,
|
|
15
|
-
providerSessionId: sessionId,
|
|
16
|
-
finalResponse,
|
|
9
|
+
let abortPromise;
|
|
10
|
+
const abort = () => {
|
|
11
|
+
abortPromise ??= abortOpencodeSession(client, sessionId, input.workspace);
|
|
17
12
|
};
|
|
13
|
+
if (input.signal?.aborted)
|
|
14
|
+
abort();
|
|
15
|
+
else
|
|
16
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
17
|
+
try {
|
|
18
|
+
input.signal?.throwIfAborted();
|
|
19
|
+
const promptResult = await promptOpencodeSession(client, sessionId, input);
|
|
20
|
+
await waitForOpencodeSession(client, sessionId, input.signal);
|
|
21
|
+
const messages = await readOpencodeMessages(client, sessionId, input.signal);
|
|
22
|
+
const finalResponse = requireFinalResponse("OpenCode", extractOpenCodeFinalResponse(messages) || extractOpenCodeFinalResponse(promptResult));
|
|
23
|
+
return {
|
|
24
|
+
provider: this.provider,
|
|
25
|
+
providerSessionId: sessionId,
|
|
26
|
+
finalResponse,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
input.signal?.removeEventListener("abort", abort);
|
|
31
|
+
if (abortPromise)
|
|
32
|
+
await abortPromise;
|
|
33
|
+
}
|
|
18
34
|
}
|
|
19
35
|
finally {
|
|
20
36
|
server.close();
|
|
@@ -27,7 +43,7 @@ async function createOpencodeSession(client, input) {
|
|
|
27
43
|
directory: input.workspace,
|
|
28
44
|
location: { directory: input.workspace },
|
|
29
45
|
...(input.model ? { model: parseOpencodeModel(input.model) } : {}),
|
|
30
|
-
}, { throwOnError: true });
|
|
46
|
+
}, { throwOnError: true, signal: input.signal });
|
|
31
47
|
const id = readNestedString(result, ["id"]) ??
|
|
32
48
|
readNestedString(result, ["data", "id"]) ??
|
|
33
49
|
readNestedString(result, ["session", "id"]) ??
|
|
@@ -47,19 +63,30 @@ async function promptOpencodeSession(client, sessionId, input) {
|
|
|
47
63
|
...(input.model ? { model: parseOpencodeModel(input.model) } : {}),
|
|
48
64
|
...(input.thinking ? { variant: input.thinking } : {}),
|
|
49
65
|
};
|
|
50
|
-
return session.prompt(promptInput, { throwOnError: true });
|
|
66
|
+
return session.prompt(promptInput, { throwOnError: true, signal: input.signal });
|
|
51
67
|
}
|
|
52
|
-
async function waitForOpencodeSession(client, sessionId) {
|
|
68
|
+
async function waitForOpencodeSession(client, sessionId, signal) {
|
|
53
69
|
const session = client.session;
|
|
54
70
|
if (!session?.wait)
|
|
55
71
|
return;
|
|
56
|
-
await session.wait({ sessionID: sessionId }, { throwOnError: true });
|
|
72
|
+
await session.wait({ sessionID: sessionId }, { throwOnError: true, signal });
|
|
57
73
|
}
|
|
58
|
-
async function readOpencodeMessages(client, sessionId) {
|
|
74
|
+
async function readOpencodeMessages(client, sessionId, signal) {
|
|
59
75
|
const session = client.session;
|
|
60
76
|
if (!session?.messages)
|
|
61
77
|
return undefined;
|
|
62
|
-
return session.messages({ sessionID: sessionId, order: "asc", limit: 100 }, { throwOnError: true });
|
|
78
|
+
return session.messages({ sessionID: sessionId, order: "asc", limit: 100 }, { throwOnError: true, signal });
|
|
79
|
+
}
|
|
80
|
+
async function abortOpencodeSession(client, sessionId, workspace) {
|
|
81
|
+
const session = client.session;
|
|
82
|
+
if (!session?.abort)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
await session.abort({ sessionID: sessionId, directory: workspace }, { throwOnError: true });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// The prompt request may already have observed the AbortSignal and closed the local server.
|
|
89
|
+
}
|
|
63
90
|
}
|
|
64
91
|
function parseOpencodeModel(model) {
|
|
65
92
|
const separator = model.indexOf("/");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { removeDevspaceNodeModulesBinFromPath } from "../path.js";
|
|
3
|
-
import { asRecord, assertPipedChild, errorMessage, readArray, readNestedString, requireFinalResponse, unwrapProviderPayload, } from "../shared.js";
|
|
3
|
+
import { asRecord, assertPipedChild, errorMessage, readArray, readNestedString, requireFinalResponse, terminateChildOnAbort, unwrapProviderPayload, } from "../shared.js";
|
|
4
4
|
const PI_AGENT_TIMEOUT_MS = 120_000;
|
|
5
5
|
export class PiRpcSubagentAdapter {
|
|
6
6
|
provider = "pi";
|
|
@@ -19,6 +19,7 @@ export class PiRpcSubagentAdapter {
|
|
|
19
19
|
windowsHide: true,
|
|
20
20
|
});
|
|
21
21
|
assertPipedChild(child);
|
|
22
|
+
const detachAbort = terminateChildOnAbort(child, input.signal);
|
|
22
23
|
const rpc = new JsonLineRpc(child);
|
|
23
24
|
let streamingText = "";
|
|
24
25
|
let streamingProviderError = "";
|
|
@@ -33,7 +34,7 @@ export class PiRpcSubagentAdapter {
|
|
|
33
34
|
try {
|
|
34
35
|
const state = await rpc.request({ type: "get_state" });
|
|
35
36
|
const providerSessionId = readNestedString(state, ["sessionId"]) ?? input.providerSessionId ?? null;
|
|
36
|
-
const done = rpc.waitForEvent((event) => asRecord(event)?.type === "agent_end", PI_AGENT_TIMEOUT_MS);
|
|
37
|
+
const done = rpc.waitForEvent((event) => asRecord(event)?.type === "agent_end", PI_AGENT_TIMEOUT_MS, input.signal);
|
|
37
38
|
await rpc.request({ type: "prompt", message: input.prompt });
|
|
38
39
|
const agentEnd = await done;
|
|
39
40
|
const sessionMessages = await rpc.request({ type: "get_messages" });
|
|
@@ -55,6 +56,7 @@ export class PiRpcSubagentAdapter {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
finally {
|
|
59
|
+
detachAbort();
|
|
58
60
|
child.kill();
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -103,19 +105,31 @@ class JsonLineRpc {
|
|
|
103
105
|
this.eventSubscribers.add(callback);
|
|
104
106
|
return () => this.eventSubscribers.delete(callback);
|
|
105
107
|
}
|
|
106
|
-
waitForEvent(predicate, timeoutMs) {
|
|
108
|
+
waitForEvent(predicate, timeoutMs, signal) {
|
|
107
109
|
return new Promise((resolve, reject) => {
|
|
108
|
-
const
|
|
110
|
+
const finish = (callback) => {
|
|
111
|
+
clearTimeout(timer);
|
|
109
112
|
unsubscribe();
|
|
113
|
+
signal?.removeEventListener("abort", abort);
|
|
114
|
+
callback();
|
|
115
|
+
};
|
|
116
|
+
const abort = () => finish(() => {
|
|
117
|
+
const error = new Error("Pi RPC cancelled.");
|
|
118
|
+
error.name = "AbortError";
|
|
119
|
+
reject(error);
|
|
120
|
+
});
|
|
121
|
+
const timer = setTimeout(() => finish(() => {
|
|
110
122
|
reject(new Error(`Pi RPC timed out waiting for agent completion\n${this.stderr}`.trim()));
|
|
111
|
-
}, timeoutMs);
|
|
123
|
+
}), timeoutMs);
|
|
112
124
|
const unsubscribe = this.onEvent((event) => {
|
|
113
125
|
if (!predicate(event))
|
|
114
126
|
return;
|
|
115
|
-
|
|
116
|
-
unsubscribe();
|
|
117
|
-
resolve(event);
|
|
127
|
+
finish(() => resolve(event));
|
|
118
128
|
});
|
|
129
|
+
if (signal?.aborted)
|
|
130
|
+
abort();
|
|
131
|
+
else
|
|
132
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
119
133
|
});
|
|
120
134
|
}
|
|
121
135
|
handleStdout(chunk) {
|
|
@@ -38,3 +38,29 @@ export function requireFinalResponse(provider, response) {
|
|
|
38
38
|
}
|
|
39
39
|
return trimmed;
|
|
40
40
|
}
|
|
41
|
+
export function linkedAbortController(signal) {
|
|
42
|
+
if (!signal)
|
|
43
|
+
return { dispose() { } };
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const abort = () => controller.abort(signal.reason);
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
abort();
|
|
48
|
+
else
|
|
49
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
50
|
+
return {
|
|
51
|
+
controller,
|
|
52
|
+
dispose() {
|
|
53
|
+
signal.removeEventListener("abort", abort);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function terminateChildOnAbort(child, signal) {
|
|
58
|
+
if (!signal)
|
|
59
|
+
return () => { };
|
|
60
|
+
const abort = () => child.kill();
|
|
61
|
+
if (signal.aborted)
|
|
62
|
+
abort();
|
|
63
|
+
else
|
|
64
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
65
|
+
return () => signal.removeEventListener("abort", abort);
|
|
66
|
+
}
|
|
@@ -9,17 +9,23 @@ export class SubagentSessionCapability {
|
|
|
9
9
|
activityLifecycle;
|
|
10
10
|
mailbox;
|
|
11
11
|
providerRunner;
|
|
12
|
+
ownerAliveOverride;
|
|
13
|
+
activeRuns = new Map();
|
|
12
14
|
constructor(config, activityLifecycle, options = {}) {
|
|
13
15
|
this.config = config;
|
|
14
16
|
this.activityLifecycle = activityLifecycle;
|
|
15
17
|
this.mailbox = new SubagentDeliveryMailbox(config.stateDir);
|
|
16
18
|
this.providerRunner = options.providerRunner ?? defaultProviderRunner;
|
|
19
|
+
this.ownerAliveOverride = options.ownerAlive;
|
|
17
20
|
}
|
|
18
21
|
async run(input, context, options) {
|
|
19
22
|
const manager = new SubagentSessionManager(this.config, {
|
|
20
23
|
launch: (request) => this.launch(request),
|
|
21
24
|
});
|
|
22
25
|
try {
|
|
26
|
+
const reconciled = manager.reconcile({ workspaceId: context.workspaceId }, (run) => this.ownerAlive(run));
|
|
27
|
+
for (const entry of reconciled)
|
|
28
|
+
this.recordInterruption(entry, options.activityId);
|
|
23
29
|
switch (input.operation) {
|
|
24
30
|
case "start": {
|
|
25
31
|
const started = await manager.start({
|
|
@@ -67,6 +73,18 @@ export class SubagentSessionCapability {
|
|
|
67
73
|
},
|
|
68
74
|
};
|
|
69
75
|
}
|
|
76
|
+
case "stop":
|
|
77
|
+
return { value: await this.stop(manager, input.sessionId, context.workspaceId) };
|
|
78
|
+
case "delete": {
|
|
79
|
+
const deleted = manager.delete(input.sessionId, { workspaceId: context.workspaceId });
|
|
80
|
+
this.mailbox.discardSession(deleted.id);
|
|
81
|
+
return {
|
|
82
|
+
value: {
|
|
83
|
+
operation: "delete",
|
|
84
|
+
deletedSessionId: deleted.id,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
70
88
|
case "list":
|
|
71
89
|
return {
|
|
72
90
|
value: {
|
|
@@ -105,12 +123,99 @@ export class SubagentSessionCapability {
|
|
|
105
123
|
};
|
|
106
124
|
}
|
|
107
125
|
launch(request) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
const ownerId = `subagent-owner-${process.pid}-${request.runId}`;
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const completion = executeSubagentRun(this.config, { ...request, signal: controller.signal }, this.providerRunner).then((result) => {
|
|
129
|
+
this.recordCompletion(result);
|
|
130
|
+
return result;
|
|
131
|
+
});
|
|
132
|
+
this.activeRuns.set(request.runId, { ownerId, controller, completion });
|
|
133
|
+
void completion.finally(() => {
|
|
134
|
+
this.activeRuns.delete(request.runId);
|
|
135
|
+
}).catch(() => {
|
|
136
|
+
// Unexpected orchestration failures surface through later reconciliation.
|
|
137
|
+
});
|
|
138
|
+
return { id: ownerId, pid: process.pid };
|
|
139
|
+
}
|
|
140
|
+
async stop(manager, sessionId, workspaceId) {
|
|
141
|
+
let session = manager.get(sessionId, { workspaceId });
|
|
142
|
+
if (!session) {
|
|
143
|
+
throw new SubagentSessionError("subagent.session_not_found", `Unknown Subagent Session in this Workspace: ${sessionId}`);
|
|
144
|
+
}
|
|
145
|
+
const activeRun = session.activeRun;
|
|
146
|
+
if (!activeRun) {
|
|
147
|
+
return { operation: "stop", session: publicSession(session) };
|
|
148
|
+
}
|
|
149
|
+
const handle = this.activeRuns.get(activeRun.id);
|
|
150
|
+
if (!handle) {
|
|
151
|
+
session = manager.get(session.id, { workspaceId }) ?? session;
|
|
152
|
+
if (!session.activeRun)
|
|
153
|
+
return { operation: "stop", session: publicSession(session) };
|
|
154
|
+
throw new SubagentSessionError("subagent.cancel_unavailable", `Subagent Run ${activeRun.id} has no live cancellation owner.`);
|
|
155
|
+
}
|
|
156
|
+
handle.controller.abort(new Error(`Subagent Run ${activeRun.id} cancelled by stop.`));
|
|
157
|
+
await handle.completion;
|
|
158
|
+
session = manager.get(session.id, { workspaceId });
|
|
159
|
+
if (!session) {
|
|
160
|
+
throw new SubagentSessionError("subagent.session_not_found", `Unknown Subagent Session in this Workspace: ${sessionId}`);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
operation: "stop",
|
|
164
|
+
session: publicSession(session),
|
|
165
|
+
...(session.latestRun?.id === activeRun.id ? { run: publicRun(session.latestRun) } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
ownerAlive(run) {
|
|
169
|
+
if (this.ownerAliveOverride)
|
|
170
|
+
return this.ownerAliveOverride(run);
|
|
171
|
+
if (!run.ownerId || run.ownerPid === undefined)
|
|
172
|
+
return false;
|
|
173
|
+
const active = this.activeRuns.get(run.id);
|
|
174
|
+
if (run.ownerPid === process.pid)
|
|
175
|
+
return active?.ownerId === run.ownerId;
|
|
176
|
+
try {
|
|
177
|
+
process.kill(run.ownerPid, 0);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return error.code === "EPERM";
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
recordInterruption(entry, fallbackActivityId) {
|
|
185
|
+
const sourceActivityId = entry.run.activityId ?? fallbackActivityId;
|
|
186
|
+
if (!sourceActivityId)
|
|
187
|
+
return;
|
|
188
|
+
const record = () => this.activityLifecycle.recordLinked({
|
|
189
|
+
sourceActivityId,
|
|
190
|
+
tool: "subagent_result",
|
|
191
|
+
request: { sessionId: entry.session.id, runId: entry.run.id },
|
|
192
|
+
result: {
|
|
193
|
+
sessionId: entry.session.id,
|
|
194
|
+
runId: entry.run.id,
|
|
195
|
+
provider: entry.session.provider,
|
|
196
|
+
status: "interrupted",
|
|
197
|
+
},
|
|
198
|
+
outcome: { type: "failed", error: "Subagent Run interrupted." },
|
|
113
199
|
});
|
|
200
|
+
try {
|
|
201
|
+
record();
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
if (!fallbackActivityId || fallbackActivityId === sourceActivityId)
|
|
205
|
+
return;
|
|
206
|
+
this.activityLifecycle.recordLinked({
|
|
207
|
+
sourceActivityId: fallbackActivityId,
|
|
208
|
+
tool: "subagent_result",
|
|
209
|
+
request: { sessionId: entry.session.id, runId: entry.run.id },
|
|
210
|
+
result: {
|
|
211
|
+
sessionId: entry.session.id,
|
|
212
|
+
runId: entry.run.id,
|
|
213
|
+
provider: entry.session.provider,
|
|
214
|
+
status: "interrupted",
|
|
215
|
+
},
|
|
216
|
+
outcome: { type: "failed", error: "Subagent Run interrupted." },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
114
219
|
}
|
|
115
220
|
recordCompletion(completion) {
|
|
116
221
|
if (!completion.activityId)
|
|
@@ -39,6 +39,9 @@ export class SubagentDeliveryMailbox {
|
|
|
39
39
|
hasSession(sessionId) {
|
|
40
40
|
return this.files().includes(`${sessionId}.json`);
|
|
41
41
|
}
|
|
42
|
+
discardSession(sessionId) {
|
|
43
|
+
rmSync(this.pathFor(sessionId), { force: true });
|
|
44
|
+
}
|
|
42
45
|
claim(predicate) {
|
|
43
46
|
const deliveries = [];
|
|
44
47
|
for (const file of this.files()) {
|
|
@@ -27,73 +27,64 @@ export async function executeSubagentRun(config, input, providerRunner = runSuba
|
|
|
27
27
|
thinking: record.thinking,
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
|
-
|
|
30
|
+
let outcome = "succeeded";
|
|
31
|
+
let result;
|
|
32
|
+
let errorMessage;
|
|
31
33
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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");
|
|
34
|
+
await hooks.run("SubagentStart", hookInvocation);
|
|
35
|
+
input.signal?.throwIfAborted();
|
|
36
|
+
result = await runSessionProvider(config, record, input.prompt, providerRunner, input.signal);
|
|
37
|
+
input.signal?.throwIfAborted();
|
|
64
38
|
}
|
|
65
39
|
catch (error) {
|
|
66
|
-
|
|
40
|
+
if (isCancelled(error, input.signal)) {
|
|
41
|
+
outcome = "cancelled";
|
|
42
|
+
errorMessage = "Subagent Run cancelled.";
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
outcome = "failed";
|
|
46
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
67
50
|
await hooks.run("SubagentStop", {
|
|
68
51
|
...hookInvocation,
|
|
69
52
|
payload: {
|
|
70
53
|
...hookInvocation.payload,
|
|
71
|
-
status:
|
|
54
|
+
status: outcome,
|
|
72
55
|
},
|
|
73
56
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
});
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (outcome === "succeeded") {
|
|
60
|
+
outcome = "failed";
|
|
61
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
94
62
|
}
|
|
95
|
-
return completion(record, input, "failed", message);
|
|
96
63
|
}
|
|
64
|
+
const finishedAt = new Date().toISOString();
|
|
65
|
+
store.update(record.id, {
|
|
66
|
+
...(result?.providerSessionId ? { providerSessionId: result.providerSessionId } : {}),
|
|
67
|
+
status: "idle",
|
|
68
|
+
activeRun: undefined,
|
|
69
|
+
latestRun: {
|
|
70
|
+
id: input.runId,
|
|
71
|
+
status: outcome,
|
|
72
|
+
finishedAt,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
if (record.workspaceId) {
|
|
76
|
+
mailbox.write({
|
|
77
|
+
sessionId: record.id,
|
|
78
|
+
runId: input.runId,
|
|
79
|
+
workspaceId: record.workspaceId,
|
|
80
|
+
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
81
|
+
provider: record.provider,
|
|
82
|
+
outcome,
|
|
83
|
+
...(outcome === "succeeded" && result ? { finalResponse: result.finalResponse } : {}),
|
|
84
|
+
...(outcome !== "succeeded" && errorMessage ? { error: errorMessage } : {}),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return completion(record, input, outcome, errorMessage);
|
|
97
88
|
}
|
|
98
89
|
finally {
|
|
99
90
|
store.close();
|
|
@@ -118,7 +109,7 @@ export async function executeSubagentSession(config, sessionId, prompt) {
|
|
|
118
109
|
store.close();
|
|
119
110
|
}
|
|
120
111
|
}
|
|
121
|
-
async function runSessionProvider(config, session, prompt, providerRunner) {
|
|
112
|
+
async function runSessionProvider(config, session, prompt, providerRunner, signal) {
|
|
122
113
|
if (!isSubagentProvider(session.provider)) {
|
|
123
114
|
throw new Error(`Unknown subagent provider for Session ${session.id}: ${session.provider}`);
|
|
124
115
|
}
|
|
@@ -130,6 +121,7 @@ async function runSessionProvider(config, session, prompt, providerRunner) {
|
|
|
130
121
|
writeMode: "allowed",
|
|
131
122
|
model: session.model,
|
|
132
123
|
thinking: session.thinking,
|
|
124
|
+
signal,
|
|
133
125
|
});
|
|
134
126
|
}
|
|
135
127
|
if (session.profileName === session.provider) {
|
|
@@ -139,15 +131,16 @@ async function runSessionProvider(config, session, prompt, providerRunner) {
|
|
|
139
131
|
writeMode: "allowed",
|
|
140
132
|
model: session.model,
|
|
141
133
|
thinking: session.thinking,
|
|
134
|
+
signal,
|
|
142
135
|
});
|
|
143
136
|
}
|
|
144
137
|
const profiles = await loadSubagentProfiles(config, session.workspaceRoot);
|
|
145
138
|
const profile = profiles.find((candidate) => candidate.name === session.profileName);
|
|
146
139
|
if (!profile)
|
|
147
140
|
throw new Error(`Subagent profile not found: ${session.profileName}`);
|
|
148
|
-
return runSubagentProfile(profile, session, prompt, providerRunner);
|
|
141
|
+
return runSubagentProfile(profile, session, prompt, providerRunner, signal);
|
|
149
142
|
}
|
|
150
|
-
async function runSubagentProfile(profile, session, prompt, providerRunner) {
|
|
143
|
+
async function runSubagentProfile(profile, session, prompt, providerRunner, signal) {
|
|
151
144
|
const body = profile.body.trim();
|
|
152
145
|
const firstPrompt = body ? `${body}\n\nTask:\n${prompt}` : prompt;
|
|
153
146
|
return providerRunner(session.provider, {
|
|
@@ -156,8 +149,12 @@ async function runSubagentProfile(profile, session, prompt, providerRunner) {
|
|
|
156
149
|
writeMode: "allowed",
|
|
157
150
|
model: session.model,
|
|
158
151
|
thinking: session.thinking,
|
|
152
|
+
signal,
|
|
159
153
|
});
|
|
160
154
|
}
|
|
155
|
+
function isCancelled(error, signal) {
|
|
156
|
+
return signal?.aborted === true || (error instanceof Error && error.name === "AbortError");
|
|
157
|
+
}
|
|
161
158
|
function completion(session, input, outcome, error) {
|
|
162
159
|
return {
|
|
163
160
|
sessionId: session.id,
|
|
@@ -52,13 +52,16 @@ export class SubagentSessionManager {
|
|
|
52
52
|
const run = session.activeRun;
|
|
53
53
|
if (!run)
|
|
54
54
|
throw new Error(`Subagent Session ${session.id} did not create an active Run.`);
|
|
55
|
-
this.launcher.launch({
|
|
55
|
+
const owned = this.assignOwner(session, run, this.launcher.launch({
|
|
56
56
|
sessionId: session.id,
|
|
57
57
|
runId,
|
|
58
58
|
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
59
59
|
prompt: input.prompt,
|
|
60
|
-
});
|
|
61
|
-
return {
|
|
60
|
+
}));
|
|
61
|
+
return {
|
|
62
|
+
session: owned,
|
|
63
|
+
run: owned.activeRun ?? (owned.latestRun?.id === run.id ? owned.latestRun : run),
|
|
64
|
+
};
|
|
62
65
|
}
|
|
63
66
|
resume(input, scope = {}) {
|
|
64
67
|
const existing = this.store.getInScope(input.sessionId, scope);
|
|
@@ -90,17 +93,58 @@ export class SubagentSessionManager {
|
|
|
90
93
|
status: "running",
|
|
91
94
|
activeRun: run,
|
|
92
95
|
});
|
|
93
|
-
this.launcher.launch({
|
|
96
|
+
const owned = this.assignOwner(session, run, this.launcher.launch({
|
|
94
97
|
sessionId: session.id,
|
|
95
98
|
runId,
|
|
96
99
|
...(input.activityId ? { activityId: input.activityId } : {}),
|
|
97
100
|
prompt: input.prompt,
|
|
98
|
-
});
|
|
99
|
-
return {
|
|
101
|
+
}));
|
|
102
|
+
return {
|
|
103
|
+
session: owned,
|
|
104
|
+
run: owned.activeRun ?? (owned.latestRun?.id === run.id ? owned.latestRun : run),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
reconcile(scope, ownerAlive) {
|
|
108
|
+
const reconciled = [];
|
|
109
|
+
for (const session of this.store.list(scope)) {
|
|
110
|
+
const run = session.activeRun;
|
|
111
|
+
if (!run || ownerAlive(run))
|
|
112
|
+
continue;
|
|
113
|
+
const interrupted = {
|
|
114
|
+
id: run.id,
|
|
115
|
+
status: "interrupted",
|
|
116
|
+
...(run.activityId ? { activityId: run.activityId } : {}),
|
|
117
|
+
...(run.startedAt ? { startedAt: run.startedAt } : {}),
|
|
118
|
+
finishedAt: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
const updated = this.store.update(session.id, {
|
|
121
|
+
status: "idle",
|
|
122
|
+
activeRun: undefined,
|
|
123
|
+
latestRun: interrupted,
|
|
124
|
+
});
|
|
125
|
+
reconciled.push({ session: updated, run: interrupted });
|
|
126
|
+
}
|
|
127
|
+
return reconciled;
|
|
128
|
+
}
|
|
129
|
+
delete(sessionId, scope = {}) {
|
|
130
|
+
const session = this.store.getInScope(sessionId, scope);
|
|
131
|
+
if (!session) {
|
|
132
|
+
throw new SubagentSessionError("subagent.session_not_found", `Unknown Subagent Session in this Workspace: ${sessionId}`);
|
|
133
|
+
}
|
|
134
|
+
if (session.activeRun) {
|
|
135
|
+
throw new SubagentSessionError("subagent.busy", `Subagent Session ${session.id} already has active Run ${session.activeRun.id}.`);
|
|
136
|
+
}
|
|
137
|
+
this.store.delete(session.id);
|
|
138
|
+
return session;
|
|
100
139
|
}
|
|
101
140
|
close() {
|
|
102
141
|
this.store.close();
|
|
103
142
|
}
|
|
143
|
+
assignOwner(session, run, owner) {
|
|
144
|
+
if (!owner)
|
|
145
|
+
return session;
|
|
146
|
+
return this.store.assignActiveRunOwner(session.id, run.id, owner);
|
|
147
|
+
}
|
|
104
148
|
}
|
|
105
149
|
function newRunId() {
|
|
106
150
|
return `run_${randomUUID().replaceAll("-", "").slice(0, 12)}`;
|
|
@@ -3,6 +3,7 @@ export function createSubagentMcpRuntime(config, activityLifecycle, options = {}
|
|
|
3
3
|
const capability = config.subagents
|
|
4
4
|
? new SubagentSessionCapability(config, activityLifecycle, {
|
|
5
5
|
providerRunner: options.subagentProviderRunner,
|
|
6
|
+
ownerAlive: options.subagentOwnerAlive,
|
|
6
7
|
})
|
|
7
8
|
: undefined;
|
|
8
9
|
return {
|
|
@@ -47,6 +47,8 @@ export class SubagentSessionStore {
|
|
|
47
47
|
status: "running",
|
|
48
48
|
...(input.activeRun.activityId ? { activityId: input.activeRun.activityId } : {}),
|
|
49
49
|
startedAt: input.activeRun.startedAt,
|
|
50
|
+
...(input.activeRun.ownerId ? { ownerId: input.activeRun.ownerId } : {}),
|
|
51
|
+
...(input.activeRun.ownerPid !== undefined ? { ownerPid: input.activeRun.ownerPid } : {}),
|
|
50
52
|
},
|
|
51
53
|
}
|
|
52
54
|
: {}),
|
|
@@ -67,6 +69,8 @@ export class SubagentSessionStore {
|
|
|
67
69
|
active_run_id,
|
|
68
70
|
active_activity_id,
|
|
69
71
|
active_run_started_at,
|
|
72
|
+
active_owner_id,
|
|
73
|
+
active_owner_pid,
|
|
70
74
|
latest_run_id,
|
|
71
75
|
latest_run_outcome,
|
|
72
76
|
latest_run_finished_at,
|
|
@@ -75,8 +79,8 @@ export class SubagentSessionStore {
|
|
|
75
79
|
hook_reports_json,
|
|
76
80
|
created_at,
|
|
77
81
|
updated_at
|
|
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);
|
|
82
|
+
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, null, null, null, ?, ?)`)
|
|
83
|
+
.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, record.activeRun?.ownerId ?? null, record.activeRun?.ownerPid ?? null, null, null, null, record.createdAt, record.updatedAt);
|
|
80
84
|
return record;
|
|
81
85
|
}
|
|
82
86
|
get(idOrPrefix) {
|
|
@@ -126,6 +130,8 @@ export class SubagentSessionStore {
|
|
|
126
130
|
active_run_id = ?,
|
|
127
131
|
active_activity_id = ?,
|
|
128
132
|
active_run_started_at = ?,
|
|
133
|
+
active_owner_id = ?,
|
|
134
|
+
active_owner_pid = ?,
|
|
129
135
|
latest_run_id = ?,
|
|
130
136
|
latest_run_outcome = ?,
|
|
131
137
|
latest_run_finished_at = ?,
|
|
@@ -134,9 +140,21 @@ export class SubagentSessionStore {
|
|
|
134
140
|
hook_reports_json = null,
|
|
135
141
|
updated_at = ?
|
|
136
142
|
where 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);
|
|
143
|
+
.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.activeRun?.ownerId ?? null, updated.activeRun?.ownerPid ?? null, updated.latestRun?.id ?? null, updated.latestRun && updated.latestRun.status !== "running" ? updated.latestRun.status : null, updated.latestRun?.finishedAt ?? null, updated.updatedAt, updated.id);
|
|
138
144
|
return updated;
|
|
139
145
|
}
|
|
146
|
+
assignActiveRunOwner(id, runId, owner) {
|
|
147
|
+
this.database.sqlite.prepare(`update local_agent_sessions
|
|
148
|
+
set active_owner_id = ?, active_owner_pid = ?, updated_at = ?
|
|
149
|
+
where id = ? and active_run_id = ?`).run(owner.id, owner.pid ?? null, new Date().toISOString(), id, runId);
|
|
150
|
+
const current = this.getById(id);
|
|
151
|
+
if (!current)
|
|
152
|
+
throw new Error(`Unknown subagent id: ${id}`);
|
|
153
|
+
return current;
|
|
154
|
+
}
|
|
155
|
+
delete(id) {
|
|
156
|
+
this.database.sqlite.prepare("delete from local_agent_sessions where id = ?").run(id);
|
|
157
|
+
}
|
|
140
158
|
close() {
|
|
141
159
|
this.database.close();
|
|
142
160
|
}
|
|
@@ -157,6 +175,8 @@ function rowToSubagentSession(row) {
|
|
|
157
175
|
status: "running",
|
|
158
176
|
...(row.active_activity_id ? { activityId: row.active_activity_id } : {}),
|
|
159
177
|
...(row.active_run_started_at ? { startedAt: row.active_run_started_at } : {}),
|
|
178
|
+
...(row.active_owner_id ? { ownerId: row.active_owner_id } : {}),
|
|
179
|
+
...(row.active_owner_pid !== null ? { ownerPid: row.active_owner_pid } : {}),
|
|
160
180
|
}
|
|
161
181
|
: undefined;
|
|
162
182
|
const latestOutcome = readOutcome(row.latest_run_outcome);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"release:publish": "node scripts/release/publish.mjs",
|
|
48
48
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
49
49
|
"start": "node dist/cli.js serve",
|
|
50
|
-
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/release-version.test.mjs && tsx src/oauth/router.test.ts && tsx src/remote-auth-cli.test.ts && tsx src/remote-ssh-auth-cli.test.ts && tsx src/remote-workspace-relay.test.ts && tsx src/remote-workspace-relay-process.test.ts && 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/workspace-lifecycle-app.test.ts && tsx src/ui/activity/model.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/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/operations/edit-preflight.test.ts && tsx src/skills.test.ts && tsx src/db/migrations.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/operations/core-operation-executor.test.ts && tsx src/operations/bulk-mutation.test.ts && tsx src/operations/batch/scheduler.test.ts && tsx src/operations/batch/executor-policy.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 && npm run build:app && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
50
|
+
"test": "node --test scripts/debug/runtime.test.mjs scripts/release-proof.test.mjs scripts/release/release-gate.test.mjs scripts/release/release-version.test.mjs && tsx src/oauth/router.test.ts && tsx src/remote-auth-cli.test.ts && tsx src/remote-ssh-auth-cli.test.ts && tsx src/remote-workspace-relay.test.ts && tsx src/remote-workspace-relay-process.test.ts && 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/workspace-lifecycle-app.test.ts && tsx src/ui/activity/model.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/subagents/providers/adapters/codex.test.ts && tsx src/subagents/providers/registry.test.ts && tsx src/subagents/providers/availability.test.ts && tsx src/subagents/profiles.test.ts && tsx src/subagents/cli-target.test.ts && tsx src/subagents/sessions/store.test.ts && tsx src/subagents/sessions/manager.test.ts && tsx src/subagents/sessions/mcp/capability.server.test.ts && tsx src/subagents/sessions/mcp/continuation.server.test.ts && tsx src/subagents/sessions/mcp/lifecycle.server.test.ts && tsx src/subagents/sessions/mcp/reconciliation.server.test.ts && tsx src/subagents/sessions/mcp/routing.server.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/operations/edit-preflight.test.ts && tsx src/skills.test.ts && tsx src/db/migrations.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/operations/core-operation-executor.test.ts && tsx src/operations/bulk-mutation.test.ts && tsx src/operations/batch/scheduler.test.ts && tsx src/operations/batch/executor-policy.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 && npm run build:app && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
51
51
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
52
|
"release:check": "node scripts/release-version.mjs check",
|
|
53
53
|
"release:tag-check": "node scripts/release-version.mjs tag",
|