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/dist/person/rpc.d.ts
CHANGED
|
@@ -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
|
|
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: {
|
package/dist/person/rpc.js
CHANGED
|
@@ -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({
|
package/dist/person/tui.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode/plugin/tui";
|
|
2
|
-
import type { AttentionSoundName } from "@opencode/plugin/tui/context";
|
|
3
2
|
import { type AskedPermission, type AskedQuestion, type Asks, type TreeSession } from "./rpc.js";
|
|
4
3
|
type Context = Plugin.Context;
|
|
5
4
|
type FormInfo = NonNullable<ReturnType<Context["data"]["session"]["form"]["list"]>>[number];
|
|
6
5
|
type FormField = FormInfo["fields"][number];
|
|
7
6
|
type FormAnswer = Parameters<Context["data"]["session"]["form"]["reply"]>[0]["answer"];
|
|
8
|
-
|
|
7
|
+
type FormValue = FormAnswer[string];
|
|
8
|
+
/**
|
|
9
|
+
* Something a session of the tree waits on the person for; `key` is unique across kinds. `prompted` marks a permission
|
|
10
|
+
* request the top session's courier_answer prompt asks about.
|
|
11
|
+
*/
|
|
9
12
|
export type Ask = {
|
|
10
13
|
readonly key: string;
|
|
11
14
|
readonly session: TreeSession;
|
|
12
15
|
} & ({
|
|
13
16
|
readonly kind: "permission";
|
|
14
17
|
readonly request: AskedPermission;
|
|
18
|
+
readonly prompted?: boolean;
|
|
15
19
|
} | {
|
|
16
20
|
readonly kind: "question";
|
|
17
21
|
readonly question: AskedQuestion;
|
|
@@ -23,23 +27,30 @@ export type Ask = {
|
|
|
23
27
|
export declare const INBOX_COMMAND = "courier.asks";
|
|
24
28
|
/** How long after the first of a burst of asks the inbox reads the tree again, to sound the new ones. */
|
|
25
29
|
export declare const SETTLE_MS = 250;
|
|
26
|
-
/** The
|
|
30
|
+
/** The top session, at depth 0, as its own permission requests name it. */
|
|
31
|
+
export declare const topSession: (asks: Asks) => TreeSession;
|
|
32
|
+
/**
|
|
33
|
+
* The asks of a tree in the order the inbox lists them: the top session's own permission requests, then by the tree's
|
|
34
|
+
* order, each session's permissions, questions, then forms.
|
|
35
|
+
*/
|
|
27
36
|
export declare function asksOf(asks: Asks, forms: ReadonlyMap<string, ReadonlyArray<FormInfo>>): Ask[];
|
|
28
|
-
/** Where an ask comes from: the titles from the top session down to the asking one, as "root › child › grandchild". */
|
|
29
|
-
export declare function pathOf(asks: Asks, sessionID: string, topTitle: string): string;
|
|
37
|
+
/** Where an ask comes from: the titles from the top session down to the asking one, as "root › child › grandchild", each as the TUI knows it now, else as spawned. */
|
|
38
|
+
export declare function pathOf(asks: Asks, sessionID: string, topTitle: string, currentTitle?: (sessionID: string) => string | undefined): string;
|
|
30
39
|
/** What an ask is, in one line. */
|
|
31
40
|
export declare function askTitle(ask: Ask): string;
|
|
32
|
-
/** The line under an ask: its depth and group,
|
|
41
|
+
/** The line under an ask: its depth (the top's own say so) and group, that a question's call was cut off, or that the top's courier_answer prompt asks for it. */
|
|
33
42
|
export declare function askFooter(ask: Ask): string;
|
|
34
|
-
/**
|
|
35
|
-
export declare
|
|
36
|
-
/**
|
|
37
|
-
export declare function
|
|
43
|
+
/** Whether a form field applies, given the answers so far (a hidden field's default among them): its `when` conditions hold, as OpenCode reads them (a condition on an unanswered field is false; on a multiselect, `eq` means includes). */
|
|
44
|
+
export declare function active(field: FormField, answers: FormAnswer): boolean;
|
|
45
|
+
/** What is wrong with a field's answer by the limits OpenCode's form service checks on reply, worded as its form view does; undefined when it fits. */
|
|
46
|
+
export declare function misfit(field: FormField, value: FormValue): string | undefined;
|
|
47
|
+
/** The limits of a typed field as a prompt's placeholder: "1-5", "at least 3 characters", "email"; undefined without any. */
|
|
48
|
+
export declare function limitsOf(field: FormField): string | undefined;
|
|
38
49
|
/**
|
|
39
50
|
* Adds the asks inbox: what the open session's courier tree waits on the person for, read through the person's RPC and
|
|
40
|
-
* answered in dialogs, opened by its command
|
|
51
|
+
* answered in dialogs, opened by its command, its open list read again on `follows`; a sound for each new ask.
|
|
41
52
|
*/
|
|
42
|
-
export declare function addAsksInbox(ctx: Context): {
|
|
53
|
+
export declare function addAsksInbox(ctx: Context, follows: ReadonlySet<string>): {
|
|
43
54
|
open: (sessionID?: string | undefined) => Promise<void>;
|
|
44
55
|
cleanup: () => void;
|
|
45
56
|
};
|
package/dist/tree/domain.d.ts
CHANGED
|
@@ -26,6 +26,29 @@ export declare function readNativeChildren(options: unknown): {
|
|
|
26
26
|
type Times = Awaited<ReturnType<Context["session"]["get"]>>["time"];
|
|
27
27
|
/** Whether a session with these times runs: it has never finished a turn, or something reached it after the last one ended. */
|
|
28
28
|
export declare const busy: (time: Pick<Times, "updated" | "idle">) => boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a session a tree is read of is live: it runs (as the limits count it: busy, and its turn has not failed or
|
|
31
|
+
* been interrupted since its last prompt), waits on a request or form, or owes a report; one OpenCode no longer knows is not.
|
|
32
|
+
*/
|
|
33
|
+
export declare const isLive: (time: Pick<Times, "updated" | "idle"> | undefined, waits: number, report: {
|
|
34
|
+
readonly owes: boolean;
|
|
35
|
+
readonly ended: boolean;
|
|
36
|
+
} | undefined) => boolean;
|
|
37
|
+
/** A session of a tree read, as `cut` takes it: where it is in the tree, when it was started, and whether it is live. */
|
|
38
|
+
export interface Cuttable {
|
|
39
|
+
readonly sessionID: string;
|
|
40
|
+
readonly parentID: string;
|
|
41
|
+
readonly createdAt: number;
|
|
42
|
+
readonly live: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Which sessions a listing of at most `max` keeps, of a tree read in the order it is drawn: the live ones first, then
|
|
46
|
+
* the newest, each with the sessions above it so the tree still draws; `leftOut` counts the rest.
|
|
47
|
+
*/
|
|
48
|
+
export declare function cut<T extends Cuttable>(nodes: ReadonlyArray<T>, max: number): {
|
|
49
|
+
kept: T[];
|
|
50
|
+
leftOut: number;
|
|
51
|
+
};
|
|
29
52
|
/** A session started with courier_spawn, recorded under the session that started it. */
|
|
30
53
|
export interface RosterEntry {
|
|
31
54
|
readonly sessionID: string;
|
package/dist/tree/domain.js
CHANGED
|
@@ -25,6 +25,32 @@ export function readNativeChildren(options) {
|
|
|
25
25
|
}
|
|
26
26
|
/** Whether a session with these times runs: it has never finished a turn, or something reached it after the last one ended. */
|
|
27
27
|
export const busy = (time) => time.idle === undefined || millis(time.updated) > millis(time.idle);
|
|
28
|
+
/**
|
|
29
|
+
* Whether a session a tree is read of is live: it runs (as the limits count it: busy, and its turn has not failed or
|
|
30
|
+
* been interrupted since its last prompt), waits on a request or form, or owes a report; one OpenCode no longer knows is not.
|
|
31
|
+
*/
|
|
32
|
+
export const isLive = (time, waits, report) => time !== undefined && ((busy(time) && !report?.ended) || waits > 0 || (report?.owes ?? false));
|
|
33
|
+
/**
|
|
34
|
+
* Which sessions a listing of at most `max` keeps, of a tree read in the order it is drawn: the live ones first, then
|
|
35
|
+
* the newest, each with the sessions above it so the tree still draws; `leftOut` counts the rest.
|
|
36
|
+
*/
|
|
37
|
+
export function cut(nodes, max) {
|
|
38
|
+
const byID = new Map(nodes.map((node) => [node.sessionID, node]));
|
|
39
|
+
const ranked = nodes
|
|
40
|
+
.map((node, order) => ({ node, order }))
|
|
41
|
+
.sort((a, b) => Number(b.node.live) - Number(a.node.live) || b.node.createdAt - a.node.createdAt || a.order - b.order);
|
|
42
|
+
const kept = new Set();
|
|
43
|
+
for (const { node } of ranked) {
|
|
44
|
+
const chain = [];
|
|
45
|
+
for (let above = node; above && !kept.has(above.sessionID); above = byID.get(above.parentID))
|
|
46
|
+
chain.push(above.sessionID);
|
|
47
|
+
if (kept.size + chain.length > max)
|
|
48
|
+
continue;
|
|
49
|
+
for (const sessionID of chain)
|
|
50
|
+
kept.add(sessionID);
|
|
51
|
+
}
|
|
52
|
+
return { kept: nodes.filter((node) => kept.has(node.sessionID)), leftOut: nodes.length - kept.size };
|
|
53
|
+
}
|
|
28
54
|
/** `lineage` over roster entries already read. */
|
|
29
55
|
export function lineageIn(entries, sessionID) {
|
|
30
56
|
return lineageBy(bySession(entries), sessionID);
|
package/dist/tree/events.d.ts
CHANGED
|
@@ -51,8 +51,8 @@ export declare function notePrompt(ports: WatchPorts, event: SessionEvent & {
|
|
|
51
51
|
*/
|
|
52
52
|
export declare function noteInterrupted(ports: WatchPorts, event: SessionEvent): Promise<boolean>;
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* Forgets what a deleted session leaves (report state, groups) and, unless isolated or a parent on the roster, its
|
|
55
|
+
* entry. Returns the sessions forgotten.
|
|
56
56
|
*/
|
|
57
57
|
export declare function noteDeleted(ports: WatchPorts, event: SessionEvent): Promise<string[]>;
|
|
58
58
|
/** A `Waits` asking `suppliers` one after another, cheapest first: the first that waits answers. */
|
package/dist/tree/events.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { envelope } from "../kernel/protocol.js";
|
|
2
2
|
import { claim, SEEN_MAX, setBounded } from "../platform/bounded.js";
|
|
3
|
-
import { forgetDeleted } from "./forget.js";
|
|
3
|
+
import { forgetChild, forgetDeleted } from "./forget.js";
|
|
4
4
|
import { hasCompleteGroup, leave, outIn } from "./group.js";
|
|
5
5
|
import { awaited, reportOf, stoppedSincePrompt } from "./report.js";
|
|
6
6
|
import { childStore, groupStore } from "./store.js";
|
|
7
7
|
import { lastReply } from "./status.js";
|
|
8
8
|
import { failureNotice, silentNotice } from "./text.js";
|
|
9
|
+
import { forgetWakes, settleWake, wakeEnded, wakeStarted, watchWake } from "./wake.js";
|
|
9
10
|
/**
|
|
10
11
|
* Tells the parent of a child whose turn failed, since a child that cannot reach its model cannot
|
|
11
12
|
* call courier_send. Returns the parents told.
|
|
@@ -88,8 +89,8 @@ export async function noteInterrupted(ports, event) {
|
|
|
88
89
|
return true;
|
|
89
90
|
}
|
|
90
91
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
92
|
+
* Forgets what a deleted session leaves (report state, groups) and, unless isolated or a parent on the roster, its
|
|
93
|
+
* entry. Returns the sessions forgotten.
|
|
93
94
|
*/
|
|
94
95
|
export async function noteDeleted(ports, event) {
|
|
95
96
|
const { sessionID } = event.data;
|
|
@@ -99,6 +100,8 @@ export async function noteDeleted(ports, event) {
|
|
|
99
100
|
const entry = parentID === undefined ? undefined : await children.entry(parentID, sessionID);
|
|
100
101
|
const member = entry && (await outIn(ports, entry));
|
|
101
102
|
await forgetDeleted(ports, sessionID, forgotten, member, event.created);
|
|
103
|
+
if (parentID !== undefined && entry && !entry.isolated && started.length === 0)
|
|
104
|
+
await forgetChild(ports.storage, parentID, sessionID);
|
|
102
105
|
return forgotten;
|
|
103
106
|
}
|
|
104
107
|
/** A `Waits` asking `suppliers` one after another, cheapest first: the first that waits answers. */
|
|
@@ -191,13 +194,37 @@ function routeTree(ports, state, outside, event) {
|
|
|
191
194
|
return noteDeleted(ports, event);
|
|
192
195
|
return undefined;
|
|
193
196
|
}
|
|
197
|
+
/** The events that move a courier message waiting in a session's inbox: enqueued, taken, cancelled, and the turns around it. */
|
|
198
|
+
function routeWake(ports, event) {
|
|
199
|
+
if (event.type === "session.inbox.enqueued")
|
|
200
|
+
return watchWake(ports, event);
|
|
201
|
+
if (event.type === "session.inbox.delivered" || event.type === "session.inbox.cancelled")
|
|
202
|
+
return settleWake(ports, event);
|
|
203
|
+
if (event.type === "session.execution.started")
|
|
204
|
+
return wakeStarted(ports, event);
|
|
205
|
+
if (TURN_ENDS.has(event.type))
|
|
206
|
+
return wakeEnded(ports, event);
|
|
207
|
+
if (event.type === "session.deleted")
|
|
208
|
+
return forgetWakes(ports, event.data.sessionID);
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
/** The tree's handling of an event alongside the wake watch's, which touches other keys and never fails the tree's. */
|
|
212
|
+
async function route(ports, state, outside, event) {
|
|
213
|
+
const watched = routeWake(ports, event)?.catch((error) => ports.log(`courier watch: wake of ${event.type}: ${String(error)}`));
|
|
214
|
+
try {
|
|
215
|
+
return await routeTree(ports, state, outside, event);
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
await watched;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
194
221
|
/**
|
|
195
222
|
* Tells parents of a child's failed or silent turn, unless it waits on what `outside` says, and notes
|
|
196
223
|
* its prompts, interruptions and deletion; `after` a turn's end, seals the groups of its session.
|
|
197
224
|
*/
|
|
198
225
|
export function treeEvents(state, outside) {
|
|
199
226
|
return {
|
|
200
|
-
handle: (ports, event) =>
|
|
227
|
+
handle: (ports, event) => route(ports, state, outside, event),
|
|
201
228
|
// After: a silent end is judged while the group the nudge may deliver is still there to wait on.
|
|
202
229
|
after: (ports, event) => (TURN_ENDS.has(event.type) ? sealGroups(ports, event) : undefined),
|
|
203
230
|
};
|
package/dist/tree/index.d.ts
CHANGED
|
@@ -3,8 +3,9 @@ export { shapeContext } from "./context.js";
|
|
|
3
3
|
export { lineageIn, nativeRootOf, readLimits, readNativeChildren } from "./domain.js";
|
|
4
4
|
export { together, treeEvents, waitsOn } from "./events.js";
|
|
5
5
|
export { deliverReleased } from "./group.js";
|
|
6
|
-
export type { CleanupPorts, CourierPorts, Pending, PendingPrompt, RosterEntry, RosterStorage, SpawnGate, Waits, WatchPorts, WatchState, } from "./ports.js";
|
|
6
|
+
export type { CleanupPorts, CourierPorts, Pending, PendingPrompt, RosterEntry, RosterStorage, ShownForm, SpawnGate, Waits, WatchPorts, WatchState, } from "./ports.js";
|
|
7
7
|
export { allEntries, answeringTop, lineage, pruneExpired, RETENTION_MS } from "./roster.js";
|
|
8
8
|
export { subtree } from "./subtree.js";
|
|
9
9
|
export { registerTreeRpc } from "./rpc-handlers.js";
|
|
10
10
|
export { addTreeTools } from "./tools.js";
|
|
11
|
+
export { wakeMissed } from "./wake.js";
|
package/dist/tree/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// What the rest of the plugin uses of the session tree: its tools, RPC, event module, context hook and tick
|
|
1
|
+
// What the rest of the plugin uses of the session tree: its tools, RPC, event module, context hook and tick jobs
|
|
2
2
|
// for `src/index.ts`, and the lineage and subtree queries and types `person/` reads.
|
|
3
3
|
export { dirtyOf, headOf, inspectWorktree } from "./cleanup.js";
|
|
4
4
|
export { shapeContext } from "./context.js";
|
|
@@ -9,3 +9,4 @@ export { allEntries, answeringTop, lineage, pruneExpired, RETENTION_MS } from ".
|
|
|
9
9
|
export { subtree } from "./subtree.js";
|
|
10
10
|
export { registerTreeRpc } from "./rpc-handlers.js";
|
|
11
11
|
export { addTreeTools } from "./tools.js";
|
|
12
|
+
export { wakeMissed } from "./wake.js";
|
package/dist/tree/panel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CourierPorts, Pending } from "./ports.js";
|
|
1
|
+
import type { CourierPorts, Pending, ShownForm } from "./ports.js";
|
|
2
2
|
import type { Tree, Wait } from "./rpc.js";
|
|
3
3
|
type Messages = Awaited<ReturnType<CourierPorts["session"]["context"]>>;
|
|
4
4
|
/** The last tool call in a session's messages, and when it was made. */
|
|
@@ -8,6 +8,11 @@ export declare function lastTool(messages: Messages): {
|
|
|
8
8
|
} | undefined;
|
|
9
9
|
/** A request as the panel says what a paused session waits on: a permission request, or its first question. */
|
|
10
10
|
export declare const waitOf: (pending: Pending) => Wait;
|
|
11
|
-
/**
|
|
11
|
+
/** A form a session shows, as the panel says what it waits on. */
|
|
12
|
+
export declare const formWait: (form: ShownForm) => Wait;
|
|
13
|
+
/**
|
|
14
|
+
* The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, the live ones
|
|
15
|
+
* first, then the newest, each as `row` reads it; `truncated` counts the older ones left out, and `stalled` says the root is.
|
|
16
|
+
*/
|
|
12
17
|
export declare function panel(ports: CourierPorts, rootID: string): Promise<Tree>;
|
|
13
18
|
export {};
|
package/dist/tree/panel.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { busy } from "./domain.js";
|
|
2
|
-
import { lastStatus,
|
|
2
|
+
import { lastStatus, stoppedSincePrompt } from "./report.js";
|
|
3
3
|
import { standingIn } from "./status.js";
|
|
4
4
|
import { childStore } from "./store.js";
|
|
5
|
-
import {
|
|
5
|
+
import { readTree } from "./subtree.js";
|
|
6
|
+
import { stalledAmong } from "./wake.js";
|
|
6
7
|
/** The last tool call in a session's messages, and when it was made. */
|
|
7
8
|
export function lastTool(messages) {
|
|
8
9
|
for (const message of messages.toReversed()) {
|
|
@@ -18,6 +19,8 @@ export function lastTool(messages) {
|
|
|
18
19
|
export const waitOf = (pending) => pending.type === "permission"
|
|
19
20
|
? { type: "permission", action: pending.action, resources: [...pending.resources] }
|
|
20
21
|
: { type: "question", question: pending.questions[0]?.question ?? "" };
|
|
22
|
+
/** A form a session shows, as the panel says what it waits on. */
|
|
23
|
+
export const formWait = (form) => ({ type: "form", title: form.title });
|
|
21
24
|
/** A session's report since its last prompt, as the panel shows it; none for one spawned before this was kept. */
|
|
22
25
|
function reportRow(report, status) {
|
|
23
26
|
if (!report)
|
|
@@ -28,25 +31,27 @@ function reportRow(report, status) {
|
|
|
28
31
|
return { report: { state: "ended", progressed: report.progressed } };
|
|
29
32
|
return { report: { state: "reported", progressed: report.progressed, ...(status ? { status } : {}) } };
|
|
30
33
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
34
|
+
/**
|
|
35
|
+
* What a session does: waiting on a request to the person or a form it shows them, stalled, running, idle, or gone when
|
|
36
|
+
* OpenCode no longer knows it. A message waiting in its inbox makes a stalled session look busy.
|
|
37
|
+
*/
|
|
38
|
+
function stateOf(info, waits, stalled, running) {
|
|
33
39
|
if (!info)
|
|
34
40
|
return "gone";
|
|
35
41
|
if (waits)
|
|
36
42
|
return "waiting";
|
|
43
|
+
if (stalled)
|
|
44
|
+
return "stalled";
|
|
37
45
|
return running ? "running" : "idle";
|
|
38
46
|
}
|
|
39
47
|
/** 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) {
|
|
48
|
+
async function row(ports, { entry, depth, info, pending, forms, report }, stalled) {
|
|
41
49
|
const { sessionID } = entry;
|
|
42
|
-
const
|
|
43
|
-
const [info, messages, pending, report, status, standing] = await Promise.all([
|
|
44
|
-
ports.session.get({ sessionID }).catch(() => undefined),
|
|
50
|
+
const [messages, status, standing, stopped] = await Promise.all([
|
|
45
51
|
ports.session.context({ sessionID }).catch(() => []),
|
|
46
|
-
ports.
|
|
47
|
-
reportOf(children, sessionID).catch(() => undefined),
|
|
48
|
-
lastStatus(children, sessionID).catch(() => undefined),
|
|
52
|
+
lastStatus(childStore(ports.storage), sessionID).catch(() => undefined),
|
|
49
53
|
entry.group ? standingIn(ports, entry, entry.group) : undefined,
|
|
54
|
+
stoppedSincePrompt(childStore(ports.storage), sessionID).catch(() => false),
|
|
50
55
|
]);
|
|
51
56
|
const running = info !== undefined && busy(info.time);
|
|
52
57
|
const tool = lastTool(messages);
|
|
@@ -55,8 +60,10 @@ async function row(ports, entry, depth) {
|
|
|
55
60
|
parentID: entry.parentID,
|
|
56
61
|
title: info?.title ?? entry.title,
|
|
57
62
|
depth,
|
|
58
|
-
state: stateOf(info, pending.length, running),
|
|
63
|
+
state: stateOf(info, pending.length + forms.length, stalled !== undefined, running),
|
|
64
|
+
...(stalled !== undefined ? { stalled } : {}),
|
|
59
65
|
...(info?.outcome ? { outcome: info.outcome } : {}),
|
|
66
|
+
...(stopped ? { stopped } : {}),
|
|
60
67
|
...reportRow(report, status),
|
|
61
68
|
...(entry.group && standing ? { group: { name: entry.group, standing } } : {}),
|
|
62
69
|
isolated: entry.isolated,
|
|
@@ -64,12 +71,17 @@ async function row(ports, entry, depth) {
|
|
|
64
71
|
...(info && !running && info.time.idle !== undefined ? { idle: info.time.idle } : {}),
|
|
65
72
|
...(info ? { cost: info.cost } : {}),
|
|
66
73
|
...(tool ? { tool } : {}),
|
|
67
|
-
waits: pending.map(waitOf),
|
|
74
|
+
waits: [...pending.map(waitOf), ...forms.map(formWait)],
|
|
68
75
|
};
|
|
69
76
|
}
|
|
70
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The tree panel's rows: the sessions under a session as courier_tree lists them, at most `maxTotal`, the live ones
|
|
79
|
+
* first, then the newest, each as `row` reads it; `truncated` counts the older ones left out, and `stalled` says the root is.
|
|
80
|
+
*/
|
|
71
81
|
export async function panel(ports, rootID) {
|
|
72
|
-
const {
|
|
73
|
-
const
|
|
74
|
-
|
|
82
|
+
const { kept, leftOut } = await readTree(ports, rootID);
|
|
83
|
+
const stalled = await stalledAmong(ports, [rootID, ...kept.map((node) => node.entry.sessionID)]);
|
|
84
|
+
const rows = await Promise.all(kept.map((node) => row(ports, node, stalled.get(node.entry.sessionID))));
|
|
85
|
+
const own = stalled.get(rootID);
|
|
86
|
+
return { rows, ...(leftOut ? { truncated: leftOut } : {}), ...(own !== undefined ? { stalled: own } : {}) };
|
|
75
87
|
}
|
package/dist/tree/ports.d.ts
CHANGED
|
@@ -80,6 +80,35 @@ export interface ChildStore {
|
|
|
80
80
|
/** Removes report keys `reported` found, each best effort. */
|
|
81
81
|
dropReported(keys: ReadonlyArray<string>): Promise<void>;
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* A courier message waiting in a session's inbox, watched until it is delivered: `running` while the session runs a
|
|
85
|
+
* turn, which takes it at its next step; `idleAt` since the session was last seen idle with it waiting.
|
|
86
|
+
*/
|
|
87
|
+
export interface Wake {
|
|
88
|
+
readonly sessionID: string;
|
|
89
|
+
readonly inboxID: string;
|
|
90
|
+
/** When OpenCode enqueued it. */
|
|
91
|
+
readonly at: number;
|
|
92
|
+
/** Its text and description, which waking the session again sends under the same inbox id. */
|
|
93
|
+
readonly text: string;
|
|
94
|
+
readonly description?: string;
|
|
95
|
+
readonly running: boolean;
|
|
96
|
+
readonly idleAt?: number;
|
|
97
|
+
/** When the scheduler woke the session again, the message still waiting. */
|
|
98
|
+
readonly rewoken?: number;
|
|
99
|
+
/** When the session was found still not woken after that: it is stalled. */
|
|
100
|
+
readonly stalled?: number;
|
|
101
|
+
}
|
|
102
|
+
export interface WakeStore {
|
|
103
|
+
/** Writes a watched message, replacing what was kept of it. */
|
|
104
|
+
put(wake: Wake): Promise<void>;
|
|
105
|
+
/** The messages watched in a session's inbox. */
|
|
106
|
+
of(sessionID: string): Promise<Wake[]>;
|
|
107
|
+
/** Every message watched. */
|
|
108
|
+
all(): Promise<Wake[]>;
|
|
109
|
+
/** Stops watching a message: delivered, cancelled, or its session gone. */
|
|
110
|
+
remove(sessionID: string, inboxID: string): Promise<void>;
|
|
111
|
+
}
|
|
83
112
|
/** A member of a join group with where it is kept. */
|
|
84
113
|
export interface Membership extends Member {
|
|
85
114
|
readonly parentID: string;
|
|
@@ -133,6 +162,8 @@ export interface CourierPorts {
|
|
|
133
162
|
readonly now: () => number;
|
|
134
163
|
/** The permission requests and questions a session waits on, wherever they are pending. */
|
|
135
164
|
readonly pending: (sessionID: string) => Promise<ReadonlyArray<Pending>>;
|
|
165
|
+
/** The forms a session shows, which only the person can answer: web search's, a plugin's; not the question tool's. */
|
|
166
|
+
readonly forms: (sessionID: string) => Promise<ReadonlyArray<ShownForm>>;
|
|
136
167
|
readonly limits: Limits;
|
|
137
168
|
/** The `nativeChildren` option: a child started without isolate is created with OpenCode's own `parentID`. */
|
|
138
169
|
readonly nativeChildren?: boolean;
|
|
@@ -143,6 +174,11 @@ export interface CourierPorts {
|
|
|
143
174
|
readonly nudge: () => void;
|
|
144
175
|
readonly log: (message: string) => void;
|
|
145
176
|
}
|
|
177
|
+
/** A form a session shows, as `CourierPorts.forms` lists it; `person/` keeps them. */
|
|
178
|
+
export interface ShownForm {
|
|
179
|
+
readonly id: string;
|
|
180
|
+
readonly title: string;
|
|
181
|
+
}
|
|
146
182
|
/** One question a session waits on, as `Pending` shows it; `person/`'s `Prompt` has these fields. */
|
|
147
183
|
export interface PendingPrompt {
|
|
148
184
|
readonly question: string;
|
package/dist/tree/roster.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/tree/roster.js
CHANGED
|
@@ -31,11 +31,13 @@ export async function answeringTop(storage, sessionID, callerID, what) {
|
|
|
31
31
|
return top;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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) =>
|
|
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
|
}
|