opencode-courier 0.2.3 → 0.3.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 (50) hide show
  1. package/README.md +124 -20
  2. package/dist/bounded.d.ts +2 -4
  3. package/dist/bounded.js +2 -4
  4. package/dist/cleanup.d.ts +8 -3
  5. package/dist/cleanup.js +24 -12
  6. package/dist/courier.d.ts +116 -11
  7. package/dist/courier.js +239 -55
  8. package/dist/group.d.ts +86 -0
  9. package/dist/group.js +247 -0
  10. package/dist/hub.d.ts +52 -77
  11. package/dist/hub.js +68 -65
  12. package/dist/index.d.ts +4 -8
  13. package/dist/index.js +27 -20
  14. package/dist/json.d.ts +4 -0
  15. package/dist/json.js +11 -0
  16. package/dist/later.d.ts +6 -6
  17. package/dist/later.js +6 -5
  18. package/dist/limits.d.ts +65 -0
  19. package/dist/limits.js +109 -0
  20. package/dist/notices.d.ts +125 -15
  21. package/dist/notices.js +304 -25
  22. package/dist/question/answer.js +6 -9
  23. package/dist/question/lifecycle.d.ts +10 -19
  24. package/dist/question/lifecycle.js +10 -19
  25. package/dist/question/pure.d.ts +3 -4
  26. package/dist/question/pure.js +2 -3
  27. package/dist/question/relay.js +13 -27
  28. package/dist/question/runtime.d.ts +2 -3
  29. package/dist/question/runtime.js +2 -3
  30. package/dist/relay.d.ts +2 -4
  31. package/dist/relay.js +2 -4
  32. package/dist/report.d.ts +48 -0
  33. package/dist/report.js +88 -0
  34. package/dist/roster.d.ts +29 -17
  35. package/dist/roster.js +57 -26
  36. package/dist/skill.d.ts +13 -0
  37. package/dist/skill.js +9 -0
  38. package/dist/stop.d.ts +41 -0
  39. package/dist/stop.js +120 -0
  40. package/dist/tools.d.ts +20 -0
  41. package/dist/tools.js +81 -12
  42. package/dist/tree.d.ts +102 -0
  43. package/dist/tree.js +58 -0
  44. package/dist/version.d.ts +4 -10
  45. package/dist/version.js +4 -10
  46. package/dist/watch.d.ts +55 -18
  47. package/dist/watch.js +219 -34
  48. package/dist/webhook.d.ts +10 -15
  49. package/dist/webhook.js +11 -20
  50. package/package.json +1 -1
@@ -6,25 +6,18 @@ import type { QuestionPorts } from "../hub.js";
6
6
  */
7
7
  export declare const shutdownReportedAt: (directory?: string) => number | undefined;
8
8
  /**
9
- * Called for OpenCode's `location.shutdown`: the location at `directory` is closing, which
10
- * withdraws its open forms as if the person had dismissed them (before unloading the plugin
11
- * there, since OpenCode 2.0.22). The event's location is optional in the schema; without it,
12
- * the shutdown counts for every location. `at` is when, by the watcher's clock (epoch milliseconds).
9
+ * Called for `location.shutdown`: the location at `directory` is closing, which withdraws its open
10
+ * forms as if dismissed; with no location it counts for all. `at` is the watcher's clock, epoch ms.
13
11
  */
14
12
  export declare function locationClosing(at: number, directory?: string): void;
15
13
  /**
16
- * Whether a dismissal just seen in the location at `directory` was that location closing rather
17
- * than the person: true when it shut down within the dismissal grace before, or does so, or this
18
- * instance unloads, within the grace from now, by the clock of `ports`. A shutdown reported without a location counts for every
19
- * location, and with the directory unknown, any location's shutdown counts.
14
+ * Whether a dismissal just seen at `directory` was the location closing rather than the person: it
15
+ * shut down within the grace, before or after, or this instance unloads in it; unknown matches any.
20
16
  */
21
17
  export declare const closingSoon: (ports: QuestionPorts, loaded: () => boolean, directory: string | undefined) => Effect.Effect<boolean>;
22
18
  /**
23
- * Adds a plugin instance's ports to `loaded`, which copies of other versions read to tell a cut-off
24
- * question through it, until the returned function is called. A location loading again forgets the
25
- * shutdown recorded for it, so a dismissal there is not mistaken for that shutdown. One recorded for
26
- * every location is left alone, since a dismissal held in another location may still need it; it
27
- * ages out with the grace.
19
+ * Adds an instance's ports to `loaded`, which other copies tell cut-off questions through, until the
20
+ * returned function is called. A location loading again forgets the shutdown recorded for it alone.
28
21
  */
