@indigoai-us/hq-cli 5.345.62 → 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.
@@ -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,
@@ -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