opencode-courier 0.10.0 → 0.10.2
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 +11 -8
- package/dist/memory/domain.d.ts +4 -1
- package/dist/memory/events.js +7 -5
- package/dist/memory/record.js +33 -14
- package/dist/scheduling/later.d.ts +2 -2
- package/dist/tree/domain.d.ts +29 -5
- package/dist/tree/domain.js +23 -4
- package/dist/tree/events.d.ts +7 -2
- package/dist/tree/events.js +19 -5
- package/dist/tree/group.js +2 -2
- package/dist/tree/index.d.ts +3 -2
- package/dist/tree/index.js +3 -2
- package/dist/tree/limits.js +10 -7
- package/dist/tree/panel.d.ts +1 -1
- package/dist/tree/panel.js +4 -6
- package/dist/tree/ports.d.ts +1 -1
- package/dist/tree/running.d.ts +18 -0
- package/dist/tree/running.js +58 -0
- package/dist/tree/send.js +7 -2
- package/dist/tree/status.d.ts +5 -0
- package/dist/tree/status.js +1 -0
- package/dist/tree/subtree.d.ts +6 -1
- package/dist/tree/subtree.js +6 -3
- package/dist/tree/summary.js +3 -4
- package/dist/tree/tui.d.ts +4 -1
- package/dist/tree/wake.d.ts +21 -3
- package/dist/tree/wake.js +51 -18
- package/dist/tui.js +47 -47
- package/package.json +7 -7
package/dist/tree/send.js
CHANGED
|
@@ -2,6 +2,7 @@ import { envelope, isStatus, STATUSES } from "../kernel/protocol.js";
|
|
|
2
2
|
import { groupStanding, holdReport, membershipFor, unholdForBlocked } from "./group.js";
|
|
3
3
|
import { childStore, groupStore } from "./store.js";
|
|
4
4
|
import { reportBody } from "./text.js";
|
|
5
|
+
import { noteSent } from "./wake.js";
|
|
5
6
|
/**
|
|
6
7
|
* Drops a message into another session's inbox; OpenCode wakes that session if it is idle. From a spawned session
|
|
7
8
|
* to the one that started it, it is its report or its progress (`noteReport`); a group member's final report is held (`hold`).
|
|
@@ -17,13 +18,17 @@ export async function send(ports, from, input) {
|
|
|
17
18
|
const held = noted?.member && status && status !== "blocked" ? await hold(ports, noted.member, { at: noted.at, status, message: input.message, ...(input.artifacts ? { artifacts: input.artifacts } : {}) }) : undefined;
|
|
18
19
|
if (held)
|
|
19
20
|
return { status, report: true, held };
|
|
21
|
+
const text = envelope(from, reportBody(input.message, input.artifacts), status ? { status } : {});
|
|
22
|
+
const description = `Message from ${from}`;
|
|
20
23
|
const delivered = await ports.session.synthetic({
|
|
21
24
|
sessionID: input.sessionID,
|
|
22
|
-
text
|
|
23
|
-
description
|
|
25
|
+
text,
|
|
26
|
+
description,
|
|
24
27
|
metadata: { source: "courier", from, ...(status ? { status } : {}) },
|
|
25
28
|
delivery: input.queue ? "queue" : "steer",
|
|
26
29
|
});
|
|
30
|
+
// Watched now, not when OpenCode's enqueued event is handled: a spawn in the same step must count the child it wakes.
|
|
31
|
+
await noteSent(ports, { sessionID: input.sessionID, inboxID: delivered.id, text, description }).catch(() => undefined);
|
|
27
32
|
// A blocked report, once delivered, is the member's latest word: an earlier report held for it would be stale.
|
|
28
33
|
if (noted?.member && status === "blocked")
|
|
29
34
|
await unholdForBlocked(groupStore(ports.storage), noted.member).catch(() => undefined);
|
package/dist/tree/status.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export declare function status(ports: CourierPorts, input: StatusInput): Promise
|
|
|
15
15
|
outcome: "failed" | "interrupted" | "succeeded" | undefined;
|
|
16
16
|
updated: number;
|
|
17
17
|
idle: number | undefined;
|
|
18
|
+
archived: number | undefined;
|
|
18
19
|
lastText: string | undefined;
|
|
19
20
|
pending: readonly import("./ports.js").Pending[] | undefined;
|
|
20
21
|
}>>;
|
|
@@ -38,6 +39,7 @@ export declare function listChildren(ports: CourierPorts, parentID: string): Pro
|
|
|
38
39
|
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
39
40
|
updated?: number | undefined;
|
|
40
41
|
idle?: number | undefined;
|
|
42
|
+
archived?: number | undefined;
|
|
41
43
|
lastText?: string | undefined;
|
|
42
44
|
pending?: readonly import("./ports.js").Pending[] | undefined;
|
|
43
45
|
} | {
|
|
@@ -51,6 +53,7 @@ export declare function listChildren(ports: CourierPorts, parentID: string): Pro
|
|
|
51
53
|
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
52
54
|
updated?: number | undefined;
|
|
53
55
|
idle?: number | undefined;
|
|
56
|
+
archived?: number | undefined;
|
|
54
57
|
lastText?: string | undefined;
|
|
55
58
|
pending?: readonly import("./ports.js").Pending[] | undefined;
|
|
56
59
|
} | {
|
|
@@ -88,6 +91,7 @@ export declare function described(ports: CourierPorts, entry: RosterEntry): Prom
|
|
|
88
91
|
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
89
92
|
updated?: number | undefined;
|
|
90
93
|
idle?: number | undefined;
|
|
94
|
+
archived?: number | undefined;
|
|
91
95
|
lastText?: string | undefined;
|
|
92
96
|
pending?: readonly import("./ports.js").Pending[] | undefined;
|
|
93
97
|
} | {
|
|
@@ -101,6 +105,7 @@ export declare function described(ports: CourierPorts, entry: RosterEntry): Prom
|
|
|
101
105
|
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
102
106
|
updated?: number | undefined;
|
|
103
107
|
idle?: number | undefined;
|
|
108
|
+
archived?: number | undefined;
|
|
104
109
|
lastText?: string | undefined;
|
|
105
110
|
pending?: readonly import("./ports.js").Pending[] | undefined;
|
|
106
111
|
} | {
|
package/dist/tree/status.js
CHANGED
package/dist/tree/subtree.d.ts
CHANGED
|
@@ -20,9 +20,12 @@ export declare function subtree(storage: RosterStorage, rootID: string, retentio
|
|
|
20
20
|
nodes: Below[];
|
|
21
21
|
}>;
|
|
22
22
|
type Info = Awaited<ReturnType<Plugin.Context["session"]["get"]>>;
|
|
23
|
-
/** A session of a tree as a read looks it up: OpenCode's info of it, if it still knows it, and what it waits on. */
|
|
23
|
+
/** A session of a tree as a read looks it up: OpenCode's info of it, if it still knows it, whether it runs, and what it waits on. */
|
|
24
24
|
export interface Looked extends Below {
|
|
25
25
|
readonly info: Info | undefined;
|
|
26
|
+
readonly running: boolean;
|
|
27
|
+
/** When its last turn ended, if it does not run and that is known. */
|
|
28
|
+
readonly endedAt?: number;
|
|
26
29
|
readonly pending: ReadonlyArray<Pending>;
|
|
27
30
|
readonly forms: ReadonlyArray<ShownForm>;
|
|
28
31
|
}
|
|
@@ -70,6 +73,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
|
|
|
70
73
|
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
71
74
|
updated?: number | undefined;
|
|
72
75
|
idle?: number | undefined;
|
|
76
|
+
archived?: number | undefined;
|
|
73
77
|
lastText?: string | undefined;
|
|
74
78
|
pending?: readonly Pending[] | undefined;
|
|
75
79
|
} | {
|
|
@@ -90,6 +94,7 @@ export declare function tree(ports: CourierPorts, rootID: string): Promise<{
|
|
|
90
94
|
outcome?: "failed" | "interrupted" | "succeeded" | undefined;
|
|
91
95
|
updated?: number | undefined;
|
|
92
96
|
idle?: number | undefined;
|
|
97
|
+
archived?: number | undefined;
|
|
93
98
|
lastText?: string | undefined;
|
|
94
99
|
pending?: readonly Pending[] | undefined;
|
|
95
100
|
} | {
|
package/dist/tree/subtree.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cut, isLive } from "./domain.js";
|
|
2
2
|
import { lastStatus, reportOf } from "./report.js";
|
|
3
3
|
import { isExpired } from "./roster.js";
|
|
4
|
+
import { turnOf } from "./running.js";
|
|
4
5
|
import { described } from "./status.js";
|
|
5
6
|
import { childStore } from "./store.js";
|
|
6
7
|
/**
|
|
@@ -42,8 +43,10 @@ export async function lookUp(ports, rootID) {
|
|
|
42
43
|
ports.pending(node.entry.sessionID).catch(() => []),
|
|
43
44
|
ports.forms(node.entry.sessionID).catch(() => []),
|
|
44
45
|
]);
|
|
46
|
+
const turn = info === undefined ? undefined : await turnOf(ports.session, node.entry.sessionID, info.time);
|
|
47
|
+
const running = turn?.running ?? false;
|
|
45
48
|
// A form outlives its session's turn only when its withdrawal was missed, as while no instance followed the events.
|
|
46
|
-
return { ...node, info,
|
|
49
|
+
return { ...node, info, running, ...(turn?.endedAt !== undefined ? { endedAt: turn.endedAt } : {}), pending, forms: running ? forms : [] };
|
|
47
50
|
}));
|
|
48
51
|
}
|
|
49
52
|
/**
|
|
@@ -54,7 +57,7 @@ export async function readTree(ports, rootID, max = ports.limits.maxTotal) {
|
|
|
54
57
|
const children = childStore(ports.storage);
|
|
55
58
|
const all = await Promise.all((await lookUp(ports, rootID)).map(async (node) => {
|
|
56
59
|
const report = await reportOf(children, node.entry.sessionID).catch(() => undefined);
|
|
57
|
-
return { ...node, report, live: isLive(node.info
|
|
60
|
+
return { ...node, report, live: isLive(node.info ? node.running : undefined, node.pending.length + node.forms.length, report) };
|
|
58
61
|
}));
|
|
59
62
|
const listing = cut(all.map((node) => ({ sessionID: node.entry.sessionID, parentID: node.entry.parentID, createdAt: node.entry.createdAt, live: node.live, node })), max);
|
|
60
63
|
return { kept: listing.kept.map(({ node }) => node), leftOut: listing.leftOut };
|
package/dist/tree/summary.js
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import { busy } from "./domain.js";
|
|
2
1
|
import { groupStandings } from "./group.js";
|
|
3
2
|
import { groupStore } from "./store.js";
|
|
4
3
|
import { lookUp } from "./subtree.js";
|
|
5
4
|
import { stalledAmong } from "./wake.js";
|
|
6
5
|
/**
|
|
7
6
|
* 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
|
|
7
|
+
* OpenCode no longer knows it. A message waiting in its inbox is no turn (`runs`); a stalled one reads stalled.
|
|
9
8
|
*/
|
|
10
|
-
function activity({ info, pending, forms, entry }, stalled) {
|
|
9
|
+
function activity({ info, running, pending, forms, entry }, stalled) {
|
|
11
10
|
if (!info)
|
|
12
11
|
return undefined;
|
|
13
12
|
if (pending.length || forms.length)
|
|
14
13
|
return "waiting";
|
|
15
14
|
if (stalled.has(entry.sessionID))
|
|
16
15
|
return "stalled";
|
|
17
|
-
return
|
|
16
|
+
return running ? "running" : "idle";
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
19
|
* Every session started under a session, counted by what it is doing (the session itself too, when it is stalled), and its
|
package/dist/tree/tui.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { type Accessor } from "solid-js";
|
|
|
4
4
|
import { type MascotState, type Token } from "./mascot.js";
|
|
5
5
|
import { type Forgotten, type Row, type Stopped, type Summary, type Wait } from "./rpc.js";
|
|
6
6
|
type Context = Plugin.Context;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The events after which a summary may read differently: a turn started or ended, a request or form, a session made, removed,
|
|
9
|
+
* archived or unarchived.
|
|
10
|
+
*/
|
|
8
11
|
export declare const CHANGES: Set<string>;
|
|
9
12
|
/** How long after the first of a burst of changes the entry asks the server again. */
|
|
10
13
|
export declare const SETTLE_MS = 250;
|
package/dist/tree/wake.d.ts
CHANGED
|
@@ -39,6 +39,16 @@ export interface Execution {
|
|
|
39
39
|
* Returns whether it is watched, which one sent with `resume: false` is not.
|
|
40
40
|
*/
|
|
41
41
|
export declare function watchWake(ports: WatchPorts, event: Enqueued): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Watches a message `courier_send` has just had enqueued, without waiting for its enqueued event, so the limits see a reported child woken
|
|
44
|
+
* for it at once; the event leaves this row be. Dropped again if the message is already stored, delivered before it was written.
|
|
45
|
+
*/
|
|
46
|
+
export declare function noteSent(ports: Pick<WatchPorts, "storage" | "session" | "now">, sent: {
|
|
47
|
+
readonly sessionID: string;
|
|
48
|
+
readonly inboxID: string;
|
|
49
|
+
readonly text: string;
|
|
50
|
+
readonly description?: string;
|
|
51
|
+
}): Promise<void>;
|
|
42
52
|
/** Stops watching a message once it is delivered or cancelled. */
|
|
43
53
|
export declare function settleWake(ports: Pick<WatchPorts, "storage">, event: InboxSettled): Promise<void>;
|
|
44
54
|
/** A turn started in a session: the messages waiting there are its to take. */
|
|
@@ -54,16 +64,24 @@ export declare function wakeEnded(ports: WatchPorts, event: Execution & {
|
|
|
54
64
|
}): Promise<void>;
|
|
55
65
|
/** Forgets the messages watched in a session's inbox: it was deleted, or stopped, which nothing is to wake again. */
|
|
56
66
|
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. */
|
|
67
|
+
/** The ports the scheduler's tick wakes sessions through: what every hub-v2 member's `later` ports have, whose session is its `ctx.session`. */
|
|
58
68
|
export type WakePorts = GroupPorts & {
|
|
69
|
+
readonly session: Pick<WatchPorts["session"], "get" | "context">;
|
|
59
70
|
readonly now: () => number;
|
|
60
71
|
};
|
|
61
72
|
/**
|
|
62
|
-
* Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`,
|
|
63
|
-
*
|
|
73
|
+
* Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, flags stalled one still
|
|
74
|
+
* waiting `WAKE_MS` later, and marks running one a turn it shows will take; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
|
|
64
75
|
*/
|
|
65
76
|
export declare function wakeMissed(ports: WakePorts, claimed: Set<string>): Promise<void>;
|
|
66
77
|
/** 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
78
|
export declare function missed(wake: Wake, now: number): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Whether a courier message sent with a wake waits undelivered in the session's inbox while it is idle, waiting or, unless
|
|
81
|
+
* `stalled` is false, stalled; one a running turn is to take is not counted, as `runs` says that turn runs.
|
|
82
|
+
*/
|
|
83
|
+
export declare function waitsInInbox(ports: Pick<WatchPorts, "storage">, sessionID: string, { stalled }?: {
|
|
84
|
+
stalled?: boolean | undefined;
|
|
85
|
+
}): Promise<boolean>;
|
|
68
86
|
/** The sessions among `sessionIDs` flagged stalled, each with when its oldest stalled message was enqueued. */
|
|
69
87
|
export declare function stalledAmong(ports: Pick<WatchPorts, "storage">, sessionIDs: ReadonlyArray<string>): Promise<Map<string, number>>;
|
package/dist/tree/wake.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { busy, endsIdle } from "./domain.js";
|
|
1
2
|
import { wakeStore } from "./store.js";
|
|
2
3
|
/** How long a courier message may wait in an idle session's inbox before its wake counts as missed. */
|
|
3
4
|
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
5
|
/**
|
|
7
6
|
* Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
|
|
8
7
|
* Returns whether it is watched, which one sent with `resume: false` is not.
|
|
@@ -16,19 +15,30 @@ export async function watchWake(ports, event) {
|
|
|
16
15
|
if (payload.metadata.resume === false)
|
|
17
16
|
return false;
|
|
18
17
|
const at = event.created ?? ports.now();
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
inboxID,
|
|
24
|
-
at,
|
|
25
|
-
text: payload.text,
|
|
26
|
-
...(typeof payload.description === "string" ? { description: payload.description } : {}),
|
|
27
|
-
running: !idle,
|
|
28
|
-
...(idle ? { idleAt: at } : {}),
|
|
29
|
-
});
|
|
18
|
+
// A row `noteSent` wrote already is kept: it may have moved on since, and is no later than this one.
|
|
19
|
+
if ((await wakeStore(ports.storage).of(sessionID)).some((wake) => wake.inboxID === inboxID))
|
|
20
|
+
return true;
|
|
21
|
+
await watch(ports, { sessionID, inboxID, at, text: payload.text, ...(typeof payload.description === "string" ? { description: payload.description } : {}) });
|
|
30
22
|
return true;
|
|
31
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Watches a message `courier_send` has just had enqueued, without waiting for its enqueued event, so the limits see a reported child woken
|
|
26
|
+
* for it at once; the event leaves this row be. Dropped again if the message is already stored, delivered before it was written.
|
|
27
|
+
*/
|
|
28
|
+
export async function noteSent(ports, sent) {
|
|
29
|
+
const store = wakeStore(ports.storage);
|
|
30
|
+
if ((await store.of(sent.sessionID)).some((wake) => wake.inboxID === sent.inboxID))
|
|
31
|
+
return;
|
|
32
|
+
await watch(ports, { ...sent, at: ports.now() });
|
|
33
|
+
const stored = await ports.session.context({ sessionID: sent.sessionID }).then((messages) => messages.some((message) => message.id === sent.inboxID), () => false);
|
|
34
|
+
if (stored)
|
|
35
|
+
await store.remove(sent.sessionID, sent.inboxID);
|
|
36
|
+
}
|
|
37
|
+
async function watch(ports, wake) {
|
|
38
|
+
// Read after the enqueue: a turn it started has stored messages since, and an unknown session is taken to run.
|
|
39
|
+
const idle = await ports.session.context({ sessionID: wake.sessionID }).then(endsIdle, () => false);
|
|
40
|
+
await wakeStore(ports.storage).put({ ...wake, running: !idle, ...(idle ? { idleAt: wake.at } : {}) });
|
|
41
|
+
}
|
|
32
42
|
/** Stops watching a message once it is delivered or cancelled. */
|
|
33
43
|
export async function settleWake(ports, event) {
|
|
34
44
|
await wakeStore(ports.storage).remove(event.data.sessionID, event.data.inboxID);
|
|
@@ -44,15 +54,18 @@ export async function wakeStarted(ports, event) {
|
|
|
44
54
|
* subscription; one still waiting is watched as waiting in an idle session from now.
|
|
45
55
|
*/
|
|
46
56
|
export async function wakeEnded(ports, event) {
|
|
47
|
-
if (event.data.reason === "shutdown")
|
|
48
|
-
return;
|
|
49
57
|
const { sessionID } = event.data;
|
|
50
58
|
const store = wakeStore(ports.storage);
|
|
51
59
|
const waiting = await store.of(sessionID);
|
|
52
60
|
if (!waiting.length)
|
|
53
61
|
return;
|
|
54
62
|
const at = event.created ?? ports.now();
|
|
55
|
-
const
|
|
63
|
+
const messages = await ports.session.context({ sessionID }).catch(() => []);
|
|
64
|
+
// A plain decline reports a shutdown after the aborted step that ends its turn; a real one between steps ends none. One during a
|
|
65
|
+
// step reads as ended too, until the turn the next boot resumes is seen to start or `wakeAgain` finds it running.
|
|
66
|
+
if (event.data.reason === "shutdown" && !endsIdle(messages))
|
|
67
|
+
return;
|
|
68
|
+
const stored = new Set(messages.map((message) => message.id));
|
|
56
69
|
await Promise.all(waiting.map((wake) => (stored.has(wake.inboxID) ? store.remove(sessionID, wake.inboxID) : store.put({ ...wake, running: false, idleAt: at }))));
|
|
57
70
|
}
|
|
58
71
|
/** Forgets the messages watched in a session's inbox: it was deleted, or stopped, which nothing is to wake again. */
|
|
@@ -61,8 +74,8 @@ export async function forgetWakes(ports, sessionID) {
|
|
|
61
74
|
await Promise.all((await store.of(sessionID)).map((wake) => store.remove(sessionID, wake.inboxID)));
|
|
62
75
|
}
|
|
63
76
|
/**
|
|
64
|
-
* Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`,
|
|
65
|
-
*
|
|
77
|
+
* Sends once more, under its inbox id, each courier message an idle session has left waiting for `WAKE_MS`, flags stalled one still
|
|
78
|
+
* waiting `WAKE_MS` later, and marks running one a turn it shows will take; run by the scheduler's owner, `claimed` keeping a tick alongside off it.
|
|
66
79
|
*/
|
|
67
80
|
export async function wakeMissed(ports, claimed) {
|
|
68
81
|
const store = wakeStore(ports.storage);
|
|
@@ -86,6 +99,11 @@ async function wakeAgain(ports, store, wake, now) {
|
|
|
86
99
|
const current = (await store.of(wake.sessionID)).find((one) => one.inboxID === wake.inboxID);
|
|
87
100
|
if (!current || !missed(current, now))
|
|
88
101
|
return;
|
|
102
|
+
// A shutdown during a step reads as an end, and the turn the next boot resumes may start unseen: that turn takes the message.
|
|
103
|
+
if (await runsNow(ports, wake.sessionID)) {
|
|
104
|
+
const { idleAt: _, ...running } = current;
|
|
105
|
+
return await store.put({ ...running, running: true });
|
|
106
|
+
}
|
|
89
107
|
if (current.rewoken !== undefined) {
|
|
90
108
|
ports.log(`courier wake: ${wake.sessionID} has not taken message ${wake.inboxID} since it was woken again; flagged stalled`);
|
|
91
109
|
return await store.put({ ...current, stalled: now });
|
|
@@ -109,6 +127,21 @@ async function wakeAgain(ports, store, wake, now) {
|
|
|
109
127
|
ports.log(`courier wake: could not wake ${wake.sessionID} again for ${wake.inboxID}: ${String(error)}`);
|
|
110
128
|
}
|
|
111
129
|
}
|
|
130
|
+
/** Whether a session shows a turn running, by its times and its transcript read now; one that cannot be read does not, and is woken again. */
|
|
131
|
+
async function runsNow(ports, sessionID) {
|
|
132
|
+
const info = await ports.session.get({ sessionID }).catch(() => undefined);
|
|
133
|
+
if (!info || !busy(info.time))
|
|
134
|
+
return false;
|
|
135
|
+
const messages = await ports.session.context({ sessionID }).catch(() => []);
|
|
136
|
+
return messages.length > 0 && !endsIdle(messages);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Whether a courier message sent with a wake waits undelivered in the session's inbox while it is idle, waiting or, unless
|
|
140
|
+
* `stalled` is false, stalled; one a running turn is to take is not counted, as `runs` says that turn runs.
|
|
141
|
+
*/
|
|
142
|
+
export async function waitsInInbox(ports, sessionID, { stalled = true } = {}) {
|
|
143
|
+
return (await wakeStore(ports.storage).of(sessionID)).some((wake) => !wake.running && (stalled || wake.stalled === undefined));
|
|
144
|
+
}
|
|
112
145
|
/** The sessions among `sessionIDs` flagged stalled, each with when its oldest stalled message was enqueued. */
|
|
113
146
|
export async function stalledAmong(ports, sessionIDs) {
|
|
114
147
|
const wanted = new Set(sessionIDs);
|
package/dist/tui.js
CHANGED
|
@@ -312,7 +312,7 @@ function addMemoryView(ctx) {
|
|
|
312
312
|
} : chosen(value)), requested]);
|
|
313
313
|
}
|
|
314
314
|
const showList = (read, current) => {
|
|
315
|
-
const
|
|
315
|
+
const open = openSession();
|
|
316
316
|
const now = Date.now();
|
|
317
317
|
return show({
|
|
318
318
|
title: `Memory (${read.records.length})`,
|
|
@@ -321,7 +321,7 @@ function addMemoryView(ctx) {
|
|
|
321
321
|
current
|
|
322
322
|
} : {},
|
|
323
323
|
options: read.records.map((record) => ({
|
|
324
|
-
title: recordTitle(record,
|
|
324
|
+
title: recordTitle(record, open),
|
|
325
325
|
value: record,
|
|
326
326
|
category: recordCategory(record, now),
|
|
327
327
|
description: recordDescription(record),
|
|
@@ -865,12 +865,12 @@ function addAsksInbox(ctx, follows) {
|
|
|
865
865
|
} = ask;
|
|
866
866
|
const answers = [];
|
|
867
867
|
for (const [index, prompt] of question.questions.entries()) {
|
|
868
|
-
const
|
|
868
|
+
const count = question.questions.length > 1 ? ` (${index + 1}/${question.questions.length})` : "";
|
|
869
869
|
const choices = prompt.options.map((option) => ({
|
|
870
870
|
...option,
|
|
871
871
|
value: option.label
|
|
872
872
|
}));
|
|
873
|
-
const picked = await choose(`${prompt.header}: ${prompt.question}${
|
|
873
|
+
const picked = await choose(`${prompt.header}: ${prompt.question}${count}`, choices, {
|
|
874
874
|
multiple: !!prompt.multiple,
|
|
875
875
|
own: true
|
|
876
876
|
});
|
|
@@ -1025,26 +1025,26 @@ function addAsksInbox(ctx, follows) {
|
|
|
1025
1025
|
const {
|
|
1026
1026
|
form
|
|
1027
1027
|
} = ask;
|
|
1028
|
-
const
|
|
1028
|
+
const answer = {};
|
|
1029
1029
|
for (const field of form.fields) {
|
|
1030
|
-
if (!active(field,
|
|
1030
|
+
if (!active(field, answer))
|
|
1031
1031
|
continue;
|
|
1032
1032
|
if ("hidden" in field && field.hidden) {
|
|
1033
1033
|
if ("default" in field && field.default !== undefined)
|
|
1034
|
-
|
|
1034
|
+
answer[field.key] = field.default;
|
|
1035
1035
|
continue;
|
|
1036
1036
|
}
|
|
1037
1037
|
const value = await answerField(form, field);
|
|
1038
1038
|
if (value === undefined)
|
|
1039
1039
|
return false;
|
|
1040
1040
|
if (value !== SKIP)
|
|
1041
|
-
|
|
1041
|
+
answer[field.key] = value;
|
|
1042
1042
|
}
|
|
1043
1043
|
try {
|
|
1044
1044
|
await ctx.client.session.form.reply({
|
|
1045
1045
|
sessionID: form.sessionID,
|
|
1046
1046
|
formID: form.id,
|
|
1047
|
-
answer
|
|
1047
|
+
answer
|
|
1048
1048
|
});
|
|
1049
1049
|
} catch (error) {
|
|
1050
1050
|
const tag = error?._tag;
|
|
@@ -1116,8 +1116,8 @@ function addAsksInbox(ctx, follows) {
|
|
|
1116
1116
|
try {
|
|
1117
1117
|
return await Promise.race([choosing.then((chosen) => ({
|
|
1118
1118
|
chosen
|
|
1119
|
-
})), newer.then((
|
|
1120
|
-
read
|
|
1119
|
+
})), newer.then((read) => ({
|
|
1120
|
+
read
|
|
1121
1121
|
}))]);
|
|
1122
1122
|
} finally {
|
|
1123
1123
|
listing = undefined;
|
|
@@ -1502,7 +1502,7 @@ var treeRpc = Rpc3.define({
|
|
|
1502
1502
|
});
|
|
1503
1503
|
|
|
1504
1504
|
// src/tree/tui.tsx
|
|
1505
|
-
var CHANGES = new Set(["session.created", "session.deleted", "session.status", "session.idle", "session.execution.started", "session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.inbox.delivered", "session.synthetic", "permission.asked", "permission.replied", "form.created", "form.replied", "form.cancelled"]);
|
|
1505
|
+
var CHANGES = new Set(["session.created", "session.deleted", "session.status", "session.idle", "session.execution.started", "session.execution.succeeded", "session.execution.failed", "session.execution.interrupted", "session.inbox.delivered", "session.synthetic", "session.archived", "session.unarchived", "permission.asked", "permission.replied", "form.created", "form.replied", "form.cancelled"]);
|
|
1506
1506
|
var PANEL_CHANGES = new Set([...CHANGES, "session.tool.called", "session.tool.success", "session.tool.failed", "session.usage.updated", "session.renamed"]);
|
|
1507
1507
|
var SETTLE_MS2 = 250;
|
|
1508
1508
|
var PANEL_SETTLE_MS = 1000;
|
|
@@ -1534,14 +1534,14 @@ function createMotion(enabled) {
|
|
|
1534
1534
|
const now = untrack(moving);
|
|
1535
1535
|
for (const one of now)
|
|
1536
1536
|
one.step(FRAME_MS * every);
|
|
1537
|
-
setFrame((
|
|
1537
|
+
setFrame((count) => count + every);
|
|
1538
1538
|
const left = now.filter((one) => !one.done());
|
|
1539
1539
|
if (left.length !== now.length)
|
|
1540
1540
|
setMoving(left);
|
|
1541
1541
|
}), FRAME_MS * every);
|
|
1542
1542
|
onCleanup(() => clearInterval(timer));
|
|
1543
1543
|
});
|
|
1544
|
-
const tween = (from, to,
|
|
1544
|
+
const tween = (from, to, duration, ease) => {
|
|
1545
1545
|
if (!enabled || from === to)
|
|
1546
1546
|
return {
|
|
1547
1547
|
value: () => to,
|
|
@@ -1551,11 +1551,11 @@ function createMotion(enabled) {
|
|
|
1551
1551
|
value: from
|
|
1552
1552
|
};
|
|
1553
1553
|
const timeline = new Timeline({
|
|
1554
|
-
duration
|
|
1554
|
+
duration,
|
|
1555
1555
|
autoplay: false
|
|
1556
1556
|
});
|
|
1557
1557
|
timeline.add(target, {
|
|
1558
|
-
duration
|
|
1558
|
+
duration,
|
|
1559
1559
|
ease,
|
|
1560
1560
|
value: to
|
|
1561
1561
|
});
|
|
@@ -1568,7 +1568,7 @@ function createMotion(enabled) {
|
|
|
1568
1568
|
},
|
|
1569
1569
|
done: () => {
|
|
1570
1570
|
frame();
|
|
1571
|
-
return elapsed >=
|
|
1571
|
+
return elapsed >= duration;
|
|
1572
1572
|
},
|
|
1573
1573
|
step: (ms) => {
|
|
1574
1574
|
elapsed += ms;
|
|
@@ -1620,14 +1620,14 @@ function createBarFill(motion, counts) {
|
|
|
1620
1620
|
const [easing, setEasing] = createSignal();
|
|
1621
1621
|
let last;
|
|
1622
1622
|
createEffect(() => {
|
|
1623
|
-
const
|
|
1623
|
+
const cells = target();
|
|
1624
1624
|
const shown = counts().started > 0;
|
|
1625
|
-
if (last &&
|
|
1625
|
+
if (last && cells !== last.cells) {
|
|
1626
1626
|
const from = untrack(() => easing()?.value()) ?? last.cells;
|
|
1627
|
-
setEasing(last.shown ? motion.tween(from,
|
|
1627
|
+
setEasing(last.shown ? motion.tween(from, cells, BAR_MS, "outQuad") : undefined);
|
|
1628
1628
|
}
|
|
1629
1629
|
last = {
|
|
1630
|
-
cells
|
|
1630
|
+
cells,
|
|
1631
1631
|
shown
|
|
1632
1632
|
};
|
|
1633
1633
|
});
|
|
@@ -1667,8 +1667,8 @@ function mascotState(rows, {
|
|
|
1667
1667
|
return rows.length && rows.every((row) => row.state === "gone") ? "gone" : "idle";
|
|
1668
1668
|
}
|
|
1669
1669
|
function countsLine(rows) {
|
|
1670
|
-
const
|
|
1671
|
-
return [
|
|
1670
|
+
const count = (state) => rows.filter((row) => row.state === state).length;
|
|
1671
|
+
return [count("waiting") ? `${count("waiting")} waiting on you` : "", count("stalled") ? `${count("stalled")} stalled` : "", count("running") ? `${count("running")} running` : "", count("idle") ? `${count("idle")} idle` : ""].filter(Boolean).join(" \xB7 ");
|
|
1672
1672
|
}
|
|
1673
1673
|
var GLYPH_ROWS = 12;
|
|
1674
1674
|
var GLYPH_COLUMNS = 54;
|
|
@@ -1694,12 +1694,12 @@ function summaryLine(summary, maxWidth = Infinity) {
|
|
|
1694
1694
|
}
|
|
1695
1695
|
function duration2(ms) {
|
|
1696
1696
|
const seconds = Math.max(0, Math.floor(ms / 1000));
|
|
1697
|
-
const
|
|
1697
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
1698
1698
|
if (seconds < 60)
|
|
1699
1699
|
return `${seconds}s`;
|
|
1700
1700
|
if (seconds < 3600)
|
|
1701
|
-
return `${Math.floor(seconds / 60)}m ${
|
|
1702
|
-
return `${Math.floor(seconds / 3600)}h ${
|
|
1701
|
+
return `${Math.floor(seconds / 60)}m ${pad(seconds % 60)}s`;
|
|
1702
|
+
return `${Math.floor(seconds / 3600)}h ${pad(Math.floor(seconds / 60) % 60)}m`;
|
|
1703
1703
|
}
|
|
1704
1704
|
function parts(rows, parentID) {
|
|
1705
1705
|
const children = rows.filter((row) => row.parentID === parentID);
|
|
@@ -1715,8 +1715,8 @@ function filledCells({
|
|
|
1715
1715
|
return started ? Math.round(reported / started * width) : 0;
|
|
1716
1716
|
}
|
|
1717
1717
|
function bar(counts, width = 10, filled = filledCells(counts, width)) {
|
|
1718
|
-
const
|
|
1719
|
-
return `[${"\u2588".repeat(
|
|
1718
|
+
const cells = Math.max(0, Math.min(width, Math.round(filled)));
|
|
1719
|
+
return `[${"\u2588".repeat(cells)}${"\u2591".repeat(width - cells)}] ${counts.reported}/${counts.started} reported`;
|
|
1720
1720
|
}
|
|
1721
1721
|
function waitLine(wait) {
|
|
1722
1722
|
if (wait.type === "permission")
|
|
@@ -1727,7 +1727,7 @@ function statusLine(row) {
|
|
|
1727
1727
|
return statusParts(row).map((part) => part.text).join(" \xB7 ");
|
|
1728
1728
|
}
|
|
1729
1729
|
function statusParts(row) {
|
|
1730
|
-
const
|
|
1730
|
+
const parts = [{
|
|
1731
1731
|
text: stateWord(row),
|
|
1732
1732
|
kind: "state"
|
|
1733
1733
|
}, {
|
|
@@ -1740,7 +1740,7 @@ function statusParts(row) {
|
|
|
1740
1740
|
text: row.isolated ? "isolated" : "",
|
|
1741
1741
|
kind: "plain"
|
|
1742
1742
|
}];
|
|
1743
|
-
return
|
|
1743
|
+
return parts.filter((part) => part.text);
|
|
1744
1744
|
}
|
|
1745
1745
|
var SPINNER = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1746
1746
|
function rowMark(row, theme, frame) {
|
|
@@ -1796,16 +1796,16 @@ function stateWord(row) {
|
|
|
1796
1796
|
return row.outcome;
|
|
1797
1797
|
return row.state;
|
|
1798
1798
|
}
|
|
1799
|
-
function reportWords(
|
|
1800
|
-
if (!
|
|
1799
|
+
function reportWords(report, stopped) {
|
|
1800
|
+
if (!report)
|
|
1801
1801
|
return "";
|
|
1802
|
-
if (
|
|
1802
|
+
if (report.state === "owed")
|
|
1803
1803
|
return "owes a report";
|
|
1804
|
-
if (
|
|
1804
|
+
if (report.state === "ended")
|
|
1805
1805
|
return stopped ? "" : "ended without a report";
|
|
1806
|
-
return ["reported",
|
|
1806
|
+
return ["reported", report.status].filter(Boolean).join(" ");
|
|
1807
1807
|
}
|
|
1808
|
-
var counted = (
|
|
1808
|
+
var counted = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
1809
1809
|
var CONTEXT_WARN = 0.8;
|
|
1810
1810
|
function activityParts(row, now) {
|
|
1811
1811
|
return [`ran ${duration2((row.idle ?? now) - row.created)}`, row.tool ? `last ${row.tool.name} ${duration2(now - row.tool.at)} ago` : "no tool call yet", row.context !== undefined ? `ctx ${Math.round(row.context * 100)}%` : "", row.cost !== undefined ? `$${row.cost.toFixed(2)}` : ""].filter(Boolean);
|
|
@@ -1835,7 +1835,7 @@ function courierOf(ctx) {
|
|
|
1835
1835
|
var REREAD_MS = 15000;
|
|
1836
1836
|
function changes(ctx, types, settle) {
|
|
1837
1837
|
const [changed, setChanged] = createSignal(0);
|
|
1838
|
-
const reread = setInterval(() => setChanged((
|
|
1838
|
+
const reread = setInterval(() => setChanged((count) => count + 1), REREAD_MS);
|
|
1839
1839
|
let settling;
|
|
1840
1840
|
const unlisten = ctx.data.listen(({
|
|
1841
1841
|
details
|
|
@@ -1844,7 +1844,7 @@ function changes(ctx, types, settle) {
|
|
|
1844
1844
|
return;
|
|
1845
1845
|
settling = setTimeout(() => {
|
|
1846
1846
|
settling = undefined;
|
|
1847
|
-
setChanged((
|
|
1847
|
+
setChanged((count) => count + 1);
|
|
1848
1848
|
}, settle);
|
|
1849
1849
|
});
|
|
1850
1850
|
return [changed, () => {
|
|
@@ -2487,9 +2487,9 @@ function TreePanel(props) {
|
|
|
2487
2487
|
get each() {
|
|
2488
2488
|
return statusParts(row);
|
|
2489
2489
|
},
|
|
2490
|
-
children: (part,
|
|
2490
|
+
children: (part, at) => [_$createComponent(Show, {
|
|
2491
2491
|
get when() {
|
|
2492
|
-
return
|
|
2492
|
+
return at();
|
|
2493
2493
|
},
|
|
2494
2494
|
get children() {
|
|
2495
2495
|
var _el$32 = _$createElement("text");
|
|
@@ -2527,7 +2527,7 @@ function TreePanel(props) {
|
|
|
2527
2527
|
get each() {
|
|
2528
2528
|
return line.split(" \xB7 ");
|
|
2529
2529
|
},
|
|
2530
|
-
children: (part,
|
|
2530
|
+
children: (part, at) => [_$memo(() => at() ? " \xB7 " : ""), _$memo(() => _$memo(() => part === `ctx ${Math.round(row.context * 100)}%`)() ? (() => {
|
|
2531
2531
|
var _el$37 = _$createElement("span");
|
|
2532
2532
|
_$insert(_el$37, part);
|
|
2533
2533
|
_$effect((_$p) => _$setProp(_el$37, "style", {
|
|
@@ -2664,7 +2664,7 @@ function addTreePanel(ctx, verbs = {}) {
|
|
|
2664
2664
|
return;
|
|
2665
2665
|
});
|
|
2666
2666
|
const follow = (from, sessionID) => {
|
|
2667
|
-
const
|
|
2667
|
+
const token = ++attempt;
|
|
2668
2668
|
const under = (tree) => tree?.rows.some((row) => row.sessionID === sessionID) === true;
|
|
2669
2669
|
Object.assign(from, {
|
|
2670
2670
|
at: undefined,
|
|
@@ -2675,7 +2675,7 @@ function addTreePanel(ctx, verbs = {}) {
|
|
|
2675
2675
|
(async () => {
|
|
2676
2676
|
const inside = await treeOf(from.root);
|
|
2677
2677
|
const own = under(inside) ? undefined : await treeOf(sessionID);
|
|
2678
|
-
if (
|
|
2678
|
+
if (token !== attempt || following !== from)
|
|
2679
2679
|
return;
|
|
2680
2680
|
if (under(inside))
|
|
2681
2681
|
show(from, sessionID, from.root, inside);
|
|
@@ -2769,10 +2769,10 @@ function addTreePanel(ctx, verbs = {}) {
|
|
|
2769
2769
|
});
|
|
2770
2770
|
const sessionID = target || await ctx.ui.dialog.select({
|
|
2771
2771
|
title: "Stop which session?",
|
|
2772
|
-
options: rows.map((
|
|
2773
|
-
title:
|
|
2774
|
-
value:
|
|
2775
|
-
description: `${
|
|
2772
|
+
options: rows.map((row) => ({
|
|
2773
|
+
title: row.title,
|
|
2774
|
+
value: row.sessionID,
|
|
2775
|
+
description: `${row.sessionID} \xB7 ${statusLine(row)}`
|
|
2776
2776
|
}))
|
|
2777
2777
|
});
|
|
2778
2778
|
if (!sessionID)
|