opencode-courier 0.13.8 → 0.15.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 (163) hide show
  1. package/README.md +7 -9
  2. package/dist/index.d.ts +0 -8
  3. package/dist/index.js +7 -54
  4. package/dist/kernel/protocol.d.ts +3 -17
  5. package/dist/kernel/protocol.js +3 -17
  6. package/dist/kernel/tool.d.ts +0 -2
  7. package/dist/kernel/tool.js +0 -1
  8. package/dist/memory/domain.d.ts +6 -85
  9. package/dist/memory/domain.js +7 -63
  10. package/dist/memory/events.d.ts +0 -11
  11. package/dist/memory/events.js +1 -12
  12. package/dist/memory/forget.d.ts +0 -5
  13. package/dist/memory/forget.js +0 -4
  14. package/dist/memory/index.js +0 -2
  15. package/dist/memory/lessons.d.ts +1 -9
  16. package/dist/memory/lessons.js +2 -11
  17. package/dist/memory/note.d.ts +2 -11
  18. package/dist/memory/note.js +5 -11
  19. package/dist/memory/pin.d.ts +0 -5
  20. package/dist/memory/pin.js +0 -4
  21. package/dist/memory/ports.d.ts +2 -22
  22. package/dist/memory/recall.d.ts +0 -5
  23. package/dist/memory/recall.js +0 -12
  24. package/dist/memory/record.d.ts +0 -4
  25. package/dist/memory/record.js +8 -31
  26. package/dist/memory/rpc-handlers.d.ts +3 -6
  27. package/dist/memory/rpc-handlers.js +0 -6
  28. package/dist/memory/rpc.d.ts +6 -19
  29. package/dist/memory/rpc.js +1 -16
  30. package/dist/memory/store.d.ts +0 -5
  31. package/dist/memory/store.js +1 -16
  32. package/dist/memory/sweep.d.ts +0 -4
  33. package/dist/memory/sweep.js +0 -4
  34. package/dist/memory/text.d.ts +1 -26
  35. package/dist/memory/text.js +8 -38
  36. package/dist/memory/tools.d.ts +0 -2
  37. package/dist/memory/tools.js +4 -6
  38. package/dist/memory/tui.d.ts +1 -35
  39. package/dist/person/events.d.ts +0 -16
  40. package/dist/person/events.js +3 -30
  41. package/dist/person/form.d.ts +0 -9
  42. package/dist/person/form.js +0 -10
  43. package/dist/person/index.d.ts +0 -2
  44. package/dist/person/permission.d.ts +0 -34
  45. package/dist/person/permission.js +2 -28
  46. package/dist/person/ports.d.ts +0 -16
  47. package/dist/person/ports.js +0 -1
  48. package/dist/person/question/answer.d.ts +0 -28
  49. package/dist/person/question/answer.js +0 -49
  50. package/dist/person/question/index.js +0 -2
  51. package/dist/person/question/lifecycle.d.ts +0 -26
  52. package/dist/person/question/lifecycle.js +1 -32
  53. package/dist/person/question/pure.d.ts +0 -11
  54. package/dist/person/question/pure.js +0 -11
  55. package/dist/person/question/relay.d.ts +0 -1
  56. package/dist/person/question/relay.js +5 -67
  57. package/dist/person/question/runtime.d.ts +0 -14
  58. package/dist/person/question/runtime.js +1 -26
  59. package/dist/person/question/shared.d.ts +0 -17
  60. package/dist/person/question/shared.js +0 -20
  61. package/dist/person/question/types.d.ts +0 -44
  62. package/dist/person/rpc-handlers.d.ts +0 -7
  63. package/dist/person/rpc-handlers.js +0 -6
  64. package/dist/person/rpc.d.ts +0 -20
  65. package/dist/person/rpc.js +0 -16
  66. package/dist/person/text.d.ts +0 -45
  67. package/dist/person/text.js +2 -54
  68. package/dist/person/tools.d.ts +0 -3
  69. package/dist/person/tools.js +3 -4
  70. package/dist/person/tui.d.ts +0 -21
  71. package/dist/platform/bounded.d.ts +0 -8
  72. package/dist/platform/bounded.js +0 -8
  73. package/dist/platform/events.d.ts +0 -18
  74. package/dist/platform/events.js +3 -22
  75. package/dist/platform/hub.d.ts +0 -113
  76. package/dist/platform/hub.js +1 -76
  77. package/dist/platform/json.d.ts +0 -3
  78. package/dist/platform/json.js +1 -3
  79. package/dist/platform/storage.d.ts +0 -7
  80. package/dist/platform/storage.js +0 -6
  81. package/dist/platform/version.d.ts +0 -9
  82. package/dist/platform/version.js +0 -8
  83. package/dist/rpc.js +0 -1
  84. package/dist/scheduling/index.d.ts +0 -1
  85. package/dist/scheduling/index.js +0 -1
  86. package/dist/scheduling/later.d.ts +0 -11
  87. package/dist/scheduling/later.js +0 -14
  88. package/dist/scheduling/text.d.ts +0 -1
  89. package/dist/scheduling/text.js +0 -1
  90. package/dist/scheduling/tools.d.ts +0 -1
  91. package/dist/scheduling/tools.js +6 -8
  92. package/dist/skill.d.ts +1 -9
  93. package/dist/skill.js +40 -97
  94. package/dist/tree/cleanup.d.ts +0 -20
  95. package/dist/tree/cleanup.js +0 -26
  96. package/dist/tree/context.d.ts +0 -5
  97. package/dist/tree/context.js +17 -13
  98. package/dist/tree/domain.d.ts +0 -64
  99. package/dist/tree/domain.js +7 -45
  100. package/dist/tree/events.d.ts +5 -36
  101. package/dist/tree/events.js +23 -55
  102. package/dist/tree/forget.d.ts +0 -15
  103. package/dist/tree/forget.js +0 -18
  104. package/dist/tree/group.d.ts +0 -22
  105. package/dist/tree/group.js +6 -39
  106. package/dist/tree/index.d.ts +2 -2
  107. package/dist/tree/index.js +1 -3
  108. package/dist/tree/limits.d.ts +1 -4
  109. package/dist/tree/limits.js +9 -13
  110. package/dist/tree/mascot.d.ts +0 -25
  111. package/dist/tree/mascot.js +1 -32
  112. package/dist/tree/model.d.ts +0 -5
  113. package/dist/tree/model.js +2 -15
  114. package/dist/tree/nudge.d.ts +0 -11
  115. package/dist/tree/nudge.js +3 -16
  116. package/dist/tree/panel.d.ts +0 -8
  117. package/dist/tree/panel.js +3 -16
  118. package/dist/tree/ports.d.ts +5 -139
  119. package/dist/tree/report.d.ts +1 -10
  120. package/dist/tree/report.js +4 -11
  121. package/dist/tree/roster.d.ts +0 -21
  122. package/dist/tree/roster.js +0 -21
  123. package/dist/tree/rpc-handlers.d.ts +6 -1
  124. package/dist/tree/rpc-handlers.js +0 -5
  125. package/dist/tree/rpc.d.ts +9 -109
  126. package/dist/tree/rpc.js +2 -38
  127. package/dist/tree/running.d.ts +0 -6
  128. package/dist/tree/running.js +2 -10
  129. package/dist/tree/send.d.ts +0 -4
  130. package/dist/tree/send.js +4 -31
  131. package/dist/tree/spawn.d.ts +1 -7
  132. package/dist/tree/spawn.js +15 -56
  133. package/dist/tree/standing.d.ts +0 -8
  134. package/dist/tree/standing.js +1 -11
  135. package/dist/tree/status.d.ts +0 -8
  136. package/dist/tree/status.js +1 -9
  137. package/dist/tree/stop.d.ts +2 -9
  138. package/dist/tree/stop.js +58 -25
  139. package/dist/tree/store.d.ts +0 -8
  140. package/dist/tree/store.js +4 -38
  141. package/dist/tree/subtree.d.ts +8 -26
  142. package/dist/tree/subtree.js +37 -35
  143. package/dist/tree/summary.d.ts +0 -4
  144. package/dist/tree/summary.js +0 -8
  145. package/dist/tree/text.d.ts +4 -46
  146. package/dist/tree/text.js +44 -83
  147. package/dist/tree/tools.d.ts +0 -6
  148. package/dist/tree/tools.js +15 -15
  149. package/dist/tree/tui.d.ts +0 -81
  150. package/dist/tree/wake.d.ts +0 -30
  151. package/dist/tree/wake.js +5 -39
  152. package/dist/tui.js +24 -10
  153. package/package.json +8 -9
  154. package/dist/webhook/index.d.ts +0 -5
  155. package/dist/webhook/index.js +0 -3
  156. package/dist/webhook/receiver.d.ts +0 -80
  157. package/dist/webhook/receiver.js +0 -339
  158. package/dist/webhook/subscriptions.d.ts +0 -29
  159. package/dist/webhook/subscriptions.js +0 -52
  160. package/dist/webhook/text.d.ts +0 -23
  161. package/dist/webhook/text.js +0 -82
  162. package/dist/webhook/tools.d.ts +0 -19
  163. package/dist/webhook/tools.js +0 -46
