@intentius/chant 0.94.0 → 0.95.1
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/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +10 -4
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +2 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/op/builders.d.ts +14 -3
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +6 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/operator.d.ts +90 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts +84 -0
- package/dist/op/steward-beside.d.ts.map +1 -0
- package/dist/op/steward.d.ts +87 -2
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/workspace/box-services.d.ts +31 -0
- package/dist/workspace/box-services.d.ts.map +1 -0
- package/dist/workspace/compose-graph.d.ts +11 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +5 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/declaration.d.ts +37 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +57 -1
- package/dist/workspace/graph-cache.d.ts +168 -0
- package/dist/workspace/graph-cache.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -5
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/kind-readers.d.ts +39 -0
- package/dist/workspace/kind-readers.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +29 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts +15 -1
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/member-run.d.ts +2 -0
- package/dist/workspace/member-run.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +3 -1
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +10 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +4 -2
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +8 -3
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +23 -7
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +12 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/workspace-kinds.schema.json +26 -0
- package/package.json +1 -1
- package/src/cli/commands/carve-bridge.test.ts +7 -3
- package/src/cli/handlers/operator-steward-signal.e2e.test.ts +97 -0
- package/src/cli/handlers/operator.ts +53 -11
- package/src/cli/handlers/run.test.ts +71 -0
- package/src/cli/handlers/run.ts +65 -7
- package/src/cli/main.ts +17 -6
- package/src/cli/mcp/server.test.ts +14 -0
- package/src/cli/mcp/server.ts +10 -4
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/registry.ts +2 -0
- package/src/cli/static-config-read.test.ts +8 -2
- package/src/meta/source-is-text.test.ts +21 -3
- package/src/okf.test.ts +6 -1
- package/src/op/builders.ts +14 -3
- package/src/op/index.ts +8 -2
- package/src/op/operator.ts +264 -16
- package/src/op/steward-beside.test.ts +267 -0
- package/src/op/steward-beside.ts +219 -0
- package/src/op/steward-points.test.ts +61 -1
- package/src/op/steward.ts +135 -3
- package/src/workspace/box-services.test.ts +129 -0
- package/src/workspace/box-services.ts +51 -0
- package/src/workspace/checks/boxes.test.ts +1 -0
- package/src/workspace/compose-graph.test.ts +1 -0
- package/src/workspace/compose-graph.ts +11 -0
- package/src/workspace/composites.test.ts +1 -1
- package/src/workspace/composites.ts +12 -5
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -2
- package/src/workspace/declaration.schema.json +57 -1
- package/src/workspace/declaration.ts +104 -0
- package/src/workspace/graph-cache.test.ts +343 -0
- package/src/workspace/graph-cache.ts +409 -0
- package/src/workspace/graph-cli.ts +93 -26
- package/src/workspace/graph-contract.test.ts +129 -6
- package/src/workspace/graph.schema.json +21 -1
- package/src/workspace/kind-readers.e2e.test.ts +68 -0
- package/src/workspace/kind-readers.test.ts +134 -0
- package/src/workspace/kind-readers.ts +111 -0
- package/src/workspace/kinds.test.ts +49 -0
- package/src/workspace/kinds.ts +59 -2
- package/src/workspace/member-commands.test.ts +15 -0
- package/src/workspace/member-commands.ts +47 -7
- package/src/workspace/member-run.ts +10 -2
- package/src/workspace/reason-codes.ts +3 -1
- package/src/workspace/records-amend.schema.json +1 -0
- package/src/workspace/records-cli.ts +20 -12
- package/src/workspace/records-contract.test.ts +2 -1
- package/src/workspace/records-new.schema.json +1 -0
- package/src/workspace/records-quorum.test.ts +10 -3
- package/src/workspace/records-sessions-write.test.ts +2 -1
- package/src/workspace/records-write.test.ts +101 -1
- package/src/workspace/records-write.ts +50 -6
- package/src/workspace/records.ts +22 -5
- package/src/workspace/status-contract.test.ts +31 -1
- package/src/workspace/status-stewards.ts +39 -6
- package/src/workspace/status.schema.json +49 -4
- package/src/workspace/status.ts +22 -0
- package/src/workspace/trust/record-seal.test.ts +26 -2
- package/src/workspace/work-evidence.schema.json +1 -0
- package/src/workspace/workspace-kinds.schema.json +26 -0
package/src/op/builders.ts
CHANGED
|
@@ -916,10 +916,21 @@ export const spriteApplyNetworkPolicy = (args: {
|
|
|
916
916
|
return activity("spriteApplyNetworkPolicy", rest, profile ?? "fastIdempotent");
|
|
917
917
|
};
|
|
918
918
|
|
|
919
|
-
/**
|
|
919
|
+
/**
|
|
920
|
+
* Reconcile a sprite's background services (create-or-update, optionally
|
|
921
|
+
* start). With an `id`, through the Sprites API and its `services`; without
|
|
922
|
+
* one, inside the sprite through sprite-env, applying the box block's
|
|
923
|
+
* services (`box: true`, #2880): `only` names some of them, `start` starts
|
|
924
|
+
* the applied ones that are not running, `restart` restarts the converged
|
|
925
|
+
* ones. Defaults to the `fastIdempotent` profile (override via `profile`).
|
|
926
|
+
*/
|
|
920
927
|
export const spriteApplyServices = (args: {
|
|
921
|
-
id
|
|
922
|
-
|
|
928
|
+
id?: string;
|
|
929
|
+
box?: boolean;
|
|
930
|
+
only?: string[];
|
|
931
|
+
restart?: boolean;
|
|
932
|
+
spriteEnv?: string;
|
|
933
|
+
services?: Array<{
|
|
923
934
|
name: string;
|
|
924
935
|
cmd: string;
|
|
925
936
|
args?: string[];
|
package/src/op/index.ts
CHANGED
|
@@ -109,15 +109,21 @@ export type {
|
|
|
109
109
|
export {
|
|
110
110
|
discoverConvergeOps, runOperatorRound, runOperatorForever, formatRoundLine,
|
|
111
111
|
formatSignalLine, DEFAULT_OPERATOR_INTERVAL_MS, acquireStewardLease,
|
|
112
|
-
acquireStewardTurn, STEWARD_TURN_WAIT_MS,
|
|
112
|
+
acquireStewardTurn, STEWARD_TURN_WAIT_MS, createBesideState, waitForBesideRuns, stopBesideRuns,
|
|
113
113
|
} from "./operator";
|
|
114
|
+
export type { BesideState } from "./operator";
|
|
115
|
+
export {
|
|
116
|
+
spawnBesideRun, inProcessBesideLauncher, holdBesideLease, askReady, DEFAULT_READY_TIMEOUT_MS,
|
|
117
|
+
} from "./steward-beside";
|
|
118
|
+
export type { BesideStart, BesideExit, BesideHandle, BesideLauncher, BesideWhy, HeldBesideLease, ReadyAnswer } from "./steward-beside";
|
|
114
119
|
export {
|
|
115
120
|
declareSteward, isStewardDeclaration, stewardFormFor, stewardOpConfig, normaliseStewardForm, stewardLeaseName,
|
|
116
|
-
stewardTurnLeaseName,
|
|
121
|
+
stewardTurnLeaseName, stewardBesideOf, stewardBesideFor, stewardTurnOps, readinessKeys,
|
|
117
122
|
STEWARD_KIND, STEWARD_FORMS, STEWARD_NAME_PATTERN, DEFAULT_STEWARD_ENV,
|
|
118
123
|
} from "./steward";
|
|
119
124
|
export type {
|
|
120
125
|
StewardDeclaration, StewardDeclarationConfig, StewardForm, StewardFormSpec, StewardOpInput,
|
|
126
|
+
StewardBeside, StewardBesideInput,
|
|
121
127
|
} from "./steward";
|
|
122
128
|
export { discoverStewards } from "./discover";
|
|
123
129
|
export {
|
package/src/op/operator.ts
CHANGED
|
@@ -38,12 +38,13 @@
|
|
|
38
38
|
import type { ActivityFn, ActivityProfile } from "./activity-registry";
|
|
39
39
|
import { discoverOps, type DiscoveredOp } from "./discover";
|
|
40
40
|
import { runOpLocally, OpRunFailure, type OpRunResult } from "./local-executor";
|
|
41
|
-
import { acquireLease, releaseLease, stillHoldsLease, currentHolderId, DEFAULT_LEASE_TTL_MS, type LeaseRecord, type AcquireLeaseResult } from "../lifecycle/lease";
|
|
41
|
+
import { acquireLease, releaseLease, stillHoldsLease, currentHolderId, readLease, DEFAULT_LEASE_TTL_MS, type LeaseRecord, type AcquireLeaseResult } from "../lifecycle/lease";
|
|
42
42
|
import { randomUUID } from "node:crypto";
|
|
43
43
|
import { readRunLedger, runEnvOf } from "../lifecycle/run-ledger";
|
|
44
44
|
import { readGateLedger, latestResolutionSince } from "../lifecycle/gate-ledger";
|
|
45
45
|
import { enterStewardTurn } from "./steward-turn";
|
|
46
|
-
import { stewardLeaseName, stewardTurnLeaseName, type StewardDeclaration } from "./steward";
|
|
46
|
+
import { stewardBesideOf, stewardLeaseName, stewardTurnLeaseName, stewardTurnOps, type StewardDeclaration } from "./steward";
|
|
47
|
+
import { askReady, spawnBesideRun, type BesideHandle, type BesideLauncher, type BesideWhy } from "./steward-beside";
|
|
47
48
|
import { stewardWorkHolder } from "./work-lease-run";
|
|
48
49
|
import { StaleLockError } from "../lifecycle/git";
|
|
49
50
|
import { cronMatches, cronDueBetween } from "./cron";
|
|
@@ -146,7 +147,78 @@ export type OperatorTickEvent =
|
|
|
146
147
|
* lease for the whole steward, not one per op). The next round tries
|
|
147
148
|
* again.
|
|
148
149
|
*/
|
|
149
|
-
| { kind: "turn-busy"; op: string; env: string; steward: string; heldBy?: string }
|
|
150
|
+
| { kind: "turn-busy"; op: string; env: string; steward: string; heldBy?: string }
|
|
151
|
+
/**
|
|
152
|
+
* A steward's Op beside its turns (#2861) was started in a process of its
|
|
153
|
+
* own, and the round went on without waiting for it. `why` is its cron, its
|
|
154
|
+
* ready step (`keys` are the work keys it named), or a waiting run to
|
|
155
|
+
* resume (`resumed`, the question now answered) or a gated one (`approved`).
|
|
156
|
+
* `holder` is the Op's lease holder and its work lease holder.
|
|
157
|
+
*/
|
|
158
|
+
| {
|
|
159
|
+
kind: "beside-started";
|
|
160
|
+
op: string;
|
|
161
|
+
env: string;
|
|
162
|
+
why: BesideWhy;
|
|
163
|
+
holder: string;
|
|
164
|
+
keys?: string[];
|
|
165
|
+
resumed?: string;
|
|
166
|
+
approved?: { op: string; gate: string };
|
|
167
|
+
}
|
|
168
|
+
/** A run of an Op beside the turns is in flight (#2861): this operator's, or one whose lease another holder has, such as a hand `chant run`. Nothing was started. */
|
|
169
|
+
| { kind: "beside-running"; op: string; env: string; heldBy?: string }
|
|
170
|
+
/** A run this operator started beside the turns ended (#2861), reported on the round after. `code` is `chant run`'s exit code: 0 ok, 3 gated or waiting, 1 failed. */
|
|
171
|
+
| { kind: "beside-ended"; op: string; env: string; code: number | null; error?: string }
|
|
172
|
+
/**
|
|
173
|
+
* An Op beside the turns whose ready step names no work (#2861), or only
|
|
174
|
+
* work this operator already started a run for (`already`).
|
|
175
|
+
*/
|
|
176
|
+
| { kind: "skipped-not-ready"; op: string; env: string; already?: string[] }
|
|
177
|
+
/** An Op beside the turns whose ready step failed, or answered in a shape it can't read (#2861). Nothing was started. */
|
|
178
|
+
| { kind: "ready-failed"; op: string; env: string; error: string };
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* What a local steward's operator keeps about the runs it starts beside its
|
|
182
|
+
* turns (#2861), across rounds: the ones in flight, the work keys a ready
|
|
183
|
+
* step named that it already started a run for, and the runs that ended since
|
|
184
|
+
* the last round, which the next round reports. `runOperatorForever` owns one
|
|
185
|
+
* for its life; a restarted operator starts empty and finds a run still in
|
|
186
|
+
* flight by its lease.
|
|
187
|
+
*/
|
|
188
|
+
export interface BesideState {
|
|
189
|
+
running: Map<string, { handle: BesideHandle; holder: string; why: BesideWhy }>;
|
|
190
|
+
started: Map<string, Set<string>>;
|
|
191
|
+
ended: OperatorTickEvent[];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** A fresh {@link BesideState}. */
|
|
195
|
+
export function createBesideState(): BesideState {
|
|
196
|
+
return { running: new Map(), started: new Map(), ended: [] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Wait for every run in flight beside the turns to end. `chant operator --steward --once` does before it exits. */
|
|
200
|
+
export async function waitForBesideRuns(state: BesideState): Promise<void> {
|
|
201
|
+
await Promise.all([...state.running.values()].map((r) => r.handle.done));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Ask every run in flight beside the turns to stop, and wait up to `graceMs`
|
|
206
|
+
* for them to end. The operator does when it stops, so no run it started
|
|
207
|
+
* outlives it holding the Op's lease past its renewals.
|
|
208
|
+
*/
|
|
209
|
+
export async function stopBesideRuns(state: BesideState, graceMs = 10_000): Promise<void> {
|
|
210
|
+
const runs = [...state.running.values()];
|
|
211
|
+
for (const r of runs) r.handle.stop();
|
|
212
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
213
|
+
await Promise.race([
|
|
214
|
+
Promise.all(runs.map((r) => r.handle.done)),
|
|
215
|
+
new Promise<void>((resolve) => {
|
|
216
|
+
timer = setTimeout(resolve, graceMs);
|
|
217
|
+
timer.unref?.();
|
|
218
|
+
}),
|
|
219
|
+
]);
|
|
220
|
+
if (timer) clearTimeout(timer);
|
|
221
|
+
}
|
|
150
222
|
|
|
151
223
|
export interface OperatorRoundOptions {
|
|
152
224
|
cwd?: string;
|
|
@@ -185,6 +257,16 @@ export interface OperatorRoundOptions {
|
|
|
185
257
|
* read. Defaults to the workspace's `points` read; a test injects one.
|
|
186
258
|
*/
|
|
187
259
|
readQuestions?: (cwd: string) => Promise<Map<string, string> | null>;
|
|
260
|
+
/**
|
|
261
|
+
* The runs a steward's operator started beside its turns (#2861), kept
|
|
262
|
+
* across rounds. A round given none keeps its own, which it forgets when
|
|
263
|
+
* it returns.
|
|
264
|
+
*/
|
|
265
|
+
besideState?: BesideState;
|
|
266
|
+
/** How a run beside the turns is started. Default: `chant run <op>` in a process of its own ({@link spawnBesideRun}). */
|
|
267
|
+
launchBeside?: BesideLauncher;
|
|
268
|
+
/** The environment the steward operator was started for (`--env`), passed on to a run it starts beside its turns. */
|
|
269
|
+
stewardEnv?: string;
|
|
188
270
|
}
|
|
189
271
|
|
|
190
272
|
/** A steward's Op whose last run waits on a decision point (#2749). */
|
|
@@ -336,12 +418,12 @@ async function releaseStewardTurn(steward: string, holder: string, token: string
|
|
|
336
418
|
}
|
|
337
419
|
|
|
338
420
|
/**
|
|
339
|
-
* The Ops one round
|
|
340
|
-
* steward's Op with no schedule is considered
|
|
341
|
-
* steward's that waits on a decision point.
|
|
421
|
+
* The Ops one round runs as turns: a steward's Ops but those beside its turns
|
|
422
|
+
* (#2861), or every ConvergeOp. A steward's Op with no schedule is considered
|
|
423
|
+
* only to resume a run of the steward's that waits on a decision point.
|
|
342
424
|
*/
|
|
343
425
|
async function roundOps(opts: OperatorRoundOptions): Promise<DiscoveredOp["config"][]> {
|
|
344
|
-
if (opts.steward) return
|
|
426
|
+
if (opts.steward) return stewardTurnOps(opts.steward);
|
|
345
427
|
const { ops } = await discoverConvergeOps({ cwd: opts.cwd, env: opts.env });
|
|
346
428
|
return ops.map((d) => d.config);
|
|
347
429
|
}
|
|
@@ -388,6 +470,10 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
388
470
|
|
|
389
471
|
if (!(await holdSteward())) return events;
|
|
390
472
|
|
|
473
|
+
// The runs started beside the turns that ended since the last round (#2861).
|
|
474
|
+
const besideState = opts.besideState ?? createBesideState();
|
|
475
|
+
events.push(...besideState.ended.splice(0));
|
|
476
|
+
|
|
391
477
|
// A steward's Ops whose last run waits on a decision point (#2749), read
|
|
392
478
|
// once per round, and the state of those questions now.
|
|
393
479
|
const waiting = steward ? await stewardWaits(steward, opts) : new Map<string, WaitState>();
|
|
@@ -395,6 +481,7 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
395
481
|
// been approved since.
|
|
396
482
|
const gated = steward ? await stewardGates(steward, opts) : new Map<string, GateWaitState>();
|
|
397
483
|
|
|
484
|
+
let stewardLost = false;
|
|
398
485
|
for (const config of await roundOps(opts)) {
|
|
399
486
|
const env = steward ? runEnvOf(config) : (envOf(config) ?? "unknown");
|
|
400
487
|
const wait = waiting.get(config.name);
|
|
@@ -548,12 +635,152 @@ export async function runOperatorRound(opts: OperatorRoundOptions): Promise<Oper
|
|
|
548
635
|
restoreTurn?.();
|
|
549
636
|
if (steward && turn?.acquired) await releaseStewardTurn(steward.name, holder, turn.lease!.token, { cwd: opts.cwd });
|
|
550
637
|
}
|
|
551
|
-
if (!(await holdSteward()))
|
|
638
|
+
if (!(await holdSteward())) {
|
|
639
|
+
stewardLost = true;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// The Ops beside the turns (#2861) are considered whatever became of the
|
|
645
|
+
// turns: a turn in progress holds none of them. Only a steward that lost its
|
|
646
|
+
// own lease starts nothing.
|
|
647
|
+
if (steward && !stewardLost) {
|
|
648
|
+
await besideRound(steward, opts, holder, besideState, waiting, gated, events);
|
|
552
649
|
}
|
|
553
650
|
|
|
554
651
|
return events;
|
|
555
652
|
}
|
|
556
653
|
|
|
654
|
+
/**
|
|
655
|
+
* One round for a steward's Ops beside its turns (#2861). For each one not
|
|
656
|
+
* already running: start it when a run of the steward's waits on a question
|
|
657
|
+
* now answered or a gate now approved, when its cron fires, or when its ready
|
|
658
|
+
* step names work it has not started a run for. The run is started and not
|
|
659
|
+
* waited for.
|
|
660
|
+
*/
|
|
661
|
+
async function besideRound(
|
|
662
|
+
steward: StewardDeclaration,
|
|
663
|
+
opts: OperatorRoundOptions,
|
|
664
|
+
holder: string,
|
|
665
|
+
state: BesideState,
|
|
666
|
+
waiting: Map<string, WaitState>,
|
|
667
|
+
gated: Map<string, GateWaitState>,
|
|
668
|
+
events: OperatorTickEvent[],
|
|
669
|
+
): Promise<void> {
|
|
670
|
+
for (const beside of stewardBesideOf(steward)) {
|
|
671
|
+
const config = steward.ops.find((op) => op.name === beside.op);
|
|
672
|
+
if (!config) continue;
|
|
673
|
+
const env = runEnvOf(config);
|
|
674
|
+
|
|
675
|
+
// The cron is read whether or not a run is in flight, so a fire during a
|
|
676
|
+
// run is dropped (overlap "skip") rather than owed afterwards.
|
|
677
|
+
const cron = config.schedule?.cron;
|
|
678
|
+
let due = false;
|
|
679
|
+
if (cron) {
|
|
680
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
681
|
+
const lastSeen = opts.scheduleState?.get(config.name);
|
|
682
|
+
due = lastSeen === undefined ? cronMatches(cron, now) : cronDueBetween(cron, lastSeen, now);
|
|
683
|
+
opts.scheduleState?.set(config.name, now);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const mine = state.running.get(config.name);
|
|
687
|
+
if (mine) {
|
|
688
|
+
events.push({ kind: "beside-running", op: config.name, env, heldBy: mine.holder });
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
// A run another process holds the lease for: a hand `chant run`, or one
|
|
692
|
+
// this steward started before its operator restarted.
|
|
693
|
+
let current: LeaseRecord | undefined;
|
|
694
|
+
try {
|
|
695
|
+
current = (await readLease(config.name, { cwd: opts.cwd })).record;
|
|
696
|
+
} catch (err) {
|
|
697
|
+
events.push({ kind: "lease-error", op: config.name, env, error: err instanceof Error ? err.message : String(err) });
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const now = (opts.now ?? (() => new Date()))();
|
|
701
|
+
if (current && new Date(current.expiresAt).getTime() > now.getTime()) {
|
|
702
|
+
events.push({ kind: "beside-running", op: config.name, env, heldBy: current.holder });
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const wait = waiting.get(config.name);
|
|
707
|
+
const stopped = gated.get(config.name);
|
|
708
|
+
const standing: OperatorTickEvent | undefined = wait
|
|
709
|
+
? { kind: "waiting-on-point", op: config.name, env, point: wait.point, question: wait.question, state: wait.state }
|
|
710
|
+
: stopped
|
|
711
|
+
? { kind: "waiting-on-gate", op: config.name, env, gateOp: stopped.gateOp, gate: stopped.gate }
|
|
712
|
+
: undefined;
|
|
713
|
+
|
|
714
|
+
let why: BesideWhy | undefined;
|
|
715
|
+
if (wait?.answered) why = "resumed";
|
|
716
|
+
else if (stopped?.approved) why = "approved";
|
|
717
|
+
else if (due) why = "cron";
|
|
718
|
+
|
|
719
|
+
let keys: string[] | undefined;
|
|
720
|
+
if (!why && beside.ready) {
|
|
721
|
+
const answer = await askReady(beside.ready, opts.activities, opts.signal);
|
|
722
|
+
if ("error" in answer) {
|
|
723
|
+
events.push({ kind: "ready-failed", op: config.name, env, error: answer.error });
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (answer.ready) {
|
|
727
|
+
const seen = state.started.get(config.name) ?? new Set<string>();
|
|
728
|
+
if (answer.keys.length === 0 || answer.keys.some((k) => !seen.has(k))) {
|
|
729
|
+
why = "ready";
|
|
730
|
+
keys = answer.keys;
|
|
731
|
+
} else {
|
|
732
|
+
events.push(standing ?? { kind: "skipped-not-ready", op: config.name, env, already: answer.keys });
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
} else {
|
|
736
|
+
events.push(standing ?? { kind: "skipped-not-ready", op: config.name, env });
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (!why) {
|
|
741
|
+
const idle: OperatorTickEvent | undefined = standing ?? (cron ? { kind: "skipped-not-due", op: config.name, env, cron } : undefined);
|
|
742
|
+
if (idle) events.push(idle);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const runHolder = stewardWorkHolder(steward.name, config.name, holder);
|
|
747
|
+
let handle: BesideHandle;
|
|
748
|
+
try {
|
|
749
|
+
handle = (opts.launchBeside ?? spawnBesideRun)({
|
|
750
|
+
op: config,
|
|
751
|
+
steward: steward.name,
|
|
752
|
+
...(opts.stewardEnv !== undefined ? { env: opts.stewardEnv } : {}),
|
|
753
|
+
holder: runHolder,
|
|
754
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
755
|
+
...(opts.leaseTtlMs !== undefined ? { leaseTtlMs: opts.leaseTtlMs } : {}),
|
|
756
|
+
});
|
|
757
|
+
} catch (err) {
|
|
758
|
+
events.push({ kind: "tick-failed", op: config.name, env, error: err instanceof Error ? err.message : String(err) });
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (keys) {
|
|
762
|
+
const seen = state.started.get(config.name) ?? new Set<string>();
|
|
763
|
+
for (const k of keys) seen.add(k);
|
|
764
|
+
state.started.set(config.name, seen);
|
|
765
|
+
}
|
|
766
|
+
state.running.set(config.name, { handle, holder: runHolder, why });
|
|
767
|
+
void handle.done.then((exit) => {
|
|
768
|
+
state.running.delete(config.name);
|
|
769
|
+
state.ended.push({ kind: "beside-ended", op: config.name, env, code: exit.code, ...(exit.error ? { error: exit.error } : {}) });
|
|
770
|
+
});
|
|
771
|
+
events.push({
|
|
772
|
+
kind: "beside-started",
|
|
773
|
+
op: config.name,
|
|
774
|
+
env,
|
|
775
|
+
why,
|
|
776
|
+
holder: runHolder,
|
|
777
|
+
...(keys && keys.length > 0 ? { keys } : {}),
|
|
778
|
+
...(why === "resumed" && wait ? { resumed: wait.question } : {}),
|
|
779
|
+
...(why === "approved" && stopped ? { approved: { op: stopped.gateOp, gate: stopped.gate } } : {}),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
557
784
|
/**
|
|
558
785
|
* What a failed tick says about itself (#2301).
|
|
559
786
|
*
|
|
@@ -673,17 +900,24 @@ export async function runOperatorForever(opts: OperatorLoopOptions): Promise<voi
|
|
|
673
900
|
// lands on a firing minute — a fresh operator owes no catch-up for the ticks
|
|
674
901
|
// it was not running for.
|
|
675
902
|
const scheduleState = opts.scheduleState ?? new Map<string, Date>();
|
|
903
|
+
// The runs a steward starts beside its turns (#2861), for the life of the
|
|
904
|
+
// loop. When the loop stops, so do they.
|
|
905
|
+
const besideState = opts.besideState ?? createBesideState();
|
|
676
906
|
const intervalMs = opts.intervalMs ?? DEFAULT_OPERATOR_INTERVAL_MS;
|
|
677
907
|
const subscribers = opts.subscribers ?? [];
|
|
678
908
|
|
|
679
|
-
// Nothing to subscribe to: the loop below is
|
|
680
|
-
//
|
|
909
|
+
// Nothing to subscribe to: the loop below is the loop that shipped before
|
|
910
|
+
// #1981, and takes none of the machinery's cost.
|
|
681
911
|
if (subscribers.length === 0) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
912
|
+
try {
|
|
913
|
+
while (!opts.signal?.aborted) {
|
|
914
|
+
const events = await runOperatorRound({ ...opts, holder, scheduleState, besideState });
|
|
915
|
+
opts.onRound?.(events);
|
|
916
|
+
if (opts.signal?.aborted) break;
|
|
917
|
+
await sleepAbortable(intervalMs, opts.signal);
|
|
918
|
+
}
|
|
919
|
+
} finally {
|
|
920
|
+
await stopBesideRuns(besideState);
|
|
687
921
|
}
|
|
688
922
|
return;
|
|
689
923
|
}
|
|
@@ -736,7 +970,7 @@ export async function runOperatorForever(opts: OperatorLoopOptions): Promise<voi
|
|
|
736
970
|
while (!opts.signal?.aborted) {
|
|
737
971
|
gate.roundStarted();
|
|
738
972
|
const startedAt = Date.now();
|
|
739
|
-
const events = await runOperatorRound({ ...opts, holder, scheduleState });
|
|
973
|
+
const events = await runOperatorRound({ ...opts, holder, scheduleState, besideState });
|
|
740
974
|
opts.onRound?.(events);
|
|
741
975
|
if (opts.signal?.aborted) break;
|
|
742
976
|
|
|
@@ -750,6 +984,7 @@ export async function runOperatorForever(opts: OperatorLoopOptions): Promise<voi
|
|
|
750
984
|
}
|
|
751
985
|
} finally {
|
|
752
986
|
await closeAll();
|
|
987
|
+
await stopBesideRuns(besideState);
|
|
753
988
|
}
|
|
754
989
|
}
|
|
755
990
|
|
|
@@ -780,5 +1015,18 @@ export function formatRoundLine(event: OperatorTickEvent): string {
|
|
|
780
1015
|
return `operator: steward ${event.steward} skipped=1(steward-lease-held${event.heldBy ? `:${event.heldBy}` : ""})`;
|
|
781
1016
|
case "turn-busy":
|
|
782
1017
|
return `operator: ${event.op}@${event.env} skipped=1(turn-held${event.heldBy ? `:${event.heldBy}` : ""}, steward "${event.steward}" is already mid-turn)`;
|
|
1018
|
+
case "beside-started":
|
|
1019
|
+
return `operator: ${event.op}@${event.env} started=1(beside:${event.why}) holder="${event.holder}"` +
|
|
1020
|
+
(event.keys ? ` keys="${event.keys.join(",")}"` : "") +
|
|
1021
|
+
(event.resumed ? ` resumed="${event.resumed}"` : "") +
|
|
1022
|
+
(event.approved ? ` approved="${event.approved.op}/${event.approved.gate}"` : "");
|
|
1023
|
+
case "beside-running":
|
|
1024
|
+
return `operator: ${event.op}@${event.env} skipped=1(beside-running${event.heldBy ? `:${event.heldBy}` : ""})`;
|
|
1025
|
+
case "beside-ended":
|
|
1026
|
+
return `operator: ${event.op}@${event.env} ended=1(beside) exit=${event.code ?? "none"}` + (event.error ? ` error="${event.error}"` : "");
|
|
1027
|
+
case "skipped-not-ready":
|
|
1028
|
+
return `operator: ${event.op}@${event.env} skipped=1(not-ready${event.already ? `:already-started:${event.already.join(",")}` : ""})`;
|
|
1029
|
+
case "ready-failed":
|
|
1030
|
+
return `operator: ${event.op}@${event.env} failed=1(ready) error="${event.error}"`;
|
|
783
1031
|
}
|
|
784
1032
|
}
|