@indigoai-us/hq-cli 5.345.156 → 5.345.159
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 +231 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +531 -6
- package/dist/command-catalog.generated.js +674 -27
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -31,6 +31,7 @@ export function buildHostCapacitySnapshot(input) {
|
|
|
31
31
|
...(input.probe !== undefined ? { probe: input.probe } : {}),
|
|
32
32
|
...(input.env !== undefined ? { env: input.env } : {}),
|
|
33
33
|
...(input.hostname !== undefined ? { hostname: input.hostname } : {}),
|
|
34
|
+
...(input.readClaudeCredential !== undefined ? { readClaudeCredential: input.readClaudeCredential } : {}),
|
|
34
35
|
});
|
|
35
36
|
const accountLanes = providerLanes.filter((lane) => lane.account === id);
|
|
36
37
|
const liveLanes = accountLanes.filter((lane) => lane.state === "running" || lane.state === "queued" || lane.state === "awaiting_input");
|
|
@@ -30,7 +30,7 @@ export declare function queueLaneTerminalResume(current: LaneRecord, at: string,
|
|
|
30
30
|
trigger?: "resume" | "watcher";
|
|
31
31
|
}): LaneTerminalResumeResult;
|
|
32
32
|
/** Persist a manager status miss and queue its one bounded follow-up turn. */
|
|
33
|
-
export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string): LaneRecord;
|
|
33
|
+
export declare function queueManagerStatusGateResume(hqRoot: string, laneId: string, entry: Omit<ManagerStatusGateHistoryEntry, "event" | "resume_queued" | "resume_skipped_reason">, resumeText: string, skip?: (current: LaneRecord) => boolean): LaneRecord;
|
|
34
34
|
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.";
|
|
35
35
|
export declare const LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT = "You ended blocked with no pending question. Record the event you wait on in waiting_on, re-check each blocker, and continue the next unblocked story.";
|
|
36
36
|
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.";
|
|
@@ -75,8 +75,10 @@ export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
/** Persist a manager status miss and queue its one bounded follow-up turn. */
|
|
78
|
-
export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText) {
|
|
78
|
+
export function queueManagerStatusGateResume(hqRoot, laneId, entry, resumeText, skip = () => false) {
|
|
79
79
|
return retryUpdateLane(hqRoot, laneId, "watcher", (current) => {
|
|
80
|
+
if (skip(current))
|
|
81
|
+
return current;
|
|
80
82
|
const gate = current.status_gate;
|
|
81
83
|
if (gate === undefined || gate.turn_id !== entry.turn_id)
|
|
82
84
|
return current;
|
package/dist/lib/lanes/links.js
CHANGED
|
@@ -117,6 +117,9 @@ function resolvePointer(root, sid, role, options) {
|
|
|
117
117
|
if (role === "child" && !safeId(child))
|
|
118
118
|
throw new LaneLinkError("child_name_invalid", `Invalid child name: ${child ?? ""}`);
|
|
119
119
|
const link = options.link ?? pointer.link;
|
|
120
|
+
if (role === "conductor" && link !== pointer.link) {
|
|
121
|
+
throw new LaneLinkError("link_role_invalid", "A conductor can only consume or wait on its own link");
|
|
122
|
+
}
|
|
120
123
|
if (role === "child") {
|
|
121
124
|
if (options.link !== undefined && !safeId(options.link))
|
|
122
125
|
throw new LaneLinkError("link_invalid", "Invalid --link");
|
|
@@ -412,15 +415,18 @@ export async function runLaneLink(subcommand, options) {
|
|
|
412
415
|
}
|
|
413
416
|
if (subcommand === "inbox" || subcommand === "read") {
|
|
414
417
|
const role = subcommand === "inbox" ? "child" : "conductor";
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
+
// Preserve explicit-link validation errors before resolving ownership.
|
|
419
|
+
if (role === "conductor" && options.link !== undefined)
|
|
420
|
+
linkPath(root, options.link);
|
|
421
|
+
const resolved = resolvePointer(root, requireSid(), role, options);
|
|
418
422
|
linkPath(root, resolved.link);
|
|
419
423
|
const text = drainLinkQueue(queueDir(root, resolved.link, role === "child" ? resolved.child : undefined));
|
|
420
424
|
return { output: { messages: text } };
|
|
421
425
|
}
|
|
422
426
|
if (subcommand === "wait") {
|
|
423
|
-
|
|
427
|
+
if (options.link !== undefined)
|
|
428
|
+
linkPath(root, options.link);
|
|
429
|
+
const resolved = resolvePointer(root, requireSid(), "conductor", options);
|
|
424
430
|
return await waitForReport(linkPath(root, resolved.link), options.timeout ?? 1800, options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))));
|
|
425
431
|
}
|
|
426
432
|
if (subcommand === "list") {
|
package/dist/lib/lanes/loop.js
CHANGED
|
@@ -1002,6 +1002,21 @@ function appendJournal(runDir, value) {
|
|
|
1002
1002
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1003
1003
|
fs.appendFileSync(file, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
1004
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Where a synthetic failed or blocked handoff goes. The phase worker may have
|
|
1007
|
+
* written its own handoff to result_path already (for example one that failed
|
|
1008
|
+
* validation). That file is the only copy of the worker's evidence, so it is
|
|
1009
|
+
* never overwritten: the synthetic handoff is written beside it instead
|
|
1010
|
+
* (`<name>.lane-failure.json`). With no worker file, result_path itself gets
|
|
1011
|
+
* the synthetic handoff so the pipeline driver still sees the phase end.
|
|
1012
|
+
*/
|
|
1013
|
+
function syntheticHandoffPath(resultPath) {
|
|
1014
|
+
if (!fs.existsSync(resultPath))
|
|
1015
|
+
return resultPath;
|
|
1016
|
+
const ext = path.extname(resultPath);
|
|
1017
|
+
const base = ext ? resultPath.slice(0, -ext.length) : resultPath;
|
|
1018
|
+
return `${base}.lane-failure${ext || ".json"}`;
|
|
1019
|
+
}
|
|
1005
1020
|
function writeResultAt(runDir, activeFile, record, result, includeHandoffValidationError = true) {
|
|
1006
1021
|
const localPath = path.join(resultsDir(runDir), resultName(record.envelope_id));
|
|
1007
1022
|
atomicJson(localPath, result);
|
|
@@ -1025,7 +1040,7 @@ function writeResultAt(runDir, activeFile, record, result, includeHandoffValidat
|
|
|
1025
1040
|
back_pressure: { tests: "skip", lint: "skip", typecheck: "skip", build: "skip" },
|
|
1026
1041
|
context_for_next: [result.error ?? result.status, ...(result.question_id ? [`Decision question: ${result.question_id}`] : [])].join("\n"),
|
|
1027
1042
|
};
|
|
1028
|
-
atomicJson(path.resolve(externalPath), failure);
|
|
1043
|
+
atomicJson(syntheticHandoffPath(path.resolve(externalPath)), failure);
|
|
1029
1044
|
}
|
|
1030
1045
|
}
|
|
1031
1046
|
else
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node binary to use for a child process started by a long-lived lane
|
|
3
|
+
* process (loop worker, watcher, drain hook registration).
|
|
4
|
+
*
|
|
5
|
+
* `process.execPath` is fixed when the process starts. A Node upgrade can
|
|
6
|
+
* remove that file while the process keeps running (Homebrew deletes the old
|
|
7
|
+
* Cellar directory), and every later child then fails to start. When the
|
|
8
|
+
* launch-time binary is gone:
|
|
9
|
+
*
|
|
10
|
+
* 1. For a Homebrew Cellar path (`<prefix>/Cellar/<formula>/<version>/bin/node`),
|
|
11
|
+
* use the stable `<prefix>/bin/node` symlink when it exists. That points at
|
|
12
|
+
* the upgraded install of the same prefix and does not depend on PATH or on
|
|
13
|
+
* a version manager shim reading the child's cwd.
|
|
14
|
+
* 2. Otherwise fall back to `node` resolved through the child env PATH.
|
|
15
|
+
*/
|
|
16
|
+
export declare function liveNodeBinary(execPath?: string, exists?: (candidate: string) => boolean): string;
|
|
17
|
+
//# sourceMappingURL=node-binary.d.ts.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The Node binary to use for a child process started by a long-lived lane
|
|
5
|
+
* process (loop worker, watcher, drain hook registration).
|
|
6
|
+
*
|
|
7
|
+
* `process.execPath` is fixed when the process starts. A Node upgrade can
|
|
8
|
+
* remove that file while the process keeps running (Homebrew deletes the old
|
|
9
|
+
* Cellar directory), and every later child then fails to start. When the
|
|
10
|
+
* launch-time binary is gone:
|
|
11
|
+
*
|
|
12
|
+
* 1. For a Homebrew Cellar path (`<prefix>/Cellar/<formula>/<version>/bin/node`),
|
|
13
|
+
* use the stable `<prefix>/bin/node` symlink when it exists. That points at
|
|
14
|
+
* the upgraded install of the same prefix and does not depend on PATH or on
|
|
15
|
+
* a version manager shim reading the child's cwd.
|
|
16
|
+
* 2. Otherwise fall back to `node` resolved through the child env PATH.
|
|
17
|
+
*/
|
|
18
|
+
export function liveNodeBinary(execPath = process.execPath, exists = fs.existsSync) {
|
|
19
|
+
if (exists(execPath))
|
|
20
|
+
return execPath;
|
|
21
|
+
const parts = execPath.split(path.sep);
|
|
22
|
+
const cellar = parts.lastIndexOf("Cellar");
|
|
23
|
+
if (cellar > 0) {
|
|
24
|
+
const prefixLink = path.join(parts.slice(0, cellar).join(path.sep) || path.sep, "bin", "node");
|
|
25
|
+
if (exists(prefixLink))
|
|
26
|
+
return prefixLink;
|
|
27
|
+
}
|
|
28
|
+
return "node";
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=node-binary.js.map
|
|
@@ -27,7 +27,7 @@ export interface OutpostDispatchResult {
|
|
|
27
27
|
stderr: string;
|
|
28
28
|
}
|
|
29
29
|
export interface OutpostLaneCommand {
|
|
30
|
-
action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target" | "repair-company-target" | "blocked-escalation-target";
|
|
30
|
+
action: "message" | "resume" | "stop" | "show" | "logs" | "grant" | "grant-target" | "reparent-target" | "approve-target" | "repair-company-target" | "set-target" | "blocked-escalation-target";
|
|
31
31
|
laneId: string;
|
|
32
32
|
hqBinary?: string;
|
|
33
33
|
text?: string;
|
|
@@ -46,6 +46,7 @@ export interface OutpostLaneCommand {
|
|
|
46
46
|
profileRepinContext?: string;
|
|
47
47
|
approvalContext?: string;
|
|
48
48
|
senior?: string;
|
|
49
|
+
actor?: string;
|
|
49
50
|
reason?: string;
|
|
50
51
|
company?: string;
|
|
51
52
|
compact?: boolean;
|
|
@@ -225,6 +225,7 @@ export function buildOutpostLaneCommand(input) {
|
|
|
225
225
|
["--approval-context", input.approvalContext],
|
|
226
226
|
["--failed-resume-context", input.failedResumeContext],
|
|
227
227
|
["--senior", input.senior],
|
|
228
|
+
["--actor", input.actor],
|
|
228
229
|
["--reason", input.reason],
|
|
229
230
|
["--company", input.company],
|
|
230
231
|
["--message-id", input.messageId],
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* compare-and-set. The per-lane watcher owns progress/terminal/silence of a
|
|
6
6
|
* live process — this pass owns the transitions no single lane can own.
|
|
7
7
|
*/
|
|
8
|
+
import { accountMatchesCompany } from "./account-company.js";
|
|
8
9
|
import * as fs from "node:fs";
|
|
9
10
|
import * as path from "node:path";
|
|
10
11
|
import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
@@ -724,6 +725,8 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
|
|
|
724
725
|
for (const account of lane.admission_accounts) {
|
|
725
726
|
if (account === lane.account)
|
|
726
727
|
continue;
|
|
728
|
+
if (!accountMatchesCompany(config.providers[lane.provider].accounts[account], resolveLaneCompany(hqRoot, lane).company, config.providers[lane.provider].accounts))
|
|
729
|
+
continue;
|
|
727
730
|
if (config.providers[lane.provider].accounts[account]?.enabled !== true)
|
|
728
731
|
continue;
|
|
729
732
|
const status = accountStatus(lane.provider, account, config, {
|
|
@@ -754,6 +757,7 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
|
|
|
754
757
|
function writeAdmissionAccount(hqRoot, lane, account, dryRun) {
|
|
755
758
|
return casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "queued" &&
|
|
756
759
|
current.account === lane.account &&
|
|
760
|
+
accountMatchesCompany(loadLanesConfig(hqRoot).providers[current.provider].accounts[account], resolveLaneCompany(hqRoot, current).company, loadLanesConfig(hqRoot).providers[current.provider].accounts) &&
|
|
757
761
|
current.admission_accounts?.includes(account) === true, (current) => ({ ...current, account }), dryRun).record;
|
|
758
762
|
}
|
|
759
763
|
function casQuestionIf(hqRoot, questionId, shouldWrite, updater, dryRun) {
|
|
@@ -15,6 +15,7 @@ import { isOwnedPoolForwarder, poolHostsFile, readRemoteControlPoolsConfig } fro
|
|
|
15
15
|
import { accountStatus, isDefaultProviderHome } from "./accounts.js";
|
|
16
16
|
import { effectiveAccountHome } from "./account-home.js";
|
|
17
17
|
import { LanesError } from "./errors.js";
|
|
18
|
+
import { readLaneClaudeCredential } from "./claude-credential.js";
|
|
18
19
|
import { defaultProbe } from "./models.js";
|
|
19
20
|
import { encodeClaudeProjectDir } from "./move-account.js";
|
|
20
21
|
/** Keep every credential and network route tied to the selected pool. */
|
|
@@ -117,6 +118,7 @@ export function prepareRemoteControlLane(input) {
|
|
|
117
118
|
}
|
|
118
119
|
const status = accountStatus("claude", "a", input.config, {
|
|
119
120
|
hostname: input.hostname, existsSync: fs.existsSync, probe: defaultProbe, env: input.env,
|
|
121
|
+
readClaudeCredential: readLaneClaudeCredential,
|
|
120
122
|
});
|
|
121
123
|
if (!status.logged_in) {
|
|
122
124
|
throw new LanesError("remote_control_login_missing", "Claude account a must be logged in for Remote Control", {});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* mutable fields are re-read from the lane record (spec §5.4).
|
|
9
9
|
*/
|
|
10
10
|
import { PROVIDER_HOME_ENV } from "./accounts.js";
|
|
11
|
+
import { type ClaudeCredentialReader } from "./claude-credential.js";
|
|
11
12
|
import { type RunCompactionFn } from "./compaction.js";
|
|
12
13
|
import { type ResolvedEffort } from "./effort.js";
|
|
13
14
|
import { type KnownModelsDeps } from "./models.js";
|
|
@@ -66,6 +67,8 @@ export type ProveDetachedFn = (pid: number) => DetachProof;
|
|
|
66
67
|
export type KillProcessFn = (pid: number, signal?: NodeJS.Signals) => void;
|
|
67
68
|
export interface SpawnLaneDeps {
|
|
68
69
|
env?: NodeJS.ProcessEnv;
|
|
70
|
+
/** Claude credential expiry check; see spawnCredentialReader for the default. */
|
|
71
|
+
readClaudeCredential?: ClaudeCredentialReader;
|
|
69
72
|
/** Builds the lane's fingerprint from the worker env. Defaults to laneStartFingerprint. */
|
|
70
73
|
buildFingerprint?: (hqRoot: string, workerEnv: NodeJS.ProcessEnv) => string | null;
|
|
71
74
|
/** Test seam for the default-off quota account failover registry gate. */
|
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -7,19 +7,22 @@
|
|
|
7
7
|
* probed for existence and never read. Identity in the child is `HQ_LANE_ID`;
|
|
8
8
|
* mutable fields are re-read from the lane record (spec §5.4).
|
|
9
9
|
*/
|
|
10
|
+
import { accountMatchesCompany, accountMayRetainLane, companyMismatchError } from "./account-company.js";
|
|
10
11
|
import { spawn, spawnSync } from "node:child_process";
|
|
11
12
|
import { randomUUID } from "node:crypto";
|
|
12
13
|
import * as fs from "node:fs";
|
|
13
14
|
import * as path from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { configureRemoteControlChildEnv, prepareRemoteControlLane, remoteControlSelectionConfig } from "./remote-control-lane.js";
|
|
16
|
-
import { PROVIDER_HOME_ENV, accountStatus, isDefaultProviderHome, isLoggedOut, resolveAccount, selectBalancedAccount, } from "./accounts.js";
|
|
17
|
+
import { PROVIDER_HOME_ENV, accountStatus, isDefaultProviderHome, isLoggedOut, isSignInExpired, resolveAccount, selectBalancedAccount, } from "./accounts.js";
|
|
18
|
+
import { readLaneClaudeCredential } from "./claude-credential.js";
|
|
17
19
|
import { effectiveAccountHome } from "./account-home.js";
|
|
18
20
|
import { buildHostCapacitySnapshot } from "./host-capacity.js";
|
|
19
21
|
import { selectClaudeAccount } from "./claude-balance.js";
|
|
20
22
|
import { loadQuotaState } from "./quota.js";
|
|
21
23
|
import { COMPACTION_PROMPT, compactionAfterUsage, compactionDue, compactLaneSession, } from "./compaction.js";
|
|
22
24
|
import { installDrainHook } from "./hooks-install.js";
|
|
25
|
+
import { liveNodeBinary } from "./node-binary.js";
|
|
23
26
|
import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
|
|
24
27
|
import { appendEffortFlag, resolveLaneEffort, } from "./effort.js";
|
|
25
28
|
import { defaultProbe, validateLaneModel, validateProfileLaneModel, } from "./models.js";
|
|
@@ -252,9 +255,19 @@ function selectSpawnAccount(input) {
|
|
|
252
255
|
};
|
|
253
256
|
if (lane.account === undefined) {
|
|
254
257
|
const modelSpecified = pinnedModel !== undefined || profileModel !== undefined;
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
259
|
+
const allowedAccounts = Object.entries(config.providers[lane.provider].accounts)
|
|
260
|
+
.filter(([id, account]) => accountMatchesCompany(account, company, config.providers[lane.provider].accounts) &&
|
|
261
|
+
(!modelSpecified || supportsSpawnModel(lane.provider, id, config, pinnedModel, profileModel, modelDeps, hostname)))
|
|
262
|
+
.map(([id]) => id);
|
|
263
|
+
const mismatch = companyMismatchError({
|
|
264
|
+
provider: lane.provider,
|
|
265
|
+
providerAccounts: config.providers[lane.provider].accounts,
|
|
266
|
+
candidates: Object.keys(config.providers[lane.provider].accounts),
|
|
267
|
+
company,
|
|
268
|
+
});
|
|
269
|
+
if (mismatch !== undefined)
|
|
270
|
+
throw mismatch;
|
|
258
271
|
return {
|
|
259
272
|
account: selectBalancedAccount(lane.provider, config, {
|
|
260
273
|
...context,
|
|
@@ -269,7 +282,13 @@ function selectSpawnAccount(input) {
|
|
|
269
282
|
let refusal;
|
|
270
283
|
const operatorMovePending = lane.pending_resume !== undefined &&
|
|
271
284
|
lane.account_history?.at(-1)?.reason === "operator_move";
|
|
272
|
-
if (current
|
|
285
|
+
if (current !== undefined && !accountMayRetainLane(current, resolveLaneCompany(hqRoot, lane).company, config.providers[lane.provider].accounts)) {
|
|
286
|
+
reason = "account_unavailable";
|
|
287
|
+
refusal = new LanesError("account_company_mismatch", `Account ${lane.account} belongs to another company`, {
|
|
288
|
+
provider: lane.provider, account: lane.account,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
else if (current === undefined || !current.enabled) {
|
|
273
292
|
reason = "account_unavailable";
|
|
274
293
|
refusal = new AccountResolutionError(current === undefined ? "unknown_account" : "account_disabled", `Account ${JSON.stringify(lane.account)} is unavailable for ${lane.provider}`, { provider: lane.provider, account: lane.account });
|
|
275
294
|
}
|
|
@@ -278,11 +297,18 @@ function selectSpawnAccount(input) {
|
|
|
278
297
|
existsSync,
|
|
279
298
|
hostname,
|
|
280
299
|
...(accountDeps?.probe ? { probe: accountDeps.probe } : {}),
|
|
300
|
+
...(accountDeps?.readClaudeCredential ? { readClaudeCredential: accountDeps.readClaudeCredential } : {}),
|
|
281
301
|
...(accountDeps?.env ? { env: accountDeps.env } : {}),
|
|
282
302
|
});
|
|
283
303
|
if (isLoggedOut(status)) {
|
|
284
304
|
reason = repairRecordedAccount ? "recorded_account_logged_out" : "account_unavailable";
|
|
285
|
-
refusal = new LanesError("account_not_logged_in",
|
|
305
|
+
refusal = new LanesError("account_not_logged_in", isSignInExpired(status)
|
|
306
|
+
? expiredSignInMessage(lane.provider, lane.account, status)
|
|
307
|
+
: `Account ${lane.account} for ${lane.provider} is not logged in`, {
|
|
308
|
+
provider: lane.provider,
|
|
309
|
+
account: lane.account,
|
|
310
|
+
...(isSignInExpired(status) ? { sign_in_state: "expired", sign_in_expires_at: status.sign_in_expires_at ?? null } : {}),
|
|
311
|
+
});
|
|
286
312
|
}
|
|
287
313
|
else {
|
|
288
314
|
try {
|
|
@@ -339,6 +365,7 @@ function selectSpawnAccount(input) {
|
|
|
339
365
|
config,
|
|
340
366
|
existsSync,
|
|
341
367
|
probe: accountDeps?.probe,
|
|
368
|
+
readClaudeCredential: accountDeps?.readClaudeCredential,
|
|
342
369
|
env: accountDeps?.env,
|
|
343
370
|
hostname,
|
|
344
371
|
now,
|
|
@@ -348,6 +375,7 @@ function selectSpawnAccount(input) {
|
|
|
348
375
|
targetHost: lane.host,
|
|
349
376
|
actualTargetHost: lane.host,
|
|
350
377
|
requestedPlacement: null,
|
|
378
|
+
company: resolveLaneCompany(hqRoot, lane).company,
|
|
351
379
|
config,
|
|
352
380
|
quotaState: loadQuotaState(hqRoot),
|
|
353
381
|
...((recordedModel ?? pinnedModel ?? profileModel) !== undefined
|
|
@@ -366,11 +394,13 @@ function selectSpawnAccount(input) {
|
|
|
366
394
|
};
|
|
367
395
|
}
|
|
368
396
|
const allowedAccounts = Object.entries(config.providers[lane.provider].accounts)
|
|
397
|
+
.filter(([, account]) => accountMatchesCompany(account, resolveLaneCompany(hqRoot, lane).company, config.providers[lane.provider].accounts))
|
|
369
398
|
.filter(([id, account]) => id !== lane.account && account.enabled)
|
|
370
399
|
.filter(([id]) => accountStatus(lane.provider, id, config, {
|
|
371
400
|
existsSync,
|
|
372
401
|
hostname,
|
|
373
402
|
...(accountDeps?.probe ? { probe: accountDeps.probe } : {}),
|
|
403
|
+
...(accountDeps?.readClaudeCredential ? { readClaudeCredential: accountDeps.readClaudeCredential } : {}),
|
|
374
404
|
...(accountDeps?.env ? { env: accountDeps.env } : {}),
|
|
375
405
|
}).logged_in === true)
|
|
376
406
|
.filter(([id]) => supportsSpawnModel(lane.provider, id, config, pinnedModel, profileModel, modelDeps, hostname))
|
|
@@ -406,6 +436,11 @@ function selectSpawnAccount(input) {
|
|
|
406
436
|
account_reasons: accountReasons,
|
|
407
437
|
});
|
|
408
438
|
}
|
|
439
|
+
if (lane.company !== null && lane.state === "queued" && lane.pending_resume !== undefined && err instanceof AccountResolutionError) {
|
|
440
|
+
throw new LanesError("same_company_account_unavailable", "No eligible account assigned to the lane's company is available", {
|
|
441
|
+
provider: lane.provider, account: lane.account, company: lane.company,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
409
444
|
if (reason === "account_unavailable" || reason === "recorded_account_logged_out") {
|
|
410
445
|
if (refusal instanceof Error)
|
|
411
446
|
throw refusal;
|
|
@@ -545,6 +580,7 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
|
|
|
545
580
|
const accountDeps = {
|
|
546
581
|
probe: modelDeps.probe ?? defaultProbe,
|
|
547
582
|
env,
|
|
583
|
+
...spawnCredentialReader(deps, modelDeps),
|
|
548
584
|
};
|
|
549
585
|
const profile = lane.worker_profile ? loadWorkerProfile(hqRoot, lane.worker_profile) : undefined;
|
|
550
586
|
let quotaFailoverEnabled = false;
|
|
@@ -572,6 +608,9 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
|
|
|
572
608
|
: undefined;
|
|
573
609
|
const unavailableAccounts = previouslyUnavailableAccounts(lane);
|
|
574
610
|
const allowedAccounts = Object.entries(config.providers[lane.provider].accounts)
|
|
611
|
+
.filter(([id, account]) => id === lane.account
|
|
612
|
+
? accountMayRetainLane(account, resolveLaneCompany(hqRoot, lane).company, config.providers[lane.provider].accounts)
|
|
613
|
+
: accountMatchesCompany(account, resolveLaneCompany(hqRoot, lane).company, config.providers[lane.provider].accounts))
|
|
575
614
|
.filter(([id, account]) => (id !== lane.account || (quotaFailoverEnabled && reason === "usage_limit" && retryingExpiredQuota)) &&
|
|
576
615
|
!unavailableAccounts.has(id) && account.enabled)
|
|
577
616
|
.filter(([id]) => !quotaFailoverEnabled ||
|
|
@@ -583,6 +622,7 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
|
|
|
583
622
|
existsSync,
|
|
584
623
|
hostname,
|
|
585
624
|
...(accountDeps?.probe ? { probe: accountDeps.probe } : {}),
|
|
625
|
+
...(accountDeps?.readClaudeCredential ? { readClaudeCredential: accountDeps.readClaudeCredential } : {}),
|
|
586
626
|
...(accountDeps?.env ? { env: accountDeps.env } : {}),
|
|
587
627
|
}).logged_in === true)
|
|
588
628
|
.filter(([id]) => supportsSpawnModel(lane.provider, id, config, pinnedModel, profileModel, modelDeps, hostname))
|
|
@@ -635,6 +675,7 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
|
|
|
635
675
|
existsSync,
|
|
636
676
|
hostname,
|
|
637
677
|
...(accountDeps?.probe ? { probe: accountDeps.probe } : {}),
|
|
678
|
+
...(accountDeps?.readClaudeCredential ? { readClaudeCredential: accountDeps.readClaudeCredential } : {}),
|
|
638
679
|
...(accountDeps?.env ? { env: accountDeps.env } : {}),
|
|
639
680
|
}).logged_in !== true)
|
|
640
681
|
return { account: id, reason: "login_required" };
|
|
@@ -666,6 +707,22 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
|
|
|
666
707
|
return retryUpdateLane(hqRoot, lane.lane_id, writer, holdLane);
|
|
667
708
|
}
|
|
668
709
|
}
|
|
710
|
+
if (resolveLaneCompany(hqRoot, lane).company !== null && err instanceof AccountResolutionError) {
|
|
711
|
+
const waitForAccount = (current) => {
|
|
712
|
+
if (current.state !== retryFromState || current.account !== lane.account || current.provider_session_id !== lane.provider_session_id) {
|
|
713
|
+
throw new LanesError("account_retry_conflict", "Lane changed while company account recovery was prepared", { lane_id: lane.lane_id });
|
|
714
|
+
}
|
|
715
|
+
return {
|
|
716
|
+
...current, state: "queued", queued_reason: "account_retry", queued_details: undefined,
|
|
717
|
+
queued_at: at, admission_reserved_at: undefined, terminal_reason: undefined,
|
|
718
|
+
terminal_details: undefined, interrupted_at: undefined,
|
|
719
|
+
pending_resume: current.provider_session_id
|
|
720
|
+
? pendingResumeForRetry(hqRoot, current, current.provider_session_id, at)
|
|
721
|
+
: undefined,
|
|
722
|
+
};
|
|
723
|
+
};
|
|
724
|
+
return deps.dryRun ? waitForAccount(lane) : retryUpdateLane(hqRoot, lane.lane_id, writer, waitForAccount);
|
|
725
|
+
}
|
|
669
726
|
if (reason === "account_unavailable" || unavailableAccounts.size > 0) {
|
|
670
727
|
throw new LanesError("account_not_logged_in", `Account ${lane.account ?? "(none)"} for ${lane.provider} is unavailable and no other logged-in account can serve this lane`, { provider: lane.provider, account: lane.account ?? null });
|
|
671
728
|
}
|
|
@@ -845,6 +902,21 @@ function appendCodexWorkerPolicyDirFlag(provider, args, workerPolicyDir) {
|
|
|
845
902
|
return [...args, "-c", setting];
|
|
846
903
|
return [...args.slice(0, separator), "-c", setting, ...args.slice(separator)];
|
|
847
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* The Claude credential expiry check rides with the real provider probe: a
|
|
907
|
+
* caller on the production probe also gets the real Keychain/file read, and a
|
|
908
|
+
* test that injects a fake probe gets no credential read unless it passes one.
|
|
909
|
+
*/
|
|
910
|
+
function spawnCredentialReader(deps, modelDeps) {
|
|
911
|
+
if (deps.readClaudeCredential !== undefined)
|
|
912
|
+
return { readClaudeCredential: deps.readClaudeCredential };
|
|
913
|
+
return modelDeps.probe === defaultProbe ? { readClaudeCredential: readLaneClaudeCredential } : {};
|
|
914
|
+
}
|
|
915
|
+
function expiredSignInMessage(provider, account, status) {
|
|
916
|
+
const at = status.sign_in_expires_at ? ` at ${status.sign_in_expires_at}` : "";
|
|
917
|
+
return `Account ${account} for ${provider} has an expired Claude sign-in (access token expired${at}). ` +
|
|
918
|
+
`Open Claude Code on this account and run /login, then retry.`;
|
|
919
|
+
}
|
|
848
920
|
function accountHomeHostname(deps) {
|
|
849
921
|
return (deps.hostname?.trim() ||
|
|
850
922
|
deps.admissionHost?.hostname.trim() ||
|
|
@@ -1958,7 +2030,7 @@ function spawnTrackedWorker(spawnProcess, command, args, env, cwd, stdoutPath, s
|
|
|
1958
2030
|
throw error;
|
|
1959
2031
|
}
|
|
1960
2032
|
const runnerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../dist/lib/lanes/worker-process.js");
|
|
1961
|
-
return spawnDetached(spawnProcess,
|
|
2033
|
+
return spawnDetached(spawnProcess, liveNodeBinary(), [runnerPath, command, ...args], { ...env, [LANE_WORKER_EXIT_PATH_ENV]: exitPath }, cwd, stdoutPath, stderrPath);
|
|
1962
2034
|
}
|
|
1963
2035
|
function requireDetached(proveDetached, pid, kind) {
|
|
1964
2036
|
const proof = proveDetached(pid);
|
|
@@ -2093,7 +2165,7 @@ export function armLaneWatcher(hqRoot, laneId, deps = {}) {
|
|
|
2093
2165
|
if (!cliEntry) {
|
|
2094
2166
|
throw new LanesError("watcher_cli_entry_unavailable", "Cannot arm the lane watcher without the current hq CLI entrypoint", { lane_id: laneId });
|
|
2095
2167
|
}
|
|
2096
|
-
const watcherPid = spawnDetached(spawnProcess,
|
|
2168
|
+
const watcherPid = spawnDetached(spawnProcess, liveNodeBinary(), [watcherMainPath(), laneId], {
|
|
2097
2169
|
...env,
|
|
2098
2170
|
HQ_ROOT: hqRoot,
|
|
2099
2171
|
HQ_LANE_ID: laneId,
|
|
@@ -2332,6 +2404,7 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2332
2404
|
const accountDeps = {
|
|
2333
2405
|
probe: modelDeps.probe ?? defaultProbe,
|
|
2334
2406
|
env,
|
|
2407
|
+
...spawnCredentialReader(deps, modelDeps),
|
|
2335
2408
|
};
|
|
2336
2409
|
const diskAccount = laneDiskAccount(config, existing.provider, existing.account);
|
|
2337
2410
|
if (diskAccount !== undefined) {
|
|
@@ -2342,6 +2415,22 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2342
2415
|
...current,
|
|
2343
2416
|
state: "queued",
|
|
2344
2417
|
queued_reason: "low_disk",
|
|
2418
|
+
// Commit an explicit resume's instruction together with the hold. A
|
|
2419
|
+
// later admission must continue that turn, not start the old brief.
|
|
2420
|
+
pending_resume: trigger.kind === "resume" ? {
|
|
2421
|
+
...(existing.provider_session_id !== undefined ? { session_id: existing.provider_session_id } : {}),
|
|
2422
|
+
trigger: "resume",
|
|
2423
|
+
caller: typeof env.HQ_LANE_ID === "string" ? env.HQ_LANE_ID.trim() || "user" : "user",
|
|
2424
|
+
resumed_from: trigger.from,
|
|
2425
|
+
...(trigger.fromFailed === true ? { from_failed: true } : {}),
|
|
2426
|
+
...(trigger.expectedSenior !== undefined ? { expected_senior: trigger.expectedSenior } : {}),
|
|
2427
|
+
...(trigger.text !== undefined ? { text: trigger.text } : {}),
|
|
2428
|
+
...(trigger.message_id !== undefined ? { message_id: trigger.message_id } : {}),
|
|
2429
|
+
...(trigger.fresh === true ? { fresh_thread: true } : {}),
|
|
2430
|
+
...(deps.workingDirectory !== undefined ? { working_directory: deps.workingDirectory } : {}),
|
|
2431
|
+
compact: launch.compact !== false,
|
|
2432
|
+
at: measuredAt,
|
|
2433
|
+
} : current.pending_resume,
|
|
2345
2434
|
queued_details: {
|
|
2346
2435
|
reason: "low_disk",
|
|
2347
2436
|
hq_free_bytes: free.hq.free_bytes,
|
|
@@ -2422,7 +2511,28 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
|
|
|
2422
2511
|
if (preservedAccountModel !== undefined)
|
|
2423
2512
|
pinnedModel = preservedAccountModel;
|
|
2424
2513
|
const resolved = selection.account;
|
|
2425
|
-
const status = accountStatus(lane.provider, resolved.id, config, {
|
|
2514
|
+
const status = accountStatus(lane.provider, resolved.id, config, {
|
|
2515
|
+
existsSync,
|
|
2516
|
+
hostname,
|
|
2517
|
+
...(accountDeps?.readClaudeCredential ? { readClaudeCredential: accountDeps.readClaudeCredential } : {}),
|
|
2518
|
+
});
|
|
2519
|
+
if (isSignInExpired(status)) {
|
|
2520
|
+
const failed = markPreSpawnFailure
|
|
2521
|
+
? markSpawnFailure(hqRoot, lane.lane_id, "account_not_logged_in", {
|
|
2522
|
+
provider: lane.provider,
|
|
2523
|
+
account: resolved.id,
|
|
2524
|
+
sign_in_state: "expired",
|
|
2525
|
+
})
|
|
2526
|
+
: existing;
|
|
2527
|
+
throw new LanesError("account_not_logged_in", expiredSignInMessage(lane.provider, resolved.id, status), {
|
|
2528
|
+
provider: lane.provider,
|
|
2529
|
+
account: resolved.id,
|
|
2530
|
+
lane_id: lane.lane_id,
|
|
2531
|
+
sign_in_state: "expired",
|
|
2532
|
+
sign_in_expires_at: status.sign_in_expires_at ?? null,
|
|
2533
|
+
lane: failed,
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2426
2536
|
if (isLoggedOut(status)) {
|
|
2427
2537
|
const failed = markPreSpawnFailure
|
|
2428
2538
|
? markSpawnFailure(hqRoot, lane.lane_id, "account_not_logged_in", {
|
|
@@ -24,6 +24,10 @@ export interface StoreDeps {
|
|
|
24
24
|
closeSync: typeof fs.closeSync;
|
|
25
25
|
fsyncSync: typeof fs.fsyncSync;
|
|
26
26
|
chmodSync: typeof fs.chmodSync;
|
|
27
|
+
/** Optional so existing fakes keep working; defaults to node:fs. */
|
|
28
|
+
statSync?: (p: string) => fs.Stats;
|
|
29
|
+
fstatSync?: (fd: number) => fs.Stats;
|
|
30
|
+
linkSync?: typeof fs.linkSync;
|
|
27
31
|
}
|
|
28
32
|
export declare function resolveStoreHqRoot(hqRoot?: string, env?: NodeJS.ProcessEnv, cwd?: string): string;
|
|
29
33
|
export declare function lanesStoreRoot(hqRoot?: string): string;
|
|
@@ -35,6 +39,8 @@ export declare function sanitizeHostComponent(hostname: string): string;
|
|
|
35
39
|
export declare function mintLaneId(deps?: Pick<StoreDeps, "now" | "hostname"> & Partial<Pick<StoreDeps, "generateLaneUlid">>): string;
|
|
36
40
|
export declare function mintQuestionId(deps?: Pick<StoreDeps, "now" | "hostname">): string;
|
|
37
41
|
export declare function appendCappedHistory<T>(history: readonly T[], entry: T, cap?: number): T[];
|
|
42
|
+
/** Test seam: run withRecordLock with overridden deps and a shorter timeout. */
|
|
43
|
+
export declare function withRecordLockForTests<T>(recordPath: string, overrides: Partial<StoreDeps>, fn: () => T, timeoutMs?: number): T;
|
|
38
44
|
/** Run a lane-owned file read/modify/write transaction under its sidecar lock. */
|
|
39
45
|
export declare function withLaneRecordFileLock<T>(recordPath: string, fn: () => T): T;
|
|
40
46
|
export declare function parseLaneRecord(raw: unknown, filePath?: string): LaneRecord;
|