@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.
@@ -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);
@@ -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,3 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerLanesLinkCommand(lanes: Command): void;
3
+ //# sourceMappingURL=lanes-link.d.ts.map
@@ -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 child = spawn(process.execPath, [entry, "lanes", "_loop-worker", laneId], {
21
- cwd: ctx.hqRoot(),
22
- detached: true,
23
- stdio: "ignore",
24
- env: { ...ctx.env, HQ_LANE_LOOP_CONTROLLER: "1", ...(initialAdmission ? { HQ_LANE_LOOP_INITIAL: "1" } : {}) },
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 (body.loop_stall?.lane_id === askingLane.lane_id && askingLane.loop_mode === true) {
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;
@@ -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
- const pid = loopStatus(runDirPath, laneId).pid;
9435
- if (pid === undefined)
9436
- throw new LanesError("loop_controller_pid_missing", `Loop controller for ${laneId} is alive without a recorded pid`, { lane_id: laneId });
9437
- return pid;
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: 10 * 60_000,
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
- return { ok: false, from, to: null, detail: `could not look up the newest hq-cloud: ${npmFailure(r)}` };
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
- return { ok: false, from, to: null, detail: `npm listed no hq-cloud release at or above ${bundled.version} in the same major version` };
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
- return {
230
- ok: false,
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
- return { ok: true, from, to: managedHqCloud(daemonRoot)?.version ?? null };
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