29
22
  export declare function joinRelay(ports: QuestionPorts): () => void;
30
23
  /** Called for OpenCode's `form.created`: a question call's form is shown, so its question can be relayed. */
@@ -37,15 +30,13 @@ export declare function formShown(event: {
37
30
  };
38
31
  }): void;
39
32
  /**
40
- * Called when an event stream may have missed forms being shown: every call still waiting for its
41
- * form is taken as shown. One still waiting for its permission check is relayed early, which is the
42
- * lesser harm.
33
+ * Called when an event stream may have missed forms shown: every call still waiting for its form is
34
+ * taken as shown; one still waiting for its permission check is relayed early, the lesser harm.
43
35
  */
44
36
  export declare function formsMayHaveBeenMissed(): void;
45
37
  /**
46
- * Called when an instance starts following OpenCode's events. Every instance is sent every event,
47
- * so a form shown is missed only while none follows them; then the calls waiting are released, and
48
- * true is returned, for the caller to release them again on its first event, once it is connected.
38
+ * Called when an instance starts following OpenCode's events: a shown form is missed only while none
39
+ * follows, so waiting calls are released; true tells the caller to release again on its first event.
49
40
  */
50
41
  export declare function eventsFollowed(): boolean;
51
42
  /** Called when an instance's event stream ended or broke. */
@@ -12,10 +12,8 @@ const shutdownAt = (directory) => directory === undefined
12
12
  */
13
13
  export const shutdownReportedAt = (directory) => shutdownAt(directory) || undefined;
14
14
  /**
15
- * Called for OpenCode's `location.shutdown`: the location at `directory` is closing, which
16
- * withdraws its open forms as if the person had dismissed them (before unloading the plugin
17
- * there, since OpenCode 2.0.22). The event's location is optional in the schema; without it,
18
- * the shutdown counts for every location. `at` is when, by the watcher's clock (epoch milliseconds).
15
+ * Called for `location.shutdown`: the location at `directory` is closing, which withdraws its open
16
+ * forms as if dismissed; with no location it counts for all. `at` is the watcher's clock, epoch ms.
19
17
  */
20
18
  export function locationClosing(at, directory) {
21
19
  shared.shutdowns.set(directory ?? ANYWHERE, at);
@@ -23,10 +21,8 @@ export function locationClosing(at, directory) {
23
21
  wake();
24
22
  }
25
23
  /**
26
- * Whether a dismissal just seen in the location at `directory` was that location closing rather
27
- * than the person: true when it shut down within the dismissal grace before, or does so, or this
28
- * instance unloads, within the grace from now, by the clock of `ports`. A shutdown reported without a location counts for every
29
- * location, and with the directory unknown, any location's shutdown counts.
24
+ * Whether a dismissal just seen at `directory` was the location closing rather than the person: it
25
+ * shut down within the grace, before or after, or this instance unloads in it; unknown matches any.
30
26
  */
31
27
  export const closingSoon = (ports, loaded, directory) => Effect.suspend(() => {
32
28
  const ms = ports.timing.dismissalGraceMs;
@@ -47,11 +43,8 @@ export const closingSoon = (ports, loaded, directory) => Effect.suspend(() => {
47
43
  }), () => (Deferred.isDoneUnsafe(closed) ? Effect.succeed(true) : Effect.raceFirst(Deferred.await(closed), Effect.as(Effect.sleep(ms), false))), () => Effect.sync(() => shared.closingWaiters.delete(wake)));
48
44
  });
49
45
  /**
50
- * Adds a plugin instance's ports to `loaded`, which copies of other versions read to tell a cut-off
51
- * question through it, until the returned function is called. A location loading again forgets the
52
- * shutdown recorded for it, so a dismissal there is not mistaken for that shutdown. One recorded for
53
- * every location is left alone, since a dismissal held in another location may still need it; it
54
- * ages out with the grace.
46
+ * Adds an instance's ports to `loaded`, which other copies tell cut-off questions through, until the
47
+ * returned function is called. A location loading again forgets the shutdown recorded for it alone.
55
48
  */
