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
package/dist/watch.js DELETED
@@ -1,474 +0,0 @@
1
- import { addBounded, setBounded } from "./bounded.js";
2
- import { lastReply } from "./courier.js";
3
- import { dropGroups, hasCompleteGroup, markLeft, membershipOf } from "./group.js";
4
- import { envelope, failureNotice, formNotice, formSettledNotice, kindOf, permissionNotice, permissionSettledNotice, silentNotice, } from "./notices.js";
5
- import { eventsFollowed, eventsLeft, formShown, formsMayHaveBeenMissed, locationClosing, pendingQuestions } from "./question/index.js";
6
- import { listEverywhere, QUESTION_FORM, } from "./relay.js";
7
- import { scheduledFor } from "./later.js";
8
- import { awaited, forgetReport, prompted, reportOf, settled, stoppedSincePrompt, told, untold } from "./report.js";
9
- import { allEntries, children, entriesOf, entryUnder, indexedEntry, indexedParent, lineage } from "./roster.js";
10
- import { subscriptions } from "./webhook.js";
11
- /** How long to wait before subscribing again after the event stream ended or broke. */
12
- export const RESUBSCRIBE_MS = 5_000;
13
- /** How many handled event ids, and waiting or answered requests, are remembered. */
14
- const SEEN_MAX = 1_000;
15
- /** Claims a value in one of the shared sets; false when it was claimed already. */
16
- const claim = (set, value) => addBounded(set, value, SEEN_MAX);
17
- /**
18
- * Tells the parent of a child whose turn failed, since a child that cannot reach its model cannot
19
- * call courier_send. Returns the parents told.
20
- */
21
- export async function reportFailure(ports, seen, event) {
22
- if (!claim(seen, event.id))
23
- return [];
24
- const { sessionID, error } = event.data;
25
- const entries = await entriesOf(ports.storage, sessionID);
26
- const at = event.created ?? ports.now();
27
- // Not in the way of the notice: kept only so the limits stop counting the child.
28
- if (entries.length)
29
- await settled(ports.storage, sessionID, "failed", at).catch((error) => ports.log(`courier watch: could not note the failed turn of ${sessionID}: ${String(error)}`));
30
- const out = await Promise.all(entries.map((entry) => outIn(ports, entry)));
31
- // Left before the notice, so the parent waits on the group meanwhile; nudged after it, so the group's
32
- // message does not overtake the notice, short of an interval tick falling in between.
33
- const completed = await Promise.all(out.map((member) => (member ? leave(ports, member, "failed", at) : Promise.resolve(false))));
34
- // A session courier_stop stopped is not a failure to tell: its parent was told of the stop, or ordered it.
35
- const quiet = entries.length > 0 && (await stoppedSincePrompt(ports.storage, sessionID).catch(() => false));
36
- try {
37
- if (quiet)
38
- return [];
39
- await Promise.all(entries.map((entry, index) => ports.session.synthetic({
40
- sessionID: entry.parentID,
41
- text: envelope(sessionID, failureNotice(entry.title, error, out[index]?.group), { failed: error.type }),
42
- description: `Session ${sessionID} failed`,
43
- metadata: { source: "courier", from: sessionID, failed: true },
44
- delivery: "steer",
45
- })));
46
- }
47
- finally {
48
- if (completed.includes(true))
49
- ports.nudge();
50
- }
51
- return entries.map((entry) => entry.parentID);
52
- }
53
- /** A child's membership of an open group it is still out in: the group waits for it, and its end takes it out without a report. */
54
- async function outIn(ports, entry) {
55
- if (!entry.group)
56
- return undefined;
57
- try {
58
- const member = await membershipOf(ports.storage, entry.parentID, entry.group, entry.sessionID);
59
- return member?.out ? member : undefined;
60
- }
61
- catch (error) {
62
- ports.log(`courier watch: could not read ${entry.sessionID}'s place in group ${entry.group}: ${String(error)}`);
63
- return undefined;
64
- }
65
- }
66
- /** Takes a member out of its group without a report; true when that completes the group, for the caller to nudge the scheduler. */
67
- async function leave(ports, member, by, at) {
68
- try {
69
- return await markLeft(ports.storage, member, by, at);
70
- }
71
- catch (error) {
72
- ports.log(`courier watch: could not take ${member.sessionID} out of group ${member.group}: ${String(error)}`);
73
- return false;
74
- }
75
- }
76
- /** Inbox items that are not courier's or the parent's business: the person's own prompts, compactions and moves. */
77
- const UNOWED = new Set(["user", "compaction", "move"]);
78
- /** Remembers the type of an item enqueued for a spawned session, for `notePrompt`; other sessions cost one read. */
79
- export async function noteEnqueued(ports, inbox, event) {
80
- const { sessionID, inboxID, item } = event.data;
81
- if ((await indexedParent(ports.storage, sessionID)) === undefined)
82
- return false;
83
- setBounded(inbox, inboxID, item.type, SEEN_MAX);
84
- return true;
85
- }
86
- /**
87
- * Notes that a message reached a spawned session, which then owes its parent a report, unless `inbox` knows it for one of
88
- * `UNOWED` (`spawn` notes the task itself); reads the reverse index alone, and claims nothing: the note is the same twice.
89
- */
90
- export async function notePrompt(ports, event, inbox) {
91
- const { sessionID, inboxID } = event.data;
92
- // Kept, not deleted: the hub's other subscription looks it up for the same delivery.
93
- if (inboxID !== undefined && UNOWED.has(inbox?.get(inboxID) ?? ""))
94
- return false;
95
- if ((await indexedParent(ports.storage, sessionID)) === undefined)
96
- return false;
97
- await prompted(ports.storage, sessionID, event.created ?? ports.now());
98
- return true;
99
- }
100
- /**
101
- * Notes that a spawned session's turn was stopped, which takes it out of its group without a report; one
102
- * stopped by a shutdown resumes on the next start.
103
- */
104
- export async function noteInterrupted(ports, event) {
105
- if (event.data.reason === "shutdown")
106
- return false;
107
- const { sessionID } = event.data;
108
- const entry = await indexedEntry(ports.storage, sessionID);
109
- if (entry === undefined)
110
- return false;
111
- const at = event.created ?? ports.now();
112
- await settled(ports.storage, sessionID, "interrupted", at);
113
- const member = await outIn(ports, entry);
114
- if (member && (await leave(ports, member, "interrupted", at)))
115
- ports.nudge();
116
- return true;
117
- }
118
- /**
119
- * Drops the report state, and the groups, of a session OpenCode deleted, and the report state of the sessions it
120
- * started, whose reports can no longer be delivered; it leaves its own group. Returns the sessions forgotten.
121
- */
122
- export async function noteDeleted(ports, event) {
123
- const { sessionID } = event.data;
124
- const [parentID, started] = await Promise.all([indexedParent(ports.storage, sessionID), children(ports.storage, sessionID)]);
125
- const forgotten = [...(parentID === undefined ? [] : [sessionID]), ...started.map((child) => child.sessionID)];
126
- const entry = parentID === undefined ? undefined : await entryUnder(ports.storage, parentID, sessionID);
127
- const member = entry && (await outIn(ports, entry));
128
- // Its groups go whether or not it has children on the roster: a held report outlives the member's entry.
129
- const [completed] = await Promise.all([
130
- member ? leave(ports, member, "deleted", event.created ?? ports.now()) : Promise.resolve(false),
131
- ...forgotten.map((id) => forgetReport(ports.storage, id)),
132
- dropGroups(ports.storage, sessionID),
133
- ]);
134
- if (completed)
135
- ports.nudge();
136
- return forgotten;
137
- }
138
- /**
139
- * Whether a session prompted at `prompt` waits: on an untold report of a session it started, a complete group of
140
- * its about to be delivered, a request of its own, a scheduled message, or a webhook it subscribed to since. Cheapest first.
141
- */
142
- async function waits(ports, sessionID, prompt) {
143
- const started = await children(ports.storage, sessionID);
144
- if ((await Promise.all(started.map((entry) => awaited(ports.storage, entry.sessionID)))).includes(true))
145
- return true;
146
- if ((await listEverywhere([...ports.permissions()], sessionID)).some((found) => found.requests.length))
147
- return true;
148
- // A group with a member still out is waited on through that member, above; one whose every out member was told of is not.
149
- if (started.some((entry) => entry.group) && (await hasCompleteGroup(ports.storage, sessionID)))
150
- return true;
151
- if ((await pendingQuestions(ports.storage, sessionID)).length)
152
- return true;
153
- const [scheduled, subscribed] = await Promise.all([scheduledFor(ports.storage, sessionID), subscriptions(ports)]);
154
- // An older subscription is no wait: a prompt, most likely its delivery, has come since.
155
- return scheduled.length > 0 || subscribed.some((subscription) => subscription.sessionID === sessionID && subscription.createdAt >= prompt);
156
- }
157
- /**
158
- * Tells the parent of a spawned session whose turn ended without reporting to it since its last
159
- * prompt, unless it waits; finds the session by the reverse index alone. Returns the parents told.
160
- */
161
- export async function reportSilent(ports, seen, event) {
162
- const { sessionID } = event.data;
163
- const entry = await indexedEntry(ports.storage, sessionID);
164
- // Claimed after the lookup, so other sessions' turns do not crowd `seen`, and before anything is told.
165
- if (!entry || !claim(seen, event.id))
166
- return [];
167
- const report = await reportOf(ports.storage, sessionID);
168
- if (!report?.owes || (await waits(ports, sessionID, report.prompt)))
169
- return [];
170
- if (await stoppedSincePrompt(ports.storage, sessionID).catch(() => false))
171
- return [];
172
- // The notice goes out without the reply rather than not at all.
173
- const lastText = await ports.session.context({ sessionID }).then(lastReply, () => undefined);
174
- const group = (await outIn(ports, entry))?.group;
175
- // Before the notice, which may end the parent's turn at once: once told, it no longer waits for this one.
176
- const toldAt = event.created ?? ports.now();
177
- await told(ports.storage, sessionID, toldAt).catch((error) => ports.log(`courier watch: could not note that the parent of ${sessionID} was told: ${String(error)}`));
178
- try {
179
- await ports.session.synthetic({
180
- sessionID: entry.parentID,
181
- text: envelope(sessionID, silentNotice(entry.title, lastText, report.progressed, group), { ended: "without-report" }),
182
- description: `Session ${sessionID} ended without a report`,
183
- metadata: { source: "courier", from: sessionID, ended: "without-report" },
184
- delivery: "steer",
185
- });
186
- }
187
- catch (error) {
188
- // Not told after all: the parent still waits for it.
189
- await untold(ports.storage, sessionID, toldAt).catch(() => undefined);
190
- throw error;
191
- }
192
- return [entry.parentID];
193
- }
194
- /** The session at the top of a lineage: where its permission requests go, since that is where the person is. */
195
- const topOf = (chain) => chain.at(-1).parentID;
196
- /**
197
- * Tells the session at the top, the parent of a child or the session a chain of children started
198
- * from, what a waiting child asks and which answers there are, waking it. Returns the sessions told.
199
- */
200
- export async function reportAsked(ports, state, event) {
201
- if (!claim(state.seen, event.id))
202
- return [];
203
- const request = event.data;
204
- const chain = await lineage(ports.storage, request.sessionID);
205
- // Claimed only now, after the lookup: a request answered meanwhile is in `answered`, and one
206
- // already relayed, from the event or from the pending list, is in `waiting`.
207
- if (!chain.length || state.answered.has(request.id) || !claim(state.waiting, request.id))
208
- return [];
209
- const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
210
- await ports.session.synthetic({
211
- sessionID: topOf(chain),
212
- text: envelope(request.sessionID, permissionNotice(chain[0].title, request, startedBy), {
213
- asks: "permission",
214
- request: request.id,
215
- }),
216
- description: `Session ${request.sessionID} asks for permission`,
217
- metadata: { source: "courier", from: request.sessionID, asks: "permission", requestID: request.id },
218
- delivery: "steer",
219
- });
220
- return [topOf(chain)];
221
- }
222
- /**
223
- * Tells the session told about a request that it was answered some other way, so it does not pass on
224
- * a stale question; requests answered through courier_answer no longer wait and are skipped.
225
- */
226
- export async function reportReplied(ports, state, event) {
227
- if (!claim(state.seen, event.id))
228
- return [];
229
- const { sessionID, requestID, reply } = event.data;
230
- if (!state.waiting.delete(requestID)) {
231
- claim(state.answered, requestID);
232
- return [];
233
- }
234
- const chain = await lineage(ports.storage, sessionID);
235
- if (!chain.length)
236
- return [];
237
- await ports.session.synthetic({
238
- sessionID: topOf(chain),
239
- text: envelope(sessionID, permissionSettledNotice(chain[0].title, requestID, reply), { answered: reply, request: requestID }),
240
- description: `Session ${sessionID} no longer asks for permission`,
241
- metadata: { source: "courier", from: sessionID, answered: reply, requestID },
242
- delivery: "steer",
243
- });
244
- return [topOf(chain)];
245
- }
246
- /**
247
- * Tells the top session about a form OpenCode shows in a spawned session, such as web search asking
248
- * for its provider, which only the person can answer; question forms are left to the question relay.
249
- */
250
- export async function reportForm(ports, state, event) {
251
- if (!claim(state.seen, event.id))
252
- return [];
253
- const form = event.data.form;
254
- if (kindOf(form) === QUESTION_FORM)
255
- return [];
256
- const chain = await lineage(ports.storage, form.sessionID);
257
- // As with a permission request, claimed after the lookup, so a form settled meanwhile is not told.
258
- if (!chain.length || state.forms.settled.has(form.id) || state.forms.told.has(form.id))
259
- return [];
260
- const startedBy = chain.length > 1 ? chain[0].parentID : undefined;
261
- const kind = kindOf(form);
262
- const telling = ports.session.synthetic({
263
- sessionID: topOf(chain),
264
- text: envelope(form.sessionID, formNotice(chain[0].title, form, startedBy), {
265
- asks: "form",
266
- form: form.id,
267
- ...(kind ? { kind } : {}),
268
- }),
269
- description: `Session ${form.sessionID} shows a form`,
270
- metadata: { source: "courier", from: form.sessionID, asks: "form", formID: form.id, ...(kind ? { kind } : {}) },
271
- delivery: "steer",
272
- });
273
- setBounded(state.forms.told, form.id, telling, SEEN_MAX);
274
- await telling;
275
- return [topOf(chain)];
276
- }
277
- /** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
278
- export async function reportFormSettled(ports, state, event) {
279
- if (!claim(state.seen, event.id))
280
- return [];
281
- const { id, sessionID } = event.data;
282
- const telling = state.forms.told.get(id);
283
- if (!telling) {
284
- claim(state.forms.settled, id);
285
- return [];
286
- }
287
- state.forms.told.delete(id);
288
- const settled = event.type === "form.replied" ? "answered" : "cancelled";
289
- // After the notice that the form is shown, which another instance may still be sending, so the
290
- // settling never arrives first; and not at all when that notice did not go out.
291
- const [chain, told] = await Promise.all([lineage(ports.storage, sessionID), telling.then(() => true, () => false)]);
292
- if (!chain.length || !told)
293
- return [];
294
- await ports.session.synthetic({
295
- sessionID: topOf(chain),
296
- text: envelope(sessionID, formSettledNotice(chain[0].title, id, settled), { settled, form: id }),
297
- description: `Session ${sessionID} no longer shows a form`,
298
- metadata: { source: "courier", from: sessionID, settled, formID: id },
299
- delivery: "steer",
300
- });
301
- return [topOf(chain)];
302
- }
303
- /**
304
- * Relays the requests that spawned sessions already wait on, in every loaded location, which the
305
- * event stream does not repeat: those asked while it was down, before the watcher (re)subscribed.
306
- */
307
- export async function relayPending(ports, state) {
308
- const sessions = new Set((await allEntries(ports.storage)).map((entry) => entry.sessionID));
309
- const domains = [...ports.permissions()];
310
- // Each request is claimed before its notice goes out, so relaying them all at once tells each
311
- // once, however many locations list it; a location that cannot be read lists none.
312
- const relay = async (sessionID) => {
313
- const listed = (await listEverywhere(domains, sessionID)).flatMap((found) => found.requests);
314
- await Promise.all(listed.map((request) => reportAsked(ports, state, { id: `pending:${request.id}`, data: request })));
315
- };
316
- await Promise.all([...sessions].map(relay));
317
- }
318
- const pause = (ms, signal) => new Promise((resolve) => {
319
- const timer = setTimeout(done, ms);
320
- function done() {
321
- clearTimeout(timer);
322
- signal.removeEventListener("abort", done);
323
- resolve();
324
- }
325
- signal.addEventListener("abort", done);
326
- });
327
- const TURN_ENDS = new Set(["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"]);
328
- /** A session's turn ended, sealing its groups: one that is complete has the scheduler deliver it now. */
329
- async function sealGroups(ports, event) {
330
- if (event.data.reason === "shutdown")
331
- return;
332
- try {
333
- if (await hasCompleteGroup(ports.storage, event.data.sessionID))
334
- ports.nudge();
335
- }
336
- catch (error) {
337
- ports.log(`courier watch: could not read the groups of ${event.data.sessionID}: ${String(error)}`);
338
- }
339
- }
340
- /**
341
- * Handles one event of OpenCode's stream; those that cannot concern a spawned child, or a question
342
- * relayed for one, are ignored.
343
- */
344
- async function handle(ports, state, event) {
345
- try {
346
- return await route(ports, state, event);
347
- }
348
- finally {
349
- // After: a silent end is judged while the group the nudge may deliver is still there to wait on.
350
- if (TURN_ENDS.has(event.type))
351
- await sealGroups(ports, event);
352
- }
353
- }
354
- async function route(ports, state, event) {
355
- if (event.type === "session.execution.failed")
356
- return reportFailure(ports, state.seen, event);
357
- if (event.type === "session.execution.succeeded")
358
- return reportSilent(ports, state.seen, event);
359
- if (event.type === "session.execution.interrupted")
360
- return noteInterrupted(ports, event);
361
- if (event.type === "session.inbox.enqueued")
362
- return noteEnqueued(ports, state.inbox, event);
363
- if (event.type === "session.inbox.delivered")
364
- return notePrompt(ports, event, state.inbox);
365
- if (event.type === "session.deleted")
366
- return noteDeleted(ports, event);
367
- if (event.type === "permission.asked")
368
- return reportAsked(ports, state, event);
369
- if (event.type === "permission.replied")
370
- return reportReplied(ports, state, event);
371
- if (event.type === "form.created") {
372
- // Not claimed: the hub's other subscription, or another copy's watcher, may resolve the same
373
- // waiting call, which is harmless.
374
- formShown(event);
375
- return reportForm(ports, state, event);
376
- }
377
- if (event.type === "form.replied" || event.type === "form.cancelled")
378
- return reportFormSettled(ports, state, event);
379
- // Not claimed either: a location closing withdraws its forms, which must not pass for dismissals.
380
- // The event's location is optional; without one, the shutdown counts for every location.
381
- if (event.type === "location.shutdown") {
382
- const directory = event.location?.directory;
383
- locationClosing(ports.now(), typeof directory === "string" ? directory : undefined);
384
- }
385
- return [];
386
- }
387
- const ENDED = { done: true, value: undefined };
388
- /**
389
- * `events` until it ends or `signal` aborts, the stream left open; each wait hears the abort only while
390
- * it waits, so a watcher that follows events for the life of the process holds nothing per event.
391
- */
392
- const until = (events, signal) => ({
393
- [Symbol.asyncIterator]: () => ({
394
- next: () => new Promise((resolve, reject) => {
395
- if (signal.aborted)
396
- return resolve(ENDED);
397
- const stop = () => resolve(ENDED);
398
- signal.addEventListener("abort", stop, { once: true });
399
- // Once the signal has aborted, whatever the stream ends this wait with is not wanted.
400
- events.next().then((next) => {
401
- signal.removeEventListener("abort", stop);
402
- resolve(next);
403
- }, (error) => {
404
- signal.removeEventListener("abort", stop);
405
- reject(error);
406
- });
407
- }),
408
- }),
409
- });
410
- /** Relays the requests already pending, kept in `relaying` until done, for the watcher's end to wait for. */
411
- function relayAlongside(ports, state, relaying) {
412
- const relayed = relayPending(ports, state)
413
- .catch((error) => ports.log(`courier watch: could not relay pending requests: ${String(error)}`))
414
- .finally(() => relaying.delete(relayed));
415
- relaying.add(relayed);
416
- }
417
- /** Handles one event, logging a failure rather than ending the watch. */
418
- function handleLogged(ports, state, event) {
419
- return handle(ports, state, event).catch((error) => {
420
- const data = event.data;
421
- const sessionID = data?.sessionID ?? data?.form?.sessionID;
422
- ports.log(`courier watch: could not handle ${event.type} of ${sessionID}: ${String(error)}`);
423
- });
424
- }
425
- /**
426
- * Follows OpenCode's events until `signal` aborts: tells parents of a spawned child's failed or silent
427
- * turn, its pending permission or form, notes forms and shutdowns for the relay; ends after its current event.
428
- */
429
- export async function watchChildren(ports, state, signal, retryMs = RESUBSCRIBE_MS) {
430
- const relaying = new Set();
431
- while (!signal.aborted) {
432
- let following = false;
433
- let events;
434
- try {
435
- events = ports.event.subscribe({ signal })[Symbol.asyncIterator]();
436
- // Question forms shown while no instance followed the events were not seen: released now,
437
- // and again on the first event, by when the stream is surely connected.
438
- let missed = eventsFollowed();
439
- following = true;
440
- // Alongside the new subscription; a request both relays see is relayed once.
441
- relayAlongside(ports, state, relaying);
442
- for await (const event of until(events, signal)) {
443
- if (missed)
444
- formsMayHaveBeenMissed();
445
- missed = false;
446
- await handleLogged(ports, state, event);
447
- }
448
- }
449
- catch (error) {
450
- if (!signal.aborted)
451
- ports.log(`courier watch: event stream broke: ${String(error)}`);
452
- }
453
- finally {
454
- if (following)
455
- eventsLeft();
456
- // Closed without waiting: closing the adapter's stream waits on OpenCode, which may not answer
457
- // while a location closes, and the leave waiting for this watcher must not wait for that too.
458
- void events?.return?.().catch(() => { });
459
- }
460
- if (!signal.aborted)
461
- await pause(retryMs, signal);
462
- }
463
- await Promise.allSettled(relaying);
464
- }
465
- /**
466
- * Makes the hub start its subscriptions to OpenCode's events with this copy's watcher, the one loaded
467
- * last; `join` starts them. The second is a standby, connected before the first's member leaves.
468
- */
469
- export function watchFromHub(hub, state, retryMs = RESUBSCRIBE_MS) {
470
- hub.subscribe = (member) => {
471
- const stop = new AbortController();
472
- return { member, stop, done: watchChildren(member.watch, state, stop.signal, retryMs) };
473
- };
474
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes