opencode-courier 0.5.0 → 0.7.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.
- package/README.md +39 -30
- package/dist/index.js +9 -5
- package/dist/person/form.d.ts +4 -1
- package/dist/person/form.js +5 -0
- package/dist/person/index.d.ts +2 -1
- package/dist/person/index.js +1 -0
- package/dist/person/permission.d.ts +14 -0
- package/dist/person/permission.js +15 -0
- package/dist/person/ports.d.ts +8 -2
- package/dist/person/rpc-handlers.d.ts +37 -30
- package/dist/person/rpc-handlers.js +26 -16
- package/dist/person/rpc.d.ts +43 -24
- package/dist/person/rpc.js +4 -3
- package/dist/person/tui.d.ts +23 -12
- package/dist/tree/domain.d.ts +23 -0
- package/dist/tree/domain.js +26 -0
- package/dist/tree/events.d.ts +2 -2
- package/dist/tree/events.js +31 -4
- package/dist/tree/index.d.ts +2 -1
- package/dist/tree/index.js +2 -1
- package/dist/tree/panel.d.ts +7 -2
- package/dist/tree/panel.js +29 -17
- package/dist/tree/ports.d.ts +36 -0
- package/dist/tree/roster.d.ts +5 -0
- package/dist/tree/roster.js +5 -3
- package/dist/tree/rpc-handlers.d.ts +55 -6
- package/dist/tree/rpc-handlers.js +30 -1
- package/dist/tree/rpc.d.ts +111 -22
- package/dist/tree/rpc.js +30 -9
- 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/subtree.d.ts +40 -9
- package/dist/tree/subtree.js +40 -19
- package/dist/tree/summary.d.ts +2 -2
- package/dist/tree/summary.js +17 -16
- package/dist/tree/text.js +4 -1
- package/dist/tree/tools.js +2 -1
- package/dist/tree/tui.d.ts +17 -6
- package/dist/tree/wake.d.ts +68 -0
- package/dist/tree/wake.js +119 -0
- package/dist/tui.js +617 -197
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,10 +14,9 @@ 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
|
|
|
@@ -45,8 +44,8 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
|
|
|
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
46
|
against it end to end; the current one is tested on **OpenCode 2.0.29**. CI also runs the live
|
|
48
|
-
suite on the newest OpenCode release, so a host release that breaks the
|
|
49
|
-
first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
|
|
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: [Supported OpenCode version](#supported-opencode-version).
|
|
50
49
|
|
|
51
50
|
## Courier or the built-in subagent tool?
|
|
52
51
|
|
|
@@ -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,7 @@ 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 | 2.0.29 |
|
|
116
|
+
| 0.5.0, 0.6.0, 0.7.0 | 2.0.29 |
|
|
121
117
|
| 0.4.0, 0.4.1 | 2.0.28 |
|
|
122
118
|
| 0.2.3, 0.3.0 | 2.0.26 |
|
|
123
119
|
| 0.2.2 | 2.0.24 |
|
|
@@ -213,6 +209,11 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
|
|
|
213
209
|
plugin, with the error or its last reply, waking it if idle; one that waits on a request, its own
|
|
214
210
|
children, a scheduled message or a webhook is not, nor is one that answers what you typed in its
|
|
215
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
|
+
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).
|
|
216
217
|
- **Permission requests and questions reach you.** A child's request or question is passed to the
|
|
217
218
|
session at the top of its tree, any number of levels down. For a request, that session proposes an
|
|
218
219
|
answer with `courier_answer`, and OpenCode asks you in its own permission prompt, in that session
|
|
@@ -226,7 +227,8 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
|
|
|
226
227
|
[terminal](#in-the-terminal). [More](docs/reference.md#seeing-and-stopping-a-subtree).
|
|
227
228
|
|
|
228
229
|
Children, held reports, scheduled messages and open questions survive a compaction or a restart;
|
|
229
|
-
entries are dropped after 14 days
|
|
230
|
+
entries are dropped after 14 days, or at once when OpenCode deletes the child, unless it is isolated
|
|
231
|
+
([roster](docs/reference.md#roster)).
|
|
230
232
|
|
|
231
233
|
## Session trees
|
|
232
234
|
|
|
@@ -256,28 +258,35 @@ does not. Each spawned session's brief says when to split its task and when to d
|
|
|
256
258
|
|
|
257
259
|
The package ships a part for OpenCode's terminal UI, which the TUI loads from the server's plugin
|
|
258
260
|
list; the server half behaves the same whether or not a TUI is attached. In a session that started
|
|
259
|
-
sessions, the prompt's footer shows what the tree under it is doing: how many
|
|
260
|
-
|
|
261
|
+
sessions, the prompt's footer shows what the tree under it is doing: how many wait on you (a
|
|
262
|
+
permission request, a question or a form they show), are [stalled](docs/reference.md#a-missed-wake),
|
|
263
|
+
run or are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
|
|
261
264
|
reported:
|
|
262
265
|
|
|
263
266
|
```
|
|
264
|
-
courier
|
|
267
|
+
courier 1 waiting on you · 2 running · group pair 1/2
|
|
265
268
|
```
|
|
266
269
|
|
|
267
270
|
`/courier tree` opens a panel beside the session with the tree under it, a row per session indented
|
|
268
|
-
by depth, with what it does, its report and its group's standing; a session
|
|
269
|
-
|
|
271
|
+
by depth, with what it does, its report and its group's standing; a session waiting on you gets a line per
|
|
272
|
+
request or form (`shows a form: Web Search`), a stalled one since when its message waits, a session that started others gets
|
|
273
|
+
a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. A tree that has
|
|
274
|
+
started more than `maxTotal` sessions over time is listed live ones first, then newest, and the header
|
|
275
|
+
says how many older ones are left out; the footer counts them all. `↑`/`↓` select,
|
|
270
276
|
`enter` opens the session, `s` stops it and everything under it after you confirm, as `courier_stop`
|
|
271
|
-
would,
|
|
277
|
+
would, `x` forgets a `gone` session after you confirm, removing its worktree by `courier_cleanup`'s
|
|
278
|
+
rules, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
|
|
272
279
|
follows you between sessions: into one of its tree it keeps that tree, the session on screen marked
|
|
273
280
|
`(on screen)`; elsewhere it shows that session's tree, or hides while you are in a session without one,
|
|
274
281
|
or on the home screen, and shows again once you are back in one; only `q` closes it for good.
|
|
275
282
|
|
|
276
283
|
`ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on you" or a click
|
|
277
284
|
on "waiting on you" opens the asks inbox: every permission request, question and form waiting
|
|
278
|
-
anywhere in the tree,
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
anywhere in the tree, however many sessions it has, oldest first, after the top session's own permission
|
|
286
|
+
requests, such as the prompt `courier_answer` has OpenCode show there, with where it comes from, kept current
|
|
287
|
+
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
|
|
288
|
+
sounds for a child's report and a group's release, and OpenCode's own for a new ask, which the courier
|
|
289
|
+
adds a notification to for a native child ([more](docs/reference.md#in-the-terminal)).
|
|
281
290
|
|
|
282
291
|
While a permission prompt or form is shown in the session, OpenCode hides the footer, so open the
|
|
283
292
|
inbox with `ctrl+x k` (or `/asks` once the prompt is back).
|
package/dist/index.js
CHANGED
|
@@ -7,21 +7,24 @@ import { watchFromHub } from "./platform/events.js";
|
|
|
7
7
|
import { hub, openInProcess, permissions as domains, sharedState } from "./platform/hub.js";
|
|
8
8
|
import { builtVersions, versionNotice } from "./platform/version.js";
|
|
9
9
|
import { addSchedulingTools, cancelScheduled, dueJob, isScheduled } from "./scheduling/index.js";
|
|
10
|
-
import { addPersonTools, asksPermission, asksQuestion, joinRelay, noticeCutOff, pendingOf, pendingQuestions, personEvents, portsAt, registerPersonRpc, relayQuestions, } from "./person/index.js";
|
|
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() }));
|
|
16
16
|
/** The type of each item enqueued for a spawned session, outside the hub like the gate. */
|
|
17
17
|
const inbox = sharedState("inbox", () => new Map());
|
|
18
|
+
/** The forms shown now, which the person events keep and the footer and tree panel count; outside the hub like the inbox. */
|
|
19
|
+
const shown = sharedState("formsShown", () => new Map());
|
|
18
20
|
/**
|
|
19
21
|
* What each tick of the scheduler does, in order, through the first loaded member, of whichever copy:
|
|
20
|
-
* due `courier_later` messages,
|
|
22
|
+
* due `courier_later` messages, released join groups, then missed wakes. Every hub-v2 member's `later` is its LaterPorts.
|
|
21
23
|
*/
|
|
22
24
|
const jobs = [
|
|
23
25
|
dueJob,
|
|
24
26
|
(owner, claimed) => deliverReleased(owner.later, claimed),
|
|
27
|
+
(owner, claimed) => wakeMissed(owner.later, claimed),
|
|
25
28
|
];
|
|
26
29
|
const { join, track, nudge } = openInProcess(jobs);
|
|
27
30
|
/** The permission domains of the loaded instances: every member's is its location's, whichever copy joined it. */
|
|
@@ -30,7 +33,7 @@ const permissions = domains;
|
|
|
30
33
|
// several subscriptions and copies see the same events. The scheduler's claim set is the hub's own.
|
|
31
34
|
const { watched } = hub;
|
|
32
35
|
const watchState = { seen: watched.seen, inbox };
|
|
33
|
-
const personState = { ...watched, forms: hub.forms };
|
|
36
|
+
const personState = { ...watched, forms: hub.forms, shown };
|
|
34
37
|
/**
|
|
35
38
|
* What a session waits on outside the tree, for the silent-turn rule, cheapest first: a request of its
|
|
36
39
|
* own to the person, then a scheduled message or a webhook it subscribed to since its last prompt.
|
|
@@ -93,6 +96,7 @@ export const courier = () => Plugin.define({
|
|
|
93
96
|
]);
|
|
94
97
|
return [...requests, ...questions];
|
|
95
98
|
},
|
|
99
|
+
forms: (sessionID) => Promise.resolve(formsShown(shown, sessionID)),
|
|
96
100
|
};
|
|
97
101
|
const cleanupPorts = { ...ports, inspect: inspectWorktree };
|
|
98
102
|
const answerPorts = { storage: ctx.storage, permissions, assert: (input) => ctx.permission.assert(input) };
|
|
@@ -123,7 +127,7 @@ export const courier = () => Plugin.define({
|
|
|
123
127
|
// What the TUI part reads and its stop; a host that cannot register it still gets the rest, the tools included.
|
|
124
128
|
await Promise.resolve()
|
|
125
129
|
.then(() => registerTreeRpc(ctx.rpc, stopPorts))
|
|
126
|
-
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions
|
|
130
|
+
.then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions }))
|
|
127
131
|
.catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
|
|
128
132
|
await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, log }, event));
|
|
129
133
|
const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
|
package/dist/person/form.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ShownForm } from "../tree/index.js";
|
|
2
|
+
import { type Form, type FormsShown, type PersonPorts, type PersonState } from "./ports.js";
|
|
2
3
|
/** The `metadata.kind` of the forms OpenCode's question tool asks with; the question relay passes those on. */
|
|
3
4
|
export declare const QUESTION_FORM = "question";
|
|
4
5
|
/** The part of OpenCode's `form.created` event the notice is made from. */
|
|
@@ -24,3 +25,5 @@ export interface FormSettled {
|
|
|
24
25
|
export declare function reportForm(ports: PersonPorts, state: PersonState, event: FormCreated): Promise<string[]>;
|
|
25
26
|
/** Tells the session told about a form that it was answered or withdrawn, so it does not send the person to a form that is gone. */
|
|
26
27
|
export declare function reportFormSettled(ports: PersonPorts, state: PersonState, event: FormSettled): Promise<string[]>;
|
|
28
|
+
/** The forms a session shows now, oldest first, as the tree's `forms` port lists them. */
|
|
29
|
+
export declare const formsShown: (shown: FormsShown, sessionID: string) => ShownForm[];
|
package/dist/person/form.js
CHANGED
|
@@ -15,6 +15,8 @@ export async function reportForm(ports, state, event) {
|
|
|
15
15
|
const form = event.data.form;
|
|
16
16
|
if (kindOf(form) === QUESTION_FORM)
|
|
17
17
|
return [];
|
|
18
|
+
if (!state.forms.settled.has(form.id))
|
|
19
|
+
setBounded(state.shown, form.id, { sessionID: form.sessionID, title: form.title }, SEEN_MAX);
|
|
18
20
|
const chain = await lineage(ports.storage, form.sessionID);
|
|
19
21
|
// As with a permission request, claimed after the lookup, so a form settled meanwhile is not told.
|
|
20
22
|
if (!chain.length || state.forms.settled.has(form.id) || state.forms.told.has(form.id))
|
|
@@ -41,6 +43,7 @@ export async function reportFormSettled(ports, state, event) {
|
|
|
41
43
|
if (!claim(state.seen, event.id))
|
|
42
44
|
return [];
|
|
43
45
|
const { id, sessionID } = event.data;
|
|
46
|
+
state.shown.delete(id);
|
|
44
47
|
const telling = state.forms.told.get(id);
|
|
45
48
|
if (!telling) {
|
|
46
49
|
claim(state.forms.settled, id);
|
|
@@ -62,3 +65,5 @@ export async function reportFormSettled(ports, state, event) {
|
|
|
62
65
|
});
|
|
63
66
|
return [topOf(chain)];
|
|
64
67
|
}
|
|
68
|
+
/** The forms a session shows now, oldest first, as the tree's `forms` port lists them. */
|
|
69
|
+
export const formsShown = (shown, sessionID) => [...shown].flatMap(([id, form]) => (form.sessionID === sessionID ? [{ id, title: form.title }] : []));
|
package/dist/person/index.d.ts
CHANGED
|
@@ -2,9 +2,10 @@ import type { PendingPrompt, WatchPorts } from "../tree/index.js";
|
|
|
2
2
|
import type { PersonPorts } from "./ports.js";
|
|
3
3
|
import type { Prompt } from "./question/index.js";
|
|
4
4
|
export { personEvents } from "./events.js";
|
|
5
|
+
export { formsShown } from "./form.js";
|
|
5
6
|
export { asksPermission, pendingOf } from "./permission.js";
|
|
6
7
|
export type { AnswerPorts, Permissions, Waiting } from "./permission.js";
|
|
7
|
-
export type { PersonPorts, PersonState } from "./ports.js";
|
|
8
|
+
export type { FormsShown, PersonPorts, PersonState } from "./ports.js";
|
|
8
9
|
export { registerPersonRpc } from "./rpc-handlers.js";
|
|
9
10
|
export { asksQuestion, joinRelay, noticeCutOff, pendingQuestions, portsAt, relayQuestions } from "./question/index.js";
|
|
10
11
|
export type { QuestionPorts } from "./question/index.js";
|
package/dist/person/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { personEvents } from "./events.js";
|
|
2
|
+
export { formsShown } from "./form.js";
|
|
2
3
|
export { asksPermission, pendingOf } from "./permission.js";
|
|
3
4
|
export { registerPersonRpc } from "./rpc-handlers.js";
|
|
4
5
|
export { asksQuestion, joinRelay, noticeCutOff, pendingQuestions, portsAt, relayQuestions } from "./question/index.js";
|
|
@@ -17,6 +17,7 @@ export interface PermissionRequest {
|
|
|
17
17
|
readonly resources: ReadonlyArray<string>;
|
|
18
18
|
readonly save?: ReadonlyArray<string>;
|
|
19
19
|
readonly message?: string;
|
|
20
|
+
readonly metadata?: Readonly<Record<string, unknown>>;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* What answering a permission request did; `answered` is false when no location loaded here held the
|
|
@@ -94,6 +95,19 @@ export declare const isDeclined: (error: unknown) => boolean;
|
|
|
94
95
|
* session, a proposed `once` only once OpenCode allowed it there; `answered` is false when no location loaded here holds it.
|
|
95
96
|
*/
|
|
96
97
|
export declare function answer(ports: AnswerPorts, waiting: Waiting, callerID: string, input: AnswerInput): Promise<PermissionAnswered>;
|
|
98
|
+
/** The child's request a prompt of courier_answer's asks about, read from the metadata `askCaller` gives it; undefined for any other request. */
|
|
99
|
+
export declare function passedOn(metadata: Readonly<Record<string, unknown>> | undefined): {
|
|
100
|
+
sessionID: string;
|
|
101
|
+
requestID: string;
|
|
102
|
+
} | undefined;
|
|
103
|
+
/** The requests a session waits on of its own, in whichever location holds them, each with the child's request it passes on when courier_answer asks it. */
|
|
104
|
+
export declare function ownRequests(permissions: Iterable<Permissions>, sessionID: string): Promise<{
|
|
105
|
+
request: PermissionRequest;
|
|
106
|
+
passesOn: {
|
|
107
|
+
sessionID: string;
|
|
108
|
+
requestID: string;
|
|
109
|
+
} | undefined;
|
|
110
|
+
}[]>;
|
|
97
111
|
/** The requests a session waits on, in whichever location holds them, for courier_status. */
|
|
98
112
|
export declare function pendingOf(permissions: Iterable<Permissions>, sessionID: string): Promise<Pending[]>;
|
|
99
113
|
export {};
|
|
@@ -115,6 +115,21 @@ export async function answer(ports, waiting, callerID, input) {
|
|
|
115
115
|
throw new Error(refusedText(sessionID, requestID, refused));
|
|
116
116
|
return { sessionID, requestID, reply, answered: true };
|
|
117
117
|
}
|
|
118
|
+
/** The child's request a prompt of courier_answer's asks about, read from the metadata `askCaller` gives it; undefined for any other request. */
|
|
119
|
+
export function passedOn(metadata) {
|
|
120
|
+
const { source, asks, from, requestID } = metadata ?? {};
|
|
121
|
+
if (source !== "courier" || asks !== "permission" || typeof from !== "string" || typeof requestID !== "string")
|
|
122
|
+
return undefined;
|
|
123
|
+
return { sessionID: from, requestID };
|
|
124
|
+
}
|
|
125
|
+
/** The requests a session waits on of its own, in whichever location holds them, each with the child's request it passes on when courier_answer asks it. */
|
|
126
|
+
export async function ownRequests(permissions, sessionID) {
|
|
127
|
+
const found = new Map();
|
|
128
|
+
for (const { requests } of await listEverywhere(permissions, sessionID))
|
|
129
|
+
for (const request of requests)
|
|
130
|
+
found.set(request.id, request);
|
|
131
|
+
return [...found.values()].map((request) => ({ request, passesOn: passedOn(request.metadata) }));
|
|
132
|
+
}
|
|
118
133
|
/** The requests a session waits on, in whichever location holds them, for courier_status. */
|
|
119
134
|
export async function pendingOf(permissions, sessionID) {
|
|
120
135
|
const found = new Map();
|
package/dist/person/ports.d.ts
CHANGED
|
@@ -19,15 +19,21 @@ export interface PersonPorts {
|
|
|
19
19
|
readonly now: () => number;
|
|
20
20
|
readonly log: (message: string) => void;
|
|
21
21
|
}
|
|
22
|
+
/** The forms spawned sessions and others show, which only the person can answer, by form id; not the question tool's. */
|
|
23
|
+
export type FormsShown = Map<string, {
|
|
24
|
+
readonly sessionID: string;
|
|
25
|
+
readonly title: string;
|
|
26
|
+
}>;
|
|
22
27
|
/**
|
|
23
|
-
* What every plugin instance in the process shares of the person events: the handled event ids,
|
|
24
|
-
*
|
|
28
|
+
* What every plugin instance in the process shares of the person events: the handled event ids (the tree's too),
|
|
29
|
+
* the requests and forms told about or answered first, and the forms shown now.
|
|
25
30
|
*/
|
|
26
31
|
export interface PersonState {
|
|
27
32
|
readonly seen: Set<string>;
|
|
28
33
|
readonly waiting: Waiting;
|
|
29
34
|
readonly answered: Set<string>;
|
|
30
35
|
readonly forms: FormsTold;
|
|
36
|
+
readonly shown: FormsShown;
|
|
31
37
|
}
|
|
32
38
|
/** The session at the top of a lineage: where its permission requests go, since that is where the person is. */
|
|
33
39
|
export declare const topOf: (chain: RosterEntry[]) => string;
|
|
@@ -6,12 +6,10 @@ import { type Answered, type Asks } from "./rpc.js";
|
|
|
6
6
|
export interface PersonRpcPorts {
|
|
7
7
|
readonly questions: QuestionPorts;
|
|
8
8
|
readonly permissions: () => Iterable<Permissions>;
|
|
9
|
-
/** At most this many sessions of a tree are read, as courier_tree reads them. */
|
|
10
|
-
readonly maxTotal: number;
|
|
11
9
|
}
|
|
12
10
|
/**
|
|
13
|
-
* The permission requests and questions waiting in the courier tree a session is in,
|
|
14
|
-
* top of its lineage down, so each session of the tree sees the same inbox.
|
|
11
|
+
* The permission requests and questions waiting in the courier tree a session is in, the top's own requests first, read from the
|
|
12
|
+
* roster from the top of its lineage down, so each session of the tree sees the same inbox.
|
|
15
13
|
*/
|
|
16
14
|
export declare function asks(ports: PersonRpcPorts, sessionID: string): Promise<Asks>;
|
|
17
15
|
/** Passes the person's answers to a question on, as courier_answer does; a refusal, such as answers that do not fit, comes back as `problem`. */
|
|
@@ -33,6 +31,25 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
33
31
|
readonly sessionID: import("effect/Schema").String;
|
|
34
32
|
}>;
|
|
35
33
|
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
34
|
+
readonly top: string;
|
|
35
|
+
readonly sessions: readonly {
|
|
36
|
+
readonly sessionID: string;
|
|
37
|
+
readonly title: string;
|
|
38
|
+
readonly parentID: string;
|
|
39
|
+
readonly depth: number;
|
|
40
|
+
readonly group?: string | undefined;
|
|
41
|
+
}[];
|
|
42
|
+
readonly permissions: readonly {
|
|
43
|
+
readonly sessionID: string;
|
|
44
|
+
readonly resources: readonly string[];
|
|
45
|
+
readonly action: string;
|
|
46
|
+
readonly requestID: string;
|
|
47
|
+
readonly save?: readonly string[] | undefined;
|
|
48
|
+
readonly passesOn?: {
|
|
49
|
+
readonly sessionID: string;
|
|
50
|
+
readonly requestID: string;
|
|
51
|
+
} | undefined;
|
|
52
|
+
}[];
|
|
36
53
|
readonly questions: readonly {
|
|
37
54
|
readonly questions: readonly {
|
|
38
55
|
readonly options: readonly {
|
|
@@ -47,13 +64,7 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
47
64
|
readonly requestID: string;
|
|
48
65
|
readonly stopped?: boolean | undefined;
|
|
49
66
|
}[];
|
|
50
|
-
|
|
51
|
-
readonly sessionID: string;
|
|
52
|
-
readonly resources: readonly string[];
|
|
53
|
-
readonly action: string;
|
|
54
|
-
readonly requestID: string;
|
|
55
|
-
readonly save?: readonly string[] | undefined;
|
|
56
|
-
}[];
|
|
67
|
+
}, {
|
|
57
68
|
readonly top: string;
|
|
58
69
|
readonly sessions: readonly {
|
|
59
70
|
readonly sessionID: string;
|
|
@@ -62,8 +73,17 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
62
73
|
readonly depth: number;
|
|
63
74
|
readonly group?: string | undefined;
|
|
64
75
|
}[];
|
|
65
|
-
readonly
|
|
66
|
-
|
|
76
|
+
readonly permissions: readonly {
|
|
77
|
+
readonly sessionID: string;
|
|
78
|
+
readonly resources: readonly string[];
|
|
79
|
+
readonly action: string;
|
|
80
|
+
readonly requestID: string;
|
|
81
|
+
readonly save?: readonly string[] | undefined;
|
|
82
|
+
readonly passesOn?: {
|
|
83
|
+
readonly sessionID: string;
|
|
84
|
+
readonly requestID: string;
|
|
85
|
+
} | undefined;
|
|
86
|
+
}[];
|
|
67
87
|
readonly questions: readonly {
|
|
68
88
|
readonly questions: readonly {
|
|
69
89
|
readonly options: readonly {
|
|
@@ -78,22 +98,6 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
78
98
|
readonly requestID: string;
|
|
79
99
|
readonly stopped?: boolean | undefined;
|
|
80
100
|
}[];
|
|
81
|
-
readonly permissions: readonly {
|
|
82
|
-
readonly sessionID: string;
|
|
83
|
-
readonly resources: readonly string[];
|
|
84
|
-
readonly action: string;
|
|
85
|
-
readonly requestID: string;
|
|
86
|
-
readonly save?: readonly string[] | undefined;
|
|
87
|
-
}[];
|
|
88
|
-
readonly top: string;
|
|
89
|
-
readonly sessions: readonly {
|
|
90
|
-
readonly sessionID: string;
|
|
91
|
-
readonly title: string;
|
|
92
|
-
readonly parentID: string;
|
|
93
|
-
readonly depth: number;
|
|
94
|
-
readonly group?: string | undefined;
|
|
95
|
-
}[];
|
|
96
|
-
readonly truncated?: number | undefined;
|
|
97
101
|
}> & import("effect/Schema").Struct<{
|
|
98
102
|
readonly top: import("effect/Schema").String;
|
|
99
103
|
readonly sessions: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
@@ -109,6 +113,10 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
109
113
|
readonly action: import("effect/Schema").String;
|
|
110
114
|
readonly resources: import("effect/Schema").$Array<import("effect/Schema").String>;
|
|
111
115
|
readonly save: import("effect/Schema").optionalKey<import("effect/Schema").$Array<import("effect/Schema").String>>;
|
|
116
|
+
readonly passesOn: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
|
|
117
|
+
readonly sessionID: import("effect/Schema").String;
|
|
118
|
+
readonly requestID: import("effect/Schema").String;
|
|
119
|
+
}>>;
|
|
112
120
|
}>>;
|
|
113
121
|
readonly questions: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
114
122
|
readonly sessionID: import("effect/Schema").String;
|
|
@@ -124,7 +132,6 @@ export declare function registerPersonRpc(rpc: Pick<Plugin.Context["rpc"], "regi
|
|
|
124
132
|
}>>;
|
|
125
133
|
readonly stopped: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
|
|
126
134
|
}>>;
|
|
127
|
-
readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
|
|
128
135
|
}>;
|
|
129
136
|
};
|
|
130
137
|
readonly answer: {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { lineage, subtree } from "../tree/index.js";
|
|
2
|
-
import { pendingOf } from "./permission.js";
|
|
2
|
+
import { ownRequests, pendingOf } from "./permission.js";
|
|
3
3
|
import { answerForPerson, pendingQuestions } from "./question/index.js";
|
|
4
4
|
import { personRpc } from "./rpc.js";
|
|
5
5
|
/**
|
|
6
|
-
* The permission requests and questions waiting in the courier tree a session is in,
|
|
7
|
-
* top of its lineage down, so each session of the tree sees the same inbox.
|
|
6
|
+
* The permission requests and questions waiting in the courier tree a session is in, the top's own requests first, read from the
|
|
7
|
+
* roster from the top of its lineage down, so each session of the tree sees the same inbox.
|
|
8
8
|
*/
|
|
9
9
|
export async function asks(ports, sessionID) {
|
|
10
10
|
const { storage } = ports.questions;
|
|
11
11
|
const top = (await lineage(storage, sessionID)).at(-1)?.parentID ?? sessionID;
|
|
12
|
-
const { nodes
|
|
12
|
+
const { nodes } = await subtree(storage, top, { now: ports.questions.now() });
|
|
13
|
+
const own = nodes.length ? await ownRequests(ports.permissions(), top).catch(() => []) : [];
|
|
13
14
|
const waiting = await Promise.all(nodes.map(({ entry }) => Promise.all([
|
|
14
15
|
pendingOf(ports.permissions(), entry.sessionID).catch(() => []),
|
|
15
16
|
pendingQuestions(storage, entry.sessionID).catch(() => []),
|
|
@@ -23,24 +24,33 @@ export async function asks(ports, sessionID) {
|
|
|
23
24
|
depth,
|
|
24
25
|
...(entry.group ? { group: entry.group } : {}),
|
|
25
26
|
})),
|
|
26
|
-
permissions:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
]
|
|
36
|
-
|
|
27
|
+
permissions: [
|
|
28
|
+
...own.map(({ request, passesOn }) => ({
|
|
29
|
+
sessionID: top,
|
|
30
|
+
requestID: request.id,
|
|
31
|
+
action: request.action,
|
|
32
|
+
resources: [...request.resources],
|
|
33
|
+
...(request.save?.length ? { save: [...request.save] } : {}),
|
|
34
|
+
...(passesOn ? { passesOn } : {}),
|
|
35
|
+
})),
|
|
36
|
+
...nodes.flatMap(({ entry }, index) => waiting[index][0].flatMap((pending) => pending.type === "permission"
|
|
37
|
+
? [
|
|
38
|
+
{
|
|
39
|
+
sessionID: entry.sessionID,
|
|
40
|
+
requestID: pending.requestID,
|
|
41
|
+
action: pending.action,
|
|
42
|
+
resources: pending.resources,
|
|
43
|
+
...(pending.save ? { save: pending.save } : {}),
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
: [])),
|
|
47
|
+
],
|
|
37
48
|
questions: nodes.flatMap(({ entry }, index) => waiting[index][1].map(({ requestID, questions, stopped }) => ({
|
|
38
49
|
sessionID: entry.sessionID,
|
|
39
50
|
requestID,
|
|
40
51
|
questions,
|
|
41
52
|
...(stopped ? { stopped } : {}),
|
|
42
53
|
}))),
|
|
43
|
-
...(truncated ? { truncated: ports.maxTotal } : {}),
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
/** Passes the person's answers to a question on, as courier_answer does; a refusal, such as answers that do not fit, comes back as `problem`. */
|