opencode-courier 0.4.1 → 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.
@@ -1,8 +1,8 @@
1
1
  import { busy } from "./domain.js";
2
- import { lastStatus, reportOf } from "./report.js";
2
+ import { lastStatus, stoppedSincePrompt } from "./report.js";
3
3
  import { standingIn } from "./status.js";
4
4
  import { childStore } from "./store.js";
5
- import { subtree } from "./subtree.js";
5
+ import { readTree } from "./subtree.js";
6
6
  /** The last tool call in a session's messages, and when it was made. */
7
7
  export function lastTool(messages) {
8
8
  for (const message of messages.toReversed()) {
@@ -18,6 +18,8 @@ export function lastTool(messages) {
18
18
  export const waitOf = (pending) => pending.type === "permission"
19
19
  ? { type: "permission", action: pending.action, resources: [...pending.resources] }
20
20
  : { type: "question", question: pending.questions[0]?.question ?? "" };
21
+ /** A form a session shows, as the panel says what it waits on. */
22
+ export const formWait = (form) => ({ type: "form", title: form.title });
21
23
  /** A session's report since its last prompt, as the panel shows it; none for one spawned before this was kept. */
22
24
  function reportRow(report, status) {
23
25
  if (!report)
@@ -28,7 +30,7 @@ function reportRow(report, status) {
28
30
  return { report: { state: "ended", progressed: report.progressed } };
29
31
  return { report: { state: "reported", progressed: report.progressed, ...(status ? { status } : {}) } };
30
32
  }
31
- /** What a session does: waiting on a request to the person, running, idle, or gone when OpenCode no longer knows it. */
33
+ /** What a session does: waiting on a request to the person or a form it shows them, running, idle, or gone when OpenCode no longer knows it. */
32
34
  function stateOf(info, waits, running) {
33
35
  if (!info)
34
36
  return "gone";
@@ -37,16 +39,13 @@ function stateOf(info, waits, running) {
37
39
  return running ? "running" : "idle";
38
40
  }
39
41
  /** One session of the panel: what it is doing, its report and group standing, its last tool call and its cost. */
40
- async function row(ports, entry, depth) {
42
+ async function row(ports, { entry, depth, info, pending, forms, report }) {
41
43
  const { sessionID } = entry;
42
- const children = childStore(ports.storage);
43
- const [info, messages, pending, report, status, standing] = await Promise.all([
44
- ports.session.get({ sessionID }).catch(() => undefined),
44
+ const [messages, status, standing, stopped] = await Promise.all([
45
45
  ports.session.context({ sessionID }).catch(() => []),
46
- ports.pending(sessionID).catch(() => []),
47
- reportOf(children, sessionID).catch(() => undefined),
48
- lastStatus(children, sessionID).catch(() => undefined),
46
+ lastStatus(childStore(ports.storage), sessionID).catch(() => undefined),
49
47
  entry.group ? standingIn(ports, entry, entry.group) : undefined,
48
+ stoppedSincePrompt(childStore(ports.storage), sessionID).catch(() => false),
50
49
  ]);
51
50
  const running = info !== undefined && busy(info.time);
52
51
  const tool = lastTool(messages);
@@ -55,8 +54,9 @@ async function row(ports, entry, depth) {
55
54
  parentID: entry.parentID,
56
55
  title: info?.title ?? entry.title,
57
56
  depth,
58
- state: stateOf(info, pending.length, running),
57
+ state: stateOf(info, pending.length + forms.length, running),
59
58
  ...(info?.outcome ? { outcome: info.outcome } : {}),
59
+ ...(stopped ? { stopped } : {}),
60
60
  ...reportRow(report, status),
61
61
  ...(entry.group && standing ? { group: { name: entry.group, standing } } : {}),
62
62
  isolated: entry.isolated,
@@ -64,12 +64,15 @@ async function row(ports, entry, depth) {
64
64
  ...(info && !running && info.time.idle !== undefined ? { idle: info.time.idle } : {}),
65
65
  ...(info ? { cost: info.cost } : {}),
66
66
  ...(tool ? { tool } : {}),
67
- waits: pending.map(waitOf),
67
+ waits: [...pending.map(waitOf), ...forms.map(formWait)],
68
68
  };
69
69
  }
70
- /** The tree panel's rows: the sessions courier_tree reads under a session, at most `maxTotal`, each as `row` reads it. */
70
+ /**
71
+ * 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.
73
+ */
71
74
  export async function panel(ports, rootID) {
72
- const { nodes, truncated } = await subtree(ports.storage, rootID, ports.limits.maxTotal);
73
- const rows = await Promise.all(nodes.map(({ entry, depth }) => row(ports, entry, depth)));
74
- return { rows, ...(truncated ? { truncated: ports.limits.maxTotal } : {}) };
75
+ const { kept, leftOut } = await readTree(ports, rootID);
76
+ const rows = await Promise.all(kept.map((node) => row(ports, node)));
77
+ return { rows, ...(leftOut ? { truncated: leftOut } : {}) };
75
78
  }
@@ -133,6 +133,8 @@ export interface CourierPorts {
133
133
  readonly now: () => number;
134
134
  /** The permission requests and questions a session waits on, wherever they are pending. */
135
135
  readonly pending: (sessionID: string) => Promise<ReadonlyArray<Pending>>;
136
+ /** The forms a session shows, which only the person can answer: web search's, a plugin's; not the question tool's. */
137
+ readonly forms: (sessionID: string) => Promise<ReadonlyArray<ShownForm>>;
136
138
  readonly limits: Limits;
137
139
  /** The `nativeChildren` option: a child started without isolate is created with OpenCode's own `parentID`. */
138
140
  readonly nativeChildren?: boolean;
@@ -143,6 +145,11 @@ export interface CourierPorts {
143
145
  readonly nudge: () => void;
144
146
  readonly log: (message: string) => void;
145
147
  }
148
+ /** A form a session shows, as `CourierPorts.forms` lists it; `person/` keeps them. */
149
+ export interface ShownForm {
150
+ readonly id: string;
151
+ readonly title: string;
152
+ }
146
153
  /** One question a session waits on, as `Pending` shows it; `person/`'s `Prompt` has these fields. */
147
154
  export interface PendingPrompt {
148
155
  readonly question: string;
@@ -18,6 +18,11 @@ export declare function lineage(storage: RosterStorage, sessionID: string): Prom
18
18
  export declare function answeringTop(storage: RosterStorage, sessionID: string, callerID: string, what: string): Promise<string>;
19
19
  /** Whether a directory still exists; tests pass a fake. */
20
20
  export type Exists = (directory: string) => boolean;
21
+ /**
22
+ * Whether an entry is past retention at `now`: recorded more than `RETENTION_MS` before, unless it is an isolated
23
+ * child whose worktree exists, which is kept so it can still be cleaned up.
24
+ */
25
+ export declare const isExpired: (entry: RosterEntry, now: number, exists?: Exists) => boolean;
21
26
  /** A parent's children, oldest first, after dropping the expired ones. */
22
27
  export declare function current(storage: RosterStorage, parentID: string, now: number, exists?: Exists): Promise<RosterEntry[]>;
23
28
  /**
@@ -31,11 +31,13 @@ export async function answeringTop(storage, sessionID, callerID, what) {
31
31
  return top;
32
32
  }
33
33
  /**
34
- * Forgets the given entries that were recorded more than `RETENTION_MS` before `now`, and returns
35
- * the rest. An isolated child is kept while its worktree exists, so it can still be cleaned up.
34
+ * Whether an entry is past retention at `now`: recorded more than `RETENTION_MS` before, unless it is an isolated
35
+ * child whose worktree exists, which is kept so it can still be cleaned up.
36
36
  */
37
+ export const isExpired = (entry, now, exists = existsSync) => now - entry.createdAt > RETENTION_MS && !(entry.isolated && exists(entry.directory));
38
+ /** Forgets the given entries that are expired at `now`, and returns the rest. */
37
39
  async function dropExpired(storage, entries, now, exists) {
38
- const expired = new Set(entries.filter((entry) => now - entry.createdAt > RETENTION_MS && !(entry.isolated && exists(entry.directory))));
40
+ const expired = new Set(entries.filter((entry) => isExpired(entry, now, exists)));
39
41
  await Promise.all([...expired].map((entry) => forgetChild(storage, entry.parentID, entry.sessionID, entry.group)));
40
42
  return entries.filter((entry) => !expired.has(entry));
41
43
  }
@@ -14,24 +14,22 @@ 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;
17
18
  readonly idle: number;
18
19
  readonly groups: readonly {
19
20
  readonly name: string;
20
21
  readonly reported: number;
21
22
  readonly members: number;
22
23
  }[];
23
- readonly waiting: number;
24
- readonly truncated?: number | undefined;
25
24
  }, {
26
25
  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 truncated?: number | undefined;
35
33
  }> & import("effect/Schema").Struct<{
36
34
  readonly running: import("effect/Schema").Int;
37
35
  readonly waiting: import("effect/Schema").Int;
@@ -41,7 +39,6 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
41
39
  readonly reported: import("effect/Schema").Int;
42
40
  readonly members: import("effect/Schema").Int;
43
41
  }>>;
44
- readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
45
42
  }>;
46
43
  };
47
44
  readonly tree: {
@@ -68,11 +65,15 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
68
65
  } | {
69
66
  readonly type: "question";
70
67
  readonly question: string;
68
+ } | {
69
+ readonly type: "form";
70
+ readonly title: string;
71
71
  })[];
72
72
  readonly group?: {
73
73
  readonly name: string;
74
74
  readonly standing: string;
75
75
  } | undefined;
76
+ readonly stopped?: boolean | undefined;
76
77
  readonly idle?: number | undefined;
77
78
  readonly report?: {
78
79
  readonly progressed: boolean;
@@ -103,11 +104,15 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
103
104
  } | {
104
105
  readonly type: "question";
105
106
  readonly question: string;
107
+ } | {
108
+ readonly type: "form";
109
+ readonly title: string;
106
110
  })[];
107
111
  readonly group?: {
108
112
  readonly name: string;
109
113
  readonly standing: string;
110
114
  } | undefined;
115
+ readonly stopped?: boolean | undefined;
111
116
  readonly idle?: number | undefined;
112
117
  readonly report?: {
113
118
  readonly progressed: boolean;
@@ -130,6 +135,7 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
130
135
  readonly depth: import("effect/Schema").Int;
131
136
  readonly state: import("effect/Schema").Literals<readonly ["running", "waiting", "idle", "gone"]>;
132
137
  readonly outcome: import("effect/Schema").optionalKey<import("effect/Schema").Literals<readonly ["succeeded", "failed", "interrupted"]>>;
138
+ readonly stopped: import("effect/Schema").optionalKey<import("effect/Schema").Boolean>;
133
139
  readonly report: import("effect/Schema").optionalKey<import("effect/Schema").Struct<{
134
140
  readonly state: import("effect/Schema").Literals<readonly ["owed", "reported", "ended"]>;
135
141
  readonly status: import("effect/Schema").optionalKey<import("effect/Schema").String>;
@@ -154,6 +160,9 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
154
160
  }>, import("effect/Schema").Struct<{
155
161
  readonly type: import("effect/Schema").Literal<"question">;
156
162
  readonly question: import("effect/Schema").String;
163
+ }>, import("effect/Schema").Struct<{
164
+ readonly type: import("effect/Schema").Literal<"form">;
165
+ readonly title: import("effect/Schema").String;
157
166
  }>]>>;
158
167
  }>>;
159
168
  readonly truncated: import("effect/Schema").optionalKey<import("effect/Schema").Int>;
@@ -196,6 +205,37 @@ export declare function registerTreeRpc(rpc: Pick<Plugin.Context["rpc"], "regist
196
205
  readonly refused: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & import("effect/Schema").Struct<{}>;
197
206
  };
198
207
  };
208
+ readonly cleanup: {
209
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
210
+ readonly sessionID: string;
211
+ readonly by: string;
212
+ }, {
213
+ readonly sessionID: string;
214
+ readonly by: string;
215
+ }> & import("effect/Schema").Struct<{
216
+ readonly sessionID: import("effect/Schema").String;
217
+ readonly by: import("effect/Schema").String;
218
+ }>;
219
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
220
+ readonly outcome: "removed" | "gone" | "forgotten";
221
+ } | {
222
+ readonly outcome: "kept";
223
+ readonly reason: string;
224
+ }, {
225
+ readonly outcome: "removed" | "gone" | "forgotten";
226
+ } | {
227
+ readonly outcome: "kept";
228
+ readonly reason: string;
229
+ }> & import("effect/Schema").Union<readonly [import("effect/Schema").Struct<{
230
+ readonly outcome: import("effect/Schema").Literals<readonly ["forgotten", "removed", "gone"]>;
231
+ }>, import("effect/Schema").Struct<{
232
+ readonly outcome: import("effect/Schema").Literal<"kept">;
233
+ readonly reason: import("effect/Schema").String;
234
+ }>]>;
235
+ readonly errors: {
236
+ readonly refused: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & import("effect/Schema").Struct<{}>;
237
+ };
238
+ };
199
239
  };
200
240
  readonly events: {};
201
241
  }>>;
