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,61 +1,36 @@
1
- /**
2
- * The memory's pure rules, on plain data: the option, a record's shape, each part's status, the split line and its revisions,
3
- * its reads and pin, the retention and how a refresh keeps the parts the roster has forgotten. Imports nothing of the plugin's but `kernel/` and `platform/json.ts`.
4
- */
5
1
  import { STATUSES } from "../kernel/protocol.js";
6
2
  import { obj } from "../platform/json.js";
7
- /** How many unpinned records a project keeps: those with a lesson first, then the newest by `movedAt`. */
8
3
  export const MAX_RECORDS = 30;
9
- /** Unpinned records that last moved longer ago than this are dropped when the plugin loads. */
10
4
  export const RETENTION_MS = 90 * 24 * 60 * 60_000;
11
- /** A record's `split` while its top has stated no split line. */
12
5
  export const NOT_STATED = "not stated";
13
- /** The most characters a lesson holds: courier_note refuses what is longer, and a record written by hand is clipped when read. */
14
6
  export const LESSON_MAX = 300;
15
- /**
16
- * How a part stands: its last report's status, how its wait ended without one (`failed`, `interrupted`), `stopped` by
17
- * courier_stop, `gone` before it settled, `stalled` on a message it will not take, or `out`: it owes a report.
18
- */
19
7
  export const PART_STATUSES = [...STATUSES, "interrupted", "stopped", "gone", "stalled", "out"];
20
8
  export const isPartStatus = (value) => typeof value === "string" && PART_STATUSES.includes(value);
21
- /** Whether a part has settled, as every part of a finished tree has: it neither owes a report nor is stalled. */
22
9
  export const isSettled = (status) => status !== "out" && status !== "stalled";
23
- /** How many revisions of the split line a record keeps, the newest. */
24
10
  export const MAX_REVISIONS = 3;
25
- /** The `memory` option: whether records are kept and listed; on unless `false`. */
26
11
  export function readMemory(options) {
27
12
  const value = obj(options).memory;
28
13
  if (value === undefined || typeof value === "boolean")
29
14
  return { memory: value !== false, problems: [] };
30
15
  return { memory: true, problems: [`memory must be true or false, not ${JSON.stringify(value)}; using true`] };
31
16
  }
