@cotal-ai/runtime 0.67.0 → 0.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fork.d.ts.map +1 -1
- package/dist/fork.js +4 -6
- package/dist/fork.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mesh-handler.d.ts +22 -55
- package/dist/mesh-handler.d.ts.map +1 -1
- package/dist/mesh-handler.js +63 -151
- package/dist/mesh-handler.js.map +1 -1
- package/dist/migrate.js +1 -1
- package/dist/migrate.js.map +1 -1
- package/dist/pause-tokens.d.ts +13 -0
- package/dist/pause-tokens.d.ts.map +1 -0
- package/dist/pause-tokens.js +54 -0
- package/dist/pause-tokens.js.map +1 -0
- package/dist/resolve-checkpoint.d.ts +0 -28
- package/dist/resolve-checkpoint.d.ts.map +1 -1
- package/dist/resolve-checkpoint.js +1 -85
- package/dist/resolve-checkpoint.js.map +1 -1
- package/dist/run-command.d.ts.map +1 -1
- package/dist/run-command.js +5 -4
- package/dist/run-command.js.map +1 -1
- package/dist/run-host.d.ts.map +1 -1
- package/dist/run-host.js +2 -2
- package/dist/run-host.js.map +1 -1
- package/dist/run-scope-authority.d.ts.map +1 -1
- package/dist/run-scope-authority.js +1 -28
- package/dist/run-scope-authority.js.map +1 -1
- package/package.json +4 -4
package/dist/mesh-handler.js
CHANGED
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
import { createHash } from "node:crypto";
|
|
20
20
|
import { isAbsolute } from "node:path";
|
|
21
21
|
import { realpathSync } from "node:fs";
|
|
22
|
-
import { mintCheckpoint, heartbeatCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointAnswer, readCheckpointStatus, readCheckpointSpec, reconcileCheckpointSchedule, handleCheckpointFire, eptStreamName, eptSubject, chatStream, waitConsumerName, waitConsumerConfig, isConcreteChannel, assertSafePattern, assertValidChannel, presenceBucket, principalKey, liveKvEntries, IncompleteKvScan, openMembersRegistry, openChannelRegistry, writeChannelConfig, readMember, commitMember, tombstoneMember, StaleMembershipWrite, runNoticeId, writeRunNotice, actionContext, invokeCommand, replyRefusedBeforeEffect, readGoalResult, readGoalStatus, resolveService, listRunNotices, listRunMigrations, markRunNoticeConsumed, listRunNoticesForRun, readRunRecord, writeRunStatus, EpEnvelopeError, assertAdmittedPublish, assertAdmittedSubscribe, assertNotRevoked, } from "@cotal-ai/core";
|
|
22
|
+
import { mintCheckpoint, heartbeatCheckpoint, resumeCheckpoint, readCheckpointSettle, readCheckpointAnswer, readCheckpointStatus, readCheckpointSpec, reconcileCheckpointSchedule, handleCheckpointFire, eptStreamName, eptSubject, chatStream, waitConsumerName, waitConsumerConfig, isConcreteChannel, assertSafePattern, assertValidChannel, presenceBucket, principalKey, liveKvEntries, IncompleteKvScan, openMembersRegistry, openChannelRegistry, writeChannelConfig, readMember, commitMember, tombstoneMember, StaleMembershipWrite, runNoticeId, writeRunNotice, actionContext, invokeCommand, invokeRepairingSplit, replyRefusedBeforeEffect, readGoalResult, readGoalStatus, resolveService, listRunNotices, listRunMigrations, markRunNoticeConsumed, listRunNoticesForRun, readRunRecord, writeRunStatus, EpEnvelopeError, assertAdmittedPublish, assertAdmittedSubscribe, assertNotRevoked, } from "@cotal-ai/core";
|
|
23
23
|
import { renderRunContext } from "./run-context.js";
|
|
24
24
|
import { migrationSeats } from "./migrate.js";
|
|
25
25
|
import { Kvm } from "@nats-io/kv";
|
|
26
|
-
import { parseDuration, Cancelled, EffectError, Journal, EffectRefused, askSchemaShape, conformsToAskSchema,
|
|
26
|
+
import { parseDuration, Cancelled, EffectError, Journal, EffectRefused, askSchemaShape, conformsToAskSchema, journalEntryKeyString, stepKeyString, } from "@cotal-ai/lang";
|
|
27
27
|
import { loopLag, servedDespiteStarvation } from "./host-starvation.js";
|
|
28
|
+
import { askAttemptToken, derivedToken, pauseTokens } from "./pause-tokens.js";
|
|
28
29
|
/**
|
|
29
30
|
* A checkpoint resumed with no answer to read.
|
|
30
31
|
*
|
|
@@ -113,54 +114,23 @@ export class MeshHandler {
|
|
|
113
114
|
return this.managerService;
|
|
114
115
|
}
|
|
115
116
|
/**
|
|
116
|
-
* One manager call, with a SPEC 13.2 bind refusal REPAIRED rather than raised
|
|
117
|
+
* One manager call, with a SPEC 13.2 bind refusal REPAIRED rather than raised
|
|
118
|
+
* ({@link invokeRepairingSplit}).
|
|
117
119
|
*
|
|
118
|
-
* A run resolves the manager on the class rail
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* attempt and not a second: the marker together with `not-executed` is the responder's own
|
|
127
|
-
* statement that no effect of the command exists, which is what {@link replyRefusedBeforeEffect}
|
|
128
|
-
* checks, and it is the same licence core's `Endpoint.invokeService` re-issues on. The stale
|
|
129
|
-
* class handle is dropped first, so the re-issue re-describes rather than rebinding the
|
|
130
|
-
* incarnation that was just refused.
|
|
131
|
-
*
|
|
132
|
-
* BOUNDED, because a re-issue draws the same queue again. The describe and the invoke stay two
|
|
133
|
-
* independent trips, so a space of m managers still splits (m-1)/m of the time and the repair
|
|
134
|
-
* converges geometrically rather than deterministically; after {@link BIND_SPLIT_REISSUES} of
|
|
135
|
-
* them the refusal surfaces unchanged, still stating that the command did not run. What removes
|
|
136
|
-
* the residual is addressing one instance, and the run's caller holds no instance-rail grant for
|
|
137
|
-
* a command its program did not place (SPEC 13.9, `run-driver-grants.ts`), so that is a wider
|
|
138
|
-
* change than this one.
|
|
139
|
-
*
|
|
140
|
-
* A PINNED handle is never repaired. It addresses one instance by name, so a refusal from it is
|
|
141
|
-
* that incarnation answering about itself, and re-resolving onto the class rail would reinstate
|
|
142
|
-
* the anycast fallback #1616 removed.
|
|
120
|
+
* A run resolves the manager on the class rail, so in a space with more than one manager its
|
|
121
|
+
* invoke routinely reaches a member other than the one that answered the describe, and that
|
|
122
|
+
* member refuses before dispatching. Raised as the effect's own failure, that refusal ends the
|
|
123
|
+
* run and consumes the run id and its journal (#1638), although it says the command did not run.
|
|
124
|
+
* The class memo is dropped before each re-describe, so later calls do not start from the bind
|
|
125
|
+
* that was just refused. After the shared bound the refusal surfaces unchanged; what removes that
|
|
126
|
+
* residual is addressing one instance, and the run's caller holds no instance-rail grant for a
|
|
127
|
+
* command its program did not place (SPEC 13.9, `run-driver-grants.ts`).
|
|
143
128
|
*/
|
|
144
129
|
async invokeManager(service, command, args, opts) {
|
|
145
|
-
|
|
146
|
-
for (let reissues = 0;; reissues += 1) {
|
|
147
|
-
const reply = await invokeCommand(this.nc, this.binding.space, handle, command, args, opts);
|
|
148
|
-
if (reply.reply.ok !== false || !replyRefusedBeforeEffect(reply.reply.error))
|
|
149
|
-
return reply;
|
|
150
|
-
if (handle.pinnedInstanceId !== undefined || reissues === BIND_SPLIT_REISSUES)
|
|
151
|
-
return reply;
|
|
130
|
+
return invokeRepairingSplit(this.nc, this.binding.space, service, command, args, opts, () => {
|
|
152
131
|
this.managerService = undefined;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
// The repair could not be attempted. The REFUSAL is what surfaces, not the resolve failure:
|
|
158
|
-
// every caller of this method already reads a refused reply as "the manager declined", and
|
|
159
|
-
// this one states that nothing ran, which is the fact a describe timeout raised in its
|
|
160
|
-
// place would lose.
|
|
161
|
-
return reply;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
132
|
+
return this.manager();
|
|
133
|
+
});
|
|
164
134
|
}
|
|
165
135
|
/**
|
|
166
136
|
* #1616 item 5 — PHASE A, RESOLVE ON THE HOST THAT WILL LAUNCH. A directory is a fact about one
|
|
@@ -411,7 +381,7 @@ export class MeshHandler {
|
|
|
411
381
|
await new Promise((r) => setTimeout(r, WAIT_POLL_MS).unref());
|
|
412
382
|
}
|
|
413
383
|
}
|
|
414
|
-
if (rows.some((p) => p.card
|
|
384
|
+
if (rows.some((p) => p.card.name === holder.name && p.lifecycleUid === holder.uid))
|
|
415
385
|
throw new EffectError("L4008", "worktree", `spawn(${persona}) would put a second agent into the worktree "${worktree}" while ${holder.name}#${holder.uid} is live in it; two agents MUST NOT share a worktree concurrently (L3022/L4008)`);
|
|
416
386
|
const now = this.worktreeHolders.get(worktree);
|
|
417
387
|
if (now !== undefined && now !== holder)
|
|
@@ -460,8 +430,7 @@ export class MeshHandler {
|
|
|
460
430
|
for (const e of owed) {
|
|
461
431
|
if (e.requestId === undefined)
|
|
462
432
|
continue;
|
|
463
|
-
|
|
464
|
-
await this.cancelTimer({ endpoint: this.binding.endpoint, token: holdRequestId(e.requestId) });
|
|
433
|
+
await this.endPause(e);
|
|
465
434
|
if (e.kind === "spawn") {
|
|
466
435
|
await this.dischargeSpawn(e);
|
|
467
436
|
continue;
|
|
@@ -480,52 +449,29 @@ export class MeshHandler {
|
|
|
480
449
|
if (typeof x?.name === "string" && typeof x?.uid === "string" && typeof x?.goalId === "string")
|
|
481
450
|
this.turnGoals.get(`${x.name}#${x.uid}`)?.delete(x.goalId);
|
|
482
451
|
}
|
|
483
|
-
if (e.kind === "waitUntil") {
|
|
484
|
-
// A `waitUntil` arms ONE cadence pause per observation, each under a derived token, so the
|
|
485
|
-
// sweep releases the one that is actually open: the observation the entry is on. The
|
|
486
|
-
// earlier ones already settled (that is how the wait got here) and `cancelTimer` tolerates
|
|
487
|
-
// a claim that loses its own race, so releasing the current index is both necessary and
|
|
488
|
-
// sufficient. Attempt 0 never arms anything, which is why the index is the observation
|
|
489
|
-
// COUNT rather than the count minus one.
|
|
490
|
-
const attempt = (e.observations ?? []).length;
|
|
491
|
-
if (attempt > 0)
|
|
492
|
-
await this.cancelTimer({
|
|
493
|
-
endpoint: this.binding.endpoint,
|
|
494
|
-
token: derivedToken(e.requestId, `observe-${attempt}`),
|
|
495
|
-
});
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
await this.endPause(e);
|
|
499
452
|
}
|
|
500
453
|
}
|
|
501
454
|
/**
|
|
502
|
-
* End the
|
|
503
|
-
* consumer. A cancelled loser's discharge calls it, and so does a hold before
|
|
504
|
-
* (spec/cotal-lang.md §7.8), which claims
|
|
505
|
-
* as the discharge leaves it.
|
|
455
|
+
* End the pauses a step armed: claim every token {@link pauseTokens} says it owns and, for a
|
|
456
|
+
* `wait`, close its consumer. A cancelled loser's discharge calls it, and so does a hold before
|
|
457
|
+
* its first bind (spec/cotal-lang.md §7.8), which claims the held step's open attempt. An `ask`'s
|
|
458
|
+
* relay goal is left as the discharge leaves it.
|
|
506
459
|
*/
|
|
507
460
|
async endPause(e) {
|
|
508
461
|
if (e.requestId === undefined)
|
|
509
462
|
return;
|
|
510
|
-
|
|
463
|
+
for (const token of pauseTokens(e))
|
|
464
|
+
await this.cancelTimer({ endpoint: this.binding.endpoint, token });
|
|
465
|
+
if (e.kind !== "wait")
|
|
466
|
+
return;
|
|
467
|
+
if (this.services) {
|
|
468
|
+
await this.services.waits.close(e.requestId);
|
|
511
469
|
return;
|
|
512
|
-
// An ask's armed timer is its CURRENT attempt's, whose token is bound as `askToken`; a
|
|
513
|
-
// crash before the first bind leaves attempt 1, which is the request id itself.
|
|
514
|
-
const current = e.kind === "ask" && typeof e.external?.askToken === "string"
|
|
515
|
-
? e.external.askToken
|
|
516
|
-
: e.requestId;
|
|
517
|
-
await this.cancelTimer({ endpoint: this.binding.endpoint, token: current });
|
|
518
|
-
if (e.kind === "wait") {
|
|
519
|
-
await this.cancelTimer({ endpoint: this.binding.endpoint, token: derivedToken(e.requestId, "wait-timeout") });
|
|
520
|
-
if (this.services) {
|
|
521
|
-
await this.services.waits.close(e.requestId);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
try {
|
|
525
|
-
await this.jsm.consumers.delete(chatStream(this.binding.space), waitConsumerName(e.requestId));
|
|
526
|
-
}
|
|
527
|
-
catch { /* never created, or already deleted — nothing is held either way */ }
|
|
528
470
|
}
|
|
471
|
+
try {
|
|
472
|
+
await this.jsm.consumers.delete(chatStream(this.binding.space), waitConsumerName(e.requestId));
|
|
473
|
+
}
|
|
474
|
+
catch { /* never created, or already deleted — nothing is held either way */ }
|
|
529
475
|
}
|
|
530
476
|
/**
|
|
531
477
|
* Withdraw a cancelled `notify`'s undelivered notices.
|
|
@@ -1028,8 +974,8 @@ export class MeshHandler {
|
|
|
1028
974
|
}
|
|
1029
975
|
throw e;
|
|
1030
976
|
}
|
|
1031
|
-
if (!rows.some((p) => p.card
|
|
1032
|
-
const reason = rows.some((p) => p.card
|
|
977
|
+
if (!rows.some((p) => p.card.name === name && p.lifecycleUid === uid)) {
|
|
978
|
+
const reason = rows.some((p) => p.card.name === name) ? "superseded" : "lapsed";
|
|
1033
979
|
lapsedSince = lapseWindow(lapsedSince, lastReadAt, this.now());
|
|
1034
980
|
if (reason === "superseded" || readAt - lapsedSince >= LAPSE_CONFIRM_MS) {
|
|
1035
981
|
if (primary !== undefined)
|
|
@@ -1527,8 +1473,8 @@ export class MeshHandler {
|
|
|
1527
1473
|
}
|
|
1528
1474
|
throw e;
|
|
1529
1475
|
}
|
|
1530
|
-
if (!rows.some((pr) => pr.card
|
|
1531
|
-
const reason = rows.some((pr) => pr.card
|
|
1476
|
+
if (!rows.some((pr) => pr.card.name === name && pr.lifecycleUid === uid)) {
|
|
1477
|
+
const reason = rows.some((pr) => pr.card.name === name) ? "superseded" : "lapsed";
|
|
1532
1478
|
lapsedSince = lapseWindow(lapsedSince, lastReadAt, this.now());
|
|
1533
1479
|
if (reason === "superseded" || readAt - lapsedSince >= LAPSE_CONFIRM_MS) {
|
|
1534
1480
|
await this.cancelTimer(primary);
|
|
@@ -2032,19 +1978,22 @@ export class MeshHandler {
|
|
|
2032
1978
|
await (await this.channelRegistry()).delete(plan.channel);
|
|
2033
1979
|
}
|
|
2034
1980
|
}
|
|
2035
|
-
/** Every live presence row that decodes
|
|
2036
|
-
* malformed self-publish must not break another agent's name
|
|
2037
|
-
* row means belongs to the caller: `resolveMemberPrincipal`
|
|
2038
|
-
* `waitDown` reads it as the death it is waiting for. */
|
|
1981
|
+
/** Every live presence row that decodes to a card with a string id and name. Foreign bytes in
|
|
1982
|
+
* the bucket are skipped — a peer's malformed self-publish must not break another agent's name
|
|
1983
|
+
* resolution — and what an ABSENT row means belongs to the caller: `resolveMemberPrincipal`
|
|
1984
|
+
* refuses the join loudly, and `waitDown` reads it as the death it is waiting for. */
|
|
2039
1985
|
async presenceRows() {
|
|
2040
1986
|
const rows = [];
|
|
2041
1987
|
for (const e of await liveKvEntries(await this.presenceRegistry())) {
|
|
1988
|
+
let row;
|
|
2042
1989
|
try {
|
|
2043
|
-
|
|
1990
|
+
row = e.json();
|
|
2044
1991
|
}
|
|
2045
1992
|
catch {
|
|
2046
|
-
|
|
1993
|
+
continue;
|
|
2047
1994
|
}
|
|
1995
|
+
if (isPresenceRow(row))
|
|
1996
|
+
rows.push(row);
|
|
2048
1997
|
}
|
|
2049
1998
|
return rows;
|
|
2050
1999
|
}
|
|
@@ -2462,19 +2411,11 @@ const GOAL_POLL_MS = 2_000;
|
|
|
2462
2411
|
const SPAWN_ACCEPT_DEADLINE_MS = 30_000;
|
|
2463
2412
|
/** Bound on the manager's synchronous `turn` ACCEPT reply (the relay registration, not the yield). */
|
|
2464
2413
|
const TURN_ACCEPT_DEADLINE_MS = 30_000;
|
|
2465
|
-
/** How many times {@link MeshHandler.invokeManager} re-issues one manager call after a
|
|
2466
|
-
* `not-executed` bind refusal. Every re-issue is a first attempt, so the bound is a loop guard and
|
|
2467
|
-
* not a duplication guard: it stops a class whose describe and invoke never agree from re-issuing
|
|
2468
|
-
* forever. Nine attempts leave a two-manager space a 1-in-512 residual where the unrepaired refusal
|
|
2469
|
-
* was 1-in-2 (#1638). The loop turns only on a refusal that has already been ANSWERED, so an
|
|
2470
|
-
* attempt costs a describe and an invoke round trip and never an elapsed deadline: a call nobody
|
|
2471
|
-
* answers raises its own `deadline-exceeded`, which is not a bind refusal and is not re-issued. */
|
|
2472
|
-
const BIND_SPLIT_REISSUES = 8;
|
|
2473
2414
|
/** How many class-rail despawns a discharge sends before it stops waiting for the allocating
|
|
2474
2415
|
* manager to answer. Each is one {@link MeshHandler.invokeManager} call, whose describe and invoke
|
|
2475
2416
|
* both land on the allocator with probability 1/m^2 per trip in a space of m managers, so one call
|
|
2476
|
-
* reaches it with probability (1 - ((m-1)/m)^
|
|
2477
|
-
* 65 then miss with probability about 2^-65 and
|
|
2417
|
+
* reaches it with probability (1 - ((m-1)/m)^17) / m: about 1/2 for m = 2 and 0.25 for m = 4. All
|
|
2418
|
+
* 65 then miss with probability about 2^-65 and 9e-9. */
|
|
2478
2419
|
const DESPAWN_ROUTE_ATTEMPTS = 64;
|
|
2479
2420
|
/** A step key's enclosing scope: the journal's own rendering (`entry.scope`), re-derived so the
|
|
2480
2421
|
* live path and the adoption rebuild key the handoff memos identically. */
|
|
@@ -2635,6 +2576,17 @@ function isConclavePlan(v) {
|
|
|
2635
2576
|
&& typeof r.generation === "number" && typeof r.joined === "boolean";
|
|
2636
2577
|
});
|
|
2637
2578
|
}
|
|
2579
|
+
/** Whether a decoded presence value carries the string `card.id` and `card.name` that every
|
|
2580
|
+
* presence reader here matches on or returns. */
|
|
2581
|
+
function isPresenceRow(v) {
|
|
2582
|
+
if (typeof v !== "object" || v === null)
|
|
2583
|
+
return false;
|
|
2584
|
+
const card = v.card;
|
|
2585
|
+
if (typeof card !== "object" || card === null)
|
|
2586
|
+
return false;
|
|
2587
|
+
const c = card;
|
|
2588
|
+
return typeof c.id === "string" && typeof c.name === "string";
|
|
2589
|
+
}
|
|
2638
2590
|
/** Split an agent handle `<name>#<lifecycleUid>` on its LAST `#` — the uid alphabet
|
|
2639
2591
|
* (`[a-z0-9]{26,32}`) cannot carry one, a name could. */
|
|
2640
2592
|
function parseAgentHandle(agent) {
|
|
@@ -2647,7 +2599,7 @@ function parseAgentHandle(agent) {
|
|
|
2647
2599
|
* the effect's own catchable failure — the agent is down or gone (L4002) — and more than one is
|
|
2648
2600
|
* an ambiguity no membership row may be written under. */
|
|
2649
2601
|
function resolveMemberPrincipal(rows, agent, name, uid) {
|
|
2650
|
-
const matches = rows.filter((p) => p.card
|
|
2602
|
+
const matches = rows.filter((p) => p.card.name === name && p.lifecycleUid === uid);
|
|
2651
2603
|
if (matches.length === 1)
|
|
2652
2604
|
return matches[0].card.id;
|
|
2653
2605
|
if (matches.length === 0)
|
|
@@ -2665,15 +2617,6 @@ function assertConclaveChannel(channel) {
|
|
|
2665
2617
|
/** How often a pause that nobody will answer looks for the broker's fire. Same argument as
|
|
2666
2618
|
* `WAIT_POLL_MS`: the deadline is durable and this is only how late its observation can be. */
|
|
2667
2619
|
const FIRE_POLL_MS = 2_000;
|
|
2668
|
-
/** A second deadline for one step, derived so a resume re-derives it instead of remembering it.
|
|
2669
|
-
* Same shape and alphabet as a request id, so it is a valid `<token>` by construction. */
|
|
2670
|
-
function derivedToken(requestId, purpose) {
|
|
2671
|
-
return createHash("sha256").update(`${requestId}:${purpose}`, "utf8").digest("base64url").slice(0, 43);
|
|
2672
|
-
}
|
|
2673
|
-
/** An ask attempt's pause token: attempt 1 IS the step's request id; a re-ask derives its own. */
|
|
2674
|
-
function askAttemptToken(requestId, attempt) {
|
|
2675
|
-
return attempt === 1 ? requestId : derivedToken(requestId, `ask-attempt-${attempt}`);
|
|
2676
|
-
}
|
|
2677
2620
|
/** The recorded ask progress a resume re-enters at, or undefined for a fresh first attempt. The
|
|
2678
2621
|
* external is bytes from an earlier process, so the shape is checked rather than trusted. */
|
|
2679
2622
|
function askResume(v) {
|
|
@@ -2782,46 +2725,15 @@ export async function rearmOutstandingPauses(deps, binding, entries) {
|
|
|
2782
2725
|
*
|
|
2783
2726
|
* An entry is open when its LAST record is `pending`, so the map is built in append order and the
|
|
2784
2727
|
* later record wins — a step that settled has a settled entry after its pending one, and reading
|
|
2785
|
-
* only the first would re-arm timers for pauses that are already over.
|
|
2786
|
-
*
|
|
2787
|
-
*
|
|
2788
|
-
* the three mints a pause that looks like its own, but a wait's idle window and timeout, an ask
|
|
2789
|
-
* attempt's deadline, and a turn's deadline authority are mediated deadlines exactly as `sleep`'s
|
|
2790
|
-
* is, and one adopted at a new epoch would otherwise wait on a deadline no live epoch fires.
|
|
2791
|
-
*
|
|
2792
|
-
* An idle wait with a timeout arms TWO, and the second is DERIVED rather than recorded, so it is
|
|
2793
|
-
* re-derived here for the same reason the live path derives it: a resume that had to remember it
|
|
2794
|
-
* would be carrying state the key already determines. Emitting it for a wait that never minted one
|
|
2795
|
-
* is harmless by construction — the reconciler reads the checkpoint's status first and re-emits
|
|
2796
|
-
* nothing when there is none — and the alternative, reading the request shape back out of the
|
|
2797
|
-
* entry to decide, would make the repair depend on a field a replay is not guaranteed to carry.
|
|
2798
|
-
* An ask's armed pause is its CURRENT attempt's, whose token is bound as `askToken`; before the
|
|
2799
|
-
* first bind it is attempt 1, which is the request id itself. A `turn`'s is under its goal id,
|
|
2800
|
-
* which is the request id: that pause is the client-side L4003 authority the run keeps for a
|
|
2801
|
-
* manager that dies, so leaving it armed at the predecessor's coordinates would go dark in exactly
|
|
2802
|
-
* the window recovery opens. A held step's hold is armed under its hold id once the hold binds; the
|
|
2803
|
-
* attempt token the hold claimed is still listed and re-arms nothing, because the reconciler
|
|
2804
|
-
* re-emits a schedule only for a waiting pause.
|
|
2728
|
+
* only the first would re-arm timers for pauses that are already over. What each open step owns is
|
|
2729
|
+
* {@link pauseTokens}: a held step's attempt token is still listed after its hold binds and
|
|
2730
|
+
* re-arms nothing, because the reconciler re-emits a schedule only for a waiting pause.
|
|
2805
2731
|
*/
|
|
2806
2732
|
export function outstandingPauseTokens(entries) {
|
|
2807
2733
|
const last = new Map();
|
|
2808
2734
|
for (const e of entries)
|
|
2809
2735
|
last.set(journalEntryKeyString(e), e);
|
|
2810
|
-
|
|
2811
|
-
for (const e of last.values()) {
|
|
2812
|
-
if (e.state !== "pending" || e.requestId === undefined)
|
|
2813
|
-
continue;
|
|
2814
|
-
// `turn` arms its client-side deadline authority under the step's request id (the goal id).
|
|
2815
|
-
if (e.kind === "sleep" || e.kind === "checkpoint" || e.kind === "turn")
|
|
2816
|
-
tokens.push(e.requestId);
|
|
2817
|
-
else if (e.kind === "ask")
|
|
2818
|
-
tokens.push(typeof e.external?.askToken === "string" ? e.external.askToken : e.requestId);
|
|
2819
|
-
else if (e.kind === "wait")
|
|
2820
|
-
tokens.push(e.requestId, derivedToken(e.requestId, "wait-timeout"));
|
|
2821
|
-
if (e.hold !== undefined)
|
|
2822
|
-
tokens.push(holdRequestId(e.requestId));
|
|
2823
|
-
}
|
|
2824
|
-
return tokens;
|
|
2736
|
+
return [...last.values()].filter((e) => e.state === "pending").flatMap(pauseTokens);
|
|
2825
2737
|
}
|
|
2826
2738
|
/**
|
|
2827
2739
|
* The settle watcher over EPF, which is where the one-use settle fact lives.
|