@@ -1,9 +1,14 @@
1
+ import { describeFailure } from "../kernel/protocol.js";
2
+ import { isNotFound } from "../platform/json.js";
3
+ import { cleanup } from "./cleanup.js";
1
4
  import { panel } from "./panel.js";
2
5
  import { treeRpc } from "./rpc.js";
3
6
  import { stop } from "./stop.js";
7
+ import { childStore } from "./store.js";
4
8
  import { summary } from "./summary.js";
5
9
  /** Serves the tree's RPC at the plugin's location, through the same use cases as the tools; disposed with the plugin. */
6
10
  export function registerTreeRpc(rpc, ports) {
11
+ const refused = (cause) => (cause instanceof Error ? cause.message : String(cause));
7
12
  return rpc.register(treeRpc, {
8
13
  summary: ({ sessionID }) => summary(ports.courier, sessionID),
9
14
  tree: ({ sessionID }) => panel(ports.courier, sessionID),
@@ -13,8 +18,32 @@ export function registerTreeRpc(rpc, ports) {
13
18
  return { stopped: stopped.map(({ sessionID, title, outcome }) => ({ sessionID, title, outcome })), cancelled: cancelled.length };
14
19
  }
15
20
  catch (cause) {
16
- return error("refused", cause instanceof Error ? cause.message : String(cause), {});
21
+ return error("refused", refused(cause), {});
22
+ }
23
+ },
24
+ cleanup: async ({ sessionID, by }, { error }) => {
25
+ try {
26
+ return await forget(ports, by, sessionID);
27
+ }
28
+ catch (cause) {
29
+ return error("refused", refused(cause), {});
17
30
  }
18
31
  },
19
32
  });
20
33
  }
34
+ /**
35
+ * Forgets a session OpenCode no longer knows as courier_cleanup called from its own parent, once `by` is found above it,
36
+ * as courier_stop checks. A session OpenCode still has, or cannot say it lost, is refused: forgetting it loses its report.
37
+ */
38
+ async function forget(ports, by, sessionID) {
39
+ const chain = await childStore(ports.courier.storage).lineage(sessionID);
40
+ if (!chain.length)
41
+ throw new Error(`${sessionID} was not started with courier_spawn, so there is nothing to forget.`);
42
+ if (!chain.some((entry) => entry.parentID === by))
43
+ throw new Error(`${by} is not above ${sessionID}: only a session above it, in the line of sessions that started it with courier_spawn, can forget it.`);
44
+ const known = await ports.courier.session.get({ sessionID }).then(() => `${sessionID} is still known to OpenCode: only a gone session is forgotten here.`, (error) => (isNotFound(error) ? undefined : describeFailure("session.get", error).message));
45
+ if (known)
46
+ throw new Error(known);
47
+ const result = await cleanup(ports.cleanup, chain[0].parentID, { sessionID });
48
+ return result.outcome === "kept" ? { outcome: "kept", reason: result.reason } : { outcome: result.outcome };
49
+ }
@@ -1,9 +1,9 @@
1
1
  import { Schema } from "effect";