56
49
  export function joinRelay(ports) {
57
50
  shared.loaded.add(ports);
@@ -72,9 +65,8 @@ export function formShown(event) {
72
65
  shared.shown.delete(key);
73
66
  }
74
67
  /**
75
- * Called when an event stream may have missed forms being shown: every call still waiting for its
76
- * form is taken as shown. One still waiting for its permission check is relayed early, which is the
77
- * lesser harm.
68
+ * Called when an event stream may have missed forms shown: every call still waiting for its form is
69
+ * taken as shown; one still waiting for its permission check is relayed early, the lesser harm.
78
70
  */
79
71
  export function formsMayHaveBeenMissed() {
80
72
  for (const shown of shared.shown.values())
@@ -82,9 +74,8 @@ export function formsMayHaveBeenMissed() {
82
74
  shared.shown.clear();
83
75
  }
84
76
  /**
85
- * Called when an instance starts following OpenCode's events. Every instance is sent every event,
86
- * so a form shown is missed only while none follows them; then the calls waiting are released, and
87
- * true is returned, for the caller to release them again on its first event, once it is connected.
77
+ * Called when an instance starts following OpenCode's events: a shown form is missed only while none
78
+ * follows, so waiting calls are released; true tells the caller to release again on its first event.
88
79
  */
89
80
  export function eventsFollowed() {
90
81
  const missed = shared.following === 0 && shared.followed;
@@ -28,11 +28,10 @@ export type CallExit = Exit.Exit<{
28
28
  /** How a relayed call that the top session did not answer was settled in the asking session. */
29
29
  export declare function settledIn(exit: CallExit): Elsewhere;
30
30
  /**
31
- * Orders strings by their UTF-16 code units, the default sort's order, stated. The order is never
32
- * shown; it only has to come out the same on both sides of a comparison, which a locale-aware
33
- * compare would not promise: it can rank two different labels as equal and leave them as listed.
31
+ * Orders strings by UTF-16 code units, the default sort's order, stated: it only has to come out the
32
+ * same on both sides of a comparison; a locale-aware compare may rank two different labels as equal.
34
33
  */
35
- export declare function byCodeUnit(a: string, b: string): 1 | 0 | -1;
34
+ export declare function byCodeUnit(a: string, b: string): 0 | 1 | -1;
36
35
  export declare const choices: (questions: ReadonlyArray<Prompt>) => string;
37
36
  export declare const wording: (questions: ReadonlyArray<Prompt>) => string;
38
37
  /** The result of a linked call withdrawn because its question was settled without it. */
@@ -40,9 +40,8 @@ export function settledIn(exit) {
40
40
  return { by: "failed", error: String(Cause.squash(exit.cause)) };
41
41
  }
42
42
  /**
43
- * Orders strings by their UTF-16 code units, the default sort's order, stated. The order is never
44
- * shown; it only has to come out the same on both sides of a comparison, which a locale-aware
45
- * compare would not promise: it can rank two different labels as equal and leave them as listed.
43
+ * Orders strings by UTF-16 code units, the default sort's order, stated: it only has to come out the
44
+ * same on both sides of a comparison; a locale-aware compare may rank two different labels as equal.
46
45
  */
47
46
  export function byCodeUnit(a, b) {
48
47
  if (a === b)
@@ -8,8 +8,7 @@ import { background, run, within } from "./runtime.js";
8
8
  import { anyLoaded, isPassing, keyOf, QUESTION_TOOL, shared, storedOf } from "./shared.js";
9
9
  /**
10
10
  * Tells the top session about a question its closing location cut off, after the closing grace of
11
- * `ports` (the instance whose call was cut off), through an instance still loaded then, if any (any will do: a notice reaches a session in any location).
12
- * Without one, the next load tells it.
11
+ * `ports`, through any instance still loaded then; without one, the next load tells it.
13
12
  */
14
13
  function tellLater(ports, question) {
15
14
  const tellNow = Effect.suspend(() => {
@@ -44,9 +43,8 @@ const settle = (ports, question, exit, loaded) => Effect.gen(function* () {
44
43
  // A dismissal is held from now, alongside the wait below, so the grace is not spent waiting.
45
44
  const dismissed = Exit.isFailure(exit) && isDismissal(exit.cause);
46
45
  const held = dismissed && !unloaded ? yield* Effect.forkChild(closingSoon(ports, loaded, question.directory), { startImmediately: true }) : undefined;
47
- // Telling the top session first, so what follows does not overtake the notice or come before it
48
- // is stored; for a while only, so a notice that never returns does not hold everything else up.
49
- // `told` is false when the top session was not told, or not yet.
46
+ // The top session is told first, for a while only, so what follows neither overtakes the notice
47
+ // nor waits forever on one that never returns; `told` is false when it was not told, or not yet.
50
48
  const told = (yield* within(question.relaying, ports.timing.relayWaitMs)) === true;
51
49
  // Answered by the top session.
52
50
  if (Exit.isSuccess(exit) && exit.value.by === "top") {
@@ -58,15 +56,8 @@ const settle = (ports, question, exit, loaded) => Effect.gen(function* () {
58
56
  if (shared.questions.get(question.requestID) !== question) {
59
57
  return yield* Effect.promise(() => forget(ports, question));
60
58
  }
61
- // Cut off: the turn was stopped, or OpenCode is closing the location, which withdraws every open
62
- // form there as if the person had dismissed it (on a server shutdown, say), before unloading the
63
- // plugin there (since 2.0.22) or after (the beta). So a dismissal is held for a moment, to see
64
- // whether the location's shutdown or this instance's unload follows it. The question stays,
65
- // answered by message from now on; a top session already asking the person passes their answer
66
- // on that way. A closing location is not told now: the next load tells it, in this process or
67
- // after a restart, or an instance still loaded does, a little later, when the process is still
68
- // running then. The unload is looked at again here: it may have come during the wait above, after
69
- // a held dismissal had already been taken for the person's.
59
+ // Cut off: the turn was stopped, or the closing location withdrew its forms as if dismissed, so a
60
+ // dismissal is held for a following shutdown or unload; the question stays, answered by message.
70
61
  const closing = unloaded || !loaded() || (held !== undefined && (yield* Fiber.join(held)));
71
62
  if (Exit.isFailure(exit) && (closing || (!dismissed && Exit.hasInterrupts(exit))))
72
63
  return closing ? tellLater(ports, question) : yield* Effect.promise(() => stopped(ports, question));
@@ -103,9 +94,8 @@ function startSettling(ports, question, exit, loaded) {
103
94
  });
104
95
  }
105
96
  /**
106
- * The question call of a session started with courier_spawn: once its form is shown, the session at
107
- * the top is told, and whichever answer comes first ends the call, the other side's question being
108
- * withdrawn. Other sessions' calls run unchanged.
97
+ * A spawned session's question call: once its form is shown the top session is told, and whichever
98
+ * answer comes first ends the call, the other side's question withdrawn. Other calls run unchanged.
109
99
  */
110
100
  function asking(ports, directory, original) {
111
101
  return (input, context) => Effect.gen(function* () {
@@ -180,9 +170,8 @@ function asking(ports, directory, original) {
180
170
  });
181
171
  }
182
172
  /**
183
- * The waiting question a top session's call asks again, the oldest if several: the same questions
184
- * with the same choices, in any order. Matching choices alone are not enough, or an unrelated
185
- * yes-or-no question of the top session would answer a child's.
173
+ * The waiting question a top session's call asks again, the oldest if several: the same question with
174
+ * the same choices, in any order. Choices alone would let an unrelated yes-or-no answer a child's.
186
175
  */
187
176
  function linkFor(sessionID, questions) {
188
177
  const asked = { choices: choices(questions), wording: wording(questions) };
@@ -195,9 +184,8 @@ function linkFor(sessionID, questions) {
195
184
  .sort((a, b) => a.askedAt - b.askedAt)[0];
196
185
  }
197
186
  /**
198
- * The result of a top session's question call that no waiting question was linked to: when one
199
- * waits with the same choices, reworded or asked before that session asked, it says how to pass
200
- * the answers on.
187
+ * The result of a top session's question call that no waiting question was linked to: when one waits
188
+ * with the same choices, reworded or asked earlier, it says how to pass the answers on.
201
189
  */
202
190
  function unlinkedResult(result, sessionID, questions) {
203
191
  if (typeof result.content !== "string")
@@ -209,10 +197,8 @@ function unlinkedResult(result, sessionID, questions) {
209
197
  return { ...result, content: `${result.content}\n${unlinkedNote(similar)}` };
210
198
  }
211
199
  /**
212
- * After a linked call failed, its location still loaded: a dismissal by the person, unless the
213
- * location is closing (see settle), dismisses the question they were asked for too, or is logged
214
- * when the question was answered meanwhile; a call stopped while the asking call was cut off as
215
- * well tells the top session, which nobody has told yet.
200
+ * After a linked call failed with its location still loaded: a dismissal by the person dismisses the
201
+ * child's question too, or is logged if answered meanwhile; a stop while cut off tells the top.
216
202
  */
217
203
  const linkedCallFailed = (ports, linked, cause, loaded, directory) => Effect.gen(function* () {
218
204
  if (isDismissal(cause)) {
@@ -1,8 +1,7 @@
1
1
  import { Clock, Effect } from "effect";
2
2
  /**
3
- * Runs an effect of the relay in a fiber of its own, by the relay's clock, which is the clock of
4
- * its ports (`ports.now`): the promise settles as the effect ends, and is rejected if it fails or
5
- * is interrupted. Interrupting what waits on the promise does not stop the fiber.
3
+ * Runs a relay effect in its own fiber by the relay's clock (`ports.now`): the promise settles as the
4
+ * effect ends, rejecting on failure or interruption; interrupting the waiter does not stop the fiber.
6
5
  */
7
6
  export declare function run<A, E>(effect: Effect.Effect<A, E>): Promise<A>;
8
7
  /**
@@ -38,9 +38,8 @@ let scope = Scope.makeUnsafe();
38
38
  */
39
39
  const start = (effect) => Effect.runFork(Effect.provideService(effect, Clock.Clock, clock)).pipe(Fiber.runIn(scope));
40
40
  /**
41
- * Runs an effect of the relay in a fiber of its own, by the relay's clock, which is the clock of
42
- * its ports (`ports.now`): the promise settles as the effect ends, and is rejected if it fails or
43
- * is interrupted. Interrupting what waits on the promise does not stop the fiber.
41
+ * Runs a relay effect in its own fiber by the relay's clock (`ports.now`): the promise settles as the
42
+ * effect ends, rejecting on failure or interruption; interrupting the waiter does not stop the fiber.
44
43
  */
45
44
  export function run(effect) {
46
45
  return Effect.runPromise(Fiber.join(start(effect)));
package/dist/relay.d.ts CHANGED
@@ -45,10 +45,8 @@ export declare function listEverywhere<D extends Pick<Permissions, "list">>(perm
45
45
  requests: import("@opencode/client").PermissionRequest[];
46
46
  })[]>;
47
47
  /**
48
- * Answers a permission request of a session started, directly or through others, from the
49
- * caller, which must be the session at the top: requests go there, to the person, so a session
50
- * started with courier_spawn cannot approve what its own children ask. `answered` is false when
51
- * no location loaded in this server holds the request, which may then wait in another server.
48
+ * Answers a permission request of a session started, directly or through others, from the caller,
49
+ * which must be the top session; `answered` is false when no location loaded here holds the request.
52
50
  */
53
51
  export declare function answer(ports: AnswerPorts, waiting: Waiting, callerID: string, input: AnswerInput): Promise<PermissionAnswered>;
54
52
  /** The requests a session waits on, in whichever location holds them, for courier_status. */
package/dist/relay.js CHANGED
@@ -19,10 +19,8 @@ async function locate(ports, sessionID, requestID) {
19
19
  return undefined;
20
20
  }
21
21
  /**
22
- * Answers a permission request of a session started, directly or through others, from the
23
- * caller, which must be the session at the top: requests go there, to the person, so a session
24
- * started with courier_spawn cannot approve what its own children ask. `answered` is false when
25
- * no location loaded in this server holds the request, which may then wait in another server.
22
+ * Answers a permission request of a session started, directly or through others, from the caller,
23
+ * which must be the top session; `answered` is false when no location loaded here holds the request.
26
24
  */
27
25
  export async function answer(ports, waiting, callerID, input) {
28
26
  const reply = input.reply;
@@ -0,0 +1,48 @@
1
+ import { type Status } from "./notices.js";
2
+ import type { Storage } from "./storage.js";
3
+ export declare const promptKey: (sessionID: string) => string;
4
+ export declare const settledKey: (sessionID: string) => string;
5
+ export declare const progressKey: (sessionID: string) => string;
6
+ export declare const toldKey: (sessionID: string) => string;
7
+ export declare const stoppedKey: (sessionID: string) => string;
8
+ /** How a session's wait for a report ended: it reported to its parent, or its turn failed or was interrupted. */
9
+ export type Settled = "report" | "failed" | "interrupted";
10
+ /**
11
+ * Notes that a prompt or message reached a spawned session at `at`, in epoch milliseconds, unless a
12
+ * later one is noted: both of the hub's subscriptions note each event, one of them maybe behind.
13
+ */
14
+ export declare function prompted(storage: Pick<Storage, "get" | "set">, sessionID: string, at: number): Promise<void>;
15
+ /**
16
+ * Notes that a spawned session reported to its parent, with `status` when its report carried one, or
17
+ * its turn failed or was interrupted, at `at`, unless a later end is noted.
18
+ */
19
+ export declare function settled(storage: Pick<Storage, "get" | "set">, sessionID: string, by: Settled, at: number, status?: Status): Promise<void>;
20
+ /** Notes that a spawned session messaged its parent without a status at `at`, which is progress, not its report. */
21
+ export declare function progressed(storage: Pick<Storage, "get" | "set">, sessionID: string, at: number): Promise<void>;
22
+ /** Notes that a spawned session's parent was told, at `at`, that a turn of it ended without a report, unless a later telling is noted. */
23
+ export declare function told(storage: Pick<Storage, "get" | "set">, sessionID: string, at: number): Promise<void>;
24
+ /** Takes back the note `told` made at `at`, whose notice did not go out; a later one stays, and one that cannot be read goes. */
25
+ export declare function untold(storage: Pick<Storage, "get" | "remove">, sessionID: string, at: number): Promise<void>;
26
+ /** Notes that courier_stop stopped a spawned session at `at`, so the failure and silent-end notices stay quiet for it. */
27
+ export declare function stopped(storage: Pick<Storage, "set">, sessionID: string, at: number): Promise<void>;
28
+ /** Whether courier_stop stopped a session after its last prompt, or with none noted, at all. */
29
+ export declare function stoppedSincePrompt(storage: Pick<Storage, "get">, sessionID: string): Promise<boolean>;
30
+ /** The status of a spawned session's last report to its parent, if it has reported with one. */
31
+ export declare function lastStatus(storage: Pick<Storage, "get">, sessionID: string): Promise<Status | undefined>;
32
+ /**
33
+ * What is known of a spawned session's report: when a prompt last reached it, and whether since then it owes one, has
34
+ * messaged its parent without a status, or failed or was interrupted. Undefined for one spawned before this was kept.
35
+ */
36
+ export declare function reportOf(storage: Pick<Storage, "get">, sessionID: string): Promise<{
37
+ prompt: number;
38
+ owes: boolean;
39
+ progressed: boolean;
40
+ ended: boolean;
41
+ } | undefined>;
42
+ /**
43
+ * Whether its parent still waits on a spawned session's report: it owes one, and the parent has not
44
+ * been told since its last prompt that it ended a turn without one.
45
+ */
46
+ export declare function awaited(storage: Pick<Storage, "get">, sessionID: string): Promise<boolean>;
47
+ /** Drops what is kept about a session, with its roster entry. */
48
+ export declare function forgetReport(storage: Pick<Storage, "remove">, sessionID: string): Promise<void>;
package/dist/report.js ADDED
@@ -0,0 +1,88 @@
1
+ import { obj } from "./json.js";
2
+ import { isStatus } from "./notices.js";
3
+ /**
4
+ * Whether a spawned session has reported since its last prompt, under `report/<sessionID>/`: `prompt`, `settled` (with
5
+ * the report's status), `progress` (a message to its parent without a status) and `told`; docs/reference.md has each.
6
+ */
7
+ const PREFIX = "report/";
8
+ export const promptKey = (sessionID) => `${PREFIX}${sessionID}/prompt`;
9
+ export const settledKey = (sessionID) => `${PREFIX}${sessionID}/settled`;
10
+ export const progressKey = (sessionID) => `${PREFIX}${sessionID}/progress`;
11
+ export const toldKey = (sessionID) => `${PREFIX}${sessionID}/told`;
12
+ export const stoppedKey = (sessionID) => `${PREFIX}${sessionID}/stopped`;
13
+ const timeOf = (value) => {
14
+ const at = obj(value).at;
15
+ return typeof at === "number" && Number.isFinite(at) ? at : undefined;
16
+ };
17
+ /**
18
+ * Notes that a prompt or message reached a spawned session at `at`, in epoch milliseconds, unless a
19
+ * later one is noted: both of the hub's subscriptions note each event, one of them maybe behind.
20
+ */
21
+ export async function prompted(storage, sessionID, at) {
22
+ if ((timeOf(await storage.get(promptKey(sessionID))) ?? -Infinity) < at)
23
+ await storage.set(promptKey(sessionID), { at });
24
+ }
25
+ /**
26
+ * Notes that a spawned session reported to its parent, with `status` when its report carried one, or
27
+ * its turn failed or was interrupted, at `at`, unless a later end is noted.
28
+ */
29
+ export async function settled(storage, sessionID, by, at, status) {
30
+ if ((timeOf(await storage.get(settledKey(sessionID))) ?? -Infinity) <= at)
31
+ await storage.set(settledKey(sessionID), { at, by, ...(status ? { status } : {}) });
32
+ }
33
+ /** Notes that a spawned session messaged its parent without a status at `at`, which is progress, not its report. */
34
+ export async function progressed(storage, sessionID, at) {
35
+ if ((timeOf(await storage.get(progressKey(sessionID))) ?? -Infinity) <= at)
36
+ await storage.set(progressKey(sessionID), { at });
37
+ }
38
+ /** Notes that a spawned session's parent was told, at `at`, that a turn of it ended without a report, unless a later telling is noted. */
39
+ export async function told(storage, sessionID, at) {
40
+ if ((timeOf(await storage.get(toldKey(sessionID))) ?? -Infinity) <= at)
41
+ await storage.set(toldKey(sessionID), { at });
42
+ }
43
+ /** Takes back the note `told` made at `at`, whose notice did not go out; a later one stays, and one that cannot be read goes. */
44
+ export async function untold(storage, sessionID, at) {
45
+ const stored = await storage.get(toldKey(sessionID)).then(timeOf, () => at);
46
+ if (stored === at)
47
+ await storage.remove(toldKey(sessionID));
48
+ }
49
+ /** Whether `at` is known and no earlier than the prompt at `prompt`. */
50
+ const since = (at, prompt) => at !== undefined && at >= prompt;
51
+ /** Notes that courier_stop stopped a spawned session at `at`, so the failure and silent-end notices stay quiet for it. */
52
+ export async function stopped(storage, sessionID, at) {
53
+ await storage.set(stoppedKey(sessionID), { at });
54
+ }
55
+ /** Whether courier_stop stopped a session after its last prompt, or with none noted, at all. */
56
+ export async function stoppedSincePrompt(storage, sessionID) {
57
+ const [stoppedAt, prompt] = await Promise.all([stoppedKey, promptKey].map((key) => storage.get(key(sessionID)).then(timeOf)));
58
+ return stoppedAt !== undefined && (prompt === undefined || stoppedAt >= prompt);
59
+ }
60
+ /** The status of a spawned session's last report to its parent, if it has reported with one. */
61
+ export async function lastStatus(storage, sessionID) {
62
+ const status = obj(await storage.get(settledKey(sessionID))).status;
63
+ return isStatus(status) ? status : undefined;
64
+ }
65
+ /**
66
+ * What is known of a spawned session's report: when a prompt last reached it, and whether since then it owes one, has
67
+ * messaged its parent without a status, or failed or was interrupted. Undefined for one spawned before this was kept.
68
+ */
69
+ export async function reportOf(storage, sessionID) {
70
+ const [prompt, settledValue, progress] = await Promise.all([promptKey, settledKey, progressKey].map((key) => storage.get(key(sessionID))));
71
+ const promptAt = timeOf(prompt);
72
+ if (promptAt === undefined)
73
+ return undefined;
74
+ const done = since(timeOf(settledValue), promptAt);
75
+ return { prompt: promptAt, owes: !done, progressed: since(timeOf(progress), promptAt), ended: done && obj(settledValue).by !== "report" };
76
+ }
77
+ /**
78
+ * Whether its parent still waits on a spawned session's report: it owes one, and the parent has not
79
+ * been told since its last prompt that it ended a turn without one.
80
+ */
81
+ export async function awaited(storage, sessionID) {
82
+ const [prompt, settledAt, toldAt] = await Promise.all([promptKey, settledKey, toldKey].map((key) => storage.get(key(sessionID)).then(timeOf)));
83
+ return prompt !== undefined && !since(settledAt, prompt) && !since(toldAt, prompt);
84
+ }
85
+ /** Drops what is kept about a session, with its roster entry. */
86
+ export async function forgetReport(storage, sessionID) {
87
+ await Promise.all([promptKey, settledKey, progressKey, toldKey, stoppedKey].map((key) => storage.remove(key(sessionID))));
88
+ }
package/dist/roster.d.ts CHANGED
@@ -18,11 +18,14 @@ export interface RosterEntry {
18
18
  readonly project?: string;
19
19
  /** For an isolated child, the commit its worktree was made from; its own work is what came after. */
20
20
  readonly base?: string;
21
+ /** `"status"` when its brief asked for a report with a status; absent for a child an earlier release briefed. */
22
+ readonly reports?: "status";
23
+ /** The join group courier_spawn put it in, whose release holds its report; absent for a child in none. */
24
+ readonly group?: string;
21
25
  }
22
26
  /**
23
- * What the reverse index holds for a child: the sessions above it, its parent first and the session
24
- * at the top, which courier_spawn did not start, last. Its roster entry is under the first, and each
25
- * of the others is the parent of the one before it.
27
+ * What the reverse index holds for a child: the sessions above it, its parent first and the top
28
+ * session, which courier_spawn did not start, last; each is the parent of the one before it.
26
29
  */
27
30
  export interface ReverseEntry {
28
31
  readonly ancestors: ReadonlyArray<string>;
@@ -36,10 +39,12 @@ export declare function reverseKey(sessionID: string): string;
36
39
  */
37
40
  export declare function record(storage: RosterStorage, entry: RosterEntry): Promise<void>;
38
41
  /**
39
- * Removes a child's roster entry and its reverse key. Only the entry's removal can fail it: a
40
- * reverse key left behind leads nowhere, and the next load drops it.
42
+ * Removes a child's roster entry, its reverse key, its report state and, given its group, its membership. Only
43
+ * the entry's removal can fail it: a reverse key left behind leads nowhere, and the next load drops it.
41
44
  */
42
- export declare function remove(storage: RosterStorage, parentID: string, sessionID: string): Promise<void>;
45
+ export declare function remove(storage: RosterStorage, parentID: string, sessionID: string, group?: string): Promise<{
46
+ droppedFromGroup: boolean;
47
+ }>;
43
48
  /** Removes a child from its parent's roster; false when it was not there. */
44
49
  export declare function forget(storage: RosterStorage, parentID: string, sessionID: string): Promise<boolean>;
45
50
  /** A parent's children, oldest first. */
@@ -48,19 +53,28 @@ export declare function children(storage: RosterStorage, parentID: string): Prom
48
53
  export declare function allEntries(storage: RosterStorage): Promise<RosterEntry[]>;
49
54
  /** A session's roster entries: one for the parent that started it, none if courier_spawn did not. */
50
55
  export declare function entriesOf(storage: RosterStorage, sessionID: string): Promise<RosterEntry[]>;
56
+ /** The session that started a session, by the reverse index alone, without scanning the roster. */
57
+ export declare function indexedParent(storage: RosterStorage, sessionID: string): Promise<string | undefined>;
58
+ /** A session's roster entry under a parent, if it is there. */
59
+ export declare function entryUnder(storage: RosterStorage, parentID: string, sessionID: string): Promise<RosterEntry | undefined>;
60
+ /** A session's roster entry, by the reverse index alone, without scanning the roster. */
61
+ export declare function indexedEntry(storage: RosterStorage, sessionID: string): Promise<RosterEntry | undefined>;
51
62
  /**
52
- * The roster entries from a session up to the top, its own first: the session's, its parent's, and
53
- * so on, ending with the entry whose parent courier_spawn did not start. Empty when courier_spawn
54
- * did not start the session.
63
+ * The roster entries from a session up to the top, its own first, ending with the entry whose parent
64
+ * courier_spawn did not start; empty when courier_spawn did not start the session.
55
65
  */
56
66
  export declare function lineage(storage: RosterStorage, sessionID: string): Promise<RosterEntry[]>;
67
+ /** `lineage` by the reverse index alone: empty, without scanning the roster, when it does not name the session. */
68
+ export declare function indexedLineage(storage: RosterStorage, sessionID: string): Promise<never[] | RosterEntry[]>;
57
69
  /** `lineage` over roster entries already read. */
58
70
  export declare function lineageIn(entries: ReadonlyArray<RosterEntry>, sessionID: string): RosterEntry[];
71
+ /** Roster entries by their session, for `lineageBy`. */
72
+ export declare function bySession(entries: ReadonlyArray<RosterEntry>): Map<string, RosterEntry>;
73
+ /** `lineage` over roster entries already read and mapped by `bySession`. */
74
+ export declare function lineageBy(bySession: ReadonlyMap<string, RosterEntry>, sessionID: string): RosterEntry[];
59
75
  /**
60
- * The session at the top of a spawned session's lineage, where its permission requests and
61
- * questions go, since that is where the person is; only it may answer them. Throws when the
62
- * session was not started with courier_spawn or `callerID` is another session. `what` names
63
- * what is being answered.
76
+ * The top of a spawned session's lineage, where its permission requests and questions go, since the
77
+ * person is there. Throws unless the session was spawned and `callerID` is that top; `what` names it.
64
78
  */
65
79
  export declare function answeringTop(storage: RosterStorage, sessionID: string, callerID: string, what: string): Promise<string>;
66
80
  /** Whether a directory still exists; tests pass a fake. */
@@ -73,9 +87,7 @@ export declare function current(storage: RosterStorage, parentID: string, now: n
73
87
  */
74
88
  export declare function pruneExpired(storage: RosterStorage, now: number, exists?: Exists): Promise<void>;
75
89
  /**
76
- * Writes the reverse keys missing for the given entries, or naming another parent, all of the roster, such as those an older
77
- * copy of the plugin recorded, and drops the reverse keys whose entry is gone, such as those of an
78
- * entry an older copy removed. Keys that lead to their entry are left as they are, so running it
79
- * again changes nothing.
90
+ * Writes the reverse keys missing or stale for the given entries, or the whole roster, and drops those
91
+ * whose entry is gone, as an older copy's writes and removals leave them; a rerun changes nothing.
80
92
  */
81
93
  export declare function backfill(storage: RosterStorage, entries: ReadonlyArray<RosterEntry>): Promise<void>;