@indigoai-us/hq-cli 5.175.0 → 5.177.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 +21 -0
- package/dist/command-catalog.generated.d.ts +40 -0
- package/dist/command-catalog.generated.js +52 -0
- package/dist/commands/bot.d.ts +1 -0
- package/dist/commands/bot.js +96 -5
- package/dist/commands/lanes.d.ts +4 -1
- package/dist/commands/lanes.js +605 -121
- package/dist/lib/lanes/hire-grants.d.ts +25 -0
- package/dist/lib/lanes/hire-grants.js +85 -0
- package/dist/lib/lanes/index.d.ts +3 -1
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +3 -1
- package/dist/lib/lanes/outpost-dispatch.js +7 -1
- package/dist/lib/lanes/store.d.ts +5 -1
- package/dist/lib/lanes/store.js +25 -3
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type LaneRecord } from "./types.js";
|
|
2
|
+
export declare const LANE_HIRE_GRANTS_FLAG = "lanes.hire-grants";
|
|
3
|
+
export declare function aliveDirectChildren(lanes: readonly LaneRecord[], parentLaneId: string): LaneRecord[];
|
|
4
|
+
export declare function assignedChildHireGrants(children: readonly LaneRecord[], exceptLaneId?: string): number;
|
|
5
|
+
export interface HireGrantAdmissionSnapshot {
|
|
6
|
+
parent: LaneRecord;
|
|
7
|
+
children: LaneRecord[];
|
|
8
|
+
parentGrant: number;
|
|
9
|
+
availableChildGrant: number;
|
|
10
|
+
}
|
|
11
|
+
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
12
|
+
export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T): T;
|
|
13
|
+
export interface HireGrantBackfillRow {
|
|
14
|
+
lane_id: string;
|
|
15
|
+
existing_grant: number | null;
|
|
16
|
+
alive_children: number;
|
|
17
|
+
profile_max_concurrent: number | null;
|
|
18
|
+
allocated_child_grants: number;
|
|
19
|
+
proposed_grant: number;
|
|
20
|
+
grant_shortfall: number;
|
|
21
|
+
action: "set" | "unchanged";
|
|
22
|
+
}
|
|
23
|
+
/** Select alive manager lanes and calculate the non-destructive one-time backfill. */
|
|
24
|
+
export declare function planHireGrantBackfill(lanes: readonly LaneRecord[], profileMaxConcurrent: ReadonlyMap<string, number>): HireGrantBackfillRow[];
|
|
25
|
+
//# sourceMappingURL=hire-grants.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { isTerminalLaneState } from "./types.js";
|
|
2
|
+
import { listLaneIds, readLane, withLaneHireLocks } from "./store.js";
|
|
3
|
+
import { LanesError } from "./errors.js";
|
|
4
|
+
export const LANE_HIRE_GRANTS_FLAG = "lanes.hire-grants";
|
|
5
|
+
export function aliveDirectChildren(lanes, parentLaneId) {
|
|
6
|
+
return lanes.filter((lane) => lane.senior.kind === "lane" &&
|
|
7
|
+
lane.senior.id === parentLaneId &&
|
|
8
|
+
!isTerminalLaneState(lane.state));
|
|
9
|
+
}
|
|
10
|
+
export function assignedChildHireGrants(children, exceptLaneId) {
|
|
11
|
+
return children.reduce((sum, child) => child.lane_id === exceptLaneId ? sum : sum + (child.hire_grant ?? 0), 0);
|
|
12
|
+
}
|
|
13
|
+
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
14
|
+
export function withHireGrantAdmission(hqRoot, parentLaneId, fn) {
|
|
15
|
+
return withLaneHireLocks(hqRoot, [parentLaneId], () => {
|
|
16
|
+
const parent = readLane(hqRoot, parentLaneId);
|
|
17
|
+
const allLanes = listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
|
|
18
|
+
const children = aliveDirectChildren(allLanes, parentLaneId);
|
|
19
|
+
const parentGrant = parent.hire_grant ?? 0;
|
|
20
|
+
const availableChildGrant = Math.max(0, parentGrant - assignedChildHireGrants(children));
|
|
21
|
+
return fn({ parent, children, parentGrant, availableChildGrant });
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Select alive manager lanes and calculate the non-destructive one-time backfill. */
|
|
25
|
+
export function planHireGrantBackfill(lanes, profileMaxConcurrent) {
|
|
26
|
+
const candidates = lanes.filter((lane) => {
|
|
27
|
+
if (isTerminalLaneState(lane.state))
|
|
28
|
+
return false;
|
|
29
|
+
return aliveDirectChildren(lanes, lane.lane_id).length > 0 ||
|
|
30
|
+
lane.worker_profile === "hq-lanes-manager";
|
|
31
|
+
});
|
|
32
|
+
const candidateIds = new Set(candidates.map((lane) => lane.lane_id));
|
|
33
|
+
const proposed = new Map();
|
|
34
|
+
const active = new Set();
|
|
35
|
+
const proposalFor = (lane) => {
|
|
36
|
+
const cached = proposed.get(lane.lane_id);
|
|
37
|
+
if (cached !== undefined)
|
|
38
|
+
return cached;
|
|
39
|
+
if (active.has(lane.lane_id)) {
|
|
40
|
+
throw new LanesError("hire_grant_backfill_cycle", `Cannot plan hire grants for cyclic lane hierarchy at ${lane.lane_id}`, { lane_id: lane.lane_id });
|
|
41
|
+
}
|
|
42
|
+
active.add(lane.lane_id);
|
|
43
|
+
const children = aliveDirectChildren(lanes, lane.lane_id);
|
|
44
|
+
const allocated = children.reduce((sum, child) => {
|
|
45
|
+
if (child.hire_grant !== undefined)
|
|
46
|
+
return sum + child.hire_grant;
|
|
47
|
+
return sum + (candidateIds.has(child.lane_id) ? proposalFor(child) : 0);
|
|
48
|
+
}, 0);
|
|
49
|
+
const profileMax = lane.worker_profile
|
|
50
|
+
? profileMaxConcurrent.get(lane.worker_profile) ?? 0
|
|
51
|
+
: 0;
|
|
52
|
+
const value = lane.hire_grant ?? Math.max(children.length, profileMax, allocated);
|
|
53
|
+
active.delete(lane.lane_id);
|
|
54
|
+
proposed.set(lane.lane_id, value);
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
for (const lane of candidates)
|
|
58
|
+
proposalFor(lane);
|
|
59
|
+
return candidates
|
|
60
|
+
.map((lane) => {
|
|
61
|
+
const children = aliveDirectChildren(lanes, lane.lane_id);
|
|
62
|
+
const profileMax = lane.worker_profile
|
|
63
|
+
? profileMaxConcurrent.get(lane.worker_profile) ?? null
|
|
64
|
+
: null;
|
|
65
|
+
const existing = lane.hire_grant ?? null;
|
|
66
|
+
const allocatedChildGrants = children.reduce((sum, child) => {
|
|
67
|
+
if (child.hire_grant !== undefined)
|
|
68
|
+
return sum + child.hire_grant;
|
|
69
|
+
return sum + (candidateIds.has(child.lane_id) ? proposalFor(child) : 0);
|
|
70
|
+
}, 0);
|
|
71
|
+
const proposedGrant = proposalFor(lane);
|
|
72
|
+
return {
|
|
73
|
+
lane_id: lane.lane_id,
|
|
74
|
+
existing_grant: existing,
|
|
75
|
+
alive_children: children.length,
|
|
76
|
+
profile_max_concurrent: profileMax,
|
|
77
|
+
allocated_child_grants: allocatedChildGrants,
|
|
78
|
+
proposed_grant: proposedGrant,
|
|
79
|
+
grant_shortfall: Math.max(0, allocatedChildGrants - proposedGrant),
|
|
80
|
+
action: existing === null ? "set" : "unchanged",
|
|
81
|
+
};
|
|
82
|
+
})
|
|
83
|
+
.sort((a, b) => a.lane_id.localeCompare(b.lane_id));
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=hire-grants.js.map
|
|
@@ -3,6 +3,8 @@
|
|
|
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, withHireGrantAdmission } from "./hire-grants.js";
|
|
7
|
+
export type { HireGrantBackfillRow } from "./hire-grants.js";
|
|
6
8
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
7
9
|
export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
|
8
10
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
@@ -16,7 +18,7 @@ export { resolveLanesHqRoot } from "./hq-root.js";
|
|
|
16
18
|
export type { LanesHqRootOpts } from "./hq-root.js";
|
|
17
19
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
18
20
|
export type { OutpostDispatchResult, OutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
19
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, } from "./store.js";
|
|
21
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
20
22
|
export type { StoreDeps } from "./store.js";
|
|
21
23
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
22
24
|
export type { LaneWatchDeps, LaneWatchResult } from "./watch.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,6 +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, withHireGrantAdmission } from "./hire-grants.js";
|
|
6
7
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
7
8
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
8
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";
|
|
@@ -10,7 +11,7 @@ export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory,
|
|
|
10
11
|
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_MIN_AVAILABLE_MEM_MB, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
11
12
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
12
13
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
13
|
-
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, } from "./store.js";
|
|
14
|
+
export { LANES_STORE_REL, LANE_RECORDS_DIR, PENDING_INDEX_DIR, QUESTION_RECORDS_DIR, appendCappedHistory, appendLaneAttribution, appendLaneSeniorHistory, appendQuestionRouting, createLane, createQuestion, isConflictSidecarName, laneRecordPath, lanesStoreRoot, listLaneIds, listQuestionIds, mergeLaneRecords, mergeQuestionRecords, mintLaneId, mintQuestionId, parseLaneRecord, parseQuestionRecord, pendingMarkerPath, questionRecordPath, readLane, readQuestion, resolveStoreHqRoot, sanitizeHostComponent, tryReadLane, tryReadQuestion, retryUpdateLane, retryUpdateQuestion, syncLaneRecordFromOwner, updateLane, updateQuestion, withLaneHireLocks, } from "./store.js";
|
|
14
15
|
export { DEFAULT_LANE_WATCH_INTERVAL_MS, MAX_LANE_WATCH_INTERVAL_MS, MAX_WATCHED_LANES, MAX_WATCH_OUTPUT_LINES, runLaneWatch, } from "./watch.js";
|
|
15
16
|
export { MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_TASK_OUTPUT_MAX_BYTES, MONITOR_TRANSCRIPT_MAX_BYTES, MONITOR_UNCOVERED_EXIT_CODE, coveredLaneIds, isMonitorTaskLive, monitorReminderText, parseCanonicalWatchCommand, parseMonitorTranscript, readLiveMonitorEvidence, } from "./monitor.js";
|
|
16
17
|
export { IMDS_HOST, IMDS_INSTANCE_ID_URL, IMDS_TIMEOUT_MS, IMDS_TOKEN_URL, defaultReadImdsInstanceId, resolveMachineIdentity, } from "./machine-identity.js";
|
|
@@ -27,13 +27,15 @@ export interface OutpostDispatchResult {
|
|
|
27
27
|
stderr: string;
|
|
28
28
|
}
|
|
29
29
|
export interface OutpostLaneCommand {
|
|
30
|
-
action: "message" | "resume" | "stop" | "show";
|
|
30
|
+
action: "message" | "resume" | "stop" | "show" | "grant";
|
|
31
31
|
laneId: string;
|
|
32
|
+
hqBinary?: string;
|
|
32
33
|
text?: string;
|
|
33
34
|
project?: string;
|
|
34
35
|
story?: string;
|
|
35
36
|
account?: string;
|
|
36
37
|
model?: string;
|
|
38
|
+
hire?: string;
|
|
37
39
|
compact?: boolean;
|
|
38
40
|
}
|
|
39
41
|
export interface OutpostExecPort {
|
|
@@ -73,13 +73,19 @@ export function buildOutpostIdentityBackfillCommand(expectedId) {
|
|
|
73
73
|
}
|
|
74
74
|
/** Build the same `hq outposts exec` remote command used by lane pickup. */
|
|
75
75
|
export function buildOutpostLaneCommand(input) {
|
|
76
|
-
const args = [
|
|
76
|
+
const args = [
|
|
77
|
+
input.hqBinary?.trim() || "hq",
|
|
78
|
+
"lanes",
|
|
79
|
+
input.action,
|
|
80
|
+
input.laneId,
|
|
81
|
+
];
|
|
77
82
|
for (const [flag, value] of [
|
|
78
83
|
["--text", input.text],
|
|
79
84
|
["--project", input.project],
|
|
80
85
|
["--story", input.story],
|
|
81
86
|
["--account", input.account],
|
|
82
87
|
["--model", input.model],
|
|
88
|
+
["--hire", input.hire],
|
|
83
89
|
]) {
|
|
84
90
|
if (value !== undefined)
|
|
85
91
|
args.push(flag, value);
|
|
@@ -53,13 +53,17 @@ export declare function readLane(hqRoot: string, laneId: string, deps?: StoreDep
|
|
|
53
53
|
export declare function tryReadQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord | null;
|
|
54
54
|
export declare function readQuestion(hqRoot: string, questionId: string, deps?: StoreDeps): QuestionRecord;
|
|
55
55
|
export declare function createLane(hqRoot: string, init: Omit<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at"> & Partial<Pick<LaneRecord, "schema_version" | "lane_id" | "version" | "created_at" | "updated_at">>, deps?: StoreDeps): LaneRecord;
|
|
56
|
+
/** Hierarchy lock: separate from record writes so callers can CAS-write while holding it. */
|
|
57
|
+
export declare function withLaneHireLocks<T>(hqRoot: string, laneIds: readonly string[], fn: () => T, deps?: StoreDeps): T;
|
|
56
58
|
export declare function updateLane(hqRoot: string, laneId: string, expectedVersion: number, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps): LaneRecord;
|
|
57
59
|
/**
|
|
58
60
|
* Replace the primary copy with a terminal snapshot returned by the Outpost
|
|
59
61
|
* that owns the lane. The owner is authoritative on equal versions; a newer
|
|
60
62
|
* primary version is a conflict and must not be overwritten.
|
|
61
63
|
*/
|
|
62
|
-
export declare function syncLaneRecordFromOwner(hqRoot: string, laneId: string, outpostId: string, rawOwnerLane: unknown, deps?: StoreDeps
|
|
64
|
+
export declare function syncLaneRecordFromOwner(hqRoot: string, laneId: string, outpostId: string, rawOwnerLane: unknown, deps?: StoreDeps, options?: {
|
|
65
|
+
allowNonTerminal?: boolean;
|
|
66
|
+
}): LaneRecord;
|
|
63
67
|
/** Compare-and-set with conflict retry. Callers that race a concurrent writer re-read. */
|
|
64
68
|
export declare function retryUpdateLane(hqRoot: string, laneId: string, writer: LaneWriter, updater: (current: LaneRecord) => LaneRecord, deps?: StoreDeps, attempts?: number): LaneRecord;
|
|
65
69
|
/** Compare-and-set with conflict retry for question records (spec §6.2). */
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -388,6 +388,14 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
388
388
|
requireInt(raw.version, "version");
|
|
389
389
|
requireInt(raw.depth, "depth");
|
|
390
390
|
requireInt(raw.priority, "priority");
|
|
391
|
+
if (raw.hire_grant !== undefined &&
|
|
392
|
+
(typeof raw.hire_grant !== "number" || !Number.isSafeInteger(raw.hire_grant) || raw.hire_grant < 0)) {
|
|
393
|
+
throw new LaneSchemaError("Expected non-negative safe integer for hire_grant", {
|
|
394
|
+
path: filePath,
|
|
395
|
+
field: "hire_grant",
|
|
396
|
+
value: raw.hire_grant,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
391
399
|
requireString(raw.project_id, "project_id");
|
|
392
400
|
requireString(raw.story_id, "story_id");
|
|
393
401
|
requireTimestamp(raw.created_at, "created_at", filePath);
|
|
@@ -760,6 +768,7 @@ export function createLane(hqRoot, init, deps = defaultStoreDeps()) {
|
|
|
760
768
|
const filePath = laneRecordPath(hqRoot, lane_id);
|
|
761
769
|
const record = {
|
|
762
770
|
...init,
|
|
771
|
+
hire_grant: init.hire_grant ?? 0,
|
|
763
772
|
schema_version: init.schema_version ?? LANE_SCHEMA_VERSION,
|
|
764
773
|
lane_id,
|
|
765
774
|
version: 1,
|
|
@@ -780,6 +789,18 @@ export function createLane(hqRoot, init, deps = defaultStoreDeps()) {
|
|
|
780
789
|
return record;
|
|
781
790
|
});
|
|
782
791
|
}
|
|
792
|
+
/** Hierarchy lock: separate from record writes so callers can CAS-write while holding it. */
|
|
793
|
+
export function withLaneHireLocks(hqRoot, laneIds, fn, deps = defaultStoreDeps()) {
|
|
794
|
+
const uniqueIds = [...new Set(laneIds)].sort();
|
|
795
|
+
const acquire = (index) => {
|
|
796
|
+
const laneId = uniqueIds[index];
|
|
797
|
+
if (laneId === undefined)
|
|
798
|
+
return fn();
|
|
799
|
+
const lockPath = path.join(lanesStoreRoot(hqRoot), "hire-locks", `${laneId}.json`);
|
|
800
|
+
return withRecordLock(lockPath, deps, () => acquire(index + 1));
|
|
801
|
+
};
|
|
802
|
+
return acquire(0);
|
|
803
|
+
}
|
|
783
804
|
/** Value equality over JSON-shaped record fields; the clone breaks reference identity. */
|
|
784
805
|
function fieldUnchanged(before, after) {
|
|
785
806
|
if (Object.is(before, after))
|
|
@@ -815,7 +836,7 @@ export function updateLane(hqRoot, laneId, expectedVersion, writer, updater, dep
|
|
|
815
836
|
* that owns the lane. The owner is authoritative on equal versions; a newer
|
|
816
837
|
* primary version is a conflict and must not be overwritten.
|
|
817
838
|
*/
|
|
818
|
-
export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane, deps = defaultStoreDeps()) {
|
|
839
|
+
export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane, deps = defaultStoreDeps(), options = {}) {
|
|
819
840
|
let ownerLane;
|
|
820
841
|
try {
|
|
821
842
|
ownerLane = parseLaneRecord(rawOwnerLane);
|
|
@@ -830,13 +851,14 @@ export function syncLaneRecordFromOwner(hqRoot, laneId, outpostId, rawOwnerLane,
|
|
|
830
851
|
if (ownerLane.lane_id !== laneId ||
|
|
831
852
|
ownerLane.host.kind !== "outpost" ||
|
|
832
853
|
ownerLane.host.id !== outpostId ||
|
|
833
|
-
!isTerminalLaneState(ownerLane.state)) {
|
|
834
|
-
throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} did not return
|
|
854
|
+
(!options.allowNonTerminal && !isTerminalLaneState(ownerLane.state))) {
|
|
855
|
+
throw new LanesError("outpost_lane_sync_invalid", `Outpost ${outpostId} did not return an owned lane snapshot for lane ${laneId}`, {
|
|
835
856
|
lane_id: laneId,
|
|
836
857
|
outpost_id: outpostId,
|
|
837
858
|
owner_lane_id: ownerLane.lane_id,
|
|
838
859
|
owner_host: ownerLane.host,
|
|
839
860
|
owner_state: ownerLane.state,
|
|
861
|
+
require_terminal: !options.allowNonTerminal,
|
|
840
862
|
});
|
|
841
863
|
}
|
|
842
864
|
const filePath = laneRecordPath(hqRoot, laneId);
|
|
@@ -299,6 +299,8 @@ export interface LaneRecord {
|
|
|
299
299
|
worker_pack?: string;
|
|
300
300
|
/** Personal worker profile id applied to every spawn. */
|
|
301
301
|
worker_profile?: string;
|
|
302
|
+
/** Maximum number of direct child lanes this lane may keep alive; old records default to 0. */
|
|
303
|
+
hire_grant?: number;
|
|
302
304
|
/** Content hash of the profile last applied to this lane. */
|
|
303
305
|
worker_profile_hash?: string;
|
|
304
306
|
/** Profile fields intentionally unsupported by the selected provider. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -219,6 +219,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
219
219
|
schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
|
|
220
220
|
project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
|
|
221
221
|
depth: ["command"], senior: ["command"], senior_history: ["command"],
|
|
222
|
+
hire_grant: ["creator", "command"],
|
|
222
223
|
state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
|
|
223
224
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
224
225
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|