32
- /** A line beginning with `split:` after any markdown (whitespace, emphasis, heading, quote and list marks). */
33
17
  const STARTS_SPLIT = /^[\s*_#>`-]*split:/i;
34
- /** A line holding `split:` as a word. */
35
18
  const HOLDS_SPLIT = /\bsplit:/i;
36
- /** The split line a text states, as the real-model checker reads it: a line beginning with `split:` after any markdown, else one holding it. */
37
19
  export function splitLineOf(text) {
38
20
  const lines = text.split("\n");
39
21
  return (lines.find((line) => STARTS_SPLIT.test(line)) ?? lines.find((line) => HOLDS_SPLIT.test(line)))?.trim();
40
22
  }
41
- /** Whether a split line is stated, beginning with `split:` after any markdown, not a passing mention nor `not stated`. */
42
23
  export const isStated = (line) => STARTS_SPLIT.test(line);
43
- /** The record's `split` once `text` is read: kept once stated; a mention gives way to the first stated line; `not stated` to either. */
44
24
  export function splitOf(recorded, text) {
45
25
  if (isStated(recorded))
46
26
  return recorded;
47
27
  const found = splitLineOf(text);
48
28
  return found !== undefined && (recorded === NOT_STATED || isStated(found)) ? found : recorded;
49
29
  }
50
- /** Every line of a text that begins with `split:` after any markdown, trimmed: a passing mention of a split is none. */
51
30
  export const splitLinesOf = (text) => text
52
31
  .split("\n")
53
32
  .filter((line) => STARTS_SPLIT.test(line))
54
33
  .map((line) => line.trim());
55
- /**
56
- * The lines after the one recorded last: after its reply, else (compacted, or unread by an overlapping refresh) those stated since its
57
- * time; without a revision, after `split`'s first statement; all when neither is found.
58
- */
59
34
  function statedSince(previous, stated) {
60
35
  const last = previous.revisions?.at(-1);
61
36
  const words = (line) => splitProse(line.text);
@@ -68,10 +43,6 @@ function statedSince(previous, stated) {
68
43
  return stated.filter((line) => line.at !== undefined && line.at > last.at);
69
44
  return stated.slice(stated.findLastIndex((line) => words(line) === splitProse(last.text)) + 1);
70
45
  }
71
- /**
72
- * The revisions once the split lines `stated` are read at a refresh at `at` with `parts` parts: those `statedSince` the line recorded
73
- * last, each appended when its words differ from the line before, so a re-read appends nothing.
74
- */
75
46
  export function revise(previous, stated, at, parts) {
76
47
  const recorded = splitProse(previous.revisions?.at(-1)?.text ?? previous.split);
77
48
  const { added } = statedSince(previous, stated).reduce(({ last, added }, line) => {
@@ -81,28 +52,18 @@ export function revise(previous, stated, at, parts) {
81
52
  const revisions = [...(previous.revisions ?? []), ...added].slice(-MAX_REVISIONS);
82
53
  return revisions.length ? revisions : undefined;
83
54
  }
84
- /**
85
- * The split and revisions once the stated line `found` replaces a mention: when `found` is among the revisions (recorded beside the
86
- * mention before #294), the first of them was stated first and is `split`, the rest stay; else `found` is, beside them all.
87
- */
88
55
  export function replacingMention(found, revisions) {
89
56
  const [first, ...rest] = revisions ?? [];
90
57
  if (!first || !revisions.some((one) => splitProse(one.text) === splitProse(found)))
91
58
  return { split: found, ...(first ? { revisions } : {}) };
92
59
  return { split: first.text, ...(rest.length ? { revisions: rest } : {}) };
93
60
  }
94
- /** Heading, quote and list marks before a line's words, each with the space markdown puts after it, unlike the `#` of `#282`. */
95
61
  const BLOCK_MARKS = /^(?:\s|#+\s|>\s|[-+*]\s|\d+[.)]\s)*/;
96
- /** A bold or bold italic span within a line, opening at a word's start and closing at its end, unlike the asterisks of `2**3` or a `**` beside a slash. */
97
62
  const BOLD = /(?<=^|[\s(["'])(\*\*\*?)([^\s*/][^*]*?)(?<![\s(/])\1(?=$|[\s.,;:!?)\]"'])/gu;
98
- /** An italic span opens only before a letter, digit, quote or code span, which tells it from the globs of `*.ts and *.md` or `src/*.ts`. */
99
63
  const ITALIC = /(?<=^|[\s(["'])\*([\p{L}\p{N}\0"'][^*]*?)(?<![\s(/])\*(?=$|[\s.,;:!?)\]"'])/gu;
100
- /** A label the line opens with in underscores or a code span (`_split:_`, `` `split:` ``), which its colon tells from `__init__`. */
101
64
  const LABEL = /^(__?|`)([^\s_`][^_`]*:)\1(?=\s)/;
102
- /** A code span, whose asterisks are its words', and the stand-in it is masked by while emphasis is taken off. */
103
65
  const CODE_SPAN = /`[^`]*`/g;
104
66
  const MASKED = /\0(\d+)\0/g;
105
- /** Marks taken off a line they wrap whole, with what its words must open with; `*` may recur inside as a glob's, the others not at all. */
106
67
  const STARS = [
107
68
  ["***", /[^\s*/]/],
108
69
  ["**", /[^\s*/]/],
@@ -123,7 +84,6 @@ const unwrapped = (line, marks) => {
123
84
  const found = marks.find(wraps);
124
85
  return found ? inner(found[0]).trim() : line;
125
86
  };
126
- /** A split line as the listing shows it: one line, without its block marks, the asterisks wrapping it or a span in it, nor an underscore or code span wrapping it. */
127
87
  export function splitProse(split) {
128
88
  const code = [];
129
89
  const masked = oneLine(split.replaceAll("\0", ""))
@@ -133,7 +93,6 @@ export function splitProse(split) {
133
93
  const prose = unwrapped(masked, STARS).replace(BOLD, "$2").replace(ITALIC, "$1");
134
94
  return unwrapped(prose.replace(MASKED, (_, i) => code[Number(i)]), WRAPS);
135
95
  }
136
- /** A part's status by `Standing`; one whose report state is not known is still out. */
137
96
  export function partStatus({ stopped, stalled, report }) {
138
97
  if (stopped)
139
98
  return "stopped";
@@ -145,65 +104,50 @@ export function partStatus({ stopped, stalled, report }) {
145
104
  return report.ended;
146
105
  return report.status ?? "done";
147
106
  }
148
- /**
149
- * A part whose session could not be read, from what was read of it and its last record: its title, duration and cost as
150
- * last recorded; once OpenCode no longer knows it (`gone`), settled as read, else as last recorded, else `gone`.
151
- */
107
+ export function subagentStatus({ running, stalled, outcome }) {
108
+ if (stalled)
109
+ return "stalled";
110
+ if (running)
111
+ return "out";
112
+ return outcome === "failed" || outcome === "interrupted" ? outcome : "done";
113
+ }
152
114
  export function unreadPart(read, last, gone) {
153
115
  const settledLast = last && isSettled(last.status) ? last.status : "gone";
154
116
  const status = !gone || isSettled(read.status) ? read.status : settledLast;
155
117
  const { title, duration, cost } = last ?? read;
156
118
  return { ...read, title, status, ...(duration !== undefined ? { duration } : {}), ...(cost !== undefined ? { cost } : {}) };
157
119
  }
158
- /**
159
- * The parts of a refreshed record: those recorded before, in their order, each as `current` has it (read now, or found
160
- * gone), else as last recorded, then those first seen now, in the order the tree is drawn.
161
- */
162
120
  export function mergeParts(previous, current) {
163
121
  const now = new Map(current.map((part) => [part.sessionID, part]));
164
122
  const kept = previous.map((part) => now.get(part.sessionID) ?? part);
165
123
  const known = new Set(previous.map((part) => part.sessionID));
166
124
  return [...kept, ...current.filter((part) => !known.has(part.sessionID))];
167
125
  }
168
- /** `recalled` after one more call at `at`, the top's first call noted when `byTop`. */
169
126
  export function noteRecall(recalled, byTop, at) {
170
127
  const calls = (recalled?.calls ?? 0) + 1;
171
128
  const first = recalled?.at ?? (byTop ? at : undefined);
172
129
  return first === undefined ? { calls } : { at: first, calls };
173
130
  }
174
- /** A text as the one line a lesson is: whitespace and line breaks collapsed to spaces, trimmed. */
175
131
  export const oneLine = (text) => text.replace(/\s+/g, " ").trim();
176
- /**
177
- * A hand-written lesson's text as one line, clipped at `LESSON_MAX`; `clipped` is the length it had when it was clipped.
178
- * courier_note does not clip: it refuses a line over `LESSON_MAX` (`note.ts`).
179
- */
180
132
  export function lessonText(text) {
181
133
  const line = oneLine(text);
182
134
  if (line.length <= LESSON_MAX)
183
135
  return { text: line };
184
136
  return { text: line.slice(0, LESSON_MAX).trimEnd(), clipped: line.length };
185
137
  }
186
- /** One reader tree's `reads` after courier_recall listed the record or gave it in full at `at`. */
187
138
  export function noteRead(reads, kind, at) {
188
139
  return { shown: (reads?.shown ?? 0) + (kind === "shown" ? 1 : 0), read: (reads?.read ?? 0) + (kind === "read" ? 1 : 0), at: Math.max(reads?.at ?? 0, at) };
189
140
  }
190
- /** The reads of every reader tree as one: the counts summed, `at` the latest; none without a reader. */
191
141
  export function sumReads(reads) {
192
142
  if (!reads.length)
193
143
  return undefined;
194
144
  return reads.reduce((sum, one) => ({ shown: sum.shown + one.shown, read: sum.read + one.read, at: Math.max(sum.at, one.at) }));
195
145
  }
196
- /**
197
- * The records a project keeps at `now`: every pinned one, whatever its age, and of the rest the first `MAX_RECORDS` by
198
- * whether each has a lesson, then by `movedAt`, none older than `RETENTION_MS`; the rest are dropped.
199
- */
200
146
  export function keep(records, now) {
201
147
  const pinned = records.filter((record) => record.pinned);
202
- // Aged out first, so that a stale record takes no slot from a live one behind it in the order.
203
148
  const live = records.filter((record) => !record.pinned && now - record.movedAt <= RETENTION_MS);
204
149
  const ordered = live.toSorted((a, b) => Number(Boolean(b.lesson)) - Number(Boolean(a.lesson)) || b.movedAt - a.movedAt);
205
150
  const kept = [...pinned, ...ordered.slice(0, MAX_RECORDS)];
206
151
  return { kept, dropped: records.filter((record) => !kept.includes(record)) };
207
152
  }
208
- /** Records as courier_recall and the memory view list them: the pinned first, then by when each last moved, newest first. */
209
153
  export const pinnedFirst = (records) => records.toSorted((a, b) => Number(Boolean(b.pinned)) - Number(Boolean(a.pinned)) || b.movedAt - a.movedAt);
@@ -1,6 +1,5 @@
1
1
  import type { EventModule, EventPorts } from "../platform/events.js";
2
2
  import type { MemoryState, RefreshPorts } from "./ports.js";
3
- /** OpenCode's `session.execution.*` events, as the memory reads them. */
4
3
  interface Execution {
5
4
  readonly id: string;
6
5
  readonly created?: number;
@@ -9,18 +8,8 @@ interface Execution {
9
8
  readonly reason?: string;
10
9
  };
11
10
  }
12
- /** The ports the event module runs on: the watcher's, which every hub-v2 member joins with. */
13
11
  export type MemoryEventPorts = RefreshPorts & EventPorts;
14
- /**
15
- * The top of the tree a session is in: by the reverse index for a spawned one, else the session itself when it called
16
- * courier_recall or has started sessions; undefined for a session in no tree.
17
- */
18
12
  export declare function topOf(ports: Pick<RefreshPorts, "storage">, state: Pick<MemoryState, "tops">, sessionID: string): Promise<string | undefined>;
19
- /** A turn ended in a tree: its record is written again, once however many subscriptions see the event. Returns the top refreshed. */
20
13
  export declare function noteTurnEnd(ports: RefreshPorts, state: MemoryState, event: Execution): Promise<string | undefined>;
21
- /**
22
- * Keeps each tree's record current: after the tree's and the person's notices of a turn's end have gone out, and the
23
- * report state it settled is written, the record of the tree it belongs to is rebuilt. A failure is logged, never thrown.
24
- */
25
14
  export declare function memoryEvents(state: MemoryState): EventModule<MemoryEventPorts>;
26
15
  export {};
@@ -3,10 +3,6 @@ import { childrenOf, endsIdle, indexedLineage } from "../tree/index.js";
3
3
  import { refresh } from "./record.js";
4
4
  const TURN_END_TYPES = ["session.execution.succeeded", "session.execution.failed", "session.execution.interrupted"];
5
5
  const TURN_ENDS = new Set(TURN_END_TYPES);
6
- /**
7
- * The top of the tree a session is in: by the reverse index for a spawned one, else the session itself when it called
8
- * courier_recall or has started sessions; undefined for a session in no tree.
9
- */
10
6
  export async function topOf(ports, state, sessionID) {
11
7
  const chain = await indexedLineage(ports.storage, sessionID);
12
8
  if (chain.length)
@@ -15,16 +11,13 @@ export async function topOf(ports, state, sessionID) {
15
11
  return sessionID;
16
12
  return (await childrenOf(ports.storage, sessionID)).length ? sessionID : undefined;
17
13
  }
18
- /** A turn ended in a tree: its record is written again, once however many subscriptions see the event. Returns the top refreshed. */
19
14
  export async function noteTurnEnd(ports, state, event) {
20
15
  const top = await topOf(ports, state, event.data.sessionID);
21
16
  if (top === undefined)
22
17
  return undefined;
23
- // A shutdown's interruption is resumed on the next start, unless a plain decline ended the turn: its transcript ends on an aborted step.
18
+ // a plain permission decline also reports "shutdown" (api-notes)
24
19
  if (event.data.reason === "shutdown" && !endsIdle(await ports.session.context({ sessionID: event.data.sessionID }).catch(() => [])))
25
20
  return undefined;
26
- // Claimed after the lookup, so other sessions' turns do not crowd `seen`; released on a failure, as a write is
27
- // the same twice, so the hub's other subscription may still make it.
28
21
  if (!claim(state.seen, event.id))
29
22
  return undefined;
30
23
  try {
@@ -36,10 +29,6 @@ export async function noteTurnEnd(ports, state, event) {
36
29
  }
37
30
  return top;
38
31
  }
39
- /**
40
- * Keeps each tree's record current: after the tree's and the person's notices of a turn's end have gone out, and the
41
- * report state it settled is written, the record of the tree it belongs to is rebuilt. A failure is logged, never thrown.
42
- */
43
32
  export function memoryEvents(state) {
44
33
  return {
45
34
  types: TURN_END_TYPES,
@@ -1,10 +1,5 @@
1
1
  import type { MemoryPorts } from "./ports.js";
2
- /** What forgetting did: the record is gone, only its lesson is, or there was nothing to forget. */
3
2
  export type Forgotten = "record" | "lesson" | "none";
4
- /**
5
- * Forgets the record of the tree under `sessionID` in the ports' project, as the memory view's `x` does, or with
6
- * `lesson` only its lesson, writing the record back without it. Whatever the memory option says: it only removes.
7
- */
8
3
  export declare function forget(ports: Pick<MemoryPorts, "storage" | "projectID">, sessionID: string, { lesson }?: {
9
4
  lesson?: boolean | undefined;
10
5
  }): Promise<Forgotten>;
@@ -1,8 +1,4 @@
1
1
  import { memoryStore } from "./store.js";
2
- /**
3
- * Forgets the record of the tree under `sessionID` in the ports' project, as the memory view's `x` does, or with
4
- * `lesson` only its lesson, writing the record back without it. Whatever the memory option says: it only removes.
5
- */
6
2
  export async function forget(ports, sessionID, { lesson = false } = {}) {
7
3
  const store = memoryStore(ports.storage);
8
4
  const record = await store.get(ports.projectID, sessionID);
@@ -1,5 +1,3 @@
1
- // What the rest of the plugin uses of the memory: its tools, event module, RPC, option and sweep for `src/index.ts`,
2
- // and the record's types.
3
1
  export { LESSON_MAX, MAX_RECORDS, NOT_STATED, readMemory, RETENTION_MS } from "./domain.js";
4
2
  export { memoryEvents } from "./events.js";
5
3
  export { lessonsOf } from "./lessons.js";
@@ -1,12 +1,4 @@
1
1
  import { type Entry } from "../tree/index.js";
2
2
  import type { MemoryPorts } from "./ports.js";
3
- /**
4
- * The `lessons` entry of this project, as `courier_spawn` writes it to a spawned session before its prompt and to a root at
5
- * its first spawn: none with the memory option off, or while no record of the project holds a lesson.
6
- */
7
3
  export declare function lessonsOf(ports: MemoryPorts): Promise<ReadonlyArray<Entry>>;
8
- /**
9
- * Puts the project's lessons entry again, once a lesson was written, on the writer's tree's top, the top's current children
10
- * below `maxDepth` and the writer, each only where it differs from the entry stored; a failure is logged. Returns the sessions put.
11
- */
12
- export declare function relesson(ports: MemoryPorts, writerID: string, top: string): Promise<string[]>;
4
+ export declare function relesson(ports: MemoryPorts, top: string): Promise<string[]>;
@@ -3,21 +3,13 @@ import { childrenOf } from "../tree/index.js";
3
3
  import { pinnedFirst } from "./domain.js";
4
4
  import { memoryStore } from "./store.js";
5
5
  import { LESSONS_KEY, lessonsEntry } from "./text.js";
6
- /**
7
- * The `lessons` entry of this project, as `courier_spawn` writes it to a spawned session before its prompt and to a root at
8
- * its first spawn: none with the memory option off, or while no record of the project holds a lesson.
9
- */
10
6
  export async function lessonsOf(ports) {
11
7
  if (!ports.memory)
12
8
  return [];
13
9
  const value = lessonsEntry(pinnedFirst(await memoryStore(ports.storage).ofProject(ports.projectID)));
14
10
  return value === undefined ? [] : [{ key: LESSONS_KEY, value }];
15
11
  }
16
- /**
17
- * Puts the project's lessons entry again, once a lesson was written, on the writer's tree's top, the top's current children
18
- * below `maxDepth` and the writer, each only where it differs from the entry stored; a failure is logged. Returns the sessions put.
19
- */
20
- export async function relesson(ports, writerID, top) {
12
+ export async function relesson(ports, top) {
21
13
  const entries = ports.session.instructions?.entry;
22
14
  if (!entries)
23
15
  return [];
@@ -29,7 +21,7 @@ export async function relesson(ports, writerID, top) {
29
21
  if (!entry)
30
22
  return [];
31
23
  const children = ports.maxDepth > 1 ? await childrenOf(ports.storage, top) : [];
32
- sessions = [...new Set([top, ...children.map((child) => child.sessionID), writerID])];
24
+ sessions = [top, ...children.map((child) => child.sessionID)];
33
25
  }
34
26
  catch (error) {
35
27
  ports.log(`courier_note: the lessons entry could not be read again: ${String(error)}`);
@@ -45,7 +37,6 @@ export async function relesson(ports, writerID, top) {
45
37
  return true;
46
38
  }
47
39
  catch (error) {
48
- // A child on the roster that OpenCode no longer knows is a normal path, not one to log.
49
40
  if (!isNotFound(error))
50
41
  ports.log(`courier_note: the lessons entry of ${sessionID} could not be written: ${String(error)}`);
51
42
  return false;
@@ -3,16 +3,12 @@ import type { MemoryPorts } from "./ports.js";
3
3
  export interface NoteInput {
4
4
  readonly text: string;
5
5
  }
6
- /**
7
- * Why courier_note wrote nothing: the option is off, the caller is a leaf, its text is empty or longer than `LESSON_MAX`
8
- * (`length` is what it had as one line), or its tree has no record to write on.
9
- */
10
6
  export type Refusal = {
11
7
  readonly kind: "off";
12
8
  } | {
13
- readonly kind: "leaf";
9
+ readonly kind: "below";
14
10
  readonly depth: number;
15
- readonly maxDepth: number;
11
+ readonly top: string;
16
12
  } | {
17
13
  readonly kind: "empty";
18
14
  } | {
@@ -22,7 +18,6 @@ export type Refusal = {
22
18
  readonly kind: "unrecorded";
23
19
  readonly top: string;
24
20
  };
25
- /** What courier_note gives: the lesson written on the caller's tree's record, with the one it replaced, or why it was not. */
26
21
  export type Noted = {
27
22
  readonly kind: "noted";
28
23
  readonly projectID: string;
@@ -30,8 +25,4 @@ export type Noted = {
30
25
  readonly lesson: Lesson;
31
26
  readonly replaced?: Lesson;
32
27
  } | Refusal;
33
- /**
34
- * courier_note: writes `text`, as one line of at most `LESSON_MAX` characters (longer is refused, never clipped), as the lesson
35
- * on the record of the caller's own tree in this project, replacing the one there; the top is the top of the caller's lineage, or itself.
36
- */
37
28
  export declare function note(ports: MemoryPorts, callerID: string, input: NoteInput): Promise<Noted>;
@@ -2,31 +2,25 @@ import { lineage } from "../tree/index.js";
2
2
  import { LESSON_MAX, oneLine } from "./domain.js";
3
3
  import { relesson } from "./lessons.js";
4
4
  import { memoryStore } from "./store.js";
5
- /**
6
- * courier_note: writes `text`, as one line of at most `LESSON_MAX` characters (longer is refused, never clipped), as the lesson
7
- * on the record of the caller's own tree in this project, replacing the one there; the top is the top of the caller's lineage, or itself.
8
- */
9
5
  export async function note(ports, callerID, input) {
10
6
  if (!ports.memory)
11
7
  return { kind: "off" };
12
8
  const chain = await lineage(ports.storage, callerID);
13
- const depth = chain.length;
14
- if (depth >= ports.maxDepth)
15
- return { kind: "leaf", depth, maxDepth: ports.maxDepth };
9
+ const top = chain.at(-1)?.parentID ?? callerID;
10
+ if (chain.length)
11
+ return { kind: "below", depth: chain.length, top };
16
12
  const text = oneLine(typeof input.text === "string" ? input.text : "");
17
13
  if (!text)
18
14
  return { kind: "empty" };
19
15
  if (text.length > LESSON_MAX)
20
16
  return { kind: "long", length: text.length };
21
- const top = chain.at(-1)?.parentID ?? callerID;
22
17
  const store = memoryStore(ports.storage);
23
18
  const { projectID } = ports;
24
19
  const record = await store.get(projectID, top);
25
20
  if (!record)
26
21
  return { kind: "unrecorded", top };
27
- const lesson = { text, at: ports.now(), ...(depth ? { depth } : {}) };
22
+ const lesson = { text, at: ports.now() };
28
23
  await store.put(projectID, top, { ...record, lesson });
29
- // The lessons entry of the top, its children and the writer moves with it; the next tree's sessions get it at their spawn.
30
- await relesson(ports, callerID, top);
24
+ await relesson(ports, top);
31
25
  return { kind: "noted", projectID, top, lesson, ...(record.lesson ? { replaced: record.lesson } : {}) };
32
26
  }
@@ -1,8 +1,3 @@
1
1
  import type { MemoryPorts } from "./ports.js";
2
- /** What pinning did: the record is pinned, unpinned, or there is no record to pin. */
3
2
  export type PinOutcome = "pinned" | "unpinned" | "none";
4
- /**
5
- * Pins the record of the tree under `sessionID` in the ports' project, as the memory view's `p` does, or with `pinned`
6
- * false unpins it; a record pinned already keeps when it was. Whatever the memory option says: a pin is the person's.
7
- */
8
3
  export declare function pin(ports: Pick<MemoryPorts, "storage" | "projectID" | "now">, sessionID: string, pinned: boolean): Promise<PinOutcome>;
@@ -1,8 +1,4 @@
1
1
  import { memoryStore } from "./store.js";
2
- /**
3
- * Pins the record of the tree under `sessionID` in the ports' project, as the memory view's `p` does, or with `pinned`
4
- * false unpins it; a record pinned already keeps when it was. Whatever the memory option says: a pin is the person's.
5
- */
6
2
  export async function pin(ports, sessionID, pinned) {
7
3
  const store = memoryStore(ports.storage);
8
4
  const record = await store.get(ports.projectID, sessionID);
@@ -1,55 +1,35 @@
1
- /** The memory's ports: what refreshing a record and courier_recall need of the plugin context, and what the process shares. */
2
1
  import type { Plugin } from "@opencode/plugin";
3
2
  import type { Storage } from "../platform/storage.js";
4
- import type { Instructions, ModelWindow } from "../tree/index.js";
3
+ import type { Instructions, Lister, ModelWindow } from "../tree/index.js";
5
4
  import type { Pinned, ReadKind, Reads, StoredRecord, TreeRecord } from "./domain.js";
6
5
  export type { Lesson, PartRecord, PartStatus, Pinned, ReadKind, Reads, Recalled, Revision, StoredRecord, TreeRecord } from "./domain.js";
7
6
  type Context = Plugin.Context;
8
- /** What refreshing a record runs on: every hub-v2 member's watch ports have these, so the event module runs through any. */
9
7
  export interface RefreshPorts {
10
8
  readonly storage: Storage;
11
- readonly session: Pick<Context["session"], "get" | "context">;
9
+ readonly session: Pick<Context["session"], "get" | "context"> & Lister;
12
10
  readonly now: () => number;
13
11
  readonly log: (message: string) => void;
14
12
  }
15
- /** What courier_recall and courier_note run on: one instance's ports. */
16
13
  export interface MemoryPorts extends RefreshPorts {
17
- /** With `instructions`, the keyed session entries, which courier_note puts the lessons entry through; missing on a host before 2.2.0. */
18
14
  readonly session: RefreshPorts["session"] & Instructions;
19
- /** The project of the plugin's location, whose records the tools read and write. */
20
15
  readonly projectID: string;
21
- /** The `memory` option: off, and the tools write and list nothing. */
22
16
  readonly memory: boolean;
23
- /** The tree's `maxDepth` limit: a session at it is a leaf, which courier_note refuses. */
24
17
  readonly maxDepth: number;
25
18
  }
26
- /** What every plugin instance in the process shares of the memory, under a fixed key outside the hub. */
27
19
  export interface MemoryState {
28
- /** The context windows of the models known, by `<providerID>/<modelID>`, filled as each instance loads. */
29
20
  readonly windows: Map<string, ModelWindow>;
30
- /** The directories of the locations whose instance has memory off: no record is written for a tree whose top runs there. */
31
21
  readonly off: Set<string>;
32
- /** The top sessions that called courier_recall, whose turn ends refresh their record before they have started a session. */
33
22
  readonly tops: Set<string>;
34
- /** The turn ends recorded, so the hub's two subscriptions refresh once. */
35
23
  readonly seen: Set<string>;
36
24
  }
37
- /** The records, by project and top session, each read with its reads and pin, which lie beside it and are written apart. */
38
25
  export interface MemoryStore {
39
26
  get(projectID: string, sessionID: string): Promise<TreeRecord | undefined>;
40
- /** Writes the record's facts; its `reads` and `pinned` are left out, as nothing but `noteRead` and `pin` write them. */
41
27
  put(projectID: string, sessionID: string, record: TreeRecord): Promise<void>;
42
- /** Removes the record with its reads and pin. */
43
28
  remove(projectID: string, sessionID: string): Promise<void>;
44
- /** Counts a listing or a full read of the record by the tree under `by` at `at`; gives that tree's reads of it. */
45
29
  noteRead(projectID: string, sessionID: string, by: string, kind: ReadKind, at: number): Promise<Reads>;
46
- /** Pins the record, or with `undefined` unpins it. */
47
30
  pin(projectID: string, sessionID: string, pinned: Pinned | undefined): Promise<void>;
48
- /** A project's records, in key order. */
49
31
  ofProject(projectID: string): Promise<StoredRecord[]>;
50
- /** Every project's records. */
51
32
  all(): Promise<StoredRecord[]>;
52
- /** The trees with reads or a pin but no record, left by a record removed while it was read or pinned. */
53
33
  strays(): Promise<Array<{
54
34
  projectID: string;
55
35
  sessionID: string;
@@ -3,7 +3,6 @@ import type { MemoryPorts, MemoryState } from "./ports.js";
3
3
  export interface RecallInput {
4
4
  readonly sessionID?: string;
5
5
  }
6
- /** What courier_recall gives: the option is off, the project's records with the caller's tree named, one record in full, or none under that session. */
7
6
  export type Recall = {
8
7
  readonly kind: "off";
9
8
  } | {
@@ -20,8 +19,4 @@ export type Recall = {
20
19
  readonly projectID: string;
21
20
  readonly sessionID: string;
22
21
  };
23
- /**
24
- * courier_recall: notes the call on the caller's tree, then lists the project's records, pinned first and then newest first,
25
- * or gives the one under `sessionID` in full, each counted on the other trees' records; with the memory option off, nothing is written or read.
26
- */
27
22
  export declare function recall(ports: MemoryPorts, state: Pick<MemoryState, "tops">, callerID: string, input: RecallInput): Promise<Recall>;
@@ -3,11 +3,9 @@ import { finite, millis } from "../platform/json.js";
3
3
  import { lineage } from "../tree/index.js";
4
4
  import { NOT_STATED, noteRead, noteRecall, pinnedFirst } from "./domain.js";
5
5
  import { memoryStore } from "./store.js";
6
- /** The top of the caller's tree: the top of its lineage, or itself. */
7
6
  async function topOfCaller(ports, callerID) {
8
7
  return (await lineage(ports.storage, callerID)).at(-1)?.parentID ?? callerID;
9
8
  }
10
- /** Notes the call on the caller's tree's record, which is written now if the tree has none yet, so the call is remembered before any session is started. */
11
9
  async function note(ports, top, byTop) {
12
10
  const store = memoryStore(ports.storage);
13
11
  const now = ports.now();
@@ -26,10 +24,6 @@ async function note(ports, top, byTop) {
26
24
  parts: [],
27
25
  });
28
26
  }
29
- /**
30
- * Counts, on each record of another tree, that this call listed it or gave it in full, best effort: a count lost is
31
- * logged, never the call's failure. Gives the reads of the reader's tree on each, in order, or undefined where it failed.
32
- */
33
27
  function count(ports, top, kind, sessionIDs) {
34
28
  const store = memoryStore(ports.storage);
35
29
  const now = ports.now();
@@ -38,10 +32,6 @@ function count(ports, top, kind, sessionIDs) {
38
32
  return undefined;
39
33
  })));
40
34
  }
41
- /**
42
- * courier_recall: notes the call on the caller's tree, then lists the project's records, pinned first and then newest first,
43
- * or gives the one under `sessionID` in full, each counted on the other trees' records; with the memory option off, nothing is written or read.
44
- */
45
35
  export async function recall(ports, state, callerID, input) {
46
36
  if (!ports.memory)
47
37
  return { kind: "off" };
@@ -50,13 +40,11 @@ export async function recall(ports, state, callerID, input) {
50
40
  addBounded(state.tops, top, SEEN_MAX);
51
41
  const store = memoryStore(ports.storage);
52
42
  const { projectID } = ports;
53
- // Some models send null, or an empty string, for an optional field they leave out.
54
43
  const wanted = typeof input.sessionID === "string" ? input.sessionID.trim() : "";
55
44
  if (wanted) {
56
45
  const record = await store.get(projectID, wanted);
57
46
  if (!record)
58
47
  return { kind: "missing", projectID, sessionID: wanted };
59
- // Another tree's record is given with this read added to the reads just read; the caller's own is not counted.
60
48
  const [counted] = wanted === top ? [undefined] : await count(ports, top, "read", [wanted]);
61
49
  const reads = counted ? noteRead(record.reads, "read", counted.at) : record.reads;
62
50
  return { kind: "record", projectID, record: { ...record, ...(reads ? { reads } : {}), projectID, sessionID: wanted } };
@@ -1,7 +1,3 @@
1
1
  import { type TreeRecord } from "./domain.js";
2
2
  import type { MemoryState, RefreshPorts } from "./ports.js";
3
- /**
4
- * Writes the record of the tree under `top` as it stands at `at`, after the turn of `ended`; a part the roster has forgotten
5
- * keeps its last recorded state, unless found gone before it settled. Nothing for a top unknown to OpenCode or with memory off.
6
- */
7
3
  export declare function refresh(ports: RefreshPorts, state: Pick<MemoryState, "windows" | "off">, top: string, at: number, ended?: string): Promise<TreeRecord | undefined>;