opencode-courier 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +180 -274
  2. package/dist/index.js +47 -73
  3. package/dist/kernel/protocol.d.ts +20 -0
  4. package/dist/kernel/protocol.js +36 -0
  5. package/dist/kernel/tool.d.ts +5 -0
  6. package/dist/kernel/tool.js +5 -0
  7. package/dist/person/events.d.ts +23 -0
  8. package/dist/person/events.js +113 -0
  9. package/dist/person/form.d.ts +26 -0
  10. package/dist/person/form.js +64 -0
  11. package/dist/person/index.d.ts +17 -0
  12. package/dist/person/index.js +5 -0
  13. package/dist/{relay.d.ts → person/permission.d.ts} +27 -21
  14. package/dist/{relay.js → person/permission.js} +9 -5
  15. package/dist/person/ports.d.ts +58 -0
  16. package/dist/person/ports.js +2 -0
  17. package/dist/{question → person/question}/answer.d.ts +33 -4
  18. package/dist/{question → person/question}/answer.js +43 -8
  19. package/dist/person/question/index.d.ts +7 -0
  20. package/dist/{question → person/question}/index.js +3 -3
  21. package/dist/{question → person/question}/lifecycle.d.ts +1 -1
  22. package/dist/{question → person/question}/pure.d.ts +1 -2
  23. package/dist/{question → person/question}/pure.js +1 -1
  24. package/dist/{question → person/question}/relay.d.ts +1 -1
  25. package/dist/{question → person/question}/relay.js +14 -8
  26. package/dist/{question → person/question}/shared.d.ts +13 -3
  27. package/dist/person/question/shared.js +52 -0
  28. package/dist/person/question/types.d.ts +121 -0
  29. package/dist/person/question/types.js +1 -0
  30. package/dist/person/rpc-handlers.d.ts +160 -0
  31. package/dist/person/rpc-handlers.js +62 -0
  32. package/dist/person/rpc.d.ts +227 -0
  33. package/dist/person/rpc.js +70 -0
  34. package/dist/person/text.d.ts +68 -0
  35. package/dist/person/text.js +262 -0
  36. package/dist/person/tools.d.ts +20 -0
  37. package/dist/person/tools.js +55 -0
  38. package/dist/person/tui.d.ts +46 -0
  39. package/dist/{bounded.d.ts → platform/bounded.d.ts} +4 -0
  40. package/dist/{bounded.js → platform/bounded.js} +4 -0
  41. package/dist/platform/events.d.ts +34 -0
  42. package/dist/platform/events.js +121 -0
  43. package/dist/platform/hub.d.ts +195 -0
  44. package/dist/{hub.js → platform/hub.js} +27 -71
  45. package/dist/{version.js → platform/version.js} +1 -1
  46. package/dist/rpc.d.ts +2 -0
  47. package/dist/rpc.js +3 -0
  48. package/dist/scheduling/index.d.ts +6 -0
  49. package/dist/scheduling/index.js +5 -0
  50. package/dist/{later.d.ts → scheduling/later.d.ts} +7 -3
  51. package/dist/{later.js → scheduling/later.js} +12 -4
  52. package/dist/scheduling/text.d.ts +6 -0
  53. package/dist/scheduling/text.js +10 -0
  54. package/dist/scheduling/tools.d.ts +14 -0
  55. package/dist/scheduling/tools.js +48 -0
  56. package/dist/skill.d.ts +4 -0
  57. package/dist/skill.js +54 -1
  58. package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +1 -27
  59. package/dist/{cleanup.js → tree/cleanup.js} +9 -16
  60. package/dist/tree/context.d.ts +15 -0
  61. package/dist/tree/context.js +34 -0
  62. package/dist/tree/domain.d.ts +101 -0
  63. package/dist/tree/domain.js +68 -0
  64. package/dist/{watch.d.ts → tree/events.d.ts} +10 -37
  65. package/dist/tree/events.js +204 -0
  66. package/dist/tree/forget.d.ts +24 -0
  67. package/dist/tree/forget.js +59 -0
  68. package/dist/tree/group.d.ts +82 -0
  69. package/dist/tree/group.js +203 -0
  70. package/dist/tree/index.d.ts +10 -0
  71. package/dist/tree/index.js +11 -0
  72. package/dist/tree/limits.d.ts +10 -0
  73. package/dist/{limits.js → tree/limits.js} +6 -55
  74. package/dist/tree/panel.d.ts +13 -0
  75. package/dist/tree/panel.js +75 -0
  76. package/dist/tree/ports.d.ts +276 -0
  77. package/dist/tree/ports.js +1 -0
  78. package/dist/tree/report.d.ts +23 -0
  79. package/dist/tree/report.js +33 -0
  80. package/dist/tree/roster.d.ts +27 -0
  81. package/dist/tree/roster.js +62 -0
  82. package/dist/tree/rpc-handlers.d.ts +201 -0
  83. package/dist/tree/rpc-handlers.js +20 -0
  84. package/dist/tree/rpc.d.ts +344 -0
  85. package/dist/tree/rpc.js +83 -0
  86. package/dist/tree/send.d.ts +14 -0
  87. package/dist/tree/send.js +99 -0
  88. package/dist/tree/spawn.d.ts +21 -0
  89. package/dist/tree/spawn.js +173 -0
  90. package/dist/tree/status.d.ts +128 -0
  91. package/dist/tree/status.js +62 -0
  92. package/dist/{stop.d.ts → tree/stop.d.ts} +1 -9
  93. package/dist/{stop.js → tree/stop.js} +9 -13
  94. package/dist/tree/store.d.ts +24 -0
  95. package/dist/tree/store.js +264 -0
  96. package/dist/{tree.d.ts → tree/subtree.d.ts} +5 -6
  97. package/dist/{tree.js → tree/subtree.js} +7 -5
  98. package/dist/tree/summary.d.ts +7 -0
  99. package/dist/tree/summary.js +35 -0
  100. package/dist/tree/text.d.ts +77 -0
  101. package/dist/tree/text.js +287 -0
  102. package/dist/tree/tools.d.ts +54 -0
  103. package/dist/{tools.js → tree/tools.js} +12 -142
  104. package/dist/tree/tui.d.ts +53 -0
  105. package/dist/tui.d.ts +3 -0
  106. package/dist/tui.js +1342 -0
  107. package/dist/webhook/index.d.ts +5 -0
  108. package/dist/webhook/index.js +3 -0
  109. package/dist/{webhook.d.ts → webhook/receiver.d.ts} +17 -23
  110. package/dist/{webhook.js → webhook/receiver.js} +58 -50
  111. package/dist/webhook/subscriptions.d.ts +29 -0
  112. package/dist/webhook/subscriptions.js +52 -0
  113. package/dist/webhook/text.d.ts +23 -0
  114. package/dist/webhook/text.js +82 -0
  115. package/dist/webhook/tools.d.ts +19 -0
  116. package/dist/webhook/tools.js +46 -0
  117. package/package.json +18 -7
  118. package/dist/courier.d.ts +0 -200
  119. package/dist/courier.js +0 -315
  120. package/dist/group.d.ts +0 -86
  121. package/dist/group.js +0 -247
  122. package/dist/hub.d.ts +0 -300
  123. package/dist/limits.d.ts +0 -65
  124. package/dist/notices.d.ts +0 -356
  125. package/dist/notices.js +0 -678
  126. package/dist/question/index.d.ts +0 -6
  127. package/dist/question/shared.js +0 -24
  128. package/dist/report.d.ts +0 -48
  129. package/dist/report.js +0 -88
  130. package/dist/roster.d.ts +0 -93
  131. package/dist/roster.js +0 -206
  132. package/dist/tools.d.ts +0 -94
  133. package/dist/watch.js +0 -474
  134. /package/dist/{question → person/question}/lifecycle.js +0 -0
  135. /package/dist/{question → person/question}/runtime.d.ts +0 -0
  136. /package/dist/{question → person/question}/runtime.js +0 -0
  137. /package/dist/{json.d.ts → platform/json.d.ts} +0 -0
  138. /package/dist/{json.js → platform/json.js} +0 -0
  139. /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
  140. /package/dist/{storage.js → platform/storage.js} +0 -0
  141. /package/dist/{version.d.ts → platform/version.d.ts} +0 -0
