opencode-courier 0.10.1 → 0.10.3
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 +25 -13
- 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.js +3 -5
- 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 +5 -5
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { millis } from "../platform/json.js";
|
|
2
|
+
import { setBounded } from "../platform/bounded.js";
|
|
3
|
+
import { busy, endedAt, turnEnd } from "./domain.js";
|
|
4
|
+
/** How many sessions' answers each session port remembers. */
|
|
5
|
+
const ANSWERS_MAX = 500;
|
|
6
|
+
/** How long after its stamp an answer of rest is read again: an item enqueued then may be delivered, stamping nothing until its step. */
|
|
7
|
+
const FRESH_MS = 10_000;
|
|
8
|
+
/** How long an answer of rest is kept: an item waiting since a lost wake is delivered on the wake sent again, which stamps nothing. */
|
|
9
|
+
const REST_MS = 30_000;
|
|
10
|
+
const RUNNING = { running: true };
|
|
11
|
+
const rest = (at) => (at === undefined ? { running: false } : { running: false, endedAt: at });
|
|
12
|
+
/** Each session port's last answer per session, which holds until the next stamp, but for those `turnOf` drops or keeps `until`. */
|
|
13
|
+
const answers = new WeakMap();
|
|
14
|
+
/**
|
|
15
|
+
* Whether a session runs and, at rest, when its last turn ended: by its times when they show rest, else by its transcript
|
|
16
|
+
* (#324, #326), kept until the next stamp, a rest for `REST_MS` and not within `FRESH_MS` of it; one unread or empty runs.
|
|
17
|
+
*/
|
|
18
|
+
export function turnOf(session, sessionID, time, now = Date.now()) {
|
|
19
|
+
return resolve(session, sessionID, time, now, () => session.context({ sessionID }), true);
|
|
20
|
+
}
|
|
21
|
+
/** `turnOf` for a caller that reads the transcript itself: it answers from these `messages`, read just now, and keeps the answer. */
|
|
22
|
+
export function turnIn(session, sessionID, time, messages, now = Date.now()) {
|
|
23
|
+
return resolve(session, sessionID, time, now, () => messages, false);
|
|
24
|
+
}
|
|
25
|
+
function resolve(session, sessionID, time, now, load, cached) {
|
|
26
|
+
if (!busy(time))
|
|
27
|
+
return Promise.resolve(rest(millis(time.idle)));
|
|
28
|
+
const known = answers.get(session) ?? new Map();
|
|
29
|
+
answers.set(session, known);
|
|
30
|
+
const stamp = `${millis(time.updated)} ${millis(time.idle)}`;
|
|
31
|
+
const found = known.get(sessionID);
|
|
32
|
+
if (cached && found?.stamp === stamp && now < found.until)
|
|
33
|
+
return found.turn;
|
|
34
|
+
// Only its own answer: a newer read of the session may have taken its place meanwhile.
|
|
35
|
+
const forget = () => {
|
|
36
|
+
if (known.get(sessionID) === answer)
|
|
37
|
+
known.delete(sessionID);
|
|
38
|
+
};
|
|
39
|
+
const turn = load().then((messages) => {
|
|
40
|
+
const end = turnEnd(messages);
|
|
41
|
+
if (!messages.length)
|
|
42
|
+
forget();
|
|
43
|
+
if (!end)
|
|
44
|
+
return RUNNING;
|
|
45
|
+
if (now - millis(time.updated) < FRESH_MS)
|
|
46
|
+
forget();
|
|
47
|
+
answer.until = now + REST_MS;
|
|
48
|
+
return rest(endedAt(end, time));
|
|
49
|
+
}, () => {
|
|
50
|
+
forget();
|
|
51
|
+
return RUNNING;
|
|
52
|
+
});
|
|
53
|
+
const answer = { stamp, turn, until: Number.POSITIVE_INFINITY };
|
|
54
|
+
known.delete(sessionID);
|
|
55
|
+
setBounded(known, sessionID, answer, ANSWERS_MAX);
|
|
56
|
+
return turn;
|
|
57
|
+
}
|
|
58
|
+
export const runs = (session, sessionID, time, now) => turnOf(session, sessionID, time, now).then((turn) => turn.running);
|
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);
|