opencode-courier 0.3.0 → 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 (141) hide show
  1. package/README.md +175 -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 +1219 -0
  107. package/dist/webhook/index.d.ts +5 -0
  108. package/dist/webhook/index.js +3 -0
  109. package/dist/{webhook.d.ts → webhook/receiver.d.ts} +17 -23
  110. package/dist/{webhook.js → webhook/receiver.js} +58 -50
  111. package/dist/webhook/subscriptions.d.ts +29 -0
  112. package/dist/webhook/subscriptions.js +52 -0
  113. package/dist/webhook/text.d.ts +23 -0
  114. package/dist/webhook/text.js +82 -0
  115. package/dist/webhook/tools.d.ts +19 -0
  116. package/dist/webhook/tools.js +46 -0
  117. package/package.json +18 -7
  118. package/dist/courier.d.ts +0 -200
  119. package/dist/courier.js +0 -315
  120. package/dist/group.d.ts +0 -86
  121. package/dist/group.js +0 -247
  122. package/dist/hub.d.ts +0 -300
  123. package/dist/limits.d.ts +0 -65
  124. package/dist/notices.d.ts +0 -356
  125. package/dist/notices.js +0 -678
  126. package/dist/question/index.d.ts +0 -6
  127. package/dist/question/shared.js +0 -24
  128. package/dist/report.d.ts +0 -48
  129. package/dist/report.js +0 -88
  130. package/dist/roster.d.ts +0 -93
  131. package/dist/roster.js +0 -206
  132. package/dist/tools.d.ts +0 -94
  133. package/dist/watch.js +0 -474
  134. /package/dist/{question → person/question}/lifecycle.js +0 -0
  135. /package/dist/{question → person/question}/runtime.d.ts +0 -0
  136. /package/dist/{question → person/question}/runtime.js +0 -0
  137. /package/dist/{json.d.ts → platform/json.d.ts} +0 -0
  138. /package/dist/{json.js → platform/json.js} +0 -0
  139. /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
  140. /package/dist/{storage.js → platform/storage.js} +0 -0
  141. /package/dist/{version.d.ts → platform/version.d.ts} +0 -0
