opencode-courier 0.2.3 → 0.4.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.
Files changed (143) hide show
  1. package/README.md +198 -193
  2. package/dist/index.d.ts +4 -8
  3. package/dist/index.js +66 -85
  4. package/dist/kernel/protocol.d.ts +20 -0
  5. package/dist/kernel/protocol.js +36 -0
  6. package/dist/kernel/tool.d.ts +5 -0
  7. package/dist/kernel/tool.js +5 -0
  8. package/dist/person/events.d.ts +23 -0
  9. package/dist/person/events.js +113 -0
  10. package/dist/person/form.d.ts +26 -0
  11. package/dist/person/form.js +64 -0
  12. package/dist/person/index.d.ts +17 -0
  13. package/dist/person/index.js +5 -0
  14. package/dist/{relay.d.ts → person/permission.d.ts} +29 -25
  15. package/dist/{relay.js → person/permission.js} +11 -9
  16. package/dist/person/ports.d.ts +58 -0
  17. package/dist/person/ports.js +2 -0
  18. package/dist/{question → person/question}/answer.d.ts +33 -4
  19. package/dist/{question → person/question}/answer.js +49 -17
  20. package/dist/person/question/index.d.ts +7 -0
  21. package/dist/{question → person/question}/index.js +3 -3
  22. package/dist/person/question/lifecycle.d.ts +43 -0
  23. package/dist/{question → person/question}/lifecycle.js +10 -19
  24. package/dist/{question → person/question}/pure.d.ts +4 -6
  25. package/dist/{question → person/question}/pure.js +3 -4
  26. package/dist/{question → person/question}/relay.d.ts +1 -1
  27. package/dist/{question → person/question}/relay.js +27 -35
  28. package/dist/{question → person/question}/runtime.d.ts +2 -3
  29. package/dist/{question → person/question}/runtime.js +2 -3
  30. package/dist/{question → person/question}/shared.d.ts +13 -3
  31. package/dist/person/question/shared.js +52 -0
  32. package/dist/person/question/types.d.ts +121 -0
  33. package/dist/person/question/types.js +1 -0
  34. package/dist/person/rpc-handlers.d.ts +160 -0
  35. package/dist/person/rpc-handlers.js +62 -0
  36. package/dist/person/rpc.d.ts +227 -0
  37. package/dist/person/rpc.js +70 -0
  38. package/dist/person/text.d.ts +68 -0
  39. package/dist/{notices.js → person/text.js} +53 -190
  40. package/dist/person/tools.d.ts +20 -0
  41. package/dist/person/tools.js +55 -0
  42. package/dist/person/tui.d.ts +46 -0
  43. package/dist/platform/bounded.d.ts +12 -0
  44. package/dist/{bounded.js → platform/bounded.js} +6 -4
  45. package/dist/platform/events.d.ts +34 -0
  46. package/dist/platform/events.js +121 -0
  47. package/dist/platform/hub.d.ts +195 -0
  48. package/dist/{hub.js → platform/hub.js} +78 -119
  49. package/dist/platform/json.d.ts +8 -0
  50. package/dist/platform/json.js +15 -0
  51. package/dist/platform/version.d.ts +16 -0
  52. package/dist/{version.js → platform/version.js} +5 -11
  53. package/dist/rpc.d.ts +2 -0
  54. package/dist/rpc.js +3 -0
  55. package/dist/scheduling/index.d.ts +6 -0
  56. package/dist/scheduling/index.js +5 -0
  57. package/dist/{later.d.ts → scheduling/later.d.ts} +13 -9
  58. package/dist/{later.js → scheduling/later.js} +18 -9
  59. package/dist/scheduling/text.d.ts +6 -0
  60. package/dist/scheduling/text.js +10 -0
  61. package/dist/scheduling/tools.d.ts +14 -0
  62. package/dist/scheduling/tools.js +48 -0
  63. package/dist/skill.d.ts +17 -0
  64. package/dist/skill.js +62 -0
  65. package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +5 -26
  66. package/dist/{cleanup.js → tree/cleanup.js} +20 -15
  67. package/dist/tree/context.d.ts +15 -0
  68. package/dist/tree/context.js +34 -0
  69. package/dist/tree/domain.d.ts +101 -0
  70. package/dist/tree/domain.js +68 -0
  71. package/dist/tree/events.d.ts +71 -0
  72. package/dist/tree/events.js +204 -0
  73. package/dist/tree/forget.d.ts +24 -0
  74. package/dist/tree/forget.js +59 -0
  75. package/dist/tree/group.d.ts +82 -0
  76. package/dist/tree/group.js +203 -0
  77. package/dist/tree/index.d.ts +10 -0
  78. package/dist/tree/index.js +11 -0
  79. package/dist/tree/limits.d.ts +10 -0
  80. package/dist/tree/limits.js +60 -0
  81. package/dist/tree/panel.d.ts +13 -0
  82. package/dist/tree/panel.js +75 -0
  83. package/dist/tree/ports.d.ts +276 -0
  84. package/dist/tree/ports.js +1 -0
  85. package/dist/tree/report.d.ts +23 -0
  86. package/dist/tree/report.js +33 -0
  87. package/dist/tree/roster.d.ts +27 -0
  88. package/dist/tree/roster.js +62 -0
  89. package/dist/tree/rpc-handlers.d.ts +201 -0
  90. package/dist/tree/rpc-handlers.js +20 -0
  91. package/dist/tree/rpc.d.ts +344 -0
  92. package/dist/tree/rpc.js +83 -0
  93. package/dist/tree/send.d.ts +14 -0
  94. package/dist/tree/send.js +99 -0
  95. package/dist/tree/spawn.d.ts +21 -0
  96. package/dist/tree/spawn.js +173 -0
  97. package/dist/tree/status.d.ts +128 -0
  98. package/dist/tree/status.js +62 -0
  99. package/dist/tree/stop.d.ts +33 -0
  100. package/dist/tree/stop.js +116 -0
  101. package/dist/tree/store.d.ts +24 -0
  102. package/dist/tree/store.js +264 -0
  103. package/dist/tree/subtree.d.ts +101 -0
  104. package/dist/tree/subtree.js +60 -0
  105. package/dist/tree/summary.d.ts +7 -0
  106. package/dist/tree/summary.js +35 -0
  107. package/dist/tree/text.d.ts +77 -0
  108. package/dist/tree/text.js +287 -0
  109. package/dist/tree/tools.d.ts +54 -0
  110. package/dist/tree/tools.js +167 -0
  111. package/dist/tree/tui.d.ts +53 -0
  112. package/dist/tui.d.ts +3 -0
  113. package/dist/tui.js +1219 -0
  114. package/dist/webhook/index.d.ts +5 -0
  115. package/dist/webhook/index.js +3 -0
  116. package/dist/{webhook.d.ts → webhook/receiver.d.ts} +25 -36
  117. package/dist/{webhook.js → webhook/receiver.js} +66 -67
  118. package/dist/webhook/subscriptions.d.ts +29 -0
  119. package/dist/webhook/subscriptions.js +52 -0
  120. package/dist/webhook/text.d.ts +23 -0
  121. package/dist/webhook/text.js +82 -0
  122. package/dist/webhook/tools.d.ts +19 -0
  123. package/dist/webhook/tools.js +46 -0
  124. package/package.json +18 -7
  125. package/dist/bounded.d.ts +0 -10
  126. package/dist/courier.d.ts +0 -95
  127. package/dist/courier.js +0 -131
  128. package/dist/hub.d.ts +0 -325
  129. package/dist/json.d.ts +0 -4
  130. package/dist/json.js +0 -4
  131. package/dist/notices.d.ts +0 -246
  132. package/dist/question/index.d.ts +0 -6
  133. package/dist/question/lifecycle.d.ts +0 -52
  134. package/dist/question/shared.js +0 -24
  135. package/dist/roster.d.ts +0 -81
  136. package/dist/roster.js +0 -175
  137. package/dist/tools.d.ts +0 -74
  138. package/dist/tools.js +0 -228
  139. package/dist/version.d.ts +0 -22
  140. package/dist/watch.d.ts +0 -61
  141. package/dist/watch.js +0 -289
  142. /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
  143. /package/dist/{storage.js → platform/storage.js} +0 -0