2
2
  /** The sessions started under a session, as the prompt footer's status entry shows them. */
3
3
  declare const Summary: Schema.Struct<{
4
- /** Running, and waiting on no request. */
4
+ /** Running, and waiting on no request or form. */
5
5
  readonly running: Schema.Int;
6
- /** Waiting on a permission request or a question, which go to the person. */
6
+ /** Waiting on a permission request, a question or a form it shows, which go to the person. */
7
7
  readonly waiting: Schema.Int;
8
8
  /** Not running: done, or waiting for a message. */
9
9
  readonly idle: Schema.Int;
@@ -12,11 +12,9 @@ declare const Summary: Schema.Struct<{
12
12
  readonly reported: Schema.Int;
13
13
  readonly members: Schema.Int;
14
14
  }>>;
15
- /** Set when the subtree had more sessions than `maxTotal`, which were not counted. */
16
- readonly truncated: Schema.optionalKey<Schema.Int>;
17
15
  }>;
18
16
  export type Summary = typeof Summary.Type;
19
- /** A request a session waits on until the person answers it: a permission request, or its first question. */
17
+ /** A request a session waits on until the person answers it: a permission request, its first question, or a form it shows. */
20
18
  declare const Wait: Schema.Union<readonly [Schema.Struct<{
21
19
  readonly type: Schema.Literal<"permission">;
22
20
  readonly action: Schema.String;
@@ -24,6 +22,9 @@ declare const Wait: Schema.Union<readonly [Schema.Struct<{
24
22
  }>, Schema.Struct<{
25
23
  readonly type: Schema.Literal<"question">;
26
24
  readonly question: Schema.String;
25
+ }>, Schema.Struct<{
26
+ readonly type: Schema.Literal<"form">;
27
+ readonly title: Schema.String;
27
28
  }>]>;
28
29
  /** One session of the tree panel: what courier_tree shows of it, and what it is doing, in epoch milliseconds. */
29
30
  declare const Row: Schema.Struct<{
@@ -32,10 +33,12 @@ declare const Row: Schema.Struct<{
32
33
  readonly title: Schema.String;
33
34
  /** A child of the panel's session is at depth 1. */
34
35
  readonly depth: Schema.Int;
35
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
36
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
36
37
  readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "gone"]>;
37
38
  /** How its last turn ended. */
38
39
  readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
40
+ /** Set when courier_stop stopped it since its last prompt. */
41
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
39
42
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
40
43
  readonly report: Schema.optionalKey<Schema.Struct<{
41
44
  readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
@@ -65,9 +68,12 @@ declare const Row: Schema.Struct<{
65
68
  }>, Schema.Struct<{
66
69
  readonly type: Schema.Literal<"question">;
67
70
  readonly question: Schema.String;
71
+ }>, Schema.Struct<{
72
+ readonly type: Schema.Literal<"form">;
73
+ readonly title: Schema.String;
68
74
  }>]>>;
69
75
  }>;
70
- /** The sessions under a session, as courier_tree reads them, in the order a tree is drawn. */
76
+ /** The sessions under a session, as courier_tree lists them, in the order a tree is drawn. */
71
77
  declare const Tree: Schema.Struct<{
72
78
  readonly rows: Schema.$Array<Schema.Struct<{
73
79
  readonly sessionID: Schema.String;
@@ -75,10 +81,12 @@ declare const Tree: Schema.Struct<{
75
81
  readonly title: Schema.String;
76
82
  /** A child of the panel's session is at depth 1. */
77
83
  readonly depth: Schema.Int;
78
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
84
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
79
85
  readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "gone"]>;
80
86
  /** How its last turn ended. */
81
87
  readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
88
+ /** Set when courier_stop stopped it since its last prompt. */
89
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
82
90
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
83
91
  readonly report: Schema.optionalKey<Schema.Struct<{
84
92
  readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
@@ -108,9 +116,12 @@ declare const Tree: Schema.Struct<{
108
116
  }>, Schema.Struct<{
109
117
  readonly type: Schema.Literal<"question">;
110
118
  readonly question: Schema.String;
119
+ }>, Schema.Struct<{
120
+ readonly type: Schema.Literal<"form">;
121
+ readonly title: Schema.String;
111
122
  }>]>>;
112
123
  }>>;
113
- /** Set when the subtree had more sessions than `maxTotal`, which were left out. */
124
+ /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
114
125
  readonly truncated: Schema.optionalKey<Schema.Int>;
115
126
  }>;
116
127
  export type Row = typeof Row.Type;
@@ -127,6 +138,14 @@ declare const Stopped: Schema.Struct<{
127
138
  readonly cancelled: Schema.Int;
128
139
  }>;
129
140
  export type Stopped = typeof Stopped.Type;
141
+ /** What courier_cleanup did: forgot it, removed its worktree, found that already gone, or kept the worktree, saying why. */
142
+ declare const Forgotten: Schema.Union<readonly [Schema.Struct<{
143
+ readonly outcome: Schema.Literals<readonly ["forgotten", "removed", "gone"]>;
144
+ }>, Schema.Struct<{
145
+ readonly outcome: Schema.Literal<"kept">;
146
+ readonly reason: Schema.String;
147
+ }>]>;
148
+ export type Forgotten = typeof Forgotten.Type;
130
149
  export declare const treeRpc: {
131
150
  readonly id: "courier";
132
151
  readonly methods: {
@@ -140,28 +159,26 @@ export declare const treeRpc: {
140
159
  }>;
141
160
  readonly output: import("effect/StandardSchema").StandardSchemaV1<{
142
161
  readonly running: number;
162
+ readonly waiting: number;
143
163
  readonly idle: number;
144
164
  readonly groups: readonly {
145
165
  readonly name: string;
146
166
  readonly reported: number;
147
167
  readonly members: number;
148
168
  }[];
149
- readonly waiting: number;
150
- readonly truncated?: number | undefined;
151
169
  }, {
152
170
  readonly running: number;
171
+ readonly waiting: number;
153
172
  readonly idle: number;
154
173
  readonly groups: readonly {
155
174
  readonly name: string;
156
175
  readonly reported: number;
157
176
  readonly members: number;
158
177
  }[];
159
- readonly waiting: number;
160
- readonly truncated?: number | undefined;
161
178
  }> & Schema.Struct<{
162
- /** Running, and waiting on no request. */
179
+ /** Running, and waiting on no request or form. */
163
180
  readonly running: Schema.Int;
164
- /** Waiting on a permission request or a question, which go to the person. */
181
+ /** Waiting on a permission request, a question or a form it shows, which go to the person. */
165
182
  readonly waiting: Schema.Int;
166
183
  /** Not running: done, or waiting for a message. */
167
184
  readonly idle: Schema.Int;
@@ -170,8 +187,6 @@ export declare const treeRpc: {
170
187
  readonly reported: Schema.Int;
171
188
  readonly members: Schema.Int;
172
189
  }>>;
173
- /** Set when the subtree had more sessions than `maxTotal`, which were not counted. */
174
- readonly truncated: Schema.optionalKey<Schema.Int>;
175
190
  }>;
176
191
  };
177
192
  readonly tree: {
@@ -198,11 +213,15 @@ export declare const treeRpc: {
198
213
  } | {
199
214
  readonly type: "question";
200
215
  readonly question: string;
216
+ } | {
217
+ readonly type: "form";
218
+ readonly title: string;
201
219
  })[];
202
220
  readonly group?: {
203
221
  readonly name: string;
204
222
  readonly standing: string;
205
223
  } | undefined;
224
+ readonly stopped?: boolean | undefined;
206
225
  readonly idle?: number | undefined;
207
226
  readonly report?: {
208
227
  readonly progressed: boolean;
@@ -233,11 +252,15 @@ export declare const treeRpc: {
233
252
  } | {
234
253
  readonly type: "question";
235
254
  readonly question: string;
255
+ } | {
256
+ readonly type: "form";
257
+ readonly title: string;
236
258
  })[];
237
259
  readonly group?: {
238
260
  readonly name: string;
239
261
  readonly standing: string;
240
262
  } | undefined;
263
+ readonly stopped?: boolean | undefined;
241
264
  readonly idle?: number | undefined;
242
265
  readonly report?: {
243
266
  readonly progressed: boolean;
@@ -259,10 +282,12 @@ export declare const treeRpc: {
259
282
  readonly title: Schema.String;
260
283
  /** A child of the panel's session is at depth 1. */
261
284
  readonly depth: Schema.Int;
262
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
285
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
263
286
  readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "gone"]>;
264
287
  /** How its last turn ended. */
265
288
  readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
289
+ /** Set when courier_stop stopped it since its last prompt. */
290
+ readonly stopped: Schema.optionalKey<Schema.Boolean>;
266
291
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
267
292
  readonly report: Schema.optionalKey<Schema.Struct<{
268
293
  readonly state: Schema.Literals<readonly ["owed", "reported", "ended"]>;
@@ -292,9 +317,12 @@ export declare const treeRpc: {
292
317
  }>, Schema.Struct<{
293
318
  readonly type: Schema.Literal<"question">;
294
319
  readonly question: Schema.String;
320
+ }>, Schema.Struct<{
321
+ readonly type: Schema.Literal<"form">;
322
+ readonly title: Schema.String;
295
323
  }>]>>;
296
324
  }>>;
297
- /** Set when the subtree had more sessions than `maxTotal`, which were left out. */
325
+ /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
298
326
  readonly truncated: Schema.optionalKey<Schema.Int>;
299
327
  }>;
300
328
  };
@@ -337,6 +365,38 @@ export declare const treeRpc: {
337
365
  readonly refused: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & Schema.Struct<{}>;
338
366
  };
339
367
  };