@@ -0,0 +1,173 @@
1
+ import { describeFailure } from "../kernel/protocol.js";
2
+ import { isGroupName } from "./domain.js";
3
+ import { joinGroup, leaveGroup } from "./group.js";
4
+ import { admit } from "./limits.js";
5
+ import { childStore, groupStore } from "./store.js";
6
+ import { childBrief, childBranch } from "./text.js";
7
+ function titleOf(task) {
8
+ const line = task.trim().split("\n")[0] ?? "";
9
+ return line.length > 60 ? `${line.slice(0, 57)}...` : line;
10
+ }
11
+ /**
12
+ * The model a child runs on: its parent's, so it does not fall back to OpenCode's default, which
13
+ * the parent may have been moved off for a reason. An agent that names its own model keeps it.
14
+ */
15
+ async function inheritedModel(ports, parentID, agent) {
16
+ const parent = await ports.session.get({ sessionID: parentID });
17
+ if (!parent.model || !agent)
18
+ return parent.model;
19
+ const named = await ports.agent.get({ agentID: agent, location: { directory: ports.directory } });
20
+ return named.data.model ? undefined : parent.model;
21
+ }
22
+ /** Removes a worktree made for a child that was never created; one that cannot be removed stays. */
23
+ async function dropWorktree(ports, directory) {
24
+ try {
25
+ await ports.worktree.remove({ projectID: ports.projectID, directory, force: false });
26
+ }
27
+ catch {
28
+ // The spawn's own failure is the one to report.
29
+ }
30
+ }
31
+ /**
32
+ * Creates a child session, hands it the task and returns at once; the child reports back with
33
+ * courier_send. Refused past a limit, whether or not the `context` hook hid the tool.
34
+ */
35
+ export async function spawn(ports, parentID, input) {
36
+ // Some models send null, or an empty string, for an optional field they leave out.
37
+ const group = typeof input.group === "string" ? input.group.trim() || undefined : (input.group ?? undefined);
38
+ if (group !== undefined && !isGroupName(group))
39
+ throw new Error(`group must be a name of 1 to 60 letters, digits, dots, dashes or underscores, not ${JSON.stringify(group)}.`);
40
+ const admitted = await admit(ports, parentID);
41
+ try {
42
+ return await start(ports, parentID, input, admitted, group);
43
+ }
44
+ finally {
45
+ admitted.release();
46
+ }
47
+ }
48
+ async function start(ports, parentID, input, admitted, group) {
49
+ const { worktree, handedDown, ...child } = await create(ports, parentID, input);
50
+ admitted.reservation.sessionID = child.sessionID;
51
+ const entry = {
52
+ ...child,
53
+ parentID,
54
+ isolated: worktree !== undefined,
55
+ createdAt: ports.now(),
56
+ reports: "status",
57
+ ...(worktree ? { source: ports.directory, project: ports.projectID } : {}),
58
+ ...(group ? { group } : {}),
59
+ };
60
+ const { rosterError, groupError } = await enrol(ports, entry, group);
61
+ ports.roles.delete(parentID);
62
+ await handOver(ports, entry, admitted.depth, input.task, !rosterError, !rosterError && !groupError);
63
+ return {
64
+ sessionID: entry.sessionID,
65
+ directory: entry.directory,
66
+ ...(group && !rosterError && !groupError ? { group } : {}),
67
+ ...(rosterError ? { rosterError } : {}),
68
+ ...(groupError ? { groupError } : {}),
69
+ ...(handedDown ? { fromParent: true } : {}),
70
+ ...(handedDown?.uncommitted ? { uncommitted: true } : {}),
71
+ };
72
+ }
73
+ /** Creates the child's session, in a worktree of its own if asked, which goes again if the session cannot be created. */
74
+ async function create(ports, parentID, input) {
75
+ // A failed lookup must not keep the child from starting; it then runs on OpenCode's default.
76
+ const model = await inheritedModel(ports, parentID, input.agent).catch(() => undefined);
77
+ const handedDown = input.isolate ? await parentWork(ports, parentID) : undefined;
78
+ const worktree = input.isolate
79
+ ? (await ports.worktree.create({ projectID: ports.projectID, ...(handedDown ? { from: handedDown.directory, branch: handedDown.head } : {}) })).directory
80
+ : undefined;
81
+ const base = worktree ? await ports.head(worktree) : undefined;
82
+ const title = input.title ?? titleOf(input.task);
83
+ // OpenCode takes a location or a parentID, never both, so an isolated child cannot be a native one.
84
+ const native = ports.nativeChildren === true && !worktree;
85
+ const child = await ports.session
86
+ .create({
87
+ title,
88
+ ...(input.agent ? { agent: input.agent } : {}),
89
+ ...(model ? { model } : {}),
90
+ ...(worktree ? { location: { directory: worktree } } : {}),
91
+ // An empty ruleset, not none: a native child would otherwise run on its parent's session rules.
92
+ ...(native ? { parentID, permissions: [] } : {}),
93
+ metadata: { courier: { parentID } },
94
+ })
95
+ .catch(async (error) => {
96
+ // No session will ever use the fresh worktree, and nothing records it, so it goes now.
97
+ if (worktree)
98
+ await dropWorktree(ports, worktree);
99
+ throw error;
100
+ });
101
+ return {
102
+ sessionID: child.id,
103
+ title,
104
+ directory: worktree ?? child.location.directory,
105
+ worktree,
106
+ handedDown,
107
+ ...(base ? { base } : {}),
108
+ ...(native ? { native: true } : {}),
109
+ };
110
+ }
111
+ /**
112
+ * The worktree and HEAD an isolated parent hands its isolated child, by the reverse index alone; undefined for any
113
+ * other parent. An unreadable HEAD fails the spawn rather than start the child without the parent's work.
114
+ */
115
+ async function parentWork(ports, parentID) {
116
+ const parent = await childStore(ports.storage).indexedEntry(parentID);
117
+ if (!parent?.isolated)
118
+ return undefined;
119
+ const head = await ports.head(parent.directory);
120
+ if (!head)
121
+ throw new Error(`the HEAD of your worktree ${parent.directory} could not be read, so an isolated session cannot start from it; nothing was started. Start it without isolate, or fix the worktree.`);
122
+ const uncommitted = (await ports.dirty(parent.directory).catch(() => undefined)) === true;
123
+ return { directory: parent.directory, head, uncommitted };
124
+ }
125
+ /**
126
+ * Records the child on the roster, before the prompt, so a child that exists is on the roster even if prompting
127
+ * fails, then in its group. A failed write must not keep the child from its task: each is reported, not thrown.
128
+ */
129
+ async function enrol(ports, entry, group) {
130
+ const rosterError = await childStore(ports.storage).record(entry).then(() => undefined, (error) => describeFailure("roster", error).message);
131
+ const groupError = group && !rosterError ? await join(ports, entry, group) : undefined;
132
+ return { rosterError, groupError };
133
+ }
134
+ /**
135
+ * Hands the child its task. `rostered` says whether its report state is kept, which a child off the roster has none
136
+ * of, and `grouped` whether it is in its group; a child that never got its task leaves both, as it will never report.
137
+ */
138
+ async function handOver(ports, entry, depth, task, rostered, grouped) {
139
+ // A baseline, until OpenCode's event for the prompt's delivery moves it on; written first, so it never overtakes that.
140
+ const children = childStore(ports.storage);
141
+ if (rostered)
142
+ await children.prompted(entry.sessionID, ports.now()).catch(() => undefined);
143
+ try {
144
+ await ports.session.prompt({ sessionID: entry.sessionID, text: childBrief(entry.parentID, task, depth, ports.limits, entry.isolated ? childBranch(entry.sessionID) : undefined) });
145
+ }
146
+ catch (error) {
147
+ if (rostered)
148
+ await children.settled(entry.sessionID, "failed", ports.now()).catch(() => undefined);
149
+ if (grouped && entry.group)
150
+ await leaveGroup(groupStore(ports.storage), entry.parentID, entry.group, entry.sessionID, "failed", ports.now()).then((complete) => complete && ports.nudge(), (leaveError) => ports.log(`courier_spawn: ${entry.sessionID} could not leave group ${entry.group}: ${String(leaveError)}`));
151
+ throw error;
152
+ }
153
+ }
154
+ /**
155
+ * Joins a child to its group; one that cannot be joined is recorded in no group, so it reports on its own,
156
+ * and the result says so. Returns the error's message, if any.
157
+ */
158
+ async function join(ports, entry, group) {
159
+ const groups = groupStore(ports.storage);
160
+ try {
161
+ await joinGroup(groups, entry.parentID, group, entry.sessionID, entry.title, ports.now());
162
+ return undefined;
163
+ }
164
+ catch (error) {
165
+ const { group: _, ...ungrouped } = entry;
166
+ // A write that failed may have landed all the same: a member left there would hold its group up for good.
167
+ await Promise.all([
168
+ childStore(ports.storage).record(ungrouped).catch(() => undefined),
169
+ groups.remove(entry.parentID, group, entry.sessionID).catch(() => undefined),
170
+ ]);
171
+ return describeFailure("group", error).message;
172
+ }
173
+ }
@@ -0,0 +1,128 @@
1
+ import type { Plugin } from "@opencode/plugin";
2
+ import type { CourierPorts, RosterEntry } from "./ports.js";
3
+ type Context = Plugin.Context;
4
+ export interface StatusInput {
5
+ readonly sessionID: string;
6
+ }
7
+ export interface ChildrenInput {
8
+ readonly sessionID?: string;
9
+ }
10
+ /** A one-off look at a session, for check-ins; not meant to be called in a loop. */
11
+ export declare function status(ports: CourierPorts, input: StatusInput): Promise<Partial<{
12
+ sessionID: string;
13
+ title: string | undefined;
14
+ parentID: string | undefined;
15
+ outcome: "failed" | "interrupted" | "succeeded" | undefined;
16
+ updated: number;
17
+ idle: number | undefined;
18
+ lastText: string | undefined;
19
+ pending: readonly import("./ports.js").Pending[] | undefined;
20
+ }>>;
21
+ /** The text of a session's last reply, from its context's messages, or undefined when it has none. */
22
+ export declare function lastReply(messages: Awaited<ReturnType<Context["session"]["context"]>>): string | undefined;
23
+ /**
24
+ * The sessions a parent started, each with what courier_status reports, or the error it gave; a child in a
25
+ * group with the group's name and its standing there: `held`, `out`, how it left, `released` or `unknown`.
26
+ */
27
+ export declare function listChildren(ports: CourierPorts, parentID: string): Promise<({
28
+ group: {
29
+ name: string;
30
+ report: "failed" | "interrupted" | "deleted" | "dropped" | "held" | "out" | "released" | "unknown";
31
+ };
32
+ directory: string;
33
+ isolated: boolean;
34
+ created: number;
35
+ sessionID?: string | undefined;
36
+ title?: string | undefined;
37
+ parentID?: string | undefined;
38
+ outcome?: "failed" | "interrupted" | "succeeded" | undefined;
39
+ updated?: number | undefined;
40
+ idle?: number | undefined;
41
+ lastText?: string | undefined;
42
+ pending?: readonly import("./ports.js").Pending[] | undefined;
43
+ } | {
44
+ group?: undefined;
45
+ directory: string;
46
+ isolated: boolean;
47
+ created: number;
48
+ sessionID?: string | undefined;
49
+ title?: string | undefined;
50
+ parentID?: string | undefined;
51
+ outcome?: "failed" | "interrupted" | "succeeded" | undefined;
52
+ updated?: number | undefined;
53
+ idle?: number | undefined;
54
+ lastText?: string | undefined;
55
+ pending?: readonly import("./ports.js").Pending[] | undefined;
56
+ } | {
57
+ error: string;
58
+ group: {
59
+ name: string;
60
+ report: "failed" | "interrupted" | "deleted" | "dropped" | "held" | "out" | "released" | "unknown";
61
+ };
62
+ directory: string;
63
+ isolated: boolean;
64
+ created: number;
65
+ sessionID: string;
66
+ title: string;
67
+ } | {
68
+ error: string;
69
+ group?: undefined;
70
+ directory: string;
71
+ isolated: boolean;
72
+ created: number;
73
+ sessionID: string;
74
+ title: string;
75
+ })[]>;
76
+ /** One roster entry as courier_children shows it: what courier_status reports with its directory and group standing, or the error it gave. */
77
+ export declare function described(ports: CourierPorts, entry: RosterEntry): Promise<{
78
+ group: {
79
+ name: string;
80
+ report: "failed" | "interrupted" | "deleted" | "dropped" | "held" | "out" | "released" | "unknown";
81
+ };
82
+ directory: string;
83
+ isolated: boolean;
84
+ created: number;
85
+ sessionID?: string | undefined;
86
+ title?: string | undefined;
87
+ parentID?: string | undefined;
88
+ outcome?: "failed" | "interrupted" | "succeeded" | undefined;
89
+ updated?: number | undefined;
90
+ idle?: number | undefined;
91
+ lastText?: string | undefined;
92
+ pending?: readonly import("./ports.js").Pending[] | undefined;
93
+ } | {
94
+ group?: undefined;
95
+ directory: string;
96
+ isolated: boolean;
97
+ created: number;
98
+ sessionID?: string | undefined;
99
+ title?: string | undefined;
100
+ parentID?: string | undefined;
101
+ outcome?: "failed" | "interrupted" | "succeeded" | undefined;
102
+ updated?: number | undefined;
103
+ idle?: number | undefined;
104
+ lastText?: string | undefined;
105
+ pending?: readonly import("./ports.js").Pending[] | undefined;
106
+ } | {
107
+ error: string;
108
+ group: {
109
+ name: string;
110
+ report: "failed" | "interrupted" | "deleted" | "dropped" | "held" | "out" | "released" | "unknown";
111
+ };
112
+ directory: string;
113
+ isolated: boolean;
114
+ created: number;
115
+ sessionID: string;
116
+ title: string;
117
+ } | {
118
+ error: string;
119
+ group?: undefined;
120
+ directory: string;
121
+ isolated: boolean;
122
+ created: number;
123
+ sessionID: string;
124
+ title: string;
125
+ }>;
126
+ /** A child's standing in the join group its roster entry names, `unknown` when that cannot be read. */
127
+ export declare function standingIn(ports: Pick<CourierPorts, "storage">, entry: RosterEntry, group: string): Promise<"failed" | "interrupted" | "deleted" | "dropped" | "held" | "out" | "released" | "unknown">;
128
+ export {};
@@ -0,0 +1,62 @@
1
+ import { describeFailure } from "../kernel/protocol.js";
2
+ import { standingOf } from "./domain.js";
3
+ import { current } from "./roster.js";
4
+ import { groupStore } from "./store.js";
5
+ /** A one-off look at a session, for check-ins; not meant to be called in a loop. */
6
+ export async function status(ports, input) {
7
+ const [info, messages, pending] = await Promise.all([
8
+ ports.session.get({ sessionID: input.sessionID }),
9
+ ports.session.context({ sessionID: input.sessionID }),
10
+ ports.pending(input.sessionID),
11
+ ]);
12
+ const lastText = lastReply(messages);
13
+ return withoutUndefined({
14
+ sessionID: info.id,
15
+ title: info.title,
16
+ parentID: info.parentID,
17
+ outcome: info.outcome,
18
+ updated: info.time.updated,
19
+ idle: info.time.idle,
20
+ lastText,
21
+ pending: pending.length ? pending : undefined,
22
+ });
23
+ }
24
+ /** The text of a session's last reply, from its context's messages, or undefined when it has none. */
25
+ export function lastReply(messages) {
26
+ const last = messages.findLast((message) => message.type === "assistant");
27
+ return last?.type === "assistant"
28
+ ? last.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("")
29
+ : undefined;
30
+ }
31
+ /**
32
+ * The sessions a parent started, each with what courier_status reports, or the error it gave; a child in a
33
+ * group with the group's name and its standing there: `held`, `out`, how it left, `released` or `unknown`.
34
+ */
35
+ export async function listChildren(ports, parentID) {
36
+ const entries = await current(ports.storage, parentID, ports.now());
37
+ return Promise.all(entries.map((entry) => described(ports, entry)));
38
+ }
39
+ /** One roster entry as courier_children shows it: what courier_status reports with its directory and group standing, or the error it gave. */
40
+ export async function described(ports, entry) {
41
+ const group = entry.group ? { group: { name: entry.group, report: await standingIn(ports, entry, entry.group) } } : {};
42
+ const roster = { directory: entry.directory, isolated: entry.isolated, created: entry.createdAt, ...group };
43
+ try {
44
+ return { ...(await status(ports, { sessionID: entry.sessionID })), ...roster };
45
+ }
46
+ catch (error) {
47
+ return {
48
+ sessionID: entry.sessionID,
49
+ title: entry.title,
50
+ ...roster,
51
+ error: describeFailure("courier_status", error).message,
52
+ };
53
+ }
54
+ }
55
+ /** A child's standing in the join group its roster entry names, `unknown` when that cannot be read. */
56
+ export function standingIn(ports, entry, group) {
57
+ return groupStore(ports.storage).member(entry.parentID, group, entry.sessionID).then(standingOf, () => "unknown");
58
+ }
59
+ /** OpenCode leaves a tool call hanging when its metadata holds `undefined`, so results drop those keys. */
60
+ function withoutUndefined(value) {
61
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined));
62
+ }
@@ -1,12 +1,4 @@
1
- import { type CleanupPorts } from "./cleanup.js";
2
- import { type CourierPorts } from "./courier.js";
3
- import { type LaterPorts } from "./later.js";
4
- import { type CleanupResult } from "./notices.js";
5
- export interface StopPorts {
6
- readonly courier: CourierPorts;
7
- readonly later: LaterPorts;
8
- readonly cleanup: CleanupPorts;
9
- }
1
+ import type { CleanupResult, StopPorts } from "./ports.js";
10
2
  export interface StopInput {
11
3
  readonly sessionID: string;
12
4
  readonly cleanup?: boolean;
@@ -1,11 +1,9 @@
1
+ import { isNotFound } from "../platform/json.js";
2
+ import { describeFailure, envelope } from "../kernel/protocol.js";
1
3
  import { cleanup } from "./cleanup.js";
2
- import { describeFailure } from "./courier.js";
3
- import { isNotFound } from "./json.js";
4
- import { cancel, scheduledFor } from "./later.js";
5
- import { envelope, stoppedNotice } from "./notices.js";
6
- import { settled, stopped } from "./report.js";
7
- import { lineage } from "./roster.js";
8
- import { subtree } from "./tree.js";
4
+ import { childStore } from "./store.js";
5
+ import { subtree } from "./subtree.js";
6
+ import { stoppedNotice } from "./text.js";
9
7
  /** Rounds of looking again for sessions started while others were being stopped. */
10
8
  const ROUNDS = 3;
11
9
  /**
@@ -15,7 +13,7 @@ const ROUNDS = 3;
15
13
  export async function stop(ports, callerID, input) {
16
14
  const { storage } = ports.courier;
17
15
  const { sessionID } = input;
18
- const chain = await lineage(storage, sessionID);
16
+ const chain = await childStore(storage).lineage(sessionID);
19
17
  if (!chain.length)
20
18
  throw new Error(`${sessionID} was not started with courier_spawn, so courier_stop cannot stop it.`);
21
19
  const target = chain[0];
@@ -57,11 +55,9 @@ function eachDeepestFirst(nodes, act) {
57
55
  /** Notes that a session was stopped, so the failure and silent-end notices stay quiet for it, and cancels its pending messages. */
58
56
  async function silence(ports, sessionID, cancelled) {
59
57
  const { storage, now, log } = ports.courier;
60
- await stopped(storage, sessionID, now()).catch((error) => log(`courier_stop: could not note that ${sessionID} was stopped: ${String(error)}`));
58
+ await childStore(storage).stopped(sessionID, now()).catch((error) => log(`courier_stop: could not note that ${sessionID} was stopped: ${String(error)}`));
61
59
  try {
62
- const pending = await scheduledFor(storage, sessionID);
63
- const dropped = await Promise.all(pending.map(async (later) => ((await cancel(ports.later, later.id)) ? later.id : undefined)));
64
- cancelled.push(...dropped.filter((id) => id !== undefined));
60
+ cancelled.push(...(await ports.cancelScheduled(sessionID)));
65
61
  }
66
62
  catch (error) {
67
63
  log(`courier_stop: could not cancel the messages scheduled for ${sessionID}: ${String(error)}`);
@@ -74,7 +70,7 @@ async function interrupt(ports, { entry, depth }) {
74
70
  const { interrupted } = await session.interrupt({ sessionID });
75
71
  // The event for it says so too; noted here as well, so the end is settled when this returns.
76
72
  if (interrupted)
77
- await settled(storage, sessionID, "interrupted", now()).catch(() => undefined);
73
+ await childStore(storage).settled(sessionID, "interrupted", now()).catch(() => undefined);
78
74
  return { sessionID, title, depth, outcome: interrupted ? "interrupted" : "idle" };
79
75
  }
80
76
  catch (error) {
@@ -0,0 +1,24 @@
1
+ import { type Storage } from "../platform/storage.js";
2
+ import type { ChildStore, GroupStore } from "./ports.js";
3
+ /**
4
+ * What the reverse index holds for a child: the sessions above it, its parent first and the top
5
+ * session, which courier_spawn did not start, last; each is the parent of the one before it.
6
+ */
7
+ export interface ReverseEntry {
8
+ readonly ancestors: ReadonlyArray<string>;
9
+ }
10
+ export declare function rosterKey(parentID: string, sessionID: string): string;
11
+ export declare function reverseKey(sessionID: string): string;
12
+ export declare const promptKey: (sessionID: string) => string;
13
+ export declare const settledKey: (sessionID: string) => string;
14
+ export declare const progressKey: (sessionID: string) => string;
15
+ export declare const toldKey: (sessionID: string) => string;
16
+ export declare const stoppedKey: (sessionID: string) => string;
17
+ export declare const memberKey: (parentID: string, group: string, sessionID: string) => string;
18
+ /** The key prefix of a parent's groups, and of one group's members. */
19
+ export declare const groupsPrefix: (parentID: string) => string;
20
+ export declare const groupPrefix: (parentID: string, group: string) => string;
21
+ /** The `ChildStore` over the plugin's storage. */
22
+ export declare function childStore(storage: Storage): ChildStore;
23
+ /** The `GroupStore` over the plugin's storage. */
24
+ export declare function groupStore(storage: Storage): GroupStore;