@@ -0,0 +1,264 @@
1
+ import { num, obj, str } from "../platform/json.js";
2
+ import { isStatus } from "../kernel/protocol.js";
3
+ import { scanAll, scanEntries } from "../platform/storage.js";
4
+ import { lineageIn } from "./domain.js";
5
+ // The tree's storage: the only file that knows its keys and layouts. Older copies and a second server on the same data
6
+ // directory read and write the same keys, so a key or a stored value never changes shape.
7
+ const PREFIX = "roster/";
8
+ /**
9
+ * The reverse index: a child's `ReverseEntry` under `roster-by-child/<sessionID>`, written with its
10
+ * roster entry; only an index, so `roster/` is scanned for entries lacking one, and confirms a hit.
11
+ */
12
+ const BY_CHILD = "roster-by-child/";
13
+ export function rosterKey(parentID, sessionID) {
14
+ return `${PREFIX}${parentID}/${sessionID}`;
15
+ }
16
+ export function reverseKey(sessionID) {
17
+ return `${BY_CHILD}${sessionID}`;
18
+ }
19
+ /**
20
+ * Whether a spawned session has reported since its last prompt, under `report/<sessionID>/`: `prompt`, `settled` (with
21
+ * the report's status), `progress` (a message to its parent without a status) and `told`; docs/maintainers/architecture.md has each.
22
+ */
23
+ const REPORT_PREFIX = "report/";
24
+ export const promptKey = (sessionID) => `${REPORT_PREFIX}${sessionID}/prompt`;
25
+ export const settledKey = (sessionID) => `${REPORT_PREFIX}${sessionID}/settled`;
26
+ export const progressKey = (sessionID) => `${REPORT_PREFIX}${sessionID}/progress`;
27
+ export const toldKey = (sessionID) => `${REPORT_PREFIX}${sessionID}/told`;
28
+ export const stoppedKey = (sessionID) => `${REPORT_PREFIX}${sessionID}/stopped`;
29
+ const REPORT_KEYS = {
30
+ prompt: promptKey,
31
+ settled: settledKey,
32
+ progress: progressKey,
33
+ told: toldKey,
34
+ stopped: stoppedKey,
35
+ };
36
+ /**
37
+ * Join groups: each member of a parent's group under `group/<parentID>/<name>/<sessionID>`, with its
38
+ * held report or how it left; released and dropped once none is out. docs/maintainers/architecture.md, § Storage.
39
+ */
40
+ const GROUP_PREFIX = "group/";
41
+ export const memberKey = (parentID, group, sessionID) => `${GROUP_PREFIX}${parentID}/${group}/${sessionID}`;
42
+ /** The key prefix of a parent's groups, and of one group's members. */
43
+ export const groupsPrefix = (parentID) => `${GROUP_PREFIX}${parentID}/`;
44
+ export const groupPrefix = (parentID, group) => `${groupsPrefix(parentID)}${group}/`;
45
+ function isReverse(value) {
46
+ const ancestors = value?.ancestors;
47
+ return Array.isArray(ancestors) && ancestors.length > 0 && ancestors.every((id) => typeof id === "string");
48
+ }
49
+ /** Whether a reverse key's value leads to this entry. */
50
+ function indexes(value, entry) {
51
+ return isReverse(value) && value.ancestors[0] === entry.parentID;
52
+ }
53
+ const timeOf = (value) => {
54
+ const at = obj(value).at;
55
+ return typeof at === "number" && Number.isFinite(at) ? at : undefined;
56
+ };
57
+ const noteOf = (value) => ({ at: timeOf(value), by: obj(value).by, status: obj(value).status });
58
+ /** The `ChildStore` over the plugin's storage. */
59
+ export function childStore(storage) {
60
+ const entry = async (parentID, sessionID) => (await storage.get(rosterKey(parentID, sessionID)));
61
+ const all = () => scanAll(storage, PREFIX);
62
+ const indexedParent = async (sessionID) => {
63
+ const indexed = await storage.get(reverseKey(sessionID));
64
+ return isReverse(indexed) ? indexed.ancestors[0] : undefined;
65
+ };
66
+ const walkIndex = async (sessionID, first) => {
67
+ let indexed = first;
68
+ const chain = [];
69
+ let below = sessionID;
70
+ // One read per level, all at once, and one more for the top's own reverse key, in case it was
71
+ // recorded after the key that names it; the chain ends where an entry is gone, as a scan's would.
72
+ for (;;) {
73
+ const { ancestors } = indexed;
74
+ const top = ancestors.at(-1);
75
+ const [entries, above] = await Promise.all([
76
+ Promise.all(ancestors.map((parentID, level) => storage.get(rosterKey(parentID, level ? ancestors[level - 1] : below)))),
77
+ storage.get(reverseKey(top)),
78
+ ]);
79
+ for (const found of entries) {
80
+ if (!found || chain.some((known) => known.sessionID === found.sessionID))
81
+ return chain;
82
+ chain.push(found);
83
+ }
84
+ if (!isReverse(above))
85
+ return chain;
86
+ below = top;
87
+ indexed = above;
88
+ }
89
+ };
90
+ const lineage = async (sessionID) => {
91
+ const first = await storage.get(reverseKey(sessionID));
92
+ if (!isReverse(first))
93
+ return lineageIn(await all(), sessionID);
94
+ return walkIndex(sessionID, first);
95
+ };
96
+ const index = async (child) => {
97
+ const above = await storage.get(reverseKey(child.parentID));
98
+ const ancestors = isReverse(above) ? above.ancestors : (await lineage(child.parentID)).map((parent) => parent.parentID);
99
+ await storage.set(reverseKey(child.sessionID), { ancestors: [child.parentID, ...ancestors] });
100
+ };
101
+ /**
102
+ * Drops a reverse key whose entry was not in the roster read, unless that entry is there now:
103
+ * another instance may have recorded the child since, and it writes the entry before the key.
104
+ */
105
+ const dropDangling = async (sessionID, value) => {
106
+ if (isReverse(value) && (await storage.get(rosterKey(value.ancestors[0], sessionID))) !== undefined)
107
+ return;
108
+ await storage.remove(reverseKey(sessionID));
109
+ };
110
+ const forgetReport = async (sessionID) => {
111
+ await Promise.all(Object.values(REPORT_KEYS).map((key) => storage.remove(key(sessionID))));
112
+ };
113
+ return {
114
+ // The entry is what counts: written first, so a failed index write leaves it to be found by a scan.
115
+ async record(child) {
116
+ await storage.set(rosterKey(child.parentID, child.sessionID), { ...child });
117
+ await index(child).catch(() => undefined);
118
+ },
119
+ async remove(parentID, sessionID) {
120
+ await Promise.all([
121
+ storage.remove(rosterKey(parentID, sessionID)),
122
+ storage.remove(reverseKey(sessionID)).catch(() => undefined),
123
+ forgetReport(sessionID).catch(() => undefined),
124
+ ]);
125
+ },
126
+ entry,
127
+ async children(parentID) {
128
+ const entries = await scanAll(storage, `${PREFIX}${parentID}/`);
129
+ return entries.sort((a, b) => a.createdAt - b.createdAt);
130
+ },
131
+ all,
132
+ async entriesOf(sessionID) {
133
+ const parentID = await indexedParent(sessionID);
134
+ if (parentID === undefined)
135
+ return (await all()).filter((found) => found.sessionID === sessionID);
136
+ const found = await storage.get(rosterKey(parentID, sessionID));
137
+ return found === undefined ? [] : [found];
138
+ },
139
+ indexedParent,
140
+ async indexedEntry(sessionID) {
141
+ const parentID = await indexedParent(sessionID);
142
+ return parentID === undefined ? undefined : entry(parentID, sessionID);
143
+ },
144
+ lineage,
145
+ async indexedLineage(sessionID) {
146
+ const first = await storage.get(reverseKey(sessionID));
147
+ return isReverse(first) ? walkIndex(sessionID, first) : [];
148
+ },
149
+ async backfill(entries) {
150
+ const indexed = new Map((await scanEntries(storage, BY_CHILD)).map(({ key, value }) => [key, value]));
151
+ const sessions = new Set(entries.map((child) => child.sessionID));
152
+ await Promise.all([
153
+ ...entries
154
+ .filter((child) => !indexes(indexed.get(child.sessionID), child))
155
+ .map((child) => storage.set(reverseKey(child.sessionID), { ancestors: lineageIn(entries, child.sessionID).map((above) => above.parentID) })),
156
+ ...[...indexed]
157
+ .filter(([sessionID]) => !sessions.has(sessionID))
158
+ .map(([sessionID, value]) => dropDangling(sessionID, value)),
159
+ ]);
160
+ },
161
+ async notes(sessionID, fields) {
162
+ const values = await Promise.all(fields.map((field) => storage.get(REPORT_KEYS[field](sessionID))));
163
+ return Object.fromEntries(fields.map((field, i) => [field, noteOf(values[i])]));
164
+ },
165
+ // Both of the hub's subscriptions note each event, one of them maybe behind: an earlier note never replaces a later one.
166
+ async prompted(sessionID, at) {
167
+ if ((timeOf(await storage.get(promptKey(sessionID))) ?? -Infinity) < at)
168
+ await storage.set(promptKey(sessionID), { at });
169
+ },
170
+ async settled(sessionID, by, at, status) {
171
+ if ((timeOf(await storage.get(settledKey(sessionID))) ?? -Infinity) <= at)
172
+ await storage.set(settledKey(sessionID), { at, by, ...(status ? { status } : {}) });
173
+ },
174
+ async progressed(sessionID, at) {
175
+ if ((timeOf(await storage.get(progressKey(sessionID))) ?? -Infinity) <= at)
176
+ await storage.set(progressKey(sessionID), { at });
177
+ },
178
+ async told(sessionID, at) {
179
+ if ((timeOf(await storage.get(toldKey(sessionID))) ?? -Infinity) <= at)
180
+ await storage.set(toldKey(sessionID), { at });
181
+ },
182
+ async untold(sessionID, at) {
183
+ const stored = await storage.get(toldKey(sessionID)).then(timeOf, () => at);
184
+ if (stored === at)
185
+ await storage.remove(toldKey(sessionID));
186
+ },
187
+ async stopped(sessionID, at) {
188
+ await storage.set(stoppedKey(sessionID), { at });
189
+ },
190
+ async peek(sessionID, field) {
191
+ const value = await storage.get(REPORT_KEYS[field](sessionID));
192
+ return value;
193
+ },
194
+ async restore(sessionID, field, value) {
195
+ const key = REPORT_KEYS[field](sessionID);
196
+ await (value === undefined ? storage.remove(key) : storage.set(key, value));
197
+ },
198
+ forgetReport,
199
+ async reported() {
200
+ const keys = new Map();
201
+ for (const { key } of await scanEntries(storage, REPORT_PREFIX)) {
202
+ const slash = key.indexOf("/");
203
+ const sessionID = key.slice(0, slash);
204
+ if (slash > 0)
205
+ keys.set(sessionID, [...(keys.get(sessionID) ?? []), `${REPORT_PREFIX}${key}`]);
206
+ }
207
+ return keys;
208
+ },
209
+ async dropReported(keys) {
210
+ await Promise.all(keys.map((key) => storage.remove(key).catch(() => undefined)));
211
+ },
212
+ };
213
+ }
214
+ function readMember(value) {
215
+ const given = obj(value);
216
+ const title = str(given.title);
217
+ const joinedAt = num(given.joinedAt);
218
+ if (title === undefined || joinedAt === undefined)
219
+ return undefined;
220
+ const report = readReport(given.report);
221
+ const left = obj(given.left);
222
+ const by = str(left.by);
223
+ return {
224
+ title,
225
+ joinedAt,
226
+ ...(report ? { report } : {}),
227
+ ...(by === "failed" || by === "interrupted" || by === "deleted" || by === "dropped" ? { left: { at: num(left.at) ?? 0, by } } : {}),
228
+ };
229
+ }
230
+ function readReport(value) {
231
+ const given = obj(value);
232
+ const status = given.status;
233
+ const message = str(given.message);
234
+ const at = num(given.at);
235
+ if (!isStatus(status) || message === undefined || at === undefined)
236
+ return undefined;
237
+ return { at, status, message, ...(given.artifacts !== undefined ? { artifacts: given.artifacts } : {}) };
238
+ }
239
+ /** A member as the storage takes it, which asks for an index signature the interface lacks. */
240
+ const stored = (member) => ({ ...member });
241
+ /** The `GroupStore` over the plugin's storage. */
242
+ export function groupStore(storage) {
243
+ /** The members under `prefix`, a key prefix within `GROUP_PREFIX`; a value that is not a member is left out. */
244
+ const membersUnder = async (prefix) => {
245
+ const entries = await scanEntries(storage, prefix);
246
+ return entries.flatMap(({ key, value }) => {
247
+ const parts = `${prefix.slice(GROUP_PREFIX.length)}${key}`.split("/");
248
+ const member = parts.length === 3 ? readMember(value) : undefined;
249
+ return member ? [{ ...member, parentID: parts[0], group: parts[1], sessionID: parts[2] }] : [];
250
+ });
251
+ };
252
+ return {
253
+ member: async (parentID, group, sessionID) => readMember(await storage.get(memberKey(parentID, group, sessionID))),
254
+ all: () => membersUnder(GROUP_PREFIX),
255
+ ofParent: (parentID) => membersUnder(groupsPrefix(parentID)),
256
+ ofGroup: (parentID, group) => membersUnder(groupPrefix(parentID, group)),
257
+ async put(parentID, group, sessionID, member) {
258
+ await storage.set(memberKey(parentID, group, sessionID), stored(member));
259
+ },
260
+ async remove(parentID, group, sessionID) {
261
+ await storage.remove(memberKey(parentID, group, sessionID));
262
+ },
263
+ };
264
+ }
@@ -0,0 +1,101 @@
1
+ import type { CourierPorts, RosterEntry, RosterStorage } from "./ports.js";
2
+ /** A session below the one a subtree is read from, with how many levels below it is: a child is at depth 1. */
3
+ export interface Below {
4
+ readonly entry: RosterEntry;
5
+ readonly depth: number;
6
+ }
7
+ /**
8
+ * The sessions started from a session, directly or through others, read one prefix scan per parent, at most `max`
9
+ * (`truncated` if more), in the order a tree is drawn.
10
+ */
11
+ export declare function subtree(storage: RosterStorage, rootID: string, max?: number): Promise<{
12
+ nodes: Below[];
13
+ truncated: boolean;
14
+ }>;
15
+ /**
16
+ * The subtree under a session, at most `maxTotal` sessions: what courier_children shows of each, plus its depth,
17
+ * its parent and its report state.
18
+ */
19
+ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
20
+ truncated?: number | undefined;
21
+ sessionID: string;
22
+ sessions: ({
23
+ report?: {
24
+ status?: "done" | "partial" | "blocked" | "failed" | undefined;
25
+ owes: boolean;
26
+ progressed: boolean;
27
+ ended: boolean;
28
+ } | undefined;
29
+ parentID: string;
30
+ depth: number;
31
+ group: {
32
+ name: string;
33
+ report: "failed" | "interrupted" | "deleted" | "dropped" | "held" | "out" | "released" | "unknown";
34
+ };
35
+ directory: string;
36
+ isolated: boolean;
37
+ created: number;
38
+ sessionID?: string | undefined;
39
+ title?: string | undefined;
40
+ outcome?: "failed" | "interrupted" | "succeeded" | undefined;
41
+ updated?: number | undefined;
42
+ idle?: number | undefined;
43
+ lastText?: string | undefined;
44
+ pending?: readonly import("./ports.js").Pending[] | undefined;
45
+ } | {
46
+ report?: {
47
+ status?: "done" | "partial" | "blocked" | "failed" | undefined;
48
+ owes: boolean;
49
+ progressed: boolean;
50
+ ended: boolean;
51
+ } | undefined;
52
+ parentID: string;
53
+ depth: number;
54
+ group?: undefined;
55
+ directory: string;
56
+ isolated: boolean;
57
+ created: number;
58
+ sessionID?: string | undefined;
59
+ title?: string | undefined;
60
+ outcome?: "failed" | "interrupted" | "succeeded" | undefined;
61
+ updated?: number | undefined;
62
+ idle?: number | undefined;
63
+ lastText?: string | undefined;
64
+ pending?: readonly import("./ports.js").Pending[] | undefined;
65
+ } | {
66
+ report?: {
67
+ status?: "done" | "partial" | "blocked" | "failed" | undefined;
68
+ owes: boolean;
69
+ progressed: boolean;
70
+ ended: boolean;
71
+ } | undefined;
72
+ parentID: string;
73
+ depth: number;
74
+ error: string;
75
+ group: {
76
+ name: string;
77
+ report: "failed" | "interrupted" | "deleted" | "dropped" | "held" | "out" | "released" | "unknown";
78
+ };
79
+ directory: string;
80
+ isolated: boolean;
81
+ created: number;
82
+ sessionID: string;
83
+ title: string;
84
+ } | {
85
+ report?: {
86
+ status?: "done" | "partial" | "blocked" | "failed" | undefined;
87
+ owes: boolean;
88
+ progressed: boolean;
89
+ ended: boolean;
90
+ } | undefined;
91
+ parentID: string;
92
+ depth: number;
93
+ error: string;
94
+ group?: undefined;
95
+ directory: string;
96
+ isolated: boolean;
97
+ created: number;
98
+ sessionID: string;
99
+ title: string;
100
+ })[];
101
+ }>;
@@ -0,0 +1,60 @@
1
+ import { lastStatus, reportOf } from "./report.js";
2
+ import { described } from "./status.js";
3
+ import { childStore } from "./store.js";
4
+ /**
5
+ * The sessions started from a session, directly or through others, read one prefix scan per parent, at most `max`
6
+ * (`truncated` if more), in the order a tree is drawn.
7
+ */
8
+ export async function subtree(storage, rootID, max = Number.POSITIVE_INFINITY) {
9
+ const children = childStore(storage);
10
+ const byParent = new Map();
11
+ const seen = new Set([rootID]);
12
+ let truncated = false;
13
+ const expand = async (level, depth) => {
14
+ if (!level.length || truncated)
15
+ return;
16
+ const found = await Promise.all(level.map((parentID) => children.children(parentID)));
17
+ const next = [];
18
+ for (const [index, entries] of found.entries()) {
19
+ const fresh = entries.filter((entry) => !seen.has(entry.sessionID));
20
+ const kept = fresh.slice(0, Math.max(0, max - (seen.size - 1)));
21
+ truncated ||= kept.length < fresh.length;
22
+ for (const entry of kept)
23
+ seen.add(entry.sessionID);
24
+ if (kept.length)
25
+ byParent.set(level[index], kept.map((entry) => ({ entry, depth })));
26
+ next.push(...kept.map((entry) => entry.sessionID));
27
+ }
28
+ return expand(next, depth + 1);
29
+ };
30
+ await expand([rootID], 1);
31
+ return { nodes: drawn(byParent, rootID), truncated };
32
+ }
33
+ /** The nodes in the order a tree is drawn: each followed by what it started. */
34
+ function drawn(byParent, parentID) {
35
+ return (byParent.get(parentID) ?? []).flatMap((node) => [node, ...drawn(byParent, node.entry.sessionID)]);
36
+ }
37
+ /**
38
+ * The subtree under a session, at most `maxTotal` sessions: what courier_children shows of each, plus its depth,
39
+ * its parent and its report state.
40
+ */
41
+ export async function tree(ports, rootID) {
42
+ const { nodes, truncated } = await subtree(ports.storage, rootID, ports.limits.maxTotal);
43
+ const children = childStore(ports.storage);
44
+ const listed = await Promise.all(nodes.map(async ({ entry, depth }) => {
45
+ const [info, report, last] = await Promise.all([
46
+ described(ports, entry),
47
+ reportOf(children, entry.sessionID).catch(() => undefined),
48
+ lastStatus(children, entry.sessionID).catch(() => undefined),
49
+ ]);
50
+ return {
51
+ ...info,
52
+ parentID: entry.parentID,
53
+ depth,
54
+ ...(report
55
+ ? { report: { owes: report.owes, progressed: report.progressed, ended: report.ended, ...(last ? { status: last } : {}) } }
56
+ : {}),
57
+ };
58
+ }));
59
+ return { sessionID: rootID, sessions: listed, ...(truncated ? { truncated: ports.limits.maxTotal } : {}) };
60
+ }
@@ -0,0 +1,7 @@
1
+ import type { CourierPorts } from "./ports.js";
2
+ import type { Summary } from "./rpc.js";
3
+ /**
4
+ * The sessions started under a session, at most `maxTotal` as courier_tree reads them, counted by what they
5
+ * are doing, and its open join groups, those of its own children, with how far each has reported.
6
+ */
7
+ export declare function summary(ports: CourierPorts, sessionID: string): Promise<Summary>;
@@ -0,0 +1,35 @@
1
+ import { busy } from "./domain.js";
2
+ import { groupStandings } from "./group.js";
3
+ import { groupStore } from "./store.js";
4
+ import { subtree } from "./subtree.js";
5
+ /** What a session is doing: waiting on a request it made, running, or idle; undefined when OpenCode no longer knows it. */
6
+ async function activity(ports, sessionID) {
7
+ const [info, pending] = await Promise.all([
8
+ ports.session.get({ sessionID }).catch(() => undefined),
9
+ ports.pending(sessionID).catch(() => []),
10
+ ]);
11
+ if (!info)
12
+ return undefined;
13
+ if (pending.length)
14
+ return "waiting";
15
+ return busy(info.time) ? "running" : "idle";
16
+ }
17
+ /**
18
+ * The sessions started under a session, at most `maxTotal` as courier_tree reads them, counted by what they
19
+ * are doing, and its open join groups, those of its own children, with how far each has reported.
20
+ */
21
+ export async function summary(ports, sessionID) {
22
+ const counted = async () => {
23
+ const { nodes, truncated } = await subtree(ports.storage, sessionID, ports.limits.maxTotal);
24
+ return { activities: await Promise.all(nodes.map(({ entry }) => activity(ports, entry.sessionID))), truncated };
25
+ };
26
+ const [{ activities, truncated }, standings] = await Promise.all([counted(), groupStandings(groupStore(ports.storage), sessionID)]);
27
+ const count = (kind) => activities.filter((item) => item === kind).length;
28
+ return {
29
+ running: count("running"),
30
+ waiting: count("waiting"),
31
+ idle: count("idle"),
32
+ groups: standings.map(({ name, reported, members }) => ({ name, reported, members })),
33
+ ...(truncated ? { truncated: ports.limits.maxTotal } : {}),
34
+ };
35
+ }
@@ -0,0 +1,77 @@
1
+ import type { Artifacts, CleanupResult, ExecutionError, HeldReport, LeftMember, Limits, Sent } from "./ports.js";
2
+ /** The branch an isolated child commits its work on, and names in the `branch` of its report. */
3
+ export declare const childBranch: (sessionID: string) => string;
4
+ /** What a spawned session at `depth` is told; `depth` 1 is a child of a session nobody spawned. `branch` is for an isolated one. */
5
+ export declare function childBrief(parentID: string, task: string, depth: number, limits: Limits, branch?: string): string;
6
+ /** How every part the context hook adds begins, so a second copy of the plugin does not add its own. */
7
+ export declare const ROLE_PREFIX = "opencode-courier role:";
8
+ /**
9
+ * The system part naming a session's place in its tree: `depth` 0 for a session nobody spawned that
10
+ * has started one, `null` for one that has started none, which is pointed to the skill. The same on every request.
11
+ */
12
+ export declare function rolePart(depth: number | null, limits: Limits): string;
13
+ export declare const depthRefusal: (depth: number, maxDepth: number) => string;
14
+ export declare const childrenRefusal: (running: number, maxChildren: number) => string;
15
+ export declare const totalRefusal: (running: number, maxTotal: number) => string;
16
+ /**
17
+ * The body of a report: its text, then the artifacts in a fixed layout, the same for every report,
18
+ * so a parent finds the branch, the commits, the files and the checks without reading prose.
19
+ */
20
+ export declare function reportBody(message: string, artifacts: Artifacts | undefined): string;
21
+ export declare function spawnText(child: {
22
+ readonly sessionID: string;
23
+ readonly directory: string;
24
+ readonly group?: string;
25
+ readonly rosterError?: string;
26
+ readonly groupError?: string;
27
+ readonly fromParent?: boolean;
28
+ readonly uncommitted?: boolean;
29
+ }): string;
30
+ /** courier_send's result: for a message to the session that started the sender, whether it was its report. */
31
+ export declare function sendText(sessionID: string, sent?: Pick<Sent, "status" | "report" | "held">): string;
32
+ /** courier_status's result: the session's state, as JSON. */
33
+ export declare const statusText: (status: unknown) => string;
34
+ export declare const childrenText: (listed: ReadonlyArray<unknown>) => string;
35
+ export declare function cleanupText(result: CleanupResult): string;
36
+ /** courier_tree's result: the sessions under a session, drawn depth first, as JSON. */
37
+ export declare function treeText(tree: {
38
+ readonly sessions: ReadonlyArray<unknown>;
39
+ readonly truncated?: number;
40
+ }): string;
41
+ /** What courier_stop did to each session, and to the messages and worktrees. */
42
+ export declare function stopText(result: {
43
+ readonly sessionID: string;
44
+ readonly stopped: ReadonlyArray<{
45
+ readonly sessionID: string;
46
+ readonly outcome: string;
47
+ readonly error?: string;
48
+ }>;
49
+ readonly cancelled: ReadonlyArray<string>;
50
+ readonly cleanup?: ReadonlyArray<CleanupResult | {
51
+ readonly sessionID: string;
52
+ readonly outcome: "failed";
53
+ readonly error: string;
54
+ }>;
55
+ readonly told?: string;
56
+ }): string;
57
+ /** What the parent of a stopped session is told when the stop was ordered by a session above both. */
58
+ export declare const stoppedNotice: (title: string, by: string) => string;
59
+ /** Why a worktree in this state must be kept, or undefined when removing it loses nothing. */
60
+ export declare function keepReason(state: {
61
+ readonly changes: readonly string[];
62
+ readonly commits: readonly string[];
63
+ }): string | undefined;
64
+ export declare function failureNotice(title: string, error: ExecutionError, group?: string): string;
65
+ /**
66
+ * What the parent is told when a session it started ends its turn without a report to it;
67
+ * `progressed` when it messaged the parent without a status since its last prompt.
68
+ */
69
+ export declare function silentNotice(title: string, lastText: string | undefined, progressed?: boolean, group?: string): string;
70
+ /**
71
+ * What the parent gets when a group it named is released: every report, with its status, text and artifacts in
72
+ * `reportBody`'s layout, and the members that left without one, in the order they joined.
73
+ */
74
+ export declare function groupNotice(group: string, reports: ReadonlyArray<HeldReport & {
75
+ readonly sessionID: string;
76
+ readonly title: string;
77
+ }>, left?: ReadonlyArray<LeftMember>): string;