opencode-courier 0.13.8 → 0.15.0
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/README.md +7 -9
- package/dist/index.d.ts +0 -8
- package/dist/index.js +7 -54
- package/dist/kernel/protocol.d.ts +3 -17
- package/dist/kernel/protocol.js +3 -17
- package/dist/kernel/tool.d.ts +0 -2
- package/dist/kernel/tool.js +0 -1
- package/dist/memory/domain.d.ts +6 -85
- package/dist/memory/domain.js +7 -63
- package/dist/memory/events.d.ts +0 -11
- package/dist/memory/events.js +1 -12
- package/dist/memory/forget.d.ts +0 -5
- package/dist/memory/forget.js +0 -4
- package/dist/memory/index.js +0 -2
- package/dist/memory/lessons.d.ts +1 -9
- package/dist/memory/lessons.js +2 -11
- package/dist/memory/note.d.ts +2 -11
- package/dist/memory/note.js +5 -11
- package/dist/memory/pin.d.ts +0 -5
- package/dist/memory/pin.js +0 -4
- package/dist/memory/ports.d.ts +2 -22
- package/dist/memory/recall.d.ts +0 -5
- package/dist/memory/recall.js +0 -12
- package/dist/memory/record.d.ts +0 -4
- package/dist/memory/record.js +8 -31
- package/dist/memory/rpc-handlers.d.ts +3 -6
- package/dist/memory/rpc-handlers.js +0 -6
- package/dist/memory/rpc.d.ts +6 -19
- package/dist/memory/rpc.js +1 -16
- package/dist/memory/store.d.ts +0 -5
- package/dist/memory/store.js +1 -16
- package/dist/memory/sweep.d.ts +0 -4
- package/dist/memory/sweep.js +0 -4
- package/dist/memory/text.d.ts +1 -26
- package/dist/memory/text.js +8 -38
- package/dist/memory/tools.d.ts +0 -2
- package/dist/memory/tools.js +4 -6
- package/dist/memory/tui.d.ts +1 -35
- package/dist/person/events.d.ts +0 -16
- package/dist/person/events.js +3 -30
- package/dist/person/form.d.ts +0 -9
- package/dist/person/form.js +0 -10
- package/dist/person/index.d.ts +0 -2
- package/dist/person/permission.d.ts +0 -34
- package/dist/person/permission.js +2 -28
- package/dist/person/ports.d.ts +0 -16
- package/dist/person/ports.js +0 -1
- package/dist/person/question/answer.d.ts +0 -28
- package/dist/person/question/answer.js +0 -49
- package/dist/person/question/index.js +0 -2
- package/dist/person/question/lifecycle.d.ts +0 -26
- package/dist/person/question/lifecycle.js +1 -32
- package/dist/person/question/pure.d.ts +0 -11
- package/dist/person/question/pure.js +0 -11
- package/dist/person/question/relay.d.ts +0 -1
- package/dist/person/question/relay.js +5 -67
- package/dist/person/question/runtime.d.ts +0 -14
- package/dist/person/question/runtime.js +1 -26
- package/dist/person/question/shared.d.ts +0 -17
- package/dist/person/question/shared.js +0 -20
- package/dist/person/question/types.d.ts +0 -44
- package/dist/person/rpc-handlers.d.ts +0 -7
- package/dist/person/rpc-handlers.js +0 -6
- package/dist/person/rpc.d.ts +0 -20
- package/dist/person/rpc.js +0 -16
- package/dist/person/text.d.ts +0 -45
- package/dist/person/text.js +2 -54
- package/dist/person/tools.d.ts +0 -3
- package/dist/person/tools.js +3 -4
- package/dist/person/tui.d.ts +0 -21
- package/dist/platform/bounded.d.ts +0 -8
- package/dist/platform/bounded.js +0 -8
- package/dist/platform/events.d.ts +0 -18
- package/dist/platform/events.js +3 -22
- package/dist/platform/hub.d.ts +0 -113
- package/dist/platform/hub.js +1 -76
- package/dist/platform/json.d.ts +0 -3
- package/dist/platform/json.js +1 -3
- package/dist/platform/storage.d.ts +0 -7
- package/dist/platform/storage.js +0 -6
- package/dist/platform/version.d.ts +0 -9
- package/dist/platform/version.js +0 -8
- package/dist/rpc.js +0 -1
- package/dist/scheduling/index.d.ts +0 -1
- package/dist/scheduling/index.js +0 -1
- package/dist/scheduling/later.d.ts +0 -11
- package/dist/scheduling/later.js +0 -14
- package/dist/scheduling/text.d.ts +0 -1
- package/dist/scheduling/text.js +0 -1
- package/dist/scheduling/tools.d.ts +0 -1
- package/dist/scheduling/tools.js +6 -8
- package/dist/skill.d.ts +1 -9
- package/dist/skill.js +40 -97
- package/dist/tree/cleanup.d.ts +0 -20
- package/dist/tree/cleanup.js +0 -26
- package/dist/tree/context.d.ts +0 -5
- package/dist/tree/context.js +17 -13
- package/dist/tree/domain.d.ts +0 -64
- package/dist/tree/domain.js +7 -45
- package/dist/tree/events.d.ts +5 -36
- package/dist/tree/events.js +23 -55
- package/dist/tree/forget.d.ts +0 -15
- package/dist/tree/forget.js +0 -18
- package/dist/tree/group.d.ts +0 -22
- package/dist/tree/group.js +6 -39
- package/dist/tree/index.d.ts +2 -2
- package/dist/tree/index.js +1 -3
- package/dist/tree/limits.d.ts +1 -4
- package/dist/tree/limits.js +9 -13
- package/dist/tree/mascot.d.ts +0 -25
- package/dist/tree/mascot.js +1 -32
- package/dist/tree/model.d.ts +0 -5
- package/dist/tree/model.js +2 -15
- package/dist/tree/nudge.d.ts +0 -11
- package/dist/tree/nudge.js +3 -16
- package/dist/tree/panel.d.ts +0 -8
- package/dist/tree/panel.js +3 -16
- package/dist/tree/ports.d.ts +5 -139
- package/dist/tree/report.d.ts +1 -10
- package/dist/tree/report.js +4 -11
- package/dist/tree/roster.d.ts +0 -21
- package/dist/tree/roster.js +0 -21
- package/dist/tree/rpc-handlers.d.ts +6 -1
- package/dist/tree/rpc-handlers.js +0 -5
- package/dist/tree/rpc.d.ts +9 -109
- package/dist/tree/rpc.js +2 -38
- package/dist/tree/running.d.ts +0 -6
- package/dist/tree/running.js +2 -10
- package/dist/tree/send.d.ts +0 -4
- package/dist/tree/send.js +4 -31
- package/dist/tree/spawn.d.ts +1 -7
- package/dist/tree/spawn.js +15 -56
- package/dist/tree/standing.d.ts +0 -8
- package/dist/tree/standing.js +1 -11
- package/dist/tree/status.d.ts +0 -8
- package/dist/tree/status.js +1 -9
- package/dist/tree/stop.d.ts +2 -9
- package/dist/tree/stop.js +58 -25
- package/dist/tree/store.d.ts +0 -8
- package/dist/tree/store.js +4 -38
- package/dist/tree/subtree.d.ts +8 -26
- package/dist/tree/subtree.js +37 -35
- package/dist/tree/summary.d.ts +0 -4
- package/dist/tree/summary.js +0 -8
- package/dist/tree/text.d.ts +4 -46
- package/dist/tree/text.js +44 -83
- package/dist/tree/tools.d.ts +0 -6
- package/dist/tree/tools.js +15 -15
- package/dist/tree/tui.d.ts +0 -81
- package/dist/tree/wake.d.ts +0 -30
- package/dist/tree/wake.js +5 -39
- package/dist/tui.js +24 -10
- package/package.json +8 -9
- package/dist/webhook/index.d.ts +0 -5
- package/dist/webhook/index.js +0 -3
- package/dist/webhook/receiver.d.ts +0 -80
- package/dist/webhook/receiver.js +0 -339
- package/dist/webhook/subscriptions.d.ts +0 -29
- package/dist/webhook/subscriptions.js +0 -52
- package/dist/webhook/text.d.ts +0 -23
- package/dist/webhook/text.js +0 -82
- package/dist/webhook/tools.d.ts +0 -19
- package/dist/webhook/tools.js +0 -46
package/dist/tree/send.js
CHANGED
|
@@ -4,17 +4,13 @@ import { syncStanding } from "./standing.js";
|
|
|
4
4
|
import { childStore, groupStore } from "./store.js";
|
|
5
5
|
import { reportBody } from "./text.js";
|
|
6
6
|
import { noteSent } from "./wake.js";
|
|
7
|
-
/**
|
|
8
|
-
* Drops a message into another session's inbox; OpenCode wakes that session if it is idle. From a spawned session
|
|
9
|
-
* to the one that started it, it is its report or its progress (`noteReport`); a group member's final report is held (`hold`).
|
|
10
|
-
*/
|
|
11
7
|
export async function send(ports, from, input) {
|
|
12
8
|
const { status, dropped } = await statusFor(ports, from, input);
|
|
13
|
-
//
|
|
9
|
+
// the delivery may wake the parent, whose turn may end before a later note
|
|
14
10
|
const noted = await noteReport(ports, from, input.sessionID, status).catch(() => undefined);
|
|
15
11
|
try {
|
|
16
12
|
const held = noted?.member && status && status !== "blocked" ? await hold(ports, noted.member, { at: noted.at, status, message: input.message, ...(input.artifacts ? { artifacts: input.artifacts } : {}) }) : undefined;
|
|
17
|
-
//
|
|
13
|
+
// an entry put now shows at the woken parent's next step (api-notes)
|
|
18
14
|
if (noted)
|
|
19
15
|
await syncStanding(ports, input.sessionID);
|
|
20
16
|
if (held)
|
|
@@ -24,13 +20,11 @@ export async function send(ports, from, input) {
|
|
|
24
20
|
}
|
|
25
21
|
catch (error) {
|
|
26
22
|
await noted?.undo().catch(() => undefined);
|
|
27
|
-
// The standing as before the note, once the note is taken back.
|
|
28
23
|
if (noted)
|
|
29
24
|
await syncStanding(ports, input.sessionID);
|
|
30
25
|
throw error;
|
|
31
26
|
}
|
|
32
27
|
}
|
|
33
|
-
/** Delivers the message, watches it, and takes a held report back after a blocked one; the message id and status delivered. */
|
|
34
28
|
async function deliver(ports, from, input, status, noted) {
|
|
35
29
|
const text = envelope(from, reportBody(input.message, input.artifacts), status ? { status } : {});
|
|
36
30
|
const description = `Message from ${from}`;
|
|
@@ -41,19 +35,14 @@ async function deliver(ports, from, input, status, noted) {
|
|
|
41
35
|
metadata: { source: "courier", from, ...(status ? { status } : {}) },
|
|
42
36
|
delivery: input.queue ? "queue" : "steer",
|
|
43
37
|
});
|
|
44
|
-
//
|
|
38
|
+
// the enqueued event is handled apart from this call (api-notes)
|
|
45
39
|
await noteSent(ports, { sessionID: input.sessionID, inboxID: delivered.id, text, description }).catch(() => undefined);
|
|
46
|
-
// A blocked report, once delivered, is the member's latest word: an earlier report held for it would be stale.
|
|
47
40
|
if (noted?.member && status === "blocked")
|
|
48
41
|
await unholdForBlocked(groupStore(ports.storage), noted.member).catch(() => undefined);
|
|
49
42
|
return { messageID: delivered.id, ...(status ? { status } : {}) };
|
|
50
43
|
}
|
|
51
|
-
|
|
52
|
-
* The message's status, checked, as OpenCode does not check the schema. A status is a report's: on a message to a
|
|
53
|
-
* session below the sender it means nothing, so it is `dropped`, and the result says so.
|
|
54
|
-
*/
|
|
44
|
+
// tool inputs use plain schemas, so the value is checked here (api-notes)
|
|
55
45
|
async function statusFor(ports, from, input) {
|
|
56
|
-
// Some models send null for an optional field they leave out.
|
|
57
46
|
const given = input.status ?? undefined;
|
|
58
47
|
if (given === undefined)
|
|
59
48
|
return {};
|
|
@@ -61,15 +50,10 @@ async function statusFor(ports, from, input) {
|
|
|
61
50
|
throw new Error(`status must be one of ${STATUSES.join(", ")}, not ${JSON.stringify(given)}.`);
|
|
62
51
|
return (await isBelow(ports, from, input.sessionID)) ? { dropped: given } : { status: given };
|
|
63
52
|
}
|
|
64
|
-
/** Whether `from` started `to`, directly or through others, by the reverse index; false when the index does not name `to` or cannot be read. */
|
|
65
53
|
async function isBelow(ports, from, to) {
|
|
66
54
|
const above = await childStore(ports.storage).indexedAncestors(to).catch(() => []);
|
|
67
55
|
return above.includes(from);
|
|
68
56
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Notes `from`'s message to `to` when `to` started it: as its report when it carries a status, or when an earlier
|
|
71
|
-
* release briefed `from`, else as progress. Returns which, when, how to take the note back, and `from`'s group membership.
|
|
72
|
-
*/
|
|
73
57
|
async function noteReport(ports, from, to, status) {
|
|
74
58
|
const children = childStore(ports.storage);
|
|
75
59
|
const entry = await children.indexedEntry(from);
|
|
@@ -80,7 +64,6 @@ async function noteReport(ports, from, to, status) {
|
|
|
80
64
|
const before = await children.peek(from, field);
|
|
81
65
|
const at = ports.now();
|
|
82
66
|
await (report ? children.settled(from, "report", at, status) : children.progressed(from, at));
|
|
83
|
-
// Only its own note: another courier_send of the session's may have noted its report since.
|
|
84
67
|
const undo = async () => {
|
|
85
68
|
const now = (await children.peek(from, field));
|
|
86
69
|
if (now?.at !== at || (report && now.by !== "report"))
|
|
@@ -89,10 +72,6 @@ async function noteReport(ports, from, to, status) {
|
|
|
89
72
|
};
|
|
90
73
|
return { report, at, undo, member: status !== undefined ? await memberIn(ports, entry) : undefined };
|
|
91
74
|
}
|
|
92
|
-
/**
|
|
93
|
-
* A child's membership of an open group of its parent's, by its roster entry; none once courier_cleanup dropped it. One that
|
|
94
|
-
* cannot be read is dropped from the group, best effort, as the report then goes on its own and the group must not wait for it.
|
|
95
|
-
*/
|
|
96
75
|
async function memberIn(ports, entry) {
|
|
97
76
|
const groups = groupStore(ports.storage);
|
|
98
77
|
try {
|
|
@@ -106,24 +85,18 @@ async function memberIn(ports, entry) {
|
|
|
106
85
|
return undefined;
|
|
107
86
|
}
|
|
108
87
|
}
|
|
109
|
-
/**
|
|
110
|
-
* Holds a member's report with its group, and nudges the scheduler if that completes the group. Undefined
|
|
111
|
-
* when it could not be held: the member is dropped from the group, which releases without it, and reports on its own.
|
|
112
|
-
*/
|
|
113
88
|
async function hold(ports, member, report) {
|
|
114
89
|
const groups = groupStore(ports.storage);
|
|
115
90
|
try {
|
|
116
91
|
await holdReport(groups, member, report);
|
|
117
92
|
}
|
|
118
93
|
catch (error) {
|
|
119
|
-
// Neither held nor dropped, the group would wait for ever: the call fails, and the sender sends it again.
|
|
120
94
|
await groups.remove(member.parentID, member.group, member.sessionID).catch((dropError) => {
|
|
121
95
|
throw new Error(`the report could not be held with group ${member.group} (${String(error)}), nor the member dropped from it (${String(dropError)}); send it again.`);
|
|
122
96
|
});
|
|
123
97
|
ports.nudge();
|
|
124
98
|
return undefined;
|
|
125
99
|
}
|
|
126
|
-
// Held whatever the count gives; a count that cannot be read gets the nudge, as a tick that finds nothing due is cheap.
|
|
127
100
|
const standing = await groupStanding(groups, member.parentID, member.group).catch(() => undefined);
|
|
128
101
|
if (standing?.complete !== false)
|
|
129
102
|
ports.nudge();
|
package/dist/tree/spawn.d.ts
CHANGED
|
@@ -5,18 +5,12 @@ export interface SpawnInput {
|
|
|
5
5
|
readonly agent?: string;
|
|
6
6
|
readonly isolate?: boolean;
|
|
7
7
|
readonly group?: string;
|
|
8
|
-
/** `provider/model` to run on; left out, the agent's own model, else the caller's. */
|
|
9
8
|
readonly model?: string;
|
|
10
|
-
/** The variant of that model, its reasoning effort; left out, the chosen model's own. */
|
|
11
9
|
readonly variant?: string;
|
|
12
|
-
/** Whether the child is OpenCode's subagent of the caller; left out, the `nativeChildren` option decides. */
|
|
13
10
|
readonly native?: boolean;
|
|
14
11
|
}
|
|
15
|
-
/**
|
|
16
|
-
* Creates a child session, hands it the task and returns at once; the child reports back with
|
|
17
|
-
* courier_send. Refused past a limit, whether or not the `context` hook hid the tool.
|
|
18
|
-
*/
|
|
19
12
|
export declare function spawn(ports: CourierPorts, parentID: string, input: SpawnInput): Promise<{
|
|
13
|
+
belowTop?: boolean | undefined;
|
|
20
14
|
native?: boolean | undefined;
|
|
21
15
|
model?: string | undefined;
|
|
22
16
|
uncommitted?: boolean | undefined;
|
package/dist/tree/spawn.js
CHANGED
|
@@ -6,30 +6,19 @@ import { chooseModel } from "./model.js";
|
|
|
6
6
|
import { putEntries, syncStanding } from "./standing.js";
|
|
7
7
|
import { childStore, groupStore } from "./store.js";
|
|
8
8
|
import { childBrief, childBranch, nativeIsolateRefusal, ROLE_KEY, rolePart } from "./text.js";
|
|
9
|
-
|
|
10
|
-
* A spawned session's own rules: OpenCode's schedule tools are hidden, as their wake-ups go round the tree, so a child
|
|
11
|
-
* that schedules one and ends its turn would read as silent. A native child would otherwise run on its parent's rules.
|
|
12
|
-
*/
|
|
9
|
+
// a schedule's wake-up is invisible to Waits (api-notes, OpenCode's schedules)
|
|
13
10
|
const CHILD_RULES = [{ action: "opencode_schedule_*", resource: "*", effect: "deny" }];
|
|
14
11
|
function titleOf(task) {
|
|
15
12
|
const line = task.trim().split("\n")[0] ?? "";
|
|
16
13
|
return line.length > 60 ? `${line.slice(0, 57)}...` : line;
|
|
17
14
|
}
|
|
18
|
-
/** Removes a worktree made for a child that was never created; one that cannot be removed stays. */
|
|
19
15
|
async function dropWorktree(ports, directory) {
|
|
20
16
|
try {
|
|
21
17
|
await ports.worktree.remove({ projectID: ports.projectID, directory, force: false });
|
|
22
18
|
}
|
|
23
|
-
catch {
|
|
24
|
-
// The spawn's own failure is the one to report.
|
|
25
|
-
}
|
|
19
|
+
catch { }
|
|
26
20
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Creates a child session, hands it the task and returns at once; the child reports back with
|
|
29
|
-
* courier_send. Refused past a limit, whether or not the `context` hook hid the tool.
|
|
30
|
-
*/
|
|
31
21
|
export async function spawn(ports, parentID, input) {
|
|
32
|
-
// Some models send null, or an empty string, for an optional field they leave out.
|
|
33
22
|
const group = typeof input.group === "string" ? input.group.trim() || undefined : (input.group ?? undefined);
|
|
34
23
|
if (group !== undefined && !isGroupName(group))
|
|
35
24
|
throw new Error(`group must be a name of 1 to 60 letters, digits, dots, dashes or underscores, not ${JSON.stringify(group)}.`);
|
|
@@ -63,7 +52,6 @@ async function start(ports, parentID, input, admitted, group) {
|
|
|
63
52
|
await handOver(ports, entry, admitted.depth, input.task, !rosterError, !rosterError && !groupError);
|
|
64
53
|
}
|
|
65
54
|
finally {
|
|
66
|
-
// The parent's standing with the child on it, prompted or failed, announced at its next step.
|
|
67
55
|
await syncStanding(ports, parentID);
|
|
68
56
|
}
|
|
69
57
|
return {
|
|
@@ -72,25 +60,22 @@ async function start(ports, parentID, input, admitted, group) {
|
|
|
72
60
|
...(group && !rosterError && !groupError ? { group } : {}),
|
|
73
61
|
...(rosterError ? { rosterError } : {}),
|
|
74
62
|
...(groupError ? { groupError } : {}),
|
|
75
|
-
...(handedDown ? { fromParent: true } : {}),
|
|
63
|
+
...(handedDown?.from ? { fromParent: true } : {}),
|
|
76
64
|
...(handedDown?.uncommitted ? { uncommitted: true } : {}),
|
|
77
65
|
...(model ? { model } : {}),
|
|
78
66
|
...(entry.native ? { native: true } : {}),
|
|
67
|
+
...(admitted.depth > 1 ? { belowTop: true } : {}),
|
|
79
68
|
};
|
|
80
69
|
}
|
|
81
|
-
/**
|
|
82
|
-
* Creates the child's session, in a worktree of its own if asked, which goes again if the session cannot be created.
|
|
83
|
-
* `model` names the child's model when it is not its parent's, for the roster and the result.
|
|
84
|
-
*/
|
|
85
70
|
async function create(ports, parentID, input) {
|
|
86
71
|
const { model, parent } = await chooseModel(ports, parentID, input, input.agent);
|
|
87
72
|
const handedDown = input.isolate ? await parentWork(ports, parentID) : undefined;
|
|
88
73
|
const worktree = input.isolate
|
|
89
|
-
? (await ports.worktree.create({ projectID: ports.projectID, ...(handedDown ? { from: handedDown.directory, branch: handedDown.head } : {}) })).directory
|
|
74
|
+
? (await ports.worktree.create({ projectID: ports.projectID, ...(handedDown?.from ? { from: handedDown.from.directory, branch: handedDown.from.head } : {}) })).directory
|
|
90
75
|
: undefined;
|
|
91
76
|
const base = worktree ? await ports.head(worktree) : undefined;
|
|
92
77
|
const title = input.title ?? titleOf(input.task);
|
|
93
|
-
// OpenCode takes a location or a parentID, never both
|
|
78
|
+
// OpenCode takes a location or a parentID, never both
|
|
94
79
|
const native = (input.native ?? ports.nativeChildren === true) && !worktree;
|
|
95
80
|
const child = await ports.session
|
|
96
81
|
.create({
|
|
@@ -103,7 +88,6 @@ async function create(ports, parentID, input) {
|
|
|
103
88
|
metadata: { courier: { parentID } },
|
|
104
89
|
})
|
|
105
90
|
.catch(async (error) => {
|
|
106
|
-
// No session will ever use the fresh worktree, and nothing records it, so it goes now.
|
|
107
91
|
if (worktree)
|
|
108
92
|
await dropWorktree(ports, worktree);
|
|
109
93
|
throw error;
|
|
@@ -119,35 +103,22 @@ async function create(ports, parentID, input) {
|
|
|
119
103
|
...(model && (!parent || modelName(model) !== modelName(parent)) ? { model: modelName(model) } : {}),
|
|
120
104
|
};
|
|
121
105
|
}
|
|
122
|
-
/**
|
|
123
|
-
* The worktree and HEAD an isolated parent hands its isolated child, by the reverse index alone; undefined for any
|
|
124
|
-
* other parent. An unreadable HEAD fails the spawn rather than start the child without the parent's work.
|
|
125
|
-
*/
|
|
126
106
|
async function parentWork(ports, parentID) {
|
|
127
107
|
const parent = await childStore(ports.storage).indexedEntry(parentID);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return { directory: parent.directory, head, uncommitted };
|
|
108
|
+
const directory = parent?.directory ?? ports.directory;
|
|
109
|
+
const head = parent?.isolated ? await ports.head(directory) : undefined;
|
|
110
|
+
if (parent?.isolated && !head)
|
|
111
|
+
throw new Error(`the HEAD of your worktree ${directory} could not be read, so an isolated session cannot start from it; nothing was started. Start it without isolate, or fix the worktree.`);
|
|
112
|
+
const uncommitted = (await ports.dirty(directory).catch(() => undefined)) === true;
|
|
113
|
+
return { ...(head ? { from: { directory, head } } : {}), uncommitted };
|
|
135
114
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Records the child on the roster, before the prompt, so a child that exists is on the roster even if prompting
|
|
138
|
-
* fails, then in its group. A failed write must not keep the child from its task: each is reported, not thrown.
|
|
139
|
-
*/
|
|
140
115
|
async function enrol(ports, entry, group) {
|
|
141
116
|
const rosterError = await childStore(ports.storage).record(entry).then(() => undefined, (error) => describeFailure("roster", error).message);
|
|
142
117
|
const groupError = group && !rosterError ? await join(ports, entry, group) : undefined;
|
|
143
118
|
return { rosterError, groupError };
|
|
144
119
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Hands the child its task. `rostered` says whether its report state is kept, which a child off the roster has none
|
|
147
|
-
* of, and `grouped` whether it is in its group; a child that never got its task leaves both, as it will never report.
|
|
148
|
-
*/
|
|
149
120
|
async function handOver(ports, entry, depth, task, rostered, grouped) {
|
|
150
|
-
//
|
|
121
|
+
// the prompt's inbox.delivered event moves this on; so written first
|
|
151
122
|
const children = childStore(ports.storage);
|
|
152
123
|
if (rostered)
|
|
153
124
|
await children.prompted(entry.sessionID, ports.now()).catch(() => undefined);
|
|
@@ -162,10 +133,6 @@ async function handOver(ports, entry, depth, task, rostered, grouped) {
|
|
|
162
133
|
throw error;
|
|
163
134
|
}
|
|
164
135
|
}
|
|
165
|
-
/**
|
|
166
|
-
* Joins a child to its group; one that cannot be joined is recorded in no group, so it reports on its own,
|
|
167
|
-
* and the result says so. Returns the error's message, if any.
|
|
168
|
-
*/
|
|
169
136
|
async function join(ports, entry, group) {
|
|
170
137
|
const groups = groupStore(ports.storage);
|
|
171
138
|
try {
|
|
@@ -174,7 +141,6 @@ async function join(ports, entry, group) {
|
|
|
174
141
|
}
|
|
175
142
|
catch (error) {
|
|
176
143
|
const { group: _, ...ungrouped } = entry;
|
|
177
|
-
// A write that failed may have landed all the same: a member left there would hold its group up for good.
|
|
178
144
|
await Promise.all([
|
|
179
145
|
childStore(ports.storage).record(ungrouped).catch(() => undefined),
|
|
180
146
|
groups.remove(entry.parentID, group, entry.sessionID).catch(() => undefined),
|
|
@@ -182,10 +148,7 @@ async function join(ports, entry, group) {
|
|
|
182
148
|
return describeFailure("group", error).message;
|
|
183
149
|
}
|
|
184
150
|
}
|
|
185
|
-
|
|
186
|
-
* The child's role and the other modules' entries before its prompt, so its first request carries them, and a root's own at
|
|
187
|
-
* its first spawn, remembered for the `context` hook; a parent that is no root is forgotten there, as its depth may have changed.
|
|
188
|
-
*/
|
|
151
|
+
// entries put before the first step are in its baseline (api-notes)
|
|
189
152
|
async function nameBoth(ports, parentID, childID, depth) {
|
|
190
153
|
const extra = await extraEntries(ports);
|
|
191
154
|
await nameRole(ports, childID, depth);
|
|
@@ -200,7 +163,6 @@ async function nameBoth(ports, parentID, childID, depth) {
|
|
|
200
163
|
else
|
|
201
164
|
ports.roles.delete(parentID);
|
|
202
165
|
}
|
|
203
|
-
/** The entries the other modules give a spawned session, the memory's lessons; none when they cannot be read, logged. */
|
|
204
166
|
async function extraEntries(ports) {
|
|
205
167
|
if (!ports.entries || !ports.session.instructions)
|
|
206
168
|
return [];
|
|
@@ -212,10 +174,7 @@ async function extraEntries(ports) {
|
|
|
212
174
|
return [];
|
|
213
175
|
}
|
|
214
176
|
}
|
|
215
|
-
|
|
216
|
-
* Writes a session's role part to its `role` entry: a child's system prompt carries it from its first request, a root's
|
|
217
|
-
* transcript as an update. False on a host without entries, or when the put failed, logged: the `context` hook adds the part.
|
|
218
|
-
*/
|
|
177
|
+
// a host before 2.2.0 has no entries; the context hook adds the part
|
|
219
178
|
async function nameRole(ports, sessionID, depth) {
|
|
220
179
|
const entries = ports.session.instructions?.entry;
|
|
221
180
|
if (!entries)
|
package/dist/tree/standing.d.ts
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import type { Entry, ReportNote, RosterStorage, StandingPorts } from "./ports.js";
|
|
2
2
|
import { type ChildState, type StandingRow } from "./text.js";
|
|
3
|
-
/** A child's state from its report notes, as `reportOf` and the silent-turn rule read them. */
|
|
4
3
|
export declare function stateOf(notes: {
|
|
5
4
|
readonly [K in "prompt" | "settled" | "progress" | "told" | "stopped"]: ReportNote;
|
|
6
5
|
}): ChildState;
|
|
7
|
-
/** The rows of a parent's standing entry: its unexpired children, oldest first, each with its state and group standing. */
|
|
8
6
|
export declare function standingRows(storage: RosterStorage, parentID: string, now: number): Promise<StandingRow[]>;
|
|
9
|
-
/** What `syncStanding` did to the parent's entry. */
|
|
10
7
|
export type Synced = "put" | "unchanged" | "removed" | "absent" | "skipped";
|
|
11
|
-
/**
|
|
12
|
-
* Writes a parent's `standing` entry once something of it changed: put when its text differs from the entry's, removed when
|
|
13
|
-
* the parent has no children left; skipped without the keyed entries. A failure is logged, never thrown.
|
|
14
|
-
*/
|
|
15
8
|
export declare function syncStanding(ports: StandingPorts, parentID: string): Promise<Synced>;
|
|
16
|
-
/** Writes `given` entries to a session, each put logged on failure and never thrown: the session gets its task all the same. */
|
|
17
9
|
export declare function putEntries(ports: StandingPorts, sessionID: string, given: ReadonlyArray<Entry>): Promise<void>;
|
package/dist/tree/standing.js
CHANGED
|
@@ -4,9 +4,7 @@ import { isNotFound } from "../platform/json.js";
|
|
|
4
4
|
import { isExpired, standingOf } from "./domain.js";
|
|
5
5
|
import { childStore, groupStore } from "./store.js";
|
|
6
6
|
import { STANDING_KEY, standingEntry } from "./text.js";
|
|
7
|
-
/** Whether `at` is known and no earlier than the prompt at `prompt`. */
|
|
8
7
|
const since = (at, prompt) => at !== undefined && at >= prompt;
|
|
9
|
-
/** A child's state from its report notes, as `reportOf` and the silent-turn rule read them. */
|
|
10
8
|
export function stateOf(notes) {
|
|
11
9
|
const prompt = notes.prompt.at;
|
|
12
10
|
if (notes.stopped.at !== undefined && (prompt === undefined || notes.stopped.at >= prompt))
|
|
@@ -21,10 +19,8 @@ export function stateOf(notes) {
|
|
|
21
19
|
return { kind: by };
|
|
22
20
|
return { kind: "reported", status: isStatus(status) ? status : "done", ...progressed };
|
|
23
21
|
}
|
|
24
|
-
/** The rows of a parent's standing entry: its unexpired children, oldest first, each with its state and group standing. */
|
|
25
22
|
export async function standingRows(storage, parentID, now) {
|
|
26
23
|
const children = childStore(storage);
|
|
27
|
-
// The groups read best effort, as courier_children reads them: a member's standing is `unknown` when they cannot be.
|
|
28
24
|
const [entries, members] = await Promise.all([children.children(parentID), groupStore(storage).ofParent(parentID).catch(() => undefined)]);
|
|
29
25
|
const current = entries.filter((entry) => !isExpired(entry, now, existsSync));
|
|
30
26
|
return Promise.all(current.map(async (entry) => {
|
|
@@ -39,12 +35,7 @@ export async function standingRows(storage, parentID, now) {
|
|
|
39
35
|
};
|
|
40
36
|
}));
|
|
41
37
|
}
|
|
42
|
-
/** The syncs under way in this process, by parent, each run after the one before it: a sync that read earlier never writes last. */
|
|
43
38
|
const inTurn = new Map();
|
|
44
|
-
/**
|
|
45
|
-
* Writes a parent's `standing` entry once something of it changed: put when its text differs from the entry's, removed when
|
|
46
|
-
* the parent has no children left; skipped without the keyed entries. A failure is logged, never thrown.
|
|
47
|
-
*/
|
|
48
39
|
export function syncStanding(ports, parentID) {
|
|
49
40
|
const entries = ports.session?.instructions?.entry;
|
|
50
41
|
if (!entries)
|
|
@@ -75,13 +66,12 @@ async function sync(ports, parentID, entries) {
|
|
|
75
66
|
return "put";
|
|
76
67
|
}
|
|
77
68
|
catch (error) {
|
|
78
|
-
//
|
|
69
|
+
// a put on a session OpenCode deleted fails NotFound (api-notes)
|
|
79
70
|
if (!isNotFound(error))
|
|
80
71
|
ports.log?.(`courier: the standing entry of ${parentID} could not be written, so it may be behind courier_children: ${String(error)}`);
|
|
81
72
|
return "skipped";
|
|
82
73
|
}
|
|
83
74
|
}
|
|
84
|
-
/** Writes `given` entries to a session, each put logged on failure and never thrown: the session gets its task all the same. */
|
|
85
75
|
export async function putEntries(ports, sessionID, given) {
|
|
86
76
|
const entries = ports.session?.instructions?.entry;
|
|
87
77
|
if (!entries)
|
package/dist/tree/status.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ export interface StatusInput {
|
|
|
7
7
|
export interface ChildrenInput {
|
|
8
8
|
readonly sessionID?: string;
|
|
9
9
|
}
|
|
10
|
-
/** One session's state and last reply, as courier_children shows each child. */
|
|
11
10
|
export declare function status(ports: CourierPorts, input: StatusInput): Promise<Partial<{
|
|
12
11
|
sessionID: string;
|
|
13
12
|
title: string | undefined;
|
|
@@ -19,12 +18,7 @@ export declare function status(ports: CourierPorts, input: StatusInput): Promise
|
|
|
19
18
|
lastText: string | undefined;
|
|
20
19
|
pending: readonly import("./ports.js").Pending[] | undefined;
|
|
21
20
|
}>>;
|
|
22
|
-
/** The text of a session's last reply, from its context's messages, or undefined when it has none. */
|
|
23
21
|
export declare function lastReply(messages: Awaited<ReturnType<Context["session"]["context"]>>): string | undefined;
|
|
24
|
-
/**
|
|
25
|
-
* The sessions a parent started, each with its state and last reply, or the error it gave; a child in a
|
|
26
|
-
* group with the group's name and its standing there: `held`, `out`, how it left, `released` or `unknown`.
|
|
27
|
-
*/
|
|
28
22
|
export declare function listChildren(ports: CourierPorts, parentID: string): Promise<({
|
|
29
23
|
group: {
|
|
30
24
|
name: string;
|
|
@@ -84,7 +78,6 @@ export declare function listChildren(ports: CourierPorts, parentID: string): Pro
|
|
|
84
78
|
sessionID: string;
|
|
85
79
|
title: string;
|
|
86
80
|
})[]>;
|
|
87
|
-
/** One roster entry as courier_children shows it: the session's state with its directory and group standing, or the error it gave. */
|
|
88
81
|
export declare function described(ports: CourierPorts, entry: RosterEntry): Promise<{
|
|
89
82
|
group: {
|
|
90
83
|
name: string;
|
|
@@ -144,6 +137,5 @@ export declare function described(ports: CourierPorts, entry: RosterEntry): Prom
|
|
|
144
137
|
sessionID: string;
|
|
145
138
|
title: string;
|
|
146
139
|
}>;
|
|
147
|
-
/** A child's standing in the join group its roster entry names, `unknown` when that cannot be read. */
|
|
148
140
|
export declare function standingIn(ports: Pick<CourierPorts, "storage">, entry: RosterEntry, group: string): Promise<"failed" | "interrupted" | "out" | "unknown" | "deleted" | "dropped" | "held" | "released">;
|
|
149
141
|
export {};
|
package/dist/tree/status.js
CHANGED
|
@@ -2,7 +2,6 @@ import { describeFailure } from "../kernel/protocol.js";
|
|
|
2
2
|
import { standingOf, startedAs } from "./domain.js";
|
|
3
3
|
import { current } from "./roster.js";
|
|
4
4
|
import { groupStore } from "./store.js";
|
|
5
|
-
/** One session's state and last reply, as courier_children shows each child. */
|
|
6
5
|
export async function status(ports, input) {
|
|
7
6
|
const [info, messages, pending] = await Promise.all([
|
|
8
7
|
ports.session.get({ sessionID: input.sessionID }),
|
|
@@ -22,22 +21,16 @@ export async function status(ports, input) {
|
|
|
22
21
|
pending: pending.length ? pending : undefined,
|
|
23
22
|
});
|
|
24
23
|
}
|
|
25
|
-
/** The text of a session's last reply, from its context's messages, or undefined when it has none. */
|
|
26
24
|
export function lastReply(messages) {
|
|
27
25
|
const last = messages.findLast((message) => message.type === "assistant");
|
|
28
26
|
return last?.type === "assistant"
|
|
29
27
|
? last.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("")
|
|
30
28
|
: undefined;
|
|
31
29
|
}
|
|
32
|
-
/**
|
|
33
|
-
* The sessions a parent started, each with its state and last reply, or the error it gave; a child in a
|
|
34
|
-
* group with the group's name and its standing there: `held`, `out`, how it left, `released` or `unknown`.
|
|
35
|
-
*/
|
|
36
30
|
export async function listChildren(ports, parentID) {
|
|
37
31
|
const entries = await current(ports.storage, parentID, ports.now());
|
|
38
32
|
return Promise.all(entries.map((entry) => described(ports, entry)));
|
|
39
33
|
}
|
|
40
|
-
/** One roster entry as courier_children shows it: the session's state with its directory and group standing, or the error it gave. */
|
|
41
34
|
export async function described(ports, entry) {
|
|
42
35
|
const group = entry.group ? { group: { name: entry.group, report: await standingIn(ports, entry, entry.group) } } : {};
|
|
43
36
|
const roster = { directory: entry.directory, ...startedAs(entry), created: entry.createdAt, ...group };
|
|
@@ -53,11 +46,10 @@ export async function described(ports, entry) {
|
|
|
53
46
|
};
|
|
54
47
|
}
|
|
55
48
|
}
|
|
56
|
-
/** A child's standing in the join group its roster entry names, `unknown` when that cannot be read. */
|
|
57
49
|
export function standingIn(ports, entry, group) {
|
|
58
50
|
return groupStore(ports.storage).member(entry.parentID, group, entry.sessionID).then(standingOf, () => "unknown");
|
|
59
51
|
}
|
|
60
|
-
|
|
52
|
+
// metadata with an undefined value never completes the call (api-notes)
|
|
61
53
|
function withoutUndefined(value) {
|
|
62
54
|
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
|
|
63
55
|
}
|
package/dist/tree/stop.d.ts
CHANGED
|
@@ -3,31 +3,24 @@ export interface StopInput {
|
|
|
3
3
|
readonly sessionID: string;
|
|
4
4
|
readonly cleanup?: boolean;
|
|
5
5
|
}
|
|
6
|
-
/** What happened to one session: its turn was interrupted, it was not running, OpenCode no longer knows it, or the interrupt failed. */
|
|
7
6
|
export interface Stopped {
|
|
8
7
|
readonly sessionID: string;
|
|
9
8
|
readonly title: string;
|
|
10
9
|
readonly depth: number;
|
|
11
10
|
readonly outcome: "interrupted" | "idle" | "gone" | "failed";
|
|
12
11
|
readonly error?: string;
|
|
12
|
+
readonly subagent?: true;
|
|
13
13
|
}
|
|
14
14
|
export interface StopResult {
|
|
15
15
|
readonly sessionID: string;
|
|
16
|
-
/** Every session stopped, deepest first; the target is the last. */
|
|
17
16
|
readonly stopped: ReadonlyArray<Stopped>;
|
|
18
|
-
/** The ids of the courier_later messages cancelled. */
|
|
19
17
|
readonly cancelled: ReadonlyArray<string>;
|
|
20
|
-
/** With `cleanup`: what became of each isolated session's worktree, deepest first. */
|
|
21
18
|
readonly cleanup?: ReadonlyArray<CleanupResult | {
|
|
22
19
|
readonly sessionID: string;
|
|
23
20
|
readonly outcome: "failed";
|
|
24
21
|
readonly error: string;
|
|
25
22
|
}>;
|
|
26
|
-
/** The session told that the target was stopped, when that is not the caller. */
|
|
27
23
|
readonly told?: string;
|
|
28
24
|
}
|
|
29
|
-
|
|
30
|
-
* Stops a session and everything under it, deepest first, and cancels the messages scheduled for them; only a session above the target may.
|
|
31
|
-
* The caller is not told; the target's parent is, unless that is the caller.
|
|
32
|
-
*/
|
|
25
|
+
export declare const SETTLE_MS = 2000;
|
|
33
26
|
export declare function stop(ports: StopPorts, callerID: string, input: StopInput): Promise<StopResult>;
|
package/dist/tree/stop.js
CHANGED
|
@@ -6,15 +6,12 @@ import { childStore } from "./store.js";
|
|
|
6
6
|
import { subtree } from "./subtree.js";
|
|
7
7
|
import { stoppedNotice } from "./text.js";
|
|
8
8
|
import { forgetWakes } from "./wake.js";
|
|
9
|
-
/** Rounds of looking again for sessions started while others were being stopped. */
|
|
10
9
|
const ROUNDS = 3;
|
|
11
|
-
|
|
12
|
-
* Stops a session and everything under it, deepest first, and cancels the messages scheduled for them; only a session above the target may.
|
|
13
|
-
* The caller is not told; the target's parent is, unless that is the caller.
|
|
14
|
-
*/
|
|
10
|
+
export const SETTLE_MS = 2_000;
|
|
15
11
|
export async function stop(ports, callerID, input) {
|
|
16
|
-
const { storage } = ports.courier;
|
|
12
|
+
const { storage, session } = ports.courier;
|
|
17
13
|
const { sessionID } = input;
|
|
14
|
+
const startedAt = Date.now();
|
|
18
15
|
const chain = await childStore(storage).lineage(sessionID);
|
|
19
16
|
if (!chain.length)
|
|
20
17
|
throw new Error(`${sessionID} was not started with courier_spawn, so courier_stop cannot stop it.`);
|
|
@@ -31,15 +28,17 @@ export async function stop(ports, callerID, input) {
|
|
|
31
28
|
for (const node of pending)
|
|
32
29
|
seen.add(node.entry.sessionID);
|
|
33
30
|
nodes.push(...pending);
|
|
34
|
-
|
|
35
|
-
await Promise.all(pending.map((node) => silence(ports, node.entry.sessionID, cancelled)));
|
|
31
|
+
await Promise.all(pending.map((node) => silence(ports, node, cancelled)));
|
|
36
32
|
results.push(...(await eachDeepestFirst(pending, (node) => interrupt(ports, node))));
|
|
37
|
-
const meanwhile = (await subtree(storage, sessionID)).nodes.filter((node) => !seen.has(node.entry.sessionID));
|
|
33
|
+
const meanwhile = (await subtree(storage, sessionID, undefined, session)).nodes.filter((node) => !seen.has(node.entry.sessionID));
|
|
38
34
|
return round(meanwhile, left - 1);
|
|
39
35
|
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await
|
|
36
|
+
const first = (await subtree(storage, sessionID, undefined, session)).nodes;
|
|
37
|
+
const background = await backgroundSubagents(ports, first);
|
|
38
|
+
await round([{ entry: target, depth: 0 }, ...first], ROUNDS);
|
|
39
|
+
await settle(ports, nodes, results, background, startedAt);
|
|
40
|
+
const parents = new Set(nodes.flatMap((node) => (node.subagent ? [] : [node.entry.parentID])));
|
|
41
|
+
await Promise.all([...parents].map((parentID) => syncStanding(ports.courier, parentID)));
|
|
43
42
|
const told = target.parentID !== callerID && results.find((one) => one.sessionID === sessionID)?.outcome === "interrupted"
|
|
44
43
|
? await tell(ports, callerID, target)
|
|
45
44
|
: undefined;
|
|
@@ -51,15 +50,15 @@ export async function stop(ports, callerID, input) {
|
|
|
51
50
|
...(told ? { told } : {}),
|
|
52
51
|
};
|
|
53
52
|
}
|
|
54
|
-
/** Runs `act` on the nodes level by level, the deepest level first and the level's nodes at once; the results in that order. */
|
|
55
53
|
function eachDeepestFirst(nodes, act) {
|
|
56
54
|
const depths = [...new Set(nodes.map((node) => node.depth))].sort((a, b) => b - a);
|
|
57
55
|
return depths.reduce((before, depth) => before.then(async (done) => [...done, ...(await Promise.all(nodes.filter((node) => node.depth === depth).map(act)))]), Promise.resolve([]));
|
|
58
56
|
}
|
|
59
|
-
|
|
60
|
-
async function silence(ports, sessionID, cancelled) {
|
|
57
|
+
async function silence(ports, { entry, subagent }, cancelled) {
|
|
61
58
|
const { storage, now, log } = ports.courier;
|
|
62
|
-
|
|
59
|
+
const { sessionID } = entry;
|
|
60
|
+
if (!subagent)
|
|
61
|
+
await childStore(storage).stopped(sessionID, now()).catch((error) => log(`courier_stop: could not note that ${sessionID} was stopped: ${String(error)}`));
|
|
63
62
|
try {
|
|
64
63
|
cancelled.push(...(await ports.cancelScheduled(sessionID)));
|
|
65
64
|
}
|
|
@@ -68,23 +67,59 @@ async function silence(ports, sessionID, cancelled) {
|
|
|
68
67
|
}
|
|
69
68
|
await forgetWakes(ports.courier, sessionID).catch((error) => log(`courier_stop: could not drop the wake watch of ${sessionID}: ${String(error)}`));
|
|
70
69
|
}
|
|
71
|
-
async function interrupt(ports, { entry, depth }) {
|
|
70
|
+
async function interrupt(ports, { entry, depth, subagent }) {
|
|
72
71
|
const { sessionID, title } = entry;
|
|
73
72
|
const { storage, session, now } = ports.courier;
|
|
73
|
+
const one = { sessionID, title, depth, ...(subagent ? { subagent } : {}) };
|
|
74
74
|
try {
|
|
75
75
|
const { interrupted } = await session.interrupt({ sessionID });
|
|
76
|
-
//
|
|
77
|
-
if (interrupted)
|
|
76
|
+
// the interrupted event may arrive after this returns
|
|
77
|
+
if (interrupted && !subagent)
|
|
78
78
|
await childStore(storage).settled(sessionID, "interrupted", now()).catch(() => undefined);
|
|
79
|
-
return {
|
|
79
|
+
return { ...one, outcome: interrupted ? "interrupted" : "idle" };
|
|
80
80
|
}
|
|
81
81
|
catch (error) {
|
|
82
82
|
if (isNotFound(error))
|
|
83
|
-
return {
|
|
84
|
-
return {
|
|
83
|
+
return { ...one, outcome: "gone" };
|
|
84
|
+
return { ...one, outcome: "failed", error: describeFailure("session.interrupt", error).message };
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
// a completed background subagent call names its child (api-notes)
|
|
88
|
+
async function backgroundSubagents(ports, nodes) {
|
|
89
|
+
const parents = [...new Set(nodes.flatMap((node) => (node.subagent ? [node.entry.parentID] : [])))];
|
|
90
|
+
const found = await Promise.all(parents.map(async (parentID) => {
|
|
91
|
+
const messages = await ports.courier.session.context({ sessionID: parentID }).catch(() => []);
|
|
92
|
+
const returned = messages.flatMap((message) => message.type === "assistant" ? message.content.flatMap((part) => (part.type === "tool" && part.name === "subagent" && part.state.status === "completed" ? [part] : [])) : []);
|
|
93
|
+
const named = (id) => returned.some((part) => part.state.status === "completed" && JSON.stringify([part.state.content, part.state.metadata ?? null]).includes(id));
|
|
94
|
+
const ids = nodes.filter((node) => node.subagent && node.entry.parentID === parentID && named(node.entry.sessionID)).map((node) => node.entry.sessionID);
|
|
95
|
+
return [parentID, new Set(ids)];
|
|
96
|
+
}));
|
|
97
|
+
return new Map(found.filter(([, ids]) => ids.size));
|
|
98
|
+
}
|
|
99
|
+
// a background subagent's end wakes its parent (api-notes)
|
|
100
|
+
async function settle(ports, nodes, results, background, since) {
|
|
101
|
+
const ended = new Set(results.flatMap((one) => (one.subagent && one.outcome === "interrupted" ? [one.sessionID] : [])));
|
|
102
|
+
await Promise.all([...background].map(async ([parentID, ids]) => {
|
|
103
|
+
const waited = new Set([...ids].filter((id) => ended.has(id)));
|
|
104
|
+
if (!waited.size || !(await told(ports, parentID, waited, since)))
|
|
105
|
+
return;
|
|
106
|
+
const parent = nodes.find((node) => node.entry.sessionID === parentID);
|
|
107
|
+
if (parent && !parent.subagent)
|
|
108
|
+
await childStore(ports.courier.storage).stopped(parentID, ports.courier.now()).catch(() => undefined);
|
|
109
|
+
if (parent)
|
|
110
|
+
await interrupt(ports, parent);
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
async function told(ports, parentID, ids, since) {
|
|
114
|
+
const messages = await ports.courier.session.context({ sessionID: parentID }).catch(() => []);
|
|
115
|
+
const noticed = new Set(messages.flatMap((message) => (message.type === "synthetic" && message.time.created >= since && typeof message.metadata?.childID === "string" ? [message.metadata.childID] : [])));
|
|
116
|
+
if ([...ids].every((id) => noticed.has(id)))
|
|
117
|
+
return true;
|
|
118
|
+
if (Date.now() - since >= SETTLE_MS)
|
|
119
|
+
return false;
|
|
120
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
121
|
+
return told(ports, parentID, ids, since);
|
|
122
|
+
}
|
|
88
123
|
async function tell(ports, callerID, target) {
|
|
89
124
|
const { session, log } = ports.courier;
|
|
90
125
|
try {
|
|
@@ -102,9 +137,7 @@ async function tell(ports, callerID, target) {
|
|
|
102
137
|
return undefined;
|
|
103
138
|
}
|
|
104
139
|
}
|
|
105
|
-
/** Removes the isolated sessions' worktrees, deepest first, through courier_cleanup's checks; a failure is reported, not thrown. */
|
|
106
140
|
async function removeWorktrees(ports, nodes) {
|
|
107
|
-
// One at a time: git locks the repository's worktree list while it removes one.
|
|
108
141
|
const removeNext = async (queue, done) => {
|
|
109
142
|
const [next, ...rest] = queue;
|
|
110
143
|
if (!next)
|