@intx/workflow-host 0.3.0 → 0.4.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/README.md +21 -4
- package/dist/adapters/mail-part-store.d.ts +46 -0
- package/dist/adapters/mail-part-store.js +251 -0
- package/dist/adapters/repo-store.js +5 -14
- package/dist/adapters/spawn-child.d.ts +42 -6
- package/dist/adapters/spawn-child.js +8 -18
- package/dist/adapters/step-invoker.d.ts +52 -2
- package/dist/adapters/step-invoker.js +230 -60
- package/dist/adapters/substrate-mailbox-store.d.ts +80 -0
- package/dist/adapters/substrate-mailbox-store.js +404 -0
- package/dist/child/child-mailbox-reader.d.ts +10 -0
- package/dist/child/child-mailbox-reader.js +23 -0
- package/dist/child/credential-cell.d.ts +8 -0
- package/dist/child/credential-cell.js +66 -0
- package/dist/child/from-process-env.d.ts +12 -0
- package/dist/child/from-process-env.js +6 -0
- package/dist/child/index.d.ts +4 -1
- package/dist/child/index.js +4 -1
- package/dist/child/mailbox-mutation-bridge.d.ts +61 -0
- package/dist/child/mailbox-mutation-bridge.js +101 -0
- package/dist/child/mailbox-watch-registry.d.ts +17 -0
- package/dist/child/mailbox-watch-registry.js +61 -0
- package/dist/child/outbound-mail-bridge.d.ts +3 -2
- package/dist/child/outbound-mail-bridge.js +20 -32
- package/dist/child/pending-request.d.ts +89 -0
- package/dist/child/pending-request.js +80 -0
- package/dist/child/run-child.d.ts +69 -7
- package/dist/child/run-child.js +307 -75
- package/dist/child/substrate-write-bridge.d.ts +3 -2
- package/dist/child/substrate-write-bridge.js +21 -38
- package/dist/child/supervisor-backed-transport.d.ts +52 -6
- package/dist/child/supervisor-backed-transport.js +205 -62
- package/dist/child/warm-agent-cache.d.ts +44 -4
- package/dist/child/warm-agent-cache.js +41 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/ipc/control-channel.d.ts +93 -2
- package/dist/ipc/control-channel.js +147 -47
- package/dist/ipc/index.d.ts +1 -1
- package/dist/ipc/index.js +1 -1
- package/dist/run-body-then-cleanup.d.ts +17 -0
- package/dist/run-body-then-cleanup.js +38 -0
- package/dist/seams/scheduler.d.ts +12 -0
- package/dist/seams/scheduler.js +13 -4
- package/dist/supervisor/cancel-signing.js +3 -7
- package/dist/supervisor/credentials.d.ts +17 -5
- package/dist/supervisor/recycle.d.ts +5 -1
- package/dist/supervisor/run-event-compaction.d.ts +2 -2
- package/dist/supervisor/run-event-compaction.js +11 -16
- package/dist/supervisor/run-event-recovery.d.ts +34 -0
- package/dist/supervisor/run-event-recovery.js +45 -0
- package/dist/supervisor/supervisor.d.ts +27 -4
- package/dist/supervisor/supervisor.js +644 -58
- package/dist/supervisor/terminal-commit.js +3 -7
- package/dist/supervisor/types.d.ts +30 -0
- package/dist/testing/change-notifier.d.ts +12 -0
- package/dist/testing/change-notifier.js +63 -0
- package/dist/testing/index.d.ts +8 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/log-capture.d.ts +52 -0
- package/dist/testing/log-capture.js +124 -0
- package/dist/testing/mail-bus.d.ts +22 -0
- package/dist/testing/mail-bus.js +78 -0
- package/dist/testing/memory-streams.d.ts +43 -0
- package/dist/testing/memory-streams.js +211 -0
- package/dist/testing/spawn-observer.d.ts +12 -0
- package/dist/testing/spawn-observer.js +36 -0
- package/dist/testing/stub-repo-store.d.ts +10 -0
- package/dist/testing/stub-repo-store.js +39 -0
- package/dist/testing/supervisor-reaper.d.ts +24 -0
- package/dist/testing/supervisor-reaper.js +49 -0
- package/dist/testing/upstream-frames.d.ts +47 -0
- package/dist/testing/upstream-frames.js +94 -0
- package/dist/workflow-definition-loader.d.ts +56 -0
- package/dist/workflow-definition-loader.js +106 -0
- package/package.json +17 -11
- package/dist/conversation-text.d.ts +0 -23
- package/dist/conversation-text.js +0 -56
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `body`, then always run `cleanup`, without letting a `cleanup`
|
|
3
|
+
* failure mask a `body` failure.
|
|
4
|
+
*
|
|
5
|
+
* When `body` throws, `cleanup` still runs and a `cleanup` failure is
|
|
6
|
+
* handed to `onCleanupErrorAfterBodyError` (to log) and then dropped, so
|
|
7
|
+
* the original `body` error is what propagates. When `body` succeeds, a
|
|
8
|
+
* `cleanup` failure propagates -- there is no primary error to protect, so
|
|
9
|
+
* a failing teardown is the error worth surfacing.
|
|
10
|
+
*
|
|
11
|
+
* This exists so teardown in a `finally` (agent close, warm-cache
|
|
12
|
+
* eviction) can surface its own failure without a `throw` inside a
|
|
13
|
+
* `finally` block, which `no-unsafe-finally` forbids precisely because it
|
|
14
|
+
* silently swallows the in-flight exception -- the masking bug this guards
|
|
15
|
+
* against.
|
|
16
|
+
*/
|
|
17
|
+
export declare function runBodyThenCleanup<T>(body: () => Promise<T>, cleanup: () => Promise<void>, onCleanupErrorAfterBodyError: (cause: unknown) => void): Promise<T>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `body`, then always run `cleanup`, without letting a `cleanup`
|
|
3
|
+
* failure mask a `body` failure.
|
|
4
|
+
*
|
|
5
|
+
* When `body` throws, `cleanup` still runs and a `cleanup` failure is
|
|
6
|
+
* handed to `onCleanupErrorAfterBodyError` (to log) and then dropped, so
|
|
7
|
+
* the original `body` error is what propagates. When `body` succeeds, a
|
|
8
|
+
* `cleanup` failure propagates -- there is no primary error to protect, so
|
|
9
|
+
* a failing teardown is the error worth surfacing.
|
|
10
|
+
*
|
|
11
|
+
* This exists so teardown in a `finally` (agent close, warm-cache
|
|
12
|
+
* eviction) can surface its own failure without a `throw` inside a
|
|
13
|
+
* `finally` block, which `no-unsafe-finally` forbids precisely because it
|
|
14
|
+
* silently swallows the in-flight exception -- the masking bug this guards
|
|
15
|
+
* against.
|
|
16
|
+
*/
|
|
17
|
+
export async function runBodyThenCleanup(body, cleanup, onCleanupErrorAfterBodyError) {
|
|
18
|
+
let outcome;
|
|
19
|
+
try {
|
|
20
|
+
outcome = { ok: true, value: await body() };
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
outcome = { ok: false, error };
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await cleanup();
|
|
27
|
+
}
|
|
28
|
+
catch (cleanupError) {
|
|
29
|
+
if (outcome.ok) {
|
|
30
|
+
throw cleanupError;
|
|
31
|
+
}
|
|
32
|
+
onCleanupErrorAfterBodyError(cleanupError);
|
|
33
|
+
}
|
|
34
|
+
if (!outcome.ok) {
|
|
35
|
+
throw outcome.error;
|
|
36
|
+
}
|
|
37
|
+
return outcome.value;
|
|
38
|
+
}
|
|
@@ -52,6 +52,18 @@ export type SchedulerOpts = {
|
|
|
52
52
|
* callbacks and to skip past-due cron entries on recovery.
|
|
53
53
|
*/
|
|
54
54
|
clock: () => Date;
|
|
55
|
+
/**
|
|
56
|
+
* Arms a one-shot timer and returns its canceller. Defaults to the global
|
|
57
|
+
* timer, which is what production wants.
|
|
58
|
+
*
|
|
59
|
+
* The clock above decides WHEN a timer should fire; this decides what
|
|
60
|
+
* actually fires it. Without both, a caller can compute a deterministic
|
|
61
|
+
* delay and still have to wait out the real interval to observe the
|
|
62
|
+
* firing -- which left this module's own tests waiting past a `fireAt`,
|
|
63
|
+
* and, in one case, waiting longer than a cancelled timer's delay to argue
|
|
64
|
+
* from silence that the cancel had worked.
|
|
65
|
+
*/
|
|
66
|
+
scheduleTimeout?: (handler: () => void, ms: number) => () => void;
|
|
55
67
|
};
|
|
56
68
|
export type SchedulerHandle = {
|
|
57
69
|
/**
|
package/dist/seams/scheduler.js
CHANGED
|
@@ -63,6 +63,15 @@ export const TimerEventEnvelope = type({
|
|
|
63
63
|
"+": "ignore",
|
|
64
64
|
});
|
|
65
65
|
export function createWorkflowHostScheduler(opts) {
|
|
66
|
+
// Production arms the global timer; a caller supplying its own can fire a
|
|
67
|
+
// queued timer on demand instead of waiting out its delay.
|
|
68
|
+
const schedule = opts.scheduleTimeout ??
|
|
69
|
+
((handler, ms) => {
|
|
70
|
+
const handle = setTimeout(handler, ms);
|
|
71
|
+
return () => {
|
|
72
|
+
clearTimeout(handle);
|
|
73
|
+
};
|
|
74
|
+
});
|
|
66
75
|
const queues = new Map();
|
|
67
76
|
const liveSubscriptions = [];
|
|
68
77
|
let started = false;
|
|
@@ -87,7 +96,7 @@ export function createWorkflowHostScheduler(opts) {
|
|
|
87
96
|
if (queues.has(key))
|
|
88
97
|
return; // idempotent
|
|
89
98
|
const delayMs = Math.max(0, fireAtMs - opts.clock().getTime());
|
|
90
|
-
const
|
|
99
|
+
const cancelTimeout = schedule(() => {
|
|
91
100
|
void fireTimer(runId, timerId).catch((cause) => {
|
|
92
101
|
// The scheduler's commit failed. Surface as unhandled so
|
|
93
102
|
// operators see it; the runtime body's awaiter will hang
|
|
@@ -97,7 +106,7 @@ export function createWorkflowHostScheduler(opts) {
|
|
|
97
106
|
: new Error(`scheduler ${String(repoId.id)}/${runId}/${timerId} commit failed: ${String(cause)}`);
|
|
98
107
|
});
|
|
99
108
|
}, delayMs);
|
|
100
|
-
queues.set(key, { runId, timerId, fireAtMs,
|
|
109
|
+
queues.set(key, { runId, timerId, fireAtMs, cancelTimeout, cron });
|
|
101
110
|
}
|
|
102
111
|
function startLiveSubscription(repoId) {
|
|
103
112
|
const abort = new AbortController();
|
|
@@ -202,7 +211,7 @@ export function createWorkflowHostScheduler(opts) {
|
|
|
202
211
|
return;
|
|
203
212
|
stopped = true;
|
|
204
213
|
for (const t of queues.values())
|
|
205
|
-
|
|
214
|
+
t.cancelTimeout();
|
|
206
215
|
queues.clear();
|
|
207
216
|
for (const sub of liveSubscriptions.splice(0)) {
|
|
208
217
|
sub.abort.abort();
|
|
@@ -216,7 +225,7 @@ export function createWorkflowHostScheduler(opts) {
|
|
|
216
225
|
const entry = queues.get(key);
|
|
217
226
|
if (entry === undefined)
|
|
218
227
|
return;
|
|
219
|
-
|
|
228
|
+
entry.cancelTimeout();
|
|
220
229
|
queues.delete(key);
|
|
221
230
|
},
|
|
222
231
|
queuedTimers() {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
// `supervisor`-kind principal), which is the cross-check the
|
|
25
25
|
// supervisor's runtime-side signing keeps coherent.
|
|
26
26
|
import { type } from "arktype";
|
|
27
|
+
import { parseEventSeq } from "@intx/hub-sessions/substrate";
|
|
27
28
|
import { hexEncode } from "@intx/types";
|
|
28
29
|
/**
|
|
29
30
|
* Path inside the workflow-run repo each `CancelRequested` event
|
|
@@ -40,7 +41,6 @@ const EVENTS_DIR = "events";
|
|
|
40
41
|
* push principal and enforces the principal-vs-origin map.
|
|
41
42
|
*/
|
|
42
43
|
export const SUPERVISOR_PRINCIPAL_KIND = "supervisor";
|
|
43
|
-
const EVENT_FILENAME_RE = /^(0|[1-9][0-9]*)\.json$/;
|
|
44
44
|
const OnDiskEnvelope = type({
|
|
45
45
|
seq: "number >= 0",
|
|
46
46
|
type: "string",
|
|
@@ -85,13 +85,9 @@ export async function commitCancelRequested(opts) {
|
|
|
85
85
|
let maxSeq = -1;
|
|
86
86
|
for (const filepath of existing.keys()) {
|
|
87
87
|
const name = filepath.slice(prefix.length);
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
88
|
+
const seq = parseEventSeq(name);
|
|
89
|
+
if (seq === null)
|
|
90
90
|
continue;
|
|
91
|
-
const seqStr = match[1];
|
|
92
|
-
if (seqStr === undefined)
|
|
93
|
-
continue;
|
|
94
|
-
const seq = Number.parseInt(seqStr, 10);
|
|
95
91
|
if (seq > maxSeq)
|
|
96
92
|
maxSeq = seq;
|
|
97
93
|
}
|
|
@@ -14,7 +14,11 @@ export declare const STEP_GRANTS_PATH = "state/grants.json";
|
|
|
14
14
|
*/
|
|
15
15
|
export declare const STEP_GRANTS_REF = "refs/heads/main";
|
|
16
16
|
export type CredentialsSnapshotStep = {
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Workflow step id: a `WorkflowDefinition.stepOrder` entry, or a step id
|
|
19
|
+
* from one of the definition's `loop` bodies (a loop body shares the
|
|
20
|
+
* enclosing definition's flat step-id namespace).
|
|
21
|
+
*/
|
|
18
22
|
stepId: string;
|
|
19
23
|
/** Mail address the step's agent presents to the bus. */
|
|
20
24
|
address: string;
|
|
@@ -24,7 +28,7 @@ export type CredentialsSnapshotStep = {
|
|
|
24
28
|
contentHash: string;
|
|
25
29
|
};
|
|
26
30
|
export type CredentialsSnapshot = {
|
|
27
|
-
/** Step-id keyed entries in
|
|
31
|
+
/** Step-id keyed entries in the caller's traversal order. */
|
|
28
32
|
steps: readonly CredentialsSnapshotStep[];
|
|
29
33
|
};
|
|
30
34
|
/**
|
|
@@ -55,9 +59,17 @@ export type AssembleCredentialsSnapshotOpts = {
|
|
|
55
59
|
/** Principal presented for each step's read. */
|
|
56
60
|
principal: Principal;
|
|
57
61
|
/**
|
|
58
|
-
*
|
|
59
|
-
* passes a single entry; multi-step deployments pass every
|
|
60
|
-
* the order the workflow asset declared.
|
|
62
|
+
* Every step id the snapshot must carry an entry for. The trivial
|
|
63
|
+
* workflow passes a single entry; multi-step deployments pass every
|
|
64
|
+
* step in the order the workflow asset declared.
|
|
65
|
+
*
|
|
66
|
+
* This is the deployment's flat step-id namespace, which is WIDER than
|
|
67
|
+
* the definition's own `stepOrder`: a `loop` body runs in-process as a
|
|
68
|
+
* child run inheriting the parent's env, so a body step authorizes
|
|
69
|
+
* against this same snapshot under its own plain step id. The caller
|
|
70
|
+
* owns that widening -- it is the layer that holds the definition --
|
|
71
|
+
* and the snapshot is total over whatever it passes, because the
|
|
72
|
+
* child's authorize treats a missing entry as unrecoverable.
|
|
61
73
|
*/
|
|
62
74
|
stepOrder: readonly string[];
|
|
63
75
|
/** Anchor run id used in agent-state repo identity and address derivation. */
|
|
@@ -56,7 +56,11 @@ export interface ChildWiring {
|
|
|
56
56
|
export interface RecycleContext {
|
|
57
57
|
/** The supervisor's full bindings, reused on respawn for credentials and spawn. */
|
|
58
58
|
readonly bindings: WorkflowSupervisorBindings;
|
|
59
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Every step id in this deployment's flat step-id namespace -- the
|
|
61
|
+
* definition's own `stepOrder` plus the step ids of every `loop` body it
|
|
62
|
+
* carries -- for credentials re-assembly.
|
|
63
|
+
*/
|
|
60
64
|
readonly stepOrder: readonly string[];
|
|
61
65
|
/** Definition hash carried on respawn env (unchanged across recycle). */
|
|
62
66
|
readonly definitionHash: string;
|
|
@@ -20,8 +20,8 @@ export type CompactRunEventsOpts = {
|
|
|
20
20
|
* Idempotent and terminal-only: a run already sealed (no `events/` subtree)
|
|
21
21
|
* or one whose latest event is not terminal is left untouched, so the call
|
|
22
22
|
* is safe to repeat. The live caller invokes it once per run, right after the
|
|
23
|
-
* run terminates;
|
|
24
|
-
*
|
|
23
|
+
* run terminates; `recoverInterruptedCompactions` re-runs it for a run whose
|
|
24
|
+
* fold a crash interrupted before it could seal.
|
|
25
25
|
*
|
|
26
26
|
* The combined file is the verbatim byte concatenation of the per-event
|
|
27
27
|
* blobs in seq order (`encodeCombinedEventLog`), the exact shape the
|
|
@@ -7,16 +7,10 @@
|
|
|
7
7
|
// without losing any event. The fold writes under the substrate's
|
|
8
8
|
// per-repo lock as the `supervisor` principal, whose `anchorRunId`
|
|
9
9
|
// the workflow-run kind handler checks against `repoId.id`.
|
|
10
|
-
import { WORKFLOW_RUN_EVENTS_FILE, encodeCombinedEventLog, } from "@intx/hub-sessions/substrate";
|
|
10
|
+
import { classifyTerminalEvent, parseEventSeq, WORKFLOW_RUN_EVENTS_FILE, encodeCombinedEventLog, } from "@intx/hub-sessions/substrate";
|
|
11
11
|
import { SUPERVISOR_PRINCIPAL_KIND } from "./cancel-signing.js";
|
|
12
12
|
const RUNS_PREFIX = "runs";
|
|
13
13
|
const EVENTS_DIR = "events";
|
|
14
|
-
const EVENT_FILENAME_RE = /^(0|[1-9][0-9]*)\.json$/;
|
|
15
|
-
const TERMINAL_EVENT_TYPES = new Set([
|
|
16
|
-
"RunCompleted",
|
|
17
|
-
"RunFailed",
|
|
18
|
-
"RunCancelled",
|
|
19
|
-
]);
|
|
20
14
|
/**
|
|
21
15
|
* Fold a terminated run's per-event `events/<seq>.json` blobs into one
|
|
22
16
|
* combined `events.jsonl`, dropping the per-event files. This shrinks the
|
|
@@ -26,8 +20,8 @@ const TERMINAL_EVENT_TYPES = new Set([
|
|
|
26
20
|
* Idempotent and terminal-only: a run already sealed (no `events/` subtree)
|
|
27
21
|
* or one whose latest event is not terminal is left untouched, so the call
|
|
28
22
|
* is safe to repeat. The live caller invokes it once per run, right after the
|
|
29
|
-
* run terminates;
|
|
30
|
-
*
|
|
23
|
+
* run terminates; `recoverInterruptedCompactions` re-runs it for a run whose
|
|
24
|
+
* fold a crash interrupted before it could seal.
|
|
31
25
|
*
|
|
32
26
|
* The combined file is the verbatim byte concatenation of the per-event
|
|
33
27
|
* blobs in seq order (`encodeCombinedEventLog`), the exact shape the
|
|
@@ -55,10 +49,10 @@ export async function compactRunEvents(opts) {
|
|
|
55
49
|
}
|
|
56
50
|
const seqs = [];
|
|
57
51
|
for (const name of filenames) {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
52
|
+
const seq = parseEventSeq(name);
|
|
53
|
+
if (seq === null)
|
|
60
54
|
continue;
|
|
61
|
-
seqs.push(
|
|
55
|
+
seqs.push(seq);
|
|
62
56
|
}
|
|
63
57
|
if (seqs.length === 0)
|
|
64
58
|
return { compacted: false };
|
|
@@ -74,7 +68,8 @@ export async function compactRunEvents(opts) {
|
|
|
74
68
|
return { compacted: false };
|
|
75
69
|
}
|
|
76
70
|
const lastType = parsed.type;
|
|
77
|
-
if (typeof lastType !== "string" ||
|
|
71
|
+
if (typeof lastType !== "string" ||
|
|
72
|
+
!classifyTerminalEvent(lastType).terminal) {
|
|
78
73
|
return { compacted: false };
|
|
79
74
|
}
|
|
80
75
|
const prefix = `${RUNS_PREFIX}/${opts.runId}/${EVENTS_DIR}/`;
|
|
@@ -90,11 +85,11 @@ export async function compactRunEvents(opts) {
|
|
|
90
85
|
const entries = [];
|
|
91
86
|
for (const [filepath, bytes] of existing) {
|
|
92
87
|
const name = filepath.slice(prefix.length);
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
88
|
+
const seq = parseEventSeq(name);
|
|
89
|
+
if (seq === null) {
|
|
95
90
|
throw new Error(`supervisor run-event-compaction: unexpected non-event file ${filepath} under run ${opts.runId}; refusing to compact`);
|
|
96
91
|
}
|
|
97
|
-
entries.push({ seq
|
|
92
|
+
entries.push({ seq, bytes });
|
|
98
93
|
}
|
|
99
94
|
if (entries.length === 0)
|
|
100
95
|
return {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RepoId, RepoStore as SubstrateRepoStore } from "@intx/hub-sessions/substrate";
|
|
2
|
+
export type RecoverInterruptedCompactionsOpts = {
|
|
3
|
+
/** Substrate handle the supervisor writes through. */
|
|
4
|
+
substrate: SubstrateRepoStore;
|
|
5
|
+
/** Workflow-run repo for this deployment. */
|
|
6
|
+
repoId: RepoId;
|
|
7
|
+
/** Events ref the workflow-run repo writes to. */
|
|
8
|
+
ref: string;
|
|
9
|
+
/** Anchor run id used to construct the supervisor principal. */
|
|
10
|
+
anchorRunId: string;
|
|
11
|
+
/** Runs the boot scan proposes as terminal-but-per-event. */
|
|
12
|
+
pendingSealRunIds: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
/** A run whose recovery fold threw, paired with the failure cause. */
|
|
15
|
+
export type RecoveryFoldFailure = {
|
|
16
|
+
runId: string;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Re-seal runs a crash left terminal but still in per-event form, by re-running
|
|
21
|
+
* the idempotent `compactRunEvents` for each proposed run. `compactRunEvents`
|
|
22
|
+
* is authoritative: it no-ops a run that is already sealed or whose latest
|
|
23
|
+
* event is not terminal, so a stale or mistaken proposal is a harmless no-op.
|
|
24
|
+
*
|
|
25
|
+
* Folds run serially. Every fold contends the same per-repo write lock that
|
|
26
|
+
* live dispatch also takes, so folding one run at a time drains the backlog
|
|
27
|
+
* without a thundering herd on that lock. One run's failure is caught so it
|
|
28
|
+
* cannot abort the rest; the failed run id and its cause are returned -- not
|
|
29
|
+
* logged here -- so the caller owns how to surface the aggregate.
|
|
30
|
+
*/
|
|
31
|
+
export declare function recoverInterruptedCompactions(opts: RecoverInterruptedCompactionsOpts): Promise<{
|
|
32
|
+
sealed: number;
|
|
33
|
+
failed: RecoveryFoldFailure[];
|
|
34
|
+
}>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Bounded recovery for run-event compaction folds a crash interrupted.
|
|
2
|
+
//
|
|
3
|
+
// When a run terminates, the supervisor fires `compactRunEvents` in the
|
|
4
|
+
// background. A crash between the terminal commit and that fold leaves the
|
|
5
|
+
// run terminal but still in per-event form, and the terminal signal never
|
|
6
|
+
// fires again for it. At the next spawn the boot scan proposes those runs;
|
|
7
|
+
// this sweep re-runs the idempotent fold for each so the leaked per-event
|
|
8
|
+
// file count is reclaimed.
|
|
9
|
+
import { compactRunEvents } from "./run-event-compaction.js";
|
|
10
|
+
/**
|
|
11
|
+
* Re-seal runs a crash left terminal but still in per-event form, by re-running
|
|
12
|
+
* the idempotent `compactRunEvents` for each proposed run. `compactRunEvents`
|
|
13
|
+
* is authoritative: it no-ops a run that is already sealed or whose latest
|
|
14
|
+
* event is not terminal, so a stale or mistaken proposal is a harmless no-op.
|
|
15
|
+
*
|
|
16
|
+
* Folds run serially. Every fold contends the same per-repo write lock that
|
|
17
|
+
* live dispatch also takes, so folding one run at a time drains the backlog
|
|
18
|
+
* without a thundering herd on that lock. One run's failure is caught so it
|
|
19
|
+
* cannot abort the rest; the failed run id and its cause are returned -- not
|
|
20
|
+
* logged here -- so the caller owns how to surface the aggregate.
|
|
21
|
+
*/
|
|
22
|
+
export async function recoverInterruptedCompactions(opts) {
|
|
23
|
+
let sealed = 0;
|
|
24
|
+
const failed = [];
|
|
25
|
+
for (const runId of opts.pendingSealRunIds) {
|
|
26
|
+
try {
|
|
27
|
+
const { compacted } = await compactRunEvents({
|
|
28
|
+
substrate: opts.substrate,
|
|
29
|
+
repoId: opts.repoId,
|
|
30
|
+
ref: opts.ref,
|
|
31
|
+
anchorRunId: opts.anchorRunId,
|
|
32
|
+
runId,
|
|
33
|
+
});
|
|
34
|
+
if (compacted)
|
|
35
|
+
sealed += 1;
|
|
36
|
+
}
|
|
37
|
+
catch (cause) {
|
|
38
|
+
failed.push({
|
|
39
|
+
runId,
|
|
40
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { sealed, failed };
|
|
45
|
+
}
|
|
@@ -135,6 +135,19 @@ export interface WorkflowSupervisor {
|
|
|
135
135
|
* credential is delivered by omitting its material so the child evicts it.
|
|
136
136
|
*/
|
|
137
137
|
deliverCredentials(opts: DeliverCredentialsOpts): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Refresh a live run's grant floor mid-run by re-reading its durable
|
|
140
|
+
* `runs/<runId>/grants.json` and pushing it as a `grants-updated` frame. The
|
|
141
|
+
* enforcement path for a standing (`scope: "always"`) approval that lowers a
|
|
142
|
+
* tool's `ask` to `allow` in that file. Unlike `deliverSignal`/
|
|
143
|
+
* `deliverSources`, a refresh for a non-live child is normal, so this
|
|
144
|
+
* NO-OPS (`skipped`) instead of throwing, and a send failure to a live child
|
|
145
|
+
* is logged loudly but stays non-fatal -- the durable file governs the next
|
|
146
|
+
* barrier/respawn. It pushes only that file's contents, never caller-supplied
|
|
147
|
+
* grants, so it can only tighten or refresh a floor. Returns whether a live
|
|
148
|
+
* push happened.
|
|
149
|
+
*/
|
|
150
|
+
deliverGrants(runId: string): Promise<"pushed" | "skipped">;
|
|
138
151
|
/**
|
|
139
152
|
* Re-register every correlation the child is currently parked on by
|
|
140
153
|
* querying it for its parked correlations and re-emitting each through
|
|
@@ -158,7 +171,11 @@ export interface WorkflowSupervisor {
|
|
|
158
171
|
getCredentialsSnapshot(): CredentialsSnapshot | null;
|
|
159
172
|
}
|
|
160
173
|
export type SpawnOpts = {
|
|
161
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Every step id in this deployment's flat step-id namespace -- the
|
|
176
|
+
* definition's own `stepOrder` plus the step ids of every `loop` body it
|
|
177
|
+
* carries -- for credentials assembly.
|
|
178
|
+
*/
|
|
162
179
|
stepOrder: readonly string[];
|
|
163
180
|
/** Content hash of the deployment's workflow definition. */
|
|
164
181
|
definitionHash: string;
|
|
@@ -235,11 +252,17 @@ export type DeliverSourcesOpts = {
|
|
|
235
252
|
};
|
|
236
253
|
export type DeliverCredentialsOpts = {
|
|
237
254
|
/**
|
|
238
|
-
* The refreshed credential material and per-handle descriptors.
|
|
239
|
-
*
|
|
240
|
-
*
|
|
255
|
+
* The refreshed credential material and per-handle descriptors. The child
|
|
256
|
+
* MERGES this into its cell (materials upsert by credentialId, bindings by
|
|
257
|
+
* consumer-and-handle); it does not evict by omission.
|
|
241
258
|
*/
|
|
242
259
|
delivery: CredentialDelivery;
|
|
260
|
+
/**
|
|
261
|
+
* CredentialIds to drop from the child's cell (a deletion or a deliberate
|
|
262
|
+
* revocation). The child removes each id's material and any binding that
|
|
263
|
+
* references it. A pure revocation pairs an empty `delivery` with these ids.
|
|
264
|
+
*/
|
|
265
|
+
revoke?: string[];
|
|
243
266
|
};
|
|
244
267
|
export type RecycleOpts = {
|
|
245
268
|
reason: string;
|