@indigoai-us/hq-cli 5.214.0 → 5.216.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 +25 -0
- package/dist/command-catalog.generated.d.ts +22 -0
- package/dist/command-catalog.generated.js +29 -0
- package/dist/commands/lanes.js +106 -64
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +9 -0
- package/dist/lib/lanes/keep-alive.js +101 -0
- package/dist/lib/lanes/mesh-events.d.ts +3 -0
- package/dist/lib/lanes/mesh-events.js +32 -3
- package/dist/lib/lanes/profile.d.ts +8 -4
- package/dist/lib/lanes/profile.js +13 -17
- package/dist/lib/lanes/questions.d.ts +1 -1
- package/dist/lib/lanes/questions.js +2 -2
- package/dist/lib/lanes/reconcile.js +126 -40
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +6 -1
- package/dist/lib/lanes/store.js +25 -0
- package/dist/lib/lanes/types.d.ts +6 -0
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.d.ts +3 -1
- package/dist/lib/lanes/watcher.js +174 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.216.0] — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Lane watchers keep valid questions from a malformed `ask` envelope when
|
|
10
|
+
repair is skipped, cannot start, or still produces invalid output. The lane
|
|
11
|
+
stores the validation errors, and recovered question context includes them.
|
|
12
|
+
Repair spawns ignore unknown profile metadata and log the field names.
|
|
13
|
+
`hq lanes create` and `hq lanes resume --worker` keep strict validation.
|
|
14
|
+
|
|
15
|
+
- `hq lanes` stops sending status updates for stories missing from the project
|
|
16
|
+
board. The watcher reports the refusal once per lane run and keeps appending
|
|
17
|
+
lane events.
|
|
18
|
+
|
|
19
|
+
## [5.215.0] — 2026-09-26
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- hq-cli 5.211.0 could resume a retired lead from its worker profile's
|
|
24
|
+
`keep_alive` value. Reconcile now requires an explicit lane opt-in written
|
|
25
|
+
after the latest envelope, and skips retired or superseded leads.
|
|
26
|
+
|
|
5
27
|
## [5.214.0] — 2026-09-26
|
|
6
28
|
|
|
7
29
|
### Added
|
|
8
30
|
|
|
31
|
+
- `hq lanes keep-alive <lane-id> --on|--off` lets a lane's senior opt in on
|
|
32
|
+
the host that owns the lane.
|
|
33
|
+
|
|
9
34
|
- `hq doctor` now checks the last hour of the sync runner log for stuck
|
|
10
35
|
realtime lag, cloud permission refusals, runner restart loops, device-limit
|
|
11
36
|
backoff, and repeated conflicts. Client health reports send these to Sentry
|
|
@@ -7399,6 +7399,28 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7399
7399
|
readonly description: "JSON on stdout (always)";
|
|
7400
7400
|
}];
|
|
7401
7401
|
readonly subcommands: readonly [];
|
|
7402
|
+
}, {
|
|
7403
|
+
readonly name: "keep-alive";
|
|
7404
|
+
readonly description: "Opt a lane into or out of automatic lead-lane resumes";
|
|
7405
|
+
readonly aliases: readonly [];
|
|
7406
|
+
readonly hidden: false;
|
|
7407
|
+
readonly usage: "[options] <lane-id>";
|
|
7408
|
+
readonly arguments: readonly [{
|
|
7409
|
+
readonly name: "lane-id";
|
|
7410
|
+
readonly required: true;
|
|
7411
|
+
readonly variadic: false;
|
|
7412
|
+
}];
|
|
7413
|
+
readonly options: readonly [{
|
|
7414
|
+
readonly flags: "--on";
|
|
7415
|
+
readonly description: "Enable explicit keep-alive for this lane";
|
|
7416
|
+
}, {
|
|
7417
|
+
readonly flags: "--off";
|
|
7418
|
+
readonly description: "Disable keep-alive for this lane";
|
|
7419
|
+
}, {
|
|
7420
|
+
readonly flags: "--json";
|
|
7421
|
+
readonly description: "Emit JSON on stdout (default)";
|
|
7422
|
+
}];
|
|
7423
|
+
readonly subcommands: readonly [];
|
|
7402
7424
|
}, {
|
|
7403
7425
|
readonly name: "grant";
|
|
7404
7426
|
readonly description: "Set a lane's alive child-lane grant as its senior";
|
|
@@ -9566,6 +9566,35 @@ export const COMMAND_CATALOG = [
|
|
|
9566
9566
|
],
|
|
9567
9567
|
"subcommands": []
|
|
9568
9568
|
},
|
|
9569
|
+
{
|
|
9570
|
+
"name": "keep-alive",
|
|
9571
|
+
"description": "Opt a lane into or out of automatic lead-lane resumes",
|
|
9572
|
+
"aliases": [],
|
|
9573
|
+
"hidden": false,
|
|
9574
|
+
"usage": "[options] <lane-id>",
|
|
9575
|
+
"arguments": [
|
|
9576
|
+
{
|
|
9577
|
+
"name": "lane-id",
|
|
9578
|
+
"required": true,
|
|
9579
|
+
"variadic": false
|
|
9580
|
+
}
|
|
9581
|
+
],
|
|
9582
|
+
"options": [
|
|
9583
|
+
{
|
|
9584
|
+
"flags": "--on",
|
|
9585
|
+
"description": "Enable explicit keep-alive for this lane"
|
|
9586
|
+
},
|
|
9587
|
+
{
|
|
9588
|
+
"flags": "--off",
|
|
9589
|
+
"description": "Disable keep-alive for this lane"
|
|
9590
|
+
},
|
|
9591
|
+
{
|
|
9592
|
+
"flags": "--json",
|
|
9593
|
+
"description": "Emit JSON on stdout (default)"
|
|
9594
|
+
}
|
|
9595
|
+
],
|
|
9596
|
+
"subcommands": []
|
|
9597
|
+
},
|
|
9569
9598
|
{
|
|
9570
9599
|
"name": "grant",
|
|
9571
9600
|
"description": "Set a lane's alive child-lane grant as its senior",
|
package/dist/commands/lanes.js
CHANGED
|
@@ -36,9 +36,10 @@ import { CompanySelectionError } from "../utils/company-selection-error.js";
|
|
|
36
36
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
37
37
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
38
38
|
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
39
|
-
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
39
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, hireGrantCapacityError, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
40
40
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
41
41
|
import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
|
|
42
|
+
import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
42
43
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
43
44
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
44
45
|
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
@@ -896,41 +897,6 @@ async function withResumeHireAdmission(ctx, lane, fn) {
|
|
|
896
897
|
return fn();
|
|
897
898
|
});
|
|
898
899
|
}
|
|
899
|
-
function hireGrantCapacityError(parent, children) {
|
|
900
|
-
const grant = parent.hire_grant ?? 0;
|
|
901
|
-
const grantor = (parent.senior.kind === "lane" || parent.senior.kind === "session") &&
|
|
902
|
-
parent.senior.id
|
|
903
|
-
? { kind: parent.senior.kind, id: parent.senior.id }
|
|
904
|
-
: null;
|
|
905
|
-
const grantorReason = grantor
|
|
906
|
-
? undefined
|
|
907
|
-
: parent.senior.kind === "user"
|
|
908
|
-
? "senior_is_user"
|
|
909
|
-
: "parent_senior_missing_id";
|
|
910
|
-
const capacity = children.length === grant
|
|
911
|
-
? `is at its hire grant (${children.length} of ${grant} alive children)`
|
|
912
|
-
: `has exhausted its hire grant (${children.length} alive children; grant ${grant})`;
|
|
913
|
-
const grantCommand = `hq lanes grant ${parent.lane_id} --hire <n>`;
|
|
914
|
-
const authorityGuidance = grantor
|
|
915
|
-
? `Its senior ${grantor.kind} ${grantor.id} can raise it with: ${grantCommand}`
|
|
916
|
-
: parent.senior.kind === "user"
|
|
917
|
-
? `Its senior is the user and can raise it with: ${grantCommand}`
|
|
918
|
-
: `No senior is recorded, so an operator must resolve ownership before running: ${grantCommand}`;
|
|
919
|
-
const message = `Lane ${parent.lane_id} ${capacity}. ${authorityGuidance}`;
|
|
920
|
-
return new LanesError("hire_grant_exhausted", message, {
|
|
921
|
-
senior_lane_id: parent.lane_id,
|
|
922
|
-
parent_grant: grant,
|
|
923
|
-
alive_children: children.map((child) => child.lane_id),
|
|
924
|
-
alive_child_count: children.length,
|
|
925
|
-
grantor,
|
|
926
|
-
...(grantorReason ? { grantor_reason: grantorReason } : {}),
|
|
927
|
-
message,
|
|
928
|
-
ask_guidance: {
|
|
929
|
-
envelope: "decision=ask; include the desired total grant and the reason",
|
|
930
|
-
senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
|
|
931
|
-
},
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
900
|
const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
|
|
935
901
|
const MAX_PLACEMENT_OUTPOSTS = 16;
|
|
936
902
|
function outpostIdentityUnknownError(message, expectedId, details = {}) {
|
|
@@ -2071,7 +2037,7 @@ async function emitLaneTerminalStateSoft(ctx, lane) {
|
|
|
2071
2037
|
process.stderr.write(`hq lanes: terminal mesh update failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2072
2038
|
}
|
|
2073
2039
|
}
|
|
2074
|
-
async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
2040
|
+
async function emitLaneStartedForStories(ctx, company, lane, threadId, knownAbsentStories = new Set()) {
|
|
2075
2041
|
let ready = lane;
|
|
2076
2042
|
if (ctx.mesh.ensureLaneThread) {
|
|
2077
2043
|
try {
|
|
@@ -2107,6 +2073,8 @@ async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
|
2107
2073
|
failures.push(err);
|
|
2108
2074
|
continue;
|
|
2109
2075
|
}
|
|
2076
|
+
if (knownAbsentStories.has(storyBoardStoryKey(story.project_id, story.story_id)))
|
|
2077
|
+
continue;
|
|
2110
2078
|
try {
|
|
2111
2079
|
await ctx.mesh.patchStoryStatus(company, story.project_id, story.story_id, "in_progress");
|
|
2112
2080
|
}
|
|
@@ -2134,20 +2102,30 @@ function storyNotOnBoardWarning(err) {
|
|
|
2134
2102
|
? err.message
|
|
2135
2103
|
: undefined;
|
|
2136
2104
|
}
|
|
2105
|
+
function storyBoardStoryKey(projectId, storyId) {
|
|
2106
|
+
return JSON.stringify([projectId, storyId]);
|
|
2107
|
+
}
|
|
2137
2108
|
function combinedStoryWarning(lane, additional = []) {
|
|
2138
2109
|
const warnings = [storyWarning(lane), ...additional]
|
|
2139
2110
|
.filter((warning) => warning !== undefined && warning.trim().length > 0);
|
|
2140
2111
|
return [...new Set(warnings)].join(" ") || null;
|
|
2141
2112
|
}
|
|
2142
|
-
async function
|
|
2113
|
+
async function storyBoardCheck(ctx, company, projectId, storyIds) {
|
|
2143
2114
|
try {
|
|
2144
2115
|
await assertStoriesOnBoard(ctx, company, projectId, storyIds);
|
|
2145
|
-
return
|
|
2116
|
+
return { absentStoryIds: new Set() };
|
|
2146
2117
|
}
|
|
2147
2118
|
catch (err) {
|
|
2148
2119
|
const warning = storyNotOnBoardWarning(err);
|
|
2149
|
-
if (warning !== undefined)
|
|
2150
|
-
|
|
2120
|
+
if (warning !== undefined) {
|
|
2121
|
+
const storyId = isLanesError(err) && typeof err.details.story_id === "string"
|
|
2122
|
+
? err.details.story_id
|
|
2123
|
+
: undefined;
|
|
2124
|
+
return {
|
|
2125
|
+
warning,
|
|
2126
|
+
absentStoryIds: new Set(storyId ? [storyId] : []),
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2151
2129
|
throw err;
|
|
2152
2130
|
}
|
|
2153
2131
|
}
|
|
@@ -2450,10 +2428,14 @@ async function runCreate(ctx, opts) {
|
|
|
2450
2428
|
const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
|
|
2451
2429
|
assertSameTenant(company, senior, seniorCompany);
|
|
2452
2430
|
const createStoryWarnings = [];
|
|
2431
|
+
const knownAbsentStories = new Set();
|
|
2453
2432
|
if (meshCompany && storyIds.length > 0) {
|
|
2454
|
-
const
|
|
2455
|
-
if (warning !== undefined)
|
|
2456
|
-
createStoryWarnings.push(warning);
|
|
2433
|
+
const boardCheck = await storyBoardCheck(ctx, meshCompany, project, storyIds);
|
|
2434
|
+
if (boardCheck.warning !== undefined)
|
|
2435
|
+
createStoryWarnings.push(boardCheck.warning);
|
|
2436
|
+
for (const storyId of boardCheck.absentStoryIds) {
|
|
2437
|
+
knownAbsentStories.add(storyBoardStoryKey(project, storyId));
|
|
2438
|
+
}
|
|
2457
2439
|
}
|
|
2458
2440
|
let resolvedWorker;
|
|
2459
2441
|
if (opts.worker) {
|
|
@@ -2935,6 +2917,7 @@ async function runCreate(ctx, opts) {
|
|
|
2935
2917
|
}
|
|
2936
2918
|
: {}),
|
|
2937
2919
|
keep_alive: keepAlive,
|
|
2920
|
+
...(keepAlive ? { keep_alive_opted_in_at: createdAt } : {}),
|
|
2938
2921
|
provider,
|
|
2939
2922
|
account: pinnedAccount,
|
|
2940
2923
|
...(accountSelection !== undefined ? { account_selection: accountSelection } : {}),
|
|
@@ -3065,7 +3048,7 @@ async function runCreate(ctx, opts) {
|
|
|
3065
3048
|
locateTranscript: ctx.locateTranscript,
|
|
3066
3049
|
});
|
|
3067
3050
|
if (meshCompany) {
|
|
3068
|
-
createStoryWarnings.push(...await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id));
|
|
3051
|
+
createStoryWarnings.push(...await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id, knownAbsentStories));
|
|
3069
3052
|
}
|
|
3070
3053
|
}
|
|
3071
3054
|
else if (outpostId) {
|
|
@@ -4493,15 +4476,7 @@ async function runShow(ctx, laneId) {
|
|
|
4493
4476
|
? { ...storedLane, state: "remote", remote_state: storedLane.state, queued_reason: undefined }
|
|
4494
4477
|
: storedLane;
|
|
4495
4478
|
const askDeliveryFailed = readJsonFile(askDeliveryFailedPath(hqRoot, laneId));
|
|
4496
|
-
|
|
4497
|
-
try {
|
|
4498
|
-
keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
|
|
4499
|
-
}
|
|
4500
|
-
catch (err) {
|
|
4501
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4502
|
-
process.stderr.write(`hq lanes show: keep-alive profile could not be loaded for ${laneId}: ${message}\n`);
|
|
4503
|
-
keepAliveState = { enabled: false, source: "none" };
|
|
4504
|
-
}
|
|
4479
|
+
const keepAliveState = resolveLaneKeepAlive(hqRoot, storedLane);
|
|
4505
4480
|
printJson({
|
|
4506
4481
|
ok: true,
|
|
4507
4482
|
action: "show",
|
|
@@ -4516,6 +4491,7 @@ async function runShow(ctx, laneId) {
|
|
|
4516
4491
|
provider_session_id: lane.provider_session_id ?? null,
|
|
4517
4492
|
provider_session_id_absent: lane.provider_session_id_absent ?? null,
|
|
4518
4493
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
4494
|
+
envelope_validation_errors: lane.envelope_validation_errors ?? [],
|
|
4519
4495
|
last_resume: lane.last_resume ?? null,
|
|
4520
4496
|
account: lane.account ?? null,
|
|
4521
4497
|
account_history: lane.account_history ?? [],
|
|
@@ -4529,6 +4505,9 @@ async function runShow(ctx, laneId) {
|
|
|
4529
4505
|
keep_alive_state: {
|
|
4530
4506
|
enabled: keepAliveState.enabled,
|
|
4531
4507
|
source: keepAliveState.source,
|
|
4508
|
+
opted_in_at: storedLane.keep_alive_opted_in_at ?? null,
|
|
4509
|
+
blocked_reason: storedLane.keep_alive_blocked_reason ??
|
|
4510
|
+
(storedLane.keep_alive === true ? null : "not_opted_in"),
|
|
4532
4511
|
consecutive_auto_resumes: lane.keep_alive_resume_count ?? 0,
|
|
4533
4512
|
last_auto_resume_at: lane.keep_alive_last_resume_at ?? null,
|
|
4534
4513
|
next_eligible_at: lane.keep_alive_next_eligible_at ?? null,
|
|
@@ -4658,6 +4637,45 @@ function printGrantResult(hqRoot, updated, hierarchy) {
|
|
|
4658
4637
|
const fields = laneHireFields(updated, hierarchy ?? loadAllLanes(hqRoot));
|
|
4659
4638
|
printJson({ ok: true, action: "grant", lane_id: updated.lane_id, lane: updated, ...fields });
|
|
4660
4639
|
}
|
|
4640
|
+
async function runKeepAlive(ctx, laneId, opts) {
|
|
4641
|
+
if (Boolean(opts.on) === Boolean(opts.off)) {
|
|
4642
|
+
throw new LanesError("keep_alive_mode_invalid", "Pass exactly one of --on or --off", { lane_id: laneId });
|
|
4643
|
+
}
|
|
4644
|
+
const hqRoot = ctx.hqRoot();
|
|
4645
|
+
const updated = await withAllLaneAdmissionLocks(hqRoot, async () => {
|
|
4646
|
+
const lane = readLane(hqRoot, laneId);
|
|
4647
|
+
if (!callerMatchesSenior(ctx, lane.senior)) {
|
|
4648
|
+
throw new LanesError("keep_alive_unauthorized", `Only senior ${seniorKey(lane.senior)} may change lane ${laneId}'s keep-alive setting`, { lane_id: laneId, senior: lane.senior });
|
|
4649
|
+
}
|
|
4650
|
+
if (!laneRunsHere(lane, reconcileHostFromCtx(ctx))) {
|
|
4651
|
+
throw new LanesError("keep_alive_owner_host_required", `Run hq lanes keep-alive ${laneId} on its owner host ${lane.host.kind}:${lane.host.id}`, { lane_id: laneId, host: lane.host });
|
|
4652
|
+
}
|
|
4653
|
+
if (lane.state === "killed" || lane.state === "cancelled" || lane.state === "failed") {
|
|
4654
|
+
throw new LanesError("keep_alive_terminal_lane", `Cannot change keep-alive for lane ${laneId} in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
|
|
4655
|
+
}
|
|
4656
|
+
let optedInAt;
|
|
4657
|
+
if (opts.on) {
|
|
4658
|
+
const retiredLaneIds = loadRetiredLaneIds(hqRoot);
|
|
4659
|
+
const exclusion = keepAliveExclusion(lane, loadAllLanes(hqRoot), retiredLaneIds);
|
|
4660
|
+
if (exclusion !== undefined) {
|
|
4661
|
+
throw new LanesError("keep_alive_lane_excluded", `Cannot opt in lane ${laneId}: it is ${exclusion}`, { lane_id: laneId, blocked_reason: exclusion });
|
|
4662
|
+
}
|
|
4663
|
+
optedInAt = ctx.now().toISOString();
|
|
4664
|
+
}
|
|
4665
|
+
return updateLane(hqRoot, laneId, lane.version, "command", (current) => ({
|
|
4666
|
+
...current,
|
|
4667
|
+
keep_alive: Boolean(opts.on),
|
|
4668
|
+
keep_alive_opted_in_at: optedInAt,
|
|
4669
|
+
keep_alive_blocked_reason: undefined,
|
|
4670
|
+
}));
|
|
4671
|
+
});
|
|
4672
|
+
printJson({
|
|
4673
|
+
ok: true,
|
|
4674
|
+
action: "keep-alive",
|
|
4675
|
+
lane_id: updated.lane_id,
|
|
4676
|
+
lane: updated,
|
|
4677
|
+
});
|
|
4678
|
+
}
|
|
4661
4679
|
async function runGrant(ctx, laneId, opts) {
|
|
4662
4680
|
if (opts.hire === undefined) {
|
|
4663
4681
|
throw new LanesError("missing_hire_grant", "--hire is required", {});
|
|
@@ -4950,6 +4968,10 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4950
4968
|
next.worker_policy_delivery = undefined;
|
|
4951
4969
|
if (profile.meta.keep_alive !== undefined) {
|
|
4952
4970
|
next.keep_alive = profile.meta.keep_alive;
|
|
4971
|
+
next.keep_alive_opted_in_at = profile.meta.keep_alive
|
|
4972
|
+
? input.at ?? new Date().toISOString()
|
|
4973
|
+
: undefined;
|
|
4974
|
+
next.keep_alive_blocked_reason = undefined;
|
|
4953
4975
|
}
|
|
4954
4976
|
if (next.model_source !== "cli") {
|
|
4955
4977
|
if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
|
|
@@ -4966,8 +4988,13 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4966
4988
|
next.effort = input.effort.effort;
|
|
4967
4989
|
next.effort_source = input.effort.source;
|
|
4968
4990
|
}
|
|
4969
|
-
if (input.keepAlive !== undefined)
|
|
4991
|
+
if (input.keepAlive !== undefined) {
|
|
4970
4992
|
next.keep_alive = input.keepAlive;
|
|
4993
|
+
next.keep_alive_opted_in_at = input.keepAlive
|
|
4994
|
+
? input.at ?? new Date().toISOString()
|
|
4995
|
+
: undefined;
|
|
4996
|
+
next.keep_alive_blocked_reason = undefined;
|
|
4997
|
+
}
|
|
4971
4998
|
}
|
|
4972
4999
|
function recordResumeSetup(ctx, lane, input) {
|
|
4973
5000
|
if (input.cliModel !== undefined) {
|
|
@@ -4998,7 +5025,7 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4998
5025
|
throw new LanesError("lane_resume_setup_conflict", `Lane ${lane.lane_id} changed while resume settings were prepared; run the resume again`, { lane_id: lane.lane_id, prepared_version: lane.version, version: current.version });
|
|
4999
5026
|
}
|
|
5000
5027
|
const next = { ...current };
|
|
5001
|
-
applyResumeProfileAndEffort(current, next, input);
|
|
5028
|
+
applyResumeProfileAndEffort(current, next, { ...input, at: ctx.now().toISOString() });
|
|
5002
5029
|
if (input.cliModel !== undefined) {
|
|
5003
5030
|
next.model = input.cliModel.trim();
|
|
5004
5031
|
next.model_source = "cli";
|
|
@@ -5237,6 +5264,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5237
5264
|
applyResumeProfileAndEffort(current, next, {
|
|
5238
5265
|
...(input.workerProfile !== undefined ? { profile: input.workerProfile } : {}),
|
|
5239
5266
|
...(input.effort !== undefined ? { effort: input.effort } : {}),
|
|
5267
|
+
at,
|
|
5240
5268
|
});
|
|
5241
5269
|
if (input.deferTurnStart) {
|
|
5242
5270
|
next.state = current.state;
|
|
@@ -6281,7 +6309,7 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
|
|
|
6281
6309
|
}
|
|
6282
6310
|
return updated;
|
|
6283
6311
|
}
|
|
6284
|
-
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}) {
|
|
6312
|
+
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
|
|
6285
6313
|
const company = lane.mesh_company ?? lane.company;
|
|
6286
6314
|
if (!company)
|
|
6287
6315
|
return;
|
|
@@ -6295,7 +6323,9 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
|
|
|
6295
6323
|
...payload,
|
|
6296
6324
|
lane_status: lane.state,
|
|
6297
6325
|
});
|
|
6298
|
-
|
|
6326
|
+
if (!skipStoryStatusPatch) {
|
|
6327
|
+
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
6328
|
+
}
|
|
6299
6329
|
}
|
|
6300
6330
|
function sameCommittedStory(current, expected, projectId, storyId) {
|
|
6301
6331
|
const currentStory = laneStory(current, projectId, storyId);
|
|
@@ -6304,9 +6334,9 @@ function sameCommittedStory(current, expected, projectId, storyId) {
|
|
|
6304
6334
|
current.story_id === expected.story_id &&
|
|
6305
6335
|
JSON.stringify(currentStory) === JSON.stringify(expectedStory));
|
|
6306
6336
|
}
|
|
6307
|
-
async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}) {
|
|
6337
|
+
async function commitStoryTransition(ctx, before, next, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
|
|
6308
6338
|
try {
|
|
6309
|
-
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload);
|
|
6339
|
+
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload, skipStoryStatusPatch);
|
|
6310
6340
|
return undefined;
|
|
6311
6341
|
}
|
|
6312
6342
|
catch (err) {
|
|
@@ -6362,10 +6392,12 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
6362
6392
|
}
|
|
6363
6393
|
const company = lane.mesh_company ?? lane.company;
|
|
6364
6394
|
const warnings = [];
|
|
6395
|
+
let storyKnownAbsent = false;
|
|
6365
6396
|
if (company) {
|
|
6366
|
-
const
|
|
6367
|
-
if (warning !== undefined)
|
|
6368
|
-
warnings.push(warning);
|
|
6397
|
+
const boardCheck = await storyBoardCheck(ctx, company, projectId, [normalizedStoryId]);
|
|
6398
|
+
if (boardCheck.warning !== undefined)
|
|
6399
|
+
warnings.push(boardCheck.warning);
|
|
6400
|
+
storyKnownAbsent = boardCheck.absentStoryIds.has(normalizedStoryId);
|
|
6369
6401
|
}
|
|
6370
6402
|
const at = ctx.now().toISOString();
|
|
6371
6403
|
const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
|
|
@@ -6381,7 +6413,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
6381
6413
|
no_story_reason: undefined,
|
|
6382
6414
|
};
|
|
6383
6415
|
});
|
|
6384
|
-
const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress");
|
|
6416
|
+
const transitionWarning = await commitStoryTransition(ctx, before, next, projectId, normalizedStoryId, "lane_started", "in_progress", {}, storyKnownAbsent);
|
|
6385
6417
|
if (transitionWarning !== undefined)
|
|
6386
6418
|
warnings.push(transitionWarning);
|
|
6387
6419
|
const finalLane = readLane(hqRoot, laneId);
|
|
@@ -8388,6 +8420,16 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8388
8420
|
.action(wrap(async (laneId, opts) => {
|
|
8389
8421
|
await runGrantOwnerTarget(ctx, laneId, opts);
|
|
8390
8422
|
}));
|
|
8423
|
+
lanes
|
|
8424
|
+
.command("keep-alive")
|
|
8425
|
+
.description("Opt a lane into or out of automatic lead-lane resumes")
|
|
8426
|
+
.argument("<lane-id>", "Lane id")
|
|
8427
|
+
.option("--on", "Enable explicit keep-alive for this lane")
|
|
8428
|
+
.option("--off", "Disable keep-alive for this lane")
|
|
8429
|
+
.option("--json", "Emit JSON on stdout (default)")
|
|
8430
|
+
.action(wrap(async (laneId, opts) => {
|
|
8431
|
+
await runKeepAlive(ctx, laneId, opts);
|
|
8432
|
+
}));
|
|
8391
8433
|
lanes
|
|
8392
8434
|
.command("grant")
|
|
8393
8435
|
.description("Set a lane's alive child-lane grant as its senior")
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
|
|
7
7
|
export type { HireGrantBackfillRow } from "./hire-grants.js";
|
|
8
8
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
9
9
|
export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./hire-grants.js";
|
|
7
7
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
8
8
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
9
9
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
import type { LaneRecord } from "./types.js";
|
|
1
2
|
export declare const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
2
3
|
export declare const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
3
4
|
export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
4
5
|
export declare const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS: number;
|
|
5
6
|
export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
|
|
6
7
|
export declare const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
8
|
+
export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
|
|
9
|
+
export declare function retiredLaneRegistryPath(hqRoot: string): string;
|
|
10
|
+
/** Missing registry means no retired lanes; every present invalid file fails closed. */
|
|
11
|
+
export declare function loadRetiredLaneIds(hqRoot: string): ReadonlySet<string>;
|
|
12
|
+
/** IDs may be stored bare or with an owner-host suffix after an underscore. */
|
|
13
|
+
export declare function isRetiredLaneId(laneId: string, retiredLaneIds: ReadonlySet<string>): boolean;
|
|
14
|
+
export declare function keepAliveSupersededBy(lane: LaneRecord, lanes: readonly LaneRecord[]): LaneRecord | undefined;
|
|
15
|
+
export declare function keepAliveExclusion(lane: LaneRecord, lanes: readonly LaneRecord[], retiredLaneIds: ReadonlySet<string>): "retired" | "superseded" | undefined;
|
|
7
16
|
export declare function leadLaneKeepAliveBackoffMs(attempt: number): number;
|
|
8
17
|
//# sourceMappingURL=keep-alive.d.ts.map
|
|
@@ -1,9 +1,110 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { LanesError } from "./errors.js";
|
|
1
4
|
export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
|
|
2
5
|
export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
|
|
3
6
|
export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
4
7
|
export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
5
8
|
export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
6
9
|
export const LEAD_LANE_KEEP_ALIVE_RESUME_TEXT = "Automatic keep-alive resume: your last turn ended with decision done and story_complete false. Continue the open work from where you stopped. End with done only when story_complete is true; if you are waiting, use hq lanes ask or stay in the loop.";
|
|
10
|
+
export function retiredLaneRegistryPath(hqRoot) {
|
|
11
|
+
return path.join(hqRoot, "workspace", "lane-briefs", "juniors.json");
|
|
12
|
+
}
|
|
13
|
+
/** Missing registry means no retired lanes; every present invalid file fails closed. */
|
|
14
|
+
export function loadRetiredLaneIds(hqRoot) {
|
|
15
|
+
const filePath = retiredLaneRegistryPath(hqRoot);
|
|
16
|
+
let text;
|
|
17
|
+
try {
|
|
18
|
+
const stat = fs.lstatSync(filePath);
|
|
19
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
20
|
+
throw new Error("expected a regular file");
|
|
21
|
+
}
|
|
22
|
+
text = fs.readFileSync(filePath, "utf8");
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (err.code === "ENOENT")
|
|
26
|
+
return new Set();
|
|
27
|
+
throw new LanesError("keep_alive_retired_list_invalid", `Cannot read retired lane list ${filePath}: ${err instanceof Error ? err.message : String(err)}`, { path: filePath });
|
|
28
|
+
}
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
throw new LanesError("keep_alive_retired_list_invalid", `Cannot parse retired lane list ${filePath}: ${err instanceof Error ? err.message : String(err)}`, { path: filePath });
|
|
35
|
+
}
|
|
36
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
37
|
+
throw invalidRetiredList(filePath, "root must be an object");
|
|
38
|
+
}
|
|
39
|
+
const retired = parsed.retired;
|
|
40
|
+
if (retired === undefined)
|
|
41
|
+
return new Set();
|
|
42
|
+
if (!Array.isArray(retired))
|
|
43
|
+
throw invalidRetiredList(filePath, "retired must be an array");
|
|
44
|
+
const laneIds = new Set();
|
|
45
|
+
for (const [index, entry] of retired.entries()) {
|
|
46
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
47
|
+
throw invalidRetiredList(filePath, `retired[${index}] must be an object with a lane id`);
|
|
48
|
+
}
|
|
49
|
+
const laneId = entry.lane;
|
|
50
|
+
if (typeof laneId !== "string" || laneId.trim() === "") {
|
|
51
|
+
throw invalidRetiredList(filePath, `retired[${index}].lane must be a non-empty string`);
|
|
52
|
+
}
|
|
53
|
+
laneIds.add(laneId.trim());
|
|
54
|
+
}
|
|
55
|
+
return laneIds;
|
|
56
|
+
}
|
|
57
|
+
function invalidRetiredList(filePath, message) {
|
|
58
|
+
return new LanesError("keep_alive_retired_list_invalid", `Invalid retired lane list ${filePath}: ${message}`, { path: filePath });
|
|
59
|
+
}
|
|
60
|
+
/** IDs may be stored bare or with an owner-host suffix after an underscore. */
|
|
61
|
+
export function isRetiredLaneId(laneId, retiredLaneIds) {
|
|
62
|
+
for (const retiredId of retiredLaneIds) {
|
|
63
|
+
if (retiredId === laneId ||
|
|
64
|
+
retiredId.startsWith(`${laneId}_`) ||
|
|
65
|
+
laneId.startsWith(`${retiredId}_`))
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
export function keepAliveSupersededBy(lane, lanes) {
|
|
71
|
+
const createdAt = Date.parse(lane.created_at);
|
|
72
|
+
if (!Number.isFinite(createdAt))
|
|
73
|
+
return undefined;
|
|
74
|
+
const lanesById = new Map(lanes.map((candidate) => [candidate.lane_id, candidate]));
|
|
75
|
+
return lanes.find((candidate) => {
|
|
76
|
+
if (candidate.lane_id === lane.lane_id || Date.parse(candidate.created_at) <= createdAt)
|
|
77
|
+
return false;
|
|
78
|
+
const sameCompany = lane.company === null
|
|
79
|
+
? candidate.company === null
|
|
80
|
+
: candidate.company?.uid === lane.company.uid;
|
|
81
|
+
if (!sameCompany)
|
|
82
|
+
return false;
|
|
83
|
+
// Any descendant is part of this lead's work tree, not a replacement lead.
|
|
84
|
+
let parentLaneId = candidate.senior.kind === "lane" ? candidate.senior.id : undefined;
|
|
85
|
+
const visitedLaneIds = new Set();
|
|
86
|
+
while (parentLaneId) {
|
|
87
|
+
if (parentLaneId === lane.lane_id)
|
|
88
|
+
return false;
|
|
89
|
+
if (visitedLaneIds.has(parentLaneId))
|
|
90
|
+
break;
|
|
91
|
+
visitedLaneIds.add(parentLaneId);
|
|
92
|
+
const parent = lanesById.get(parentLaneId);
|
|
93
|
+
parentLaneId = parent?.senior.kind === "lane" ? parent.senior.id : undefined;
|
|
94
|
+
}
|
|
95
|
+
const sameProfile = lane.worker_profile !== undefined &&
|
|
96
|
+
candidate.worker_profile === lane.worker_profile;
|
|
97
|
+
const projectLead = lane.project_id !== undefined &&
|
|
98
|
+
candidate.project_id === lane.project_id &&
|
|
99
|
+
(candidate.hire_grant ?? 0) > 0;
|
|
100
|
+
return sameProfile || projectLead;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function keepAliveExclusion(lane, lanes, retiredLaneIds) {
|
|
104
|
+
if (isRetiredLaneId(lane.lane_id, retiredLaneIds))
|
|
105
|
+
return "retired";
|
|
106
|
+
return keepAliveSupersededBy(lane, lanes) === undefined ? undefined : "superseded";
|
|
107
|
+
}
|
|
7
108
|
export function leadLaneKeepAliveBackoffMs(attempt) {
|
|
8
109
|
if (!Number.isSafeInteger(attempt) || attempt < 1) {
|
|
9
110
|
throw new RangeError("keep-alive attempt must be a positive safe integer");
|
|
@@ -92,6 +92,7 @@ export interface CreateLaneMeshOptions {
|
|
|
92
92
|
}) => Promise<void>;
|
|
93
93
|
}
|
|
94
94
|
export declare function storyNotOnBoardError(company: LaneCompanyRef, projectId: string, storyId: string): LanesError;
|
|
95
|
+
export declare function laneStoryStatusSuppressionKey(company: LaneCompanyRef, projectId: string, storyId: string): string;
|
|
95
96
|
export declare function createLaneMesh(opts?: CreateLaneMeshOptions): LaneMesh;
|
|
96
97
|
/** Resolve the explicit mesh attribution before falling back to the lane tenant. */
|
|
97
98
|
export declare function laneMeshCompany(lane: Pick<LaneRecord, "company" | "mesh_company">): LaneCompanyRef | undefined;
|
|
@@ -106,6 +107,8 @@ export declare function laneMeshEventIdentity(lane: LaneRecord): Record<string,
|
|
|
106
107
|
export declare function emitLaneDecisionEvents(mesh: LaneMesh, lane: LaneRecord, envelope: Pick<LaneEnvelope, "decision" | "summary" | "blocked_reason">, opts?: {
|
|
107
108
|
questionIds?: string[];
|
|
108
109
|
signal?: AbortSignal;
|
|
110
|
+
/** Per-watcher-run cache for story_not_on_board refusals. */
|
|
111
|
+
storyStatusSuppressions?: Set<string>;
|
|
109
112
|
}): Promise<void>;
|
|
110
113
|
/** Emit an abnormal terminal state even when the worker produced no envelope. */
|
|
111
114
|
export declare function emitLaneTerminalStateEvent(mesh: LaneMesh, lane: LaneRecord, state: Exclude<TerminalLaneState, "done">, summary?: string, signal?: AbortSignal): Promise<void>;
|