opencode-courier 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -26
- package/dist/index.js +3 -2
- package/dist/person/events.d.ts +2 -2
- package/dist/person/events.js +4 -3
- package/dist/skill.js +11 -6
- package/dist/tree/events.js +26 -1
- package/dist/tree/index.d.ts +1 -0
- package/dist/tree/index.js +2 -1
- package/dist/tree/panel.d.ts +1 -1
- package/dist/tree/panel.js +16 -7
- package/dist/tree/ports.d.ts +29 -0
- package/dist/tree/rpc-handlers.d.ts +14 -5
- package/dist/tree/rpc.d.ts +39 -10
- package/dist/tree/rpc.js +11 -2
- package/dist/tree/stop.js +2 -0
- package/dist/tree/store.d.ts +4 -1
- package/dist/tree/store.js +51 -0
- package/dist/tree/summary.d.ts +2 -2
- package/dist/tree/summary.js +14 -5
- package/dist/tree/text.js +1 -1
- package/dist/tree/tui.d.ts +6 -2
- package/dist/tree/wake.d.ts +69 -0
- package/dist/tree/wake.js +122 -0
- package/dist/tui.js +78 -51
- package/package.json +4 -4
package/dist/tree/rpc.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ declare const Summary: Schema.Struct<{
|
|
|
7
7
|
readonly waiting: Schema.Int;
|
|
8
8
|
/** Not running: done, or waiting for a message. */
|
|
9
9
|
readonly idle: Schema.Int;
|
|
10
|
+
/** Left a courier message waiting while idle, and did not take it once woken again; the session itself counts too. Absent at 0. */
|
|
11
|
+
readonly stalled: Schema.optionalKey<Schema.Int>;
|
|
10
12
|
readonly groups: Schema.$Array<Schema.Struct<{
|
|
11
13
|
readonly name: Schema.String;
|
|
12
14
|
readonly reported: Schema.Int;
|
|
@@ -33,8 +35,13 @@ declare const Row: Schema.Struct<{
|
|
|
33
35
|
readonly title: Schema.String;
|
|
34
36
|
/** A child of the panel's session is at depth 1. */
|
|
35
37
|
readonly depth: Schema.Int;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
+
/**
|
|
39
|
+
* `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
|
|
40
|
+
* waking it again did not start a turn; `gone` when OpenCode no longer knows it.
|
|
41
|
+
*/
|
|
42
|
+
readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
|
|
43
|
+
/** Since when its oldest message that did not wake it has waited, when it is stalled. */
|
|
44
|
+
readonly stalled: Schema.optionalKey<Schema.Number>;
|
|
38
45
|
/** How its last turn ended. */
|
|
39
46
|
readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
|
|
40
47
|
/** Set when courier_stop stopped it since its last prompt. */
|
|
@@ -81,8 +88,13 @@ declare const Tree: Schema.Struct<{
|
|
|
81
88
|
readonly title: Schema.String;
|
|
82
89
|
/** A child of the panel's session is at depth 1. */
|
|
83
90
|
readonly depth: Schema.Int;
|
|
84
|
-
/**
|
|
85
|
-
|
|
91
|
+
/**
|
|
92
|
+
* `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
|
|
93
|
+
* waking it again did not start a turn; `gone` when OpenCode no longer knows it.
|
|
94
|
+
*/
|
|
95
|
+
readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
|
|
96
|
+
/** Since when its oldest message that did not wake it has waited, when it is stalled. */
|
|
97
|
+
readonly stalled: Schema.optionalKey<Schema.Number>;
|
|
86
98
|
/** How its last turn ended. */
|
|
87
99
|
readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
|
|
88
100
|
/** Set when courier_stop stopped it since its last prompt. */
|
|
@@ -123,6 +135,8 @@ declare const Tree: Schema.Struct<{
|
|
|
123
135
|
}>>;
|
|
124
136
|
/** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
|
|
125
137
|
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
138
|
+
/** Since when a courier message has waited in the panel's own session, when that session is stalled. */
|
|
139
|
+
readonly stalled: Schema.optionalKey<Schema.Number>;
|
|
126
140
|
}>;
|
|
127
141
|
export type Row = typeof Row.Type;
|
|
128
142
|
export type Tree = typeof Tree.Type;
|
|
@@ -159,22 +173,24 @@ export declare const treeRpc: {
|
|
|
159
173
|
}>;
|
|
160
174
|
readonly output: import("effect/StandardSchema").StandardSchemaV1<{
|
|
161
175
|
readonly running: number;
|
|
162
|
-
readonly waiting: number;
|
|
163
176
|
readonly idle: number;
|
|
164
177
|
readonly groups: readonly {
|
|
165
178
|
readonly name: string;
|
|
166
179
|
readonly reported: number;
|
|
167
180
|
readonly members: number;
|
|
168
181
|
}[];
|
|
182
|
+
readonly waiting: number;
|
|
183
|
+
readonly stalled?: number | undefined;
|
|
169
184
|
}, {
|
|
170
185
|
readonly running: number;
|
|
171
|
-
readonly waiting: number;
|
|
172
186
|
readonly idle: number;
|
|
173
187
|
readonly groups: readonly {
|
|
174
188
|
readonly name: string;
|
|
175
189
|
readonly reported: number;
|
|
176
190
|
readonly members: number;
|
|
177
191
|
}[];
|
|
192
|
+
readonly waiting: number;
|
|
193
|
+
readonly stalled?: number | undefined;
|
|
178
194
|
}> & Schema.Struct<{
|
|
179
195
|
/** Running, and waiting on no request or form. */
|
|
180
196
|
readonly running: Schema.Int;
|
|
@@ -182,6 +198,8 @@ export declare const treeRpc: {
|
|
|
182
198
|
readonly waiting: Schema.Int;
|
|
183
199
|
/** Not running: done, or waiting for a message. */
|
|
184
200
|
readonly idle: Schema.Int;
|
|
201
|
+
/** Left a courier message waiting while idle, and did not take it once woken again; the session itself counts too. Absent at 0. */
|
|
202
|
+
readonly stalled: Schema.optionalKey<Schema.Int>;
|
|
185
203
|
readonly groups: Schema.$Array<Schema.Struct<{
|
|
186
204
|
readonly name: Schema.String;
|
|
187
205
|
readonly reported: Schema.Int;
|
|
@@ -205,7 +223,7 @@ export declare const treeRpc: {
|
|
|
205
223
|
readonly depth: number;
|
|
206
224
|
readonly created: number;
|
|
207
225
|
readonly isolated: boolean;
|
|
208
|
-
readonly state: "running" | "idle" | "gone" | "waiting";
|
|
226
|
+
readonly state: "running" | "idle" | "gone" | "stalled" | "waiting";
|
|
209
227
|
readonly waits: readonly ({
|
|
210
228
|
readonly type: "permission";
|
|
211
229
|
readonly action: string;
|
|
@@ -228,6 +246,7 @@ export declare const treeRpc: {
|
|
|
228
246
|
readonly state: "reported" | "ended" | "owed";
|
|
229
247
|
readonly status?: string | undefined;
|
|
230
248
|
} | undefined;
|
|
249
|
+
readonly stalled?: number | undefined;
|
|
231
250
|
readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
232
251
|
readonly tool?: {
|
|
233
252
|
readonly name: string;
|
|
@@ -235,6 +254,7 @@ export declare const treeRpc: {
|
|
|
235
254
|
} | undefined;
|
|
236
255
|
readonly cost?: number | undefined;
|
|
237
256
|
}[];
|
|
257
|
+
readonly stalled?: number | undefined;
|
|
238
258
|
readonly truncated?: number | undefined;
|
|
239
259
|
}, {
|
|
240
260
|
readonly rows: readonly {
|
|
@@ -244,7 +264,7 @@ export declare const treeRpc: {
|
|
|
244
264
|
readonly depth: number;
|
|
245
265
|
readonly created: number;
|
|
246
266
|
readonly isolated: boolean;
|
|
247
|
-
readonly state: "running" | "idle" | "gone" | "waiting";
|
|
267
|
+
readonly state: "running" | "idle" | "gone" | "stalled" | "waiting";
|
|
248
268
|
readonly waits: readonly ({
|
|
249
269
|
readonly type: "permission";
|
|
250
270
|
readonly action: string;
|
|
@@ -267,6 +287,7 @@ export declare const treeRpc: {
|
|
|
267
287
|
readonly state: "reported" | "ended" | "owed";
|
|
268
288
|
readonly status?: string | undefined;
|
|
269
289
|
} | undefined;
|
|
290
|
+
readonly stalled?: number | undefined;
|
|
270
291
|
readonly outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
271
292
|
readonly tool?: {
|
|
272
293
|
readonly name: string;
|
|
@@ -274,6 +295,7 @@ export declare const treeRpc: {
|
|
|
274
295
|
} | undefined;
|
|
275
296
|
readonly cost?: number | undefined;
|
|
276
297
|
}[];
|
|
298
|
+
readonly stalled?: number | undefined;
|
|
277
299
|
readonly truncated?: number | undefined;
|
|
278
300
|
}> & Schema.Struct<{
|
|
279
301
|
readonly rows: Schema.$Array<Schema.Struct<{
|
|
@@ -282,8 +304,13 @@ export declare const treeRpc: {
|
|
|
282
304
|
readonly title: Schema.String;
|
|
283
305
|
/** A child of the panel's session is at depth 1. */
|
|
284
306
|
readonly depth: Schema.Int;
|
|
285
|
-
/**
|
|
286
|
-
|
|
307
|
+
/**
|
|
308
|
+
* `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
|
|
309
|
+
* waking it again did not start a turn; `gone` when OpenCode no longer knows it.
|
|
310
|
+
*/
|
|
311
|
+
readonly state: Schema.Literals<readonly ["running", "waiting", "idle", "stalled", "gone"]>;
|
|
312
|
+
/** Since when its oldest message that did not wake it has waited, when it is stalled. */
|
|
313
|
+
readonly stalled: Schema.optionalKey<Schema.Number>;
|
|
287
314
|
/** How its last turn ended. */
|
|
288
315
|
readonly outcome: Schema.optionalKey<Schema.Literals<readonly ["succeeded", "failed", "interrupted"]>>;
|
|
289
316
|
/** Set when courier_stop stopped it since its last prompt. */
|
|
@@ -324,6 +351,8 @@ export declare const treeRpc: {
|
|
|
324
351
|
}>>;
|
|
325
352
|
/** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
|
|
326
353
|
readonly truncated: Schema.optionalKey<Schema.Int>;
|
|
354
|
+
/** Since when a courier message has waited in the panel's own session, when that session is stalled. */
|
|
355
|
+
readonly stalled: Schema.optionalKey<Schema.Number>;
|
|
327
356
|
}>;
|
|
328
357
|
};
|
|
329
358
|
/** Stops `sessionID` and everything under it, as courier_stop called from `by`, which must be above it. */
|
package/dist/tree/rpc.js
CHANGED
|
@@ -15,6 +15,8 @@ const Summary = Schema.Struct({
|
|
|
15
15
|
waiting: Count,
|
|
16
16
|
/** Not running: done, or waiting for a message. */
|
|
17
17
|
idle: Count,
|
|
18
|
+
/** Left a courier message waiting while idle, and did not take it once woken again; the session itself counts too. Absent at 0. */
|
|
19
|
+
stalled: Schema.optionalKey(Count),
|
|
18
20
|
groups: Schema.Array(Group),
|
|
19
21
|
});
|
|
20
22
|
/** A request a session waits on until the person answers it: a permission request, its first question, or a form it shows. */
|
|
@@ -30,8 +32,13 @@ const Row = Schema.Struct({
|
|
|
30
32
|
title: Schema.String,
|
|
31
33
|
/** A child of the panel's session is at depth 1. */
|
|
32
34
|
depth: Count,
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* `waiting` on a request to the person or a form it shows them; `stalled` when a courier message waits in its inbox and
|
|
37
|
+
* waking it again did not start a turn; `gone` when OpenCode no longer knows it.
|
|
38
|
+
*/
|
|
39
|
+
state: Schema.Literals(["running", "waiting", "idle", "stalled", "gone"]),
|
|
40
|
+
/** Since when its oldest message that did not wake it has waited, when it is stalled. */
|
|
41
|
+
stalled: Schema.optionalKey(Schema.Number),
|
|
35
42
|
/** How its last turn ended. */
|
|
36
43
|
outcome: Schema.optionalKey(Schema.Literals(["succeeded", "failed", "interrupted"])),
|
|
37
44
|
/** Set when courier_stop stopped it since its last prompt. */
|
|
@@ -55,6 +62,8 @@ const Tree = Schema.Struct({
|
|
|
55
62
|
rows: Schema.Array(Row),
|
|
56
63
|
/** How many older sessions the listing left out, when it had more than `maxTotal`: the live ones come first, then the newest. */
|
|
57
64
|
truncated: Schema.optionalKey(Count),
|
|
65
|
+
/** Since when a courier message has waited in the panel's own session, when that session is stalled. */
|
|
66
|
+
stalled: Schema.optionalKey(Schema.Number),
|
|
58
67
|
});
|
|
59
68
|
/** What a stop did to each session, deepest first, as courier_stop reports it. */
|
|
60
69
|
const Stopped = Schema.Struct({
|
package/dist/tree/stop.js
CHANGED
|
@@ -4,6 +4,7 @@ import { cleanup } from "./cleanup.js";
|
|
|
4
4
|
import { childStore } from "./store.js";
|
|
5
5
|
import { subtree } from "./subtree.js";
|
|
6
6
|
import { stoppedNotice } from "./text.js";
|
|
7
|
+
import { forgetWakes } from "./wake.js";
|
|
7
8
|
/** Rounds of looking again for sessions started while others were being stopped. */
|
|
8
9
|
const ROUNDS = 3;
|
|
9
10
|
/**
|
|
@@ -62,6 +63,7 @@ async function silence(ports, sessionID, cancelled) {
|
|
|
62
63
|
catch (error) {
|
|
63
64
|
log(`courier_stop: could not cancel the messages scheduled for ${sessionID}: ${String(error)}`);
|
|
64
65
|
}
|
|
66
|
+
await forgetWakes(ports.courier, sessionID).catch((error) => log(`courier_stop: could not drop the wake watch of ${sessionID}: ${String(error)}`));
|
|
65
67
|
}
|
|
66
68
|
async function interrupt(ports, { entry, depth }) {
|
|
67
69
|
const { sessionID, title } = entry;
|
package/dist/tree/store.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Storage } from "../platform/storage.js";
|
|
2
|
-
import type { ChildStore, GroupStore } from "./ports.js";
|
|
2
|
+
import type { ChildStore, GroupStore, WakeStore } from "./ports.js";
|
|
3
3
|
/**
|
|
4
4
|
* What the reverse index holds for a child: the sessions above it, its parent first and the top
|
|
5
5
|
* session, which courier_spawn did not start, last; each is the parent of the one before it.
|
|
@@ -18,7 +18,10 @@ export declare const memberKey: (parentID: string, group: string, sessionID: str
|
|
|
18
18
|
/** The key prefix of a parent's groups, and of one group's members. */
|
|
19
19
|
export declare const groupsPrefix: (parentID: string) => string;
|
|
20
20
|
export declare const groupPrefix: (parentID: string, group: string) => string;
|
|
21
|
+
export declare const wakeKey: (sessionID: string, inboxID: string) => string;
|
|
21
22
|
/** The `ChildStore` over the plugin's storage. */
|
|
22
23
|
export declare function childStore(storage: Storage): ChildStore;
|
|
23
24
|
/** The `GroupStore` over the plugin's storage. */
|
|
24
25
|
export declare function groupStore(storage: Storage): GroupStore;
|
|
26
|
+
/** The `WakeStore` over the plugin's storage. */
|
|
27
|
+
export declare function wakeStore(storage: Storage): WakeStore;
|
package/dist/tree/store.js
CHANGED
|
@@ -42,6 +42,12 @@ export const memberKey = (parentID, group, sessionID) => `${GROUP_PREFIX}${paren
|
|
|
42
42
|
/** The key prefix of a parent's groups, and of one group's members. */
|
|
43
43
|
export const groupsPrefix = (parentID) => `${GROUP_PREFIX}${parentID}/`;
|
|
44
44
|
export const groupPrefix = (parentID, group) => `${groupsPrefix(parentID)}${group}/`;
|
|
45
|
+
/**
|
|
46
|
+
* The courier messages waiting in a session's inbox, each under `wake/<sessionID>/<inboxID>` until it is delivered, for
|
|
47
|
+
* the scheduler to wake a session whose wake was missed. docs/maintainers/architecture.md, § Storage.
|
|
48
|
+
*/
|
|
49
|
+
const WAKE_PREFIX = "wake/";
|
|
50
|
+
export const wakeKey = (sessionID, inboxID) => `${WAKE_PREFIX}${sessionID}/${inboxID}`;
|
|
45
51
|
function isReverse(value) {
|
|
46
52
|
const ancestors = value?.ancestors;
|
|
47
53
|
return Array.isArray(ancestors) && ancestors.length > 0 && ancestors.every((id) => typeof id === "string");
|
|
@@ -262,3 +268,48 @@ export function groupStore(storage) {
|
|
|
262
268
|
},
|
|
263
269
|
};
|
|
264
270
|
}
|
|
271
|
+
/** A watched message as stored under its key's session and inbox id; one that cannot be read is left out. */
|
|
272
|
+
function readWake(sessionID, inboxID, value) {
|
|
273
|
+
const given = obj(value);
|
|
274
|
+
const at = num(given.at);
|
|
275
|
+
const text = str(given.text);
|
|
276
|
+
if (at === undefined || text === undefined || typeof given.running !== "boolean")
|
|
277
|
+
return undefined;
|
|
278
|
+
const optional = (field) => {
|
|
279
|
+
const time = num(given[field]);
|
|
280
|
+
return time === undefined ? {} : { [field]: time };
|
|
281
|
+
};
|
|
282
|
+
const description = str(given.description);
|
|
283
|
+
return {
|
|
284
|
+
sessionID,
|
|
285
|
+
inboxID,
|
|
286
|
+
at,
|
|
287
|
+
text,
|
|
288
|
+
...(description !== undefined ? { description } : {}),
|
|
289
|
+
running: given.running,
|
|
290
|
+
...optional("idleAt"),
|
|
291
|
+
...optional("rewoken"),
|
|
292
|
+
...optional("stalled"),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/** The `WakeStore` over the plugin's storage. */
|
|
296
|
+
export function wakeStore(storage) {
|
|
297
|
+
const under = async (prefix) => {
|
|
298
|
+
const entries = await scanEntries(storage, prefix);
|
|
299
|
+
return entries.flatMap(({ key, value }) => {
|
|
300
|
+
const parts = `${prefix.slice(WAKE_PREFIX.length)}${key}`.split("/");
|
|
301
|
+
const wake = parts.length === 2 ? readWake(parts[0], parts[1], value) : undefined;
|
|
302
|
+
return wake ? [wake] : [];
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
return {
|
|
306
|
+
async put({ sessionID, inboxID, ...wake }) {
|
|
307
|
+
await storage.set(wakeKey(sessionID, inboxID), { ...wake });
|
|
308
|
+
},
|
|
309
|
+
of: (sessionID) => under(`${WAKE_PREFIX}${sessionID}/`),
|
|
310
|
+
all: () => under(WAKE_PREFIX),
|
|
311
|
+
async remove(sessionID, inboxID) {
|
|
312
|
+
await storage.remove(wakeKey(sessionID, inboxID));
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
package/dist/tree/summary.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CourierPorts } from "./ports.js";
|
|
2
2
|
import type { Summary } from "./rpc.js";
|
|
3
3
|
/**
|
|
4
|
-
* Every session started under a session, counted by what it is doing,
|
|
5
|
-
* children, with how far each has reported.
|
|
4
|
+
* Every session started under a session, counted by what it is doing (the session itself too, when it is stalled), and its
|
|
5
|
+
* open join groups, those of its own children, with how far each has reported.
|
|
6
6
|
*/
|
|
7
7
|
export declare function summary(ports: CourierPorts, sessionID: string): Promise<Summary>;
|
package/dist/tree/summary.js
CHANGED
|
@@ -2,26 +2,35 @@ import { busy } from "./domain.js";
|
|
|
2
2
|
import { groupStandings } from "./group.js";
|
|
3
3
|
import { groupStore } from "./store.js";
|
|
4
4
|
import { lookUp } from "./subtree.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { stalledAmong } from "./wake.js";
|
|
6
|
+
/**
|
|
7
|
+
* What a session is doing: waiting on a request it made or a form it shows, stalled, running, or idle; undefined when
|
|
8
|
+
* OpenCode no longer knows it. A message waiting in its inbox makes a stalled session look busy.
|
|
9
|
+
*/
|
|
10
|
+
function activity({ info, pending, forms, entry }, stalled) {
|
|
7
11
|
if (!info)
|
|
8
12
|
return undefined;
|
|
9
13
|
if (pending.length || forms.length)
|
|
10
14
|
return "waiting";
|
|
15
|
+
if (stalled.has(entry.sessionID))
|
|
16
|
+
return "stalled";
|
|
11
17
|
return busy(info.time) ? "running" : "idle";
|
|
12
18
|
}
|
|
13
19
|
/**
|
|
14
|
-
* Every session started under a session, counted by what it is doing,
|
|
15
|
-
* children, with how far each has reported.
|
|
20
|
+
* Every session started under a session, counted by what it is doing (the session itself too, when it is stalled), and its
|
|
21
|
+
* open join groups, those of its own children, with how far each has reported.
|
|
16
22
|
*/
|
|
17
23
|
export async function summary(ports, sessionID) {
|
|
18
24
|
const [looked, standings] = await Promise.all([lookUp(ports, sessionID), groupStandings(groupStore(ports.storage), sessionID)]);
|
|
19
|
-
const
|
|
25
|
+
const stalled = await stalledAmong(ports, [sessionID, ...looked.map((one) => one.entry.sessionID)]);
|
|
26
|
+
const activities = looked.map((one) => activity(one, stalled));
|
|
20
27
|
const count = (kind) => activities.filter((item) => item === kind).length;
|
|
28
|
+
const stalls = count("stalled") + (stalled.has(sessionID) ? 1 : 0);
|
|
21
29
|
return {
|
|
22
30
|
running: count("running"),
|
|
23
31
|
waiting: count("waiting"),
|
|
24
32
|
idle: count("idle"),
|
|
33
|
+
...(stalls ? { stalled: stalls } : {}),
|
|
25
34
|
groups: standings.map(({ name, reported, members }) => ({ name, reported, members })),
|
|
26
35
|
};
|
|
27
36
|
}
|
package/dist/tree/text.js
CHANGED
|
@@ -47,7 +47,7 @@ function splitRule(parentID, depth, limits) {
|
|
|
47
47
|
`You are at depth ${depth} of a session tree that goes at most ${limits.maxDepth} deep. Split your task or do it yourself:`,
|
|
48
48
|
"- Split it when it has 2 or more independent parts, each substantial and touching separate files or areas: start one " +
|
|
49
49
|
`session per part with courier_spawn, at most ${limits.maxChildren} of yours running at once and ${limits.maxTotal} in the whole tree.`,
|
|
50
|
-
"- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session.",
|
|
50
|
+
"- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole task.",
|
|
51
51
|
`- Say which in one line of your reply: ${SPLIT_LINE}.`,
|
|
52
52
|
"- If you split, you orchestrate: end your turn while they work, check and integrate each part yourself when it " +
|
|
53
53
|
`reports (never hand that checking to another session), then send ${parentID} one combined report.`,
|
package/dist/tree/tui.d.ts
CHANGED
|
@@ -34,11 +34,15 @@ export declare function waitLine(wait: Wait): string;
|
|
|
34
34
|
export declare function statusLine(row: Row): string;
|
|
35
35
|
/** A leaf's activity: how long it has run, its last tool call and how long ago, and its cost. */
|
|
36
36
|
export declare function activityLine(row: Row, now: number): string;
|
|
37
|
-
/**
|
|
37
|
+
/** What a stalled session's row, or the panel of one, says of it: "a message waits in its inbox since 2m 05s, woken again in vain". */
|
|
38
|
+
export declare function stalledLine(since: number, now: number): string;
|
|
39
|
+
/** The lines a row shows under its title: an orchestrator's bar, a paused or stalled session's wait, or else a live leaf's activity. */
|
|
38
40
|
export declare function detailLines(row: Row, rows: ReadonlyArray<Row>, now: number): string[];
|
|
41
|
+
/** How often the footer and the panel ask again with no event: the scheduler flags a stalled session without one. */
|
|
42
|
+
export declare const REREAD_MS = 15000;
|
|
39
43
|
/**
|
|
40
44
|
* Adds the tree's entry to the prompt's footer: what the sessions under the open session do, asked of the RPC after each
|
|
41
|
-
* change
|
|
45
|
+
* change and every `REREAD_MS`; nothing on the home screen. Clicked while some wait on the person, it calls `onWaiting`.
|
|
42
46
|
*/
|
|
43
47
|
export declare function addTreeStatus(ctx: Context, onWaiting?: (sessionID: string) => void): () => void;
|
|
44
48
|
/** The toast after a stop: how many sessions were interrupted, and how many scheduled messages were cancelled. */
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { GroupPorts, Wake, WatchPorts } from "./ports.js";
|
|
2
|
+
/** How long a courier message may wait in an idle session's inbox before its wake counts as missed. */
|
|
3
|
+
export declare const WAKE_MS = 30000;
|
|
4
|
+
/** OpenCode's `session.inbox.enqueued`, with the item's payload, which carries the courier's metadata. */
|
|
5
|
+
export interface Enqueued {
|
|
6
|
+
readonly created?: number;
|
|
7
|
+
readonly data: {
|
|
8
|
+
readonly sessionID: string;
|
|
9
|
+
readonly inboxID: string;
|
|
10
|
+
readonly item: {
|
|
11
|
+
readonly type: string;
|
|
12
|
+
readonly payload?: {
|
|
13
|
+
readonly text?: unknown;
|
|
14
|
+
readonly description?: unknown;
|
|
15
|
+
readonly metadata?: {
|
|
16
|
+
readonly source?: unknown;
|
|
17
|
+
readonly resume?: unknown;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** OpenCode's `session.inbox.delivered` and `session.inbox.cancelled`. */
|
|
24
|
+
export interface InboxSettled {
|
|
25
|
+
readonly data: {
|
|
26
|
+
readonly sessionID: string;
|
|
27
|
+
readonly inboxID: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** OpenCode's `session.execution.*` events, which frame a session's busy period. */
|
|
31
|
+
export interface Execution {
|
|
32
|
+
readonly created?: number;
|
|
33
|
+
readonly data: {
|
|
34
|
+
readonly sessionID: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
|
|
39
|
+
* Returns whether it is watched, which one sent with `resume: false` is not.
|
|
40
|
+
*/
|
|
41
|
+
export declare function watchWake(ports: WatchPorts, event: Enqueued): Promise<boolean>;
|
|
42
|
+
/** Stops watching a message once it is delivered or cancelled. */
|
|
43
|
+
export declare function settleWake(ports: Pick<WatchPorts, "storage">, event: InboxSettled): Promise<void>;
|
|
44
|
+
/** A turn started in a session: the messages waiting there are its to take. */
|
|
45
|
+
export declare function wakeStarted(ports: Pick<WatchPorts, "storage">, event: Execution): Promise<void>;
|
|
46
|
+
/**
|
|
47
|
+
* A session's turn ended: a message it took is no longer watched, as its delivery may have been missed by this
|
|
48
|
+
* subscription; one still waiting is watched as waiting in an idle session from now.
|
|
49
|
+
*/
|
|
50
|
+
export declare function wakeEnded(ports: WatchPorts, event: Execution & {
|
|
51
|
+
readonly data: {
|
|
52
|
+
readonly reason?: string;
|
|
53
|
+
};
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
/** Forgets the messages watched in a session's inbox: it was deleted, or stopped, which nothing is to wake again. */
|
|
56
|
+
export declare function forgetWakes(ports: Pick<WatchPorts, "storage">, sessionID: string): Promise<void>;
|
|
57
|
+
/** The ports the scheduler's tick wakes sessions through: what every hub member's `later` ports have. */
|
|
58
|
+
export type WakePorts = GroupPorts & {
|
|
59
|
+
readonly now: () => number;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, and flags
|
|
63
|
+
* stalled one still waiting `WAKE_MS` later; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function wakeMissed(ports: WakePorts, claimed: Set<string>): Promise<void>;
|
|
66
|
+
/** Whether a message has waited in an idle session past `WAKE_MS`, and since a wake again, if one was sent; a stalled one is done. */
|
|
67
|
+
export declare function missed(wake: Wake, now: number): boolean;
|
|
68
|
+
/** The sessions among `sessionIDs` flagged stalled, each with when its oldest stalled message was enqueued. */
|
|
69
|
+
export declare function stalledAmong(ports: Pick<WatchPorts, "storage">, sessionIDs: ReadonlyArray<string>): Promise<Map<string, number>>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { wakeStore } from "./store.js";
|
|
2
|
+
/** How long a courier message may wait in an idle session's inbox before its wake counts as missed. */
|
|
3
|
+
export const WAKE_MS = 30_000;
|
|
4
|
+
/** Whether a session's stored messages end with the marker of an ended turn; one with none yet is taken to run. */
|
|
5
|
+
const endsIdle = (messages) => messages.at(-1)?.type === "idle";
|
|
6
|
+
/**
|
|
7
|
+
* Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
|
|
8
|
+
* Returns whether it is watched, which one sent with `resume: false` is not.
|
|
9
|
+
*/
|
|
10
|
+
export async function watchWake(ports, event) {
|
|
11
|
+
const { sessionID, inboxID, item } = event.data;
|
|
12
|
+
const payload = item.payload;
|
|
13
|
+
if (item.type !== "synthetic" || payload?.metadata?.source !== "courier" || typeof payload.text !== "string")
|
|
14
|
+
return false;
|
|
15
|
+
// Sent without a wake: it waits for the next turn, which nothing owes it.
|
|
16
|
+
if (payload.metadata.resume === false)
|
|
17
|
+
return false;
|
|
18
|
+
const at = event.created ?? ports.now();
|
|
19
|
+
// Read after the enqueue: a turn it started has stored messages since, and an unknown session is taken to run.
|
|
20
|
+
const idle = await ports.session.context({ sessionID }).then(endsIdle, () => false);
|
|
21
|
+
await wakeStore(ports.storage).put({
|
|
22
|
+
sessionID,
|
|
23
|
+
inboxID,
|
|
24
|
+
at,
|
|
25
|
+
text: payload.text,
|
|
26
|
+
...(typeof payload.description === "string" ? { description: payload.description } : {}),
|
|
27
|
+
running: !idle,
|
|
28
|
+
...(idle ? { idleAt: at } : {}),
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/** Stops watching a message once it is delivered or cancelled. */
|
|
33
|
+
export async function settleWake(ports, event) {
|
|
34
|
+
await wakeStore(ports.storage).remove(event.data.sessionID, event.data.inboxID);
|
|
35
|
+
}
|
|
36
|
+
/** A turn started in a session: the messages waiting there are its to take. */
|
|
37
|
+
export async function wakeStarted(ports, event) {
|
|
38
|
+
const store = wakeStore(ports.storage);
|
|
39
|
+
const waiting = await store.of(event.data.sessionID);
|
|
40
|
+
await Promise.all(waiting.filter((wake) => !wake.running).map(({ idleAt: _, ...wake }) => store.put({ ...wake, running: true })));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A session's turn ended: a message it took is no longer watched, as its delivery may have been missed by this
|
|
44
|
+
* subscription; one still waiting is watched as waiting in an idle session from now.
|
|
45
|
+
*/
|
|
46
|
+
export async function wakeEnded(ports, event) {
|
|
47
|
+
if (event.data.reason === "shutdown")
|
|
48
|
+
return;
|
|
49
|
+
const { sessionID } = event.data;
|
|
50
|
+
const store = wakeStore(ports.storage);
|
|
51
|
+
const waiting = await store.of(sessionID);
|
|
52
|
+
if (!waiting.length)
|
|
53
|
+
return;
|
|
54
|
+
const at = event.created ?? ports.now();
|
|
55
|
+
const stored = new Set((await ports.session.context({ sessionID }).catch(() => [])).map((message) => message.id));
|
|
56
|
+
await Promise.all(waiting.map((wake) => (stored.has(wake.inboxID) ? store.remove(sessionID, wake.inboxID) : store.put({ ...wake, running: false, idleAt: at }))));
|
|
57
|
+
}
|
|
58
|
+
/** Forgets the messages watched in a session's inbox: it was deleted, or stopped, which nothing is to wake again. */
|
|
59
|
+
export async function forgetWakes(ports, sessionID) {
|
|
60
|
+
const store = wakeStore(ports.storage);
|
|
61
|
+
await Promise.all((await store.of(sessionID)).map((wake) => store.remove(sessionID, wake.inboxID)));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, and flags
|
|
65
|
+
* stalled one still waiting `WAKE_MS` later; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
|
|
66
|
+
*/
|
|
67
|
+
export async function wakeMissed(ports, claimed) {
|
|
68
|
+
const store = wakeStore(ports.storage);
|
|
69
|
+
const now = ports.now();
|
|
70
|
+
const due = (await store.all()).filter((wake) => missed(wake, now) && !claimed.has(claimOf(wake)));
|
|
71
|
+
for (const wake of due)
|
|
72
|
+
claimed.add(claimOf(wake));
|
|
73
|
+
await Promise.all(due.map((wake) => wakeAgain(ports, store, wake, now).finally(() => claimed.delete(claimOf(wake)))));
|
|
74
|
+
}
|
|
75
|
+
const claimOf = (wake) => `wake ${wake.sessionID} ${wake.inboxID}`;
|
|
76
|
+
/** Whether a message has waited in an idle session past `WAKE_MS`, and since a wake again, if one was sent; a stalled one is done. */
|
|
77
|
+
export function missed(wake, now) {
|
|
78
|
+
if (wake.running || wake.stalled !== undefined)
|
|
79
|
+
return false;
|
|
80
|
+
const since = Math.max(wake.idleAt ?? wake.at, wake.rewoken ?? 0);
|
|
81
|
+
return now - since >= WAKE_MS;
|
|
82
|
+
}
|
|
83
|
+
async function wakeAgain(ports, store, wake, now) {
|
|
84
|
+
try {
|
|
85
|
+
// Re-read: the message may have been delivered, or the session woken, since the scan.
|
|
86
|
+
const current = (await store.of(wake.sessionID)).find((one) => one.inboxID === wake.inboxID);
|
|
87
|
+
if (!current || !missed(current, now))
|
|
88
|
+
return;
|
|
89
|
+
if (current.rewoken !== undefined) {
|
|
90
|
+
ports.log(`courier wake: ${wake.sessionID} has not taken message ${wake.inboxID} since it was woken again; flagged stalled`);
|
|
91
|
+
return await store.put({ ...current, stalled: now });
|
|
92
|
+
}
|
|
93
|
+
// Noted before the send: the turn it starts may take the message, and drop the watch, before the send returns.
|
|
94
|
+
await store.put({ ...current, rewoken: now });
|
|
95
|
+
await ports.session.synthetic({
|
|
96
|
+
sessionID: wake.sessionID,
|
|
97
|
+
id: wake.inboxID,
|
|
98
|
+
text: wake.text,
|
|
99
|
+
...(wake.description !== undefined ? { description: wake.description } : {}),
|
|
100
|
+
metadata: { source: "courier", wake: "again" },
|
|
101
|
+
delivery: "steer",
|
|
102
|
+
}).catch(async (error) => {
|
|
103
|
+
await store.put(current);
|
|
104
|
+
throw error;
|
|
105
|
+
});
|
|
106
|
+
ports.log(`courier wake: ${wake.sessionID} left message ${wake.inboxID} waiting while idle; woken again`);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
ports.log(`courier wake: could not wake ${wake.sessionID} again for ${wake.inboxID}: ${String(error)}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** The sessions among `sessionIDs` flagged stalled, each with when its oldest stalled message was enqueued. */
|
|
113
|
+
export async function stalledAmong(ports, sessionIDs) {
|
|
114
|
+
const wanted = new Set(sessionIDs);
|
|
115
|
+
const stalled = new Map();
|
|
116
|
+
for (const wake of await wakeStore(ports.storage).all()) {
|
|
117
|
+
if (wake.stalled === undefined || !wanted.has(wake.sessionID))
|
|
118
|
+
continue;
|
|
119
|
+
stalled.set(wake.sessionID, Math.min(stalled.get(wake.sessionID) ?? wake.at, wake.at));
|
|
120
|
+
}
|
|
121
|
+
return stalled;
|
|
122
|
+
}
|