@indigoai-us/hq-cli 5.345.61 → 5.345.63
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 +9 -0
- package/dist/command-catalog.generated.d.ts +736 -0
- package/dist/command-catalog.generated.js +907 -0
- package/dist/commands/core-hq-session-native.js +5 -1
- package/dist/commands/daemon.js +6 -1
- package/dist/commands/lanes-link.d.ts +3 -0
- package/dist/commands/lanes-link.js +83 -0
- package/dist/commands/lanes-loop.js +5 -6
- package/dist/commands/lanes-questions.js +27 -3
- package/dist/commands/lanes.js +26 -4
- package/dist/lib/daemon/hq-cloud.d.ts +13 -1
- package/dist/lib/daemon/hq-cloud.js +79 -16
- package/dist/lib/lanes/links.d.ts +39 -0
- package/dist/lib/lanes/links.js +450 -0
- package/dist/lib/lanes/loop.d.ts +7 -2
- package/dist/lib/lanes/loop.js +169 -57
- package/dist/utils/client-health-contract.d.ts +12 -0
- package/dist/utils/client-health-contract.js +51 -0
- package/dist/utils/version-gate.js +5 -1
- package/package.json +1 -1
|
@@ -657,8 +657,12 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
657
657
|
const oldText = existed ? deps.readFile(target, "utf8") : `session_id: ${sessionId}\nstarted_at: "${timestamp(deps.now())}"\nsenior: user\n`;
|
|
658
658
|
const rawCompanies = metaCompanySlugs(oldText);
|
|
659
659
|
const companies = rawCompanies.filter((company) => company === "personal" || exactCompanyDir(root, company, deps));
|
|
660
|
-
if (subcommand === "add" && companies.includes(slug))
|
|
660
|
+
if (subcommand === "add" && companies.includes(slug)) {
|
|
661
|
+
if (!mintScopeCapability(root, sessionId, companies, deps, true)) {
|
|
662
|
+
return { code: 1, stdout: "", stderr: `hq-session: failed to repair scope-capability for session ${sessionId}; metadata was unchanged\n` };
|
|
663
|
+
}
|
|
661
664
|
return { code: 0, stdout: "", stderr: "" };
|
|
665
|
+
}
|
|
662
666
|
const next = subcommand === "add"
|
|
663
667
|
? [...(companies[0] === "personal" ? [] : companies), slug]
|
|
664
668
|
: companies.filter((value) => value !== slug);
|
package/dist/commands/daemon.js
CHANGED
|
@@ -24,7 +24,7 @@ import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/d
|
|
|
24
24
|
import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
|
|
25
25
|
import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
|
|
26
26
|
import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
|
|
27
|
-
import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy } from "../lib/daemon/hq-cloud.js";
|
|
27
|
+
import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
|
|
28
28
|
import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
|
|
29
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
30
30
|
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
@@ -510,6 +510,7 @@ function daemonSyncStatus(d) {
|
|
|
510
510
|
const runnerSource = syncRunnerSource(runnerPath, paths.root, d.platform);
|
|
511
511
|
const managedCopy = managedHqCloud(paths.root);
|
|
512
512
|
const managedVersion = managedCopy?.version ?? null;
|
|
513
|
+
const hqCloudUpdate = readHqCloudUpdateState(paths.root);
|
|
513
514
|
let hqCloudVersion = ownerStatus?.version ?? null;
|
|
514
515
|
let hqCloudVersionSource = hqCloudVersion ? "owner-status" : "unknown";
|
|
515
516
|
if (!hqCloudVersion && runnerPath) {
|
|
@@ -547,6 +548,10 @@ function daemonSyncStatus(d) {
|
|
|
547
548
|
runnerSource,
|
|
548
549
|
managedHqCloudVersion: managedVersion,
|
|
549
550
|
pinnedHqCloudVersion: config.hqCloudVersion ?? null,
|
|
551
|
+
hqCloudUpdateLastCheckAt: hqCloudUpdate?.lastCheckAt ?? null,
|
|
552
|
+
hqCloudUpdateOutcome: hqCloudUpdate?.outcome ?? null,
|
|
553
|
+
hqCloudUpdateTarget: hqCloudUpdate?.target ?? null,
|
|
554
|
+
hqCloudUpdateErrorClass: hqCloudUpdate?.errorClass ?? null,
|
|
550
555
|
lastHeartbeat: syncUnit?.lastHeartbeatAt ?? ownerStatus?.heartbeatAt ?? null,
|
|
551
556
|
lastPassResult: syncUnit?.lastPassResult ?? null,
|
|
552
557
|
unitStatus: syncUnit?.status ?? "not-running",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
2
|
+
import { readLanesRegistryFlagFailOpen } from "../lib/lanes/registry-flag.js";
|
|
3
|
+
import { isLinkedSession, linkedSessionIdFromHookInput, LANE_SESSION_LINKS_FLAG, LaneLinkError, runLaneLinkWithFlag } from "../lib/lanes/links.js";
|
|
4
|
+
function writeJson(value) {
|
|
5
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
6
|
+
}
|
|
7
|
+
function addLinkOptions(command) {
|
|
8
|
+
return command
|
|
9
|
+
.option("--json", "Print JSON output (the default)")
|
|
10
|
+
.option("--session-id <id>", "Act as this session")
|
|
11
|
+
.option("--link <id>", "Conduct link id")
|
|
12
|
+
.option("--child <name|all>", "Child name or all children")
|
|
13
|
+
.option("--name <name>", "Child name")
|
|
14
|
+
.option("--engine <engine>", "Session engine")
|
|
15
|
+
.option("--title <title>", "Session title")
|
|
16
|
+
.option("--host-session <id>", "Host session id")
|
|
17
|
+
.option("--text <message>", "Message text")
|
|
18
|
+
.option("--text-file <path>", "Read message text from a file")
|
|
19
|
+
.option("--state <state>", "Child state")
|
|
20
|
+
.option("--note <note>", "State note")
|
|
21
|
+
.option("--timeout <seconds>", "Wait timeout in seconds", (value) => Number(value));
|
|
22
|
+
}
|
|
23
|
+
async function readHookInput() {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
for await (const chunk of process.stdin)
|
|
26
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
27
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
28
|
+
}
|
|
29
|
+
function commandError(error) {
|
|
30
|
+
const code = error instanceof LaneLinkError ? error.code : "internal";
|
|
31
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
32
|
+
process.stderr.write(`hq lanes link: ${message}\n`);
|
|
33
|
+
writeJson({ error: code, message });
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
export function registerLanesLinkCommand(lanes) {
|
|
37
|
+
const link = lanes.command("link").description("Link live sessions to a conductor through session inboxes");
|
|
38
|
+
const commands = ["open", "join", "send", "report", "status", "inbox", "read", "wait", "list", "leave", "close"];
|
|
39
|
+
for (const subcommand of commands) {
|
|
40
|
+
addLinkOptions(link.command(subcommand)).action(async (options) => {
|
|
41
|
+
try {
|
|
42
|
+
const root = resolveLanesHqRoot(process.env);
|
|
43
|
+
const enabled = await readLanesRegistryFlagFailOpen(LANE_SESSION_LINKS_FLAG, {
|
|
44
|
+
env: process.env,
|
|
45
|
+
defaultValue: true,
|
|
46
|
+
});
|
|
47
|
+
const result = await runLaneLinkWithFlag(subcommand, { ...options, hqRoot: root, env: process.env }, enabled);
|
|
48
|
+
writeJson(result.output ?? {});
|
|
49
|
+
if (result.exitCode !== undefined)
|
|
50
|
+
process.exitCode = result.exitCode;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
commandError(error);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const deliver = link.command("_deliver", { hidden: true })
|
|
58
|
+
.description("Internal linked-session hook delivery")
|
|
59
|
+
.requiredOption("--event <event>", "Hook event");
|
|
60
|
+
deliver.action(async (options) => {
|
|
61
|
+
// Hook commands must not update/re-exec the CLI inside the provider's time budget.
|
|
62
|
+
process.env.HQ_NO_UPDATE_CHECK = "1";
|
|
63
|
+
try {
|
|
64
|
+
const root = resolveLanesHqRoot(process.env);
|
|
65
|
+
const input = await readHookInput();
|
|
66
|
+
const sid = linkedSessionIdFromHookInput(input);
|
|
67
|
+
if (!sid || !isLinkedSession(root, sid))
|
|
68
|
+
return;
|
|
69
|
+
const enabled = await readLanesRegistryFlagFailOpen(LANE_SESSION_LINKS_FLAG, {
|
|
70
|
+
env: process.env,
|
|
71
|
+
defaultValue: true,
|
|
72
|
+
});
|
|
73
|
+
const result = await runLaneLinkWithFlag("_deliver", { hqRoot: root, env: process.env, event: options.event, input }, enabled);
|
|
74
|
+
if (result.output)
|
|
75
|
+
writeJson(result.output);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
process.stderr.write(`hq lanes link _deliver: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
79
|
+
// Hook failures are fail-open; queued messages remain available if delivery failed.
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=lanes-link.js.map
|
|
@@ -17,12 +17,11 @@ export function startLaneLoopController(ctx, laneId, initialAdmission = false) {
|
|
|
17
17
|
const entry = process.argv[1];
|
|
18
18
|
if (!entry)
|
|
19
19
|
throw new LanesError("loop_controller_entry_missing", "Cannot resolve the hq CLI entry point for the loop controller", { lane_id: laneId });
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
stdio: "ignore",
|
|
24
|
-
|
|
25
|
-
});
|
|
20
|
+
const args = [entry, "lanes", "_loop-worker", laneId];
|
|
21
|
+
const env = { ...ctx.env, HQ_LANE_LOOP_CONTROLLER: "1", ...(initialAdmission ? { HQ_LANE_LOOP_INITIAL: "1" } : {}) };
|
|
22
|
+
const child = ctx.spawnProcess
|
|
23
|
+
? ctx.spawnProcess(process.execPath, args, { cwd: ctx.hqRoot(), detached: true, windowsHide: true, stdio: ["ignore", 1, 2], env })
|
|
24
|
+
: spawn(process.execPath, args, { cwd: ctx.hqRoot(), detached: true, stdio: "ignore", env });
|
|
26
25
|
child.unref();
|
|
27
26
|
if (child.pid === undefined)
|
|
28
27
|
throw new LanesError("loop_controller_spawn_failed", "Loop controller did not start", { lane_id: laneId });
|
|
@@ -5,7 +5,7 @@ import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
|
5
5
|
import { appendCappedHistory, createQuestion, isLanesError, listSessionLiveness, parseLivenessState, parseQuestionRecord, parseQuestionStatus, readDaemonPresenceConnected, readLane, readQuestion, wakeAnsweredLaneNow, sessionLiveness, appendDropboxMessage, answerDropboxMessage, laneAcceptsDropboxDelivery, laneIdForAnswerDelivery, writeQuestionBody } from "../lib/lanes/index.js";
|
|
6
6
|
import { resolveQuestionCompanyWithFallback } from "../lib/lanes/questions.js";
|
|
7
7
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
8
|
-
import { applyLoopStallAnswer } from "../lib/lanes/loop.js";
|
|
8
|
+
import { applyLoopStallAnswer, findLoopStallResult } from "../lib/lanes/loop.js";
|
|
9
9
|
export function resolveAnswerChoice(choice, options, freeform) {
|
|
10
10
|
const trimmed = choice.trim();
|
|
11
11
|
if (/^[0-9]+$/.test(trimmed)) {
|
|
@@ -801,7 +801,6 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
801
801
|
});
|
|
802
802
|
return;
|
|
803
803
|
}
|
|
804
|
-
throwIfQuestionNotPending(question);
|
|
805
804
|
if (askingLane.state === "awaiting_input") {
|
|
806
805
|
assertLaneRunsHereForOperatorMutation(ctx, askingLane, "wake after answering");
|
|
807
806
|
}
|
|
@@ -819,11 +818,27 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
819
818
|
throw new LanesError("missing_choice", "--choice or --freeform is required", {});
|
|
820
819
|
}
|
|
821
820
|
const choice = freeform ?? resolveAnswerChoice(opts.choice, body.options, false);
|
|
821
|
+
const isLoopStallQuestion = body.loop_stall?.lane_id === askingLane.lane_id && askingLane.loop_mode === true;
|
|
822
|
+
const incomingAnswerKind = freeform ? "freeform" : "option";
|
|
823
|
+
const incomingNotesMatch = opts.notes
|
|
824
|
+
? question.answer?.notes_ref !== undefined && body.notes === opts.notes
|
|
825
|
+
: question.answer?.notes_ref === undefined;
|
|
826
|
+
const retryingSameLoopAnswer = isLoopStallQuestion && question.status === "answered" && question.answer?.choice === choice &&
|
|
827
|
+
question.answer.answer_kind === incomingAnswerKind && incomingNotesMatch &&
|
|
828
|
+
deps.sameQuestionAnswer(question.answer, { ...question.answer });
|
|
829
|
+
if (question.status !== "pending" && !retryingSameLoopAnswer)
|
|
830
|
+
throwIfQuestionNotPending(question);
|
|
831
|
+
const loopStallResult = isLoopStallQuestion
|
|
832
|
+
? findLoopStallResult(path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id), questionId)
|
|
833
|
+
: undefined;
|
|
822
834
|
const at = ctx.now().toISOString();
|
|
823
835
|
const answeredBy = sessionIdFromEnv(ctx.env)
|
|
824
836
|
? `session:${sessionIdFromEnv(ctx.env)}`
|
|
825
837
|
: "user";
|
|
826
838
|
const updated = casUpdateQuestion(hqRoot, questionId, (current) => {
|
|
839
|
+
if (retryingSameLoopAnswer && current.status === "answered" && current.answer?.choice === choice &&
|
|
840
|
+
deps.sameQuestionAnswer(current.answer, question.answer))
|
|
841
|
+
return current;
|
|
827
842
|
throwIfQuestionNotPending(current);
|
|
828
843
|
assertQuestionCompanyAccess(current, askingLane, requestedCompany, hqRoot);
|
|
829
844
|
const recordedCompany = current.company ?? questionCompany;
|
|
@@ -856,7 +871,12 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
856
871
|
},
|
|
857
872
|
};
|
|
858
873
|
});
|
|
859
|
-
if (
|
|
874
|
+
if (isLoopStallQuestion) {
|
|
875
|
+
if (!loopStallResult && !retryingSameLoopAnswer) {
|
|
876
|
+
process.stderr.write(`hq lanes: loop phase for question ${questionId} was already re-enqueued; recording answer as superseded\n`);
|
|
877
|
+
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: "superseded" });
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
860
880
|
const resolved = applyLoopStallAnswer({
|
|
861
881
|
runDir: path.join(hqRoot, "workspace", "lanes-runs", askingLane.lane_id),
|
|
862
882
|
questionId,
|
|
@@ -866,6 +886,10 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
866
886
|
});
|
|
867
887
|
if (!resolved)
|
|
868
888
|
throw new LanesError("loop_stall_result_missing", `Loop stall result for question ${questionId} is missing`, { question_id: questionId, lane_id: askingLane.lane_id });
|
|
889
|
+
if (resolved.action === "already_applied") {
|
|
890
|
+
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
869
893
|
const controllerPid = await deps.restartLoopController(ctx, askingLane.lane_id);
|
|
870
894
|
printJson({ ok: true, action: "questions.answer", lane_id: askingLane.lane_id, question: updated, loop_action: resolved.action, controller_pid: controllerPid });
|
|
871
895
|
return;
|
package/dist/commands/lanes.js
CHANGED
|
@@ -34,6 +34,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
|
34
34
|
import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
|
|
35
35
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
36
36
|
import { registerLaneSessionStartCommand } from "./lanes-session-start.js";
|
|
37
|
+
import { registerLanesLinkCommand } from "./lanes-link.js";
|
|
37
38
|
import { laneDepthCap } from "../lib/lanes/depth-cap.js";
|
|
38
39
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
39
40
|
import { runResidentCommandWithPackageLease } from "../utils/resident-package-preload.js";
|
|
@@ -9431,10 +9432,30 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9431
9432
|
}
|
|
9432
9433
|
const runDirPath = runDir(handlerCtx.hqRoot(), laneId);
|
|
9433
9434
|
if (loopControllerIsAlive(runDirPath)) {
|
|
9434
|
-
|
|
9435
|
-
if (
|
|
9436
|
-
|
|
9437
|
-
|
|
9435
|
+
let status = loopStatus(runDirPath, laneId);
|
|
9436
|
+
if (status.state === "parked") {
|
|
9437
|
+
const deadline = Date.now() + 10_000;
|
|
9438
|
+
while (Date.now() < deadline) {
|
|
9439
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
9440
|
+
status = loopStatus(runDirPath, laneId);
|
|
9441
|
+
if (status.state !== "parked") {
|
|
9442
|
+
if (status.pid === undefined)
|
|
9443
|
+
throw new LanesError("loop_controller_pid_missing", `Loop controller for ${laneId} left parked without a recorded pid`, { lane_id: laneId });
|
|
9444
|
+
return status.pid;
|
|
9445
|
+
}
|
|
9446
|
+
if (!loopControllerIsAlive(runDirPath))
|
|
9447
|
+
break;
|
|
9448
|
+
}
|
|
9449
|
+
if (loopControllerIsAlive(runDirPath) && loopStatus(runDirPath, laneId).state === "parked") {
|
|
9450
|
+
throw new LanesError("loop_controller_winding_down", `Loop controller for ${laneId} stayed parked while winding down`, { lane_id: laneId });
|
|
9451
|
+
}
|
|
9452
|
+
}
|
|
9453
|
+
else {
|
|
9454
|
+
const pid = status.pid;
|
|
9455
|
+
if (pid === undefined)
|
|
9456
|
+
throw new LanesError("loop_controller_pid_missing", `Loop controller for ${laneId} is alive without a recorded pid`, { lane_id: laneId });
|
|
9457
|
+
return pid;
|
|
9458
|
+
}
|
|
9438
9459
|
}
|
|
9439
9460
|
return startLaneLoopController(handlerCtx, laneId);
|
|
9440
9461
|
},
|
|
@@ -9534,6 +9555,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
9534
9555
|
.command("lanes")
|
|
9535
9556
|
.description("Create, supervise, and steer worker lanes; routed decision queue");
|
|
9536
9557
|
registerLaneSessionStartCommand(lanes);
|
|
9558
|
+
registerLanesLinkCommand(lanes);
|
|
9537
9559
|
registerLaneChecksCommand(lanes, {
|
|
9538
9560
|
hqRoot: ctx.hqRoot,
|
|
9539
9561
|
resolveCompany: ctx.resolveCompany,
|
|
@@ -71,8 +71,19 @@ export interface HqCloudUpdateOutcome {
|
|
|
71
71
|
ok: boolean;
|
|
72
72
|
from: string | null;
|
|
73
73
|
to: string | null;
|
|
74
|
+
outcome?: HqCloudUpdateState["outcome"];
|
|
75
|
+
target?: string | null;
|
|
76
|
+
errorClass?: HqCloudUpdateState["errorClass"];
|
|
77
|
+
lastCheckAt?: string;
|
|
74
78
|
detail?: string;
|
|
75
79
|
}
|
|
80
|
+
export interface HqCloudUpdateState {
|
|
81
|
+
lastCheckAt: string;
|
|
82
|
+
outcome: "updated" | "up_to_date" | "pinned" | "failed";
|
|
83
|
+
target: string | null;
|
|
84
|
+
errorClass: "registry" | "install" | "convergence" | "filesystem" | "unknown" | null;
|
|
85
|
+
}
|
|
86
|
+
export declare function readHqCloudUpdateState(daemonRoot: string): HqCloudUpdateState | null;
|
|
76
87
|
interface NpmRunResult {
|
|
77
88
|
status: number | null;
|
|
78
89
|
stdout?: string | null;
|
|
@@ -80,11 +91,12 @@ interface NpmRunResult {
|
|
|
80
91
|
error?: Error;
|
|
81
92
|
}
|
|
82
93
|
export interface UpdateHqCloudDeps {
|
|
83
|
-
run?: (file: string, args: string[], shell: boolean, env: NodeJS.ProcessEnv) => NpmRunResult;
|
|
94
|
+
run?: (file: string, args: string[], shell: boolean, env: NodeJS.ProcessEnv, timeoutMs: number) => NpmRunResult;
|
|
84
95
|
bundled?: HqCloudCopy | null;
|
|
85
96
|
platform?: NodeJS.Platform;
|
|
86
97
|
/** Test seam for bounded install retry delays. */
|
|
87
98
|
sleep?: (ms: number) => void;
|
|
99
|
+
now?: () => Date;
|
|
88
100
|
}
|
|
89
101
|
/** Install or update the updater's copy, as this (the login) user. */
|
|
90
102
|
export declare function updateManagedHqCloud(daemonRoot: string, env: NodeJS.ProcessEnv, deps?: UpdateHqCloudDeps): HqCloudUpdateOutcome;
|
|
@@ -161,6 +161,47 @@ export function currentHqCloudAfterLeaseRelease(daemonRoot, expectedVersion) {
|
|
|
161
161
|
return null;
|
|
162
162
|
return { ...managed, source: "managed" };
|
|
163
163
|
}
|
|
164
|
+
const HQ_CLOUD_UPDATE_STATE_FILE = "hq-cloud-update-state.json";
|
|
165
|
+
const HQ_CLOUD_UPDATE_STATE_MAX_BYTES = 4_096;
|
|
166
|
+
const HQ_CLOUD_UPDATE_OUTCOMES = new Set(["updated", "up_to_date", "pinned", "failed"]);
|
|
167
|
+
const HQ_CLOUD_UPDATE_ERROR_CLASSES = new Set([
|
|
168
|
+
"registry", "install", "convergence", "filesystem", "unknown",
|
|
169
|
+
]);
|
|
170
|
+
export function readHqCloudUpdateState(daemonRoot) {
|
|
171
|
+
try {
|
|
172
|
+
const file = path.join(daemonRoot, HQ_CLOUD_UPDATE_STATE_FILE);
|
|
173
|
+
const stat = fs.lstatSync(file);
|
|
174
|
+
if (!stat.isFile() || stat.size > HQ_CLOUD_UPDATE_STATE_MAX_BYTES) {
|
|
175
|
+
throw new Error("updater state is not a small regular file");
|
|
176
|
+
}
|
|
177
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
178
|
+
if (typeof raw.lastCheckAt !== "string"
|
|
179
|
+
|| !HQ_CLOUD_UPDATE_OUTCOMES.has(raw.outcome)
|
|
180
|
+
|| !(raw.target === null || typeof raw.target === "string")
|
|
181
|
+
|| !(raw.errorClass === null || HQ_CLOUD_UPDATE_ERROR_CLASSES.has(raw.errorClass)))
|
|
182
|
+
return null;
|
|
183
|
+
return raw;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error.code !== "ENOENT") {
|
|
187
|
+
console.warn("Could not read daemon hq-cloud updater state; status will omit it:", error instanceof Error ? error.message : String(error));
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function writeHqCloudUpdateState(daemonRoot, state) {
|
|
193
|
+
const file = path.join(daemonRoot, HQ_CLOUD_UPDATE_STATE_FILE);
|
|
194
|
+
const temporary = `${file}.tmp-${process.pid}`;
|
|
195
|
+
fs.writeFileSync(temporary, JSON.stringify(state) + "\n", { mode: 0o600 });
|
|
196
|
+
fs.renameSync(temporary, file);
|
|
197
|
+
}
|
|
198
|
+
function updateErrorClass(detail, fallback) {
|
|
199
|
+
if (/could not look up|npm listed no/i.test(detail))
|
|
200
|
+
return "registry";
|
|
201
|
+
return fallback;
|
|
202
|
+
}
|
|
203
|
+
const HQ_CLOUD_REGISTRY_TIMEOUT_MS = 15_000;
|
|
204
|
+
const HQ_CLOUD_INSTALL_TIMEOUT_MS = 120_000;
|
|
164
205
|
function isTransientNpmFailure(result) {
|
|
165
206
|
// spawnSync's ETIMEDOUT means this attempt hit our full 10-minute process
|
|
166
207
|
// timeout. Retrying that result would multiply one stall across every attempt.
|
|
@@ -172,11 +213,11 @@ function isTransientNpmFailure(result) {
|
|
|
172
213
|
function sleepSync(ms) {
|
|
173
214
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
174
215
|
}
|
|
175
|
-
function runNpm(file, args, shell, env) {
|
|
216
|
+
function runNpm(file, args, shell, env, timeoutMs) {
|
|
176
217
|
return spawnSync(file, args, {
|
|
177
218
|
encoding: "utf8",
|
|
178
219
|
env,
|
|
179
|
-
timeout:
|
|
220
|
+
timeout: timeoutMs,
|
|
180
221
|
stdio: ["ignore", "pipe", "pipe"],
|
|
181
222
|
windowsHide: true,
|
|
182
223
|
shell,
|
|
@@ -193,24 +234,40 @@ export function updateManagedHqCloud(daemonRoot, env, deps = {}) {
|
|
|
193
234
|
const pinnedVersion = readDaemonConfig(path.join(daemonRoot, "config.json")).hqCloudVersion;
|
|
194
235
|
const bundled = deps.bundled !== undefined ? deps.bundled : bundledHqCloud();
|
|
195
236
|
const from = managedHqCloud(daemonRoot)?.version ?? bundled?.version ?? null;
|
|
237
|
+
const checkedAt = (deps.now ?? (() => new Date()))().toISOString();
|
|
238
|
+
const finish = (result, outcome, target, errorClass) => {
|
|
239
|
+
const state = { lastCheckAt: checkedAt, outcome, target, errorClass };
|
|
240
|
+
try {
|
|
241
|
+
fs.mkdirSync(daemonRoot, { recursive: true, mode: 0o700 });
|
|
242
|
+
writeHqCloudUpdateState(daemonRoot, state);
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
246
|
+
console.warn("Could not persist daemon hq-cloud updater state; preserving update result:", detail);
|
|
247
|
+
}
|
|
248
|
+
return { ...result, outcome, target, errorClass, lastCheckAt: checkedAt };
|
|
249
|
+
};
|
|
196
250
|
if (!bundled)
|
|
197
|
-
return { ok: false, from, to: null, detail: "this CLI install has no bundled hq-cloud to match" };
|
|
251
|
+
return finish({ ok: false, from, to: null, detail: "this CLI install has no bundled hq-cloud to match" }, "failed", pinnedVersion ?? null, "unknown");
|
|
198
252
|
const prefix = managedHqCloudDir(daemonRoot);
|
|
199
253
|
try {
|
|
200
254
|
fs.mkdirSync(prefix, { recursive: true, mode: 0o700 });
|
|
201
255
|
}
|
|
202
256
|
catch (err) {
|
|
203
|
-
return { ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` };
|
|
257
|
+
return finish({ ok: false, from, to: null, detail: `could not create ${prefix}: ${err instanceof Error ? err.message : String(err)}` }, "failed", pinnedVersion ?? null, "filesystem");
|
|
204
258
|
}
|
|
205
259
|
let version = pinnedVersion;
|
|
206
260
|
if (!version) {
|
|
207
261
|
const view = hqCloudLatestCommand(bundled.version, platform);
|
|
208
|
-
const r = run(view.file, view.args, view.shell, env);
|
|
209
|
-
if (r.status !== 0)
|
|
210
|
-
|
|
262
|
+
const r = run(view.file, view.args, view.shell, env, HQ_CLOUD_REGISTRY_TIMEOUT_MS);
|
|
263
|
+
if (r.status !== 0) {
|
|
264
|
+
const detail = `could not look up the newest hq-cloud: ${npmFailure(r)}`;
|
|
265
|
+
return finish({ ok: false, from, to: null, detail }, "failed", null, updateErrorClass(detail, "registry"));
|
|
266
|
+
}
|
|
211
267
|
const latest = latestSameMajor(r.stdout ?? "", bundled.version);
|
|
212
268
|
if (!latest) {
|
|
213
|
-
|
|
269
|
+
const detail = `npm listed no hq-cloud release at or above ${bundled.version} in the same major version`;
|
|
270
|
+
return finish({ ok: false, from, to: null, detail }, "failed", null, "registry");
|
|
214
271
|
}
|
|
215
272
|
version = latest;
|
|
216
273
|
}
|
|
@@ -219,22 +276,28 @@ export function updateManagedHqCloud(daemonRoot, env, deps = {}) {
|
|
|
219
276
|
const sleep = deps.sleep ?? sleepSync;
|
|
220
277
|
let r;
|
|
221
278
|
for (let attempt = 0;; attempt++) {
|
|
222
|
-
r = run(cmd.file, cmd.args, cmd.shell, env);
|
|
279
|
+
r = run(cmd.file, cmd.args, cmd.shell, env, HQ_CLOUD_INSTALL_TIMEOUT_MS);
|
|
223
280
|
if (r.status === 0)
|
|
224
281
|
break;
|
|
225
282
|
const retryable = pinnedVersion !== undefined && pinnedVersion !== null && isTransientNpmFailure(r);
|
|
226
283
|
const delay = retryDelays[attempt];
|
|
227
284
|
if (!retryable || delay === undefined) {
|
|
228
285
|
const retries = retryable ? attempt : 0;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
from,
|
|
232
|
-
to: null,
|
|
233
|
-
detail: retries > 0 ? `pinned hq-cloud install failed after ${retries + 1} attempts: ${npmFailure(r)}` : npmFailure(r),
|
|
234
|
-
};
|
|
286
|
+
const detail = retries > 0 ? `pinned hq-cloud install failed after ${retries + 1} attempts: ${npmFailure(r)}` : npmFailure(r);
|
|
287
|
+
return finish({ ok: false, from, to: null, detail }, "failed", version, retryable ? "registry" : "install");
|
|
235
288
|
}
|
|
236
289
|
sleep(delay);
|
|
237
290
|
}
|
|
238
|
-
|
|
291
|
+
const installedPackage = managedHqCloud(daemonRoot);
|
|
292
|
+
const installedVersion = installedPackage?.version ?? null;
|
|
293
|
+
const syncRunner = installedPackage ? hqCloudRunners(installedPackage.root).syncRunner : null;
|
|
294
|
+
if (installedVersion !== version || !syncRunner) {
|
|
295
|
+
const detail = installedVersion !== version
|
|
296
|
+
? `hq-cloud install exited successfully but installed ${installedVersion ?? "no readable package"}, expected ${version}`
|
|
297
|
+
: `hq-cloud install exited successfully at ${version} but has no dist/bin/sync-runner.js`;
|
|
298
|
+
return finish({ ok: false, from, to: installedVersion, detail }, "failed", version, "convergence");
|
|
299
|
+
}
|
|
300
|
+
const outcome = pinnedVersion ? "pinned" : installedVersion === from ? "up_to_date" : "updated";
|
|
301
|
+
return finish({ ok: true, from, to: installedVersion }, outcome, version, null);
|
|
239
302
|
}
|
|
240
303
|
//# sourceMappingURL=hq-cloud.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const LANE_SESSION_LINKS_FLAG = "lanes.session-links";
|
|
2
|
+
export declare class LaneLinkError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
constructor(code: string, message: string);
|
|
5
|
+
}
|
|
6
|
+
export interface LinkOptions {
|
|
7
|
+
hqRoot: string;
|
|
8
|
+
sessionId?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
link?: string;
|
|
11
|
+
child?: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
engine?: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
hostSession?: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
textFile?: string;
|
|
18
|
+
state?: string;
|
|
19
|
+
note?: string;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
event?: string;
|
|
22
|
+
input?: string;
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
sleep?: (milliseconds: number) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export interface LinkResult {
|
|
27
|
+
output?: Record<string, unknown>;
|
|
28
|
+
exitCode?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function resolveLinkSessionId(root: string, explicit: string | undefined, env?: NodeJS.ProcessEnv): string;
|
|
31
|
+
/** Cheap hook guard used before remote flag lookup or queue access. */
|
|
32
|
+
export declare function linkedSessionIdFromHookInput(input: string): string;
|
|
33
|
+
export declare function isLinkedSession(root: string, sid: string): boolean;
|
|
34
|
+
/** Claims pending files by renaming the whole queue, matching conduct-inbox's no-loss drain. */
|
|
35
|
+
export declare function drainLinkQueue(inbox: string): string;
|
|
36
|
+
export declare function runLaneLink(subcommand: string, options: LinkOptions): Promise<LinkResult>;
|
|
37
|
+
/** Apply the default-on feature gate contract at the command boundary. */
|
|
38
|
+
export declare function runLaneLinkWithFlag(subcommand: string, options: LinkOptions, enabled: boolean): Promise<LinkResult>;
|
|
39
|
+
//# sourceMappingURL=links.d.ts.map
|