Skip to content

Commit 207961c

Browse files
committed
Preserve pilot evidence at resource boundaries
1 parent b7b3bd3 commit 207961c

7 files changed

Lines changed: 279 additions & 78 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ inkcheck can be driven by a human at a terminal, a CI job, or an optional AI cod
372372
- **Compilation** uses `inklecate`, the canonical compiler — found via `$INKLECATE_PATH`, then `PATH`, then auto-downloaded from the pinned official ink 1.2.1 release into `~/.cache/inkcheck` on first run. Downloaded archives are verified against pinned SHA-256 hashes before extraction. Stories are compiled with `-c` so all knot visits are counted.
373373
- **Exploration** runs the compiled story in [inkjs](https://github.com/y-lohse/inkjs) (the official JS runtime port), reusing pooled story instances so the compiled JSON is parsed once per pass and states rewind via `LoadJson`. Inkcheck initializes story randomness from `--story-seed` (default 1), then preserves Ink's RNG state in every saved branch; authored `SEED_RANDOM(...)` remains authoritative when executed. States are deduplicated by content hash. `INCLUDE`s are followed.
374374
- The CLI uses a bounded, adaptive portfolio search. Complementary passes — last-choice-first, first-choice-first, and inside-out DFS, a diversity-first beam, and seeded random walks — run interleaved in ten deterministic rounds. Initial weights (roughly 20/20/26/15/20%, or a shape profile's suggestion under `--auto`) are reallocated each round toward passes whose findings are still growing, with an intended 8% fractional floor per active pass. Research-only policy replay turns that intent into auditable cumulative integer service and normalizes recency to each pass's observed execution windows instead of a global state count. It requires three windows before estimating yield, expires signals after one or two measured windows without renewal, and permits experimental allocation overlays only for renewed runtime/assertion evidence or explicit goal progress; broad coverage stays with the established scheduler. The production scheduler remains unchanged until the full promotion corpus passes. The passes are complementary: the DFS orderings systematically exhaust subtrees, the beam spreads budget across variable-state lineages within a hard frontier cap, and random walks re-roll every choice point so early-choice combinations get sampled instead of repeated. Findings merge into one report, each labeled with the pass that found it, and the executed schedule appears in `--json` output.
375-
- Workload-aware `--concurrency auto` is the local portfolio default. Its live 1,024-state inside-out DFS pilot becomes the prefix of the normal first adaptive round, then either continues sequentially or overlaps untouched passes in bounded persistent workers. The 80-cell 100K gate retained exact findings, proof, and adaptive schedules with only two open-frontier activations; matched 5M *The Intercept* depth-30/depth-100 gates retained exact evidence, rejected the depth-bound job, and improved the sustained job from 657.7s to 489.0s. Every state remains inside one ceiling and duplicate pilot evaluations are zero. Explicit `1` preserves sequential execution; explicit 2-16 ceilings preserve fixed concurrency. See the [concurrency evaluation](docs/concurrency-evaluation.md).
375+
- Workload-aware `--concurrency auto` is the local portfolio default. Its live 1,024-state inside-out DFS pilot becomes the prefix of the normal first adaptive round, then either continues sequentially or overlaps untouched passes in bounded persistent workers. If time or memory binds during that pilot—or the deadline arrives before any worker initializes—the valid pilot is returned as an explicitly resource-truncated partial report, never mislabeled as state-budget exhaustion. The 80-cell 100K gate retained exact findings, proof, and adaptive schedules with only two open-frontier activations; matched 5M *The Intercept* depth-30/depth-100 gates retained exact evidence, rejected the depth-bound job, and improved the sustained job from 657.7s to 489.0s. Every state remains inside one ceiling and duplicate pilot evaluations are zero. Explicit `1` preserves sequential execution; explicit 2-16 ceilings preserve fixed concurrency. See the [concurrency evaluation](docs/concurrency-evaluation.md).
376376
- Experimental `--search=shared` keeps one global state identity and exposes the pending work through deep, novelty, and seeded frontier views. A state chosen by any view is expanded once; expanded checkpoint JSON is released immediately, compact parent links survive only while a pending descendant needs the exact repro path, and stale view IDs are periodically compacted. Reports expose component accounting and optional explicit checkpoint envelopes. Variable-state and variable-transition rarity are recorded as evaluation telemetry.
377377
- Experimental `--search=shared-variable` replaces one of every eight shared-frontier selections with a variable-rarity view. Its score combines the observed frequency of the destination variable snapshot and the rarest change on that edge; it cannot consume more than its fixed slice, so graph novelty, depth, and seeded exploration remain represented.
378378
- The moment any systematic pass visits every reachable state without hitting a limit, the whole portfolio stops: every further state would be redundant. A small fully-explorable story at the default 10,000,000-state budget still finishes in the handful of states it actually has — the large default costs nothing when a story is exhaustible.

docs/report-schema-v1.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ Search goals may contain ordered `stages`. Each stage reports `reached`, `not_re
5353

5454
Each `explore.passes` entry includes a deterministic `discoveryCurve`, bounded to at most 64 samples. Samples record pass-local state count; cumulative and marginal exact terminals, fallback visible outcomes, runtime errors, assertion violations, reached goals/stages, authored knots, and unique-state novelty; plus the gap from the immediately preceding discovery event. Portfolio reports also carry a merged curve recorded in actual scheduler order (cross-pass unique-state novelty is zero because independent pass hashes are not globally comparable). When compaction occurs, early and latest samples are retained while intermediate samples are deterministically downsampled. Wall-clock time remains observational in progress `elapsedMs`, outside the deterministic curve. These are measured yield facts, not an asymptote estimate, stopping recommendation, or coverage proof.
5555

56-
Portfolio reports record the resolved worker ceiling and `concurrencyMode` (`auto` or `fixed`) in `effectiveConfiguration`. Automatic mode adds `explore.execution.activation`: a versioned deterministic decision and reason, pilot pass/budget/consumption, uncertainty, production eligibility, and duplicate state evaluations. The production `single-pass-frontier-v3` policy reports zero duplicates because its pilot is the prefix of the ordinary first round. `explore.execution` also records requested/effective concurrency plus canonical per-pass grants, consumption, ownership, and completion/resource/failure status. Persistent worker slots retain pass state across adaptive rounds; pass grants and final merge order remain deterministic regardless of completion timing. A failed worker sets `truncatedBy.worker` and `bindingLimit: "worker"` unless another systematic pass independently proves exhaustion; the latest completed snapshots remain valid partial evidence.
56+
Portfolio reports record the resolved worker ceiling and `concurrencyMode` (`auto` or `fixed`) in `effectiveConfiguration`. Automatic mode adds `explore.execution.activation`: a versioned deterministic decision and reason, pilot pass/budget/consumption, uncertainty, production eligibility, and duplicate state evaluations. The production `single-pass-frontier-v3` policy reports zero duplicates because its pilot is the prefix of the ordinary first round. If the live pilot binds on memory or time, `pilot_memory_limit` or `pilot_time_limit` keeps that pilot as the partial result. If the deadline arrives after the pilot but before any worker initializes, `worker_initialization_deadline` does the same. These reasons set the matching resource truncation and explicitly clear `truncatedBy.maxStates`; genuine worker initialization failures remain errors. `explore.execution` also records requested/effective concurrency plus canonical per-pass grants, consumption, ownership, and completion/resource/failure status. Persistent worker slots retain pass state across adaptive rounds; pass grants and final merge order remain deterministic regardless of completion timing. A failed worker sets `truncatedBy.worker` and `bindingLimit: "worker"` unless another systematic pass independently proves exhaustion; the latest completed snapshots remain valid partial evidence.
5757

5858
Shared-search pass telemetry includes `sharedMemory.current`, per-component `peak` values, configured pending-state/byte `limits`, `releasedNodes`, and `frontierCompactions`. Components cover pending and active state JSON/variable snapshots, retained witness ancestry, dedupe keys, semantic indexes, frontier references, and findings. Serialized strings use UTF-8 byte counts; structural bytes are documented estimates. `totalAccountedBytes` is deterministic retained-payload accounting, not process heap or RSS.
5959

src/adaptive-concurrent-portfolio.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,19 @@ export interface AdaptiveConcurrentOptions extends ExploreOptions {
3838
memoryCapBytes: number;
3939
deadlineMs?: number;
4040
failPassForTest?: PortfolioPassKind;
41+
failWorkerInitializationForTest?: boolean;
4142
aggregateMemoryUsedForTest?: () => number;
4243
activationPilotStatesForTest?: number;
44+
/** Deterministic clock injection for executor deadline contract tests. */
45+
nowForTest?: () => number;
46+
}
47+
48+
/** Internal signal: the parent pilot is usable but no worker initialized before the deadline. */
49+
export class PortfolioWorkerInitializationDeadlineError extends Error {
50+
constructor() {
51+
super("all concurrent portfolio workers reached the deadline before initialization");
52+
this.name = "PortfolioWorkerInitializationDeadlineError";
53+
}
4354
}
4455

4556
export interface PassSpec {
@@ -99,8 +110,10 @@ function sanitizedOptions(options: AdaptiveConcurrentOptions): ExploreOptions {
99110
memoryCapBytes: _memoryCapBytes,
100111
deadlineMs: _deadlineMs,
101112
failPassForTest: _failPassForTest,
113+
failWorkerInitializationForTest: _failWorkerInitializationForTest,
102114
aggregateMemoryUsedForTest: _aggregateMemoryUsedForTest,
103115
activationPilotStatesForTest: _activationPilotStatesForTest,
116+
nowForTest: _nowForTest,
104117
onProgress: _onProgress,
105118
onSnapshot: _onSnapshot,
106119
onEvidence: _onEvidence,
@@ -132,6 +145,7 @@ function startSlot(
132145
memoryCapBytes: perWorkerMemory,
133146
...(options.deadlineMs === undefined ? {} : { deadlineMs: options.deadlineMs }),
134147
...(options.failPassForTest ? { failPassForTest: options.failPassForTest } : {}),
148+
...(options.failWorkerInitializationForTest ? { failWorkerInitializationForTest: true } : {}),
135149
control: controlBuffer,
136150
port: channel.port2,
137151
};
@@ -178,7 +192,8 @@ function waitFor(
178192
resources: AggregateResourceTracker,
179193
enforceAggregateMemory = true
180194
): void {
181-
const startedAt = Date.now();
195+
const now = options.nowForTest ?? Date.now;
196+
const startedAt = now();
182197
while (slots.some((slot) => !done(slot) && !slot.failed && !slot.timedOut)) {
183198
for (const slot of slots) drain(slot);
184199
// Worker-thread heap fields are isolate-local; each slot publishes its
@@ -196,13 +211,13 @@ function waitFor(
196211
}
197212
}
198213
emitProgress();
199-
if (options.deadlineMs !== undefined && Date.now() >= options.deadlineMs) {
214+
if (options.deadlineMs !== undefined && now() >= options.deadlineMs) {
200215
for (const slot of slots) {
201216
if (!done(slot) && !slot.failed) slot.timedOut = true;
202217
}
203218
break;
204219
}
205-
if (Date.now() - startedAt >= MAX_WORKER_WAIT_MS) {
220+
if (now() - startedAt >= MAX_WORKER_WAIT_MS) {
206221
for (const slot of slots) {
207222
if (!done(slot) && !slot.failed) slot.failed = "worker exceeded the seven-day executor watchdog";
208223
}
@@ -314,9 +329,18 @@ export function explorePortfolioAdaptiveConcurrent(
314329
});
315330
};
316331

317-
waitFor(slots, (slot) => slot.ready, options, emitProgress, resources);
332+
// Do not spend the remaining finalization reserve starting workers after a
333+
// pilot consumed the search deadline. Checking before the first receive
334+
// also makes the exact deadline boundary deterministic.
335+
if (options.deadlineMs !== undefined && (options.nowForTest ?? Date.now)() >= options.deadlineMs) {
336+
for (const slot of slots) slot.timedOut = true;
337+
} else {
338+
waitFor(slots, (slot) => slot.ready, options, emitProgress, resources);
339+
}
318340
if (slots.every((slot) => slot.failed || slot.timedOut)) {
341+
const deadlineOnly = slots.length > 0 && slots.every((slot) => slot.timedOut && !slot.failed);
319342
for (const slot of slots) stopSlot(slot);
343+
if (initialPilot && deadlineOnly) throw new PortfolioWorkerInitializationDeadlineError();
320344
throw new Error(`all concurrent portfolio workers failed to initialize: ${slots.map((slot) => slot.failed ?? "deadline elapsed").join("; ")}`);
321345
}
322346

src/adaptive-portfolio-worker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface AdaptivePortfolioWorkerData {
2323
memoryCapBytes: number;
2424
deadlineMs?: number;
2525
failPassForTest?: PortfolioPassKind;
26+
failWorkerInitializationForTest?: boolean;
2627
control: SharedArrayBuffer;
2728
port: MessagePort;
2829
}
@@ -77,6 +78,9 @@ function signal(message: AdaptivePortfolioWorkerMessage): void {
7778
}
7879

7980
try {
81+
if (data.failWorkerInitializationForTest) {
82+
throw new Error("injected worker initialization failure");
83+
}
8084
for (const assignment of data.assignments) {
8185
const options: ExploreOptions = {
8286
...data.options,

0 commit comments

Comments
 (0)