@@ -1,9 +1,5 @@
1
1
  import { type Storage } from "../platform/storage.js";
2
2
  import type { ChildStore, GroupStore, WakeStore } from "./ports.js";
3
- /**
4
- * What the reverse index holds for a child: the sessions above it, its parent first and the top
5
- * session, which courier_spawn did not start, last; each is the parent of the one before it.
6
- */
7
3
  export interface ReverseEntry {
8
4
  readonly ancestors: ReadonlyArray<string>;
9
5
  }
@@ -16,13 +12,9 @@ export declare const toldKey: (sessionID: string) => string;
16
12
  export declare const stoppedKey: (sessionID: string) => string;
17
13
  export declare const nudgedKey: (sessionID: string) => string;
18
14
  export declare const memberKey: (parentID: string, group: string, sessionID: string) => string;
19
- /** The key prefix of a parent's groups, and of one group's members. */
20
15
  export declare const groupsPrefix: (parentID: string) => string;
21
16
  export declare const groupPrefix: (parentID: string, group: string) => string;
22
17
  export declare const wakeKey: (sessionID: string, inboxID: string) => string;
23
- /** The `ChildStore` over the plugin's storage. */
24
18
  export declare function childStore(storage: Storage): ChildStore;
25
- /** The `GroupStore` over the plugin's storage. */
26
19
  export declare function groupStore(storage: Storage): GroupStore;