368
+ /** Forgets `sessionID` as courier_cleanup called from its parent, without `force`; `by` must be above it. */
369
+ readonly cleanup: {
370
+ readonly input: import("effect/StandardSchema").StandardSchemaV1<{
371
+ readonly sessionID: string;
372
+ readonly by: string;
373
+ }, {
374
+ readonly sessionID: string;
375
+ readonly by: string;
376
+ }> & Schema.Struct<{
377
+ readonly sessionID: Schema.String;
378
+ readonly by: Schema.String;
379
+ }>;
380
+ readonly output: import("effect/StandardSchema").StandardSchemaV1<{
381
+ readonly outcome: "removed" | "gone" | "forgotten";
382
+ } | {
383
+ readonly outcome: "kept";
384
+ readonly reason: string;
385
+ }, {
386
+ readonly outcome: "removed" | "gone" | "forgotten";
387
+ } | {
388
+ readonly outcome: "kept";
389
+ readonly reason: string;
390
+ }> & Schema.Union<readonly [Schema.Struct<{
391
+ readonly outcome: Schema.Literals<readonly ["forgotten", "removed", "gone"]>;
392
+ }>, Schema.Struct<{
393
+ readonly outcome: Schema.Literal<"kept">;
394
+ readonly reason: Schema.String;
395
+ }>]>;
396
+ readonly errors: {
397
+ readonly refused: import("effect/StandardSchema").StandardSchemaV1<{}, {}> & Schema.Struct<{}>;
398
+ };
399
+ };
340
400
  };
