@indigoai-us/hq-cli 5.215.0 → 5.217.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 +23 -0
- package/dist/commands/lanes.js +39 -53
- 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 +1 -0
- package/dist/lib/lanes/keep-alive.js +1 -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 +5 -1
- package/dist/lib/lanes/profile.js +9 -6
- package/dist/lib/lanes/questions.d.ts +1 -1
- package/dist/lib/lanes/questions.js +2 -2
- package/dist/lib/lanes/reconcile.d.ts +2 -0
- package/dist/lib/lanes/reconcile.js +234 -77
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +6 -1
- package/dist/lib/lanes/store.js +8 -0
- package/dist/lib/lanes/types.d.ts +5 -1
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.d.ts +3 -1
- package/dist/lib/lanes/watcher.js +175 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.217.0] — 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Keep-alive resumes a lane interrupted by `exited_with_partial_output` when
|
|
10
|
+
the interruption followed its explicit opt-in. Quota and host-loss
|
|
11
|
+
interruptions, login failures, and stopped lanes remain ineligible.
|
|
12
|
+
|
|
13
|
+
## [5.216.0] — 2026-09-26
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Lane watchers keep valid questions from a malformed `ask` envelope when
|
|
18
|
+
repair is skipped, cannot start, or still produces invalid output. The lane
|
|
19
|
+
stores the validation errors, and recovered question context includes them.
|
|
20
|
+
Repair spawns ignore unknown profile metadata and log the field names.
|
|
21
|
+
`hq lanes create` and `hq lanes resume --worker` keep strict validation.
|
|
22
|
+
|
|
23
|
+
- `hq lanes` stops sending status updates for stories missing from the project
|
|
24
|
+
board. The watcher reports the refusal once per lane run and keeps appending
|
|
25
|
+
lane events.
|
|
26
|
+
|
|
5
27
|
## [5.215.0] — 2026-09-26
|
|
6
28
|
|
|
7
29
|
### Fixed
|
|
@@ -9,6 +31,7 @@
|
|
|
9
31
|
- hq-cli 5.211.0 could resume a retired lead from its worker profile's
|
|
10
32
|
`keep_alive` value. Reconcile now requires an explicit lane opt-in written
|
|
11
33
|
after the latest envelope, and skips retired or superseded leads.
|
|
34
|
+
|
|
12
35
|
## [5.214.0] — 2026-09-26
|
|
13
36
|
|
|
14
37
|
### Added
|
package/dist/commands/lanes.js
CHANGED
|
@@ -36,7 +36,7 @@ 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, withAllLaneAdmissionLocks, 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
42
|
import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
|
|
@@ -897,41 +897,6 @@ async function withResumeHireAdmission(ctx, lane, fn) {
|
|
|
897
897
|
return fn();
|
|
898
898
|
});
|
|
899
899
|
}
|
|
900
|
-
function hireGrantCapacityError(parent, children) {
|
|
901
|
-
const grant = parent.hire_grant ?? 0;
|
|
902
|
-
const grantor = (parent.senior.kind === "lane" || parent.senior.kind === "session") &&
|
|
903
|
-
parent.senior.id
|
|
904
|
-
? { kind: parent.senior.kind, id: parent.senior.id }
|
|
905
|
-
: null;
|
|
906
|
-
const grantorReason = grantor
|
|
907
|
-
? undefined
|
|
908
|
-
: parent.senior.kind === "user"
|
|
909
|
-
? "senior_is_user"
|
|
910
|
-
: "parent_senior_missing_id";
|
|
911
|
-
const capacity = children.length === grant
|
|
912
|
-
? `is at its hire grant (${children.length} of ${grant} alive children)`
|
|
913
|
-
: `has exhausted its hire grant (${children.length} alive children; grant ${grant})`;
|
|
914
|
-
const grantCommand = `hq lanes grant ${parent.lane_id} --hire <n>`;
|
|
915
|
-
const authorityGuidance = grantor
|
|
916
|
-
? `Its senior ${grantor.kind} ${grantor.id} can raise it with: ${grantCommand}`
|
|
917
|
-
: parent.senior.kind === "user"
|
|
918
|
-
? `Its senior is the user and can raise it with: ${grantCommand}`
|
|
919
|
-
: `No senior is recorded, so an operator must resolve ownership before running: ${grantCommand}`;
|
|
920
|
-
const message = `Lane ${parent.lane_id} ${capacity}. ${authorityGuidance}`;
|
|
921
|
-
return new LanesError("hire_grant_exhausted", message, {
|
|
922
|
-
senior_lane_id: parent.lane_id,
|
|
923
|
-
parent_grant: grant,
|
|
924
|
-
alive_children: children.map((child) => child.lane_id),
|
|
925
|
-
alive_child_count: children.length,
|
|
926
|
-
grantor,
|
|
927
|
-
...(grantorReason ? { grantor_reason: grantorReason } : {}),
|
|
928
|
-
message,
|
|
929
|
-
ask_guidance: {
|
|
930
|
-
envelope: "decision=ask; include the desired total grant and the reason",
|
|
931
|
-
senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
|
|
932
|
-
},
|
|
933
|
-
});
|
|
934
|
-
}
|
|
935
900
|
const CAPACITY_PROBE_TIMEOUT_MS = 15_000;
|
|
936
901
|
const MAX_PLACEMENT_OUTPOSTS = 16;
|
|
937
902
|
function outpostIdentityUnknownError(message, expectedId, details = {}) {
|
|
@@ -2072,7 +2037,7 @@ async function emitLaneTerminalStateSoft(ctx, lane) {
|
|
|
2072
2037
|
process.stderr.write(`hq lanes: terminal mesh update failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
2073
2038
|
}
|
|
2074
2039
|
}
|
|
2075
|
-
async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
2040
|
+
async function emitLaneStartedForStories(ctx, company, lane, threadId, knownAbsentStories = new Set()) {
|
|
2076
2041
|
let ready = lane;
|
|
2077
2042
|
if (ctx.mesh.ensureLaneThread) {
|
|
2078
2043
|
try {
|
|
@@ -2108,6 +2073,8 @@ async function emitLaneStartedForStories(ctx, company, lane, threadId) {
|
|
|
2108
2073
|
failures.push(err);
|
|
2109
2074
|
continue;
|
|
2110
2075
|
}
|
|
2076
|
+
if (knownAbsentStories.has(storyBoardStoryKey(story.project_id, story.story_id)))
|
|
2077
|
+
continue;
|
|
2111
2078
|
try {
|
|
2112
2079
|
await ctx.mesh.patchStoryStatus(company, story.project_id, story.story_id, "in_progress");
|
|
2113
2080
|
}
|
|
@@ -2135,20 +2102,30 @@ function storyNotOnBoardWarning(err) {
|
|
|
2135
2102
|
? err.message
|
|
2136
2103
|
: undefined;
|
|
2137
2104
|
}
|
|
2105
|
+
function storyBoardStoryKey(projectId, storyId) {
|
|
2106
|
+
return JSON.stringify([projectId, storyId]);
|
|
2107
|
+
}
|
|
2138
2108
|
function combinedStoryWarning(lane, additional = []) {
|
|
2139
2109
|
const warnings = [storyWarning(lane), ...additional]
|
|
2140
2110
|
.filter((warning) => warning !== undefined && warning.trim().length > 0);
|
|
2141
2111
|
return [...new Set(warnings)].join(" ") || null;
|
|
2142
2112
|
}
|
|
2143
|
-
async function
|
|
2113
|
+
async function storyBoardCheck(ctx, company, projectId, storyIds) {
|
|
2144
2114
|
try {
|
|
2145
2115
|
await assertStoriesOnBoard(ctx, company, projectId, storyIds);
|
|
2146
|
-
return
|
|
2116
|
+
return { absentStoryIds: new Set() };
|
|
2147
2117
|
}
|
|
2148
2118
|
catch (err) {
|
|
2149
2119
|
const warning = storyNotOnBoardWarning(err);
|
|
2150
|
-
if (warning !== undefined)
|
|
2151
|
-
|
|
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
|
+
}
|
|
2152
2129
|
throw err;
|
|
2153
2130
|
}
|
|
2154
2131
|
}
|
|
@@ -2451,10 +2428,14 @@ async function runCreate(ctx, opts) {
|
|
|
2451
2428
|
const seniorCompany = companyOfSenior(hqRoot, senior, ctx);
|
|
2452
2429
|
assertSameTenant(company, senior, seniorCompany);
|
|
2453
2430
|
const createStoryWarnings = [];
|
|
2431
|
+
const knownAbsentStories = new Set();
|
|
2454
2432
|
if (meshCompany && storyIds.length > 0) {
|
|
2455
|
-
const
|
|
2456
|
-
if (warning !== undefined)
|
|
2457
|
-
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
|
+
}
|
|
2458
2439
|
}
|
|
2459
2440
|
let resolvedWorker;
|
|
2460
2441
|
if (opts.worker) {
|
|
@@ -3067,7 +3048,7 @@ async function runCreate(ctx, opts) {
|
|
|
3067
3048
|
locateTranscript: ctx.locateTranscript,
|
|
3068
3049
|
});
|
|
3069
3050
|
if (meshCompany) {
|
|
3070
|
-
createStoryWarnings.push(...await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id));
|
|
3051
|
+
createStoryWarnings.push(...await emitLaneStartedForStories(ctx, meshCompany, started, started.thread_id, knownAbsentStories));
|
|
3071
3052
|
}
|
|
3072
3053
|
}
|
|
3073
3054
|
else if (outpostId) {
|
|
@@ -4510,6 +4491,7 @@ async function runShow(ctx, laneId) {
|
|
|
4510
4491
|
provider_session_id: lane.provider_session_id ?? null,
|
|
4511
4492
|
provider_session_id_absent: lane.provider_session_id_absent ?? null,
|
|
4512
4493
|
envelope_repair_mode: lane.envelope_repair?.mode ?? null,
|
|
4494
|
+
envelope_validation_errors: lane.envelope_validation_errors ?? [],
|
|
4513
4495
|
last_resume: lane.last_resume ?? null,
|
|
4514
4496
|
account: lane.account ?? null,
|
|
4515
4497
|
account_history: lane.account_history ?? [],
|
|
@@ -6327,7 +6309,7 @@ async function ensureLaneThreadRecord(ctx, lane, projectId = lane.project_id, st
|
|
|
6327
6309
|
}
|
|
6328
6310
|
return updated;
|
|
6329
6311
|
}
|
|
6330
|
-
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}) {
|
|
6312
|
+
async function emitStoryTransition(ctx, lane, projectId, storyId, event, status, payload = {}, skipStoryStatusPatch = false) {
|
|
6331
6313
|
const company = lane.mesh_company ?? lane.company;
|
|
6332
6314
|
if (!company)
|
|
6333
6315
|
return;
|
|
@@ -6341,7 +6323,9 @@ async function emitStoryTransition(ctx, lane, projectId, storyId, event, status,
|
|
|
6341
6323
|
...payload,
|
|
6342
6324
|
lane_status: lane.state,
|
|
6343
6325
|
});
|
|
6344
|
-
|
|
6326
|
+
if (!skipStoryStatusPatch) {
|
|
6327
|
+
await ctx.mesh.patchStoryStatus(company, projectId, storyId, status === "blocked" ? "in_progress" : status);
|
|
6328
|
+
}
|
|
6345
6329
|
}
|
|
6346
6330
|
function sameCommittedStory(current, expected, projectId, storyId) {
|
|
6347
6331
|
const currentStory = laneStory(current, projectId, storyId);
|
|
@@ -6350,9 +6334,9 @@ function sameCommittedStory(current, expected, projectId, storyId) {
|
|
|
6350
6334
|
current.story_id === expected.story_id &&
|
|
6351
6335
|
JSON.stringify(currentStory) === JSON.stringify(expectedStory));
|
|
6352
6336
|
}
|
|
6353
|
-
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) {
|
|
6354
6338
|
try {
|
|
6355
|
-
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload);
|
|
6339
|
+
await emitStoryTransition(ctx, next, projectId, storyId, event, status, payload, skipStoryStatusPatch);
|
|
6356
6340
|
return undefined;
|
|
6357
6341
|
}
|
|
6358
6342
|
catch (err) {
|
|
@@ -6408,10 +6392,12 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
6408
6392
|
}
|
|
6409
6393
|
const company = lane.mesh_company ?? lane.company;
|
|
6410
6394
|
const warnings = [];
|
|
6395
|
+
let storyKnownAbsent = false;
|
|
6411
6396
|
if (company) {
|
|
6412
|
-
const
|
|
6413
|
-
if (warning !== undefined)
|
|
6414
|
-
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);
|
|
6415
6401
|
}
|
|
6416
6402
|
const at = ctx.now().toISOString();
|
|
6417
6403
|
const thread = await ensureStoryThreadForTransition(ctx, lane, projectId, normalizedStoryId);
|
|
@@ -6427,7 +6413,7 @@ async function runStoryAdd(ctx, laneId, storyId, opts) {
|
|
|
6427
6413
|
no_story_reason: undefined,
|
|
6428
6414
|
};
|
|
6429
6415
|
});
|
|
6430
|
-
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);
|
|
6431
6417
|
if (transitionWarning !== undefined)
|
|
6432
6418
|
warnings.push(transitionWarning);
|
|
6433
6419
|
const finalLane = readLane(hqRoot, laneId);
|
|
@@ -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";
|
|
@@ -5,6 +5,7 @@ export declare const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
|
5
5
|
export declare const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS: number;
|
|
6
6
|
export declare const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS: number;
|
|
7
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 declare const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. 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
9
|
export type KeepAliveBlockedReason = NonNullable<LaneRecord["keep_alive_blocked_reason"]>;
|
|
9
10
|
export declare function retiredLaneRegistryPath(hqRoot: string): string;
|
|
10
11
|
/** Missing registry means no retired lanes; every present invalid file fails closed. */
|
|
@@ -7,6 +7,7 @@ export const LEAD_LANE_KEEP_ALIVE_MAX_RESUMES = 24;
|
|
|
7
7
|
export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
|
|
8
8
|
export const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
|
|
9
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 const LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT = "Automatic keep-alive resume: your previous turn was interrupted by a provider process exit. Check your last state before continuing. 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
11
|
export function retiredLaneRegistryPath(hqRoot) {
|
|
11
12
|
return path.join(hqRoot, "workspace", "lane-briefs", "juniors.json");
|
|
12
13
|
}
|
|
@@ -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>;
|
|
@@ -229,6 +229,17 @@ function isUnknownProjectStory(error) {
|
|
|
229
229
|
typeof candidate.message === "string" &&
|
|
230
230
|
candidate.message.includes("Unknown story"));
|
|
231
231
|
}
|
|
232
|
+
function isUncodedStoryPatchNotFound(error) {
|
|
233
|
+
if (!error || typeof error !== "object")
|
|
234
|
+
return false;
|
|
235
|
+
const candidate = error;
|
|
236
|
+
// The story-absent route response is exactly `404 Not found` without a code.
|
|
237
|
+
// Keep coded 404s, especially PROJECT_VIEW_NOT_FOUND, on their own paths.
|
|
238
|
+
return (candidate.status === 404 &&
|
|
239
|
+
candidate.code === undefined &&
|
|
240
|
+
typeof candidate.message === "string" &&
|
|
241
|
+
/^404 not found$/i.test(candidate.message));
|
|
242
|
+
}
|
|
232
243
|
function shellQuote(value) {
|
|
233
244
|
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
234
245
|
}
|
|
@@ -245,6 +256,9 @@ export function storyNotOnBoardError(company, projectId, storyId) {
|
|
|
245
256
|
command,
|
|
246
257
|
});
|
|
247
258
|
}
|
|
259
|
+
export function laneStoryStatusSuppressionKey(company, projectId, storyId) {
|
|
260
|
+
return JSON.stringify([company.uid, projectId, storyId]);
|
|
261
|
+
}
|
|
248
262
|
function isAlreadyRegistered(error) {
|
|
249
263
|
if (!error || typeof error !== "object")
|
|
250
264
|
return false;
|
|
@@ -365,7 +379,7 @@ export function createLaneMesh(opts = {}) {
|
|
|
365
379
|
await patchStoryStatus(tok, company.uid, projectId, storyId, status);
|
|
366
380
|
}
|
|
367
381
|
catch (error) {
|
|
368
|
-
if (isUnknownProjectStory(error)) {
|
|
382
|
+
if (isUnknownProjectStory(error) || isUncodedStoryPatchNotFound(error)) {
|
|
369
383
|
throw storyNotOnBoardError(company, projectId, storyId);
|
|
370
384
|
}
|
|
371
385
|
throw error;
|
|
@@ -437,6 +451,21 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
437
451
|
const threadId = await ensureLaneEventThread(mesh, company, lane, lane.state, opts.signal);
|
|
438
452
|
if (!company || !threadId)
|
|
439
453
|
return;
|
|
454
|
+
const patchStoryStatus = async (status) => {
|
|
455
|
+
const suppressions = opts.storyStatusSuppressions;
|
|
456
|
+
const key = laneStoryStatusSuppressionKey(company, lane.project_id, lane.story_id);
|
|
457
|
+
if (suppressions?.has(key))
|
|
458
|
+
return;
|
|
459
|
+
try {
|
|
460
|
+
await mesh.patchStoryStatus(company, lane.project_id, lane.story_id, status);
|
|
461
|
+
}
|
|
462
|
+
catch (err) {
|
|
463
|
+
if (suppressions && err instanceof LanesError && err.error === "story_not_on_board") {
|
|
464
|
+
suppressions.add(key);
|
|
465
|
+
}
|
|
466
|
+
throw err;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
440
469
|
switch (envelope.decision) {
|
|
441
470
|
case "done": {
|
|
442
471
|
const outcomes = await Promise.allSettled([
|
|
@@ -444,7 +473,7 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
444
473
|
...laneMeshEventIdentity(lane),
|
|
445
474
|
summary: envelope.summary,
|
|
446
475
|
}, opts.signal),
|
|
447
|
-
|
|
476
|
+
patchStoryStatus("review"),
|
|
448
477
|
]);
|
|
449
478
|
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
450
479
|
if (failure?.status === "rejected")
|
|
@@ -458,7 +487,7 @@ export async function emitLaneDecisionEvents(mesh, lane, envelope, opts = {}) {
|
|
|
458
487
|
...laneMeshEventIdentity(lane),
|
|
459
488
|
blocked_reason: envelope.blocked_reason ?? "",
|
|
460
489
|
}),
|
|
461
|
-
|
|
490
|
+
patchStoryStatus("in_progress"),
|
|
462
491
|
]);
|
|
463
492
|
const failure = outcomes.find((outcome) => outcome.status === "rejected");
|
|
464
493
|
if (failure?.status === "rejected")
|
|
@@ -33,10 +33,14 @@ export interface WorkerProfileRuntime {
|
|
|
33
33
|
settingsPath?: string;
|
|
34
34
|
hooksDir?: string;
|
|
35
35
|
}
|
|
36
|
+
export interface LoadWorkerProfileOptions {
|
|
37
|
+
/** Unknown keys may be ignored only by callers with an explicit recovery path. */
|
|
38
|
+
onUnknownMetaFields?: (fields: readonly string[]) => void;
|
|
39
|
+
}
|
|
36
40
|
export declare function personalWorkerProfileDir(hqRoot: string, id: string): string;
|
|
37
41
|
/** A meta.yaml file is the explicit marker for a personal worker profile. */
|
|
38
42
|
export declare function workerProfileMarker(hqRoot: string, id: string): "absent" | "present";
|
|
39
|
-
export declare function loadWorkerProfile(hqRoot: string, id: string): WorkerProfile;
|
|
43
|
+
export declare function loadWorkerProfile(hqRoot: string, id: string, options?: LoadWorkerProfileOptions): WorkerProfile;
|
|
40
44
|
export interface EffectiveLaneKeepAlive {
|
|
41
45
|
enabled: boolean;
|
|
42
46
|
source: "lane" | "none";
|
|
@@ -117,15 +117,18 @@ function collectFiles(id, dir, relativeDir) {
|
|
|
117
117
|
visit(absoluteDir, relativeDir);
|
|
118
118
|
return files.sort((a, b) => a.localeCompare(b));
|
|
119
119
|
}
|
|
120
|
-
function parseMeta(id, dir, raw) {
|
|
120
|
+
function parseMeta(id, dir, raw, options = {}) {
|
|
121
121
|
if (!isPlainObject(raw)) {
|
|
122
122
|
throw profileError(id, dir, "meta.yaml must contain an object");
|
|
123
123
|
}
|
|
124
124
|
const unknown = Object.keys(raw).filter((key) => !PROFILE_META_KEYS.has(key));
|
|
125
125
|
if (unknown.length > 0) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
if (options.onUnknownMetaFields === undefined) {
|
|
127
|
+
throw profileError(id, dir, `meta.yaml has unknown field(s): ${unknown.join(", ")}`, {
|
|
128
|
+
fields: unknown,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
options.onUnknownMetaFields(unknown);
|
|
129
132
|
}
|
|
130
133
|
const provider = raw.provider;
|
|
131
134
|
if (typeof provider !== "string" || !isLaneProvider(provider.trim())) {
|
|
@@ -305,7 +308,7 @@ export function workerProfileMarker(hqRoot, id) {
|
|
|
305
308
|
return "present";
|
|
306
309
|
return "present";
|
|
307
310
|
}
|
|
308
|
-
export function loadWorkerProfile(hqRoot, id) {
|
|
311
|
+
export function loadWorkerProfile(hqRoot, id, options = {}) {
|
|
309
312
|
const dir = personalWorkerProfileDir(hqRoot, id);
|
|
310
313
|
if (!validProfileId(id)) {
|
|
311
314
|
throw profileError(id, dir, "profile id must be a simple directory name", {
|
|
@@ -338,7 +341,7 @@ export function loadWorkerProfile(hqRoot, id) {
|
|
|
338
341
|
field: PROFILE_META,
|
|
339
342
|
});
|
|
340
343
|
}
|
|
341
|
-
const meta = parseMeta(id, dir, metaRaw);
|
|
344
|
+
const meta = parseMeta(id, dir, metaRaw, options);
|
|
342
345
|
const systemPromptPath = path.join(dir, PROFILE_SYSTEM_PROMPT);
|
|
343
346
|
const systemPrompt = readRequiredText(id, dir, systemPromptPath, PROFILE_SYSTEM_PROMPT);
|
|
344
347
|
if (systemPrompt.trim().length === 0) {
|
|
@@ -71,5 +71,5 @@ export declare function envelopeQuestionId(laneId: string, envelopeRef: string,
|
|
|
71
71
|
* Spec §6.1: each envelope questions[] entry becomes one queue record, via
|
|
72
72
|
* raiseLaneQuestion (the same function the CLI ask path uses).
|
|
73
73
|
*/
|
|
74
|
-
export declare function raiseEnvelopeQuestions(hqRoot: string, lane: LaneRecord, envelope: LaneEnvelope, envelopeRef: string, at: string): QuestionRecord[];
|
|
74
|
+
export declare function raiseEnvelopeQuestions(hqRoot: string, lane: LaneRecord, envelope: LaneEnvelope, envelopeRef: string, at: string, questionIndexes?: readonly number[]): QuestionRecord[];
|
|
75
75
|
//# sourceMappingURL=questions.d.ts.map
|
|
@@ -222,7 +222,7 @@ function envelopeQuestionBody(item) {
|
|
|
222
222
|
* Spec §6.1: each envelope questions[] entry becomes one queue record, via
|
|
223
223
|
* raiseLaneQuestion (the same function the CLI ask path uses).
|
|
224
224
|
*/
|
|
225
|
-
export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at) {
|
|
225
|
+
export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at, questionIndexes) {
|
|
226
226
|
const items = envelope.questions ?? [];
|
|
227
227
|
const raised = [];
|
|
228
228
|
for (let i = 0; i < items.length; i += 1) {
|
|
@@ -235,7 +235,7 @@ export function raiseEnvelopeQuestions(hqRoot, lane, envelope, envelopeRef, at)
|
|
|
235
235
|
body: envelopeQuestionBody(item),
|
|
236
236
|
raisedVia: "envelope",
|
|
237
237
|
at,
|
|
238
|
-
questionId: envelopeQuestionId(lane.lane_id, envelopeRef, i),
|
|
238
|
+
questionId: envelopeQuestionId(lane.lane_id, envelopeRef, questionIndexes?.[i] ?? i),
|
|
239
239
|
}));
|
|
240
240
|
}
|
|
241
241
|
return raised;
|
|
@@ -78,6 +78,8 @@ export interface ReconcileDeps {
|
|
|
78
78
|
mesh?: LaneMesh | null;
|
|
79
79
|
/** Bound on one terminal mesh emit during dead-host recovery. Default 10 s. */
|
|
80
80
|
meshEmitTimeoutMs?: number;
|
|
81
|
+
/** Fresh keep-alive eligibility read; injected to exercise opt-in changes before queueing. */
|
|
82
|
+
readKeepAliveRecheckLane?: (hqRoot: string, laneId: string) => LaneRecord;
|
|
81
83
|
/**
|
|
82
84
|
* Awaited after lane/question records and liveness are loaded, before any
|
|
83
85
|
* CAS write. Test seam so two in-process passes can share a snapshot.
|