27
- /** The `WakeStore` over the plugin's storage. */
28
20
  export declare function wakeStore(storage: Storage): WakeStore;
@@ -2,24 +2,14 @@ import { num, obj, str } from "../platform/json.js";
2
2
  import { isStatus } from "../kernel/protocol.js";
3
3
  import { scanAll, scanEntries } from "../platform/storage.js";
4
4
  import { lineageIn } from "./domain.js";
5
- // The tree's storage: the only file that knows its keys and layouts. Older copies and a second server on the same data
6
- // directory read and write the same keys, so a key or a stored value never changes shape.
7
- const PREFIX = "roster/";
8
- /**
9
- * The reverse index: a child's `ReverseEntry` under `roster-by-child/<sessionID>`, written with its
10
- * roster entry; only an index, so `roster/` is scanned for entries lacking one, and confirms a hit.
11
- */
5
+ const ROSTER_PREFIX = "roster/";
12
6
  const BY_CHILD = "roster-by-child/";
13
7
  export function rosterKey(parentID, sessionID) {
14
- return `${PREFIX}${parentID}/${sessionID}`;
8
+ return `${ROSTER_PREFIX}${parentID}/${sessionID}`;
15
9
  }
16
10
  export function reverseKey(sessionID) {
17
11
  return `${BY_CHILD}${sessionID}`;
18
12
  }
19
- /**
20
- * Whether a spawned session has reported since its last prompt, under `report/<sessionID>/`: `prompt`, `settled`, `progress`,
21
- * `told`, `stopped` and `nudged` (the reminder to report); docs/maintainers/architecture.md has each.
22
- */
23
13
  const REPORT_PREFIX = "report/";
24
14
  export const promptKey = (sessionID) => `${REPORT_PREFIX}${sessionID}/prompt`;
25
15
  export const settledKey = (sessionID) => `${REPORT_PREFIX}${sessionID}/settled`;
@@ -35,26 +25,16 @@ const REPORT_KEYS = {
35
25
  stopped: stoppedKey,
36
26
  nudged: nudgedKey,
37
27
  };
38
- /**
39
- * Join groups: each member of a parent's group under `group/<parentID>/<name>/<sessionID>`, with its
40
- * held report or how it left; released and dropped once none is out. docs/maintainers/architecture.md, § Storage.
41
- */
42
28
  const GROUP_PREFIX = "group/";
43
29
  export const memberKey = (parentID, group, sessionID) => `${GROUP_PREFIX}${parentID}/${group}/${sessionID}`;
44
- /** The key prefix of a parent's groups, and of one group's members. */
45
30
  export const groupsPrefix = (parentID) => `${GROUP_PREFIX}${parentID}/`;
46
31
  export const groupPrefix = (parentID, group) => `${groupsPrefix(parentID)}${group}/`;
47
- /**
48
- * The courier messages waiting in a session's inbox, each under `wake/<sessionID>/<inboxID>` until it is delivered, for
49
- * the scheduler to wake a session whose wake was missed. docs/maintainers/architecture.md, § Storage.
50
- */
51
32
  const WAKE_PREFIX = "wake/";
52
33
  export const wakeKey = (sessionID, inboxID) => `${WAKE_PREFIX}${sessionID}/${inboxID}`;
53
34
  function isReverse(value) {
54
35
  const ancestors = value?.ancestors;
55
36
  return Array.isArray(ancestors) && ancestors.length > 0 && ancestors.every((id) => typeof id === "string");
56
37
  }
57
- /** Whether a reverse key's value leads to this entry. */
58
38
  function indexes(value, entry) {
59
39
  return isReverse(value) && value.ancestors[0] === entry.parentID;
60
40
  }
@@ -63,10 +43,9 @@ const timeOf = (value) => {
63
43
  return typeof at === "number" && Number.isFinite(at) ? at : undefined;
64
44
  };
65
45
  const noteOf = (value) => ({ at: timeOf(value), by: obj(value).by, status: obj(value).status, resumesAt: obj(value).resumesAt });