341
401
  readonly events: {};
342
402
  };
package/dist/tree/rpc.js CHANGED
@@ -9,20 +9,19 @@ const Count = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
9
9
  const Group = Schema.Struct({ name: Schema.String, reported: Count, members: Count });
10
10
  /** The sessions started under a session, as the prompt footer's status entry shows them. */
11
11
  const Summary = Schema.Struct({
12
- /** Running, and waiting on no request. */
12
+ /** Running, and waiting on no request or form. */
13
13
  running: Count,
14
- /** Waiting on a permission request or a question, which go to the person. */
14
+ /** Waiting on a permission request, a question or a form it shows, which go to the person. */
15
15
  waiting: Count,
16
16
  /** Not running: done, or waiting for a message. */
17
17
  idle: Count,
18
18
  groups: Schema.Array(Group),
19
- /** Set when the subtree had more sessions than `maxTotal`, which were not counted. */
20
- truncated: Schema.optionalKey(Count),
21
19
  });
22
- /** A request a session waits on until the person answers it: a permission request, or its first question. */
20
+ /** A request a session waits on until the person answers it: a permission request, its first question, or a form it shows. */
23
21
  const Wait = Schema.Union([
24
22
  Schema.Struct({ type: Schema.Literal("permission"), action: Schema.String, resources: Schema.Array(Schema.String) }),
25
23
  Schema.Struct({ type: Schema.Literal("question"), question: Schema.String }),
24
+ Schema.Struct({ type: Schema.Literal("form"), title: Schema.String }),
26
25
  ]);
27
26
  /** One session of the tree panel: what courier_tree shows of it, and what it is doing, in epoch milliseconds. */
28
27
  const Row = Schema.Struct({
@@ -31,10 +30,12 @@ const Row = Schema.Struct({
31
30
  title: Schema.String,
32
31
  /** A child of the panel's session is at depth 1. */
33
32
  depth: Count,
34
- /** `waiting` on a request to the person; `gone` when OpenCode no longer knows it. */
33
+ /** `waiting` on a request to the person or a form it shows them; `gone` when OpenCode no longer knows it. */
35
34
  state: Schema.Literals(["running", "waiting", "idle", "gone"]),
36
35
  /** How its last turn ended. */
37
36
  outcome: Schema.optionalKey(Schema.Literals(["succeeded", "failed", "interrupted"])),
37
+ /** Set when courier_stop stopped it since its last prompt. */
38
+ stopped: Schema.optionalKey(Schema.Boolean),
38
39
  /** Its report since its last prompt: still owed, arrived (held with its group or delivered), or its turn ended without one. */
39
40
  report: Schema.optionalKey(Schema.Struct({ state: Schema.Literals(["owed", "reported", "ended"]), status: Schema.optionalKey(Schema.String), progressed: Schema.Boolean })),
40
41
  /** Its join group, and its standing there: `held`, `out`, how it left, or `released`. */
@@ -49,10 +50,10 @@ const Row = Schema.Struct({
49
50
  tool: Schema.optionalKey(Schema.Struct({ name: Schema.String, at: Schema.Number })),
50
51
  waits: Schema.Array(Wait),
51
52
  });
52
- /** The sessions under a session, as courier_tree reads them, in the order a tree is drawn. */
53
+ /** The sessions under a session, as courier_tree lists them, in the order a tree is drawn. */
53
54
  const Tree = Schema.Struct({
54
55
  rows: Schema.Array(Row),
55
- /** Set when the subtree had more sessions than `maxTotal`, which were left out. */
56
+ /** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
56
57
  truncated: Schema.optionalKey(Count),
57
58
  });
58
59
  /** What a stop did to each session, deepest first, as courier_stop reports it. */
@@ -61,6 +62,11 @@ const Stopped = Schema.Struct({
61
62
  /** How many scheduled messages were cancelled. */
62
63
  cancelled: Count,
63
64
  });
65
+ /** What courier_cleanup did: forgot it, removed its worktree, found that already gone, or kept the worktree, saying why. */
66
+ const Forgotten = Schema.Union([
67
+ Schema.Struct({ outcome: Schema.Literals(["forgotten", "removed", "gone"]) }),
68
+ Schema.Struct({ outcome: Schema.Literal("kept"), reason: Schema.String }),
69
+ ]);
64
70
  export const treeRpc = Rpc.define({
65
71
  id: "courier",
66
72
  methods: {
@@ -78,6 +84,12 @@ export const treeRpc = Rpc.define({
78
84
  output: Schema.toStandardSchemaV1(Stopped),
79
85
  errors: { refused: Schema.toStandardSchemaV1(Schema.Struct({})) },
80
86
  },
87
+ /** Forgets `sessionID` as courier_cleanup called from its parent, without `force`; `by` must be above it. */
88
+ cleanup: {
89
+ input: Schema.toStandardSchemaV1(Schema.Struct({ sessionID: Schema.String, by: Schema.String })),
90
+ output: Schema.toStandardSchemaV1(Forgotten),
91
+ errors: { refused: Schema.toStandardSchemaV1(Schema.Struct({})) },
92
+ },
81
93
  },
82
94
  events: {},
83
95
  });