@indigoai-us/hq-cli 5.301.0 → 5.303.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/CHANGELOG.md +26 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
- package/assets/scaffold/core/scripts/token-usage-report.sh +72 -20
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/bot.js +1 -0
- package/dist/commands/core-checkpoint.js +1 -0
- package/dist/commands/core-handoff-fast.js +1 -0
- package/dist/commands/core-handoff.js +2 -0
- package/dist/commands/deploy-comments.js +5 -2
- package/dist/commands/integrations-connect.d.ts +3 -0
- package/dist/commands/integrations-connect.js +21 -2
- package/dist/commands/lanes-message.js +33 -3
- package/dist/commands/lanes-questions.d.ts +4 -2
- package/dist/commands/lanes-questions.js +123 -2
- package/dist/commands/lanes-resume.js +25 -4
- package/dist/commands/lanes-shared.d.ts +13 -1
- package/dist/commands/lanes-watch.js +5 -1
- package/dist/commands/lanes.js +91 -9
- package/dist/commands/monitor.js +17 -2
- package/dist/commands/report.js +5 -2
- package/dist/commands/rescue.d.ts +4 -0
- package/dist/commands/rescue.js +27 -17
- package/dist/lib/agent-kit/fallback.d.ts +1 -0
- package/dist/lib/agent-kit/fallback.js +1 -0
- package/dist/lib/bot/cli-update.d.ts +0 -8
- package/dist/lib/bot/cli-update.js +5 -5
- package/dist/lib/bot/daemon.d.ts +5 -1
- package/dist/lib/bot/daemon.js +4 -4
- package/dist/lib/bot/local-jobs.d.ts +1 -21
- package/dist/lib/bot/local-jobs.js +12 -12
- package/dist/lib/core-utils/soft-timeout.js +1 -1
- package/dist/lib/daemon/config.d.ts +0 -1
- package/dist/lib/daemon/config.js +1 -1
- package/dist/lib/deploy-apps.d.ts +13 -0
- package/dist/lib/deploy-apps.js +24 -0
- package/dist/lib/index-render/projects.js +82 -5
- package/dist/lib/lanes/delivery.d.ts +7 -3
- package/dist/lib/lanes/delivery.js +18 -8
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +13 -3
- package/dist/lib/lanes/keep-alive.js +29 -5
- package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
- package/dist/lib/lanes/outpost-dispatch.js +12 -0
- package/dist/lib/lanes/reconcile.d.ts +1 -0
- package/dist/lib/lanes/reconcile.js +358 -38
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +20 -10
- package/dist/lib/lanes/store.js +24 -0
- package/dist/lib/lanes/story-ledger.d.ts +2 -0
- package/dist/lib/lanes/story-ledger.js +8 -0
- package/dist/lib/lanes/types.d.ts +26 -4
- package/dist/lib/lanes/types.js +33 -1
- package/dist/lib/lanes/watch.d.ts +1 -0
- package/dist/lib/lanes/watch.js +19 -6
- package/dist/lib/lanes/watcher.js +139 -9
- package/dist/lib/mesh/live/daemon/run.js +36 -2
- package/dist/lib/report-snapshot.d.ts +0 -2
- package/dist/lib/report-snapshot.js +2 -2
- package/dist/lib/search-index/background.d.ts +1 -0
- package/dist/lib/search-index/background.js +1 -0
- package/dist/utils/run-bundled-script.js +103 -1
- package/dist/utils/update-command-supervisor.cjs +1 -0
- package/dist/utils/version-gate.js +1 -1
- package/package.json +1 -1
|
@@ -18,14 +18,14 @@ import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./err
|
|
|
18
18
|
import { loadWorkerProfile } from "./profile.js";
|
|
19
19
|
import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
20
20
|
import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
|
|
21
|
-
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
21
|
+
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
22
22
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
|
|
23
23
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
24
|
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
25
25
|
import { resolveLaneCompany } from "./lane-company.js";
|
|
26
26
|
import { appendDropboxMessage } from "./dropbox.js";
|
|
27
27
|
import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
|
|
28
|
-
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
28
|
+
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
29
29
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
30
30
|
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
31
31
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
@@ -33,6 +33,7 @@ import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingEx
|
|
|
33
33
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshCompany, } from "./mesh-events.js";
|
|
34
34
|
import { activeLaneStories, laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
|
|
35
35
|
import { applyStorySwitchEnvelope } from "./story-switch.js";
|
|
36
|
+
import { readProjectStoryStates } from "./story-ledger.js";
|
|
36
37
|
import { decideEnvelopeRepair, envelopeQuestionIds, routeProviderFailure } from "./watcher.js";
|
|
37
38
|
import { PROVIDER_REJECTED_REASON, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
38
39
|
/** Spec §9: unanswered at the current hop for 60 minutes → one hop. */
|
|
@@ -46,6 +47,8 @@ export const RECONCILE_LOCK_TIMEOUT_MS = 5_000;
|
|
|
46
47
|
/** A pickup probe that has not cleared its marker within this window is abandoned. */
|
|
47
48
|
export const PICKUP_VALIDATION_MAX_AGE_MS = 15 * 60 * 1000;
|
|
48
49
|
export const PICKUP_VALIDATION_TIMEOUT_REASON = "pickup_validation_timeout";
|
|
50
|
+
export const CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG = "lanes.close-interrupted-done-story-workers";
|
|
51
|
+
const INTERRUPTED_DONE_STORY_MIN_AGE_MS = 24 * 60 * 60 * 1000;
|
|
49
52
|
export const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
|
|
50
53
|
export const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
|
|
51
54
|
function spawnDepsForHost(deps) {
|
|
@@ -324,6 +327,129 @@ function casLaneIf(hqRoot, laneId, shouldWrite, updater, dryRun) {
|
|
|
324
327
|
}
|
|
325
328
|
throw new LanesError("version_conflict", `Exhausted CAS retries for lane ${laneId}`, { lane_id: laneId });
|
|
326
329
|
}
|
|
330
|
+
function laneStoryLedgerIsClosed(hqRoot, lane) {
|
|
331
|
+
const stories = laneStories(lane);
|
|
332
|
+
if (stories.length === 0)
|
|
333
|
+
return { closed: false, states: [] };
|
|
334
|
+
const states = [];
|
|
335
|
+
for (const projectId of new Set(stories.map((story) => story.project_id))) {
|
|
336
|
+
const projectStories = stories.filter((story) => story.project_id === projectId);
|
|
337
|
+
const ledgerStates = readProjectStoryStates(hqRoot, projectId, projectStories.map((story) => story.story_id), lane.company?.slug);
|
|
338
|
+
for (const story of projectStories) {
|
|
339
|
+
const state = ledgerStates.get(story.story_id);
|
|
340
|
+
if (state === undefined)
|
|
341
|
+
return { closed: false, states: [...states, "missing"] };
|
|
342
|
+
states.push(state);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { closed: states.length > 0 && states.every((state) => state === "done" || state === "dropped"), states };
|
|
346
|
+
}
|
|
347
|
+
async function closeInterruptedDoneStoryWorkers(hqRoot, lanes, questions, summary, deps, now, enabledByCompany) {
|
|
348
|
+
if (deps.dryRun)
|
|
349
|
+
return;
|
|
350
|
+
const nowMs = now.getTime();
|
|
351
|
+
const at = now.toISOString();
|
|
352
|
+
for (const lane of lanes) {
|
|
353
|
+
if (lane.depth < 1 || lane.state !== "interrupted" || lane.keep_alive === true ||
|
|
354
|
+
!laneRunsHere(lane, deps.host) || !lane.interrupted_at ||
|
|
355
|
+
!Number.isFinite(Date.parse(lane.interrupted_at)) ||
|
|
356
|
+
nowMs - Date.parse(lane.interrupted_at) < INTERRUPTED_DONE_STORY_MIN_AGE_MS ||
|
|
357
|
+
hasPendingKeepAliveWork(hqRoot, lane, questions))
|
|
358
|
+
continue;
|
|
359
|
+
let company;
|
|
360
|
+
try {
|
|
361
|
+
company = resolveLaneCompany(hqRoot, lane).company;
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close company lookup failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (!company || enabledByCompany.get(keepAliveCompanyKey(company)) !== true)
|
|
368
|
+
continue;
|
|
369
|
+
let ledger;
|
|
370
|
+
try {
|
|
371
|
+
ledger = laneStoryLedgerIsClosed(hqRoot, lane);
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close ledger read failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (!ledger.closed)
|
|
378
|
+
continue;
|
|
379
|
+
try {
|
|
380
|
+
const result = casLaneIf(hqRoot, lane.lane_id, (current) => {
|
|
381
|
+
const currentCompany = resolveLaneCompany(hqRoot, current).company;
|
|
382
|
+
if (current.state !== "interrupted" || current.depth < 1 || current.keep_alive === true ||
|
|
383
|
+
!currentCompany || enabledByCompany.get(keepAliveCompanyKey(currentCompany)) !== true ||
|
|
384
|
+
!laneRunsHere(current, deps.host) || !current.interrupted_at ||
|
|
385
|
+
!Number.isFinite(Date.parse(current.interrupted_at)) ||
|
|
386
|
+
nowMs - Date.parse(current.interrupted_at) < INTERRUPTED_DONE_STORY_MIN_AGE_MS ||
|
|
387
|
+
hasPendingKeepAliveWork(hqRoot, current, questions))
|
|
388
|
+
return false;
|
|
389
|
+
return laneStoryLedgerIsClosed(hqRoot, current).closed;
|
|
390
|
+
}, (current) => ({
|
|
391
|
+
...current,
|
|
392
|
+
state: "killed",
|
|
393
|
+
queued_reason: undefined,
|
|
394
|
+
queued_details: undefined,
|
|
395
|
+
updated_at: at,
|
|
396
|
+
terminal_reason: "story_closed",
|
|
397
|
+
terminal_details: { ...(current.terminal_details ?? {}), operator_reason: "story_closed" },
|
|
398
|
+
stop_history: [...(current.stop_history ?? []), { at, reason: "story_closed" }],
|
|
399
|
+
}), false);
|
|
400
|
+
if (!result.changed)
|
|
401
|
+
continue;
|
|
402
|
+
stopLaneProcesses(hqRoot, lane.lane_id, {
|
|
403
|
+
killProcess: deps.spawnLaneDeps?.killProcess,
|
|
404
|
+
isAlive: deps.isAlive,
|
|
405
|
+
});
|
|
406
|
+
for (const question of questions) {
|
|
407
|
+
if (question.origin.id !== lane.lane_id || question.status !== "pending")
|
|
408
|
+
continue;
|
|
409
|
+
casQuestionIf(hqRoot, question.question_id, (current) => current.status === "pending", (current) => ({
|
|
410
|
+
...current,
|
|
411
|
+
status: "cancelled",
|
|
412
|
+
routing_history: appendCappedHistory(current.routing_history, {
|
|
413
|
+
hop: current.hop,
|
|
414
|
+
audience: { kind: "user" },
|
|
415
|
+
at,
|
|
416
|
+
reason: "cancelled",
|
|
417
|
+
}),
|
|
418
|
+
}), false);
|
|
419
|
+
}
|
|
420
|
+
appendWatcherEvent(watcherEventsPath(hqRoot, lane.lane_id), {
|
|
421
|
+
event: "story_closed",
|
|
422
|
+
at,
|
|
423
|
+
lane_id: lane.lane_id,
|
|
424
|
+
story_state: [...new Set(ledger.states)].join(","),
|
|
425
|
+
terminal_reason: "story_closed",
|
|
426
|
+
});
|
|
427
|
+
const mesh = deps.mesh === undefined ? createLaneMesh({ interactive: false }) : deps.mesh;
|
|
428
|
+
if (mesh) {
|
|
429
|
+
const timeoutMs = deps.meshEmitTimeoutMs ?? 10_000;
|
|
430
|
+
let timer;
|
|
431
|
+
try {
|
|
432
|
+
await Promise.race([
|
|
433
|
+
emitLaneTerminalStateEvent(mesh, result.record, "killed", "story_closed"),
|
|
434
|
+
new Promise((_resolve, reject) => {
|
|
435
|
+
timer = setTimeout(() => reject(new Error(`terminal mesh emit timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
436
|
+
}),
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close mesh emit failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
441
|
+
}
|
|
442
|
+
finally {
|
|
443
|
+
if (timer)
|
|
444
|
+
clearTimeout(timer);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
327
453
|
function writeQueueReason(hqRoot, lane, reason, dryRun) {
|
|
328
454
|
if (lane.queued_reason === reason)
|
|
329
455
|
return;
|
|
@@ -929,7 +1055,39 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
929
1055
|
}
|
|
930
1056
|
}
|
|
931
1057
|
const readRegistryFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
1058
|
+
const storyCloseCompanies = new Map();
|
|
1059
|
+
for (const lane of lanes) {
|
|
1060
|
+
if (lane.depth < 1 || lane.state !== "interrupted" || lane.keep_alive === true ||
|
|
1061
|
+
!laneRunsHere(lane, deps.host) || !lane.interrupted_at ||
|
|
1062
|
+
!Number.isFinite(Date.parse(lane.interrupted_at)) ||
|
|
1063
|
+
nowMs - Date.parse(lane.interrupted_at) < INTERRUPTED_DONE_STORY_MIN_AGE_MS ||
|
|
1064
|
+
hasPendingKeepAliveWork(hqRoot, lane, questions) || laneStories(lane).length === 0)
|
|
1065
|
+
continue;
|
|
1066
|
+
try {
|
|
1067
|
+
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
1068
|
+
if (company)
|
|
1069
|
+
storyCloseCompanies.set(keepAliveCompanyKey(company), company);
|
|
1070
|
+
}
|
|
1071
|
+
catch (err) {
|
|
1072
|
+
markDirty(summary, { kind: "lane", id: lane.lane_id, why: `story close company lookup failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
const storyCloseEnabledByCompany = new Map();
|
|
1076
|
+
const env = deps.env ?? process.env;
|
|
1077
|
+
await Promise.all([...storyCloseCompanies.entries()].map(async ([key, company]) => {
|
|
1078
|
+
try {
|
|
1079
|
+
storyCloseEnabledByCompany.set(key, await readRegistryFlag(CLOSE_INTERRUPTED_DONE_STORY_WORKERS_FLAG, {
|
|
1080
|
+
company: company ?? undefined,
|
|
1081
|
+
env,
|
|
1082
|
+
}));
|
|
1083
|
+
}
|
|
1084
|
+
catch (err) {
|
|
1085
|
+
storyCloseEnabledByCompany.set(key, false);
|
|
1086
|
+
markDirty(summary, { kind: "lane", why: `story close flag read failed for ${company?.slug ?? "unresolved company"}: ${err instanceof Error ? err.message : String(err)}` });
|
|
1087
|
+
}
|
|
1088
|
+
}));
|
|
932
1089
|
const keepAliveFlagCompanies = new Map();
|
|
1090
|
+
const nextCheckAtFlagCompanyKeys = new Set();
|
|
933
1091
|
for (const lane of lanes) {
|
|
934
1092
|
if (!(lane.state === "done" || lane.state === "interrupted" || lane.state === "blocked" || lane.state === "killed" || lane.state === "cancelled" || lane.state === "failed") ||
|
|
935
1093
|
(lane.keep_alive !== true && lane.worker_profile === undefined) ||
|
|
@@ -940,6 +1098,7 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
940
1098
|
!providerSupportsSessionResume(lane.provider) ||
|
|
941
1099
|
!(lane.provider_session_id ?? lane.last_resume?.session_id))))
|
|
942
1100
|
continue;
|
|
1101
|
+
let hasNextCheckAt = false;
|
|
943
1102
|
if (lane.state === "done") {
|
|
944
1103
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
945
1104
|
if (!envelope || envelope.decision !== "done")
|
|
@@ -947,17 +1106,20 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
947
1106
|
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(hqRoot, lane, questions);
|
|
948
1107
|
if (doneKeepAliveVerdict(lane, envelope, hasQueuedWork) !== "eligible")
|
|
949
1108
|
continue;
|
|
1109
|
+
hasNextCheckAt = envelope.next_check_at !== undefined;
|
|
950
1110
|
}
|
|
951
1111
|
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
952
1112
|
if (company === null)
|
|
953
1113
|
continue;
|
|
954
1114
|
const key = keepAliveCompanyKey(company);
|
|
955
1115
|
keepAliveFlagCompanies.set(key, company);
|
|
1116
|
+
if (hasNextCheckAt)
|
|
1117
|
+
nextCheckAtFlagCompanyKeys.add(key);
|
|
956
1118
|
}
|
|
957
1119
|
const keepAliveFlagByCompany = new Map();
|
|
1120
|
+
const nextCheckAtFlagByCompany = new Map();
|
|
958
1121
|
const blockedResumeFlagByCompany = new Map();
|
|
959
1122
|
const supersessionLineageAwareByCompany = new Map();
|
|
960
|
-
const env = deps.env ?? process.env;
|
|
961
1123
|
await Promise.all([...keepAliveFlagCompanies.entries()].map(async ([key, company]) => {
|
|
962
1124
|
const options = {
|
|
963
1125
|
company: company ?? undefined,
|
|
@@ -967,14 +1129,18 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
967
1129
|
lane.keep_alive === true &&
|
|
968
1130
|
laneRunsHere(lane, startDeps.host) &&
|
|
969
1131
|
keepAliveCompanyKey(resolveLaneCompany(hqRoot, lane).company) === key);
|
|
970
|
-
const [keepAliveEnabled, lineageAware, blockedResumeEnabled] = await Promise.all([
|
|
1132
|
+
const [keepAliveEnabled, nextCheckAtEnabled, lineageAware, blockedResumeEnabled] = await Promise.all([
|
|
971
1133
|
readRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, options),
|
|
1134
|
+
nextCheckAtFlagCompanyKeys.has(key)
|
|
1135
|
+
? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_FLAG, options)
|
|
1136
|
+
: Promise.resolve(false),
|
|
972
1137
|
readRegistryFlag(LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, options),
|
|
973
1138
|
hasBlockedKeepAliveLane
|
|
974
1139
|
? readRegistryFlag(LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, options)
|
|
975
1140
|
: Promise.resolve(false),
|
|
976
1141
|
]);
|
|
977
1142
|
keepAliveFlagByCompany.set(key, keepAliveEnabled);
|
|
1143
|
+
nextCheckAtFlagByCompany.set(key, nextCheckAtEnabled);
|
|
978
1144
|
blockedResumeFlagByCompany.set(key, blockedResumeEnabled);
|
|
979
1145
|
supersessionLineageAwareByCompany.set(key, lineageAware);
|
|
980
1146
|
}));
|
|
@@ -995,7 +1161,8 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
995
1161
|
await withAllLaneAdmissionLocks(hqRoot, async () => {
|
|
996
1162
|
const freshLanes = loadLaneRecords(hqRoot, summary);
|
|
997
1163
|
const freshQuestions = loadQuestionRecords(hqRoot, summary);
|
|
998
|
-
await
|
|
1164
|
+
await closeInterruptedDoneStoryWorkers(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, storyCloseEnabledByCompany);
|
|
1165
|
+
await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, nextCheckAtFlagByCompany);
|
|
999
1166
|
const drainLanes = loadLaneRecords(hqRoot, summary);
|
|
1000
1167
|
await admitQueued(hqRoot, drainLanes, summary, admissionDeps, now);
|
|
1001
1168
|
await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, admissionDeps);
|
|
@@ -1741,25 +1908,119 @@ function recordKeepAliveSkip(hqRoot, lane, reason, summary, deps, detail) {
|
|
|
1741
1908
|
summary.keep_alive_blocked.push({ id: lane.lane_id, reason });
|
|
1742
1909
|
process.stderr.write(`hq lanes reconcile: keep-alive blocked for lane ${lane.lane_id} (${reason}); ${keepAliveSkipGuidance(lane.lane_id, reason)}\n`);
|
|
1743
1910
|
}
|
|
1744
|
-
if (deps.dryRun
|
|
1911
|
+
if (deps.dryRun)
|
|
1745
1912
|
return;
|
|
1746
|
-
|
|
1747
|
-
|
|
1913
|
+
let laneVersion = lane.version;
|
|
1914
|
+
if (lane.keep_alive_blocked_reason !== reason) {
|
|
1915
|
+
try {
|
|
1916
|
+
const update = casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === lane.state, (current) => ({ ...current, keep_alive_blocked_reason: reason }), false);
|
|
1917
|
+
laneVersion = update.record.version;
|
|
1918
|
+
}
|
|
1919
|
+
catch (err) {
|
|
1920
|
+
markDirty(summary, {
|
|
1921
|
+
kind: "lane",
|
|
1922
|
+
id: lane.lane_id,
|
|
1923
|
+
why: `keep-alive skip state write failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1748
1926
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
kind: "lane",
|
|
1752
|
-
id: lane.lane_id,
|
|
1753
|
-
why: `keep-alive skip state write failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1754
|
-
});
|
|
1927
|
+
if (lane.keep_alive === true) {
|
|
1928
|
+
recordKeepAliveSkipEvent(hqRoot, lane, reason, laneVersion, summary, deps, detail);
|
|
1755
1929
|
}
|
|
1756
1930
|
}
|
|
1757
|
-
function recordDoneKeepAlivePolicySkip(summary, lane, verdict) {
|
|
1931
|
+
function recordDoneKeepAlivePolicySkip(hqRoot, summary, lane, verdict, deps) {
|
|
1758
1932
|
summary.skipped.push({
|
|
1759
1933
|
kind: "lane",
|
|
1760
1934
|
id: lane.lane_id,
|
|
1761
1935
|
why: `keep_alive_${verdict}`,
|
|
1762
1936
|
});
|
|
1937
|
+
if (lane.keep_alive === true && !deps.dryRun) {
|
|
1938
|
+
recordKeepAliveSkipEvent(hqRoot, lane, verdict, lane.version, summary, deps);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
function recordKeepAliveSkipEvent(hqRoot, lane, reason, laneVersion, summary, deps, detail) {
|
|
1942
|
+
if (lane.keep_alive !== true || deps.dryRun)
|
|
1943
|
+
return;
|
|
1944
|
+
const eventsPath = watcherEventsPath(hqRoot, lane.lane_id);
|
|
1945
|
+
try {
|
|
1946
|
+
let lastSkip;
|
|
1947
|
+
try {
|
|
1948
|
+
const scanBackwards = (body, targetReason, targetVersion) => {
|
|
1949
|
+
const lines = body.split(/\r?\n/);
|
|
1950
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1951
|
+
if (!lines[index])
|
|
1952
|
+
continue;
|
|
1953
|
+
try {
|
|
1954
|
+
const value = JSON.parse(lines[index]);
|
|
1955
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1956
|
+
const event = value;
|
|
1957
|
+
if (event.event === "keep_alive_skip" &&
|
|
1958
|
+
(targetReason === undefined || (event.reason === targetReason && event.lane_version === targetVersion))) {
|
|
1959
|
+
return event;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
catch {
|
|
1964
|
+
// A partial concurrent append must not prevent future skip events.
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
return undefined;
|
|
1968
|
+
};
|
|
1969
|
+
const maxTailBytes = 64 * 1024;
|
|
1970
|
+
const fd = fs.openSync(eventsPath, "r");
|
|
1971
|
+
let fileSize;
|
|
1972
|
+
let tail;
|
|
1973
|
+
let tailOffset;
|
|
1974
|
+
try {
|
|
1975
|
+
fileSize = fs.fstatSync(fd).size;
|
|
1976
|
+
tailOffset = Math.max(0, fileSize - maxTailBytes);
|
|
1977
|
+
const buffer = Buffer.alloc(fileSize - tailOffset);
|
|
1978
|
+
let bytesRead = 0;
|
|
1979
|
+
while (bytesRead < buffer.length) {
|
|
1980
|
+
const count = fs.readSync(fd, buffer, bytesRead, buffer.length - bytesRead, tailOffset + bytesRead);
|
|
1981
|
+
if (count === 0)
|
|
1982
|
+
break;
|
|
1983
|
+
bytesRead += count;
|
|
1984
|
+
}
|
|
1985
|
+
tail = buffer.subarray(0, bytesRead).toString("utf8");
|
|
1986
|
+
}
|
|
1987
|
+
finally {
|
|
1988
|
+
fs.closeSync(fd);
|
|
1989
|
+
}
|
|
1990
|
+
// When the bounded read starts mid-line, ignore that incomplete prefix.
|
|
1991
|
+
if (tailOffset > 0) {
|
|
1992
|
+
const firstNewline = tail.indexOf("\n");
|
|
1993
|
+
tail = firstNewline < 0 ? "" : tail.slice(firstNewline + 1);
|
|
1994
|
+
}
|
|
1995
|
+
lastSkip = scanBackwards(tail, reason === "next_check_at" ? reason : undefined, laneVersion);
|
|
1996
|
+
// For small logs the tail is not a complete record boundary if a single
|
|
1997
|
+
// record exceeded the bound, so allow one full-file scan as a fallback.
|
|
1998
|
+
if (!lastSkip && fileSize < maxTailBytes) {
|
|
1999
|
+
lastSkip = scanBackwards(fs.readFileSync(eventsPath, "utf8"), reason === "next_check_at" ? reason : undefined, laneVersion);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
catch (err) {
|
|
2003
|
+
if (err.code !== "ENOENT")
|
|
2004
|
+
throw err;
|
|
2005
|
+
}
|
|
2006
|
+
if (lastSkip?.reason === reason && lastSkip.lane_version === laneVersion)
|
|
2007
|
+
return;
|
|
2008
|
+
recordKeepAliveEvent(hqRoot, lane.lane_id, summary, {
|
|
2009
|
+
event: "keep_alive_skip",
|
|
2010
|
+
at: deps.now().toISOString(),
|
|
2011
|
+
reason,
|
|
2012
|
+
...(detail === undefined ? {} : { detail }),
|
|
2013
|
+
lane_version: laneVersion,
|
|
2014
|
+
lane_state: lane.state,
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
catch (err) {
|
|
2018
|
+
markDirty(summary, {
|
|
2019
|
+
kind: "lane",
|
|
2020
|
+
id: lane.lane_id,
|
|
2021
|
+
why: `keep-alive skip event dedupe failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
1763
2024
|
}
|
|
1764
2025
|
function readLatestInterruptedClassificationAt(hqRoot, lane) {
|
|
1765
2026
|
const logPath = watcherOutLogPath(hqRoot, lane.lane_id);
|
|
@@ -1774,10 +2035,15 @@ function readLatestInterruptedClassificationAt(hqRoot, lane) {
|
|
|
1774
2035
|
}
|
|
1775
2036
|
let latestClassification;
|
|
1776
2037
|
for (const line of body.split(/\r?\n/).filter(Boolean)) {
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2038
|
+
let parsed;
|
|
2039
|
+
try {
|
|
2040
|
+
parsed = JSON.parse(line);
|
|
2041
|
+
}
|
|
2042
|
+
catch {
|
|
2043
|
+
continue;
|
|
1780
2044
|
}
|
|
2045
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
2046
|
+
continue;
|
|
1781
2047
|
const record = parsed;
|
|
1782
2048
|
if (record.event === "classification")
|
|
1783
2049
|
latestClassification = record;
|
|
@@ -1975,7 +2241,7 @@ function appendBlockedKeepAliveHistory(hqRoot, lane, terminalAt, at, reason, dep
|
|
|
1975
2241
|
}),
|
|
1976
2242
|
}), false);
|
|
1977
2243
|
}
|
|
1978
|
-
async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany) {
|
|
2244
|
+
async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, nextCheckAtFlagByCompany) {
|
|
1979
2245
|
const nowMs = now.getTime();
|
|
1980
2246
|
const nowIso = now.toISOString();
|
|
1981
2247
|
const config = loadLanesConfig(hqRoot);
|
|
@@ -2022,16 +2288,26 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2022
2288
|
const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
|
|
2023
2289
|
if (!envelope || envelope.decision !== "done")
|
|
2024
2290
|
continue;
|
|
2025
|
-
const
|
|
2026
|
-
const
|
|
2291
|
+
const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(lane))) === true;
|
|
2292
|
+
const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
|
|
2293
|
+
? hasPendingKeepAliveWork(hqRoot, lane, questions)
|
|
2294
|
+
: false;
|
|
2295
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
|
|
2296
|
+
const verdict = doneKeepAliveVerdict(lane, envelope, hasQueuedWork, {
|
|
2297
|
+
nextCheckAtEnabled,
|
|
2298
|
+
hasPendingWork,
|
|
2299
|
+
nowMs,
|
|
2300
|
+
nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
|
|
2301
|
+
});
|
|
2027
2302
|
if (verdict !== "eligible") {
|
|
2028
|
-
|
|
2029
|
-
recordDoneKeepAlivePolicySkip(summary, lane, verdict);
|
|
2303
|
+
let skipLane = lane;
|
|
2030
2304
|
if (envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
|
|
2031
|
-
casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
|
|
2305
|
+
skipLane = casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version &&
|
|
2032
2306
|
current.state === "done" &&
|
|
2033
|
-
current.keep_alive === lane.keep_alive, (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
|
|
2307
|
+
current.keep_alive === lane.keep_alive, (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun).record;
|
|
2034
2308
|
}
|
|
2309
|
+
if (verdict !== null)
|
|
2310
|
+
recordDoneKeepAlivePolicySkip(hqRoot, summary, skipLane, verdict, deps);
|
|
2035
2311
|
continue;
|
|
2036
2312
|
}
|
|
2037
2313
|
}
|
|
@@ -2043,6 +2319,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2043
2319
|
id: lane.lane_id,
|
|
2044
2320
|
why: "keep_alive_company_unresolved",
|
|
2045
2321
|
});
|
|
2322
|
+
recordKeepAliveSkipEvent(hqRoot, lane, "company_unresolved", lane.version, summary, deps);
|
|
2046
2323
|
}
|
|
2047
2324
|
continue;
|
|
2048
2325
|
}
|
|
@@ -2054,6 +2331,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2054
2331
|
id: lane.lane_id,
|
|
2055
2332
|
why: "keep_alive_flag_off",
|
|
2056
2333
|
});
|
|
2334
|
+
recordKeepAliveSkipEvent(hqRoot, lane, "keep_alive_flag_off", lane.version, summary, deps);
|
|
2057
2335
|
}
|
|
2058
2336
|
continue;
|
|
2059
2337
|
}
|
|
@@ -2134,14 +2412,24 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2134
2412
|
const latest = readLatestEnvelopeRecord(hqRoot, lane.lane_id);
|
|
2135
2413
|
if (!latest || latest.envelope.decision !== "done")
|
|
2136
2414
|
continue;
|
|
2137
|
-
const
|
|
2138
|
-
const
|
|
2415
|
+
const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(lane))) === true;
|
|
2416
|
+
const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
|
|
2417
|
+
? hasPendingKeepAliveWork(hqRoot, lane, questions)
|
|
2418
|
+
: false;
|
|
2419
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingWork;
|
|
2420
|
+
const verdict = doneKeepAliveVerdict(lane, latest.envelope, hasQueuedWork, {
|
|
2421
|
+
nextCheckAtEnabled,
|
|
2422
|
+
hasPendingWork,
|
|
2423
|
+
nowMs,
|
|
2424
|
+
nextCheckAnchorMs: Date.parse(lane.last_envelope?.at ?? lane.updated_at),
|
|
2425
|
+
});
|
|
2139
2426
|
if (verdict !== "eligible") {
|
|
2140
|
-
|
|
2141
|
-
recordDoneKeepAlivePolicySkip(summary, lane, verdict);
|
|
2427
|
+
let skipLane = lane;
|
|
2142
2428
|
if (latest.envelope.story_complete === true && lane.keep_alive_standing !== true && hasKeepAliveProgress(lane)) {
|
|
2143
|
-
casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === "done", (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun);
|
|
2429
|
+
skipLane = casLaneIf(hqRoot, lane.lane_id, (current) => current.version === lane.version && current.state === "done", (current) => ({ ...clearedKeepAliveProgress(current), keep_alive_blocked_reason: undefined }), deps.dryRun).record;
|
|
2144
2430
|
}
|
|
2431
|
+
if (verdict !== null)
|
|
2432
|
+
recordDoneKeepAlivePolicySkip(hqRoot, summary, skipLane, verdict, deps);
|
|
2145
2433
|
continue;
|
|
2146
2434
|
}
|
|
2147
2435
|
try {
|
|
@@ -2216,17 +2504,27 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2216
2504
|
const currentEnvelope = readLatestEnvelopeRecord(hqRoot, current.lane_id);
|
|
2217
2505
|
if (!currentEnvelope || currentEnvelope.envelope.decision !== "done")
|
|
2218
2506
|
continue;
|
|
2219
|
-
const
|
|
2220
|
-
const
|
|
2507
|
+
const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(current))) === true;
|
|
2508
|
+
const hasPendingWork = current.keep_alive_standing === true || nextCheckAtEnabled
|
|
2509
|
+
? hasPendingKeepAliveWork(hqRoot, current, questions)
|
|
2510
|
+
: false;
|
|
2511
|
+
const hasQueuedWork = current.keep_alive_standing === true && hasPendingWork;
|
|
2512
|
+
const verdict = doneKeepAliveVerdict(current, currentEnvelope.envelope, hasQueuedWork, {
|
|
2513
|
+
nextCheckAtEnabled,
|
|
2514
|
+
hasPendingWork,
|
|
2515
|
+
nowMs,
|
|
2516
|
+
nextCheckAnchorMs: Date.parse(current.last_envelope?.at ?? current.updated_at),
|
|
2517
|
+
});
|
|
2221
2518
|
if (verdict !== "eligible") {
|
|
2222
|
-
|
|
2223
|
-
recordDoneKeepAlivePolicySkip(summary, current, verdict);
|
|
2519
|
+
let skipLane = current;
|
|
2224
2520
|
if (currentEnvelope.envelope.story_complete === true && current.keep_alive_standing !== true && hasKeepAliveProgress(current)) {
|
|
2225
|
-
casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
2521
|
+
skipLane = casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
2226
2522
|
latestLane.state === "done" &&
|
|
2227
2523
|
latestLane.keep_alive === true &&
|
|
2228
|
-
latestLane.keep_alive_opted_in_at === current.keep_alive_opted_in_at, (latestLane) => ({ ...clearedKeepAliveProgress(latestLane), keep_alive_blocked_reason: undefined }), deps.dryRun);
|
|
2524
|
+
latestLane.keep_alive_opted_in_at === current.keep_alive_opted_in_at, (latestLane) => ({ ...clearedKeepAliveProgress(latestLane), keep_alive_blocked_reason: undefined }), deps.dryRun).record;
|
|
2229
2525
|
}
|
|
2526
|
+
if (verdict !== null)
|
|
2527
|
+
recordDoneKeepAlivePolicySkip(hqRoot, summary, skipLane, verdict, deps);
|
|
2230
2528
|
continue;
|
|
2231
2529
|
}
|
|
2232
2530
|
try {
|
|
@@ -2272,6 +2570,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2272
2570
|
id: current.lane_id,
|
|
2273
2571
|
why: "keep_alive_company_unresolved",
|
|
2274
2572
|
});
|
|
2573
|
+
recordKeepAliveSkipEvent(hqRoot, current, "company_unresolved", current.version, summary, deps);
|
|
2275
2574
|
continue;
|
|
2276
2575
|
}
|
|
2277
2576
|
const currentCompanyKey = keepAliveCompanyKey(currentCompany);
|
|
@@ -2281,6 +2580,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2281
2580
|
id: current.lane_id,
|
|
2282
2581
|
why: "keep_alive_flag_off",
|
|
2283
2582
|
});
|
|
2583
|
+
recordKeepAliveSkipEvent(hqRoot, current, "keep_alive_flag_off", current.version, summary, deps);
|
|
2284
2584
|
continue;
|
|
2285
2585
|
}
|
|
2286
2586
|
const currentExclusion = keepAliveExclusion(current, currentLanes, retiredLaneIds, supersessionLineageAwareByCompany.get(currentCompanyKey) === true, companyForLane);
|
|
@@ -2342,12 +2642,15 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2342
2642
|
? Math.max(attemptEligibleMs, storedNextMs)
|
|
2343
2643
|
: attemptEligibleMs;
|
|
2344
2644
|
if (nowMs < nextEligibleMs) {
|
|
2645
|
+
let skipLane = current;
|
|
2345
2646
|
if (resetByHuman && hasKeepAliveProgress(current)) {
|
|
2346
|
-
casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
2647
|
+
const clearedProgress = casLaneIf(hqRoot, current.lane_id, (latestLane) => latestLane.version === current.version &&
|
|
2347
2648
|
latestLane.state === current.state &&
|
|
2348
2649
|
latestLane.keep_alive === true &&
|
|
2349
2650
|
latestLane.keep_alive_opted_in_at === current.keep_alive_opted_in_at, (latestLane) => clearedKeepAliveProgress(latestLane), deps.dryRun);
|
|
2651
|
+
skipLane = clearedProgress.record;
|
|
2350
2652
|
}
|
|
2653
|
+
recordKeepAliveSkipEvent(hqRoot, skipLane, "backoff", skipLane.version, summary, deps, new Date(nextEligibleMs).toISOString());
|
|
2351
2654
|
continue;
|
|
2352
2655
|
}
|
|
2353
2656
|
const nextEligibleAt = new Date(nowMs + leadLaneKeepAliveBackoffMs(attempt + 1)).toISOString();
|
|
@@ -2362,6 +2665,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2362
2665
|
id: current.lane_id,
|
|
2363
2666
|
why: `lane_queue_full: ${current.provider} queue has ${queuedCount}/${maxQueued} lanes`,
|
|
2364
2667
|
});
|
|
2668
|
+
recordKeepAliveSkipEvent(hqRoot, current, "lane_queue_full", current.version, summary, deps, `${current.provider} queue has ${queuedCount}/${maxQueued} lanes`);
|
|
2365
2669
|
continue;
|
|
2366
2670
|
}
|
|
2367
2671
|
const seniorId = current.senior.kind === "lane" ? current.senior.id : undefined;
|
|
@@ -2399,6 +2703,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2399
2703
|
id: current.lane_id,
|
|
2400
2704
|
why: `keep_alive_hire_admission: ${grantPreflight.error}: ${grantPreflight.message}`,
|
|
2401
2705
|
});
|
|
2706
|
+
recordKeepAliveSkipEvent(hqRoot, current, "hire_admission", current.version, summary, deps, `${grantPreflight.error}: ${grantPreflight.message}`);
|
|
2402
2707
|
continue;
|
|
2403
2708
|
}
|
|
2404
2709
|
}
|
|
@@ -2481,8 +2786,22 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2481
2786
|
const envelope = readLatestEnvelopeRecord(hqRoot, latest.lane_id);
|
|
2482
2787
|
if (!envelope || envelope.envelope.decision !== "done")
|
|
2483
2788
|
continue;
|
|
2484
|
-
|
|
2789
|
+
const nextCheckAtEnabled = nextCheckAtFlagByCompany.get(keepAliveCompanyKey(companyForLane(latest))) === true;
|
|
2790
|
+
const hasPendingWork = latest.keep_alive_standing === true || nextCheckAtEnabled
|
|
2791
|
+
? hasPendingKeepAliveWork(hqRoot, latest, questions)
|
|
2792
|
+
: false;
|
|
2793
|
+
const finalDoneVerdict = doneKeepAliveVerdict(latest, envelope.envelope, latest.keep_alive_standing === true && hasPendingWork, {
|
|
2794
|
+
nextCheckAtEnabled,
|
|
2795
|
+
hasPendingWork,
|
|
2796
|
+
nowMs,
|
|
2797
|
+
nextCheckAnchorMs: Date.parse(latest.last_envelope?.at ?? latest.updated_at),
|
|
2798
|
+
});
|
|
2799
|
+
if (finalDoneVerdict !== "eligible") {
|
|
2800
|
+
if (finalDoneVerdict === "next_check_at") {
|
|
2801
|
+
recordDoneKeepAlivePolicySkip(hqRoot, summary, latest, finalDoneVerdict, deps);
|
|
2802
|
+
}
|
|
2485
2803
|
continue;
|
|
2804
|
+
}
|
|
2486
2805
|
try {
|
|
2487
2806
|
latestEligibilityAtMs = latestEnvelopeMtimeMs(hqRoot, latest, envelope.ref);
|
|
2488
2807
|
}
|
|
@@ -2514,6 +2833,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2514
2833
|
why: `keep_alive_account_selection_failed: ${accountSelectionFailure}`,
|
|
2515
2834
|
});
|
|
2516
2835
|
if (!deps.dryRun) {
|
|
2836
|
+
recordKeepAliveSkipEvent(hqRoot, current, "account_selection_failed", current.version, summary, deps, accountSelectionFailure);
|
|
2517
2837
|
recordKeepAliveEvent(hqRoot, current.lane_id, summary, {
|
|
2518
2838
|
event: "keep_alive_account_selection_failed",
|
|
2519
2839
|
at: nowIso,
|