claude-code-session-manager 0.120.0 → 0.122.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/assets/{AgentLibrary-LBg3mVL8.js → AgentLibrary-BmJ8hIPw.js} +1 -1
- package/dist/assets/{Button-C4BN5ISR.js → Button-BS4kmz4p.js} +1 -1
- package/dist/assets/{DataModel-CHhjk61s.js → DataModel-DZHBZrfL.js} +1 -1
- package/dist/assets/History-pg00b2Gd.js +2 -0
- package/dist/assets/{Hooks-ByWVgXIf.js → Hooks-BTEs0UZY.js} +1 -1
- package/dist/assets/{Library-QvYVAEmY.js → Library-DuaJ-VSn.js} +1 -1
- package/dist/assets/{ListDetail-FvNe6fo5.js → ListDetail-Cud72s-O.js} +1 -1
- package/dist/assets/{McpServers-C7EmPm8B.js → McpServers-Dy-wnG_X.js} +1 -1
- package/dist/assets/{Memory-nyLR4Yw4.js → Memory-Bs1T1Jo7.js} +1 -1
- package/dist/assets/{Panel-thElCbLX.js → Panel-8UhLXEOW.js} +1 -1
- package/dist/assets/{Permissions-FQ9GM6Ca.js → Permissions-BfwzvmBS.js} +1 -1
- package/dist/assets/Plugins-McjyvvKh.js +2 -0
- package/dist/assets/{ProvenanceBadge-lAViG8yV.js → ProvenanceBadge-D0Ov_6WJ.js} +1 -1
- package/dist/assets/{RemoteMachines-AWpARk7N.js → RemoteMachines-gOlFTvWq.js} +1 -1
- package/dist/assets/{SaveBar-c15EEGB_.js → SaveBar-Cd6hJR6K.js} +1 -1
- package/dist/assets/Scheduler-BZXKqYJP.js +16 -0
- package/dist/assets/{ScopeSwitcher-BLep1rtB.js → ScopeSwitcher-C24ysV1K.js} +1 -1
- package/dist/assets/{Settings-D1kTUIDB.js → Settings-DWnN2qxn.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-_40_f0t3.js → SkillReferenceGraph-BsWuiTbw.js} +1 -1
- package/dist/assets/{Skills-DaTkSOuU.js → Skills-6pHn6glu.js} +1 -1
- package/dist/assets/{SystemPrompt-DxNf-soM.js → SystemPrompt-DT0LBH-K.js} +1 -1
- package/dist/assets/{TagLibrary-L6KpBV-x.js → TagLibrary-DbJ0zbYL.js} +1 -1
- package/dist/assets/{TiptapBody-C_XjtJ82.js → TiptapBody-C4kIvIMH.js} +1 -1
- package/dist/assets/{Toggle-H0rERKNI.js → Toggle-pP19zlyk.js} +1 -1
- package/dist/assets/{ViewTabs-ZUWphDat.js → ViewTabs-CpzN8e5I.js} +1 -1
- package/dist/assets/{historyTotals-BxFKOztb.js → historyTotals-Caq78bb5.js} +1 -1
- package/dist/assets/{index-B5wj0DEX.js → index-Bjtguv6I.js} +434 -434
- package/dist/assets/{settingsSchema-BRcYkXJN.js → settingsSchema-CC2ySu3d.js} +1 -1
- package/dist/assets/{skillFrontmatter-DFGp4DST.js → skillFrontmatter-hDhNo8jE.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-VvrgalfC.js → useAgentsDirWatch-BuOtY25U.js} +1 -1
- package/dist/assets/{useRemotePeers-mvXKNEE3.js → useRemotePeers-CNQmRBpt.js} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -7
- package/plugins/session-manager-dev/skills/develop/standards.md +4 -10
- package/scripts/scheduler-mcp-server.cjs +2 -2
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +56 -43
- package/src/main/docEdit.cjs +1 -0
- package/src/main/index.cjs +12 -13
- package/src/main/ipcSchemas.cjs +17 -3
- package/src/main/lib/classifyPromptTicket.cjs +1 -0
- package/src/main/lib/claude/effectiveModelInfo.cjs +9 -21
- package/src/main/lib/claude/personaToolScope.cjs +7 -1
- package/src/main/lib/claude/runClaudeP.cjs +1 -0
- package/src/main/lib/epic/delegationReadiness.cjs +75 -8
- package/src/main/lib/mcpToolCatalog.cjs +0 -1
- package/src/main/lib/persona-macro/macroLibrary.cjs +75 -0
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +11 -0
- package/src/main/lib/prd/prdCreate.cjs +28 -1
- package/src/main/lib/prd/prdSizing.cjs +1 -8
- package/src/main/lib/remoteTranscriptTail.cjs +69 -2
- package/src/main/lib/runSeeders.cjs +21 -0
- package/src/main/lib/scheduler/loopEfficiency.cjs +109 -0
- package/src/main/lib/scheduler/queueHistory.cjs +44 -1
- package/src/main/lib/scheduler/queueStore.cjs +4 -72
- package/src/main/lib/scheduler/rcaReport.cjs +1 -1
- package/src/main/lib/scheduler/refireCap.cjs +40 -0
- package/src/main/lib/scheduler/reviewNotice.cjs +1 -1
- package/src/main/lib/scheduler/schedulerConfig.cjs +1 -2
- package/src/main/lib/scheduler/schedulerPaths.cjs +0 -2
- package/src/main/lib/scheduler/sessionSlots.cjs +1 -1
- package/src/main/lib/telemetry/auditLog.cjs +65 -14
- package/src/main/loopEfficiencyHandlers.cjs +160 -0
- package/src/main/queueOps.cjs +4 -9
- package/src/main/runVerify.cjs +22 -454
- package/src/main/scheduler/reviewLadder.cjs +12 -2
- package/src/main/scheduler.cjs +103 -177
- package/src/main/seedDevPlugin.cjs +49 -22
- package/src/main/seedStatus.cjs +3 -2
- package/src/main/supervisor.cjs +1 -0
- package/src/main/templates/PRD_AUTHORING.md +16 -31
- package/src/main/webRemote.cjs +82 -91
- package/src/preload/api.d.ts +39 -6
- package/src/preload/index.cjs +3 -0
- package/src/seed/agents/dev-lead.md +2 -9
- package/dist/assets/History-CL-T6Lv3.js +0 -2
- package/dist/assets/Plugins-D9KE7XTI.js +0 -2
- package/dist/assets/Scheduler-FvduP1FG.js +0 -16
package/src/main/scheduler.cjs
CHANGED
|
@@ -66,6 +66,7 @@ const { headlessPermissionArgs, ensureCliCapsProbed } = require('./lib/claude/cl
|
|
|
66
66
|
// as the authoritative guarantee.
|
|
67
67
|
ensureCliCapsProbed().catch(() => {});
|
|
68
68
|
const launchFailure = require('./lib/scheduler/launchFailure.cjs');
|
|
69
|
+
const { decideVerifyDowngrade } = require('./lib/scheduler/refireCap.cjs');
|
|
69
70
|
const { finalizeReasonFor } = require('./lib/scheduler/finalizeReason.cjs');
|
|
70
71
|
const { shouldRunFlatSweep, FLAT_SWEEP_INTERVAL_MS } = require('./lib/scheduler/flatSweepThrottle.cjs');
|
|
71
72
|
const { appendError } = require('./lib/telemetry/opsErrorLog.cjs');
|
|
@@ -124,15 +125,12 @@ const {
|
|
|
124
125
|
MAX_JOB_DURATION_MS,
|
|
125
126
|
BROADCAST_COALESCE_MS,
|
|
126
127
|
RECONCILE_SLOW_PASS_MS,
|
|
127
|
-
QUARANTINE_ESCALATE_MS
|
|
128
|
+
QUARANTINE_ESCALATE_MS,
|
|
128
129
|
PIDLESS_SPAWN_GRACE_MS,
|
|
129
130
|
INVESTIGATION_MAX_MS,
|
|
130
131
|
STARVATION_ESCALATE_MS,
|
|
131
132
|
STARVE_ESCALATION_MS,
|
|
132
133
|
} = require('./lib/scheduler/schedulerConfig.cjs');
|
|
133
|
-
const QUARANTINE_ESCALATE_MS = process.env.SM_QUARANTINE_ESCALATE_HOURS
|
|
134
|
-
? Number(process.env.SM_QUARANTINE_ESCALATE_HOURS) * 60 * 60_000
|
|
135
|
-
: QUARANTINE_ESCALATE_MS_DEFAULT;
|
|
136
134
|
// A running job past this fraction of its own budget gets a durable
|
|
137
135
|
// `budgetWarning` stamp on its row (see the budget watchdog below) so the
|
|
138
136
|
// renderer can warn BEFORE the kill, not only after.
|
|
@@ -444,7 +442,6 @@ const DEFAULT_CONFIG = {
|
|
|
444
442
|
offsetMinutes: 15,
|
|
445
443
|
defaultCwd: DEFAULT_PROJECT_CWD,
|
|
446
444
|
// 'when-available' = poll usage and fire whenever utilization < threshold.
|
|
447
|
-
// 'on-reset' = fire offsetMinutes after the next 5h reset (legacy).
|
|
448
445
|
// 'manual' = only fire on explicit Run now click.
|
|
449
446
|
firePolicy: 'when-available',
|
|
450
447
|
// For 'when-available'. Fire only when BINDING-window utilization < this
|
|
@@ -646,7 +643,6 @@ function ensureDirs() {
|
|
|
646
643
|
// would deadlock the exit path.
|
|
647
644
|
const config = require('./config.cjs');
|
|
648
645
|
const { seedAuthoringGuide } = require('./lib/prd/prdAuthoringSeed.cjs');
|
|
649
|
-
const atomicWriteJsonSync = (p, data) => config.writeJsonSync(p, data);
|
|
650
646
|
|
|
651
647
|
// ---------- scheduler-state.json (sidecar) ----------
|
|
652
648
|
|
|
@@ -721,9 +717,22 @@ function persistSchedulerState() {
|
|
|
721
717
|
|
|
722
718
|
// ---------- heartbeat log ----------
|
|
723
719
|
|
|
720
|
+
// Memory time series stamped on every heartbeat line (MB, whole numbers).
|
|
721
|
+
function memoryFields() {
|
|
722
|
+
const m = process.memoryUsage();
|
|
723
|
+
const mb = (b) => Math.max(0, Math.round((b || 0) / 1048576));
|
|
724
|
+
return {
|
|
725
|
+
rssMB: mb(m.rss),
|
|
726
|
+
heapUsedMB: mb(m.heapUsed),
|
|
727
|
+
heapTotalMB: mb(m.heapTotal),
|
|
728
|
+
externalMB: mb(m.external),
|
|
729
|
+
arrayBuffersMB: mb(m.arrayBuffers),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
724
733
|
function appendHeartbeat(entry) {
|
|
725
734
|
try {
|
|
726
|
-
const line = JSON.stringify(entry) + '\n';
|
|
735
|
+
const line = JSON.stringify({ ...memoryFields(), ...entry }) + '\n';
|
|
727
736
|
let size = 0;
|
|
728
737
|
try { size = fs.statSync(schedulerPaths.heartbeatPath()).size; } catch { /* new file */ }
|
|
729
738
|
if (size >= HEARTBEAT_MAX_BYTES) {
|
|
@@ -766,10 +775,10 @@ function drainSnapshot(jobs) {
|
|
|
766
775
|
/**
|
|
767
776
|
* Restart is triggered automatically ONLY when the installed build-info.json
|
|
768
777
|
* differs from the running codeSha (an install/update already happened) —
|
|
769
|
-
* never by polling npm.
|
|
778
|
+
* never by polling npm.
|
|
770
779
|
*/
|
|
771
780
|
function maybeAutoRequestRestart() {
|
|
772
|
-
if (process.env.
|
|
781
|
+
if (process.env.SM_DEV === '1') return null;
|
|
773
782
|
const installed = readInstalledBuildInfo();
|
|
774
783
|
const installedSha = typeof installed?.gitShortSha === 'string' ? installed.gitShortSha : null;
|
|
775
784
|
if (!upgradeDrain.installedBuildDiffers({ running: SCHEDULER_CODE_SHA, installed: installedSha })) return null;
|
|
@@ -1109,15 +1118,21 @@ function flagUnreadable(state) {
|
|
|
1109
1118
|
// shards under `<cwd>/session-manager-operations/scheduler/state/queue.json`
|
|
1110
1119
|
// plus one machine-runtime file (config/paused/lastRunAt). Reads merge every
|
|
1111
1120
|
// shard into the single state object all downstream logic already expects;
|
|
1112
|
-
// writes split it back. The old global scheduled-plans/queue.json is retired
|
|
1113
|
-
// (split at boot by queueStore.migrateLegacyGlobalQueue).
|
|
1121
|
+
// writes split it back. The old global scheduled-plans/queue.json is retired.
|
|
1114
1122
|
|
|
1115
1123
|
// Sync queue read — passed to the supervisor module (which calls it from
|
|
1116
1124
|
// supervisorTick / applyAction with no await) and the heartbeat interval.
|
|
1117
1125
|
// IPC handlers and mutate() use readQueue (async) below.
|
|
1126
|
+
// Migration: the retired legacy fire policy becomes 'when-available'
|
|
1127
|
+
// so an old install keeps dispatching. Persisted by the next mutate() write.
|
|
1128
|
+
function normalizeConfig(cfg) {
|
|
1129
|
+
if (cfg && cfg.firePolicy === 'on-reset') return { ...cfg, firePolicy: 'when-available' };
|
|
1130
|
+
return cfg;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1118
1133
|
function readQueueSync() {
|
|
1119
1134
|
const s = queueStore.readMergedSync();
|
|
1120
|
-
s.config = { ...DEFAULT_CONFIG, ...(s.config || {}) };
|
|
1135
|
+
s.config = normalizeConfig({ ...DEFAULT_CONFIG, ...(s.config || {}) });
|
|
1121
1136
|
return flagUnreadable(s);
|
|
1122
1137
|
}
|
|
1123
1138
|
|
|
@@ -1126,7 +1141,7 @@ function readQueueSync() {
|
|
|
1126
1141
|
// shard reads hands control back to the renderer between files.
|
|
1127
1142
|
async function readQueue() {
|
|
1128
1143
|
const s = await queueStore.readMerged();
|
|
1129
|
-
s.config = { ...DEFAULT_CONFIG, ...(s.config || {}) };
|
|
1144
|
+
s.config = normalizeConfig({ ...DEFAULT_CONFIG, ...(s.config || {}) });
|
|
1130
1145
|
return flagUnreadable(s);
|
|
1131
1146
|
}
|
|
1132
1147
|
|
|
@@ -2087,10 +2102,6 @@ async function refreshNextReset() {
|
|
|
2087
2102
|
return cachedNextReset;
|
|
2088
2103
|
}
|
|
2089
2104
|
|
|
2090
|
-
function getNextResetCached() {
|
|
2091
|
-
return cachedNextReset;
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
2105
|
/**
|
|
2095
2106
|
* Pure: picks the reset to pause against for a rate-limited run (PRD 1118).
|
|
2096
2107
|
* Prefers the BINDING window read off the run's own log — refreshNextReset()
|
|
@@ -2223,7 +2234,6 @@ const consecutiveRapidRateLimitsBySlug = new Map();
|
|
|
2223
2234
|
// ---------- timer ----------
|
|
2224
2235
|
|
|
2225
2236
|
let mainWindow = null;
|
|
2226
|
-
let fireTimer = null;
|
|
2227
2237
|
let resumeTimer = null;
|
|
2228
2238
|
let pollLoopTimer = null;
|
|
2229
2239
|
let rescheduleInterval = null;
|
|
@@ -2344,14 +2354,13 @@ function buildScheduleStatePayload(state) {
|
|
|
2344
2354
|
const payload = {
|
|
2345
2355
|
config: state.config,
|
|
2346
2356
|
jobs: state.jobs,
|
|
2347
|
-
scheduledFor: state.scheduledFor,
|
|
2348
2357
|
lastRunAt: state.lastRunAt,
|
|
2349
2358
|
// Distinct from lastRunAt (only stamped when a batch actually launches):
|
|
2350
2359
|
// stamped every time tickQueue reaches the picker at all. See
|
|
2351
2360
|
// classifyQueueHealth/classifyQueueStarvation's header comments for why
|
|
2352
2361
|
// the two must never merge.
|
|
2353
2362
|
lastDispatchAttemptAt: state.lastDispatchAttemptAt ?? null,
|
|
2354
|
-
nextReset:
|
|
2363
|
+
nextReset: cachedNextReset,
|
|
2355
2364
|
paused: state.paused,
|
|
2356
2365
|
drain: state.drain ?? null,
|
|
2357
2366
|
// Launch circuit breaker (issue #11): which personas cannot launch right
|
|
@@ -2428,24 +2437,6 @@ async function broadcast(opts = {}) {
|
|
|
2428
2437
|
}
|
|
2429
2438
|
}
|
|
2430
2439
|
|
|
2431
|
-
function clearFireTimer() {
|
|
2432
|
-
if (fireTimer) {
|
|
2433
|
-
clearTimeout(fireTimer);
|
|
2434
|
-
fireTimer = null;
|
|
2435
|
-
}
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
function computeFireAt(state, nextResetIso) {
|
|
2439
|
-
// Only the legacy 'on-reset' policy uses scheduled fire times. Other
|
|
2440
|
-
// policies fire either immediately on demand ('manual') or via the
|
|
2441
|
-
// when-available poll loop.
|
|
2442
|
-
if (state.config.firePolicy !== 'on-reset') return null;
|
|
2443
|
-
if (!nextResetIso) return null;
|
|
2444
|
-
const reset = new Date(nextResetIso).getTime();
|
|
2445
|
-
if (Number.isNaN(reset)) return null;
|
|
2446
|
-
return reset + (state.config.offsetMinutes * 60_000);
|
|
2447
|
-
}
|
|
2448
|
-
|
|
2449
2440
|
// PRD 1446: refreshNextReset() awaits billing.fetchUsage(), which can hang
|
|
2450
2441
|
// or back off for minutes while the usage meter is rate-limited. Left
|
|
2451
2442
|
// unbounded, that stalled THIS function's own mutate(reconcile) call for as
|
|
@@ -2459,12 +2450,9 @@ function computeFireAt(state, nextResetIso) {
|
|
|
2459
2450
|
const RESCHEDULE_TIMER_BILLING_RACE_MS = 10_000;
|
|
2460
2451
|
|
|
2461
2452
|
async function rescheduleTimer() {
|
|
2462
|
-
|
|
2463
|
-
// Wrap in try/catch — on failure use the cached value so the on-reset
|
|
2464
|
-
// timer can still be armed from the last known reset.
|
|
2465
|
-
let nextResetIso;
|
|
2453
|
+
// Wrap in try/catch — on failure the cached value is kept.
|
|
2466
2454
|
try {
|
|
2467
|
-
|
|
2455
|
+
await Promise.race([
|
|
2468
2456
|
refreshNextReset(),
|
|
2469
2457
|
new Promise((resolve) => {
|
|
2470
2458
|
const t = setTimeout(() => resolve(cachedNextReset), RESCHEDULE_TIMER_BILLING_RACE_MS);
|
|
@@ -2472,20 +2460,12 @@ async function rescheduleTimer() {
|
|
|
2472
2460
|
}),
|
|
2473
2461
|
]);
|
|
2474
2462
|
} catch {
|
|
2475
|
-
|
|
2463
|
+
// cachedNextReset stays as-is; reconcile below does not need the value.
|
|
2476
2464
|
}
|
|
2477
|
-
|
|
2465
|
+
await mutate(async (state) => {
|
|
2478
2466
|
await reconcile(state);
|
|
2479
|
-
const fa = computeFireAt(state, nextResetIso);
|
|
2480
|
-
state.scheduledFor = fa ? new Date(fa).toISOString() : null;
|
|
2481
|
-
return fa;
|
|
2482
2467
|
});
|
|
2483
2468
|
await broadcast();
|
|
2484
|
-
if (!fireAt) return;
|
|
2485
|
-
|
|
2486
|
-
const delay = Math.max(1000, fireAt - Date.now());
|
|
2487
|
-
fireTimer = setTimeout(() => { runDueJobs().catch(() => {}); }, delay);
|
|
2488
|
-
console.log(`[scheduler] next fire in ${Math.round(delay / 1000)}s @ ${new Date(fireAt).toISOString()}`);
|
|
2489
2469
|
}
|
|
2490
2470
|
|
|
2491
2471
|
// ---------- pause / resume ----------
|
|
@@ -2709,6 +2689,9 @@ function resetJobFields(job, errorMsg, opts = {}) {
|
|
|
2709
2689
|
job.error = errorMsg ?? null;
|
|
2710
2690
|
delete job.runtime;
|
|
2711
2691
|
delete job.verifierVerdict;
|
|
2692
|
+
// A reset starts the consecutive-downgrade count over (the refire path in
|
|
2693
|
+
// spawnJob re-sets it right after this call).
|
|
2694
|
+
delete job.verifyRefireCount;
|
|
2712
2695
|
delete job.humanQuestion;
|
|
2713
2696
|
delete job.uncommittedPaths;
|
|
2714
2697
|
delete job.resumeRecoveryAttempted;
|
|
@@ -2868,7 +2851,9 @@ function isBootRowAlive(j, {
|
|
|
2868
2851
|
* applyOrphanOutcome(job, outcome, killNote?, confirmedLandedCommit?) → void
|
|
2869
2852
|
*
|
|
2870
2853
|
* Mutates `job` in place to finalize a boot-orphaned 'running' job given its
|
|
2871
|
-
* classified run outcome: success
|
|
2854
|
+
* classified run outcome: success finalizes only with landed-commit evidence
|
|
2855
|
+
* (or for artifact-only PRDs), otherwise it takes the re-queue path below;
|
|
2856
|
+
* failed finalizes terminally; no_result/unknown
|
|
2872
2857
|
* re-queues to pending bounded by ORPHAN_REQUEUE_CAP. The status-mutation
|
|
2873
2858
|
* semantics (and the cap-exhaustion boundary) match the now-deleted
|
|
2874
2859
|
* reconcileQueueOffline (src/main/lib/scheduler/watchdogHelpers.cjs) verbatim; killNote
|
|
@@ -2882,12 +2867,21 @@ function isBootRowAlive(j, {
|
|
|
2882
2867
|
* orphaned job that actually landed a real commit is reachable via boot
|
|
2883
2868
|
* reconciliation even though the live reap path is now guarded. Callers
|
|
2884
2869
|
* must resolve this (a git spawn) BEFORE calling mutate(), never inside it —
|
|
2885
|
-
* pass null
|
|
2870
|
+
* pass null when there is none (a 'success' then re-queues, unless artifact-only).
|
|
2886
2871
|
*/
|
|
2887
2872
|
function applyOrphanOutcome(job, outcome, killNote = '', confirmedLandedCommit = null) {
|
|
2888
2873
|
const now = new Date().toISOString();
|
|
2889
|
-
|
|
2890
|
-
|
|
2874
|
+
// A reported 'success' completes only on landed-commit evidence, or for a
|
|
2875
|
+
// DECLARED artifact-only PRD (`deliverable: artifact` + artifactPaths — same
|
|
2876
|
+
// check as runVerify's artifact exemption) where no commit is legitimate.
|
|
2877
|
+
const artifactOnly = job.deliverable === 'artifact' && Array.isArray(job.artifactPaths) && job.artifactPaths.length > 0;
|
|
2878
|
+
if (outcome === 'success' && (confirmedLandedCommit || artifactOnly)) {
|
|
2879
|
+
if (confirmedLandedCommit) {
|
|
2880
|
+
transitionJob(job, 'completed', { reason: `boot orphan reconciliation: run succeeded, landedCommit ${confirmedLandedCommit} resolves`, source: 'applyOrphanOutcome:landed' });
|
|
2881
|
+
job.landedCommit = confirmedLandedCommit;
|
|
2882
|
+
} else {
|
|
2883
|
+
transitionJob(job, 'completed', { reason: 'boot orphan reconciliation: run succeeded (artifact-only PRD, no commit expected)', source: 'applyOrphanOutcome' });
|
|
2884
|
+
}
|
|
2891
2885
|
job.exitCode = 0;
|
|
2892
2886
|
job.error = null;
|
|
2893
2887
|
job.finishedAt = now;
|
|
@@ -2907,8 +2901,9 @@ function applyOrphanOutcome(job, outcome, killNote = '', confirmedLandedCommit =
|
|
|
2907
2901
|
delete job.runtime;
|
|
2908
2902
|
} else {
|
|
2909
2903
|
const tries = job.orphanRetries ?? 0;
|
|
2904
|
+
const noLanded = outcome === 'success' ? ' — run reported success but no landed commit was found' : '';
|
|
2910
2905
|
if (tries < ORPHAN_REQUEUE_CAP) {
|
|
2911
|
-
resetJobFields(job, `orphaned: app restarted mid-run, re-queued (attempt ${tries + 1}/${ORPHAN_REQUEUE_CAP})${killNote}`, { source: 'applyOrphanOutcome' });
|
|
2906
|
+
resetJobFields(job, `orphaned: app restarted mid-run, re-queued (attempt ${tries + 1}/${ORPHAN_REQUEUE_CAP})${killNote}${noLanded}`, { source: 'applyOrphanOutcome' });
|
|
2912
2907
|
job.orphanRetries = tries + 1;
|
|
2913
2908
|
} else if (confirmedLandedCommit) {
|
|
2914
2909
|
transitionJob(job, 'completed', { reason: `orphaned: app restarted while running, exhausted ${ORPHAN_REQUEUE_CAP} re-queue attempts${killNote}, but landedCommit ${confirmedLandedCommit} resolves — completed on evidence`, source: 'applyOrphanOutcome:landed' });
|
|
@@ -2918,9 +2913,9 @@ function applyOrphanOutcome(job, outcome, killNote = '', confirmedLandedCommit =
|
|
|
2918
2913
|
job.finishedAt = now;
|
|
2919
2914
|
delete job.runtime;
|
|
2920
2915
|
} else {
|
|
2921
|
-
transitionJob(job, 'failed', { reason: `orphaned: app restarted while running, exhausted ${ORPHAN_REQUEUE_CAP} re-queue attempts${killNote}`, source: 'applyOrphanOutcome' });
|
|
2916
|
+
transitionJob(job, 'failed', { reason: `orphaned: app restarted while running, exhausted ${ORPHAN_REQUEUE_CAP} re-queue attempts${killNote}${noLanded}`, source: 'applyOrphanOutcome' });
|
|
2922
2917
|
job.exitCode = job.exitCode ?? 1;
|
|
2923
|
-
job.error = `orphaned: app restarted while running, exhausted ${ORPHAN_REQUEUE_CAP} re-queue attempts${killNote}`;
|
|
2918
|
+
job.error = `orphaned: app restarted while running, exhausted ${ORPHAN_REQUEUE_CAP} re-queue attempts${killNote}${noLanded}`;
|
|
2924
2919
|
job.finishedAt = now;
|
|
2925
2920
|
delete job.runtime;
|
|
2926
2921
|
}
|
|
@@ -3148,10 +3143,10 @@ async function notifyOriginatingTab(job, {
|
|
|
3148
3143
|
// survive an entirely unreadable PRD: the queue row carries the Epic FK
|
|
3149
3144
|
// independently of the .md on disk (20 of 21 history rows had it when
|
|
3150
3145
|
// PRD 985 was written), so a parse failure degrades to "notified without
|
|
3151
|
-
// frontmatter detail" instead of "silently dropped".
|
|
3152
|
-
//
|
|
3153
|
-
// is
|
|
3154
|
-
//
|
|
3146
|
+
// frontmatter detail" instead of "silently dropped". The
|
|
3147
|
+
// needs_review notice path carries the same fallback (PRD 854), which
|
|
3148
|
+
// is why needs_review routing kept working while completed/failed
|
|
3149
|
+
// routing did not.
|
|
3155
3150
|
const epicId = prd?.sourcePromptId || job.epicId || null;
|
|
3156
3151
|
|
|
3157
3152
|
// Persist the job's real result text (not just the short status chip
|
|
@@ -3283,61 +3278,6 @@ async function notifyOriginatingTab(job, {
|
|
|
3283
3278
|
}
|
|
3284
3279
|
}
|
|
3285
3280
|
|
|
3286
|
-
/**
|
|
3287
|
-
* notifyNeedsReview(job, report) → Promise<boolean>
|
|
3288
|
-
*
|
|
3289
|
-
* A job parked in `needs_review` is a question, not a result: something about
|
|
3290
|
-
* the PRD was unclear, unverifiable, or ran past its acceptance criteria.
|
|
3291
|
-
* That question belongs in the conversation that WROTE the PRD, so this
|
|
3292
|
-
* appends a 'response' event to the authoring Epic's own chain (the same
|
|
3293
|
-
* channel notifyOriginatingTab uses for completed/failed, which deliberately
|
|
3294
|
-
* excludes needs_review — isNotifiableTerminalStatus). It resolves the Epic
|
|
3295
|
-
* from the PRD's `sourcePromptId`, falling back to the job's own `epicId`.
|
|
3296
|
-
*
|
|
3297
|
-
* Join-only by construction: appendResponseEventIfKnown refuses an unknown or
|
|
3298
|
-
* non-active session and returns false. Nothing here can create an Epic — if
|
|
3299
|
-
* no open authoring Epic exists, the root-cause report in the run directory
|
|
3300
|
-
* is the whole record and the Scheduler tab is where it surfaces.
|
|
3301
|
-
*
|
|
3302
|
-
* Quiet by default (this PRD): the park path no longer calls this at park
|
|
3303
|
-
* time. It records a `reviewNotice` on the job row instead and lets
|
|
3304
|
-
* flushDueReviewNotices send one grouped message once the self-heal ladder
|
|
3305
|
-
* gives up or the hold time passes. This function now only runs under the
|
|
3306
|
-
* SM_REVIEW_NOTICE_IMMEDIATE=1 kill switch (same park-time call site, for
|
|
3307
|
-
* local debugging) — it is otherwise dead code outside its own tests. That
|
|
3308
|
-
* call site stamps `reviewNotice.sentAt` itself afterward, and only when
|
|
3309
|
-
* this returns true AND the row's live notice still matches the one it just
|
|
3310
|
-
* built — never on the strength of this function's return value alone.
|
|
3311
|
-
*/
|
|
3312
|
-
async function notifyNeedsReview(job, report, {
|
|
3313
|
-
parsePrdRaw = prdParser.parsePrdRaw,
|
|
3314
|
-
appendResponseEvent = appendResponseEventIfKnown,
|
|
3315
|
-
} = {}) {
|
|
3316
|
-
try {
|
|
3317
|
-
if (!job || !report || !report.filed) return false;
|
|
3318
|
-
const prd = await resolveNotifyPrd(job, parsePrdRaw);
|
|
3319
|
-
const epicId = prd?.sourcePromptId || job.epicId || null;
|
|
3320
|
-
if (!epicId || !job.cwd) {
|
|
3321
|
-
console.log(`[scheduler] notifyNeedsReview: no authoring Epic for ${job.slug}, report only`);
|
|
3322
|
-
return false;
|
|
3323
|
-
}
|
|
3324
|
-
const message = `${report.summary}. Root-cause report: ${report.path}`;
|
|
3325
|
-
// PRD 986: like every scheduler check-in, born 'unvalidated' — the RCA
|
|
3326
|
-
// report is itself a question, not a verified outcome. No validation
|
|
3327
|
-
// prompt is enqueued here: that fires only for the terminal
|
|
3328
|
-
// completed/failed path (notifyOriginatingTab); needs_review already
|
|
3329
|
-
// routes its own question via this very report.
|
|
3330
|
-
return await appendResponseEvent(job.cwd, epicId, message, {
|
|
3331
|
-
prdSlug: job.slug,
|
|
3332
|
-
outcome: 'needs_review',
|
|
3333
|
-
validation: 'unvalidated',
|
|
3334
|
-
});
|
|
3335
|
-
} catch (e) {
|
|
3336
|
-
console.error('[scheduler] notifyNeedsReview error', job?.slug, e);
|
|
3337
|
-
return false;
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
|
|
3341
3281
|
// Single-flight guard for flushDueReviewNotices — same shape as
|
|
3342
3282
|
// gateShadowPending above: a module-level flag rather than a class, so two
|
|
3343
3283
|
// triggers landing close together (an auto-resolve skip right next to the
|
|
@@ -3348,8 +3288,7 @@ let reviewNoticeFlushPending = null;
|
|
|
3348
3288
|
/**
|
|
3349
3289
|
* flushDueReviewNotices() → Promise<void>
|
|
3350
3290
|
*
|
|
3351
|
-
* The only path that still sends a needs_review notice
|
|
3352
|
-
* SM_REVIEW_NOTICE_IMMEDIATE=1 kill switch above notifyNeedsReview). Reads
|
|
3291
|
+
* The only path that still sends a needs_review notice. Reads
|
|
3353
3292
|
* the live queue, asks reviewNotice.cjs's selectDueReviewNotices which (cwd,
|
|
3354
3293
|
* Epic, cause) groups have gone quiet — the ladder gave up
|
|
3355
3294
|
* (needsReviewAutoResolvedSkip) or the hold time passed (holdMsFromEnv) —
|
|
@@ -4234,10 +4173,9 @@ function pickRunDir() {
|
|
|
4234
4173
|
// process. It never throws and never blocks or fails a spawn: a failed
|
|
4235
4174
|
// install just logs one line and leaves the PATH addition pointing at a shim
|
|
4236
4175
|
// dir that may have nothing in it yet, no worse than today's
|
|
4237
|
-
// no-shim-at-all.
|
|
4176
|
+
// no-shim-at-all.
|
|
4238
4177
|
let timeoutShimEnsured = null;
|
|
4239
4178
|
function ensureTimeoutShimOnce() {
|
|
4240
|
-
if (process.env.SM_TIMEOUT_SHIM_DISABLE === '1') return Promise.resolve();
|
|
4241
4179
|
if (!timeoutShimEnsured) {
|
|
4242
4180
|
timeoutShimEnsured = ensureTimeoutShim().then(
|
|
4243
4181
|
(result) => {
|
|
@@ -4541,11 +4479,8 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
|
|
|
4541
4479
|
// `launchEnv` is the launch circuit breaker's degraded-mode env (e.g.
|
|
4542
4480
|
// MAX_THINKING_TOKENS=0 while an outdated CLI's thinking parameter is
|
|
4543
4481
|
// being rejected — lib/scheduler/launchFailure.cjs); applied last so it wins.
|
|
4544
|
-
// SM_TIMEOUT_SHIM_DISABLE=1 is the kill switch for the PATH addition
|
|
4545
|
-
// below — paired with the install skip inside ensureTimeoutShimOnce().
|
|
4546
|
-
const timeoutShimEnabled = process.env.SM_TIMEOUT_SHIM_DISABLE !== '1';
|
|
4547
4482
|
const childEnv = cleanChildEnv({
|
|
4548
|
-
PATH:
|
|
4483
|
+
PATH: withTimeoutShimOnPath(pathWithUserBins()),
|
|
4549
4484
|
SM_PROJECT_ROOT: cwd,
|
|
4550
4485
|
SM_SCHEDULER_JOB_SLUG: job.slug,
|
|
4551
4486
|
SM_SCHEDULER_JOB_MAY_QUEUE: job.agentType === 'architect' ? '1' : '0',
|
|
@@ -5272,11 +5207,8 @@ async function spawnInvestigation(failedJob, runDir, { deadChild = null } = {})
|
|
|
5272
5207
|
await ensureCliCapsProbed();
|
|
5273
5208
|
await ensureTimeoutShimOnce();
|
|
5274
5209
|
const claudeBin = resolveClaudeBin();
|
|
5275
|
-
// SM_TIMEOUT_SHIM_DISABLE=1 is the kill switch for the PATH addition below —
|
|
5276
|
-
// paired with the install skip inside ensureTimeoutShimOnce().
|
|
5277
|
-
const timeoutShimEnabled = process.env.SM_TIMEOUT_SHIM_DISABLE !== '1';
|
|
5278
5210
|
const childEnv = cleanChildEnv({
|
|
5279
|
-
PATH:
|
|
5211
|
+
PATH: withTimeoutShimOnPath(pathWithUserBins()), // Homebrew/user bins for macOS
|
|
5280
5212
|
BASH_DEFAULT_TIMEOUT_MS: String(BASH_DEFAULT_TIMEOUT_MS),
|
|
5281
5213
|
BASH_MAX_TIMEOUT_MS: String(BASH_MAX_TIMEOUT_MS),
|
|
5282
5214
|
});
|
|
@@ -6620,6 +6552,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6620
6552
|
// Determine effective status, applying the verifier verdict for exit=0 runs.
|
|
6621
6553
|
let effectiveStatus;
|
|
6622
6554
|
let sigtermOverrideReason = null;
|
|
6555
|
+
let capParkVerdict = null; // set only when the re-fire cap parks the job
|
|
6623
6556
|
// Wall-clock budget kill — checked FIRST and unconditionally wins:
|
|
6624
6557
|
// never 'failed', never silently 'completed', and (via
|
|
6625
6558
|
// sigtermLandedCommitEvidence/plainExitLandedCommitEvidence, whichever
|
|
@@ -6707,9 +6640,18 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6707
6640
|
) {
|
|
6708
6641
|
effectiveStatus = 'completed';
|
|
6709
6642
|
} else if (verifyResult.downgradeTo === 'pending') {
|
|
6710
|
-
// HALT or deps_unmet:
|
|
6711
|
-
|
|
6712
|
-
|
|
6643
|
+
// HALT or deps_unmet: re-fire, bounded by decideVerifyDowngrade.
|
|
6644
|
+
const downgrade = decideVerifyDowngrade(s.jobs[i2], verifyResult);
|
|
6645
|
+
if (downgrade.action === 'refire') {
|
|
6646
|
+
if (resetJobFields(s.jobs[i2], verifyResult.reason, { source: 'spawnJob:verify-downgrade' })) {
|
|
6647
|
+
s.jobs[i2].verifyRefireCount = downgrade.nextCount;
|
|
6648
|
+
}
|
|
6649
|
+
return; // job already mutated by resetJobFields; skip the rest
|
|
6650
|
+
}
|
|
6651
|
+
// Cap hit: park through the existing needs_review stamping path.
|
|
6652
|
+
effectiveStatus = 'needs_review';
|
|
6653
|
+
capParkVerdict = downgrade.verdict;
|
|
6654
|
+
sigtermOverrideReason = downgrade.reason;
|
|
6713
6655
|
} else {
|
|
6714
6656
|
// transcript_errors or verify_unavailable: escalate to needs_review.
|
|
6715
6657
|
effectiveStatus = 'needs_review';
|
|
@@ -6728,7 +6670,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6728
6670
|
// needs_review ledger stale/leftover values from before this run,
|
|
6729
6671
|
// defeating its whole `byReason` rollup for the two escalation
|
|
6730
6672
|
// paths that land here.
|
|
6731
|
-
s.jobs[i2].error =
|
|
6673
|
+
s.jobs[i2].error = capParkVerdict
|
|
6674
|
+
? sigtermOverrideReason
|
|
6675
|
+
: (effectiveStatus === 'needs_review' || s.jobs[i2].blockedByForeignWip === true)
|
|
6732
6676
|
? (verifyResult?.reason ?? sigtermOverrideReason ?? null)
|
|
6733
6677
|
// A failed job (non-zero exit) never consults verifyResult above,
|
|
6734
6678
|
// but a worktree integration failure is still worth surfacing on
|
|
@@ -6748,11 +6692,16 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6748
6692
|
// 'blocked_by_foreign_wip_streak'/'budget_exceeded' are set above
|
|
6749
6693
|
// from the sigterm/exit-code path, never from verifyResult (which
|
|
6750
6694
|
// stays null on a non-zero exit) — never clobber either here.
|
|
6751
|
-
if (
|
|
6695
|
+
if (capParkVerdict) {
|
|
6696
|
+
s.jobs[i2].verifierVerdict = capParkVerdict;
|
|
6697
|
+
} else if (verifyResult?.verdict && verifyResult.verdict !== 'clean') {
|
|
6752
6698
|
s.jobs[i2].verifierVerdict = verifyResult.verdict;
|
|
6753
6699
|
} else if (!['blocked_by_foreign_wip_streak', 'budget_exceeded'].includes(s.jobs[i2].verifierVerdict)) {
|
|
6754
6700
|
delete s.jobs[i2].verifierVerdict;
|
|
6755
6701
|
}
|
|
6702
|
+
// Only a downgrade-to-pending (which returned above) keeps the count;
|
|
6703
|
+
// every other finalize outcome, including the cap park, clears it.
|
|
6704
|
+
delete s.jobs[i2].verifyRefireCount;
|
|
6756
6705
|
if (verifyResult?.verdict === 'needs_human') {
|
|
6757
6706
|
s.jobs[i2].humanQuestion = verifyResult.humanQuestion || '';
|
|
6758
6707
|
} else {
|
|
@@ -6996,7 +6945,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
6996
6945
|
annotations: needsReviewRcaSnapshot.verifierAnnotations,
|
|
6997
6946
|
})
|
|
6998
6947
|
.then(async (report) => {
|
|
6999
|
-
// Resolve the authoring Epic
|
|
6948
|
+
// Resolve the authoring Epic (sourcePromptId, else the job epicId) —
|
|
7000
6949
|
// the review notice needs the same epicId so flushDueReviewNotices
|
|
7001
6950
|
// can route the eventual grouped message to the right place.
|
|
7002
6951
|
let epicId = null;
|
|
@@ -7011,7 +6960,6 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
7011
6960
|
// classification (e.g. selectAutoFixTargets excluding 'archive')
|
|
7012
6961
|
// and flushDueReviewNotices routes on the notice, neither re-parsing
|
|
7013
6962
|
// the RCA markdown nor losing the notice to a lost race.
|
|
7014
|
-
let builtNotice = null;
|
|
7015
6963
|
await mutate((s) => {
|
|
7016
6964
|
const j = s.jobs.find((x) => x.slug === needsReviewRcaSnapshot.slug);
|
|
7017
6965
|
applyRcaClassification(j, report);
|
|
@@ -7021,31 +6969,13 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
|
|
|
7021
6969
|
// lands, must not get a stale notice grafted back onto it.
|
|
7022
6970
|
if (j && j.status === 'needs_review') {
|
|
7023
6971
|
j.reviewNotice = buildReviewNotice({ job: j, report, epicId, now: new Date().toISOString(), prior: j.reviewNotice });
|
|
7024
|
-
builtNotice = j.reviewNotice;
|
|
7025
6972
|
}
|
|
7026
6973
|
}).catch((e) => {
|
|
7027
6974
|
console.error('[scheduler] writeRcaReport reviewNotice mutate error', needsReviewRcaSnapshot.slug, e);
|
|
7028
6975
|
});
|
|
7029
|
-
// Quiet by default
|
|
6976
|
+
// Quiet by default: the notice recorded above waits for
|
|
7030
6977
|
// flushDueReviewNotices to send it, grouped, once the ladder gives
|
|
7031
|
-
// up or the hold time passes.
|
|
7032
|
-
// the old immediate-notify behavior, for local debugging.
|
|
7033
|
-
if (process.env.SM_REVIEW_NOTICE_IMMEDIATE === '1') {
|
|
7034
|
-
const sent = await notifyNeedsReview(needsReviewRcaSnapshot, report);
|
|
7035
|
-
if (sent && builtNotice) {
|
|
7036
|
-
await mutate((s) => {
|
|
7037
|
-
const j = s.jobs.find((x) => x.slug === needsReviewRcaSnapshot.slug);
|
|
7038
|
-
// Stamp only if the live notice is still the exact one just
|
|
7039
|
-
// sent — a re-park between the send and this mutate must not
|
|
7040
|
-
// be marked sent for a notice that never went out.
|
|
7041
|
-
if (j && j.reviewNotice && j.reviewNotice.firstParkedAt === builtNotice.firstParkedAt && j.reviewNotice.cause === builtNotice.cause) {
|
|
7042
|
-
j.reviewNotice.sentAt = new Date().toISOString();
|
|
7043
|
-
}
|
|
7044
|
-
}).catch((e) => {
|
|
7045
|
-
console.error('[scheduler] writeRcaReport immediate-stamp mutate error', needsReviewRcaSnapshot.slug, e);
|
|
7046
|
-
});
|
|
7047
|
-
}
|
|
7048
|
-
}
|
|
6978
|
+
// up or the hold time passes.
|
|
7049
6979
|
})
|
|
7050
6980
|
.catch((e) => {
|
|
7051
6981
|
console.error('[scheduler] writeRcaReport error', job.slug, e);
|
|
@@ -7586,8 +7516,6 @@ async function runDueJobs({ bypassLoadGate = false } = {}) {
|
|
|
7586
7516
|
}
|
|
7587
7517
|
cancelToken = { cancelled: false };
|
|
7588
7518
|
const result = await tickQueue({ bypassLoadGate });
|
|
7589
|
-
// Clear the one-shot scheduledFor without waiting for jobs to settle.
|
|
7590
|
-
await mutate((s) => { s.scheduledFor = null; });
|
|
7591
7519
|
await broadcast();
|
|
7592
7520
|
return result;
|
|
7593
7521
|
}
|
|
@@ -8997,7 +8925,7 @@ async function runGateShadow(job) {
|
|
|
8997
8925
|
if (fs.existsSync(path.dirname(verdictsPath))) {
|
|
8998
8926
|
let existing = {};
|
|
8999
8927
|
try { existing = JSON.parse(fs.readFileSync(verdictsPath, 'utf8')) || {}; } catch { /* absent/unparseable → fresh */ }
|
|
9000
|
-
try {
|
|
8928
|
+
try { config.writeJsonSync(verdictsPath, { ...existing, gateShadow }); } catch { /* best-effort */ }
|
|
9001
8929
|
}
|
|
9002
8930
|
}
|
|
9003
8931
|
|
|
@@ -9213,7 +9141,7 @@ async function reverifyNeedsReview() {
|
|
|
9213
9141
|
// pass starting before this one's pick resolves could choose a target of
|
|
9214
9142
|
// its own too, breaking the single-flight rule.
|
|
9215
9143
|
let pick = null;
|
|
9216
|
-
if (!gateShadowPending
|
|
9144
|
+
if (!gateShadowPending) {
|
|
9217
9145
|
pick = (async () => {
|
|
9218
9146
|
const needsReview = snap.jobs.filter((j) => j.status === 'needs_review');
|
|
9219
9147
|
const headByCwd = new Map();
|
|
@@ -9931,12 +9859,12 @@ function rescheduleIntervalTick() {
|
|
|
9931
9859
|
// auto-clears WITHOUT waiting for the next app restart. Cheap-guarded by
|
|
9932
9860
|
// shouldRunPeriodicReverify, which reuses isRescanCandidate so the guard
|
|
9933
9861
|
// and the candidate filter can never drift apart again (they did once —
|
|
9934
|
-
// see that function's comment).
|
|
9935
|
-
//
|
|
9862
|
+
// see that function's comment). Test seam:
|
|
9863
|
+
// _setPeriodicReverifyEnabledForTests(false) (boot reverify above stays always-on).
|
|
9936
9864
|
// reverifyNeedsReview's auto-fix loop is capped downstream by
|
|
9937
9865
|
// MAX_CONCURRENT_INVESTIGATIONS (spawnInvestigation queues/early-returns
|
|
9938
9866
|
// past it), so this interval firing cannot fan out investigations.
|
|
9939
|
-
if (
|
|
9867
|
+
if (periodicReverifyEnabled) {
|
|
9940
9868
|
if (reviewLadder.shouldRunPeriodicReverify(s.jobs)) {
|
|
9941
9869
|
reverifyNeedsReview().catch(() => {});
|
|
9942
9870
|
}
|
|
@@ -10149,6 +10077,13 @@ function rescheduleIntervalTick() {
|
|
|
10149
10077
|
}
|
|
10150
10078
|
}
|
|
10151
10079
|
|
|
10080
|
+
// Test-only seam: the periodic reverify pass is on in production; tests turn it
|
|
10081
|
+
// off to isolate themselves from the unrelated reverify/broadcast side effects.
|
|
10082
|
+
let periodicReverifyEnabled = true;
|
|
10083
|
+
function _setPeriodicReverifyEnabledForTests(enabled) {
|
|
10084
|
+
periodicReverifyEnabled = enabled !== false;
|
|
10085
|
+
}
|
|
10086
|
+
|
|
10152
10087
|
async function init() {
|
|
10153
10088
|
ensureDirs();
|
|
10154
10089
|
// Boot phase — reconciliation, migrations, self-heal, first reset probe.
|
|
@@ -10167,16 +10102,6 @@ async function init() {
|
|
|
10167
10102
|
// Boot-time expiry pass (process-local state is empty after a restart, so
|
|
10168
10103
|
// this is a cheap belt-and-braces run against the freshly read queue).
|
|
10169
10104
|
try { runReservationExpiryPass((await readQueue()).jobs); } catch { /* best-effort */ }
|
|
10170
|
-
// Retire the global queue.json: split its rows into per-project shards
|
|
10171
|
-
// BEFORE the first read below, so boot reconciliation sees the shards.
|
|
10172
|
-
try {
|
|
10173
|
-
const m = await queueStore.migrateLegacyGlobalQueue(DEFAULT_PROJECT_CWD);
|
|
10174
|
-
if (m.migrated) {
|
|
10175
|
-
console.log(`[scheduler] legacy global queue retired: ${m.moved} row(s) split across ${m.projects} project shard(s)`);
|
|
10176
|
-
}
|
|
10177
|
-
} catch (e) {
|
|
10178
|
-
console.error('[scheduler] legacy queue split failed', e?.message);
|
|
10179
|
-
}
|
|
10180
10105
|
await prdPaths.runPrdMigration();
|
|
10181
10106
|
prdPaths.sweepQueueBackups().catch((e) => console.warn('[scheduler] backup sweep failed', e?.message));
|
|
10182
10107
|
|
|
@@ -10273,7 +10198,7 @@ async function init() {
|
|
|
10273
10198
|
const bootLandedCommitEvidence = new Map();
|
|
10274
10199
|
await Promise.all(bootSnap.jobs.map(async (j) => {
|
|
10275
10200
|
if (!immediateSlugs.includes(j.slug) || j.status !== 'running') return;
|
|
10276
|
-
if (
|
|
10201
|
+
if (!j.landedCommit) return;
|
|
10277
10202
|
const resolved = await resolveLandedCommitEvidence(j.cwd || DEFAULT_PROJECT_CWD, j.landedCommit, j.startedAt);
|
|
10278
10203
|
if (resolved) bootLandedCommitEvidence.set(j.slug, j.landedCommit);
|
|
10279
10204
|
}));
|
|
@@ -11100,6 +11025,7 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
11100
11025
|
}
|
|
11101
11026
|
|
|
11102
11027
|
module.exports = {
|
|
11028
|
+
normalizeConfig,
|
|
11103
11029
|
reportSchedulerError,
|
|
11104
11030
|
finalizeJobWorktree,
|
|
11105
11031
|
worktreeIntegrationVerdict,
|
|
@@ -11117,6 +11043,7 @@ module.exports = {
|
|
|
11117
11043
|
getSchedulerStateSnapshot,
|
|
11118
11044
|
computeDegradedBudget,
|
|
11119
11045
|
writeQueue,
|
|
11046
|
+
_setPeriodicReverifyEnabledForTests,
|
|
11120
11047
|
_mutateForTests: mutate, // mutate() has no other exported call site; test-only seam.
|
|
11121
11048
|
reconcile,
|
|
11122
11049
|
broadcast,
|
|
@@ -11175,7 +11102,6 @@ module.exports = {
|
|
|
11175
11102
|
applyOrphanOutcome,
|
|
11176
11103
|
registerAdminRoutes,
|
|
11177
11104
|
notifyOriginatingTab,
|
|
11178
|
-
notifyNeedsReview,
|
|
11179
11105
|
flushDueReviewNotices,
|
|
11180
11106
|
isNotifiableTerminalStatus,
|
|
11181
11107
|
extractResultTextFromLog,
|