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 +31 -26
- package/dist/index.js +3 -2
- package/dist/person/events.d.ts +2 -2
- package/dist/person/events.js +4 -3
- package/dist/skill.js +11 -6
- package/dist/tree/events.js +26 -1
- package/dist/tree/index.d.ts +1 -0
- package/dist/tree/index.js +2 -1
- package/dist/tree/panel.d.ts +1 -1
- package/dist/tree/panel.js +16 -7
- package/dist/tree/ports.d.ts +29 -0
- package/dist/tree/rpc-handlers.d.ts +14 -5
- package/dist/tree/rpc.d.ts +39 -10
- package/dist/tree/rpc.js +11 -2
- package/dist/tree/stop.js +2 -0
- package/dist/tree/store.d.ts +4 -1
- package/dist/tree/store.js +51 -0
- package/dist/tree/summary.d.ts +2 -2
- package/dist/tree/summary.js +14 -5
- package/dist/tree/text.js +1 -1
- package/dist/tree/tui.d.ts +6 -2
- package/dist/tree/wake.d.ts +69 -0
- package/dist/tree/wake.js +122 -0
- package/dist/tui.js +78 -51
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -14,15 +14,14 @@ polling.
|
|
|
14
14
|
[](LICENSE)
|
|
15
15
|
[](#supported-opencode-version)
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
https://github.com/user-attachments/assets/74a50b9c-462a-43f5-8737-bc7dd1917988
|
|
18
18
|
|
|
19
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
84
|
-
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
261
|
-
permission request, a question or a form they show)
|
|
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,
|
|
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. */
|
package/dist/person/events.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
/**
|
package/dist/person/events.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
"
|
|
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
|
|
19
|
-
"
|
|
20
|
-
"
|
|
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/events.js
CHANGED
|
@@ -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) =>
|
|
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
|
};
|
package/dist/tree/index.d.ts
CHANGED
package/dist/tree/index.js
CHANGED
|
@@ -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
|
|
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";
|
package/dist/tree/panel.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/tree/panel.js
CHANGED
|
@@ -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
|
-
/**
|
|
34
|
-
|
|
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
|
|
77
|
-
|
|
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
|
}
|
package/dist/tree/ports.d.ts
CHANGED
|
@@ -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: {
|