opencode-courier 0.6.0 → 0.7.1

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
@@ -14,15 +14,14 @@ polling.
14
14
  [![License: MIT](https://img.shields.io/npm/l/opencode-courier)](LICENSE)
15
15
  [![Supported OpenCode V2 version](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2Fivopogace%2Fopencode-courier%2Fmain%2Fpackage.json&query=%24.peerDependencies%5B%27%40opencode%2Fplugin%27%5D&label=opencode&color=blue)](#supported-opencode-version)
16
16
 
17
- ![A parent spawns two slow children, writes a README itself, and is woken by each report](docs/demo-async-tui.gif)
17
+ https://github.com/user-attachments/assets/74a50b9c-462a-43f5-8737-bc7dd1917988
18
18
 
19
- A parent writes a README while two slow children work, and each report wakes it ([the
20
- prompt](#demos)).
19
+ The edge cases on the live terminal UI, narrated ([what it shows](#demos)).
21
20
 
22
21
  ## Quickstart
23
22
 
24
23
  ```bash
25
- 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.30 # OpenCode V2, at the version courier is tested on
26
25
  opencode plugin add opencode-courier # by name, so OpenCode offers new releases
27
26
  opencode # in any folder
28
27
  ```
@@ -44,7 +43,7 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
44
43
 
45
44
  Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
46
45
  (`@opencode-ai/plugin`), and needs OpenCode V2. Each release is pinned to one V2 version and tested
47
- 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.30**. CI also runs the live
48
47
  suite on the newest OpenCode release, on `main` and nightly, so a host release that breaks the
49
48
  plugin shows up there first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
50
49
 
@@ -80,21 +79,18 @@ idle); `queue: true` waits until the current turn ends.
80
79
 
81
80
  ## Demos
82
81
 
83
- **The parent does not wait.** Prompt: *"Spawn two children: one runs `sleep 30` then lists the
84
- exports of math.js, the other runs `sleep 45` then lists the exports of text.js. Don't wait for
85
- them: meanwhile write a short README.md for this folder yourself, and add their reports to it when
86
- they arrive."* The recording at the top is this run: the parent writes its README and finishes its
87
- turn 16 seconds in, while both children still sleep; each report (the `Message from ses_…` line)
88
- then starts a new turn on its own, and the parent folds it into the README.
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:
89
84
 
90
- **A child's question reaches you.** Prompt: *"Spawn a child session to add a multiply function to
91
- math.js. It must ask me first whether to name it multiply, times or product."*
92
-
93
- ![A child's question opens as a form in the parent's session, times is picked, and the child carries on](docs/demo-question-tui.gif)
94
-
95
- The child's question opens in the parent's session as OpenCode's own question form, `times` is
96
- picked, and the child adds `times` to math.js and reports. Both recordings are real runs with a free
97
- model on [OpenCode Zen](https://opencode.ai/zen) (Muse Spark 1.3).
85
+ - **A join group:** one member reports and its report is held with the group, while the other waits
86
+ on a permission request, so the footer says one is waiting on you. You allow it from the asks inbox
87
+ (`ctrl+x k`), and both reports reach the parent in one message.
88
+ - **A child whose turn fails:** OpenCode shows "Session failed", the parent is told, and the
89
+ `/courier tree` row reads `failed`.
90
+ - **A child that ends its turn without a report:** the parent is told, and the row says so.
91
+ - **`/courier stop`:** pick the session, confirm, and it is stopped.
92
+ - **A form only you can answer:** a child's web search shows a form; the footer, the panel row and
93
+ the asks inbox point to it, and answering it there wakes the parent.
98
94
 
99
95
  ## Recursive orchestration
100
96
 
@@ -117,7 +113,8 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
117
113
 
118
114
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
119
115
  |---|---|
120
- | 0.5.0, 0.6.0 | 2.0.29 |
116
+ | 0.7.1 | 2.0.30 |
117
+ | 0.5.0, 0.6.0, 0.7.0 | 2.0.29 |
121
118
  | 0.4.0, 0.4.1 | 2.0.28 |
122
119
  | 0.2.3, 0.3.0 | 2.0.26 |
123
120
  | 0.2.2 | 2.0.24 |
@@ -126,12 +123,12 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
126
123
  | 0.1.6 | 0.0.0-beta-19271 (the beta line: `opencode2` from `@opencode-ai/cli`, and `@opencode-ai/plugin`) |
127
124
 
128
125
  The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
129
- OpenCode 2.0.29 only; the server part works as before on older V2 hosts from 2.0.4 on.
126
+ OpenCode 2.0.30 only; the server part works as before on older V2 hosts from 2.0.4 on.
130
127
 
131
128
  Check yours with `opencode --version`, and install the matching CLI with:
132
129
 
133
130
  ```bash
134
- npm install -g @opencode/cli@2.0.29
131
+ npm install -g @opencode/cli@2.0.30
135
132
  ```
136
133
 
137
134
  ### The plugin
@@ -213,13 +210,20 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
213
210
  plugin, with the error or its last reply, waking it if idle; one that waits on a request, its own
214
211
  children, a scheduled message or a webhook is not, nor is one that answers what you typed in its
215
212
  session. [More](docs/reference.md#a-child-that-fails).
213
+ - **A message that fails to wake its session is sent again.** A report, notice or scheduled message
214
+ (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
215
+ more under the same inbox id, which OpenCode takes for the same message, not a second one. If the
216
+ session still does not take it, the footer and the tree panel show it as `stalled`, for you to
217
+ prompt it. [More](docs/reference.md#a-missed-wake).
216
218
  - **Permission requests and questions reach you.** A child's request or question is passed to the
217
219
  session at the top of its tree, any number of levels down. For a request, that session proposes an
218
220
  answer with `courier_answer`, and OpenCode asks you in its own permission prompt, in that session
219
221
  and under its rules, before anything is passed on: its model cannot approve, only propose. For a
220
222
  question, it asks you and passes your answer back, or links its own question to the child's so your
221
223
  pick goes straight to it. Whichever answer comes first counts, and the other is withdrawn.
222
- [More](docs/reference.md#a-child-that-asks-for-permission).
224
+ A request starts a turn in the top session if it is idle; if you answer it somewhere else, the
225
+ note that it is settled waits for that session's next turn, usually the child's report or its
226
+ silence, instead of starting one of its own. [More](docs/reference.md#a-child-that-asks-for-permission).
223
227
  - **Some forms only you can answer**, such as OpenCode's web search asking for a provider: the top
224
228
  session is told, and you answer in the child's session. [More](docs/reference.md#a-child-that-shows-a-form).
225
229
  - **You can see and stop a whole subtree** with `courier_tree` and `courier_stop`, or in the
@@ -257,8 +261,9 @@ does not. Each spawned session's brief says when to split its task and when to d
257
261
 
258
262
  The package ships a part for OpenCode's terminal UI, which the TUI loads from the server's plugin
259
263
  list; the server half behaves the same whether or not a TUI is attached. In a session that started
260
- sessions, the prompt's footer shows what the tree under it is doing: how many run, wait on you (a
261
- permission request, a question or a form they show) or are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
264
+ sessions, the prompt's footer shows what the tree under it is doing: how many wait on you (a
265
+ permission request, a question or a form they show), are [stalled](docs/reference.md#a-missed-wake),
266
+ run or are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
262
267
  reported:
263
268
 
264
269
  ```
@@ -267,7 +272,7 @@ courier 1 waiting on you · 2 running · group pair 1/2
267
272
 
268
273
  `/courier tree` opens a panel beside the session with the tree under it, a row per session indented
269
274
  by depth, with what it does, its report and its group's standing; a session waiting on you gets a line per
270
- request or form (`shows a form: Web Search`), a session that started others gets
275
+ request or form (`shows a form: Web Search`), a stalled one since when its message waits, a session that started others gets
271
276
  a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. A tree that has
272
277
  started more than `maxTotal` sessions over time is listed live ones first, then newest, and the header
273
278
  says how many older ones are left out; the footer counts them all. `↑`/`↓` select,
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ 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
11
  import { addSkill } from "./skill.js";
12
- import { addTreeTools, deliverReleased, dirtyOf, headOf, inspectWorktree, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, together, treeEvents, waitsOn, } from "./tree/index.js";
12
+ import { addTreeTools, deliverReleased, wakeMissed, dirtyOf, headOf, inspectWorktree, pruneExpired, readLimits, readNativeChildren, registerTreeRpc, shapeContext, together, treeEvents, waitsOn, } from "./tree/index.js";
13
13
  import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince, } from "./webhook/index.js";
14
14
  /** The spawns under way in the process, outside the hub, so spawns in other locations, copies and hub versions count them. */
15
15
  const gate = sharedState("spawning", () => ({ reserved: new Set(), turn: Promise.resolve() }));
@@ -19,11 +19,12 @@ const inbox = sharedState("inbox", () => new Map());
19
19
  const shown = sharedState("formsShown", () => new Map());
20
20
  /**
21
21
  * What each tick of the scheduler does, in order, through the first loaded member, of whichever copy:
22
- * due `courier_later` messages, then released join groups. Every hub-v2 member's `later` is its LaterPorts.
22
+ * due `courier_later` messages, released join groups, then missed wakes. Every hub-v2 member's `later` is its LaterPorts.
23
23
  */
24
24
  const jobs = [
25
25
  dueJob,
26
26
  (owner, claimed) => deliverReleased(owner.later, claimed),
27
+ (owner, claimed) => wakeMissed(owner.later, claimed),
27
28
  ];
28
29
  const { join, track, nudge } = openInProcess(jobs);
29
30
  /** The permission domains of the loaded instances: every member's is its location's, whichever copy joined it. */
@@ -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.",
@@ -6,6 +6,7 @@ import { awaited, reportOf, stoppedSincePrompt } from "./report.js";
6
6
  import { childStore, groupStore } from "./store.js";
7
7
  import { lastReply } from "./status.js";
8
8
  import { failureNotice, silentNotice } from "./text.js";
9
+ import { forgetWakes, settleWake, wakeEnded, wakeStarted, watchWake } from "./wake.js";
9
10
  /**
10
11
  * Tells the parent of a child whose turn failed, since a child that cannot reach its model cannot
11
12
  * call courier_send. Returns the parents told.
@@ -193,13 +194,37 @@ function routeTree(ports, state, outside, event) {
193
194
  return noteDeleted(ports, event);
194
195
  return undefined;
195
196
  }
197
+ /** The events that move a courier message waiting in a session's inbox: enqueued, taken, cancelled, and the turns around it. */
198
+ function routeWake(ports, event) {
199
+ if (event.type === "session.inbox.enqueued")
200
+ return watchWake(ports, event);
201
+ if (event.type === "session.inbox.delivered" || event.type === "session.inbox.cancelled")
202
+ return settleWake(ports, event);
203
+ if (event.type === "session.execution.started")
204
+ return wakeStarted(ports, event);
205
+ if (TURN_ENDS.has(event.type))
206
+ return wakeEnded(ports, event);
207
+ if (event.type === "session.deleted")
208
+ return forgetWakes(ports, event.data.sessionID);
209
+ return undefined;
210
+ }
211
+ /** The tree's handling of an event alongside the wake watch's, which touches other keys and never fails the tree's. */
212
+ async function route(ports, state, outside, event) {
213
+ const watched = routeWake(ports, event)?.catch((error) => ports.log(`courier watch: wake of ${event.type}: ${String(error)}`));
214
+ try {
215
+ return await routeTree(ports, state, outside, event);
216
+ }
217
+ finally {
218
+ await watched;
219
+ }
220
+ }
196
221
  /**
197
222
  * Tells parents of a child's failed or silent turn, unless it waits on what `outside` says, and notes
198
223
  * its prompts, interruptions and deletion; `after` a turn's end, seals the groups of its session.
199
224
  */
200
225
  export function treeEvents(state, outside) {
201
226
  return {
202
- handle: (ports, event) => routeTree(ports, state, outside, event),
227
+ handle: (ports, event) => route(ports, state, outside, event),
203
228
  // After: a silent end is judged while the group the nudge may deliver is still there to wait on.
204
229
  after: (ports, event) => (TURN_ENDS.has(event.type) ? sealGroups(ports, event) : undefined),
205
230
  };
@@ -8,3 +8,4 @@ export { allEntries, answeringTop, lineage, pruneExpired, RETENTION_MS } from ".
8
8
  export { subtree } from "./subtree.js";
9
9
  export { registerTreeRpc } from "./rpc-handlers.js";
10
10
  export { addTreeTools } from "./tools.js";
11
+ export { wakeMissed } from "./wake.js";
@@ -1,4 +1,4 @@
1
- // What the rest of the plugin uses of the session tree: its tools, RPC, event module, context hook and tick job
1
+ // What the rest of the plugin uses of the session tree: its tools, RPC, event module, context hook and tick jobs
2
2
  // for `src/index.ts`, and the lineage and subtree queries and types `person/` reads.
3
3
  export { dirtyOf, headOf, inspectWorktree } from "./cleanup.js";
4
4
  export { shapeContext } from "./context.js";
@@ -9,3 +9,4 @@ export { allEntries, answeringTop, lineage, pruneExpired, RETENTION_MS } from ".
9
9
  export { subtree } from "./subtree.js";
10
10
  export { registerTreeRpc } from "./rpc-handlers.js";
11
11
  export { addTreeTools } from "./tools.js";
12
+ export { wakeMissed } from "./wake.js";
@@ -12,7 +12,7 @@ export declare const waitOf: (pending: Pending) => Wait;
12
12
  export declare const formWait: (form: ShownForm) => Wait;
13
13
  /**
14
14
  * The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, the live ones
15
- * first, then the newest, each as `row` reads it; `truncated` counts the older ones left out.
15
+ * first, then the newest, each as `row` reads it; `truncated` counts the older ones left out, and `stalled` says the root is.
16
16
  */
17
17
  export declare function panel(ports: CourierPorts, rootID: string): Promise<Tree>;
18
18
  export {};
@@ -3,6 +3,7 @@ import { lastStatus, stoppedSincePrompt } from "./report.js";
3
3
  import { standingIn } from "./status.js";
4
4
  import { childStore } from "./store.js";
5
5
  import { readTree } from "./subtree.js";
6
+ import { stalledAmong } from "./wake.js";
6
7
  /** The last tool call in a session's messages, and when it was made. */
7
8
  export function lastTool(messages) {
8
9
  for (const message of messages.toReversed()) {
@@ -30,16 +31,21 @@ function reportRow(report, status) {
30
31
  return { report: { state: "ended", progressed: report.progressed } };
31
32
  return { report: { state: "reported", progressed: report.progressed, ...(status ? { status } : {}) } };
32
33
  }
33
- /** What a session does: waiting on a request to the person or a form it shows them, running, idle, or gone when OpenCode no longer knows it. */
34
- function stateOf(info, waits, running) {
34
+ /**
35
+ * What a session does: waiting on a request to the person or a form it shows them, stalled, running, idle, or gone when
36
+ * OpenCode no longer knows it. A message waiting in its inbox makes a stalled session look busy.
37
+ */
38
+ function stateOf(info, waits, stalled, running) {
35
39
  if (!info)
36
40
  return "gone";
37
41
  if (waits)
38
42
  return "waiting";
43
+ if (stalled)
44
+ return "stalled";
39
45
  return running ? "running" : "idle";
40
46
  }
41
47
  /** One session of the panel: what it is doing, its report and group standing, its last tool call and its cost. */
42
- async function row(ports, { entry, depth, info, pending, forms, report }) {
48
+ async function row(ports, { entry, depth, info, pending, forms, report }, stalled) {
43
49
  const { sessionID } = entry;
44
50
  const [messages, status, standing, stopped] = await Promise.all([
45
51
  ports.session.context({ sessionID }).catch(() => []),
@@ -54,7 +60,8 @@ async function row(ports, { entry, depth, info, pending, forms, report }) {
54
60
  parentID: entry.parentID,
55
61
  title: info?.title ?? entry.title,
56
62
  depth,
57
- state: stateOf(info, pending.length + forms.length, running),
63
+ state: stateOf(info, pending.length + forms.length, stalled !== undefined, running),
64
+ ...(stalled !== undefined ? { stalled } : {}),
58
65
  ...(info?.outcome ? { outcome: info.outcome } : {}),
59
66
  ...(stopped ? { stopped } : {}),
60
67
  ...reportRow(report, status),
@@ -69,10 +76,12 @@ async function row(ports, { entry, depth, info, pending, forms, report }) {
69
76
  }
70
77
  /**
71
78
  * The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, the live ones
72
- * first, then the newest, each as `row` reads it; `truncated` counts the older ones left out.
79
+ * first, then the newest, each as `row` reads it; `truncated` counts the older ones left out, and `stalled` says the root is.
73
80
  */
74
81
  export async function panel(ports, rootID) {
75
82
  const { kept, leftOut } = await readTree(ports, rootID);
76
- const rows = await Promise.all(kept.map((node) => row(ports, node)));
77
- return { rows, ...(leftOut ? { truncated: leftOut } : {}) };
83
+ const stalled = await stalledAmong(ports, [rootID, ...kept.map((node) => node.entry.sessionID)]);
84
+ const rows = await Promise.all(kept.map((node) => row(ports, node, stalled.get(node.entry.sessionID))));
85
+ const own = stalled.get(rootID);
86
+ return { rows, ...(leftOut ? { truncated: leftOut } : {}), ...(own !== undefined ? { stalled: own } : {}) };
78
87
  }
@@ -80,6 +80,35 @@ export interface ChildStore {
80
80
  /** Removes report keys `reported` found, each best effort. */
81
81
  dropReported(keys: ReadonlyArray<string>): Promise<void>;
82
82
  }
83
+ /**
84
+ * A courier message waiting in a session's inbox, watched until it is delivered: `running` while the session runs a
85
+ * turn, which takes it at its next step; `idleAt` since the session was last seen idle with it waiting.
86
+ */
87
+ export interface Wake {
88
+ readonly sessionID: string;
89
+ readonly inboxID: string;
90
+ /** When OpenCode enqueued it. */
91
+ readonly at: number;
92
+ /** Its text and description, which waking the session again sends under the same inbox id. */
93
+ readonly text: string;
94
+ readonly description?: string;
95
+ readonly running: boolean;
96
+ readonly idleAt?: number;
97
+ /** When the scheduler woke the session again, the message still waiting. */
98
+ readonly rewoken?: number;
99
+ /** When the session was found still not woken after that: it is stalled. */
100
+ readonly stalled?: number;
101
+ }
102
+ export interface WakeStore {
103
+ /** Writes a watched message, replacing what was kept of it. */
104
+ put(wake: Wake): Promise<void>;
105
+ /** The messages watched in a session's inbox. */
106
+ of(sessionID: string): Promise<Wake[]>;
107
+ /** Every message watched. */
108
+ all(): Promise<Wake[]>;
109
+ /** Stops watching a message: delivered, cancelled, or its session gone. */
110
+ remove(sessionID: string, inboxID: string): Promise<void>;
111
+ }
83
112
  /** A member of a join group with where it is kept. */
84
113
  export interface Membership extends Member {
85
114
  readonly parentID: string;
@@ -14,26 +14,29 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
14
14
  }>;
15
15
  readonly output: import("effect/StandardSchema").StandardSchemaV1<{
16
16
  readonly running: number;
17
- readonly waiting: number;
18
17
  readonly idle: number;
19
18
  readonly groups: readonly {
20
19
  readonly name: string;
21
20
  readonly reported: number;
22
21
  readonly members: number;
23
22
  }[];
23
+ readonly waiting: number;
24
+ readonly stalled?: number | undefined;
24
25
  }, {
25
26
  readonly running: number;
26
- readonly waiting: number;
27
27
  readonly idle: number;
28
28
  readonly groups: readonly {
29
29
  readonly name: string;
30
30
  readonly reported: number;
31
31
  readonly members: number;
32
32
  }[];
33
+ readonly waiting: number;
34
+ readonly stalled?: number | undefined;
33
35
  }> & import("effect/Schema").Struct<{
34
36
  readonly running: import("effect/Schema").Int;
35
37
  readonly waiting: import("effect/Schema").Int;
36
38
  readonly idle: import("effect/Schema").Int;
39
+ readonly stalled: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
37
40
  readonly groups: import("effect/Schema").$Array<import("effect/Schema").Struct<{
38
41
  readonly name: import("effect/Schema").String;
39
42
  readonly reported: import("effect/Schema").Int;
@@ -57,7 +60,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
57
60
  readonly depth: number;
58
61
  readonly created: number;
59
62
  readonly isolated: boolean;
60
- readonly state: "running" | "idle" | "gone" | "waiting";
63
+ readonly state: "running" | "idle" | "gone" | "stalled" | "waiting";
61
64
  readonly waits: readonly ({
62
65
  readonly type: "permission";
63
66
  readonly action: string;
@@ -80,6 +83,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
80
83
  readonly state: "reported" | "ended" | "owed";
81
84
  readonly status?: string | undefined;
82
85
  } | undefined;
86
+ readonly stalled?: number | undefined;
83
87
  readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
84
88
  readonly tool?: {
85
89
  readonly name: string;
@@ -87,6 +91,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
87
91
  } | undefined;
88
92
  readonly cost?: number | undefined;
89
93
  }[];
94
+ readonly stalled?: number | undefined;
90
95
  readonly truncated?: number | undefined;
91
96
  }, {
92
97
  readonly rows: readonly {
@@ -96,7 +101,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
96
101
  readonly depth: number;
97
102
  readonly created: number;
98
103
  readonly isolated: boolean;
99
- readonly state: "running" | "idle" | "gone" | "waiting";
104
+ readonly state: "running" | "idle" | "gone" | "stalled" | "waiting";
100
105
  readonly waits: readonly ({
101
106
  readonly type: "permission";
102
107
  readonly action: string;
@@ -119,6 +124,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
119
124
  readonly state: "reported" | "ended" | "owed";
120
125
  readonly status?: string | undefined;
121
126
  } | undefined;
127
+ readonly stalled?: number | undefined;
122
128
  readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
123
129
  readonly tool?: {
124
130
  readonly name: string;
@@ -126,6 +132,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
126
132
  } | undefined;
127
133
  readonly cost?: number | undefined;
128
134
  }[];
135
+ readonly stalled?: number | undefined;
129
136
  readonly truncated?: number | undefined;
130
137
  }> & import("effect/Schema").Struct<{
131
138
  readonly rows: import("effect/Schema").$Array<import("effect/Schema").Struct<{
@@ -133,7 +140,8 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
133
140
  readonly parentID: import("effect/Schema").String;
134
141
  readonly title: import("effect/Schema").String;
135
142
  readonly depth: import("effect/Schema").Int;
136
- readonly state: import("effect/Schema").Literals<readonly ["running", "waiting", "idle", "gone"]>;
143
+ readonly state: import("effect/Schema").Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
144
+ readonly stalled: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
137
145
  readonly outcome: import("effect/Schema").optionalKey<import("effect/Schema").Literals<readonly ["succeeded", "failed", "interrupted"]>>;
138
146
  readonly stopped: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
139
147
  readonly report: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
@@ -166,6 +174,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
166
174
  }>]>>;
167
175
  }>>;
168
176
  readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
177
+ readonly stalled: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
169
178
  }>;
170
179
  };
171
180
  readonly stop: {