opencode-courier 0.7.3 → 0.9.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 (57) hide show
  1. package/README.md +119 -136
  2. package/dist/index.js +23 -4
  3. package/dist/kernel/protocol.d.ts +2 -0
  4. package/dist/kernel/protocol.js +9 -1
  5. package/dist/memory/domain.d.ts +188 -0
  6. package/dist/memory/domain.js +209 -0
  7. package/dist/memory/events.d.ts +26 -0
  8. package/dist/memory/events.js +46 -0
  9. package/dist/memory/forget.d.ts +10 -0
  10. package/dist/memory/forget.js +20 -0
  11. package/dist/memory/index.d.ts +11 -0
  12. package/dist/memory/index.js +12 -0
  13. package/dist/memory/note.d.ts +37 -0
  14. package/dist/memory/note.js +29 -0
  15. package/dist/memory/pin.d.ts +8 -0
  16. package/dist/memory/pin.js +13 -0
  17. package/dist/memory/ports.d.ts +55 -0
  18. package/dist/memory/ports.js +1 -0
  19. package/dist/memory/recall.d.ts +27 -0
  20. package/dist/memory/recall.js +67 -0
  21. package/dist/memory/record.d.ts +7 -0
  22. package/dist/memory/record.js +124 -0
  23. package/dist/memory/rpc-handlers.d.ts +179 -0
  24. package/dist/memory/rpc-handlers.js +25 -0
  25. package/dist/memory/rpc.d.ts +299 -0
  26. package/dist/memory/rpc.js +71 -0
  27. package/dist/memory/store.d.ts +14 -0
  28. package/dist/memory/store.js +174 -0
  29. package/dist/memory/sweep.d.ts +7 -0
  30. package/dist/memory/sweep.js +16 -0
  31. package/dist/memory/text.d.ts +35 -0
  32. package/dist/memory/text.js +116 -0
  33. package/dist/memory/tools.d.ts +16 -0
  34. package/dist/memory/tools.js +78 -0
  35. package/dist/memory/tui.d.ts +82 -0
  36. package/dist/person/rpc.d.ts +4 -4
  37. package/dist/platform/json.d.ts +1 -0
  38. package/dist/platform/json.js +1 -0
  39. package/dist/rpc.d.ts +1 -0
  40. package/dist/rpc.js +1 -0
  41. package/dist/skill.js +10 -3
  42. package/dist/tree/context.d.ts +2 -2
  43. package/dist/tree/context.js +4 -3
  44. package/dist/tree/index.d.ts +6 -5
  45. package/dist/tree/index.js +5 -4
  46. package/dist/tree/mascot.js +1 -1
  47. package/dist/tree/ports.d.ts +4 -2
  48. package/dist/tree/roster.d.ts +17 -0
  49. package/dist/tree/roster.js +24 -0
  50. package/dist/tree/rpc-handlers.d.ts +20 -20
  51. package/dist/tree/rpc.d.ts +20 -20
  52. package/dist/tree/status.d.ts +5 -5
  53. package/dist/tree/subtree.d.ts +2 -2
  54. package/dist/tree/text.js +2 -1
  55. package/dist/tree/tui.d.ts +11 -3
  56. package/dist/tui.js +612 -123
  57. package/package.json +1 -1
package/README.md CHANGED
@@ -38,49 +38,44 @@ If OpenCode asks to allow a child's command, the parent passes the question to y
38
38
  sometimes stall on a request; if nothing moves for a few minutes, send the prompt again. Next:
39
39
  [Using it](#using-it).
40
40
 
41
-
42
41
  ## OpenCode V2 native
43
42
 
44
43
  Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
45
- (`@opencode-ai/plugin`), and needs OpenCode V2. Each release is pinned to one V2 version and tested
46
- against it end to end; the current one is tested on **OpenCode 2.0.32**. CI also runs the live
47
- suite on the newest OpenCode release, on `main` and nightly, so a host release that breaks the
48
- plugin shows up there first. Older releases and their versions: [every release](docs/reference.md#the-opencode-version).
44
+ (`@opencode-ai/plugin`). Each release is pinned to one V2 version and tested against it end to end;
45
+ the current one is tested on **OpenCode 2.0.32**. CI also runs the live suite on the newest OpenCode
46
+ release, on `main` and nightly. Older releases: [every release](docs/reference.md#the-opencode-version).
49
47
 
50
48
  ## Courier or the built-in subagent tool?
51
49
 
52
- OpenCode V2 has its own [`subagent` tool](https://opencode.ai/v2/docs/agents): it starts an agent
53
- with fresh context in a child session, and the parent either waits for its final answer or, with
54
- `background: true`, carries on and is notified when the child finishes; passing the child's
55
- `sessionID` back prompts it again. Use it when one result back is all you need: a search, a review,
56
- a self-contained task that needs nothing from you on the way. Use courier when the work needs more:
50
+ OpenCode V2's own [`subagent` tool](https://opencode.ai/v2/docs/agents) starts an agent with fresh
51
+ context in a child session; the parent waits for its final answer or, with `background: true`, is
52
+ notified when the child finishes. Use it when one result back is all you need. Use courier when the
53
+ work needs more:
57
54
 
58
- - **Messages from the child, mid-run:** a child reports progress, or asks its parent something,
55
+ - **Messages from the child, mid-run:** a child reports progress or asks its parent something
59
56
  whenever it likes with `courier_send`, and any session can message any other.
60
57
  - **Waking an idle session from anywhere:** a message from a child, a schedule (`courier_later`) or
61
- a [webhook](#webhooks) (GitHub reviews, comments and CI runs, or any signed POST) starts a new turn
62
- in a session that has ended its own.
63
- - **Worktree isolation:** `isolate: true` gives a child its own git worktree, so children can edit
64
- files in parallel; each commits on `courier/<its session ID>` and reports that branch for its
65
- parent to merge ([nested isolation](docs/reference.md#nested-isolation)).
66
- - **Relaying to you:** a child's permission requests and questions reach you through the parent:
67
- a request is approved in OpenCode's own permission prompt in the parent's session, never by its
68
- model, and your answers go back; a form only you can answer is pointed out to you.
58
+ a [webhook](#webhooks) starts a new turn in a session that has ended its own.
59
+ - **Worktree isolation:** `isolate: true` gives a child its own git worktree, so children edit files
60
+ in parallel; each commits on `courier/<its session ID>` and reports that branch for its parent to
61
+ merge ([nested isolation](docs/reference.md#nested-isolation)).
62
+ - **Relaying to you:** a child's permission requests and questions reach you through the parent. A
63
+ request is approved in OpenCode's own permission prompt in the parent's session, never by its
64
+ model; a form only you can answer is pointed out to you.
69
65
 
70
66
  ## How it works
71
67
 
72
- A parent session calls `courier_spawn`, gets a session id back immediately and ends its turn. The
73
- child works on its own and, when it is done or stuck, calls `courier_send` with the parent's id, a
74
- status and what it produced. That message lands in the parent's inbox and OpenCode starts a new turn
75
- for the parent if it is idle. There is no polling anywhere: a message is admitted into the target
76
- session's inbox through OpenCode's own API, the way OpenCode's background subagents report to their
77
- parent. Delivery is `steer` by default (injected into the target's running turn, or starts one if
78
- idle); `queue: true` waits until the current turn ends.
68
+ A parent calls `courier_spawn`, gets a session id back at once and ends its turn. The child works
69
+ on its own and, when done or stuck, calls `courier_send` with the parent's id, a status and what it
70
+ produced. The message lands in the parent's inbox through OpenCode's own API, the way OpenCode's
71
+ background subagents report, and starts a new turn if the parent is idle. Delivery is `steer` by
72
+ default (into the target's running turn, or a new one if idle); `queue: true` waits until the current
73
+ turn ends.
79
74
 
80
75
  ## Demos
81
76
 
82
- The video at the top (1:46, narrated) is the TUI driven in `tmux` on the scripted stand-in model of
83
- the live test, with `e2e/tui.sh`'s setup, so each scene is real plugin behaviour:
77
+ The video at the top (1:46, narrated) is the TUI driven in `tmux` on the live test's scripted
78
+ stand-in model, so each scene is real plugin behaviour:
84
79
 
85
80
  - **A join group:** one member reports and its report is held with the group, while the other waits
86
81
  on a permission request, so the footer says one is waiting on you. You allow it from the asks inbox
@@ -95,11 +90,10 @@ the live test, with `e2e/tui.sh`'s setup, so each scene is real plugin behaviour
95
90
  ## Recursive orchestration
96
91
 
97
92
  A session can split a goal across child sessions, each child deciding in turn whether to split its
98
- part again, with the results checked and integrated at every level on the way back up. The plugin
99
- ships the playbook as an OpenCode skill, `courier-orchestrate`, listed in every session: models are
100
- told to load it at the start of an implementation task that may have independent parts, and
101
- `/skills` in the TUI attaches it to your prompt. What it says: [the
102
- reference](docs/reference.md#the-courier-orchestrate-skill).
93
+ part again, with the results checked and integrated at every level on the way back up. The playbook
94
+ ships as the OpenCode skill `courier-orchestrate`, listed in every session: models load it at the
95
+ start of an implementation task that may have independent parts, and `/skills` in the TUI attaches
96
+ it to your prompt ([what it says](docs/reference.md#the-courier-orchestrate-skill)).
103
97
 
104
98
  ## Install
105
99
 
@@ -107,14 +101,14 @@ reference](docs/reference.md#the-courier-orchestrate-skill).
107
101
 
108
102
  Requires OpenCode V2: the `opencode` command from `@opencode/cli` (also installed as `opencode2`).
109
103
  Each release is built and tested against exactly one OpenCode V2 version; a row lists the releases
110
- pinned to its version. OpenCode never checks it:
111
- the plugin installs and loads on any V2 host, and on a version other than the pinned one writes one
112
- line to the server log naming both ([more](docs/reference.md#the-opencode-version)).
104
+ pinned to its version. OpenCode never checks it: the plugin loads on any V2 host, and on another
105
+ version than the pinned one writes one line to the server log
106
+ ([more](docs/reference.md#the-opencode-version)).
113
107
 
114
108
  <!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
115
109
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
116
110
  |---|---|
117
- | 0.7.3 | 2.0.32 |
111
+ | 0.7.3, 0.8.0, 0.9.0 | 2.0.32 |
118
112
  | 0.7.2 | 2.0.31 |
119
113
  | 0.7.1 | 2.0.30 |
120
114
  <!-- compat-table:end -->
@@ -122,7 +116,7 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
122
116
  Older releases and their versions: [every release](docs/reference.md#the-opencode-version).
123
117
 
124
118
  The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
125
- OpenCode 2.0.32 only; the server part works as before on older V2 hosts from 2.0.4 on.
119
+ OpenCode 2.0.32 only; the server part works on older V2 hosts from 2.0.4 on.
126
120
 
127
121
  Check yours with `opencode --version`, and install the matching CLI with:
128
122
 
@@ -139,9 +133,8 @@ opencode plugin add opencode-courier
139
133
  This installs the package from npm and adds `"opencode-courier"` to `plugins` in the global
140
134
  configuration (`~/.config/opencode/opencode.json`); its options ([session trees](#session-trees),
141
135
  [webhooks](#webhooks)) go in the object form of that entry. Install it by name, without a version:
142
- OpenCode only checks plugins for updates when their entry is not an exact version, so
143
- `opencode-courier@0.2.1` is never offered a newer release ([moving such an
144
- entry](docs/reference.md#updating-the-plugin)).
136
+ OpenCode only checks plugins for updates when their entry is not an exact version
137
+ ([updating](docs/reference.md#updating-the-plugin)).
145
138
 
146
139
  From a local clone, `bun install && npm run build`, then list its `dist` **directory** in
147
140
  `opencode.json` (V2 uses `plugins`, plural; a path to a file is ignored with a warning):
@@ -155,14 +148,15 @@ From a local clone, `bun install && npm run build`, then list its `dist` **direc
155
148
  ## Using it
156
149
 
157
150
  1. Keep the background server running so sessions can be woken while you are away (`opencode
158
- service start`), and run one OpenCode server per data directory where you can: a second one, such
159
- as `opencode serve` next to it, cannot pass your answer on to a child the other runs
151
+ service start`), and run one OpenCode server per data directory where you can: a second one
152
+ cannot pass your answer on to a child the other runs
160
153
  ([why](docs/reference.md#two-servers-on-one-data-directory)).
161
154
  2. Give the agents that run children the permissions their work needs: a child that hits an approval
162
- prompt waits until you answer OpenCode's prompt for it in the parent's session. Let them use the `question` tool if they
163
- should be able to ask you. Choose a web search provider before children search the web (run one
164
- web search in your own session, or use OpenCode's "Third-party search" setting), or the first
165
- child to search shows that prompt in its own session, which only you can answer there.
155
+ prompt waits until you answer OpenCode's prompt for it in the parent's session. Let them use the
156
+ `question` tool if they should be able to ask you. Choose a web search provider before children
157
+ search the web (run one web search in your own session, or use OpenCode's "Third-party search"
158
+ setting), or the first child to search shows that prompt in its own session, which only you can
159
+ answer there.
166
160
  3. Your own session's prompts are relayed nowhere, and each one stalls the whole tree until you
167
161
  answer it. Before an unattended run, allow the orchestrating session's toolchain up front: one rule
168
162
  on `bun run *` or `npm run *` (the skill has checks run through the project's scripts), `git`, and
@@ -191,38 +185,33 @@ From a local clone, `bun install && npm run build`, then list its `dist` **direc
191
185
  | `courier_cancel` | Drops a message scheduled with `courier_later`. |
192
186
  | `courier_subscribe` | Subscribes a session to webhook deliveries for a topic (`owner/repo`, `owner/repo#12` or a name). |
193
187
  | `courier_unsubscribe` | Drops one topic, or all of a session's. |
188
+ | `courier_recall` | One look at what earlier session trees in this project did and the lesson each left; with `sessionID`, one tree's [record](#memory) in full. |
189
+ | `courier_note` | Writes this tree's one lesson on its [record](#memory), for the next orchestrator in the project: one line of at most 300 characters, naming the job and the reason. A longer one is refused, a second call replaces the first. From the top or a sub-orchestrator, never a leaf. |
194
190
 
195
191
  Each tool's parameters: [the reference](docs/reference.md#the-tools).
196
192
 
197
193
  ## How children reach you
198
194
 
199
195
  - **A child reports once, with a status**: `done`, `partial`, `blocked` (it needs a decision from
200
- its parent, and says what) or `failed`, with its artifacts (branch, commits, files, checks) listed
201
- in the message. A message without a status is progress, not the report.
202
- [More](docs/reference.md#a-childs-report).
196
+ its parent, and says what) or `failed`, with its artifacts in the message. A message without a
197
+ status is progress, not the report. [More](docs/reference.md#a-childs-report).
203
198
  - **A join group wakes you once.** Start a batch of sessions with the same `group` name, and their
204
- `done`, `partial` and `failed` reports are held until the last one is in and your turn has ended,
205
- then delivered in one message. A `blocked` report, a failure, a question or a permission request
206
- still reaches you at once. A member that will not report holds the others back: drop it with
207
- `courier_cleanup`, and the group is released without it. [More](docs/reference.md#join-groups).
208
- - **A child that fails, or ends its turn without reporting, is reported** to its parent by the
209
- plugin, with the error or its last reply, waking it if idle; one that waits on a request, its own
210
- children, a scheduled message or a webhook is not, nor is one that answers what you typed in its
211
- session. [More](docs/reference.md#a-child-that-fails).
212
- - **A message that fails to wake its session is sent again.** A report, notice or scheduled message
213
- (but not a note that a request was answered elsewhere) that waits in an idle session's inbox for 30 seconds without a turn starting for it is sent once
214
- more under the same inbox id, which OpenCode takes for the same message, not a second one. If the
215
- session still does not take it, the footer and the tree panel show it as `stalled`, for you to
216
- prompt it. [More](docs/reference.md#a-missed-wake).
217
- - **Permission requests and questions reach you.** A child's request or question is passed to the
218
- session at the top of its tree, any number of levels down. For a request, that session proposes an
219
- answer with `courier_answer`, and OpenCode asks you in its own permission prompt, in that session
220
- and under its rules, before anything is passed on: its model cannot approve, only propose. For a
221
- question, it asks you and passes your answer back, or links its own question to the child's so your
222
- pick goes straight to it. Whichever answer comes first counts, and the other is withdrawn.
223
- A request starts a turn in the top session if it is idle; if you answer it somewhere else, the
224
- note that it is settled waits for that session's next turn, usually the child's report or its
225
- silence, instead of starting one of its own. [More](docs/reference.md#a-child-that-asks-for-permission).
199
+ `done`, `partial` and `failed` reports are held until the last one is in, then delivered in one
200
+ message; a `blocked` report, a failure, a question or a permission request still reaches you at
201
+ once. A member that will not report holds the others back: drop it with `courier_cleanup`.
202
+ [More](docs/reference.md#join-groups).
203
+ - **A child that fails, or ends its turn without reporting, is reported** to its parent, with the
204
+ error or its last reply, unless it waits on a request, its own children, a scheduled message or a
205
+ webhook. [More](docs/reference.md#a-child-that-fails).
206
+ - **A message that fails to wake its session is sent again** after 30 seconds; if the session still
207
+ does not take it, the footer and the tree panel show it as `stalled`, for you to prompt it.
208
+ [More](docs/reference.md#a-missed-wake).
209
+ - **Permission requests and questions reach you** through the session at the top of the tree, any
210
+ number of levels down. For a request, that session proposes an answer with `courier_answer`, and
211
+ OpenCode asks you in its own permission prompt there before anything is passed on: its model
212
+ cannot approve, only propose. For a question, it asks you and passes your answer back, or links
213
+ its own question to the child's so your pick goes straight to it.
214
+ [More](docs/reference.md#a-child-that-asks-for-permission).
226
215
  - **Some forms only you can answer**, such as OpenCode's web search asking for a provider: the top
227
216
  session is told, and you answer in the child's session. [More](docs/reference.md#a-child-that-shows-a-form).
228
217
  - **You can see and stop a whole subtree** with `courier_tree` and `courier_stop`, or in the
@@ -249,49 +238,52 @@ options in the plugin's `options`, in the global config:
249
238
  | `maxChildren` | `5` | How many sessions one session started may have live at once. |
250
239
  | `maxTotal` | `20` | How many spawned sessions may be live at once in one tree. |
251
240
 
252
- `courier_spawn` refuses a spawn past a limit, naming it and saying what to do instead: do the work
253
- itself, or wait for a child to report. A session counts while it is live: its turn runs, it owes
254
- its parent a report (its task or a message reached it after it last reported, failed or was
255
- interrupted), or a session below it is live. One that has reported, with nothing live below it,
256
- does not. Each spawned session's brief says when to split its task and when to do it itself
241
+ `courier_spawn` refuses a spawn past a limit, naming it and saying what to do instead. A session
242
+ counts while it is live: its turn runs, it owes its parent a report, or a session below it is live.
243
+ Each spawned session's brief says when to split its task and when to do it itself
257
244
  ([more](docs/reference.md#session-trees-and-their-limits)).
258
245
 
246
+ ## Memory
247
+
248
+ The plugin keeps **one record per session tree** in the project, from facts it already has: when the
249
+ tree started and last moved, the top session's title and first `split:` line, with up to 3 later ones
250
+ that read otherwise (when each was stated, and after how many parts), and for each session started
251
+ in the tree its title, depth, isolation, join group, status, duration, cost, context use and spawns
252
+ refused; never a report's body or artifacts. A session deleted since keeps its last status and
253
+ duration, or reads `gone` when it had not settled, so it never holds its tree at `not finished`.
254
+ The one thing a model writes into it is **the tree's
255
+ lesson**, with `courier_note`: one line of at most 300 characters, from the session that verified the
256
+ tree, with the job it comes from and the reason. `courier_recall` reads the records back, each line
257
+ showing the split line as prose (its markdown marks dropped, the asterisks of its globs kept) cut at a
258
+ word, then the latest later one (`then, after 1 part: split: 2 parts: …`), and saying how long the tree ran and how long ago it last moved; the `courier-orchestrate` skill asks for
259
+ one call before the split decision and one `courier_note` before the top replies. Each record counts
260
+ how often other trees' `courier_recall` calls listed it and opened it in full, and when last, so the
261
+ person can tell a lesson still read from one nobody reads; a record the person pins in the terminal
262
+ is listed first and never swept. Each project keeps 30 unpinned records, those with a lesson first,
263
+ and none older than 90 days. `"memory":
264
+ false` in the plugin's `options` turns it off ([more](docs/reference.md#memory)).
265
+
259
266
  ## In the terminal
260
267
 
261
- The package ships a part for OpenCode's terminal UI, which the TUI loads from the server's plugin
262
- list; the server half behaves the same whether or not a TUI is attached. In a session that started
263
- sessions, the prompt's footer shows what the tree under it is doing: how many wait on you (a
264
- permission request, a question or a form they show), are [stalled](docs/reference.md#a-missed-wake),
265
- run or are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
266
- reported:
268
+ The package ships a part for OpenCode's terminal UI, loaded from the server's plugin list; the server
269
+ half behaves the same with or without it. In a session that started sessions, the prompt's footer
270
+ shows what the tree under it is doing: how many wait on you (a permission request, a question or a
271
+ form), are [stalled](docs/reference.md#a-missed-wake), run or are idle, and each
272
+ [join group](docs/reference.md#join-groups)'s members reported:
267
273
 
268
274
  ```
269
275
  courier 1 waiting on you · 2 running · group pair 1/2
270
276
  ```
271
277
 
272
- `/courier tree` opens a panel beside the session with the tree under it, a row per session indented
273
- by depth, with a mark and colour from your theme for its state (`✓` reported, a spinner `⠋` while it runs, `●` waiting on you, `!` stalled,
274
- `✗` failed or interrupted), what it does, its report and its group's standing; a session waiting on you gets a line per
275
- request or form (`shows a form: Web Search`), a stalled one since when its message waits, a session that started others gets
276
- a bar of the parts reported, a leaf how long it ran, its last tool call, how full its context is (`ctx 31%`, in the warning colour past 80%) and its cost. A tree that has
277
- started more than `maxTotal` sessions over time is listed live ones first, then newest, and the header
278
- says how many older ones are left out; the footer counts them all. `↑`/`↓` select,
279
- `enter` or a click opens the session, a row under the pointer is highlighted, `s` stops it and everything under it after you confirm, as `courier_stop`
280
- would, `x` forgets a `gone` session after you confirm, removing its worktree by `courier_cleanup`'s
281
- rules, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
282
- follows you between sessions: into one of its tree it keeps that tree, the session on screen marked
283
- `(on screen)`; elsewhere it shows that session's tree, or hides while you are in a session without one,
284
- or on the home screen, and shows again once you are back in one; only `q` closes it for good.
285
- A row whose report arrives flashes in your theme's success colour and fades back over 1.5 s, and the header's bar
286
- grows to its new length; nothing in the footer moves. Next to the header's title digs Digby, the courier's mole, drawn
287
- in your theme's colours, whose state follows the tree's: a breathing mound while all is idle, a trembling one while
288
- children run, up with a blinking sign while one waits on you, slumped while one is stalled, popping up with a letter
289
- for a report and raising mounds for a spawn. Where the panel has room the tree does not use, on an empty panel or 10
290
- rows free below the last row, a larger Digby, 16 by 8, shows the same state there instead, paws, sign, letter and all
291
- ([more](docs/reference.md#in-the-terminal)).
292
- To keep the panel still (`•` for a running row, no flash, the bar at once, Digby in each state's first frame), give the TUI part `"animations": false` in OpenCode's terminal config, `~/.config/opencode/cli.json`:
293
- the TUI passes options to its plugins only from there, not from the server's `opencode.json`, and has no animations
294
- setting a plugin can read ([more](docs/reference.md#in-the-terminal)).
278
+ `/courier tree` opens a panel beside the session with the tree under it, a row per session with a
279
+ mark for its state (`✓` reported, a spinner while it runs, `●` waiting on you, `!` stalled, `✗`
280
+ failed or interrupted), what it does, its report, its group's standing and, for a leaf, how long it
281
+ ran, its last tool call, its context use and its cost. `enter` or a click opens the session, `s`
282
+ stops it and everything under it, `x` forgets a `gone` session, `q` closes the panel;
283
+ `/courier stop <session>` stops from the prompt. Digby, the courier's mole, digs beside the header,
284
+ his state following the tree's. `"animations": false` in the TUI part's options keeps the panel
285
+ still; the TUI reads a plugin's options only from its own config, `~/.config/opencode/cli.json`
286
+ ([more](docs/reference.md#in-the-terminal)):
295
287
 
296
288
  ```jsonc
297
289
  {
@@ -299,40 +291,31 @@ setting a plugin can read ([more](docs/reference.md#in-the-terminal)).
299
291
  }
300
292
  ```
301
293
 
302
- `ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on you" or a click
303
- on "waiting on you" opens the asks inbox: every permission request, question and form waiting
304
- anywhere in the tree, however many sessions it has, oldest first, after the top session's own permission
305
- requests, such as the prompt `courier_answer` has OpenCode show there, with where it comes from, kept current
306
- while it is open as asks arrive or are settled elsewhere, answered in a dialog with no model in between; the session that started the tree hears once that you settled it. The TUI plays OpenCode's
307
- sounds for a child's report and a group's release, and OpenCode's own for a new ask, which the courier
308
- adds a notification to for a native child ([more](docs/reference.md#in-the-terminal)).
309
-
310
- While a permission prompt or form is shown in the session, OpenCode hides the footer, so open the
311
- inbox with `ctrl+x k` (or `/asks` once the prompt is back).
294
+ `ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on
295
+ you" or a click on "waiting on you" opens the asks inbox: every permission request, question and form
296
+ waiting anywhere in the tree, after the top session's own permission requests, with where each comes
297
+ from, kept current while open and answered in a dialog with no model in between.
298
+ The TUI plays OpenCode's sounds for a child's report, a group's release and a new ask. While a
299
+ permission prompt or form is shown in the session, OpenCode hides the footer, so open the inbox with
300
+ `ctrl+x k`.
301
+
302
+ `/courier memory` (or the palette's "Courier: memory") lists the project's [tree records](#memory),
303
+ pinned first, then newest first grouped by the day each tree last moved, one row each with how often
304
+ other trees read it and its lesson under it. `enter` shows one in full; `p` pins or unpins the record,
305
+ and a pinned one is listed first and never swept; `x` forgets the record under the row, or in a
306
+ record on its lesson's rows only the lesson, after a confirm; `esc` goes back and closes. The search
307
+ takes every letter but `x` and `p` ([more](docs/reference.md#the-memory-view)).
312
308
 
313
309
  ## Native child sessions
314
310
 
315
311
  Off by default. With `"nativeChildren": true` in the plugin's `options`, a child started without
316
312
  `isolate` is also created with OpenCode's own `parentID`, so OpenCode treats it as the parent's
317
- subagent: the TUI's view of the top session lists and answers the permission requests and forms of
318
- every session under it, and the paired web, desktop or mobile app's request dock answers a
319
- grandchild's ask from the top session, on a phone too. Isolated children are created as before, and
320
- the courier's own tree, limits and relays work as without the option. What changes
321
- ([more](docs/reference.md#native-child-sessions)):
322
-
323
- - **Deleting a session deletes its native children**, as OpenCode does with subagents.
324
- - **A child does not take its parent's session rules**: courier passes it an empty ruleset, so it
325
- may do what it could without the option.
326
- - **Providers see a subagent**: the child's model requests carry its parent's id for cache routing
327
- and two headers naming the parent; **on GitHub Copilot**, which meters such requests differently
328
- from a person's, courier was not tested.
329
- - **A child's asks show in the top session's view**, not in its own, and it leaves the TUI's session
330
- lists. Its permission requests and questions are answered once, in OpenCode's own prompt there; the
331
- top session's model is only told. `opencode run` and the TUI's auto-accept mode answer them as their
332
- own subagents'.
333
- - **Turn it on only with OpenCode's TUI or the paired app**, which show those prompts; with a bare
334
- API client nobody prompts you, and only `courier_answer` is left, which prompts you in the top
335
- session before it passes anything on.
313
+ subagent: the TUI's view of the top session, and the paired app's request dock, list and answer the
314
+ permission requests, questions and forms of every session under it, and the top session's model is
315
+ only told. Deleting a session then deletes its native children, the child gets an empty ruleset
316
+ rather than its parent's, and providers see a subagent (not tested on GitHub Copilot). Turn it on
317
+ only with OpenCode's TUI or the paired app, which show those prompts
318
+ ([more](docs/reference.md#native-child-sessions)).
336
319
 
337
320
  ## Webhooks
338
321
 
@@ -363,7 +346,7 @@ every default):
363
346
  arrives as a short summary marked as outside text, queued behind any running turn and waking the
364
347
  session if idle.
365
348
 
366
- The options, which events wake whom, the tunnels, the signing scheme and status codes: [the
349
+ The options, which events wake whom, the signing scheme and status codes: [the
367
350
  reference](docs/reference.md#webhooks).
368
351
 
369
352
  ## Development
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { hub, openInProcess, permissions as domains, sharedState } from "./platf
8
8
  import { builtVersions, versionNotice } from "./platform/version.js";
9
9
  import { addSchedulingTools, cancelScheduled, dueJob, isScheduled } from "./scheduling/index.js";
10
10
  import { addPersonTools, asksPermission, asksQuestion, formsShown, joinRelay, noticeCutOff, pendingOf, pendingQuestions, personEvents, portsAt, registerPersonRpc, relayQuestions, } from "./person/index.js";
11
+ import { addMemoryTools, memoryEvents, NOTE_TOOL, readMemory, RECALL_TOOL, registerMemoryRpc, sweepRecords } from "./memory/index.js";
11
12
  import { addSkill } from "./skill.js";
12
13
  import { addTreeTools, deliverReleased, wakeMissed, dirtyOf, headOf, inspectWorktree, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, together, treeEvents, waitsOn, } from "./tree/index.js";
13
14
  import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince, } from "./webhook/index.js";
@@ -17,6 +18,8 @@ const gate = sharedState("spawning", () => ({ reserved: new Set(), turn: Promise
17
18
  const inbox = sharedState("inbox", () => new Map());
18
19
  /** The forms shown now, which the person events keep and the footer and tree panel count; outside the hub like the inbox. */
19
20
  const shown = sharedState("formsShown", () => new Map());
21
+ /** What the memory shares: the models' windows, the locations with memory off, the tops that recalled and the turn ends recorded. */
22
+ const memoryState = sharedState("memory", () => ({ windows: new Map(), off: new Set(), tops: new Set(), seen: new Set() }));
20
23
  /**
21
24
  * What each tick of the scheduler does, in order, through the first loaded member, of whichever copy:
22
25
  * due `courier_later` messages, released join groups, then missed wakes. Every hub-v2 member's `later` is its LaterPorts.
@@ -40,7 +43,7 @@ const personState = { ...watched, forms: hub.forms, shown };
40
43
  */
41
44
  const outside = waitsOn(asksPermission, asksQuestion, together(isScheduled, subscribedSince));
42
45
  /** What the hub's subscriptions do with each event of OpenCode's, module by module, in this order. */
43
- const eventModules = [treeEvents(watchState, outside), personEvents(personState)];
46
+ const eventModules = [treeEvents(watchState, outside), personEvents(personState), memoryEvents(memoryState)];
44
47
  /**
45
48
  * The courier tools and webhook receiver, as a promise plugin joining the hub that runs the scheduler
46
49
  * and the watcher; its member carries the location by which the question relay finds its ports.
@@ -70,7 +73,8 @@ export const courier = () => Plugin.define({
70
73
  };
71
74
  const { limits, problems } = readLimits(ctx.options);
72
75
  const { nativeChildren, problems: nativeProblems } = readNativeChildren(ctx.options);
73
- for (const problem of [...problems, ...nativeProblems])
76
+ const { memory, problems: memoryProblems } = readMemory(ctx.options);
77
+ for (const problem of [...problems, ...nativeProblems, ...memoryProblems])
74
78
  log(`courier: ${problem}`);
75
79
  const roles = new Map();
76
80
  const ports = {
@@ -117,21 +121,32 @@ export const courier = () => Plugin.define({
117
121
  later.log(`courier webhook: not started: ${error instanceof Error ? error.message : String(error)}`);
118
122
  }
119
123
  const stopPorts = { courier: ports, cancelScheduled: (sessionID) => cancelScheduled(later, sessionID), cleanup: cleanupPorts };
120
- // The tools in a fixed order: the tree's, then the person's, the scheduled messages' and the webhooks'.
124
+ const memoryPorts = { storage: ctx.storage, session: ctx.session, now: Date.now, log, projectID: ctx.location.project.id, memory, maxDepth: limits.maxDepth };
125
+ // The tools in a fixed order: the tree's, then the person's, the scheduled messages', the webhooks' and the memory's.
121
126
  await ctx.tool.transform((tools) => {
122
127
  addTreeTools(tools, { courier: ports, cleanup: cleanupPorts, stop: stopPorts });
123
128
  addPersonTools(tools, { answer: answerPorts, questions: questionPorts, waiting: watched.waiting });
124
129
  addSchedulingTools(tools, later);
125
130
  addWebhookTools(tools, { hooks, receiving });
131
+ addMemoryTools(tools, { memory: memoryPorts, state: memoryState });
126
132
  });
127
133
  await ctx.skill.transform(addSkill);
128
134
  // What the TUI part reads and its stop; a host that cannot register it still gets the rest, the tools included.
129
135
  await Promise.resolve()
130
136
  .then(() => registerTreeRpc(ctx.rpc, stopPorts))
131
137
  .then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions }))
138
+ .then(() => registerMemoryRpc(ctx.rpc, memoryPorts))
132
139
  .catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
133
- await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, log }, event));
140
+ await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, hiddenFromLeaf: [RECALL_TOOL, NOTE_TOOL], log }, event));
134
141
  const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
142
+ const swept = sweepRecords(ctx.storage, Date.now()).catch((error) => log(`courier memory sweep: ${String(error)}`));
143
+ // The windows the memory reads each part's context use by; a tree whose top runs in a location with memory off is not recorded.
144
+ const windowed = ports
145
+ .windows()
146
+ .then((windows) => windows.forEach((window) => memoryState.windows.set(`${window.providerID}/${window.id}`, window)))
147
+ .catch((error) => log(`courier memory: cannot list the models' context windows: ${String(error)}`));
148
+ if (!memory)
149
+ memoryState.off.add(ctx.location.directory);
135
150
  const leaveRelay = joinRelay(questionPorts);
136
151
  const noticed = noticeCutOff(questionPorts).catch((error) => questionPorts.log(`courier question: stored questions: ${String(error)}`));
137
152
  const watchPorts = {
@@ -158,11 +173,15 @@ export const courier = () => Plugin.define({
158
173
  // The leave waits for them, so they do not run on against a location that is closing.
159
174
  track(member, pruned);
160
175
  track(member, noticed);
176
+ track(member, swept);
177
+ track(member, windowed);
161
178
  const leave = webhook ? joinReceiver(webhook, hooks) : undefined;
162
179
  return async () => {
163
180
  // Waits, for a moment at most, for the work this instance leaves behind, and on the last
164
181
  // leave for the scheduler's owner key to be released, so another server takes over at once.
165
182
  const leftHub = leaveHub();
183
+ if (!memory)
184
+ memoryState.off.delete(ctx.location.directory);
166
185
  leaveRelay();
167
186
  await leave?.();
168
187
  await leftHub;
@@ -14,6 +14,8 @@ export declare const SPLIT_LINE = "split: no, because \u2026; considered: <the p
14
14
  export declare const SKILL_ID = "courier-orchestrate";
15
15
  export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
16
16
  export declare function clipText(text: string, max?: number): string;
17
+ /** `clipText` cut at the last space within `max` instead, so no word is cut in two, unless that keeps less than half of `max`. */
18
+ export declare function clipWords(text: string, max: number): string;
17
19
  /** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
18
20
  export declare function defuse(text: string): string;
19
21
  /** A readable message for a failed courier call; OpenCode's own errors can carry an empty message. */
@@ -19,8 +19,16 @@ export function envelope(from, message, attributes = {}) {
19
19
  }
20
20
  /** Longest piece of free text (a review body, a generic payload) copied into a delivered message. */
21
21
  const MAX_TEXT = 1500;
22
+ const clipped = (text, kept) => `${kept}… [${text.length - kept.length} more characters]`;
22
23
  export function clipText(text, max = MAX_TEXT) {
23
- return text.length > max ? `${text.slice(0, max)}… [${text.length - max} more characters]` : text;
24
+ return text.length > max ? clipped(text, text.slice(0, max)) : text;
25
+ }
26
+ /** `clipText` cut at the last space within `max` instead, so no word is cut in two, unless that keeps less than half of `max`. */
27
+ export function clipWords(text, max) {
28
+ if (text.length <= max)
29
+ return text;
30
+ const space = text.lastIndexOf(" ", max);
31
+ return clipped(text, (space >= max / 2 ? text.slice(0, space) : text.slice(0, max)).trimEnd());
24
32
  }
25
33
  /** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
26
34
  export function defuse(text) {