66
- /** The `ChildStore` over the plugin's storage. */
67
46
  export function childStore(storage) {
68
47
  const entry = async (parentID, sessionID) => (await storage.get(rosterKey(parentID, sessionID)));
69
- const all = () => scanAll(storage, PREFIX);
48
+ const all = () => scanAll(storage, ROSTER_PREFIX);
70
49
  const indexedAncestors = async (sessionID) => {
71
50
  const indexed = await storage.get(reverseKey(sessionID));
72
51
  return isReverse(indexed) ? indexed.ancestors : [];
@@ -76,8 +55,6 @@ export function childStore(storage) {
76
55
  let indexed = first;
77
56
  const chain = [];
78
57
  let below = sessionID;
79
- // One read per level, all at once, and one more for the top's own reverse key, in case it was
80
- // recorded after the key that names it; the chain ends where an entry is gone, as a scan's would.
81
58
  for (;;) {
82
59
  const { ancestors } = indexed;
83
60
  const top = ancestors.at(-1);
@@ -107,10 +84,6 @@ export function childStore(storage) {
107
84
  const ancestors = isReverse(above) ? above.ancestors : (await lineage(child.parentID)).map((parent) => parent.parentID);
108
85
  await storage.set(reverseKey(child.sessionID), { ancestors: [child.parentID, ...ancestors] });
109
86
  };
110
- /**
111
- * Drops a reverse key whose entry was not in the roster read, unless that entry is there now:
112
- * another instance may have recorded the child since, and it writes the entry before the key.
113
- */
114
87
  const dropDangling = async (sessionID, value) => {
115
88
  if (isReverse(value) && (await storage.get(rosterKey(value.ancestors[0], sessionID))) !== undefined)
116
89
  return;
@@ -120,7 +93,6 @@ export function childStore(storage) {
120
93
  await Promise.all(Object.values(REPORT_KEYS).map((key) => storage.remove(key(sessionID))));
121
94
  };
122
95
  return {
123
- // The entry is what counts: written first, so a failed index write leaves it to be found by a scan.
124
96
  async record(child) {
125
97
  await storage.set(rosterKey(child.parentID, child.sessionID), { ...child });
126
98
  await index(child).catch(() => undefined);
@@ -134,7 +106,7 @@ export function childStore(storage) {
134
106
  },
135
107
  entry,
136
108
  async children(parentID) {
137
- const entries = await scanAll(storage, `${PREFIX}${parentID}/`);
109
+ const entries = await scanAll(storage, `${ROSTER_PREFIX}${parentID}/`);
138
110
  return entries.sort((a, b) => a.createdAt - b.createdAt);
139
111
  },
140
112
  all,
@@ -172,7 +144,6 @@ export function childStore(storage) {
172
144
  const values = await Promise.all(fields.map((field) => storage.get(REPORT_KEYS[field](sessionID))));
173
145
  return Object.fromEntries(fields.map((field, i) => [field, noteOf(values[i])]));
174
146
  },
175
- // Both of the hub's subscriptions note each event, one of them maybe behind: an earlier note never replaces a later one.
176
147
  async prompted(sessionID, at) {
177
148
  if ((timeOf(await storage.get(promptKey(sessionID))) ?? -Infinity) < at)
178
149
  await storage.set(promptKey(sessionID), { at });
@@ -254,11 +225,8 @@ function readReport(value) {
254
225
  return undefined;
255
226
  return { at, status, message, ...(given.artifacts !== undefined ? { artifacts: given.artifacts } : {}) };
256
227
  }
257
- /** A member as the storage takes it, which asks for an index signature the interface lacks. */
258
228
  const stored = (member) => ({ ...member });
259
- /** The `GroupStore` over the plugin's storage. */
260
229
  export function groupStore(storage) {
261
- /** The members under `prefix`, a key prefix within `GROUP_PREFIX`; a value that is not a member is left out. */
262
230
  const membersUnder = async (prefix) => {
263
231
  const entries = await scanEntries(storage, prefix);
264
232
  return entries.flatMap(({ key, value }) => {
@@ -280,7 +248,6 @@ export function groupStore(storage) {
280
248
  },
281
249
  };
282
250
  }
283
- /** A watched message as stored under its key's session and inbox id; one that cannot be read is left out. */
284
251
  function readWake(sessionID, inboxID, value) {
285
252
  const given = obj(value);
286
253
  const at = num(given.at);
@@ -304,7 +271,6 @@ function readWake(sessionID, inboxID, value) {
304
271
  ...optional("stalled"),
305
272
  };
306
273
  }
307
- /** The `WakeStore` over the plugin's storage. */
308
274
  export function wakeStore(storage) {
309
275
  const under = async (prefix) => {
310
276
  const entries = await scanEntries(storage, prefix);
@@ -1,61 +1,39 @@
1
1
  import type { Plugin } from "@opencode/plugin";
2
- import type { CourierPorts, Pending, RosterEntry, RosterStorage, ShownForm } from "./ports.js";
2
+ import type { CourierPorts, Lister, Pending, RosterEntry, RosterStorage, ShownForm } from "./ports.js";
3
3
  import { reportOf } from "./report.js";
4
4
  import { type Exists } from "./roster.js";
5
- /** A session below the one a subtree is read from, with how many levels below it is: a child is at depth 1. */
6
5
  export interface Below {
7
6
  readonly entry: RosterEntry;
8
7
  readonly depth: number;
8
+ readonly subagent?: true;
9
9
  }
10
- /** What a read drops: the entries past retention at `now`, as `courier_children` drops them; `exists` tells a worktree still there. */
11
10
  export interface Retention {
12
11
  readonly now: number;
13
12
  readonly exists?: Exists;
14
13
  }
15
- /**
16
- * The sessions started from a session, directly or through others, every one of them, read one prefix scan per
17
- * parent, in the order a tree is drawn; with `retention`, without the expired entries and what is under them.
18
- */
19
- export declare function subtree(storage: RosterStorage, rootID: string, retention?: Retention): Promise<{
14
+ export declare function subtree(storage: RosterStorage, rootID: string, retention?: Retention, sessions?: Lister): Promise<{
20
15
  nodes: Below[];
21
16
  }>;
22
17
  type Info = Awaited<ReturnType<Plugin.Context["session"]["get"]>>;
23
- /** A session of a tree as a read looks it up: OpenCode's info of it, if it still knows it, whether it runs, and what it waits on. */
24
18
  export interface Looked extends Below {
25
19
  readonly info: Info | undefined;
26
20
  readonly running: boolean;
27
- /** When its last turn ended, if it does not run and that is known. */
28
21
  readonly endedAt?: number;
29
22
  readonly pending: ReadonlyArray<Pending>;
30
23
  readonly forms: ReadonlyArray<ShownForm>;
31
24
  }
32
- /** A session as a listing looks it up: `Looked` plus its report state, and whether it is live (`isLive`). */
33
25
  export interface Listed extends Looked {
34
26
  readonly report: Awaited<ReturnType<typeof reportOf>>;
35
27
  readonly live: boolean;
36
28
  }
37
29
  export type ReadPorts = Pick<CourierPorts, "storage" | "session" | "pending" | "forms" | "now" | "limits">;
38
- /** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
39
- export declare function lookUp(ports: ReadPorts, rootID: string): Promise<Looked[]>;
40
- /** One session looked up, as `lookUp` does each; `info` when the caller already has it. */
30
+ export declare function lookUp(ports: ReadPorts, rootID: string, subagents?: boolean): Promise<Looked[]>;
41
31
  export declare function look(ports: ReadPorts, node: Below, known?: Info): Promise<Looked>;
42
- /**
43
- * The session a subtree is read from, looked up and listed as its own sessions are, at depth 0: its roster entry when it
44
- * was spawned, else one made of OpenCode's info of it; none when OpenCode does not know it.
45
- */
46
32
  export declare function readTop(ports: ReadPorts, rootID: string): Promise<Listed | undefined>;
47
- /**
48
- * The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
49
- * above them, in the order a tree is drawn (`cut`); `leftOut` counts the rest.
50
- */
51
33
  export declare function readTree(ports: ReadPorts, rootID: string, max?: number): Promise<{
52
34
  kept: Listed[];
53
35
  leftOut: number;
54
36
  }>;
55
- /**
56
- * The subtree under a session as courier_tree lists it (`readTree`): each session as courier_children shows it, plus
57
- * its depth, its parent and its report state; `truncated` counts the older sessions left out.
58
- */
59
37
  export declare function tree(ports: CourierPorts, rootID: string): Promise<{
60
38
  truncated?: number | undefined;
61
39
  sessionID: string;
@@ -66,6 +44,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
66
44
  progressed: boolean;
67
45
  ended: boolean;
68
46
  } | undefined;
47
+ subagent?: true | undefined;
69
48
  parentID: string;
70
49
  depth: number;
71
50
  group: {
@@ -92,6 +71,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
92
71
  progressed: boolean;
93
72
  ended: boolean;
94
73
  } | undefined;
74
+ subagent?: true | undefined;
95
75
  parentID: string;
96
76
  depth: number;
97
77
  group?: undefined;
@@ -115,6 +95,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
115
95
  progressed: boolean;
116
96
  ended: boolean;
117
97
  } | undefined;
98
+ subagent?: true | undefined;
118
99
  parentID: string;
119
100
  depth: number;
120
101
  error: string;
@@ -136,6 +117,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
136
117
  progressed: boolean;
137
118
  ended: boolean;
138
119
  } | undefined;
120
+ subagent?: true | undefined;
139
121
  parentID: string;
140
122
  depth: number;
141
123
  error: string;
@@ -4,42 +4,55 @@ import { isExpired } from "./roster.js";
4
4
  import { turnOf } from "./running.js";
5
5
  import { described } from "./status.js";
6
6
  import { childStore } from "./store.js";
7
- /**
8
- * The sessions started from a session, directly or through others, every one of them, read one prefix scan per
9
- * parent, in the order a tree is drawn; with `retention`, without the expired entries and what is under them.
10
- */
11
- export async function subtree(storage, rootID, retention) {
7
+ async function listedUnder(sessions, parentID) {
8
+ if (!sessions?.list)
9
+ return [];
10
+ const listed = await sessions.list({ parentID }).then((page) => page.data, () => []);
11
+ return listed.map((info) => ({
12
+ sessionID: info.id,
13
+ parentID,
14
+ title: info.title ?? "",
15
+ directory: info.location.directory,
16
+ isolated: false,
17
+ createdAt: info.time.created,
18
+ }));
19
+ }
20
+ export async function subtree(storage, rootID, retention, sessions) {
12
21
  const children = childStore(storage);
13
22
  const byParent = new Map();
14
23
  const seen = new Set([rootID]);
24
+ const kept = (entry) => !seen.has(entry.sessionID) && !(retention && isExpired(entry, retention.now, retention.exists));
15
25
  const expand = async (level, depth) => {
16
26
  if (!level.length)
17
27
  return;
18
- const found = await Promise.all(level.map((parentID) => children.children(parentID)));
28
+ const found = await Promise.all(level.map((parentID) => Promise.all([children.children(parentID), listedUnder(sessions, parentID)])));
19
29
  const next = [];
20
- for (const [index, entries] of found.entries()) {
21
- const kept = entries.filter((entry) => !seen.has(entry.sessionID) && !(retention && isExpired(entry, retention.now, retention.exists)));
22
- for (const entry of kept)
23
- seen.add(entry.sessionID);
24
- if (kept.length)
25
- byParent.set(level[index], kept.map((entry) => ({ entry, depth })));
26
- next.push(...kept.map((entry) => entry.sessionID));
30
+ for (const [index, [entries, listed]] of found.entries()) {
31
+ const spawned = entries.filter(kept).map((entry) => ({ entry, depth }));
32
+ for (const node of spawned)
33
+ seen.add(node.entry.sessionID);
34
+ // session.list also lists native courier children
35
+ const onRoster = new Set(entries.map((entry) => entry.sessionID));
36
+ const subagents = listed.filter((entry) => !onRoster.has(entry.sessionID) && kept(entry)).map((entry) => ({ entry, depth, subagent: true }));
37
+ for (const node of subagents)
38
+ seen.add(node.entry.sessionID);
39
+ const all = [...spawned, ...subagents].sort((a, b) => a.entry.createdAt - b.entry.createdAt);
40
+ if (all.length)
41
+ byParent.set(level[index], all);
42
+ next.push(...all.map((node) => node.entry.sessionID));
27
43
  }
28
44
  return expand(next, depth + 1);
29
45
  };
30
46
  await expand([rootID], 1);
31
47
  return { nodes: drawn(byParent, rootID) };
32
48
  }
33
- /** The nodes in the order a tree is drawn: each followed by what it started. */
34
49
  function drawn(byParent, parentID) {
35
50
  return (byParent.get(parentID) ?? []).flatMap((node) => [node, ...drawn(byParent, node.entry.sessionID)]);
36
51
  }
37
- /** Every session under a session, without those past retention, each looked up, in the order a tree is drawn. */
38
- export async function lookUp(ports, rootID) {
39
- const { nodes } = await subtree(ports.storage, rootID, { now: ports.now() });
52
+ export async function lookUp(ports, rootID, subagents = false) {
53
+ const { nodes } = await subtree(ports.storage, rootID, { now: ports.now() }, subagents ? ports.session : undefined);
40
54
  return Promise.all(nodes.map((node) => look(ports, node)));
41
55
  }
42
- /** One session looked up, as `lookUp` does each; `info` when the caller already has it. */
43
56
  export async function look(ports, node, known) {
44
57
  const [info, pending, forms] = await Promise.all([
45
58
  known ?? ports.session.get({ sessionID: node.entry.sessionID }).catch(() => undefined),
@@ -48,13 +61,9 @@ export async function look(ports, node, known) {
48
61
  ]);
49
62
  const turn = info === undefined ? undefined : await turnOf(ports.session, node.entry.sessionID, info.time);
50
63
  const running = turn?.running ?? false;
51
- // A form outlives its session's turn only when its withdrawal was missed, as while no instance followed the events.
64
+ // a form outlives its turn only if its withdrawal event was missed
52
65
  return { ...node, info, running, ...(turn?.endedAt !== undefined ? { endedAt: turn.endedAt } : {}), pending, forms: running ? forms : [] };
53
66
  }
54
- /**
55
- * The session a subtree is read from, looked up and listed as its own sessions are, at depth 0: its roster entry when it
56
- * was spawned, else one made of OpenCode's info of it; none when OpenCode does not know it.
57
- */
58
67
  export async function readTop(ports, rootID) {
59
68
  const info = await ports.session.get({ sessionID: rootID }).catch(() => undefined);
60
69
  if (!info)
@@ -73,32 +82,25 @@ export async function readTop(ports, rootID) {
73
82
  const report = await reportOf(children, rootID).catch(() => undefined);
74
83
  return { ...looked, report, live: isLive(looked.running, looked.pending.length + looked.forms.length, report) };
75
84
  }
76
- /**
77
- * The listing of at most `max` of the sessions under a session: the live ones first, then the newest, with what is
78
- * above them, in the order a tree is drawn (`cut`); `leftOut` counts the rest.
79
- */
80
85
  export async function readTree(ports, rootID, max = ports.limits.maxTotal) {
81
86
  const children = childStore(ports.storage);
82
- const all = await Promise.all((await lookUp(ports, rootID)).map(async (node) => {
83
- const report = await reportOf(children, node.entry.sessionID).catch(() => undefined);
87
+ const all = await Promise.all((await lookUp(ports, rootID, true)).map(async (node) => {
88
+ const report = node.subagent ? undefined : await reportOf(children, node.entry.sessionID).catch(() => undefined);
84
89
  return { ...node, report, live: isLive(node.info ? node.running : undefined, node.pending.length + node.forms.length, report) };
85
90
  }));
86
91
  const listing = cut(all.map((node) => ({ sessionID: node.entry.sessionID, parentID: node.entry.parentID, createdAt: node.entry.createdAt, live: node.live, node })), max);
87
92
  return { kept: listing.kept.map(({ node }) => node), leftOut: listing.leftOut };
88
93
  }
89
- /**
90
- * The subtree under a session as courier_tree lists it (`readTree`): each session as courier_children shows it, plus
91
- * its depth, its parent and its report state; `truncated` counts the older sessions left out.
92
- */
93
94
  export async function tree(ports, rootID) {
94
95
  const { kept, leftOut } = await readTree(ports, rootID);
95
96
  const children = childStore(ports.storage);
96
- const listed = await Promise.all(kept.map(async ({ entry, depth, report }) => {
97
- const [info, last] = await Promise.all([described(ports, entry), lastStatus(children, entry.sessionID).catch(() => undefined)]);
97
+ const listed = await Promise.all(kept.map(async ({ entry, depth, report, subagent }) => {
98
+ const [info, last] = await Promise.all([described(ports, entry), report ? lastStatus(children, entry.sessionID).catch(() => undefined) : undefined]);
98
99
  return {
99
100
  ...info,
100
101
  parentID: entry.parentID,
101
102
  depth,
103
+ ...(subagent ? { subagent } : {}),
102
104
  ...(report
103
105
  ? { report: { owes: report.owes, progressed: report.progressed, ended: report.ended, ...(last ? { status: last } : {}) } }
104
106
  : {}),
@@ -1,7 +1,3 @@
1
1
  import type { CourierPorts } from "./ports.js";
2
2
  import type { Summary } from "./rpc.js";
3
- /**
4
- * Every session started under a session, counted by what it is doing (the session itself too, when it is stalled), and its
5
- * open join groups, those of its own children, with how far each has reported.
6
- */
7
3
  export declare function summary(ports: CourierPorts, sessionID: string): Promise<Summary>;
@@ -2,10 +2,6 @@ import { groupStandings } from "./group.js";
2
2
  import { groupStore } from "./store.js";
3
3
  import { lookUp } from "./subtree.js";
4
4
  import { stalledAmong } from "./wake.js";
5
- /**
6
- * What a session is doing: waiting on a request it made or a form it shows, stalled, running, or idle; undefined when
7
- * OpenCode no longer knows it. A message waiting in its inbox is no turn (`runs`); a stalled one reads stalled.
8
- */
9
5
  function activity({ info, running, pending, forms, entry }, stalled) {
10
6
  if (!info)
11
7
  return undefined;
@@ -15,10 +11,6 @@ function activity({ info, running, pending, forms, entry }, stalled) {
15
11
  return "stalled";
16
12
  return running ? "running" : "idle";
17
13
  }
18
- /**
19
- * Every session started under a session, counted by what it is doing (the session itself too, when it is stalled), and its
20
- * open join groups, those of its own children, with how far each has reported.
21
- */
22
14
  export async function summary(ports, sessionID) {
23
15
  const [looked, standings] = await Promise.all([lookUp(ports, sessionID), groupStandings(groupStore(ports.storage), sessionID)]);
24
16
  const stalled = await stalledAmong(ports, [sessionID, ...looked.map((one) => one.entry.sessionID)]);
@@ -1,25 +1,13 @@
1
1
  import { type ModelRef } from "./domain.js";
2
2
  import type { Artifacts, CleanupResult, ExecutionError, HeldReport, LeftMember, Limits, Sent, Standing } from "./ports.js";
3
- /** The branch an isolated child commits its work on, and names in the `branch` of its report. */
4
3
  export declare const childBranch: (sessionID: string) => string;
5
- /** What a spawned session at `depth` is told; `depth` 1 is a child of a session nobody spawned. `branch` is for an isolated one. */
6
4
  export declare function childBrief(parentID: string, task: string, depth: number, limits: Limits, branch?: string): string;
7
- /** How every role part begins, in the role entry and in the part the context hook adds without one, so no second copy is added. */
8
5
  export declare const ROLE_PREFIX = "opencode-courier role:";
9
- /** The key of the session instruction entry carrying a spawned session's role part, `<context key="role">` in its system prompt. */
10
6
  export declare const ROLE_KEY = "role";
11
- /**
12
- * The system part naming a session's place in its tree: `depth` 0 for a session nobody spawned that
13
- * has started one, `null` for one that has started none, which is pointed to the skill. The same on every request.
14
- */
15
7
  export declare function rolePart(depth: number | null, limits: Limits): string;
16
- /** The key of the session instruction entry on a parent listing the sessions it started, `<context key="standing">`. */
17
8
  export declare const STANDING_KEY = "standing";
18
- /** How every standing entry begins. */
19
9
  export declare const STANDING_PREFIX = "opencode-courier standing:";
20
- /** How many children the entry lists; older ones past that are counted. */
21
10
  export declare const STANDING_MAX = 10;
22
- /** What a child is doing, as its parent's standing entry says it; `reported` carries the status. */
23
11
  export type ChildState = {
24
12
  readonly kind: "started" | "working" | "ended" | "failed" | "interrupted" | "stopped";
25
13
  readonly progressed?: boolean;
@@ -28,37 +16,24 @@ export type ChildState = {
28
16
  readonly status: string;
29
17
  readonly progressed?: boolean;
30
18
  };
31
- /** One child as the standing entry lists it. */
32
19
  export interface StandingRow {
33
20
  readonly sessionID: string;
34
21
  readonly title: string;
35
22
  readonly state: ChildState;
36
- /** Its join group and its standing there, `unknown` when that could not be read. */
37
23
  readonly group?: {
38
24
  readonly name: string;
39
25
  readonly standing: Standing | "unknown";
40
26
  };
41
27
  }
42
- /**
43
- * The standing entry: one line per session the parent started, oldest first, with its state and its place in its join group,
44
- * in the same words for the same state, so an unchanged standing is not put again.
45
- */
46
28
  export declare function standingEntry(rows: ReadonlyArray<StandingRow>): string;
47
29
  export declare const depthRefusal: (depth: number, maxDepth: number) => string;
48
30
  export declare const childrenRefusal: (running: number, maxChildren: number) => string;
49
- export declare const totalRefusal: (running: number, maxTotal: number) => string;
31
+ export declare const totalRefusal: (running: number, maxTotal: number, yours: number) => string;
50
32
  export declare const modelFormatRefusal: (given: string) => string;
51
- /** The model is not among those OpenCode lists; `named` says the caller named it, rather than inherited it and named a variant. */
52
33
  export declare function unknownModelRefusal(model: ModelRef, known: ReadonlyArray<Pick<ModelRef, "providerID" | "id">>, named: boolean): string;
53
34
  export declare function unknownVariantRefusal(model: ModelRef, variants: ReadonlyArray<string>): string;
54
- /** A variant was named, but the model it would apply to could not be read: the named agent's, else the caller's own or OpenCode's default. */
55
35
  export declare function variantWithoutModelRefusal(variant: string, agent?: string): string;
56
- /**
57
- * The body of a report: its text, then the artifacts in a fixed layout, the same for every report,
58
- * so a parent finds the branch, the commits, the files and the checks without reading prose.
59
- */
60
36
  export declare function reportBody(message: string, artifacts: Artifacts | undefined): string;
61
- /** courier_spawn's refusal of `native: true` with `isolate: true`: OpenCode gives a session a worktree or a parent, never both. */
62
37
  export declare const nativeIsolateRefusal: () => string;
63
38
  export declare function spawnText(child: {
64
39
  readonly sessionID: string;
@@ -68,25 +43,23 @@ export declare function spawnText(child: {
68
43
  readonly groupError?: string;
69
44
  readonly fromParent?: boolean;
70
45
  readonly uncommitted?: boolean;
71
- /** The model it runs on, when that is not the caller's own. */
72
46
  readonly model?: string;
47
+ readonly belowTop?: boolean;
73
48
  }): string;
74
- /** courier_send's result: for a message to the session that started the sender, whether it was its report; below it, the status dropped. */
75
49
  export declare function sendText(sessionID: string, sent?: Pick<Sent, "status" | "report" | "held" | "dropped">): string;
76
50
  export declare const childrenText: (listed: ReadonlyArray<unknown>) => string;
77
51
  export declare function cleanupText(result: CleanupResult): string;
78
- /** courier_tree's result: the sessions under a session, drawn depth first, as JSON. */
79
52
  export declare function treeText(tree: {
80
53
  readonly sessions: ReadonlyArray<unknown>;
81
54
  readonly truncated?: number;
82
55
  }): string;
83
- /** What courier_stop did to each session, and to the messages and worktrees. */
84
56
  export declare function stopText(result: {
85
57
  readonly sessionID: string;
86
58
  readonly stopped: ReadonlyArray<{
87
59
  readonly sessionID: string;
88
60
  readonly outcome: string;
89
61
  readonly error?: string;
62
+ readonly subagent?: true;
90
63
  }>;
91
64
  readonly cancelled: ReadonlyArray<string>;
92
65
  readonly cleanup?: ReadonlyArray<CleanupResult | {
@@ -96,29 +69,14 @@ export declare function stopText(result: {
96
69
  }>;
97
70
  readonly told?: string;
98
71
  }): string;
99
- /** What the parent of a stopped session is told when the stop was ordered by a session above both. */
100
72
  export declare const stoppedNotice: (title: string, by: string) => string;
101
- /** Why a worktree in this state must be kept, or undefined when removing it loses nothing. */
102
73
  export declare function keepReason(state: {
103
74
  readonly changes: readonly string[];
104
75
  readonly commits: readonly string[];
105
76
  }): string | undefined;
106
- /**
107
- * What the parent is told when a session it started failed its turn; `at` is when the failure happened, which the provider's
108
- * retry time (`retryAfterMs`) counts from. Message the child before that and it fails again, so the notice says to wait.
109
- */
110
- export declare function failureNotice(title: string, error: ExecutionError, group?: string, at?: number): string;
111
- /** The one reminder a spawned session gets as it is about to end its turn owing its parent a report; it carries on in the same turn. */
77
+ export declare function failureNotice(title: string, error: ExecutionError, group?: string, at?: number, below?: ReadonlyArray<string>): string;
112
78
  export declare function reportReminder(parentID: string): string;
113
- /**
114
- * What the parent is told when a session it started ends its turn without a report to it;
115
- * `progressed` when it messaged the parent without a status since its last prompt.
116
- */
117
79
  export declare function silentNotice(title: string, lastText: string | undefined, progressed?: boolean, group?: string): string;
118
- /**
119
- * What the parent gets when a group it named is released: every report, with its status, text and artifacts in
120
- * `reportBody`'s layout, and the members that left without one, in the order they joined.
121
- */
122
80
  export declare function groupNotice(group: string, reports: ReadonlyArray<HeldReport & {
123
81
  readonly sessionID: string;
124
82
  readonly title: string;