@@ -0,0 +1,276 @@
1
+ /**
2
+ * The tree's ports, what it needs of the plugin context and of other modules, and the data types they and
3
+ * its texts take; those its pure rules take are declared in `domain.ts` and re-exported here.
4
+ */
5
+ import type { Plugin } from "@opencode/plugin";
6
+ import type { Status } from "../kernel/protocol.js";
7
+ import type { Storage } from "../platform/storage.js";
8
+ import type { Limits, Member, RosterEntry } from "./domain.js";
9
+ export type { Artifacts, HeldReport, LeftMember, Limits, Member, RosterEntry, Standing } from "./domain.js";
10
+ type Context = Plugin.Context;
11
+ export type RosterStorage = Storage;
12
+ /** How a session's wait for a report ended: it reported to its parent, or its turn failed or was interrupted. */
13
+ export type Settled = "report" | "failed" | "interrupted";
14
+ /** The notes kept of a spawned session's report state; docs/maintainers/architecture.md has each. */
15
+ export type ReportField = "prompt" | "settled" | "progress" | "told" | "stopped";
16
+ /** A report note as read: when it was made, and for `settled` how the wait ended and the report's status, as stored. */
17
+ export interface ReportNote {
18
+ readonly at: number | undefined;
19
+ readonly by: unknown;
20
+ readonly status: unknown;
21
+ }
22
+ /** A stored value, as `Storage` gives and takes it. */
23
+ export type Stored = Parameters<Storage["set"]>[1];
24
+ /** The report keys found, by the session they are about. */
25
+ export type ReportKeys = ReadonlyMap<string, ReadonlyArray<string>>;
26
+ /**
27
+ * A spawned session as kept: its roster entry under its parent, the reverse index that finds it, and its report
28
+ * state, which go together. The reverse index is only an index: a session it does not name is looked for in the roster.
29
+ */
30
+ export interface ChildStore {
31
+ /** Records a child under its parent, then indexes it; a failed index write is left to a scan and the next load. */
32
+ record(entry: RosterEntry): Promise<void>;
33
+ /** Removes a child's entry and, at once and best effort, its reverse key and report state; only the entry's removal can fail it. */
34
+ remove(parentID: string, sessionID: string): Promise<void>;
35
+ /** A session's roster entry under a parent, if it is there. */
36
+ entry(parentID: string, sessionID: string): Promise<RosterEntry | undefined>;
37
+ /** A parent's children, oldest first. */
38
+ children(parentID: string): Promise<RosterEntry[]>;
39
+ /** Every parent's children. */
40
+ all(): Promise<RosterEntry[]>;
41
+ /** A session's roster entries: one for the parent that started it, none if courier_spawn did not. */
42
+ entriesOf(sessionID: string): Promise<RosterEntry[]>;
43
+ /** The session that started a session, by the reverse index alone, without scanning the roster. */
44
+ indexedParent(sessionID: string): Promise<string | undefined>;
45
+ /** A session's roster entry, by the reverse index alone, without scanning the roster. */
46
+ indexedEntry(sessionID: string): Promise<RosterEntry | undefined>;
47
+ /**
48
+ * The roster entries from a session up to the top, its own first, ending with the entry whose parent
49
+ * courier_spawn did not start; empty when courier_spawn did not start the session.
50
+ */
51
+ lineage(sessionID: string): Promise<RosterEntry[]>;
52
+ /** `lineage` by the reverse index alone: empty, without scanning the roster, when it does not name the session. */
53
+ indexedLineage(sessionID: string): Promise<RosterEntry[]>;
54
+ /** Writes the reverse keys missing or stale for the given entries, the whole roster, and drops those whose entry is gone. */
55
+ backfill(entries: ReadonlyArray<RosterEntry>): Promise<void>;
56
+ /** The given notes of a session's report state, read at once. */
57
+ notes<F extends ReportField>(sessionID: string, fields: ReadonlyArray<F>): Promise<{
58
+ readonly [K in F]: ReportNote;
59
+ }>;
60
+ /** Notes that a prompt or message reached a spawned session at `at`, unless a later one is noted. */
61
+ prompted(sessionID: string, at: number): Promise<void>;
62
+ /** Notes how a spawned session's wait for a report ended at `at`, with its report's status, unless a later end is noted. */
63
+ settled(sessionID: string, by: Settled, at: number, status?: Status): Promise<void>;
64
+ /** Notes that a spawned session messaged its parent without a status at `at`, unless a later one is noted. */
65
+ progressed(sessionID: string, at: number): Promise<void>;
66
+ /** Notes that a spawned session's parent was told at `at` that a turn ended without a report, unless a later telling is noted. */
67
+ told(sessionID: string, at: number): Promise<void>;
68
+ /** Takes back the note `told` made at `at`; a later one stays, and one that cannot be read goes. */
69
+ untold(sessionID: string, at: number): Promise<void>;
70
+ /** Notes that courier_stop stopped a spawned session at `at`. */
71
+ stopped(sessionID: string, at: number): Promise<void>;
72
+ /** A `settled` or `progress` note as stored, for `restore` to put back. */
73
+ peek(sessionID: string, field: "settled" | "progress"): Promise<Stored | undefined>;
74
+ /** Puts back a note `peek` read; one that was not there is removed. */
75
+ restore(sessionID: string, field: "settled" | "progress", value: Stored | undefined): Promise<void>;
76
+ /** Drops a session's report state. */
77
+ forgetReport(sessionID: string): Promise<void>;
78
+ /** Every report key, by the session it is about. */
79
+ reported(): Promise<ReportKeys>;
80
+ /** Removes report keys `reported` found, each best effort. */
81
+ dropReported(keys: ReadonlyArray<string>): Promise<void>;
82
+ }
83
+ /** A member of a join group with where it is kept. */
84
+ export interface Membership extends Member {
85
+ readonly parentID: string;
86
+ readonly group: string;
87
+ readonly sessionID: string;
88
+ }
89
+ /** The members of parents' join groups, each with its held report or how it left; a value that is not a member reads as none. */
90
+ export interface GroupStore {
91
+ /** A session's member in a parent's group, if it is one. */
92
+ member(parentID: string, group: string, sessionID: string): Promise<Member | undefined>;
93
+ /** The members of every group. */
94
+ all(): Promise<Membership[]>;
95
+ /** The members of a parent's groups. */
96
+ ofParent(parentID: string): Promise<Membership[]>;
97
+ /** The members of one group. */
98
+ ofGroup(parentID: string, group: string): Promise<Membership[]>;
99
+ /** Writes a member in place of what was kept for it. */
100
+ put(parentID: string, group: string, sessionID: string, member: Member): Promise<void>;
101
+ /** Removes a member whatever it holds. */
102
+ remove(parentID: string, group: string, sessionID: string): Promise<void>;
103
+ }
104
+ /** A report held with its group: which group, and how many of its members have reported so far, when that could be read. */
105
+ export interface Held {
106
+ readonly group: string;
107
+ readonly reported?: number;
108
+ readonly members?: number;
109
+ }
110
+ /**
111
+ * What courier_send did; `report` says whether the message was noted as the sender's report to the session
112
+ * that started it, and `held` that it was held with the sender's group rather than delivered, with no `messageID`.
113
+ */
114
+ export interface Sent {
115
+ readonly messageID?: string;
116
+ readonly status?: Status;
117
+ readonly report?: boolean;
118
+ readonly held?: Held;
119
+ }
120
+ /** The slice of the plugin context the courier tools use; tests pass a fake. */
121
+ export interface CourierPorts {
122
+ readonly session: Pick<Context["session"], "create" | "prompt" | "synthetic" | "get" | "context" | "interrupt">;
123
+ readonly agent: Pick<Context["agent"], "get">;
124
+ readonly worktree: Pick<Context["worktree"], "create" | "remove">;
125
+ /** The project of the plugin's location; an isolated child's worktree is made in it, and removed from it. */
126
+ readonly projectID: string;
127
+ /** The commit a directory's checkout is on, or undefined; recorded as an isolated child's base. */
128
+ readonly head: (directory: string) => Promise<string | undefined>;
129
+ /** Whether a directory's checkout has uncommitted changes, or undefined when git cannot tell. */
130
+ readonly dirty: (directory: string) => Promise<boolean | undefined>;
131
+ readonly storage: RosterStorage;
132
+ readonly directory: string;
133
+ readonly now: () => number;
134
+ /** The permission requests and questions a session waits on, wherever they are pending. */
135
+ readonly pending: (sessionID: string) => Promise<ReadonlyArray<Pending>>;
136
+ readonly limits: Limits;
137
+ /** The `nativeChildren` option: a child started without isolate is created with OpenCode's own `parentID`. */
138
+ readonly nativeChildren?: boolean;
139
+ readonly gate: SpawnGate;
140
+ /** The depths the `context` hook remembers; a parent's is dropped when it starts a child. */
141
+ readonly roles: Map<string, number | null>;
142
+ /** Has the scheduler deliver what is due now rather than at its next tick: a group a report has just completed. */
143
+ readonly nudge: () => void;
144
+ readonly log: (message: string) => void;
145
+ }
146
+ /** One question a session waits on, as `Pending` shows it; `person/`'s `Prompt` has these fields. */
147
+ export interface PendingPrompt {
148
+ readonly question: string;
149
+ readonly header: string;
150
+ readonly options: ReadonlyArray<{
151
+ readonly label: string;
152
+ readonly description: string;
153
+ }>;
154
+ readonly multiple?: boolean;
155
+ }
156
+ /** A request a session waits on until someone answers it: a permission request, or a question it asked. */
157
+ export type Pending = {
158
+ readonly type: "permission";
159
+ readonly requestID: string;
160
+ readonly action: string;
161
+ readonly resources: ReadonlyArray<string>;
162
+ readonly save?: ReadonlyArray<string>;
163
+ } | {
164
+ readonly type: "question";
165
+ readonly requestID: string;
166
+ readonly questions: ReadonlyArray<PendingPrompt>;
167
+ /** Its question call was cut off; the answer reaches it as a message. */
168
+ readonly stopped?: true;
169
+ };
170
+ /** A spawn past its limit check: its parent, the top of its tree, and its child once created. */
171
+ export interface Reservation {
172
+ readonly parentID: string;
173
+ readonly top: string;
174
+ sessionID?: string;
175
+ }
176
+ /**
177
+ * The spawns under way in the process, under a fixed process-wide key outside the hub, so spawns in
178
+ * other locations and copies count them; `turn` runs the limit checks one at a time.
179
+ */
180
+ export interface SpawnGate {
181
+ readonly reserved: Set<Reservation>;
182
+ turn: Promise<unknown>;
183
+ }
184
+ export interface LimitPorts {
185
+ readonly storage: RosterStorage;
186
+ readonly session: Pick<Context["session"], "get">;
187
+ readonly limits: Limits;
188
+ readonly gate: SpawnGate;
189
+ }
190
+ /** What courier_cleanup did: removed the worktree, found it gone, kept it and why, or forgot a child with none; `group`, one it dropped the child from. */
191
+ export type CleanupResult = {
192
+ readonly sessionID: string;
193
+ readonly directory: string;
194
+ readonly outcome: "removed" | "gone" | "forgotten";
195
+ readonly group?: string;
196
+ } | {
197
+ readonly sessionID: string;
198
+ readonly directory: string;
199
+ readonly outcome: "kept";
200
+ readonly reason: string;
201
+ readonly changes: readonly string[];
202
+ readonly commits: readonly string[];
203
+ };
204
+ /** What would be lost by removing a worktree. */
205
+ export interface WorktreeState {
206
+ /** Paths with uncommitted changes, untracked files included, as `git status --porcelain` lists them. */
207
+ readonly changes: readonly string[];
208
+ /**
209
+ * Commits reachable from the worktree's HEAD but from no branch, tag or remote-tracking ref, nor
210
+ * from the commit the worktree was made from, newest first.
211
+ */
212
+ readonly commits: readonly string[];
213
+ }
214
+ export interface CleanupPorts {
215
+ readonly storage: RosterStorage;
216
+ readonly worktree: Pick<Context["worktree"], "remove">;
217
+ /** The project of the plugin's location, for roster entries recorded before they carried their project. */
218
+ readonly projectID: string;
219
+ /** The worktree's state, or undefined when its directory no longer exists; `base` is the commit it was made from. */
220
+ readonly inspect: (directory: string, base?: string) => Promise<WorktreeState | undefined>;
221
+ /** When a member still out is dropped from its group. */
222
+ readonly now: () => number;
223
+ /** Has the scheduler deliver what is due now: a group the forgotten child may have been the last member out of. */
224
+ readonly nudge: () => void;
225
+ }
226
+ export interface StopPorts {
227
+ readonly courier: CourierPorts;
228
+ /** Cancels the messages scheduled for a session; the ids of those cancelled. */
229
+ readonly cancelScheduled: (sessionID: string) => Promise<ReadonlyArray<string>>;
230
+ readonly cleanup: CleanupPorts;
231
+ }
232
+ export interface GroupPorts {
233
+ readonly storage: Storage;
234
+ readonly session: Pick<Context["session"], "synthetic" | "get">;
235
+ readonly log: (message: string) => void;
236
+ }
237
+ /** The error of OpenCode's `session.execution.failed` event. */
238
+ export interface ExecutionError {
239
+ readonly type: string;
240
+ readonly message: string;
241
+ readonly status?: number;
242
+ }
243
+ export interface WatchPorts {
244
+ readonly storage: RosterStorage;
245
+ /** `context` reads the last reply of a session that ended its turn without reporting. */
246
+ readonly session: Pick<Context["session"], "synthetic" | "context">;
247
+ readonly event: Pick<Context["event"], "subscribe">;
248
+ /** The permission domains of every loaded location, which the `Waits` suppliers ask; an isolated child's are in its worktree's. */
249
+ readonly permissions: () => Iterable<Pick<Context["permission"], "list">>;
250
+ /** The clock, in epoch milliseconds, for an event that carries no `created`. */
251
+ readonly now: () => number;
252
+ readonly log: (message: string) => void;
253
+ /** Has the scheduler deliver what is due now rather than at its next tick: a group completed, or its parent's turn ended. */
254
+ readonly nudge: () => void;
255
+ }
256
+ /**
257
+ * What every plugin instance in the process shares. Several subscriptions and copies see the same
258
+ * events, so each is claimed synchronously and handled once.
259
+ */
260
+ export interface WatchState {
261
+ readonly seen: Set<string>;
262
+ /** The type of each item enqueued for a spawned session, by inbox id, for its delivery to look up. */
263
+ readonly inbox: Map<string, string>;
264
+ }
265
+ /**
266
+ * Whether a session waits, since `since`, on what the tree does not keep: asked by the silent-turn
267
+ * rule after the tree's own checks, through the watcher's ports; `src/index.ts` composes it.
268
+ */
269
+ export type Waits = (ports: Pick<WatchPorts, "storage" | "permissions">, sessionID: string, since: number) => Promise<boolean>;
270
+ export interface ContextPorts {
271
+ readonly storage: RosterStorage;
272
+ readonly limits: Limits;
273
+ /** Each session's depth once looked up: null for one nobody spawned that has started none, until it starts one. */
274
+ readonly roles: Map<string, number | null>;
275
+ readonly log: (message: string) => void;
276
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { type Status } from "../kernel/protocol.js";
2
+ import type { ChildStore } from "./ports.js";
3
+ type Notes = Pick<ChildStore, "notes">;
4
+ /** Whether courier_stop stopped a session after its last prompt, or with none noted, at all. */
5
+ export declare function stoppedSincePrompt(children: Notes, sessionID: string): Promise<boolean>;
6
+ /** The status of a spawned session's last report to its parent, if it has reported with one. */
7
+ export declare function lastStatus(children: Notes, sessionID: string): Promise<Status | undefined>;
8
+ /**
9
+ * What is known of a spawned session's report: when a prompt last reached it, and whether since then it owes one, has
10
+ * messaged its parent without a status, or failed or was interrupted. Undefined for one spawned before this was kept.
11
+ */
12
+ export declare function reportOf(children: Notes, sessionID: string): Promise<{
13
+ prompt: number;
14
+ owes: boolean;
15
+ progressed: boolean;
16
+ ended: boolean;
17
+ } | undefined>;
18
+ /**
19
+ * Whether its parent still waits on a spawned session's report: it owes one, and the parent has not
20
+ * been told since its last prompt that it ended a turn without one.
21
+ */
22
+ export declare function awaited(children: Notes, sessionID: string): Promise<boolean>;
23
+ export {};
@@ -0,0 +1,33 @@
1
+ import { isStatus } from "../kernel/protocol.js";
2
+ /** Whether `at` is known and no earlier than the prompt at `prompt`. */
3
+ const since = (at, prompt) => at !== undefined && at >= prompt;
4
+ /** Whether courier_stop stopped a session after its last prompt, or with none noted, at all. */
5
+ export async function stoppedSincePrompt(children, sessionID) {
6
+ const { stopped, prompt } = await children.notes(sessionID, ["stopped", "prompt"]);
7
+ return stopped.at !== undefined && (prompt.at === undefined || stopped.at >= prompt.at);
8
+ }
9
+ /** The status of a spawned session's last report to its parent, if it has reported with one. */
10
+ export async function lastStatus(children, sessionID) {
11
+ const { status } = (await children.notes(sessionID, ["settled"])).settled;
12
+ return isStatus(status) ? status : undefined;
13
+ }
14
+ /**
15
+ * What is known of a spawned session's report: when a prompt last reached it, and whether since then it owes one, has
16
+ * messaged its parent without a status, or failed or was interrupted. Undefined for one spawned before this was kept.
17
+ */
18
+ export async function reportOf(children, sessionID) {
19
+ const { prompt, settled, progress } = await children.notes(sessionID, ["prompt", "settled", "progress"]);
20
+ const promptAt = prompt.at;
21
+ if (promptAt === undefined)
22
+ return undefined;
23
+ const done = since(settled.at, promptAt);
24
+ return { prompt: promptAt, owes: !done, progressed: since(progress.at, promptAt), ended: done && settled.by !== "report" };
25
+ }
26
+ /**
27
+ * Whether its parent still waits on a spawned session's report: it owes one, and the parent has not
28
+ * been told since its last prompt that it ended a turn without one.
29
+ */
30
+ export async function awaited(children, sessionID) {
31
+ const { prompt, settled, told } = await children.notes(sessionID, ["prompt", "settled", "told"]);
32
+ return prompt.at !== undefined && !since(settled.at, prompt.at) && !since(told.at, prompt.at);
33
+ }
@@ -0,0 +1,27 @@
1
+ import type { RosterEntry, RosterStorage } from "./ports.js";
2
+ /**
3
+ * Entries older than this are dropped when their parent's roster is read, and on plugin setup,
4
+ * except isolated children whose worktree is still there: those stay until courier_cleanup.
5
+ */
6
+ export declare const RETENTION_MS: number;
7
+ /** Every parent's children. */
8
+ export declare function allEntries(storage: RosterStorage): Promise<RosterEntry[]>;
9
+ /**
10
+ * The roster entries from a session up to the top, its own first, ending with the entry whose parent
11
+ * courier_spawn did not start; empty when courier_spawn did not start the session.
12
+ */
13
+ export declare function lineage(storage: RosterStorage, sessionID: string): Promise<RosterEntry[]>;
14
+ /**
15
+ * The top of a spawned session's lineage, where its permission requests and questions go, since the
16
+ * person is there. Throws unless the session was spawned and `callerID` is that top; `what` names it.
17
+ */
18
+ export declare function answeringTop(storage: RosterStorage, sessionID: string, callerID: string, what: string): Promise<string>;
19
+ /** Whether a directory still exists; tests pass a fake. */
20
+ export type Exists = (directory: string) => boolean;
21
+ /** A parent's children, oldest first, after dropping the expired ones. */
22
+ export declare function current(storage: RosterStorage, parentID: string, now: number, exists?: Exists): Promise<RosterEntry[]>;
23
+ /**
24
+ * On loading: drops expired entries of every parent, then brings the reverse index in line with
25
+ * what is left (`backfill`) and drops report state with no entry (`dropOrphanedReports`).
26
+ */
27
+ export declare function pruneExpired(storage: RosterStorage, now: number, exists?: Exists): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import { existsSync } from "node:fs";
2
+ import { forgetChild, forgetReports } from "./forget.js";
3
+ import { childStore } from "./store.js";
4
+ /**
5
+ * Entries older than this are dropped when their parent's roster is read, and on plugin setup,
6
+ * except isolated children whose worktree is still there: those stay until courier_cleanup.
7
+ */
8
+ export const RETENTION_MS = 14 * 24 * 60 * 60_000;
9
+ /** Every parent's children. */
10
+ export async function allEntries(storage) {
11
+ return childStore(storage).all();
12
+ }
13
+ /**
14
+ * The roster entries from a session up to the top, its own first, ending with the entry whose parent
15
+ * courier_spawn did not start; empty when courier_spawn did not start the session.
16
+ */
17
+ export async function lineage(storage, sessionID) {
18
+ return childStore(storage).lineage(sessionID);
19
+ }
20
+ /**
21
+ * The top of a spawned session's lineage, where its permission requests and questions go, since the
22
+ * person is there. Throws unless the session was spawned and `callerID` is that top; `what` names it.
23
+ */
24
+ export async function answeringTop(storage, sessionID, callerID, what) {
25
+ const chain = await lineage(storage, sessionID);
26
+ if (!chain.length)
27
+ throw new Error(`${sessionID} was not started with courier_spawn, so courier_answer cannot answer for it.`);
28
+ const top = chain.at(-1).parentID;
29
+ if (top !== callerID)
30
+ throw new Error(`${sessionID}'s ${what} go to ${top}, the session at the top of the sessions started from it with courier_spawn; ${callerID} cannot answer them.`);
31
+ return top;
32
+ }
33
+ /**
34
+ * Forgets the given entries that were recorded more than `RETENTION_MS` before `now`, and returns
35
+ * the rest. An isolated child is kept while its worktree exists, so it can still be cleaned up.
36
+ */
37
+ async function dropExpired(storage, entries, now, exists) {
38
+ const expired = new Set(entries.filter((entry) => now - entry.createdAt > RETENTION_MS && !(entry.isolated && exists(entry.directory))));
39
+ await Promise.all([...expired].map((entry) => forgetChild(storage, entry.parentID, entry.sessionID, entry.group)));
40
+ return entries.filter((entry) => !expired.has(entry));
41
+ }
42
+ /** A parent's children, oldest first, after dropping the expired ones. */
43
+ export async function current(storage, parentID, now, exists = existsSync) {
44
+ return dropExpired(storage, await childStore(storage).children(parentID), now, exists);
45
+ }
46
+ /**
47
+ * On loading: drops expired entries of every parent, then brings the reverse index in line with
48
+ * what is left (`backfill`) and drops report state with no entry (`dropOrphanedReports`).
49
+ */
50
+ export async function pruneExpired(storage, now, exists = existsSync) {
51
+ const children = childStore(storage);
52
+ // Report keys are read before the roster: courier_spawn writes a child's entry before its first
53
+ // report key, so a session seen here that spawns during the sweep has its entry in the read below.
54
+ const reported = await children.reported().catch(() => new Map());
55
+ const entries = await dropExpired(storage, await children.all(), now, exists);
56
+ await Promise.all([children.backfill(entries), dropOrphanedReports(storage, reported, entries)]);
57
+ }
58
+ /** Removes the report keys read of sessions that have no roster entry. */
59
+ async function dropOrphanedReports(storage, reported, entries) {
60
+ const sessions = new Set(entries.map((entry) => entry.sessionID));
61
+ await forgetReports(storage, reported, [...reported.keys()].filter((sessionID) => !sessions.has(sessionID)));
62
+ }
@@ -0,0 +1,201 @@
1
+ import type { Plugin } from "@opencode/plugin";
2
+ import type { StopPorts } from "./ports.js";
3
+ /** Serves the tree's RPC at the plugin's location, through the same use cases as the tools; disposed with the plugin. */
4
+ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "register">, ports: StopPorts): Promise<import("@opencode/plugin/promise/rpc").RpcRegistration<{
5
+ readonly id: "courier";
6
+ readonly methods: {
7
+ readonly summary: {
8
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
9
+ readonly sessionID: string;
10
+ }, {
11
+ readonly sessionID: string;
12
+ }> & import("effect/Schema").Struct<{
13
+ readonly sessionID: import("effect/Schema").String;
14
+ }>;
15
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
16
+ readonly running: number;
17
+ readonly idle: number;
18
+ readonly groups: readonly {
19
+ readonly name: string;
20
+ readonly reported: number;
21
+ readonly members: number;
22
+ }[];
23
+ readonly waiting: number;
24
+ readonly truncated?: number | undefined;
25
+ }, {
26
+ readonly running: number;
27
+ readonly idle: number;
28
+ readonly groups: readonly {
29
+ readonly name: string;
30
+ readonly reported: number;
31
+ readonly members: number;
32
+ }[];
33
+ readonly waiting: number;
34
+ readonly truncated?: number | undefined;
35
+ }> & import("effect/Schema").Struct<{
36
+ readonly running: import("effect/Schema").Int;
37
+ readonly waiting: import("effect/Schema").Int;
38
+ readonly idle: import("effect/Schema").Int;
39
+ readonly groups: import("effect/Schema").$Array<import("effect/Schema").Struct<{
40
+ readonly name: import("effect/Schema").String;
41
+ readonly reported: import("effect/Schema").Int;
42
+ readonly members: import("effect/Schema").Int;
43
+ }>>;
44
+ readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
45
+ }>;
46
+ };
47
+ readonly tree: {
48
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
49
+ readonly sessionID: string;
50
+ }, {
51
+ readonly sessionID: string;
52
+ }> & import("effect/Schema").Struct<{
53
+ readonly sessionID: import("effect/Schema").String;
54
+ }>;
55
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
56
+ readonly rows: readonly {
57
+ readonly sessionID: string;
58
+ readonly title: string;
59
+ readonly parentID: string;
60
+ readonly depth: number;
61
+ readonly created: number;
62
+ readonly isolated: boolean;
63
+ readonly state: "running" | "idle" | "gone" | "waiting";
64
+ readonly waits: readonly ({
65
+ readonly type: "permission";
66
+ readonly action: string;
67
+ readonly resources: readonly string[];
68
+ } | {
69
+ readonly type: "question";
70
+ readonly question: string;
71
+ })[];
72
+ readonly group?: {
73
+ readonly name: string;
74
+ readonly standing: string;
75
+ } | undefined;
76
+ readonly idle?: number | undefined;
77
+ readonly report?: {
78
+ readonly progressed: boolean;
79
+ readonly state: "reported" | "ended" | "owed";
80
+ readonly status?: string | undefined;
81
+ } | undefined;
82
+ readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
83
+ readonly tool?: {
84
+ readonly name: string;
85
+ readonly at: number;
86
+ } | undefined;
87
+ readonly cost?: number | undefined;
88
+ }[];
89
+ readonly truncated?: number | undefined;
90
+ }, {
91
+ readonly rows: readonly {
92
+ readonly sessionID: string;
93
+ readonly title: string;
94
+ readonly parentID: string;
95
+ readonly depth: number;
96
+ readonly created: number;
97
+ readonly isolated: boolean;
98
+ readonly state: "running" | "idle" | "gone" | "waiting";
99
+ readonly waits: readonly ({
100
+ readonly type: "permission";
101
+ readonly action: string;
102
+ readonly resources: readonly string[];
103
+ } | {
104
+ readonly type: "question";
105
+ readonly question: string;
106
+ })[];
107
+ readonly group?: {
108
+ readonly name: string;
109
+ readonly standing: string;
110
+ } | undefined;
111
+ readonly idle?: number | undefined;
112
+ readonly report?: {
113
+ readonly progressed: boolean;
114
+ readonly state: "reported" | "ended" | "owed";
115
+ readonly status?: string | undefined;
116
+ } | undefined;
117
+ readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
118
+ readonly tool?: {
119
+ readonly name: string;
120
+ readonly at: number;
121
+ } | undefined;
122
+ readonly cost?: number | undefined;
123
+ }[];
124
+ readonly truncated?: number | undefined;
125
+ }> & import("effect/Schema").Struct<{
126
+ readonly rows: import("effect/Schema").$Array<import("effect/Schema").Struct<{
127
+ readonly sessionID: import("effect/Schema").String;
128
+ readonly parentID: import("effect/Schema").String;
129
+ readonly title: import("effect/Schema").String;
130
+ readonly depth: import("effect/Schema").Int;
131
+ readonly state: import("effect/Schema").Literals<readonly ["running", "waiting", "idle", "gone"]>;
132
+ readonly outcome: import("effect/Schema").optionalKey<import("effect/Schema").Literals<readonly ["succeeded", "failed", "interrupted"]>>;
133
+ readonly report: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
134
+ readonly state: import("effect/Schema").Literals<readonly ["owed", "reported", "ended"]>;
135
+ readonly status: import("effect/Schema").optionalKey<import("effect/Schema").String>;
136
+ readonly progressed: import("effect/Schema").Boolean;
137
+ }>>;
138
+ readonly group: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
139
+ readonly name: import("effect/Schema").String;
140
+ readonly standing: import("effect/Schema").String;
141
+ }>>;
142
+ readonly isolated: import("effect/Schema").Boolean;
143
+ readonly created: import("effect/Schema").Number;
144
+ readonly idle: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
145
+ readonly cost: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
146
+ readonly tool: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
147
+ readonly name: import("effect/Schema").String;
148
+ readonly at: import("effect/Schema").Number;
149
+ }>>;
150
+ readonly waits: import("effect/Schema").$Array<import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
151
+ readonly type: import("effect/Schema").Literal<"permission">;
152
+ readonly action: import("effect/Schema").String;
153
+ readonly resources: import("effect/Schema").$Array<import("effect/Schema").String>;
154
+ }>, import("effect/Schema").Struct<{
155
+ readonly type: import("effect/Schema").Literal<"question">;
156
+ readonly question: import("effect/Schema").String;
157
+ }>]>>;
158
+ }>>;
159
+ readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
160
+ }>;
161
+ };
162
+ readonly stop: {
163
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
164
+ readonly sessionID: string;
165
+ readonly by: string;
166
+ }, {
167
+ readonly sessionID: string;
168
+ readonly by: string;
169
+ }> & import("effect/Schema").Struct<{
170
+ readonly sessionID: import("effect/Schema").String;
171
+ readonly by: import("effect/Schema").String;
172
+ }>;
173
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
174
+ readonly stopped: readonly {
175
+ readonly sessionID: string;
176
+ readonly title: string;
177
+ readonly outcome: string;
178
+ }[];
179
+ readonly cancelled: number;
180
+ }, {
181
+ readonly stopped: readonly {
182
+ readonly sessionID: string;
183
+ readonly title: string;
184
+ readonly outcome: string;
185
+ }[];
186
+ readonly cancelled: number;
187
+ }> & import("effect/Schema").Struct<{
188
+ readonly stopped: import("effect/Schema").$Array<import("effect/Schema").Struct<{
189
+ readonly sessionID: import("effect/Schema").String;
190
+ readonly title: import("effect/Schema").String;
191
+ readonly outcome: import("effect/Schema").String;
192
+ }>>;
193
+ readonly cancelled: import("effect/Schema").Int;
194
+ }>;
195
+ readonly errors: {
196
+ readonly refused: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & import("effect/Schema").Struct<{}>;
197
+ };
198
+ };
199
+ };
200
+ readonly events: {};
201
+ }>>;
@@ -0,0 +1,20 @@
1
+ import { panel } from "./panel.js";
2
+ import { treeRpc } from "./rpc.js";
3
+ import { stop } from "./stop.js";
4
+ import { summary } from "./summary.js";
5
+ /** Serves the tree's RPC at the plugin's location, through the same use cases as the tools; disposed with the plugin. */
6
+ export function registerTreeRpc(rpc, ports) {
7
+ return rpc.register(treeRpc, {
8
+ summary: ({ sessionID }) => summary(ports.courier, sessionID),
9
+ tree: ({ sessionID }) => panel(ports.courier, sessionID),
10
+ stop: async ({ sessionID, by }, { error }) => {
11
+ try {
12
+ const { stopped, cancelled } = await stop(ports, by, { sessionID });
13
+ return { stopped: stopped.map(({ sessionID, title, outcome }) => ({ sessionID, title, outcome })), cancelled: cancelled.length };
14
+ }
15
+ catch (cause) {
16
+ return error("refused", cause instanceof Error ? cause.message : String(cause), {});
17
+ }
18
+ },
19
+ });
20
+ }