pi-roundtable 0.7.17 → 0.7.18
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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.7.18] - 2026-10-05
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Stop reading attachments as soon as they exceed the 25 MiB limit and cancel HTTP error bodies instead of leaving them open.
|
|
13
|
+
- Stop claiming or starting schedules when shutdown occurs during a database query or claim; already running turns still finish normally.
|
|
14
|
+
- Isolate keyless Jev tests from the developer's home, XDG configuration and environment.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Extract schedule precheck resolution, execution and cleanup tracking from the scheduler without changing its public contract.
|
|
19
|
+
- Release all five official packages in lockstep; MCP and sandbox include lifecycle fixes described in their changelogs.
|
|
20
|
+
|
|
8
21
|
## [0.7.17] - 2026-10-04
|
|
9
22
|
|
|
10
23
|
### Added
|
package/package.json
CHANGED
|
@@ -53,12 +53,13 @@ export async function fetchAttachments(
|
|
|
53
53
|
try {
|
|
54
54
|
const response = await fetchImpl(ref.url);
|
|
55
55
|
if (!response.ok) {
|
|
56
|
+
await response.body?.cancel();
|
|
56
57
|
fail(`download failed with HTTP ${response.status}`);
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
59
|
-
const data =
|
|
60
|
-
if (data
|
|
61
|
-
fail(`larger than 25 MB (${data
|
|
60
|
+
const data = await readAttachmentBody(response);
|
|
61
|
+
if (typeof data === "number") {
|
|
62
|
+
fail(`larger than 25 MB (${data} bytes)`);
|
|
62
63
|
continue;
|
|
63
64
|
}
|
|
64
65
|
const file = `${prefix}-${index}-${safeFileName(ref.name)}`;
|
|
@@ -81,3 +82,34 @@ export async function fetchAttachments(
|
|
|
81
82
|
}
|
|
82
83
|
return { files, failures };
|
|
83
84
|
}
|
|
85
|
+
|
|
86
|
+
/** Returns the bytes, or the observed size once the stream exceeds the limit. */
|
|
87
|
+
async function readAttachmentBody(
|
|
88
|
+
response: Response,
|
|
89
|
+
): Promise<Uint8Array | number> {
|
|
90
|
+
if (!response.body) return new Uint8Array();
|
|
91
|
+
const reader = response.body.getReader();
|
|
92
|
+
const chunks: Uint8Array[] = [];
|
|
93
|
+
let size = 0;
|
|
94
|
+
try {
|
|
95
|
+
for (;;) {
|
|
96
|
+
const { done, value } = await reader.read();
|
|
97
|
+
if (done) break;
|
|
98
|
+
size += value.byteLength;
|
|
99
|
+
if (size > MAX_ATTACHMENT_BYTES) {
|
|
100
|
+
await reader.cancel();
|
|
101
|
+
return size;
|
|
102
|
+
}
|
|
103
|
+
chunks.push(value);
|
|
104
|
+
}
|
|
105
|
+
} finally {
|
|
106
|
+
reader.releaseLock();
|
|
107
|
+
}
|
|
108
|
+
const data = new Uint8Array(size);
|
|
109
|
+
let offset = 0;
|
|
110
|
+
for (const chunk of chunks) {
|
|
111
|
+
data.set(chunk, offset);
|
|
112
|
+
offset += chunk.byteLength;
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { HoldCheck } from "../../holds.ts";
|
|
2
|
+
import type { Logger } from "../../log.ts";
|
|
3
|
+
import { timeZone, zonedStamp } from "../../time.ts";
|
|
4
|
+
import { type PrecheckTool, precheckScriptTools } from "./precheck-tools.ts";
|
|
5
|
+
import {
|
|
6
|
+
type Precheck,
|
|
7
|
+
type PrecheckOutcome,
|
|
8
|
+
type PrecheckRegistry,
|
|
9
|
+
runPrecheck,
|
|
10
|
+
SCRIPT_PRECHECK,
|
|
11
|
+
} from "./prechecks.ts";
|
|
12
|
+
import type { Schedule } from "./schedule-store.ts";
|
|
13
|
+
|
|
14
|
+
interface SchedulePrechecksOptions {
|
|
15
|
+
/** The host's prechecks and script runner; without them, a schedule that has one runs with that as its error. */
|
|
16
|
+
prechecks?: Pick<PrecheckRegistry, "get"> &
|
|
17
|
+
Partial<Pick<PrecheckRegistry, "scriptRunner">>;
|
|
18
|
+
/**
|
|
19
|
+
* The host's hold rules, read when a script saved before its tools were recorded runs: it runs
|
|
20
|
+
* only if it calls no held tool. Without them, such a script does not run.
|
|
21
|
+
*/
|
|
22
|
+
holds?: () => HoldCheck;
|
|
23
|
+
logger: Logger;
|
|
24
|
+
signal: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Runs schedule prechecks and tracks underlying runs that outlive a timeout. */
|
|
28
|
+
export class SchedulePrechecks {
|
|
29
|
+
readonly #options: SchedulePrechecksOptions;
|
|
30
|
+
readonly #pending = new Set<Promise<unknown>>();
|
|
31
|
+
|
|
32
|
+
constructor(options: SchedulePrechecksOptions) {
|
|
33
|
+
this.#options = options;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async run(
|
|
37
|
+
schedule: Schedule,
|
|
38
|
+
name: string,
|
|
39
|
+
firedAt: Date,
|
|
40
|
+
): Promise<PrecheckOutcome> {
|
|
41
|
+
const { logger } = this.#options;
|
|
42
|
+
let decision: PrecheckOutcome;
|
|
43
|
+
try {
|
|
44
|
+
const precheck = this.#resolve(schedule, firedAt);
|
|
45
|
+
decision =
|
|
46
|
+
typeof precheck === "string"
|
|
47
|
+
? { kind: "failed", error: precheck }
|
|
48
|
+
: await runPrecheck(
|
|
49
|
+
precheck,
|
|
50
|
+
{ schedule, firedAt },
|
|
51
|
+
{
|
|
52
|
+
signal: this.#options.signal,
|
|
53
|
+
settled: (run) => {
|
|
54
|
+
const tracked = run
|
|
55
|
+
.catch(() => undefined)
|
|
56
|
+
.finally(() => this.#pending.delete(tracked));
|
|
57
|
+
this.#pending.add(tracked);
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
decision = {
|
|
63
|
+
kind: "failed",
|
|
64
|
+
error: `the precheck could not be looked up: ${error instanceof Error ? error.message : String(error)}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// A warning, not an error: the woken turn carries the error already, and an error line would
|
|
68
|
+
// wake the ops agent's report turn for the same failure.
|
|
69
|
+
if (decision.kind === "failed")
|
|
70
|
+
logger.warn(
|
|
71
|
+
{ schedule: schedule.id, precheck: name, error: decision.error },
|
|
72
|
+
"precheck failed; the scheduled turn runs with the error",
|
|
73
|
+
);
|
|
74
|
+
else
|
|
75
|
+
logger.info(
|
|
76
|
+
{
|
|
77
|
+
schedule: schedule.id,
|
|
78
|
+
precheck: name,
|
|
79
|
+
wake: decision.kind === "wake",
|
|
80
|
+
},
|
|
81
|
+
"precheck decided",
|
|
82
|
+
);
|
|
83
|
+
return decision;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async drain(limitMs: number): Promise<void> {
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
88
|
+
const limit = new Promise<"late">((resolve) => {
|
|
89
|
+
timer = setTimeout(() => resolve("late"), limitMs);
|
|
90
|
+
});
|
|
91
|
+
const settled = Promise.allSettled([...this.#pending]);
|
|
92
|
+
try {
|
|
93
|
+
// pi-lens-ignore: no-single-promise-in-promise-methods — the race includes both all pending cleanup and the stop deadline
|
|
94
|
+
if ((await Promise.race([settled, limit])) === "late")
|
|
95
|
+
this.#options.logger.warn(
|
|
96
|
+
{ prechecks: this.#pending.size },
|
|
97
|
+
"prechecks still cleaning up when the scheduler stopped",
|
|
98
|
+
);
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The schedule's precheck: a registered one by name, or its script through the host's runner.
|
|
106
|
+
* A script never runs in this process; without a runner it fails, and the turn still runs.
|
|
107
|
+
*/
|
|
108
|
+
#resolve(schedule: Schedule, firedAt: Date): Precheck | string {
|
|
109
|
+
const { prechecks } = this.#options;
|
|
110
|
+
if (schedule.precheck)
|
|
111
|
+
return (
|
|
112
|
+
prechecks?.get(schedule.precheck) ??
|
|
113
|
+
"no precheck of that name is registered"
|
|
114
|
+
);
|
|
115
|
+
const script = schedule.precheckScript ?? "";
|
|
116
|
+
const runner = prechecks?.scriptRunner?.();
|
|
117
|
+
if (!runner)
|
|
118
|
+
return "this host has no precheck script runner, so the script did not run";
|
|
119
|
+
const tools = this.#tools(schedule, script, runner.toolName.bind(runner));
|
|
120
|
+
if (typeof tools === "string") return tools;
|
|
121
|
+
const zone = timeZone();
|
|
122
|
+
return {
|
|
123
|
+
name: SCRIPT_PRECHECK,
|
|
124
|
+
description: "the schedule's own precheck script",
|
|
125
|
+
...(runner.timeoutMs === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { timeoutMs: runner.timeoutMs }),
|
|
128
|
+
run: (context) =>
|
|
129
|
+
runner.run(script, {
|
|
130
|
+
...context,
|
|
131
|
+
timeZone: zone,
|
|
132
|
+
today: zonedStamp(firedAt).slice(0, 10),
|
|
133
|
+
tools: tools.map(({ server, tool }) => ({ server, tool })),
|
|
134
|
+
}),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The tools a script may call: those approved when it was saved, or, for a script saved before
|
|
140
|
+
* they were recorded, those it calls when none of them is held. Otherwise why it cannot run.
|
|
141
|
+
*/
|
|
142
|
+
#tools(
|
|
143
|
+
schedule: Schedule,
|
|
144
|
+
script: string,
|
|
145
|
+
toolName: (server: string, tool: string) => string,
|
|
146
|
+
): PrecheckTool[] | string {
|
|
147
|
+
if (schedule.precheckTools) return schedule.precheckTools;
|
|
148
|
+
const save =
|
|
149
|
+
"save it again with schedule_update, so the owner can approve the tools it calls";
|
|
150
|
+
const holds = this.#options.holds?.();
|
|
151
|
+
if (!holds)
|
|
152
|
+
return `this script was saved before its tools were recorded, and this host cannot check them; ${save}`;
|
|
153
|
+
let tools: PrecheckTool[];
|
|
154
|
+
try {
|
|
155
|
+
tools = precheckScriptTools(script, { toolName, holds });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return `this script was saved before its tools were recorded and cannot be checked now (${error instanceof Error ? error.message : String(error)}); fix it and ${save}`;
|
|
158
|
+
}
|
|
159
|
+
const held = tools.flatMap((t) =>
|
|
160
|
+
t.held ? [`${t.server}/${t.tool}`] : [],
|
|
161
|
+
);
|
|
162
|
+
if (held.length > 0)
|
|
163
|
+
return `this script was saved before its tools were recorded and calls tools that need the owner's approval (${held.join(", ")}), so it did not run; ${save}`;
|
|
164
|
+
return tools;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -2,17 +2,13 @@ import type { ScheduledOutcome } from "../../contract/channels.ts";
|
|
|
2
2
|
import type { HoldCheck } from "../../holds.ts";
|
|
3
3
|
import type { Logger } from "../../log.ts";
|
|
4
4
|
import type { ScheduleStore } from "../../services.ts";
|
|
5
|
-
import { timeZone, zonedStamp } from "../../time.ts";
|
|
6
|
-
import { type PrecheckTool, precheckScriptTools } from "./precheck-tools.ts";
|
|
7
5
|
import {
|
|
8
|
-
type Precheck,
|
|
9
6
|
type PrecheckFinding,
|
|
10
|
-
type PrecheckOutcome,
|
|
11
7
|
type PrecheckRegistry,
|
|
12
|
-
runPrecheck,
|
|
13
8
|
SCRIPT_PRECHECK,
|
|
14
9
|
} from "./prechecks.ts";
|
|
15
10
|
import { nextRun } from "./recurrence.ts";
|
|
11
|
+
import { SchedulePrechecks } from "./schedule-prechecks.ts";
|
|
16
12
|
import type { Schedule } from "./schedule-store.ts";
|
|
17
13
|
|
|
18
14
|
export type { ScheduledOutcome };
|
|
@@ -81,14 +77,19 @@ function precheckPrefix(finding: PrecheckFinding | undefined): string {
|
|
|
81
77
|
export class Scheduler {
|
|
82
78
|
readonly #options: SchedulerOptions;
|
|
83
79
|
readonly #running = new Set<Promise<void>>();
|
|
84
|
-
/** Prechecks in flight, including their cleanup after a timeout, which stop waits for. */
|
|
85
|
-
readonly #prechecks = new Set<Promise<unknown>>();
|
|
86
80
|
readonly #stopping = new AbortController();
|
|
81
|
+
readonly #prechecks: SchedulePrechecks;
|
|
87
82
|
#timer: ReturnType<typeof setInterval> | undefined;
|
|
88
83
|
#ticking = false;
|
|
89
84
|
|
|
90
85
|
constructor(options: SchedulerOptions) {
|
|
91
86
|
this.#options = options;
|
|
87
|
+
this.#prechecks = new SchedulePrechecks({
|
|
88
|
+
prechecks: options.prechecks,
|
|
89
|
+
holds: options.holds,
|
|
90
|
+
logger: options.logger,
|
|
91
|
+
signal: this.#stopping.signal,
|
|
92
|
+
});
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
start(): void {
|
|
@@ -107,20 +108,7 @@ export class Scheduler {
|
|
|
107
108
|
async stop(): Promise<void> {
|
|
108
109
|
clearInterval(this.#timer);
|
|
109
110
|
this.#stopping.abort();
|
|
110
|
-
|
|
111
|
-
const limit = new Promise<"late">((resolve) => {
|
|
112
|
-
timer = setTimeout(() => resolve("late"), PRECHECK_STOP_MS);
|
|
113
|
-
});
|
|
114
|
-
const settled = Promise.allSettled([...this.#prechecks]);
|
|
115
|
-
try {
|
|
116
|
-
if ((await Promise.race([settled, limit])) === "late")
|
|
117
|
-
this.#options.logger.warn(
|
|
118
|
-
{ prechecks: this.#prechecks.size },
|
|
119
|
-
"prechecks still cleaning up when the scheduler stopped",
|
|
120
|
-
);
|
|
121
|
-
} finally {
|
|
122
|
-
clearTimeout(timer);
|
|
123
|
-
}
|
|
111
|
+
await this.#prechecks.drain(PRECHECK_STOP_MS);
|
|
124
112
|
}
|
|
125
113
|
|
|
126
114
|
/** Claims every due schedule and starts its run; runs continue after tick resolves. */
|
|
@@ -131,12 +119,20 @@ export class Scheduler {
|
|
|
131
119
|
try {
|
|
132
120
|
const now = this.#options.now?.() ?? new Date();
|
|
133
121
|
for (const schedule of await store.due(now)) {
|
|
122
|
+
if (this.#stopping.signal.aborted) break;
|
|
134
123
|
const claimed = await store.claim(
|
|
135
124
|
schedule,
|
|
136
125
|
nextRun(schedule.recurrence, now),
|
|
137
126
|
now,
|
|
138
127
|
);
|
|
139
128
|
if (!claimed) continue;
|
|
129
|
+
if (this.#stopping.signal.aborted) {
|
|
130
|
+
await this.#record(
|
|
131
|
+
schedule,
|
|
132
|
+
"skipped: the host stopped before its run",
|
|
133
|
+
);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
140
136
|
if (now.getTime() - schedule.nextRun.getTime() > LATE_LIMIT_MS) {
|
|
141
137
|
logger.warn(
|
|
142
138
|
{ schedule: schedule.id, due: schedule.nextRun },
|
|
@@ -173,7 +169,7 @@ export class Scheduler {
|
|
|
173
169
|
schedule.precheck ??
|
|
174
170
|
(schedule.precheckScript ? SCRIPT_PRECHECK : undefined);
|
|
175
171
|
if (name) {
|
|
176
|
-
const decision = await this.#
|
|
172
|
+
const decision = await this.#prechecks.run(schedule, name, firedAt);
|
|
177
173
|
if (this.#stopping.signal.aborted) {
|
|
178
174
|
await this.#record(
|
|
179
175
|
schedule,
|
|
@@ -206,120 +202,6 @@ export class Scheduler {
|
|
|
206
202
|
);
|
|
207
203
|
}
|
|
208
204
|
|
|
209
|
-
/**
|
|
210
|
-
* The schedule's precheck: a registered one by name, or its script through the host's runner.
|
|
211
|
-
* A script never runs in this process; without a runner it fails, and the turn still runs.
|
|
212
|
-
*/
|
|
213
|
-
#resolve(schedule: Schedule, firedAt: Date): Precheck | string {
|
|
214
|
-
const { prechecks } = this.#options;
|
|
215
|
-
if (schedule.precheck)
|
|
216
|
-
return (
|
|
217
|
-
prechecks?.get(schedule.precheck) ??
|
|
218
|
-
"no precheck of that name is registered"
|
|
219
|
-
);
|
|
220
|
-
const script = schedule.precheckScript ?? "";
|
|
221
|
-
const runner = prechecks?.scriptRunner?.();
|
|
222
|
-
if (!runner)
|
|
223
|
-
return "this host has no precheck script runner, so the script did not run";
|
|
224
|
-
const tools = this.#tools(schedule, script, runner.toolName.bind(runner));
|
|
225
|
-
if (typeof tools === "string") return tools;
|
|
226
|
-
const zone = timeZone();
|
|
227
|
-
return {
|
|
228
|
-
name: SCRIPT_PRECHECK,
|
|
229
|
-
description: "the schedule's own precheck script",
|
|
230
|
-
...(runner.timeoutMs === undefined
|
|
231
|
-
? {}
|
|
232
|
-
: { timeoutMs: runner.timeoutMs }),
|
|
233
|
-
run: (context) =>
|
|
234
|
-
runner.run(script, {
|
|
235
|
-
...context,
|
|
236
|
-
timeZone: zone,
|
|
237
|
-
today: zonedStamp(firedAt).slice(0, 10),
|
|
238
|
-
tools: tools.map(({ server, tool }) => ({ server, tool })),
|
|
239
|
-
}),
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* The tools a script may call: those approved when it was saved, or, for a script saved before
|
|
245
|
-
* they were recorded, those it calls when none of them is held. Otherwise why it cannot run.
|
|
246
|
-
*/
|
|
247
|
-
#tools(
|
|
248
|
-
schedule: Schedule,
|
|
249
|
-
script: string,
|
|
250
|
-
toolName: (server: string, tool: string) => string,
|
|
251
|
-
): PrecheckTool[] | string {
|
|
252
|
-
if (schedule.precheckTools) return schedule.precheckTools;
|
|
253
|
-
const save =
|
|
254
|
-
"save it again with schedule_update, so the owner can approve the tools it calls";
|
|
255
|
-
const holds = this.#options.holds?.();
|
|
256
|
-
if (!holds)
|
|
257
|
-
return `this script was saved before its tools were recorded, and this host cannot check them; ${save}`;
|
|
258
|
-
let tools: PrecheckTool[];
|
|
259
|
-
try {
|
|
260
|
-
tools = precheckScriptTools(script, { toolName, holds });
|
|
261
|
-
} catch (error) {
|
|
262
|
-
return `this script was saved before its tools were recorded and cannot be checked now (${error instanceof Error ? error.message : String(error)}); fix it and ${save}`;
|
|
263
|
-
}
|
|
264
|
-
const held = tools.flatMap((t) =>
|
|
265
|
-
t.held ? [`${t.server}/${t.tool}`] : [],
|
|
266
|
-
);
|
|
267
|
-
if (held.length > 0)
|
|
268
|
-
return `this script was saved before its tools were recorded and calls tools that need the owner's approval (${held.join(", ")}), so it did not run; ${save}`;
|
|
269
|
-
return tools;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Runs the schedule's precheck; a missing one fails like a throw, so the turn still runs. */
|
|
273
|
-
async #precheck(
|
|
274
|
-
schedule: Schedule,
|
|
275
|
-
name: string,
|
|
276
|
-
firedAt: Date,
|
|
277
|
-
): Promise<PrecheckOutcome> {
|
|
278
|
-
const { logger } = this.#options;
|
|
279
|
-
let decision: PrecheckOutcome;
|
|
280
|
-
try {
|
|
281
|
-
const precheck = this.#resolve(schedule, firedAt);
|
|
282
|
-
decision =
|
|
283
|
-
typeof precheck === "string"
|
|
284
|
-
? { kind: "failed", error: precheck }
|
|
285
|
-
: await runPrecheck(
|
|
286
|
-
precheck,
|
|
287
|
-
{ schedule, firedAt },
|
|
288
|
-
{
|
|
289
|
-
signal: this.#stopping.signal,
|
|
290
|
-
settled: (run) => {
|
|
291
|
-
const tracked = run
|
|
292
|
-
.catch(() => undefined)
|
|
293
|
-
.finally(() => this.#prechecks.delete(tracked));
|
|
294
|
-
this.#prechecks.add(tracked);
|
|
295
|
-
},
|
|
296
|
-
},
|
|
297
|
-
);
|
|
298
|
-
} catch (error) {
|
|
299
|
-
decision = {
|
|
300
|
-
kind: "failed",
|
|
301
|
-
error: `the precheck could not be looked up: ${error instanceof Error ? error.message : String(error)}`,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
// A warning, not an error: the woken turn carries the error already, and an error line would
|
|
305
|
-
// wake the ops agent's report turn for the same failure.
|
|
306
|
-
if (decision.kind === "failed")
|
|
307
|
-
logger.warn(
|
|
308
|
-
{ schedule: schedule.id, precheck: name, error: decision.error },
|
|
309
|
-
"precheck failed; the scheduled turn runs with the error",
|
|
310
|
-
);
|
|
311
|
-
else
|
|
312
|
-
logger.info(
|
|
313
|
-
{
|
|
314
|
-
schedule: schedule.id,
|
|
315
|
-
precheck: name,
|
|
316
|
-
wake: decision.kind === "wake",
|
|
317
|
-
},
|
|
318
|
-
"precheck decided",
|
|
319
|
-
);
|
|
320
|
-
return decision;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
205
|
/** Records a run the precheck skipped and posts its note; a note that cannot be posted is logged. */
|
|
324
206
|
async #skip(schedule: Schedule, note: string | undefined): Promise<void> {
|
|
325
207
|
const { notify, logger } = this.#options;
|