opencode-courier 0.7.0 → 0.7.2

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.
package/README.md CHANGED
@@ -21,7 +21,7 @@ The edge cases on the live terminal UI, narrated ([what it shows](#demos)).
21
21
  ## Quickstart
22
22
 
23
23
  ```bash
24
- npm install -g @opencode/cli@2.0.29 # OpenCode V2, at the version courier is tested on
24
+ npm install -g @opencode/cli@2.0.31 # OpenCode V2, at the version courier is tested on
25
25
  opencode plugin add opencode-courier # by name, so OpenCode offers new releases
26
26
  opencode # in any folder
27
27
  ```
@@ -43,7 +43,7 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
43
43
 
44
44
  Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
45
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.29**. CI also runs the live
46
+ against it end to end; the current one is tested on **OpenCode 2.0.31**. CI also runs the live
47
47
  suite on the newest OpenCode release, on `main` and nightly, so a host release that breaks the
48
48
  plugin shows up there first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
49
49
 
@@ -111,8 +111,11 @@ pinned to its version. OpenCode never checks it:
111
111
  the plugin installs and loads on any V2 host, and on a version other than the pinned one writes one
112
112
  line to the server log naming both ([more](docs/reference.md#the-opencode-version)).
113
113
 
114
+ <!-- compat-table:start: generated by script/compat-table.ts from the release tags -->
114
115
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
115
116
  |---|---|
117
+ | 0.7.2 | 2.0.31 |
118
+ | 0.7.1 | 2.0.30 |
116
119
  | 0.5.0, 0.6.0, 0.7.0 | 2.0.29 |
117
120
  | 0.4.0, 0.4.1 | 2.0.28 |
118
121
  | 0.2.3, 0.3.0 | 2.0.26 |
@@ -120,14 +123,15 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
120
123
  | 0.2.1 | 2.0.23 |
121
124
  | 0.2.0 | 2.0.22 |
122
125
  | 0.1.6 | 0.0.0-beta-19271 (the beta line: `opencode2` from `@opencode-ai/cli`, and `@opencode-ai/plugin`) |
126
+ <!-- compat-table:end -->
123
127
 
124
128
  The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
125
- OpenCode 2.0.29 only; the server part works as before on older V2 hosts from 2.0.4 on.
129
+ OpenCode 2.0.31 only; the server part works as before on older V2 hosts from 2.0.4 on.
126
130
 
127
131
  Check yours with `opencode --version`, and install the matching CLI with:
128
132
 
129
133
  ```bash
130
- npm install -g @opencode/cli@2.0.29
134
+ npm install -g @opencode/cli@2.0.31
131
135
  ```
132
136
 
133
137
  ### The plugin
@@ -210,7 +214,7 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
210
214
  children, a scheduled message or a webhook is not, nor is one that answers what you typed in its
211
215
  session. [More](docs/reference.md#a-child-that-fails).
212
216
  - **A message that fails to wake its session is sent again.** A report, notice or scheduled message
213
- that waits in an idle session's inbox for 30 seconds without a turn starting for it is sent once
217
+ (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
218
  more under the same inbox id, which OpenCode takes for the same message, not a second one. If the
215
219
  session still does not take it, the footer and the tree panel show it as `stalled`, for you to
216
220
  prompt it. [More](docs/reference.md#a-missed-wake).
@@ -220,7 +224,9 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
220
224
  and under its rules, before anything is passed on: its model cannot approve, only propose. For a
221
225
  question, it asks you and passes your answer back, or links its own question to the child's so your
222
226
  pick goes straight to it. Whichever answer comes first counts, and the other is withdrawn.
223
- [More](docs/reference.md#a-child-that-asks-for-permission).
227
+ A request starts a turn in the top session if it is idle; if you answer it somewhere else, the
228
+ note that it is settled waits for that session's next turn, usually the child's report or its
229
+ silence, instead of starting one of its own. [More](docs/reference.md#a-child-that-asks-for-permission).
224
230
  - **Some forms only you can answer**, such as OpenCode's web search asking for a provider: the top
225
231
  session is told, and you answer in the child's session. [More](docs/reference.md#a-child-that-shows-a-form).
226
232
  - **You can see and stop a whole subtree** with `courier_tree` and `courier_stop`, or in the
@@ -7,8 +7,8 @@ import { type PersonPorts, type PersonState } from "./ports.js";
7
7
  */
8
8
  export declare function reportAsked(ports: PersonPorts, state: PersonState, event: PermissionAsked): Promise<string[]>;
9
9
  /**
10
- * Tells the session told about a request that it was answered some other way, so it does not pass on
11
- * a stale question; requests answered through courier_answer no longer wait and are skipped.
10
+ * Tells the session told about a request that it was answered some other way, so it does not pass on a stale
11
+ * question; requests answered through courier_answer are skipped. Sent with `resume: false`, so it starts no turn.
12
12
  */
13
13
  export declare function reportReplied(ports: PersonPorts, state: PersonState, event: PermissionReplied): Promise<string[]>;
14
14
  /**
@@ -36,8 +36,8 @@ export async function reportAsked(ports, state, event) {
36
36
  return [topOf(chain)];
37
37
  }
38
38
  /**
39
- * Tells the session told about a request that it was answered some other way, so it does not pass on
40
- * a stale question; requests answered through courier_answer no longer wait and are skipped.
39
+ * Tells the session told about a request that it was answered some other way, so it does not pass on a stale
40
+ * question; requests answered through courier_answer are skipped. Sent with `resume: false`, so it starts no turn.
41
41
  */
42
42
  export async function reportReplied(ports, state, event) {
43
43
  if (!claim(state.seen, event.id))
@@ -54,8 +54,9 @@ export async function reportReplied(ports, state, event) {
54
54
  sessionID: topOf(chain),
55
55
  text: envelope(sessionID, permissionSettledNotice(chain[0].title, requestID, reply), { answered: reply, request: requestID }),
56
56
  description: `Session ${sessionID} no longer asks for permission`,
57
- metadata: { source: "courier", from: sessionID, answered: reply, requestID },
57
+ metadata: { source: "courier", from: sessionID, answered: reply, requestID, resume: false },
58
58
  delivery: "steer",
59
+ resume: false,
59
60
  });
60
61
  return [topOf(chain)];
61
62
  }
package/dist/skill.js CHANGED
@@ -11,17 +11,22 @@ export const SKILL_CONTENT = [
11
11
  "opencode-courier role line in your system prompt, names your depth in the session tree and its limits.",
12
12
  "",
13
13
  "1. Decide: split or do. Split when the task has 2 or more independent, substantial parts touching separate files or " +
14
- "areas; do it yourself when it is small, sequential or tightly coupled. Never start exactly one session. " +
15
- "An implementation task with one tightly coupled core usually still has independent parts: its e2e scenario, its " +
14
+ "areas; do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole " +
15
+ "task: it only adds a hop. What you read and write yourself stays in your context for every later turn, so keep the " +
16
+ "deciding, checking and integrating, and hand off the long reading and writing. An implementation task with one tightly " +
17
+ "coupled core usually still has independent parts: finding out what the change needs (a session with isolate: false " +
18
+ "reads the code, or a library's or a host's source, tries things and reports what it found), its e2e scenario, its " +
16
19
  "documentation, sometimes a second test file, and a review of the core by a session with fresh context, which reads the " +
17
20
  "committed diff against the task and the repository's rules and reports its findings (isolate: false, it edits nothing). " +
18
- "Weigh each of them: do the core yourself first and commit it, so that the names and texts the other parts depend on are " +
19
- "fixed, then, if 2 or more remain, start one session per remaining part, in one group. A part that is itself big and " +
20
- "divisible is split again by the session that gets it; at the deepest level a session does its part itself. " +
21
+ "Weigh each of them. When the other parts depend on names and texts the core fixes, split in stages: the core first, " +
22
+ "yourself when it is small or by one session when it is big, merged and committed before the rest start; then, if 2 or " +
23
+ "more parts remain, one session per remaining part, in one group. A part that is itself big and divisible is split again " +
24
+ "by the session that gets it; at the deepest level a session does its part itself. " +
21
25
  `Say which in one line of your reply, before you start: ${SPLIT_LINE}, so that a reasoned decision to do it ` +
22
26
  "yourself can be told from one never made.",
23
27
  "2. Plan the parts so that no two edit the same files. Write each task so the session can do it without you: what to " +
24
- "produce and where, how to check it, what to report. Say if it may split its part further.",
28
+ "produce and where, how to check it, what to report. Say if it may split its part further, and to run its checks " +
29
+ "through the project's scripts (step 6): each permission request of a session below you wakes you and waits on the person.",
25
30
  "3. Start them: one courier_spawn per part, all with the same group name, so you are woken once with every report; " +
26
31
  "isolate: true for each part that edits files in parallel with another (commit your own work first if you are isolated " +
27
32
  "yourself). For long work, courier_later a check-in for yourself, and courier_cancel it when the reports arrive.",
package/dist/tree/text.js CHANGED
@@ -47,7 +47,7 @@ function splitRule(parentID, depth, limits) {
47
47
  `You are at depth ${depth} of a session tree that goes at most ${limits.maxDepth} deep. Split your task or do it yourself:`,
48
48
  "- Split it when it has 2 or more independent parts, each substantial and touching separate files or areas: start one " +
49
49
  `session per part with courier_spawn, at most ${limits.maxChildren} of yours running at once and ${limits.maxTotal} in the whole tree.`,
50
- "- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session.",
50
+ "- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole task.",
51
51
  `- Say which in one line of your reply: ${SPLIT_LINE}.`,
52
52
  "- If you split, you orchestrate: end your turn while they work, check and integrate each part yourself when it " +
53
53
  `reports (never hand that checking to another session), then send ${parentID} one combined report.`,
@@ -14,6 +14,7 @@ export interface Enqueued {
14
14
  readonly description?: unknown;
15
15
  readonly metadata?: {
16
16
  readonly source?: unknown;
17
+ readonly resume?: unknown;
17
18
  };
18
19
  };
19
20
  };
@@ -34,8 +35,8 @@ export interface Execution {
34
35
  };
35
36
  }
36
37
  /**
37
- * Watches a courier message enqueued for a session until it is delivered, noting whether the session runs: a running
38
- * turn takes it at its next step, an idle session is woken for it. Returns whether it is watched.
38
+ * Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
39
+ * Returns whether it is watched, which one sent with `resume: false` is not.
39
40
  */
40
41
  export declare function watchWake(ports: WatchPorts, event: Enqueued): Promise<boolean>;
41
42
  /** Stops watching a message once it is delivered or cancelled. */
package/dist/tree/wake.js CHANGED
@@ -4,14 +4,17 @@ export const WAKE_MS = 30_000;
4
4
  /** Whether a session's stored messages end with the marker of an ended turn; one with none yet is taken to run. */
5
5
  const endsIdle = (messages) => messages.at(-1)?.type === "idle";
6
6
  /**
7
- * Watches a courier message enqueued for a session until it is delivered, noting whether the session runs: a running
8
- * turn takes it at its next step, an idle session is woken for it. Returns whether it is watched.
7
+ * Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
8
+ * Returns whether it is watched, which one sent with `resume: false` is not.
9
9
  */
10
10
  export async function watchWake(ports, event) {
11
11
  const { sessionID, inboxID, item } = event.data;
12
12
  const payload = item.payload;
13
13
  if (item.type !== "synthetic" || payload?.metadata?.source !== "courier" || typeof payload.text !== "string")
14
14
  return false;
15
+ // Sent without a wake: it waits for the next turn, which nothing owes it.
16
+ if (payload.metadata.resume === false)
17
+ return false;
15
18
  const at = event.created ?? ports.now();
16
19
  // Read after the enqueue: a turn it started has stored messages since, and an unknown session is taken to run.
17
20
  const idle = await ports.session.context({ sessionID }).then(endsIdle, () => false);
package/dist/tui.js CHANGED
@@ -1151,7 +1151,7 @@ async function confirmForget(ctx, courier, location, by, target, done) {
1151
1151
  return true;
1152
1152
  }
1153
1153
  var FOCUS_PROMPT = "pane.focus.left";
1154
- var SPLIT_WIDTH = 80;
1154
+ var SPLIT_WIDTH = 108;
1155
1155
  var oneLine = (title) => title.replace(/[\r\n]+/g, " ");
1156
1156
  function TreePanel(props) {
1157
1157
  const {
@@ -1197,7 +1197,7 @@ function TreePanel(props) {
1197
1197
  presentation,
1198
1198
  focused
1199
1199
  } = input;
1200
- view.note(ctx.renderer.width > SPLIT_WIDTH ? {
1200
+ view.note(ctx.renderer.width >= SPLIT_WIDTH ? {
1201
1201
  presentation,
1202
1202
  focused
1203
1203
  } : {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
5
5
  "license": "MIT",
6
6
  "author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",
@@ -56,12 +56,12 @@
56
56
  "test:e2e": "e2e/run.sh"
57
57
  },
58
58
  "peerDependencies": {
59
- "@opencode/plugin": "2.0.29",
59
+ "@opencode/plugin": "2.0.31",
60
60
  "effect": "4.0.1"
61
61
  },
62
62
  "devDependencies": {
63
- "@opencode/plugin": "2.0.29",
64
- "@opencode/theme": "2.0.29",
63
+ "@opencode/plugin": "2.0.31",
64
+ "@opencode/theme": "2.0.31",
65
65
  "@opentui/core": "0.5.17",
66
66
  "@opentui/solid": "0.5.17",
67
67
  "@types/bun": "1.3.14",