opencode-courier 0.5.0 → 0.6.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 CHANGED
@@ -45,8 +45,8 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
45
45
  Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
46
46
  (`@opencode-ai/plugin`), and needs OpenCode V2. Each release is pinned to one V2 version and tested
47
47
  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 plugin shows up there
49
- first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
48
+ suite on the newest OpenCode release, on `main` and nightly, so a host release that breaks the
49
+ plugin shows up there first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
50
50
 
51
51
  ## Courier or the built-in subagent tool?
52
52
 
@@ -117,7 +117,7 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
117
117
 
118
118
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
119
119
  |---|---|
120
- | 0.5.0 | 2.0.29 |
120
+ | 0.5.0, 0.6.0 | 2.0.29 |
121
121
  | 0.4.0, 0.4.1 | 2.0.28 |
122
122
  | 0.2.3, 0.3.0 | 2.0.26 |
123
123
  | 0.2.2 | 2.0.24 |
@@ -226,7 +226,8 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
226
226
  [terminal](#in-the-terminal). [More](docs/reference.md#seeing-and-stopping-a-subtree).
227
227
 
228
228
  Children, held reports, scheduled messages and open questions survive a compaction or a restart;
229
- entries are dropped after 14 days ([roster](docs/reference.md#roster)).
229
+ entries are dropped after 14 days, or at once when OpenCode deletes the child, unless it is isolated
230
+ ([roster](docs/reference.md#roster)).
230
231
 
231
232
  ## Session trees
232
233
 
@@ -256,28 +257,34 @@ does not. Each spawned session's brief says when to split its task and when to d
256
257
 
257
258
  The package ships a part for OpenCode's terminal UI, which the TUI loads from the server's plugin
258
259
  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 run, wait on you or
260
- are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
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
261
262
  reported:
262
263
 
263
264
  ```
264
- courier 2 running · 1 waiting on you · group pair 1/2
265
+ courier 1 waiting on you · 2 running · group pair 1/2
265
266
  ```
266
267
 
267
268
  `/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 that started others gets
269
- a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. `↑`/`↓` select,
269
+ 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
271
+ a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. A tree that has
272
+ started more than `maxTotal` sessions over time is listed live ones first, then newest, and the header
273
+ says how many older ones are left out; the footer counts them all. `↑`/`↓` select,
270
274
  `enter` opens the session, `s` stops it and everything under it after you confirm, as `courier_stop`
271
- would, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
275
+ would, `x` forgets a `gone` session after you confirm, removing its worktree by `courier_cleanup`'s
276
+ rules, and `q` closes the panel; `/courier stop <session>` stops from the prompt. While open, the panel
272
277
  follows you between sessions: into one of its tree it keeps that tree, the session on screen marked
273
278
  `(on screen)`; elsewhere it shows that session's tree, or hides while you are in a session without one,
274
279
  or on the home screen, and shows again once you are back in one; only `q` closes it for good.
275
280
 
276
281
  `ctrl+x k` (the leader key, then `k`), `/asks` (or `/ask`), the palette's "Courier: what waits on you" or a click
277
282
  on "waiting on you" opens the asks inbox: every permission request, question and form waiting
278
- anywhere in the tree, oldest first, with where it comes from, answered in a dialog with no model in
279
- between; the session that started the tree hears once that you settled it. The TUI plays OpenCode's
280
- sounds for a child's report, a group's release and a new ask ([more](docs/reference.md#in-the-terminal)).
283
+ anywhere in the tree, however many sessions it has, oldest first, after the top session's own permission
284
+ requests, such as the prompt `courier_answer` has OpenCode show there, with where it comes from, kept current
285
+ 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
286
+ sounds for a child's report and a group's release, and OpenCode's own for a new ask, which the courier
287
+ adds a notification to for a native child ([more](docs/reference.md#in-the-terminal)).
281
288
 
282
289
  While a permission prompt or form is shown in the session, OpenCode hides the footer, so open the
283
290
  inbox with `ctrl+x k` (or `/asks` once the prompt is back).
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ 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
12
  import { addTreeTools, deliverReleased, 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";
@@ -15,6 +15,8 @@ import { addWebhookTools, joinReceiver, readConfig, receiving, subscribedSince,
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
22
  * due `courier_later` messages, then released join groups. Every hub-v2 member's `later` is its LaterPorts.
@@ -30,7 +32,7 @@ const permissions = domains;
30
32
  // several subscriptions and copies see the same events. The scheduler's claim set is the hub's own.
31
33
  const { watched } = hub;
32
34
  const watchState = { seen: watched.seen, inbox };
33
- const personState = { ...watched, forms: hub.forms };
35
+ const personState = { ...watched, forms: hub.forms, shown };
34
36
  /**
35
37
  * What a session waits on outside the tree, for the silent-turn rule, cheapest first: a request of its
36
38
  * own to the person, then a scheduled message or a webhook it subscribed to since its last prompt.
@@ -93,6 +95,7 @@ export const courier = () => Plugin.define({
93
95
  ]);
94
96
  return [...requests, ...questions];
95
97
  },
98
+ forms: (sessionID) => Promise.resolve(formsShown(shown, sessionID)),
96
99
  };
97
100
  const cleanupPorts = { ...ports, inspect: inspectWorktree };
98
101
  const answerPorts = { storage: ctx.storage, permissions, assert: (input) => ctx.permission.assert(input) };
@@ -123,7 +126,7 @@ export const courier = () => Plugin.define({
123
126
  // What the TUI part reads and its stop; a host that cannot register it still gets the rest, the tools included.
124
127
  await Promise.resolve()
125
128
  .then(() => registerTreeRpc(ctx.rpc, stopPorts))
126
- .then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions, maxTotal: limits.maxTotal }))
129
+ .then(() => registerPersonRpc(ctx.rpc, { questions: questionPorts, permissions }))
127
130
  .catch((error) => log(`courier rpc: not registered, the TUI part shows nothing and cannot stop: ${String(error)}`));
128
131
  await ctx.session.hook("context", (event) => shapeContext({ storage: ctx.storage, limits, roles, log }, event));
129
132
  const pruned = pruneExpired(ctx.storage, Date.now()).catch((error) => log(`courier roster prune: ${String(error)}`));
@@ -1,4 +1,5 @@
1
- import { type Form, type PersonPorts, type PersonState } from "./ports.js";
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[];
@@ -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 }] : []));
@@ -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";
@@ -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();
@@ -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, shared
24
- * with the tree's, and the requests and forms the top sessions were told about or that were answered first.
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, from the roster: read from the
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
- readonly permissions: readonly {
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 truncated?: number | undefined;
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, from the roster: read from the
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, truncated } = await subtree(storage, top, ports.maxTotal);
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: nodes.flatMap(({ entry }, index) => waiting[index][0].flatMap((pending) => pending.type === "permission"
27
- ? [
28
- {
29
- sessionID: entry.sessionID,
30
- requestID: pending.requestID,
31
- action: pending.action,
32
- resources: pending.resources,
33
- ...(pending.save ? { save: pending.save } : {}),
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`. */
@@ -8,7 +8,7 @@ declare const Session: Schema.Struct<{
8
8
  readonly depth: Schema.Int;
9
9
  readonly group: Schema.optionalKey<Schema.String>;
10
10
  }>;
11
- /** A permission request a session of the tree waits on. */
11
+ /** A permission request a session of the tree, the top among them, waits on. */
12
12
  declare const Permission: Schema.Struct<{
13
13
  readonly sessionID: Schema.String;
14
14
  readonly requestID: Schema.String;
@@ -16,6 +16,11 @@ declare const Permission: Schema.Struct<{
16
16
  readonly resources: Schema.$Array<Schema.String>;
17
17
  /** What "always" would allow from now on; absent when it is not offered. */
18
18
  readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
19
+ /** The request below that allowing this one passes on: the top's courier_answer prompt. */
20
+ readonly passesOn: Schema.optionalKey<Schema.Struct<{
21
+ readonly sessionID: Schema.String;
22
+ readonly requestID: Schema.String;
23
+ }>>;
19
24
  }>;
20
25
  /** A question a session of the tree waits on; `stopped` when its call was cut off and the answer goes as a message. */
21
26
  declare const Question: Schema.Struct<{
@@ -45,6 +50,7 @@ declare const Asks: Schema.Struct<{
45
50
  readonly depth: Schema.Int;
46
51
  readonly group: Schema.optionalKey<Schema.String>;
47
52
  }>>;
53
+ /** The top session's own requests first, then those of the sessions below. */
48
54
  readonly permissions: Schema.$Array<Schema.Struct<{
49
55
  readonly sessionID: Schema.String;
50
56
  readonly requestID: Schema.String;
@@ -52,6 +58,11 @@ declare const Asks: Schema.Struct<{
52
58
  readonly resources: Schema.$Array<Schema.String>;
53
59
  /** What "always" would allow from now on; absent when it is not offered. */
54
60
  readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
61
+ /** The request below that allowing this one passes on: the top's courier_answer prompt. */
62
+ readonly passesOn: Schema.optionalKey<Schema.Struct<{
63
+ readonly sessionID: Schema.String;
64
+ readonly requestID: Schema.String;
65
+ }>>;
55
66
  }>>;
56
67
  readonly questions: Schema.$Array<Schema.Struct<{
57
68
  readonly sessionID: Schema.String;
@@ -67,8 +78,6 @@ declare const Asks: Schema.Struct<{
67
78
  }>>;
68
79
  readonly stopped: Schema.optionalKey<Schema.Boolean>;
69
80
  }>>;
70
- /** Set when the tree had more sessions than `maxTotal`, which were not read. */
71
- readonly truncated: Schema.optionalKey<Schema.Int>;
72
81
  }>;
73
82
  export type Asks = typeof Asks.Type;
74
83
  export type AskedQuestion = typeof Question.Type;
@@ -93,20 +102,7 @@ export declare const personRpc: {
93
102
  readonly sessionID: Schema.String;
94
103
  }>;
95
104
  readonly output: import("effect/StandardSchema").StandardSchemaV1<{
96
- readonly questions: readonly {
97
- readonly sessionID: string;
98
- readonly requestID: string;
99
- readonly questions: readonly {
100
- readonly options: readonly {
101
- readonly label: string;
102
- readonly description: string;
103
- }[];
104
- readonly question: string;
105
- readonly header: string;
106
- readonly multiple?: boolean | undefined;
107
- }[];
108
- readonly stopped?: boolean | undefined;
109
- }[];
105
+ readonly top: string;
110
106
  readonly sessions: readonly {
111
107
  readonly sessionID: string;
112
108
  readonly title: string;
@@ -120,10 +116,11 @@ export declare const personRpc: {
120
116
  readonly requestID: string;
121
117
  readonly action: string;
122
118
  readonly save?: readonly string[] | undefined;
119
+ readonly passesOn?: {
120
+ readonly sessionID: string;
121
+ readonly requestID: string;
122
+ } | undefined;
123
123
  }[];
124
- readonly top: string;
125
- readonly truncated?: number | undefined;
126
- }, {
127
124
  readonly questions: readonly {
128
125
  readonly sessionID: string;
129
126
  readonly requestID: string;
@@ -138,6 +135,8 @@ export declare const personRpc: {
138
135
  }[];
139
136
  readonly stopped?: boolean | undefined;
140
137
  }[];
138
+ }, {
139
+ readonly top: string;
141
140
  readonly sessions: readonly {
142
141
  readonly sessionID: string;
143
142
  readonly title: string;
@@ -151,9 +150,25 @@ export declare const personRpc: {
151
150
  readonly requestID: string;
152
151
  readonly action: string;
153
152
  readonly save?: readonly string[] | undefined;
153
+ readonly passesOn?: {
154
+ readonly sessionID: string;
155
+ readonly requestID: string;
156
+ } | undefined;
157
+ }[];
158
+ readonly questions: readonly {
159
+ readonly sessionID: string;
160
+ readonly requestID: string;
161
+ readonly questions: readonly {
162
+ readonly options: readonly {
163
+ readonly label: string;
164
+ readonly description: string;
165
+ }[];
166
+ readonly question: string;
167
+ readonly header: string;
168
+ readonly multiple?: boolean | undefined;
169
+ }[];
170
+ readonly stopped?: boolean | undefined;
154
171
  }[];
155
- readonly top: string;
156
- readonly truncated?: number | undefined;
157
172
  }> & Schema.Struct<{
158
173
  /** The session at the top of the tree, which the sessions below were started from. */
159
174
  readonly top: Schema.String;
@@ -166,6 +181,7 @@ export declare const personRpc: {
166
181
  readonly depth: Schema.Int;
167
182
  readonly group: Schema.optionalKey<Schema.String>;
168
183
  }>>;
184
+ /** The top session's own requests first, then those of the sessions below. */
169
185
  readonly permissions: Schema.$Array<Schema.Struct<{
170
186
  readonly sessionID: Schema.String;
171
187
  readonly requestID: Schema.String;
@@ -173,6 +189,11 @@ export declare const personRpc: {
173
189
  readonly resources: Schema.$Array<Schema.String>;
174
190
  /** What "always" would allow from now on; absent when it is not offered. */
175
191
  readonly save: Schema.optionalKey<Schema.$Array<Schema.String>>;
192
+ /** The request below that allowing this one passes on: the top's courier_answer prompt. */
193
+ readonly passesOn: Schema.optionalKey<Schema.Struct<{
194
+ readonly sessionID: Schema.String;
195
+ readonly requestID: Schema.String;
196
+ }>>;
176
197
  }>>;
177
198
  readonly questions: Schema.$Array<Schema.Struct<{
178
199
  readonly sessionID: Schema.String;
@@ -188,8 +209,6 @@ export declare const personRpc: {
188
209
  }>>;
189
210
  readonly stopped: Schema.optionalKey<Schema.Boolean>;
190
211
  }>>;
191
- /** Set when the tree had more sessions than `maxTotal`, which were not read. */
192
- readonly truncated: Schema.optionalKey<Schema.Int>;
193
212
  }>;
194
213
  };
195
214
  readonly answer: {
@@ -14,7 +14,7 @@ const Session = Schema.Struct({
14
14
  depth: Count,
15
15
  group: Schema.optionalKey(Schema.String),
16
16
  });
17
- /** A permission request a session of the tree waits on. */
17
+ /** A permission request a session of the tree, the top among them, waits on. */
18
18
  const Permission = Schema.Struct({
19
19
  sessionID: Schema.String,
20
20
  requestID: Schema.String,
@@ -22,6 +22,8 @@ const Permission = Schema.Struct({
22
22
  resources: Schema.Array(Schema.String),
23
23
  /** What "always" would allow from now on; absent when it is not offered. */
24
24
  save: Schema.optionalKey(Schema.Array(Schema.String)),
25
+ /** The request below that allowing this one passes on: the top's courier_answer prompt. */
26
+ passesOn: Schema.optionalKey(Schema.Struct({ sessionID: Schema.String, requestID: Schema.String })),
25
27
  });
26
28
  /** One question of OpenCode's question tool. */
27
29
  const Prompt = Schema.Struct({
@@ -43,10 +45,9 @@ const Asks = Schema.Struct({
43
45
  top: Schema.String,
44
46
  /** The sessions below the top, in the order a tree is drawn. */
45
47
  sessions: Schema.Array(Session),
48
+ /** The top session's own requests first, then those of the sessions below. */
46
49
  permissions: Schema.Array(Permission),
47
50
  questions: Schema.Array(Question),
48
- /** Set when the tree had more sessions than `maxTotal`, which were not read. */
49
- truncated: Schema.optionalKey(Count),
50
51
  });
51
52
  /** What answering a question did; `problem` says why nothing was passed on, when it was refused. */
52
53
  const Answered = Schema.Struct({