@code-yeongyu/senpi-agent-core 2026.9.30 → 2026.10.1-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 +49 -20
- package/dist/agent-loop.d.ts +27 -3
- package/dist/agent-loop.js +192 -56
- package/dist/agent.d.ts +21 -7
- package/dist/agent.js +38 -18
- package/dist/harness/execution/tools.d.ts +1 -1
- package/dist/harness/execution/tools.js +0 -2
- package/dist/harness/messages.js +1 -0
- package/dist/harness/pico3/bash.d.ts +10 -0
- package/dist/harness/pico3/bash.js +25 -0
- package/dist/harness/pico3/bounded.d.ts +21 -0
- package/dist/harness/pico3/bounded.js +95 -0
- package/dist/harness/pico3/chord.d.ts +41 -0
- package/dist/harness/pico3/chord.js +147 -0
- package/dist/harness/pico3/context.d.ts +16 -0
- package/dist/harness/pico3/context.js +88 -0
- package/dist/harness/pico3/harness.d.ts +198 -0
- package/dist/harness/pico3/harness.js +645 -0
- package/dist/harness/pico3/hooks.d.ts +10 -0
- package/dist/harness/pico3/hooks.js +33 -0
- package/dist/harness/pico3/index.d.ts +19 -0
- package/dist/harness/pico3/index.js +15 -0
- package/dist/harness/pico3/jsonl.d.ts +58 -0
- package/dist/harness/pico3/jsonl.js +322 -0
- package/dist/harness/pico3/kinds/collapse.d.ts +49 -0
- package/dist/harness/pico3/kinds/collapse.js +191 -0
- package/dist/harness/pico3/kinds/entries.d.ts +78 -0
- package/dist/harness/pico3/kinds/entries.js +13 -0
- package/dist/harness/pico3/kinds/frames.d.ts +9 -0
- package/dist/harness/pico3/kinds/frames.js +75 -0
- package/dist/harness/pico3/kinds/generation.d.ts +94 -0
- package/dist/harness/pico3/kinds/generation.js +504 -0
- package/dist/harness/pico3/kinds/job.d.ts +44 -0
- package/dist/harness/pico3/kinds/job.js +143 -0
- package/dist/harness/pico3/kinds/plugin.d.ts +15 -0
- package/dist/harness/pico3/kinds/plugin.js +35 -0
- package/dist/harness/pico3/kinds/post-tools.d.ts +22 -0
- package/dist/harness/pico3/kinds/post-tools.js +145 -0
- package/dist/harness/pico3/kinds/task-api.d.ts +3 -0
- package/dist/harness/pico3/kinds/task-api.js +45 -0
- package/dist/harness/pico3/kinds/tool.d.ts +43 -0
- package/dist/harness/pico3/kinds/tool.js +373 -0
- package/dist/harness/pico3/legacy-tracker.d.ts +11 -0
- package/dist/harness/pico3/legacy-tracker.js +39 -0
- package/dist/harness/pico3/membrane.d.ts +23 -0
- package/dist/harness/pico3/membrane.js +140 -0
- package/dist/harness/pico3/memory.d.ts +53 -0
- package/dist/harness/pico3/memory.js +265 -0
- package/dist/harness/pico3/scheduler.d.ts +49 -0
- package/dist/harness/pico3/scheduler.js +437 -0
- package/dist/harness/pico3/session.d.ts +264 -0
- package/dist/harness/pico3/session.js +1328 -0
- package/dist/harness/pico3/system.d.ts +126 -0
- package/dist/harness/pico3/system.js +244 -0
- package/dist/harness/pico3/types.d.ts +973 -0
- package/dist/harness/pico3/types.js +94 -0
- package/dist/harness/pico3/view.d.ts +34 -0
- package/dist/harness/pico3/view.js +404 -0
- package/dist/harness/runtime/drive/tool-placement.js +2 -27
- package/dist/harness/telemetry.d.ts +36 -36
- package/dist/harness/tools/image.js +1 -1
- package/dist/proxy.d.ts +2 -2
- package/dist/types.d.ts +111 -34
- package/package.json +9 -5
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { withAbortSignal } from "@earendil-works/chord/context";
|
|
2
|
+
import { InvocationToken, TaskContractFault, } from "./types.js";
|
|
3
|
+
function handlerFor(kind, phase) {
|
|
4
|
+
const h = phase === undefined ? kind.initial : kind.phases[phase];
|
|
5
|
+
if (typeof h !== "function")
|
|
6
|
+
throw new TaskContractFault(kind.name, `no handler for phase ${phase ?? "initial"}`);
|
|
7
|
+
return h;
|
|
8
|
+
}
|
|
9
|
+
function validateStep(kind, step) {
|
|
10
|
+
if (typeof step !== "object" || step === null)
|
|
11
|
+
throw new TaskContractFault(kind.name, "handler returned a non-object step");
|
|
12
|
+
if ("done" in step && typeof step.done === "function")
|
|
13
|
+
return step;
|
|
14
|
+
if ("next" in step) {
|
|
15
|
+
const n = step.next;
|
|
16
|
+
if (typeof n === "function")
|
|
17
|
+
return step;
|
|
18
|
+
if (typeof n === "object" && n !== null && typeof n.phase === "string") {
|
|
19
|
+
if (!(n.phase in kind.phases))
|
|
20
|
+
throw new TaskContractFault(kind.name, `transition to unknown phase ${n.phase}`);
|
|
21
|
+
return step;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
throw new TaskContractFault(kind.name, "handler returned an invalid step");
|
|
25
|
+
}
|
|
26
|
+
function validateCompletion(kind, c) {
|
|
27
|
+
if (typeof c === "object" && c !== null) {
|
|
28
|
+
const s = c.status;
|
|
29
|
+
if (s === "completed" && "result" in c)
|
|
30
|
+
return c;
|
|
31
|
+
if (s === "failed" && "failure" in c)
|
|
32
|
+
return c;
|
|
33
|
+
}
|
|
34
|
+
throw new TaskContractFault(kind.name, "closure returned an invalid completion");
|
|
35
|
+
}
|
|
36
|
+
function validateCheckpoint(kind, cp) {
|
|
37
|
+
if (typeof cp !== "object" || cp === null || typeof cp.phase !== "string")
|
|
38
|
+
throw new TaskContractFault(kind.name, "invalid checkpoint");
|
|
39
|
+
const phase = cp.phase;
|
|
40
|
+
if (!(phase in kind.phases))
|
|
41
|
+
throw new TaskContractFault(kind.name, `checkpoint names unknown phase ${phase}`);
|
|
42
|
+
if (kind.inflight?.includes(phase))
|
|
43
|
+
throw new TaskContractFault(kind.name, `transition into in-flight phase ${phase}; write it with rt.commit before the effect instead`);
|
|
44
|
+
JSON.stringify(cp);
|
|
45
|
+
return cp;
|
|
46
|
+
}
|
|
47
|
+
export class Scheduler {
|
|
48
|
+
constructor(deps) {
|
|
49
|
+
this.enabled = false;
|
|
50
|
+
this.holds = 0;
|
|
51
|
+
this.dirty = false;
|
|
52
|
+
this.draining = false;
|
|
53
|
+
this.invocations = new Map();
|
|
54
|
+
this.taskWaiters = new Map();
|
|
55
|
+
this.inputWaiters = new Map();
|
|
56
|
+
this.idleWaiters = new Set();
|
|
57
|
+
this.deps = deps;
|
|
58
|
+
deps.session.listeners.add((r) => {
|
|
59
|
+
for (const t of r.changes.tasks) {
|
|
60
|
+
const inv = this.invocations.get(t.id);
|
|
61
|
+
if (t.abort === true && inv?.mode === "run")
|
|
62
|
+
inv.controller.abort();
|
|
63
|
+
}
|
|
64
|
+
for (const task of r.changes.tasks)
|
|
65
|
+
if (task.status === "terminal")
|
|
66
|
+
this.taskWaiters.get(task.id)?.forEach((waiter) => {
|
|
67
|
+
waiter(task);
|
|
68
|
+
});
|
|
69
|
+
for (const input of r.changes.inputs)
|
|
70
|
+
if (input.status === "done" || input.status === "unanswered")
|
|
71
|
+
this.inputWaiters.get(input.id)?.forEach((waiter) => {
|
|
72
|
+
waiter(input);
|
|
73
|
+
});
|
|
74
|
+
if (r.changes.tasks.length > 0)
|
|
75
|
+
this.kick();
|
|
76
|
+
this.checkIdle();
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
resume() {
|
|
80
|
+
this.enabled = true;
|
|
81
|
+
this.kick();
|
|
82
|
+
}
|
|
83
|
+
stop() {
|
|
84
|
+
this.enabled = false;
|
|
85
|
+
}
|
|
86
|
+
hold() {
|
|
87
|
+
this.holds++;
|
|
88
|
+
let released = false;
|
|
89
|
+
return () => {
|
|
90
|
+
if (released)
|
|
91
|
+
return;
|
|
92
|
+
released = true;
|
|
93
|
+
this.holds--;
|
|
94
|
+
if (this.holds === 0)
|
|
95
|
+
this.kick();
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
kick() {
|
|
99
|
+
if (!this.enabled)
|
|
100
|
+
return;
|
|
101
|
+
this.dirty = true;
|
|
102
|
+
if (this.holds === 0 && !this.draining)
|
|
103
|
+
this.drain().catch((e) => this.deps.onReport(e));
|
|
104
|
+
}
|
|
105
|
+
async drain() {
|
|
106
|
+
this.draining = true;
|
|
107
|
+
try {
|
|
108
|
+
while (this.dirty && this.enabled && this.holds === 0) {
|
|
109
|
+
this.dirty = false;
|
|
110
|
+
const reserved = await this.reserveEligible();
|
|
111
|
+
if (!this.enabled || this.holds > 0) {
|
|
112
|
+
this.dirty = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
for (const { task, mode } of reserved)
|
|
116
|
+
this.dispatch(task, mode);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
this.draining = false;
|
|
121
|
+
this.checkIdle();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** On the line: reserve every eligible task. */
|
|
125
|
+
async reserveEligible() {
|
|
126
|
+
const { session } = this.deps;
|
|
127
|
+
const out = [];
|
|
128
|
+
await session.commit({ type: "kernel" }, async (tx) => {
|
|
129
|
+
for (const task of [...session.liveTasks.values()]) {
|
|
130
|
+
if (!this.deps.kinds.has(task.kind))
|
|
131
|
+
continue;
|
|
132
|
+
const inv = this.invocations.get(task.id);
|
|
133
|
+
if (task.abort === true) {
|
|
134
|
+
if (inv?.mode === "abort")
|
|
135
|
+
continue;
|
|
136
|
+
if (inv?.mode === "run")
|
|
137
|
+
continue; // winding down; dispatched after it returns
|
|
138
|
+
const running = task.status === "pending" ? { ...task, status: "running" } : task;
|
|
139
|
+
if (running !== task)
|
|
140
|
+
tx.setTask(running);
|
|
141
|
+
out.push({ task: running, mode: "abort" });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (inv !== undefined)
|
|
145
|
+
continue;
|
|
146
|
+
if (task.status === "pending") {
|
|
147
|
+
let ready = true;
|
|
148
|
+
for (const dep of task.after) {
|
|
149
|
+
const d = await tx.task(dep);
|
|
150
|
+
if (d === undefined || d.status !== "terminal") {
|
|
151
|
+
ready = false;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (!ready)
|
|
156
|
+
continue;
|
|
157
|
+
const running = { ...task, status: "running" };
|
|
158
|
+
tx.setTask(running);
|
|
159
|
+
out.push({ task: running, mode: "run" });
|
|
160
|
+
}
|
|
161
|
+
else if (task.status === "running") {
|
|
162
|
+
out.push({ task, mode: "run" }); // found at open: dispatch into its current phase
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}, this.deps.ctx);
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
168
|
+
dispatch(task, mode) {
|
|
169
|
+
const { session } = this.deps;
|
|
170
|
+
const kind = this.deps.kinds.get(task.kind);
|
|
171
|
+
if (kind === undefined) {
|
|
172
|
+
this.deps.onReport(new Error(`unknown kind ${task.kind} for task ${task.id}`));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const ctx = withAbortSignal(controller.signal, this.deps.ctx);
|
|
177
|
+
const done = (async () => {
|
|
178
|
+
try {
|
|
179
|
+
if (mode === "run") {
|
|
180
|
+
const closure = await this.runPhases(task, kind, ctx);
|
|
181
|
+
if (closure === undefined || controller.signal.aborted)
|
|
182
|
+
return;
|
|
183
|
+
const lease = this.lease(task, kind, mode, ctx);
|
|
184
|
+
try {
|
|
185
|
+
await session.commit(lease.invoker, async (tx, lineCtx, control) => {
|
|
186
|
+
const current = session.liveTasks.get(task.id);
|
|
187
|
+
if (current === undefined || current.abort === true)
|
|
188
|
+
return;
|
|
189
|
+
const completion = validateCompletion(kind, await closure(tx, current, lineCtx));
|
|
190
|
+
const patched = (await tx.task(task.id)) ?? current;
|
|
191
|
+
control.setTask({ ...patched, status: "terminal", outcome: completion });
|
|
192
|
+
}, ctx, { closing: true });
|
|
193
|
+
}
|
|
194
|
+
finally {
|
|
195
|
+
lease.token.revoke();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const handlerLease = this.lease(task, kind, mode, ctx);
|
|
200
|
+
let closure;
|
|
201
|
+
try {
|
|
202
|
+
closure = await kind.abort(task, handlerLease.runtime, ctx);
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
handlerLease.token.revoke();
|
|
206
|
+
}
|
|
207
|
+
const closureLease = this.lease(task, kind, mode, ctx);
|
|
208
|
+
try {
|
|
209
|
+
await session.commit(closureLease.invoker, async (tx, lineCtx, control) => {
|
|
210
|
+
const current = session.liveTasks.get(task.id);
|
|
211
|
+
if (current === undefined)
|
|
212
|
+
return;
|
|
213
|
+
const result = (await closure(tx, current, lineCtx));
|
|
214
|
+
JSON.stringify(result);
|
|
215
|
+
const patched = (await tx.task(task.id)) ?? current;
|
|
216
|
+
control.setTask({ ...patched, status: "terminal", outcome: { status: "aborted", result } });
|
|
217
|
+
}, ctx, { closing: true });
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
closureLease.token.revoke();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
if (controller.signal.aborted)
|
|
226
|
+
return;
|
|
227
|
+
// Contract fault or unexpected throw: the task ends `faulted`, never as a declared failure.
|
|
228
|
+
this.deps.onReport(error);
|
|
229
|
+
await session
|
|
230
|
+
.commit({ type: "kernel" }, async (tx) => {
|
|
231
|
+
const current = session.liveTasks.get(task.id);
|
|
232
|
+
if (current === undefined)
|
|
233
|
+
return;
|
|
234
|
+
const outcome = {
|
|
235
|
+
status: "faulted",
|
|
236
|
+
error: error instanceof TaskContractFault ? error.message : `${kind.name}: ${String(error)}`,
|
|
237
|
+
};
|
|
238
|
+
tx.setTask({ ...current, status: "terminal", outcome });
|
|
239
|
+
// A faulted turn task: its inputs would otherwise stay placed forever; the turn state is stale.
|
|
240
|
+
if (kind.turn) {
|
|
241
|
+
const inputs = current.input.inputs;
|
|
242
|
+
if (Array.isArray(inputs))
|
|
243
|
+
await tx.resolveInputs(inputs.filter((i) => typeof i === "number"), { status: "unanswered", reason: "failed", detail: outcome.error });
|
|
244
|
+
tx.sticky(current.conversationId).turn = { tools: [] };
|
|
245
|
+
}
|
|
246
|
+
}, this.deps.ctx, { docs: [{ doc: "sticky", conversationId: task.conversationId }] })
|
|
247
|
+
.catch((e) => this.deps.onReport(e));
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
this.invocations.delete(task.id);
|
|
251
|
+
const now = session.liveTasks.get(task.id);
|
|
252
|
+
if (now === undefined)
|
|
253
|
+
await session.retire(task, this.deps.ctx).catch((e) => this.deps.onReport(e));
|
|
254
|
+
this.kick();
|
|
255
|
+
}
|
|
256
|
+
})();
|
|
257
|
+
this.invocations.set(task.id, { task, mode, controller, done });
|
|
258
|
+
}
|
|
259
|
+
lease(task, kind, mode, ctx) {
|
|
260
|
+
const token = new InvocationToken(task.id, mode);
|
|
261
|
+
const invoker = {
|
|
262
|
+
type: "task",
|
|
263
|
+
token,
|
|
264
|
+
id: task.id,
|
|
265
|
+
conversationId: task.conversationId,
|
|
266
|
+
kind,
|
|
267
|
+
core: task.kind.startsWith("pi.") &&
|
|
268
|
+
["pi.generation", "pi.tool", "pi.post_tools", "pi.collapse"].includes(task.kind),
|
|
269
|
+
mode,
|
|
270
|
+
};
|
|
271
|
+
return { token, invoker, runtime: this.deps.runtime(task, invoker, ctx) };
|
|
272
|
+
}
|
|
273
|
+
/** The phase loop. Each handler gets a capability lease revoked when that handler's promise returns. */
|
|
274
|
+
async runPhases(task, kind, ctx) {
|
|
275
|
+
const { session } = this.deps;
|
|
276
|
+
let current = task;
|
|
277
|
+
for (;;) {
|
|
278
|
+
const cp = current.checkpoint;
|
|
279
|
+
const handler = handlerFor(kind, cp?.phase);
|
|
280
|
+
const handlerLease = this.lease(task, kind, "run", ctx);
|
|
281
|
+
let step;
|
|
282
|
+
try {
|
|
283
|
+
step = validateStep(kind, await handler(current, handlerLease.runtime, ctx));
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
handlerLease.token.revoke();
|
|
287
|
+
}
|
|
288
|
+
if ("done" in step)
|
|
289
|
+
return step.done;
|
|
290
|
+
if (ctx.abortSignal?.aborted)
|
|
291
|
+
return undefined;
|
|
292
|
+
const transitionLease = this.lease(task, kind, "run", ctx);
|
|
293
|
+
let outcome;
|
|
294
|
+
try {
|
|
295
|
+
outcome = await session.commit(transitionLease.invoker, async (tx, lineCtx, control) => {
|
|
296
|
+
const live = session.liveTasks.get(task.id);
|
|
297
|
+
if (live === undefined || live.abort === true)
|
|
298
|
+
return "discard";
|
|
299
|
+
const next = typeof step.next === "function" ? await step.next(tx, live, lineCtx) : step.next;
|
|
300
|
+
if (next === "retry")
|
|
301
|
+
return "retry";
|
|
302
|
+
if (typeof next === "object" && next !== null && "status" in next) {
|
|
303
|
+
const patched = (await tx.task(task.id)) ?? live;
|
|
304
|
+
control.setTask({ ...patched, status: "terminal", outcome: validateCompletion(kind, next) });
|
|
305
|
+
return "terminal";
|
|
306
|
+
}
|
|
307
|
+
const checkpoint = validateCheckpoint(kind, next);
|
|
308
|
+
tx.checkpoint(checkpoint);
|
|
309
|
+
current = { ...live, checkpoint };
|
|
310
|
+
return "advanced";
|
|
311
|
+
}, ctx);
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
transitionLease.token.revoke();
|
|
315
|
+
}
|
|
316
|
+
if (outcome.value === "retry") {
|
|
317
|
+
current = session.liveTasks.get(task.id) ?? current;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (outcome.value !== "advanced")
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** Mark, revoke, signal, join. The scheduler then reserves the fresh abort on its next drain. */
|
|
325
|
+
async abortTask(id, ctx) {
|
|
326
|
+
const { session } = this.deps;
|
|
327
|
+
const r = await session.commit({ type: "kernel" }, async (tx) => {
|
|
328
|
+
const task = await tx.task(id);
|
|
329
|
+
if (task === undefined)
|
|
330
|
+
throw new Error(`task ${id} not found`);
|
|
331
|
+
if (task.status === "terminal")
|
|
332
|
+
return "terminal";
|
|
333
|
+
if (task.abort !== true)
|
|
334
|
+
tx.setTask({ ...task, abort: true });
|
|
335
|
+
return "marked";
|
|
336
|
+
}, ctx);
|
|
337
|
+
if (r.value === "terminal")
|
|
338
|
+
return "terminal";
|
|
339
|
+
const inv = this.invocations.get(id);
|
|
340
|
+
if (inv?.mode === "run") {
|
|
341
|
+
inv.controller.abort();
|
|
342
|
+
await inv.done;
|
|
343
|
+
}
|
|
344
|
+
this.kick();
|
|
345
|
+
return "marked";
|
|
346
|
+
}
|
|
347
|
+
// --- waiters: registered on the line, atomically with the state read -------------------
|
|
348
|
+
waitForTask(id, ctx) {
|
|
349
|
+
return this.waiter(ctx, this.taskWaiters, id, (lineCtx) => {
|
|
350
|
+
const live = this.deps.session.liveTasks.get(id);
|
|
351
|
+
return live === undefined ? this.deps.session.storage.task(id, lineCtx) : Promise.resolve(undefined);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
waitForInput(id, ctx) {
|
|
355
|
+
return this.waiter(ctx, this.inputWaiters, id, async (lineCtx) => {
|
|
356
|
+
const i = await this.deps.session.storage.input(id, lineCtx);
|
|
357
|
+
return i !== undefined && (i.status === "done" || i.status === "unanswered") ? i : undefined;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/** Read state and install the waiter in one line operation; the wait itself happens off-line. */
|
|
361
|
+
async waiter(ctx, table, id, terminal) {
|
|
362
|
+
ctx.abortSignal?.throwIfAborted();
|
|
363
|
+
const r = await this.deps.session.onLine(async (lineCtx) => {
|
|
364
|
+
const now = await terminal(lineCtx);
|
|
365
|
+
if (now !== undefined)
|
|
366
|
+
return { now };
|
|
367
|
+
return {
|
|
368
|
+
pending: new Promise((resolve, reject) => {
|
|
369
|
+
const set = table.get(id) ?? table.set(id, new Set()).get(id);
|
|
370
|
+
const w = (v) => {
|
|
371
|
+
set.delete(w);
|
|
372
|
+
ctx.abortSignal?.removeEventListener("abort", onAbort);
|
|
373
|
+
resolve(v);
|
|
374
|
+
};
|
|
375
|
+
const onAbort = () => {
|
|
376
|
+
set.delete(w);
|
|
377
|
+
reject(ctx.abortSignal?.reason ?? new Error("aborted"));
|
|
378
|
+
};
|
|
379
|
+
set.add(w);
|
|
380
|
+
ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
381
|
+
}),
|
|
382
|
+
};
|
|
383
|
+
}, ctx);
|
|
384
|
+
return "now" in r ? r.now : r.pending;
|
|
385
|
+
}
|
|
386
|
+
async waitForIdle(conversationId, ctx) {
|
|
387
|
+
ctx.abortSignal?.throwIfAborted();
|
|
388
|
+
const r = await this.deps.session.onLine(() => {
|
|
389
|
+
if (this.isIdle(conversationId))
|
|
390
|
+
return {};
|
|
391
|
+
return {
|
|
392
|
+
pending: new Promise((resolve, reject) => {
|
|
393
|
+
const w = {
|
|
394
|
+
conversationId,
|
|
395
|
+
resolve: () => {
|
|
396
|
+
this.idleWaiters.delete(w);
|
|
397
|
+
ctx.abortSignal?.removeEventListener("abort", onAbort);
|
|
398
|
+
resolve();
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
const onAbort = () => {
|
|
402
|
+
this.idleWaiters.delete(w);
|
|
403
|
+
reject(ctx.abortSignal?.reason ?? new Error("aborted"));
|
|
404
|
+
};
|
|
405
|
+
this.idleWaiters.add(w);
|
|
406
|
+
ctx.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
407
|
+
}),
|
|
408
|
+
};
|
|
409
|
+
}, ctx);
|
|
410
|
+
return r.pending;
|
|
411
|
+
}
|
|
412
|
+
isIdle(conversationId) {
|
|
413
|
+
for (const t of this.deps.session.liveTasks.values())
|
|
414
|
+
if ((conversationId === undefined || t.conversationId === conversationId) && !t.background)
|
|
415
|
+
return false;
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
checkIdle() {
|
|
419
|
+
for (const w of [...this.idleWaiters])
|
|
420
|
+
if (this.isIdle(w.conversationId) && !this.draining)
|
|
421
|
+
w.resolve();
|
|
422
|
+
}
|
|
423
|
+
/** Signal every invocation and wait for them; nothing is written. */
|
|
424
|
+
async joinAll() {
|
|
425
|
+
this.enabled = false;
|
|
426
|
+
for (const inv of this.invocations.values())
|
|
427
|
+
inv.controller.abort();
|
|
428
|
+
await Promise.allSettled([...this.invocations.values()].map((invocation) => invocation.done));
|
|
429
|
+
}
|
|
430
|
+
quiescent() {
|
|
431
|
+
return this.invocations.size === 0;
|
|
432
|
+
}
|
|
433
|
+
get liveInvocations() {
|
|
434
|
+
return this.invocations.size;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
//# sourceMappingURL=scheduler.js.map
|