@ai-dossier/sched 0.57.4 → 0.57.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -1397,6 +1397,24 @@ function journalMilestoneProgressIfDue(ctx, state, slotId, milestone, unit, now,
1397
1397
  function effectiveClosedSignal(slot, truth) {
1398
1398
  return (0, state_1.isReportSlot)(slot) ? false : truth.closed;
1399
1399
  }
1400
+ /**
1401
+ * The `QueueEntry` fields backing each entry-rail dedup event (#638): one
1402
+ * descriptor per event instead of accessor lambdas, so `journalConditionIfDue`
1403
+ * and `clearCondition` are single bodies. `condition` names the streak
1404
+ * discriminator field (#637), or `null` for a presence-only streak.
1405
+ */
1406
+ const ENTRY_DEDUP_MARKERS = {
1407
+ 'ground-truth-unreachable': {
1408
+ since: 'ground_truth_unreachable_since',
1409
+ ticks: 'ground_truth_unreachable_ticks',
1410
+ condition: 'ground_truth_unreachable_condition',
1411
+ },
1412
+ 'pr-watch-waiting': {
1413
+ since: 'pr_watch_waiting_since',
1414
+ ticks: 'pr_watch_waiting_ticks',
1415
+ condition: null,
1416
+ },
1417
+ };
1400
1418
  /**
1401
1419
  * Journal `event` for `unit` on the first tick of a new streak, then again
1402
1420
  * only every `JOURNAL_DEDUP_REANNOUNCE_TICKS` ticks while it persists (#632).
@@ -1426,23 +1444,27 @@ function effectiveClosedSignal(slot, truth) {
1426
1444
  * Returns the patched state — callers must thread it, or the marker is lost
1427
1445
  * and the event re-fires next tick as if nothing had been recorded.
1428
1446
  */
1429
- function journalConditionIfDue(ctx, state, unit, event, sinceOf, ticksOf, withMarker, extra) {
1447
+ function journalConditionIfDue(ctx, state, unit, event, condition, extra) {
1430
1448
  const issue = (0, journal_1.issueOfUnit)(unit);
1431
1449
  const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
1432
1450
  if (issue === null || entry === undefined) {
1433
1451
  journal(ctx, event, unit, extra);
1434
1452
  return state;
1435
1453
  }
1436
- const isNewStreak = sinceOf(entry) === null;
1437
- const ticks = isNewStreak ? 1 : ticksOf(entry) + 1;
1454
+ const marker = ENTRY_DEDUP_MARKERS[event];
1438
1455
  const now = ctx.deps.now();
1439
- const since = isNewStreak ? now.toISOString() : sinceOf(entry);
1440
- if (isNewStreak || ticks % types_1.JOURNAL_DEDUP_REANNOUNCE_TICKS === 0) {
1456
+ // #637: identity is the condition key + presence — a different key on the
1457
+ // same unit starts a new streak. Events without a discriminator
1458
+ // (`pr-watch-waiting`) are presence-only.
1459
+ const changed = marker.condition !== null && entry[marker.condition] !== condition;
1460
+ const { since, ticks, announce } = (0, state_1.advanceStreak)({ since: entry[marker.since], ticks: entry[marker.ticks] }, changed, now);
1461
+ if (announce) {
1441
1462
  // `at` is the decision clock (AC3); `since` is the streak's onset. Both
1442
1463
  // are needed: `ticks_persisted` is a TICK count, and the two rails tick at
1443
1464
  // different, operator-tunable rates, so it maps to no fixed wall-clock.
1444
1465
  journal(ctx, event, unit, {
1445
1466
  ...extra,
1467
+ ...(condition !== null ? { condition } : {}),
1446
1468
  at: now.toISOString(),
1447
1469
  since,
1448
1470
  ticks_persisted: ticks,
@@ -1453,39 +1475,29 @@ function journalConditionIfDue(ctx, state, unit, event, sinceOf, ticksOf, withMa
1453
1475
  // elsewhere (`isStaleFailedPark`'s window, `status.ts`'s "since" display,
1454
1476
  // `readiness.ts`'s dispatch tiebreak) as a clock that must not reset on a
1455
1477
  // silent tick.
1456
- return (0, state_1.patchEntry)(state, issue, withMarker(since, ticks), now, false);
1457
- }
1458
- function journalGroundTruthUnreachableIfDue(ctx, state, unit, extra) {
1459
- return journalConditionIfDue(ctx, state, unit, 'ground-truth-unreachable', (e) => e.ground_truth_unreachable_since, (e) => e.ground_truth_unreachable_ticks, (since, ticks) => ({
1460
- ground_truth_unreachable_since: since,
1461
- ground_truth_unreachable_ticks: ticks,
1462
- }), extra);
1463
- }
1464
- function journalPrWatchWaitingIfDue(ctx, state, unit, extra) {
1465
- return journalConditionIfDue(ctx, state, unit, 'pr-watch-waiting', (e) => e.pr_watch_waiting_since, (e) => e.pr_watch_waiting_ticks, (since, ticks) => ({ pr_watch_waiting_since: since, pr_watch_waiting_ticks: ticks }), extra);
1478
+ return (0, state_1.patchEntry)(state, issue, {
1479
+ [marker.since]: since,
1480
+ [marker.ticks]: ticks,
1481
+ ...(marker.condition !== null ? { [marker.condition]: condition } : {}),
1482
+ }, now, false);
1466
1483
  }
1467
1484
  /**
1468
- * Clear a `QueueEntry`'s `ground-truth-unreachable` marker once truth
1469
- * answers again (#632) — a no-op when nothing was set, so callers can call
1470
- * this unconditionally on every tick truth is healthy without churning
1485
+ * Clear a `QueueEntry`'s `event` dedup marker once the condition resolves
1486
+ * (#632) — a no-op when nothing was set, so callers can call this
1487
+ * unconditionally on every tick the condition is healthy without churning
1471
1488
  * `updated_at` for entries that were never in a streak.
1472
1489
  */
1473
- function clearGroundTruthUnreachable(state, unit, now) {
1474
- const issue = (0, journal_1.issueOfUnit)(unit);
1475
- const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
1476
- if (issue === null || entry === undefined || entry.ground_truth_unreachable_since === null) {
1477
- return state;
1478
- }
1479
- return (0, state_1.patchEntry)(state, issue, { ground_truth_unreachable_since: null, ground_truth_unreachable_ticks: 0 }, now, false);
1480
- }
1481
- /** Same as `clearGroundTruthUnreachable`, for the `pr-watch-waiting` marker. */
1482
- function clearPrWatchWaiting(state, unit, now) {
1490
+ function clearCondition(state, unit, event, now) {
1483
1491
  const issue = (0, journal_1.issueOfUnit)(unit);
1484
1492
  const entry = issue === null ? undefined : (0, state_1.findEntry)(state, issue);
1485
- if (issue === null || entry === undefined || entry.pr_watch_waiting_since === null) {
1493
+ const marker = ENTRY_DEDUP_MARKERS[event];
1494
+ if (issue === null || entry === undefined || entry[marker.since] === null)
1486
1495
  return state;
1487
- }
1488
- return (0, state_1.patchEntry)(state, issue, { pr_watch_waiting_since: null, pr_watch_waiting_ticks: 0 }, now, false);
1496
+ return (0, state_1.patchEntry)(state, issue, {
1497
+ [marker.since]: null,
1498
+ [marker.ticks]: 0,
1499
+ ...(marker.condition !== null ? { [marker.condition]: null } : {}),
1500
+ }, now, false);
1489
1501
  }
1490
1502
  /**
1491
1503
  * #575: journal the ignored stale milestone — shared by `reconcileRunning`'s
@@ -1727,7 +1739,7 @@ function reconcileRunning(ctx, state, slot, truth, unit) {
1727
1739
  // this unit until truth returns. The dead-pid rail above still ran — local
1728
1740
  // truth needs no network.
1729
1741
  if (!truth.reachable) {
1730
- return journalGroundTruthUnreachableIfDue(ctx, state, unit, {
1742
+ return journalConditionIfDue(ctx, state, unit, 'ground-truth-unreachable', 'poll-unreachable', {
1731
1743
  slot: slot.id,
1732
1744
  detail: 'stall/advance decisions paused until truth returns',
1733
1745
  });
@@ -1736,7 +1748,7 @@ function reconcileRunning(ctx, state, slot, truth, unit) {
1736
1748
  // a PREVIOUS tick is over.
1737
1749
  // Named for what it holds — a SchedState with the streak cleared — not for
1738
1750
  // `truth.reachable`, the boolean five lines up.
1739
- const cleared = clearGroundTruthUnreachable(state, unit, now);
1751
+ const cleared = clearCondition(state, unit, 'ground-truth-unreachable', now);
1740
1752
  // Ground truth says the unit is DONE while the agent still holds the slot —
1741
1753
  // externally-advanced state (AC3): reclaim the slot, kill the leftover agent.
1742
1754
  // A parked milestone is deliberately NOT an advance: a detached run parks
@@ -1875,7 +1887,7 @@ dispatchSignals = NO_DISPATCH_SIGNALS) {
1875
1887
  // hold the exit in `verifying` until truth returns, then decide. The agent
1876
1888
  // is already gone; no slot work is lost by waiting.
1877
1889
  if (!truth.reachable) {
1878
- return journalGroundTruthUnreachableIfDue(ctx, next, unit, {
1890
+ return journalConditionIfDue(ctx, next, unit, 'ground-truth-unreachable', 'poll-unreachable', {
1879
1891
  slot: slot.id,
1880
1892
  detail: 'exit verification paused until truth returns',
1881
1893
  });
@@ -1900,10 +1912,10 @@ dispatchSignals = NO_DISPATCH_SIGNALS) {
1900
1912
  // against an empty trail. Return the recorded state (the pre-#596 form
1901
1913
  // did — `parkUnit`'s internal guard returned the state it was handed),
1902
1914
  // not the un-recorded `next`. #632: dedup like the sibling check above
1903
- // — same marker, since only one of the two can be live for this unit
1904
- // on a given tick (this branch is reached only after `truth.reachable`
1905
- // already held).
1906
- return journalGroundTruthUnreachableIfDue(ctx, parked, unit, {
1915
+ // — same marker, but a distinct condition key (#637): this branch is
1916
+ // reached only after `truth.reachable` held, so an unreachable-poll
1917
+ // streak from the previous tick must not swallow this line.
1918
+ return journalConditionIfDue(ctx, parked, unit, 'ground-truth-unreachable', 'parked-milestone-no-pr', {
1907
1919
  slot: slot.id,
1908
1920
  detail: `parked milestone (run=${truth.milestone?.run ?? 'unknown'}) carries no parseable pr= key — holding in verifying`,
1909
1921
  });
@@ -1911,12 +1923,12 @@ dispatchSignals = NO_DISPATCH_SIGNALS) {
1911
1923
  // #632: a pr= key was found — both flavors of this unit's
1912
1924
  // ground-truth-unreachable streak (unreachable poll, unparseable pr=)
1913
1925
  // are resolved.
1914
- return parkUnit(ctx, clearGroundTruthUnreachable(parked, unit, now), unit, pr);
1926
+ return parkUnit(ctx, clearCondition(parked, unit, 'ground-truth-unreachable', now), unit, pr);
1915
1927
  }
1916
1928
  // #632: truth answered this tick and this unit isn't stuck on the
1917
1929
  // missing-pr= edge above — any streak recorded against a PREVIOUS tick
1918
1930
  // (either flavor) is over.
1919
- next = clearGroundTruthUnreachable(next, unit, now);
1931
+ next = clearCondition(next, unit, 'ground-truth-unreachable', now);
1920
1932
  // #575: fence to THIS dispatch's `spawned_at` — an agent that exited having
1921
1933
  // posted nothing new must not read as complete against the issue's
1922
1934
  // PREVIOUS run's report milestone; it falls through to the recovery ladder
@@ -2160,18 +2172,18 @@ function reconcileParked(ctx, state, prPoll) {
2160
2172
  if (!prPoll.truths.has(issue)) {
2161
2173
  continue; // parked AFTER the poll ran (this tick) — next cadence picks it up
2162
2174
  }
2163
- next = journalGroundTruthUnreachableIfDue(ctx, next, unit, {
2175
+ next = journalConditionIfDue(ctx, next, unit, 'ground-truth-unreachable', 'pr-watch-paused', {
2164
2176
  detail: 'pr watch paused until truth returns',
2165
2177
  });
2166
2178
  continue;
2167
2179
  }
2168
2180
  // #632: truth answered this tick — any unreachable streak recorded
2169
2181
  // against a PREVIOUS tick is over.
2170
- next = clearGroundTruthUnreachable(next, unit, ctx.deps.now());
2182
+ next = clearCondition(next, unit, 'ground-truth-unreachable', ctx.deps.now());
2171
2183
  const failWatch = (reason) => {
2172
2184
  journal(ctx, 'pr-watch-failed', unit, { reason, pr: entry.pr });
2173
2185
  // #632: the watch is ending (terminal failure) — no more "waiting".
2174
- return clearPrWatchWaiting(failUnit(ctx, next, unit, reason), unit, ctx.deps.now());
2186
+ return clearCondition(failUnit(ctx, next, unit, reason), unit, 'pr-watch-waiting', ctx.deps.now());
2175
2187
  };
2176
2188
  // #501: MERGED is checked FIRST, before any failure rail. A PR that is
2177
2189
  // genuinely merged is merged regardless of a leftover `auto-merge-blocked`
@@ -2185,7 +2197,7 @@ function reconcileParked(ctx, state, prPoll) {
2185
2197
  // AC1: the issue must ALSO be closed (a merged PR auto-closes it) —
2186
2198
  // until GitHub propagates, the unit stays parked and keeps watching.
2187
2199
  if (prPoll.closed.get(issue) !== true) {
2188
- next = journalPrWatchWaitingIfDue(ctx, next, unit, {
2200
+ next = journalConditionIfDue(ctx, next, unit, 'pr-watch-waiting', null, {
2189
2201
  pr: entry.pr,
2190
2202
  mergedAt: truth.mergedAt,
2191
2203
  detail: 'merge seen but issue not closed — keep watching',
@@ -2193,7 +2205,7 @@ function reconcileParked(ctx, state, prPoll) {
2193
2205
  continue;
2194
2206
  }
2195
2207
  next = (0, state_1.transitionIssue)(next, issue, 'shipped', { reason: null }, ctx.deps.now());
2196
- next = clearPrWatchWaiting(next, unit, ctx.deps.now());
2208
+ next = clearCondition(next, unit, 'pr-watch-waiting', ctx.deps.now());
2197
2209
  journal(ctx, 'merge-accepted', unit, { pr: entry.pr, mergedAt: truth.mergedAt });
2198
2210
  ctx.result.mergeAccepted.push(unit);
2199
2211
  continue;
@@ -2213,7 +2225,7 @@ function reconcileParked(ctx, state, prPoll) {
2213
2225
  // OPEN (or mergeable UNKNOWN) — keep watching. #632: this tick's truth
2214
2226
  // is NOT "merge seen but not closed", so a waiting streak from an
2215
2227
  // earlier tick's merge-then-reverted flicker is over.
2216
- next = clearPrWatchWaiting(next, unit, ctx.deps.now());
2228
+ next = clearCondition(next, unit, 'pr-watch-waiting', ctx.deps.now());
2217
2229
  }
2218
2230
  }
2219
2231
  return next;
@@ -2250,23 +2262,23 @@ function reconcileStaleFailedParks(ctx, state, prPoll) {
2250
2262
  if (!prPoll.truths.has(issue)) {
2251
2263
  continue; // failed AFTER the poll ran this tick — next cadence picks it up
2252
2264
  }
2253
- next = journalGroundTruthUnreachableIfDue(ctx, next, unit, {
2265
+ next = journalConditionIfDue(ctx, next, unit, 'ground-truth-unreachable', 'stale-failure-paused', {
2254
2266
  detail: 'stale-failure reconcile paused until truth returns',
2255
2267
  });
2256
2268
  continue;
2257
2269
  }
2258
2270
  // #632: truth answered this tick — any unreachable streak recorded
2259
2271
  // against a PREVIOUS tick is over.
2260
- next = clearGroundTruthUnreachable(next, unit, ctx.deps.now());
2272
+ next = clearCondition(next, unit, 'ground-truth-unreachable', ctx.deps.now());
2261
2273
  if (!isPrMerged(truth)) {
2262
2274
  // Still blocked/open/conflicting — stays failed. #632: not "merge seen
2263
2275
  // but not closed" either, so a waiting streak from an earlier tick's
2264
2276
  // merge-then-reverted flicker is over.
2265
- next = clearPrWatchWaiting(next, unit, ctx.deps.now());
2277
+ next = clearCondition(next, unit, 'pr-watch-waiting', ctx.deps.now());
2266
2278
  continue;
2267
2279
  }
2268
2280
  if (prPoll.closed.get(issue) !== true) {
2269
- next = journalPrWatchWaitingIfDue(ctx, next, unit, {
2281
+ next = journalConditionIfDue(ctx, next, unit, 'pr-watch-waiting', null, {
2270
2282
  pr: entry.pr,
2271
2283
  mergedAt: truth.mergedAt,
2272
2284
  reason: entry.reason,
@@ -2276,7 +2288,7 @@ function reconcileStaleFailedParks(ctx, state, prPoll) {
2276
2288
  }
2277
2289
  const failedAt = entry.updated_at;
2278
2290
  next = (0, state_1.transitionIssue)(next, issue, 'shipped', { reason: null }, ctx.deps.now());
2279
- next = clearPrWatchWaiting(next, unit, ctx.deps.now());
2291
+ next = clearCondition(next, unit, 'pr-watch-waiting', ctx.deps.now());
2280
2292
  journal(ctx, 'stale-failure-reconciled', unit, {
2281
2293
  pr: entry.pr,
2282
2294
  mergedAt: truth.mergedAt,
@@ -2371,29 +2383,15 @@ function dispatchReportAgents(ctx, state, config) {
2371
2383
  }
2372
2384
  /**
2373
2385
  * Run the teardown script for one merged unit OUTSIDE the state lock (#468
2374
- * AC2). Returns null when teardown must be retried next tick (setup info
2375
- * unreachable — a transient outage, never a failure).
2386
+ * AC2). Returns `'unreachable'` when teardown must be retried next tick
2387
+ * (setup info unreachable — a transient outage, never a failure); the caller
2388
+ * journals it through the dedup idiom in its second lock pass (#636), since
2389
+ * this function has no `SchedState` to patch.
2376
2390
  */
2377
2391
  function runTeardownFor(deps, issue) {
2378
2392
  const info = deps.groundTruth.setupInfo(issue);
2379
- const unit = `issue:${issue}`;
2380
- if (info === undefined) {
2381
- // #633: AUDITED, NOT DEDUPED. This is a tenth site with #632's shape —
2382
- // `teardownPendingIssues` reaches it every tick and a `null` return leaves
2383
- // `cleanup` unset, so an unreachable `setupInfo` re-emits this line once
2384
- // per reconcile interval for as long as the outage lasts.
2385
- //
2386
- // Left as-is deliberately: #632 enumerated nine sites and its plan makes
2387
- // the boundary load-bearing ("a site outside the table is a NEW issue, not
2388
- // a reason to widen this one") — widening is what dissolved b-07. The fix
2389
- // is tracked in #636, and is not a one-liner here: `runTeardownFor` runs
2390
- // OUTSIDE the store lock and has no `SchedState` to patch, so the marker
2391
- // has to be threaded through the caller's second lock pass.
2392
- deps.journal.append((0, journal_1.unitEvent)('ground-truth-unreachable', unit, {
2393
- detail: 'teardown paused until truth returns',
2394
- }), deps.now());
2395
- return null;
2396
- }
2393
+ if (info === undefined)
2394
+ return 'unreachable';
2397
2395
  if (info === null) {
2398
2396
  return {
2399
2397
  cleanup: 'failed-missing-setup-info',
@@ -2528,15 +2526,26 @@ function tick(deps, config) {
2528
2526
  // lock; results land in a second short lock pass together with the report
2529
2527
  // dispatch (AC2: teardown, THEN the cheap-tier report agent).
2530
2528
  const results = new Map();
2529
+ const unreachable = [];
2531
2530
  for (const issue of pass1.teardownPending) {
2532
2531
  const teardownResult = runTeardownFor(deps, issue);
2533
- if (teardownResult !== null)
2532
+ if (teardownResult === 'unreachable')
2533
+ unreachable.push(issue);
2534
+ else
2534
2535
  results.set(issue, teardownResult);
2535
2536
  }
2536
- if (results.size > 0) {
2537
+ if (results.size > 0 || unreachable.length > 0) {
2537
2538
  result = deps.store.withLock((state) => {
2538
2539
  const ctx = { deps, config, dispatch, result };
2539
- const next = recordTeardowns(ctx, state, results);
2540
+ let next = state;
2541
+ // #636: `setupInfo` answered for these — any unreachable streak is over.
2542
+ for (const issue of results.keys()) {
2543
+ next = clearCondition(next, `issue:${issue}`, 'ground-truth-unreachable', deps.now());
2544
+ }
2545
+ for (const issue of unreachable) {
2546
+ next = journalConditionIfDue(ctx, next, `issue:${issue}`, 'ground-truth-unreachable', 'teardown-paused', { detail: 'teardown paused until truth returns' });
2547
+ }
2548
+ next = recordTeardowns(ctx, next, results);
2540
2549
  const withReport = dispatchReportAgents(ctx, next, config);
2541
2550
  return { state: withReport, result: ctx.result };
2542
2551
  });