@indigoai-us/hq-cli 5.340.0 → 5.341.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.341.0] — 2026-10-03
6
+ - Keep-alive reconciliation polls every 15 seconds and admits due lane resumes within the same sweep.
7
+ - Add flag-gated `hq daemon login --stdin` for machine sign-in on disposable hosts. Credentials are piped over the local daemon socket, and the resulting token stays in hqd memory until restart or token expiry. Login is deadline-bounded, and the generic argv-based `hq daemon call` command cannot submit credentials.
8
+
5
9
  ## [5.340.0] — 2026-10-03
6
10
  - Preserve resolver output when child processes exit before Windows pipe data has drained.
7
11
  - Native `hq core dm-bind` on Windows now finds an `hq.exe` on PATH, and no longer accepts an `hq.cmd` shim it cannot launch.
@@ -3,7 +3,7 @@ Description=Periodic HQ lanes reconcile pass
3
3
 
4
4
  [Timer]
5
5
  OnBootSec=30s
6
- OnUnitActiveSec=1min
6
+ OnUnitActiveSec=15s
7
7
  AccuracySec=10s
8
8
  Persistent=true
9
9
  Unit=hq-lanes-reconcile.service
@@ -13278,6 +13278,31 @@ export declare const COMMAND_CATALOG: readonly [{
13278
13278
  readonly hidden: false;
13279
13279
  }];
13280
13280
  readonly subcommands: readonly [];
13281
+ }, {
13282
+ readonly name: "login";
13283
+ readonly description: "Sign the running daemon in from machine credentials piped on stdin; credentials and tokens stay off disk";
13284
+ readonly aliases: readonly [];
13285
+ readonly hidden: false;
13286
+ readonly helpOption: true;
13287
+ readonly helpCommand: false;
13288
+ readonly usage: "[options] [extra...]";
13289
+ readonly helpText: readonly [];
13290
+ readonly arguments: readonly [{
13291
+ readonly name: "extra";
13292
+ readonly required: false;
13293
+ readonly variadic: true;
13294
+ readonly description: "";
13295
+ }];
13296
+ readonly options: readonly [{
13297
+ readonly flags: "--stdin";
13298
+ readonly description: "Read a machine credentials JSON object from stdin";
13299
+ readonly hidden: false;
13300
+ }, {
13301
+ readonly flags: "--status";
13302
+ readonly description: "Show whether hqd has an in-memory machine sign-in";
13303
+ readonly hidden: false;
13304
+ }];
13305
+ readonly subcommands: readonly [];
13281
13306
  }, {
13282
13307
  readonly name: "call";
13283
13308
  readonly description: "Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, mcp.call, …) and print the result as JSON";
@@ -16149,6 +16149,37 @@ export const COMMAND_CATALOG = [
16149
16149
  ],
16150
16150
  "subcommands": []
16151
16151
  },
