opencode-courier 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/dist/index.js +6 -3
- 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 +5 -3
- package/dist/tree/index.d.ts +1 -1
- package/dist/tree/panel.d.ts +7 -2
- package/dist/tree/panel.js +19 -16
- package/dist/tree/ports.d.ts +7 -0
- package/dist/tree/roster.d.ts +5 -0
- package/dist/tree/roster.js +5 -3
- package/dist/tree/rpc-handlers.d.ts +45 -5
- package/dist/tree/rpc-handlers.js +30 -1
- package/dist/tree/rpc.d.ts +79 -19
- package/dist/tree/rpc.js +20 -8
- 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 +8 -16
- package/dist/tree/text.js +4 -1
- package/dist/tree/tools.js +2 -1
- package/dist/tree/tui.d.ts +11 -4
- package/dist/tui.js +567 -174
- package/package.json +1 -1
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,6 +1,6 @@
|
|
|
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";
|
|
@@ -88,8 +88,8 @@ export async function noteInterrupted(ports, event) {
|
|
|
88
88
|
return true;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
91
|
+
* Forgets what a deleted session leaves (report state, groups) and, unless isolated or a parent on the roster, its
|
|
92
|
+
* entry. Returns the sessions forgotten.
|
|
93
93
|
*/
|
|
94
94
|
export async function noteDeleted(ports, event) {
|
|
95
95
|
const { sessionID } = event.data;
|
|
@@ -99,6 +99,8 @@ export async function noteDeleted(ports, event) {
|
|
|
99
99
|
const entry = parentID === undefined ? undefined : await children.entry(parentID, sessionID);
|
|
100
100
|
const member = entry && (await outIn(ports, entry));
|
|
101
101
|
await forgetDeleted(ports, sessionID, forgotten, member, event.created);
|
|
102
|
+
if (parentID !== undefined && entry && !entry.isolated && started.length === 0)
|
|
103
|
+
await forgetChild(ports.storage, parentID, sessionID);
|
|
102
104
|
return forgotten;
|
|
103
105
|
}
|
|
104
106
|
/** A `Waits` asking `suppliers` one after another, cheapest first: the first that waits answers. */
|
package/dist/tree/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ 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";
|
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.
|
|
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,8 @@
|
|
|
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
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
|
|
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.
|
|
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
|
-
/**
|
|
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 {
|
|
73
|
-
const rows = await Promise.all(
|
|
74
|
-
return { rows, ...(
|
|
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
|
}
|
package/dist/tree/ports.d.ts
CHANGED
|
@@ -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;
|
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
|
}
|
|
@@ -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",
|
|
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
|
+
}
|