16152
+ {
16153
+ "name": "login",
16154
+ "description": "Sign the running daemon in from machine credentials piped on stdin; credentials and tokens stay off disk",
16155
+ "aliases": [],
16156
+ "hidden": false,
16157
+ "helpOption": true,
16158
+ "helpCommand": false,
16159
+ "usage": "[options] [extra...]",
16160
+ "helpText": [],
16161
+ "arguments": [
16162
+ {
16163
+ "name": "extra",
16164
+ "required": false,
16165
+ "variadic": true,
16166
+ "description": ""
16167
+ }
16168
+ ],
16169
+ "options": [
16170
+ {
16171
+ "flags": "--stdin",
16172
+ "description": "Read a machine credentials JSON object from stdin",
16173
+ "hidden": false
16174
+ },
16175
+ {
16176
+ "flags": "--status",
16177
+ "description": "Show whether hqd has an in-memory machine sign-in",
16178
+ "hidden": false
16179
+ }
16180
+ ],
16181
+ "subcommands": []
16182
+ },
16152
16183
  {
16153
16184
  "name": "call",
16154
16185
  "description": "Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, mcp.call, …) and print the result as JSON",
@@ -42,6 +42,8 @@ export interface DaemonCommandDeps {
42
42
  daemonAlive: (pid: number) => boolean;
43
43
  /** Test seam for the company sync-mode commands. */
44
44
  syncModeClient?: () => Promise<SyncModeVaultClient>;
45
+ /** Reads stdin for the one-shot daemon machine login; never reads env or argv credentials. */
46
+ readStdin?: () => Promise<string>;
45
47
  windowsUser?: string;
46
48
  windowsHeadlessLauncher?: string;
47
49
  systemUnitDir?: string;
@@ -32,6 +32,8 @@ import { writeControlRequest } from "../lib/daemon/requests.js";
32
32
  import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
33
33
  import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
34
34
  import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
35
+ import { hqdRequest, hqdSocketPath, HqdClientError } from "../lib/daemon/protocol.js";
36
+ import { parseStdinMachineCredentials } from "../lib/daemon/machine-login.js";
35
37
  import { resolveDefaultHqRoot } from "../utils/cognito-session.js";
36
38
  class DaemonCommandError extends Error {
37
39
  }
@@ -81,6 +83,7 @@ async function loadDefaults(over) {
81
83
  isRoot: over.isRoot ?? (typeof process.getuid === "function" && process.getuid() === 0),
82
84
  fileExists: over.fileExists ?? fs.existsSync,
83
85
  readFile: over.readFile ?? ((p) => fs.readFileSync(p, "utf8")),
86
+ readStdin: over.readStdin,
84
87
  writeFile: over.writeFile ??
85
88
  ((p, text) => {
86
89
  fs.mkdirSync(path.dirname(p), { recursive: true });
@@ -105,6 +108,27 @@ async function loadDefaults(over) {
105
108
  systemRecordPath: over.systemRecordPath ?? path.join(SYSTEM_ADOPTED_ROOT, "adopted.json"),
106
109
  };
107
110
  }
111
+ async function readLoginStdin() {
112
+ if (process.stdin.isTTY)
113
+ throw new DaemonCommandError("Pipe machine credentials to `hq daemon login --stdin`.");
114
+ const chunks = [];
115
+ let bytes = 0;
116
+ try {
117
+ for await (const chunk of process.stdin) {
118
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
119
+ bytes += buffer.byteLength;
120
+ if (bytes > 64 * 1024)
121
+ throw new DaemonCommandError("stdin credentials are too large");
122
+ chunks.push(buffer);
123
+ }
124
+ }
125
+ catch (err) {
126
+ if (err instanceof DaemonCommandError)
127
+ throw err;
128
+ throw new DaemonCommandError("Could not read machine credentials from stdin.");
129
+ }
130
+ return Buffer.concat(chunks).toString("utf8");
131
+ }
108
132
  function detect(d, flag, config) {
109
133
  return detectMachineType(flag, {
110
134
  platform: d.platform,
@@ -767,6 +791,45 @@ export function registerDaemonCommand(program, over = {}) {
767
791
  hqBinary: d.hqBinary,
768
792
  });
769
793
  }));
794
+ daemon
795
+ .command("login [extra...]")
796
+ .description("Sign the running daemon in from machine credentials piped on stdin; credentials and tokens stay off disk")
797
+ .option("--stdin", "Read a machine credentials JSON object from stdin")
798
+ .option("--status", "Show whether hqd has an in-memory machine sign-in")
799
+ .allowUnknownOption(true)
800
+ .action(wrap(async (d, extra, opts) => {
801
+ requireAnywhereRuntime();
802
+ if (extra?.length)
803
+ throw new DaemonCommandError("Machine credentials are accepted from stdin only; do not pass credentials as arguments.");
804
+ const socketPath = hqdSocketPath(d.home, d.env, d.platform);
805
+ if (opts.status) {
806
+ let result;
807
+ try {
808
+ result = await hqdRequest("auth.status", {}, { socketPath });
809
+ }
810
+ catch {
811
+ throw new DaemonCommandError("Could not read the daemon sign-in status.");
812
+ }
813
+ d.out(`hqd signed in: ${result.signedIn === true ? "yes" : "no"} (memory only)`);
814
+ return;
815
+ }
816
+ if (!opts.stdin)
817
+ throw new DaemonCommandError("Use `hq daemon login --stdin` to provide machine credentials.");
818
+ const raw = await (d.readStdin ?? readLoginStdin)();
819
+ const credentials = parseStdinMachineCredentials(raw);
820
+ try {
821
+ await hqdRequest("auth.login", { credentials }, { socketPath, timeoutMs: 30_000 });
822
+ }
823
+ catch (error) {
824
+ if (error instanceof HqdClientError) {
825
+ throw new DaemonCommandError(error.code === "anywhere_disabled"
826
+ ? "HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use daemon login."
827
+ : "Could not sign hqd in. Check the machine credentials and daemon status.");
828
+ }
829
+ throw new DaemonCommandError("Could not sign hqd in. Check the machine credentials and daemon status.");
830
+ }
831
+ d.out("Signed in hqd. The token is held in daemon memory and will be lost when hqd restarts.");
832
+ }));
770
833
  daemon
771
834
  .command("call <op>")
772
835
  .description("Send one request to hqd (policy.check, journal.append, memory.get, memory.put, mesh.presence, mcp.call, …) and print the result as JSON")
@@ -774,6 +837,9 @@ export function registerDaemonCommand(program, over = {}) {
774
837
  .option("--timeout <ms>", "Give up after this many milliseconds")
775
838
  .action(wrap(async (d, op, opts) => {
776
839
  requireAnywhereRuntime();
840
+ if (op === "auth.login") {
841
+ throw new Error("Use `hq daemon login --stdin`; credentials are accepted from stdin only.");
842
+ }
777
843
  let args;
778
844
  try {
779
845
  args = JSON.parse(opts.json);
@@ -230,7 +230,7 @@ function planService(service, ctx) {
230
230
  return [
231
231
  base(service, {
232
232
  kind: "scheduled",
233
- intervalSec: 60,
233
+ intervalSec: 15,
234
234
  initialDelaySec: 30,
235
235
  command: { type: "hq", args: ["lanes", "reconcile"] },
236
236
  env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
@@ -0,0 +1,20 @@
1
+ /** Ephemeral Cognito machine login for a resident hqd process. */
2
+ import type { MachineCreds } from "@indigoai-us/hq-cloud";
3
+ export declare class MachineLoginError extends Error {
4
+ readonly code: string;
5
+ constructor(code: string, message: string);
6
+ }
7
+ export type StdinMachineCredentials = Pick<MachineCreds, "username" | "secret"> & Partial<Pick<MachineCreds, "clientId" | "region" | "entityType" | "entityUid">>;
8
+ /** Parse only the established machine-creds fields. Parse errors never include input text. */
9
+ export declare function parseStdinMachineCredentials(raw: string): StdinMachineCredentials;
10
+ export interface MachineAuthenticatorOptions {
11
+ endpoint?: string;
12
+ fetchImpl?: typeof fetch;
13
+ env?: NodeJS.ProcessEnv;
14
+ signal?: AbortSignal;
15
+ }
16
+ /** Read the Cognito ID token expiry without exposing or persisting the token. */
17
+ export declare function machineTokenExpiresAt(token: string): number;
18
+ /** Authenticate without using hq-cloud's persistent machine token cache. */
19
+ export declare function authenticateMachineCredentials(credentials: StdinMachineCredentials, options?: MachineAuthenticatorOptions): Promise<string>;
20
+ //# sourceMappingURL=machine-login.d.ts.map
@@ -0,0 +1,102 @@
1
+ /** Ephemeral Cognito machine login for a resident hqd process. */
2
+ import { DEFAULT_COGNITO } from "../../utils/cognito-session.js";
3
+ const MAX_STDIN_BYTES = 64 * 1024;
4
+ export class MachineLoginError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "MachineLoginError";
10
+ }
11
+ }
12
+ /** Parse only the established machine-creds fields. Parse errors never include input text. */
13
+ export function parseStdinMachineCredentials(raw) {
14
+ if (Buffer.byteLength(raw, "utf8") > MAX_STDIN_BYTES) {
15
+ throw new MachineLoginError("bad_credentials", "stdin credentials are too large");
16
+ }
17
+ let value;
18
+ try {
19
+ value = JSON.parse(raw);
20
+ }
21
+ catch {
22
+ throw new MachineLoginError("bad_credentials", "stdin must contain a machine credentials JSON object");
23
+ }
24
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
25
+ throw new MachineLoginError("bad_credentials", "stdin must contain a machine credentials JSON object");
26
+ }
27
+ const input = value;
28
+ const allowed = new Set(["username", "secret", "clientId", "region", "entityType", "entityUid"]);
29
+ if (Object.keys(input).some((key) => !allowed.has(key))) {
30
+ throw new MachineLoginError("bad_credentials", "stdin credentials contain an unsupported field");
31
+ }
32
+ if (typeof input.username !== "string" || !input.username.trim() || typeof input.secret !== "string" || !input.secret) {
33
+ throw new MachineLoginError("bad_credentials", "stdin credentials require non-empty username and secret fields");
34
+ }
35
+ for (const name of ["clientId", "region", "entityUid"]) {
36
+ if (input[name] !== undefined && (typeof input[name] !== "string" || !input[name].trim())) {
37
+ throw new MachineLoginError("bad_credentials", `stdin credentials have an invalid ${name} field`);
38
+ }
39
+ }
40
+ if (input.entityType !== undefined && input.entityType !== "agent" && input.entityType !== "outpost") {
41
+ throw new MachineLoginError("bad_credentials", "stdin credentials have an invalid entityType field");
42
+ }
43
+ return input;
44
+ }
45
+ /** Read the Cognito ID token expiry without exposing or persisting the token. */
46
+ export function machineTokenExpiresAt(token) {
47
+ try {
48
+ const encodedPayload = token.split(".")[1];
49
+ if (!encodedPayload)
50
+ throw new Error("missing JWT payload");
51
+ const payload = JSON.parse(Buffer.from(encodedPayload, "base64url").toString("utf8"));
52
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp))
53
+ throw new Error("missing JWT expiry");
54
+ return payload.exp * 1000;
55
+ }
56
+ catch {
57
+ throw new MachineLoginError("auth_failed", "hqd sign-in failed; the identity provider returned an invalid token");
58
+ }
59
+ }
60
+ /** Authenticate without using hq-cloud's persistent machine token cache. */
61
+ export async function authenticateMachineCredentials(credentials, options = {}) {
62
+ const env = options.env ?? process.env;
63
+ const region = credentials.region ?? env.AWS_REGION ?? DEFAULT_COGNITO.region;
64
+ const clientId = credentials.clientId ?? env.HQ_COGNITO_CLIENT_ID ?? DEFAULT_COGNITO.clientId;
65
+ const endpoint = options.endpoint ?? `https://cognito-idp.${region}.amazonaws.com/`;
66
+ let response;
67
+ try {
68
+ response = await (options.fetchImpl ?? fetch)(endpoint, {
69
+ method: "POST",
70
+ headers: {
71
+ "Content-Type": "application/x-amz-json-1.1",
72
+ "X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
73
+ },
74
+ signal: options.signal,
75
+ body: JSON.stringify({
76
+ AuthFlow: "USER_PASSWORD_AUTH",
77
+ ClientId: clientId,
78
+ AuthParameters: { USERNAME: credentials.username, PASSWORD: credentials.secret },
79
+ }),
80
+ });
81
+ }
82
+ catch {
83
+ throw new MachineLoginError("auth_failed", "hqd sign-in failed; check the machine credentials and try again");
84
+ }
85
+ let body;
86
+ try {
87
+ body = await response.json();
88
+ }
89
+ catch {
90
+ throw new MachineLoginError("auth_failed", "hqd sign-in failed; check the machine credentials and try again");
91
+ }
92
+ if (!response.ok || !body || typeof body !== "object" || Array.isArray(body)) {
93
+ throw new MachineLoginError("auth_failed", "hqd sign-in failed; check the machine credentials and try again");
94
+ }
95
+ const result = body.AuthenticationResult;
96
+ const idToken = result && typeof result === "object" ? result.IdToken : undefined;
97
+ if (typeof idToken !== "string" || !idToken) {
98
+ throw new MachineLoginError("auth_failed", "hqd sign-in failed; check the machine credentials and try again");
99
+ }
100
+ return idToken;
101
+ }
102
+ //# sourceMappingURL=machine-login.js.map
@@ -14,6 +14,7 @@ export interface McpToolResult {
14
14
  export declare function jsonResult(value: unknown): McpToolResult;
15
15
  export declare const HQ_MCP_HTTP_TIMEOUT_MS = 30000;
16
16
  export declare function resolveHqMcpEndpoint(env?: NodeJS.ProcessEnv): URL;
17
+ export declare function defaultMcpToken(): Promise<string>;
17
18
  export interface HqMcpBridgeOptions {
18
19
  endpoint?: URL;
19
20
  env?: NodeJS.ProcessEnv;
@@ -27,7 +27,7 @@ export function resolveHqMcpEndpoint(env = process.env) {
27
27
  throw error;
28
28
  }
29
29
  }
30
- async function defaultMcpToken() {
30
+ export async function defaultMcpToken() {
31
31
  const { ensureCognitoToken, resolveCognitoTokenSource } = await import("../../../utils/cognito-session.js");
32
32
  if (resolveCognitoTokenSource() !== "machine") {
33
33
  const { getMcpAudienceToken, McpOAuthError } = await import("../mcp-oauth.js");
@@ -7,7 +7,7 @@
7
7
  * out of order, so clients match them by `id`.
8
8
  */
9
9
  export declare const HQD_SOCKET_NAME = "hqd.sock";
10
- export declare const HQD_OPS: readonly ["ping", "resolve", "session.open", "session.get", "session.close", "policy.check", "policy.index", "journal.append", "memory.get", "memory.put", "mesh.presence", "mcp.call", "shutdown"];
10
+ export declare const HQD_OPS: readonly ["ping", "resolve", "session.open", "session.get", "session.close", "policy.check", "policy.index", "journal.append", "memory.get", "memory.put", "mesh.presence", "mcp.call", "auth.login", "auth.status", "shutdown"];
11
11
  export type HqdOp = (typeof HQD_OPS)[number];
12
12
  /** A request line larger than this is rejected and the connection closed. */
13
13
  export declare const HQD_MAX_LINE_BYTES: number;
@@ -23,6 +23,8 @@ export const HQD_OPS = [
23
23
  "memory.put",
24
24
  "mesh.presence",
25
25
  "mcp.call",
26
+ "auth.login",
27
+ "auth.status",
26
28
  "shutdown",
27
29
  ];
28
30
  /** A request line larger than this is rejected and the connection closed. */
@@ -9,6 +9,7 @@
9
9
  import { type ResolveResult } from "../registry/registry.js";
10
10
  import { SessionRegistry } from "./session-registry.js";
11
11
  import { HqMcpBridge, type RunWorkerRequest } from "./ops/mcp.js";
12
+ import { type StdinMachineCredentials } from "./machine-login.js";
12
13
  export interface HqdServerOptions {
13
14
  socketPath: string;
14
15
  /** registry.json to resolve against (US-004). */
@@ -27,6 +28,10 @@ export interface HqdServerOptions {
27
28
  mcpEnabled?: () => boolean | Promise<boolean>;
28
29
  /** Test seam for the hosted Streamable HTTP MCP transport. */
29
30
  mcpBridge?: HqMcpBridge;
31
+ /** Test seam for machine login while keeping production credentials off disk. */
32
+ machineAuthenticator?: (credentials: StdinMachineCredentials, signal?: AbortSignal) => Promise<string>;
33
+ /** Test seam for the bounded Cognito login request. */
34
+ machineAuthTimeoutMs?: number;
30
35
  /** Test seam for reduced host parity runs without spawning an agent process. */
31
36
  workerDispatch?: (request: RunWorkerRequest, hqRoot: string, env: NodeJS.ProcessEnv, now: Date) => Promise<Record<string, unknown>>;
32
37
  /** Crash reporter for internal op failures (US-012); run.ts wires Sentry here. */
@@ -17,7 +17,8 @@ import { journalAppend, JournalOpError } from "./ops/journal.js";
17
17
  import { memoryGet, memoryPut, MemoryOpError } from "./ops/memory.js";
18
18
  import { meshPresence, MeshOpError } from "./ops/mesh.js";
19
19
  import { PolicyIndex, PolicyOpError, policyCheck } from "./ops/policy.js";
20
- import { HqMcpBridge, jsonResult, McpOpError, runWorker } from "./ops/mcp.js";
20
+ import { defaultMcpToken, HqMcpBridge, jsonResult, McpOpError, runWorker } from "./ops/mcp.js";
21
+ import { authenticateMachineCredentials, machineTokenExpiresAt, MachineLoginError, parseStdinMachineCredentials } from "./machine-login.js";
21
22
  import { HQ_MCP_TOOL_NAMES, HQD_NATIVE_TOOL_NAMES } from "../mcp/tools.js";
22
23
  import { anywhereRuntimeEnabled, HQ_ANYWHERE_RUNTIME_FLAG } from "../../commands/anywhere-runtime-flag.js";
23
24
  // The allowlist is deliberately read-only. A newly added hosted MCP tool must
@@ -110,6 +111,7 @@ export async function startHqdServer(opts) {
110
111
  const clock = opts.now ?? (() => new Date());
111
112
  const env = opts.env ?? process.env;
112
113
  const policyIndex = opts.hqRoot ? new PolicyIndex(opts.hqRoot, log) : null;
114
+ let ephemeralMachineToken;
113
115
  const requireRoot = () => {
114
116
  if (!opts.hqRoot)
115
117
  throw new OpError("no_hq_root", "hqd has no HQ root configured (set hqRoot in daemon config or HQ_ROOT)");
@@ -157,6 +159,45 @@ export async function startHqdServer(opts) {
157
159
  pid: process.pid,
158
160
  sessions: sessions.size,
159
161
  };
162
+ case "auth.status":
163
+ return Promise.resolve(mcpEnabled()).then((enabled) => {
164
+ if (!enabled)
165
+ throw new OpError("anywhere_disabled", `Daemon login is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
166
+ if (ephemeralMachineToken && ephemeralMachineToken.expiresAt <= clock().getTime())
167
+ ephemeralMachineToken = undefined;
168
+ return { signedIn: Boolean(ephemeralMachineToken), tokenSource: ephemeralMachineToken ? "stdin-memory" : null };
169
+ });
170
+ case "auth.login":
171
+ return Promise.resolve(mcpEnabled()).then(async (enabled) => {
172
+ if (!enabled)
173
+ throw new OpError("anywhere_disabled", `Daemon login is disabled by the ${HQ_ANYWHERE_RUNTIME_FLAG} flag.`);
174
+ const credentials = parseStdinMachineCredentials(JSON.stringify(args.credentials) ?? "");
175
+ const controller = new AbortController();
176
+ const timeoutMs = opts.machineAuthTimeoutMs ?? 25_000;
177
+ let timer;
178
+ try {
179
+ const authentication = (opts.machineAuthenticator ?? ((input, signal) => authenticateMachineCredentials(input, { env, signal })))(credentials, controller.signal);
180
+ const deadline = new Promise((_, reject) => {
181
+ timer = setTimeout(() => {
182
+ controller.abort();
183
+ reject(new MachineLoginError("auth_timeout", "hqd sign-in timed out; try again"));
184
+ }, timeoutMs);
185
+ });
186
+ const token = await Promise.race([authentication, deadline]);
187
+ const expiresAt = machineTokenExpiresAt(token);
188
+ if (expiresAt <= clock().getTime())
189
+ throw new MachineLoginError("auth_failed", "hqd sign-in failed; the identity provider returned an expired token");
190
+ ephemeralMachineToken = { value: token, expiresAt };
191
+ }
192
+ catch {
193
+ throw new MachineLoginError("auth_failed", "hqd sign-in failed; check the machine credentials and try again");
194
+ }
195
+ finally {
196
+ if (timer)
197
+ clearTimeout(timer);
198
+ }
199
+ return { signedIn: true, tokenSource: "stdin-memory" };
200
+ });
160
201
  case "resolve": {
161
202
  const cwd = stringArg(args, "cwd");
162
203
  return resolve(cwd);
@@ -309,7 +350,19 @@ export async function startHqdServer(opts) {
309
350
  if (remainingMs <= 0) {
310
351
  throw new McpOpError("hq_mcp_timeout", `Hosted hq-mcp request exceeded its ${timeoutMs}ms end-to-end deadline before forwarding.`);
311
352
  }
312
- hqMcp ??= new HqMcpBridge({ env });
353
+ hqMcp ??= new HqMcpBridge({
354
+ env,
355
+ getToken: async () => {
356
+ if (ephemeralMachineToken) {
357
+ if (ephemeralMachineToken.expiresAt <= clock().getTime()) {
358
+ ephemeralMachineToken = undefined;
359
+ throw new Error("The stdin machine sign-in expired; sign hqd in again.");
360
+ }
361
+ return ephemeralMachineToken.value;
362
+ }
363
+ return defaultMcpToken();
364
+ },
365
+ });
313
366
  return hqMcp.callTool(name, raw, remainingMs);
314
367
  });
315
368
  return !MCP_READ_ONLY_TOOL_NAMES.has(name)
@@ -353,7 +406,8 @@ export async function startHqdServer(opts) {
353
406
  err instanceof JournalOpError ||
354
407
  err instanceof MemoryOpError ||
355
408
  err instanceof MeshOpError ||
356
- err instanceof McpOpError
409
+ err instanceof McpOpError ||
410
+ err instanceof MachineLoginError
357
411
  ? { code: err.code, message: err.message }
358
412
  : { code: "internal", message: err instanceof Error ? err.message : String(err) };
359
413
  if (error.code === "internal") {
@@ -6,7 +6,7 @@
6
6
  * unit files under a systemd user dir and is idempotent and reversible.
7
7
  * Enabling the timer is optional — the CLI pass works without it.
8
8
  */
9
- export declare const RECONCILE_TIMER_INTERVAL = "1min";
9
+ export declare const RECONCILE_TIMER_INTERVAL = "15s";
10
10
  export interface ProviderPathEnv {
11
11
  /**
12
12
  * For each resolved provider: the directory where the command name was
@@ -13,7 +13,7 @@ import { LanesError } from "./errors.js";
13
13
  import { RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME } from "./reconcile.js";
14
14
  import { resolveHqBinary, resolveNodeBinary, } from "../mesh/live/daemon/install.js";
15
15
  import { SYSTEMD_PATH_ENV } from "../service-manager/systemd.js";
16
- export const RECONCILE_TIMER_INTERVAL = "1min";
16
+ export const RECONCILE_TIMER_INTERVAL = "15s";
17
17
  /**
18
18
  * Resolve provider commands with the installing shell's PATH. The resulting
19
19
  * directories are carried into the systemd unit, whose default PATH is too
@@ -1209,13 +1209,17 @@ export async function runPassBody(hqRoot, deps) {
1209
1209
  cpuObservation,
1210
1210
  skipAdmissionWithoutCpuSample: !hasAdmissionCandidates,
1211
1211
  };
1212
+ let dueKeepAliveLaneIds = new Set();
1212
1213
  await withAllLaneAdmissionLocks(hqRoot, async () => {
1213
1214
  const freshLanes = loadLaneRecords(hqRoot, summary);
1214
1215
  const freshQuestions = loadQuestionRecords(hqRoot, summary);
1215
1216
  await closeInterruptedDoneStoryWorkers(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, storyCloseEnabledByCompany);
1216
- await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany, deps.terminalKeepAliveCandidate);
1217
+ const dueKeepAlivesAdmitted = await resumeKeepAlive(hqRoot, freshLanes, freshQuestions, summary, admissionDeps, now, keepAliveFlagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany, deps.terminalKeepAliveCandidate, async (laneIds) => {
1218
+ dueKeepAliveLaneIds = new Set(laneIds);
1219
+ await admitQueued(hqRoot, loadLaneRecords(hqRoot, summary), summary, admissionDeps, now, laneIds);
1220
+ });
1217
1221
  const drainLanes = loadLaneRecords(hqRoot, summary);
1218
- await admitQueued(hqRoot, drainLanes, summary, admissionDeps, now);
1222
+ await admitQueued(hqRoot, drainLanes, summary, admissionDeps, now, undefined, dueKeepAlivesAdmitted ? dueKeepAliveLaneIds : undefined);
1219
1223
  await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, admissionDeps);
1220
1224
  });
1221
1225
  if (!deps.dryRun && cpuObservation.snapshot !== undefined) {
@@ -1715,7 +1719,7 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1715
1719
  }
1716
1720
  }
1717
1721
  }
1718
- async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds) {
1722
+ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, excludeLaneIds) {
1719
1723
  if (onlyLaneIds === undefined)
1720
1724
  expireAbandonedPickupValidation(hqRoot, lanes, summary, deps, now);
1721
1725
  // A lane whose pickup probe is still out is not admitted: its record is
@@ -1724,6 +1728,7 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds) {
1724
1728
  const localQueued = lanes
1725
1729
  .filter((lane) => lane.state === "queued" &&
1726
1730
  (onlyLaneIds === undefined || onlyLaneIds.has(lane.lane_id)) &&
1731
+ (excludeLaneIds === undefined || !excludeLaneIds.has(lane.lane_id)) &&
1727
1732
  lane.pickup_validation_pending !== true &&
1728
1733
  laneRunsHere(lane, deps.host))
1729
1734
  .filter((lane) => !hasFreshAdmissionReservation(lane, now.getTime()))
@@ -2324,7 +2329,7 @@ function appendBlockedKeepAliveHistory(hqRoot, lane, terminalAt, at, reason, dep
2324
2329
  }),
2325
2330
  }), false);
2326
2331
  }
2327
- async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany, terminalKeepAliveCandidate) {
2332
+ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, flagByCompany, supersessionLineageAwareByCompany, blockedResumeFlagByCompany, immediateFirstResumeFlagByCompany, terminalKeepAliveCandidate, admitDueCandidates) {
2328
2333
  const nowMs = now.getTime();
2329
2334
  const nowIso = now.toISOString();
2330
2335
  const config = loadLanesConfig(hqRoot);
@@ -2347,7 +2352,39 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2347
2352
  markError(summary, err);
2348
2353
  }
2349
2354
  }
2350
- for (const originalLane of lanes) {
2355
+ const dueCandidateIds = new Set();
2356
+ if (admitDueCandidates !== undefined && terminalKeepAliveCandidate === undefined) {
2357
+ for (const lane of lanes) {
2358
+ if (lane.state !== "done" || lane.keep_alive !== true || !laneRunsHere(lane, deps.host))
2359
+ continue;
2360
+ const envelope = readLatestEnvelopeRecord(hqRoot, lane.lane_id)?.envelope;
2361
+ if (!envelope || envelope.decision !== "done" || hasPendingKeepAliveWork(hqRoot, lane, questions))
2362
+ continue;
2363
+ const anchorMs = Date.parse(lane.last_envelope?.at ?? lane.updated_at);
2364
+ const nextCheckMs = Date.parse(envelope.next_check_at ?? "");
2365
+ if (!Number.isFinite(anchorMs) || !Number.isFinite(nextCheckMs))
2366
+ continue;
2367
+ const cappedNextCheckMs = Math.min(nextCheckMs, anchorMs + LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS);
2368
+ if (cappedNextCheckMs <= nowMs &&
2369
+ doneKeepAliveVerdict(lane, envelope, false, { nowMs, nextCheckAnchorMs: anchorMs }) === "eligible")
2370
+ dueCandidateIds.add(lane.lane_id);
2371
+ }
2372
+ }
2373
+ const candidates = dueCandidateIds.size === 0
2374
+ ? lanes
2375
+ : [...lanes].sort((a, b) => Number(dueCandidateIds.has(b.lane_id)) - Number(dueCandidateIds.has(a.lane_id)));
2376
+ const queuedDueCandidateIds = new Set();
2377
+ let dueAdmissionRan = false;
2378
+ const admitQueuedDueCandidates = async () => {
2379
+ if (dueAdmissionRan || queuedDueCandidateIds.size === 0 || admitDueCandidates === undefined)
2380
+ return;
2381
+ dueAdmissionRan = true;
2382
+ await admitDueCandidates(new Set(queuedDueCandidateIds));
2383
+ };
2384
+ for (const originalLane of candidates) {
2385
+ if (admitDueCandidates !== undefined && dueCandidateIds.size > 0 && !dueCandidateIds.has(originalLane.lane_id)) {
2386
+ await admitQueuedDueCandidates();
2387
+ }
2351
2388
  if (terminalKeepAliveCandidate && (originalLane.lane_id !== terminalKeepAliveCandidate.laneId ||
2352
2389
  originalLane.version !== terminalKeepAliveCandidate.version ||
2353
2390
  originalLane.state !== "done"))
@@ -3012,6 +3049,8 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
3012
3049
  }
3013
3050
  if (!queued.changed)
3014
3051
  continue;
3052
+ if (resumeReason === "next_check_at_due")
3053
+ queuedDueCandidateIds.add(current.lane_id);
3015
3054
  summary.resumed.push({
3016
3055
  id: current.lane_id,
3017
3056
  from: resumedFrom,
@@ -3032,6 +3071,8 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
3032
3071
  });
3033
3072
  }
3034
3073
  }
3074
+ await admitQueuedDueCandidates();
3075
+ return dueAdmissionRan;
3035
3076
  }
3036
3077
  async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneIds) {
3037
3078
  const config = loadLanesConfig(hqRoot);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.340.0",
3
+ "version": "5.341.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {