@indigoai-us/hq-cli 5.345.57 → 5.345.59

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/command-catalog.generated.d.ts +12 -0
  3. package/dist/command-catalog.generated.js +15 -0
  4. package/dist/commands/access.d.ts +14 -0
  5. package/dist/commands/access.js +69 -1
  6. package/dist/commands/agent-kit.js +20 -10
  7. package/dist/commands/billing.d.ts +13 -0
  8. package/dist/commands/billing.js +31 -0
  9. package/dist/commands/db-dump.js +2 -2
  10. package/dist/commands/db-remote-deps.js +6 -0
  11. package/dist/commands/lanes-loop.js +3 -1
  12. package/dist/commands/lanes-questions.js +16 -0
  13. package/dist/commands/lanes-session-start.js +19 -3
  14. package/dist/commands/lanes-shared.d.ts +1 -0
  15. package/dist/commands/lanes.d.ts +3 -1
  16. package/dist/commands/lanes.js +52 -2
  17. package/dist/lib/agent-kit/inbox-poll-backoff-flag.d.ts +19 -0
  18. package/dist/lib/agent-kit/inbox-poll-backoff-flag.js +70 -0
  19. package/dist/lib/agent-kit/run/inbox.d.ts +7 -3
  20. package/dist/lib/agent-kit/run/inbox.js +45 -6
  21. package/dist/lib/agent-kit/run/stop-signals.d.ts +14 -0
  22. package/dist/lib/agent-kit/run/stop-signals.js +26 -0
  23. package/dist/lib/agent-kit/services.d.ts +2 -2
  24. package/dist/lib/agent-kit/services.js +5 -3
  25. package/dist/lib/anywhere-runtime-flag-key.d.ts +1 -0
  26. package/dist/lib/anywhere-runtime-flag-key.js +1 -0
  27. package/dist/lib/anywhere-runtime-revocation-marker.d.ts +4 -0
  28. package/dist/lib/anywhere-runtime-revocation-marker.js +26 -0
  29. package/dist/lib/daemon/run.d.ts +2 -0
  30. package/dist/lib/daemon/run.js +17 -4
  31. package/dist/lib/daemon/server.js +7 -0
  32. package/dist/lib/db/control-plane.d.ts +12 -1
  33. package/dist/lib/db/usage-format.d.ts +2 -1
  34. package/dist/lib/db/usage-format.js +19 -0
  35. package/dist/lib/install/provider-wrappers.js +13 -0
  36. package/dist/lib/lanes/loop.d.ts +23 -1
  37. package/dist/lib/lanes/loop.js +134 -19
  38. package/dist/lib/lanes/questions.d.ts +5 -0
  39. package/dist/lib/lanes/reconcile.js +3 -0
  40. package/dist/lib/lanes/story-ledger.js +30 -20
  41. package/dist/lib/lanes/types.js +1 -1
  42. package/package.json +1 -1
@@ -0,0 +1,70 @@
1
+ import { createFlagClient, } from "@indigoai-us/hq-flags-client";
2
+ import { resolveFlagGate } from "../flag-registry.js";
3
+ import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
4
+ export const AGENT_KIT_INBOX_BACKOFF_FLAG = "agents.inbox-poll-backoff-agent-kit";
5
+ export const AGENT_KIT_INBOX_BACKOFF_DEFAULT = true;
6
+ export const AGENT_KIT_INBOX_BACKOFF_CACHE_MS = 3 * 60_000;
7
+ export const AGENT_KIT_INBOX_BACKOFF_TIMEOUT_MS = 2_000;
8
+ /** Read the default-on kill switch; registry outages preserve idle backoff. */
9
+ export function createAgentKitInboxBackoffFlagReader(dependencies) {
10
+ const createClient = dependencies.createClient ?? createFlagClient;
11
+ const now = dependencies.now ?? Date.now;
12
+ const report = dependencies.reportFailure ?? ((error) => {
13
+ const name = error instanceof Error ? error.name : "UnknownError";
14
+ process.stderr.write(`HQ agent-kit inbox backoff flag lookup failed (${name}); keeping the default-on behavior.\n`);
15
+ });
16
+ let cachedValue = AGENT_KIT_INBOX_BACKOFF_DEFAULT;
17
+ let lastReadAt = Number.NEGATIVE_INFINITY;
18
+ let pending = null;
19
+ const read = async () => {
20
+ let client;
21
+ let failed = false;
22
+ const reportOnce = (error) => {
23
+ if (failed)
24
+ return;
25
+ failed = true;
26
+ report(error);
27
+ };
28
+ try {
29
+ client = createClient({
30
+ endpoint: resolveFlagRegistryEndpoint(dependencies.endpoint),
31
+ ...(dependencies.companySlug ? { companyIdentifiers: [dependencies.companySlug] } : {}),
32
+ env: {},
33
+ getToken: dependencies.getToken,
34
+ ...(dependencies.fetch ? { fetch: dependencies.fetch } : {}),
35
+ refreshIntervalMs: 0,
36
+ requestTimeoutMs: AGENT_KIT_INBOX_BACKOFF_TIMEOUT_MS,
37
+ onError: reportOnce,
38
+ });
39
+ await client.ready();
40
+ cachedValue = failed
41
+ ? AGENT_KIT_INBOX_BACKOFF_DEFAULT
42
+ : resolveFlagGate(client, AGENT_KIT_INBOX_BACKOFF_FLAG, { fallback: AGENT_KIT_INBOX_BACKOFF_DEFAULT }, () => AGENT_KIT_INBOX_BACKOFF_DEFAULT);
43
+ }
44
+ catch (error) {
45
+ reportOnce(error);
46
+ cachedValue = AGENT_KIT_INBOX_BACKOFF_DEFAULT;
47
+ }
48
+ finally {
49
+ try {
50
+ client?.close();
51
+ }
52
+ catch (error) {
53
+ reportOnce(error);
54
+ cachedValue = AGENT_KIT_INBOX_BACKOFF_DEFAULT;
55
+ }
56
+ }
57
+ return cachedValue;
58
+ };
59
+ return () => {
60
+ const currentTime = now();
61
+ if (pending)
62
+ return pending;
63
+ if (currentTime - lastReadAt < AGENT_KIT_INBOX_BACKOFF_CACHE_MS)
64
+ return Promise.resolve(cachedValue);
65
+ lastReadAt = currentTime;
66
+ pending = read().finally(() => { pending = null; });
67
+ return pending;
68
+ };
69
+ }
70
+ //# sourceMappingURL=inbox-poll-backoff-flag.js.map
@@ -21,6 +21,7 @@ import { type WakeConfig, type WakeDeps, type WakeNotice, type WakeResult } from
21
21
  export declare const SEEN_IDS_NAME = "seen-ids.json";
22
22
  export declare const INBOX_JSONL_NAME = "inbox.jsonl";
23
23
  export declare const MAX_SEEN_IDS = 5000;
24
+ export declare const INBOX_POLL_BACKOFF_MAX_MS = 30000;
24
25
  export interface InboxItem {
25
26
  id: string;
26
27
  raw: Record<string, unknown>;
@@ -38,10 +39,13 @@ export interface InboxDeps {
38
39
  ack: boolean;
39
40
  getToken: () => Promise<string>;
40
41
  log: KitLogger;
41
- fetchInbox?: (token: string) => Promise<InboxFetchResult>;
42
+ fetchInbox?: (token: string, signal?: AbortSignal) => Promise<InboxFetchResult>;
42
43
  ackItem?: (token: string, id: string) => Promise<number>;
43
44
  warmCache?: (token: string, agentUid: string) => Promise<unknown>;
44
- sleep?: (ms: number) => Promise<void>;
45
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
46
+ readInboxBackoffEnabled?: () => Promise<boolean>;
47
+ signal?: AbortSignal;
48
+ companySlug?: string;
45
49
  maxPolls?: number;
46
50
  /** Override wake config lookup (tests). `null` disables the wake. */
47
51
  wakeConfig?: WakeConfig | null;
@@ -66,7 +70,7 @@ export interface InboxFetchResult {
66
70
  export declare function agentApiUrl(apiBaseUrl: string, route: string): string;
67
71
  export declare function agentInboxUrl(apiBaseUrl: string, agentUid: string): string;
68
72
  export declare function truncateBody(text: string, max?: number): string;
69
- export declare function defaultFetchInbox(agentUid: string, apiBaseUrl: string): (token: string) => Promise<InboxFetchResult>;
73
+ export declare function defaultFetchInbox(agentUid: string, apiBaseUrl: string): (token: string, signal?: AbortSignal) => Promise<InboxFetchResult>;
70
74
  export declare function defaultAckItem(agentUid: string, apiBaseUrl: string): (token: string, id: string) => Promise<number>;
71
75
  export declare function pollInboxOnce(deps: InboxDeps): Promise<{
72
76
  ok: boolean;
@@ -21,10 +21,12 @@ import { vaultApiFetch } from "../../../utils/vault-api.js";
21
21
  import { writeComponentStatus } from "../creds.js";
22
22
  import { pendingInbox } from "../inbox-state.js";
23
23
  import { writePrivateFile } from "./../private-file.js";
24
+ import { createAgentKitInboxBackoffFlagReader } from "../inbox-poll-backoff-flag.js";
24
25
  import { buildWakeNotice, currentWakeState, fireWake, readWakeConfig, shouldWake, } from "../wake.js";
25
26
  export const SEEN_IDS_NAME = "seen-ids.json";
26
27
  export const INBOX_JSONL_NAME = "inbox.jsonl";
27
28
  export const MAX_SEEN_IDS = 5000;
29
+ export const INBOX_POLL_BACKOFF_MAX_MS = 30_000;
28
30
  /** Pull `{id}` out of each server item; items without a string id are dropped. */
29
31
  export function extractInboxItems(body) {
30
32
  const rec = body && typeof body === "object" ? body : {};
@@ -107,12 +109,13 @@ export function truncateBody(text, max = INBOX_ERROR_BODY_MAX) {
107
109
  return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
108
110
  }
109
111
  export function defaultFetchInbox(agentUid, apiBaseUrl) {
110
- return async (token) => {
112
+ return async (token, signal) => {
111
113
  const url = new URL(agentInboxUrl(apiBaseUrl, agentUid));
112
114
  const res = await vaultApiFetch({
113
115
  token,
114
116
  baseUrl: url.origin,
115
117
  path: url.pathname,
118
+ ...(signal ? { signal } : {}),
116
119
  });
117
120
  const text = await res.text().catch(() => "");
118
121
  let body;
@@ -137,6 +140,8 @@ export function defaultAckItem(agentUid, apiBaseUrl) {
137
140
  };
138
141
  }
139
142
  export async function pollInboxOnce(deps) {
143
+ if (deps.signal?.aborted)
144
+ return { ok: false, mirrored: 0 };
140
145
  const fetchInbox = deps.fetchInbox ?? defaultFetchInbox(deps.agentUid, deps.apiBaseUrl);
141
146
  const ackItem = deps.ackItem ?? defaultAckItem(deps.agentUid, deps.apiBaseUrl);
142
147
  // No default cache warm: the human conversation routes 403 for agent callers.
@@ -152,9 +157,11 @@ export async function pollInboxOnce(deps) {
152
157
  }
153
158
  let res;
154
159
  try {
155
- res = await fetchInbox(token);
160
+ res = await fetchInbox(token, deps.signal);
156
161
  }
157
162
  catch (err) {
163
+ if (deps.signal?.aborted)
164
+ return { ok: false, mirrored: 0 };
158
165
  writeComponentStatus(deps.paths, "inbox", "error");
159
166
  deps.log("error", `inbox fetch failed: GET ${agentInboxUrl(deps.apiBaseUrl, deps.agentUid)}: ${err instanceof Error ? err.message : String(err)}`);
160
167
  return { ok: false, mirrored: 0 };
@@ -218,14 +225,46 @@ async function maybeWake(deps, freshCount) {
218
225
  }
219
226
  }
220
227
  export async function runInboxLoop(deps) {
221
- const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
228
+ const sleep = deps.sleep ?? ((ms, signal) => new Promise((resolve) => {
229
+ if (signal?.aborted)
230
+ return resolve();
231
+ const timer = setTimeout(done, ms);
232
+ const abort = () => {
233
+ clearTimeout(timer);
234
+ done();
235
+ };
236
+ function done() {
237
+ signal?.removeEventListener("abort", abort);
238
+ resolve();
239
+ }
240
+ signal?.addEventListener("abort", abort, { once: true });
241
+ }));
242
+ const readBackoffEnabled = deps.readInboxBackoffEnabled ?? createAgentKitInboxBackoffFlagReader({
243
+ companySlug: deps.companySlug,
244
+ getToken: deps.getToken,
245
+ });
222
246
  let polls = 0;
223
- for (;;) {
224
- await pollInboxOnce(deps);
247
+ let consecutiveEmptyPolls = 0;
248
+ while (!deps.signal?.aborted) {
249
+ const result = await pollInboxOnce(deps);
225
250
  polls += 1;
226
251
  if (deps.maxPolls !== undefined && polls >= deps.maxPolls)
227
252
  return;
228
- await sleep(deps.pollMs);
253
+ if (deps.signal?.aborted)
254
+ return;
255
+ if (!result.ok || result.mirrored > 0)
256
+ consecutiveEmptyPolls = 0;
257
+ const enabled = await readBackoffEnabled();
258
+ const delay = enabled && result.ok && result.mirrored === 0 && consecutiveEmptyPolls > 0
259
+ ? Math.min(deps.pollMs * 2 ** consecutiveEmptyPolls, Math.max(deps.pollMs, INBOX_POLL_BACKOFF_MAX_MS))
260
+ : deps.pollMs;
261
+ if (enabled && result.ok && result.mirrored === 0)
262
+ consecutiveEmptyPolls += 1;
263
+ else
264
+ consecutiveEmptyPolls = 0;
265
+ if (deps.signal?.aborted)
266
+ return;
267
+ await sleep(delay, deps.signal);
229
268
  }
230
269
  }
231
270
  //# sourceMappingURL=inbox.js.map
@@ -0,0 +1,14 @@
1
+ import { EventEmitter } from "node:events";
2
+ export declare const KIT_STOP_GRACE_MS = 10000;
3
+ interface KitStopControllerOptions {
4
+ emitter?: Pick<EventEmitter, "once" | "removeListener">;
5
+ forceExit?: (code: number) => void;
6
+ timeoutMs?: number;
7
+ }
8
+ /** Abort service work on stop, with a deadline if an awaited operation stalls. */
9
+ export declare function createKitStopController(options?: KitStopControllerOptions): {
10
+ signal: AbortSignal;
11
+ dispose(): void;
12
+ };
13
+ export {};
14
+ //# sourceMappingURL=stop-signals.d.ts.map
@@ -0,0 +1,26 @@
1
+ export const KIT_STOP_GRACE_MS = 10_000;
2
+ /** Abort service work on stop, with a deadline if an awaited operation stalls. */
3
+ export function createKitStopController(options = {}) {
4
+ const emitter = options.emitter ?? process;
5
+ const forceExit = options.forceExit ?? ((code) => process.exit(code));
6
+ const controller = new AbortController();
7
+ let deadline;
8
+ const stop = () => {
9
+ if (controller.signal.aborted)
10
+ return;
11
+ controller.abort();
12
+ deadline = setTimeout(() => forceExit(0), options.timeoutMs ?? KIT_STOP_GRACE_MS);
13
+ };
14
+ emitter.once("SIGTERM", stop);
15
+ emitter.once("SIGINT", stop);
16
+ return {
17
+ signal: controller.signal,
18
+ dispose() {
19
+ if (deadline)
20
+ clearTimeout(deadline);
21
+ emitter.removeListener("SIGTERM", stop);
22
+ emitter.removeListener("SIGINT", stop);
23
+ },
24
+ };
25
+ }
26
+ //# sourceMappingURL=stop-signals.js.map
@@ -14,8 +14,8 @@ export declare function isKitService(value: string): value is KitService;
14
14
  * file a loud error instead of a browser-login hang; HQ_AGENT_DIR keeps a
15
15
  * relocated kit consistent across restarts.
16
16
  */
17
- export declare function kitServiceEnv(paths: AgentKitPaths): Record<string, string>;
18
- export declare function kitServiceSpec(service: KitService, paths: AgentKitPaths): ServiceSpec;
17
+ export declare function kitServiceEnv(paths: AgentKitPaths, env?: NodeJS.ProcessEnv): Record<string, string>;
18
+ export declare function kitServiceSpec(service: KitService, paths: AgentKitPaths, env?: NodeJS.ProcessEnv): ServiceSpec;
19
19
  /**
20
20
  * Services to install. `sync` is included only when the kit is configured to
21
21
  * mirror the vault to disk; by default a bot reads files on demand through the
@@ -20,14 +20,16 @@ const DESCRIPTIONS = {
20
20
  * file a loud error instead of a browser-login hang; HQ_AGENT_DIR keeps a
21
21
  * relocated kit consistent across restarts.
22
22
  */
23
- export function kitServiceEnv(paths) {
23
+ export function kitServiceEnv(paths, env = process.env) {
24
+ const flagsApiUrl = env.HQ_FLAGS_API_URL;
24
25
  return {
25
26
  HQ_AGENT_DIR: paths.agentDir,
26
27
  HQ_MACHINE_CREDS_FILE: paths.machineCredsPath,
27
28
  HQ_REQUIRE_MACHINE_IDENTITY: "1",
29
+ ...(flagsApiUrl?.trim() ? { HQ_FLAGS_API_URL: flagsApiUrl } : {}),
28
30
  };
29
31
  }
30
- export function kitServiceSpec(service, paths) {
32
+ export function kitServiceSpec(service, paths, env = process.env) {
31
33
  return {
32
34
  name: service,
33
35
  label: `${KIT_LABEL_PREFIX}.${service}`,
@@ -35,7 +37,7 @@ export function kitServiceSpec(service, paths) {
35
37
  args: ["agent", "kit", "run", service],
36
38
  logPath: serviceLogPath(paths, service),
37
39
  workingDir: paths.agentDir,
38
- env: kitServiceEnv(paths),
40
+ env: kitServiceEnv(paths, env),
39
41
  restartSec: 10,
40
42
  };
41
43
  }
@@ -1,5 +1,6 @@
1
1
  /** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
2
2
  export declare const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ export declare const HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER = "hq-anywhere-runtime.revoked";
3
4
  /** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
4
5
  export declare function hqAnywhereRuntimeLocallyDisabled(env?: NodeJS.ProcessEnv): boolean;
5
6
  //# sourceMappingURL=anywhere-runtime-flag-key.d.ts.map
@@ -1,5 +1,6 @@
1
1
  /** hq-flags key for the staged HQ Anywhere runtime. The registry default is on. */
2
2
  export const HQ_ANYWHERE_RUNTIME_FLAG = "hq-anywhere-runtime";
3
+ export const HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER = "hq-anywhere-runtime.revoked";
3
4
  /** The HQ Anywhere environment kill switch stays compatible with managed provider wrappers. */
4
5
  export function hqAnywhereRuntimeLocallyDisabled(env = process.env) {
5
6
  const value = env.HQ_FLAG_HQ_ANYWHERE_RUNTIME;
@@ -0,0 +1,4 @@
1
+ export declare function anywhereRuntimeRevocationMarkerPath(home?: string, env?: NodeJS.ProcessEnv): string;
2
+ /** Keep installed wrappers aligned with the daemon's refreshed access decision. */
3
+ export declare function updateAnywhereRuntimeRevocationMarker(enabled: boolean, home?: string, env?: NodeJS.ProcessEnv): void;
4
+ //# sourceMappingURL=anywhere-runtime-revocation-marker.d.ts.map
@@ -0,0 +1,26 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER } from "./anywhere-runtime-flag-key.js";
5
+ import { registryDir } from "./registry/registry.js";
6
+ export function anywhereRuntimeRevocationMarkerPath(home = os.homedir(), env = process.env) {
7
+ return path.join(registryDir(home, env), HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER);
8
+ }
9
+ /** Keep installed wrappers aligned with the daemon's refreshed access decision. */
10
+ export function updateAnywhereRuntimeRevocationMarker(enabled, home = os.homedir(), env = process.env) {
11
+ const marker = anywhereRuntimeRevocationMarkerPath(home, env);
12
+ if (!enabled) {
13
+ fs.mkdirSync(path.dirname(marker), { recursive: true, mode: 0o700 });
14
+ fs.writeFileSync(marker, "revoked\n", { mode: 0o600 });
15
+ fs.chmodSync(marker, 0o600);
16
+ return;
17
+ }
18
+ try {
19
+ fs.unlinkSync(marker);
20
+ }
21
+ catch (error) {
22
+ if (error.code !== "ENOENT")
23
+ throw error;
24
+ }
25
+ }
26
+ //# sourceMappingURL=anywhere-runtime-revocation-marker.js.map
@@ -29,6 +29,8 @@ export interface DaemonLockConflictOutcome {
29
29
  export declare function daemonLockConflictOutcome(lock: DaemonLockResult, anywhereEnabled: boolean): DaemonLockConflictOutcome | null;
30
30
  /** hqd is available on every platform while Anywhere is enabled; its listener fails closed if setup is unsafe. */
31
31
  export declare function shouldStartHqd(anywhereEnabled: boolean): boolean;
32
+ /** Resolve the supervisor's runtime-access decision and reflect it in the local wrapper marker. */
33
+ export declare function refreshHqdRuntimeDecision(readDecision: () => Promise<boolean>, home: string, env: NodeJS.ProcessEnv, onMarkerError?: (error: unknown) => void): Promise<boolean>;
32
34
  /** Route an updater request through the daemon's existing graceful handoff. */
33
35
  export declare function handoffForPackageUpdateRequest(requested: boolean, restartDaemon: () => void): void;
34
36
  export interface RunDaemonOptions {
@@ -30,6 +30,7 @@ import { retireLegacyMeshUnits } from "./migrate.js";
30
30
  import { daemonDir as meshDaemonDir, daemonPidPath as meshDaemonPidPath } from "../mesh/live/daemon/paths.js";
31
31
  import { readPidLock } from "../mesh/live/daemon/pid-lock.js";
32
32
  import { registryDir, registryFile } from "../registry/registry.js";
33
+ import { updateAnywhereRuntimeRevocationMarker } from "../anywhere-runtime-revocation-marker.js";
33
34
  import { daemonRootRefusal } from "./install.js";
34
35
  import { detectMachineType } from "./machine-type.js";
35
36
  import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from "./paths.js";
@@ -93,6 +94,17 @@ export function daemonLockConflictOutcome(lock, anywhereEnabled) {
93
94
  export function shouldStartHqd(anywhereEnabled) {
94
95
  return anywhereEnabled;
95
96
  }
97
+ /** Resolve the supervisor's runtime-access decision and reflect it in the local wrapper marker. */
98
+ export async function refreshHqdRuntimeDecision(readDecision, home, env, onMarkerError = () => { }) {
99
+ const enabled = await readDecision();
100
+ try {
101
+ updateAnywhereRuntimeRevocationMarker(enabled, home, env);
102
+ }
103
+ catch (error) {
104
+ onMarkerError(error);
105
+ }
106
+ return enabled;
107
+ }
96
108
  /** Route an updater request through the daemon's existing graceful handoff. */
97
109
  export function handoffForPackageUpdateRequest(requested, restartDaemon) {
98
110
  if (requested)
@@ -897,13 +909,13 @@ export async function runDaemon(opts) {
897
909
  onError: (name) => log("warn", `hqd person setting read failed (${name}); keeping the last read value for up to ${HQD_PERSON_SETTING_GRACE_MS / 60_000} minutes`),
898
910
  }), { graceMs: HQD_PERSON_SETTING_GRACE_MS });
899
911
  const hqdListener = await startHqdRuntimeListener({
900
- isEnabled: async () => shouldStartHqd(await anywhereRuntimeAllowed(env, {
912
+ isEnabled: async () => refreshHqdRuntimeDecision(async () => shouldStartHqd(await anywhereRuntimeAllowed(env, {
901
913
  refreshFlag: async () => {
902
914
  const { refreshFlagRegistrySnapshot } = await import("../flag-registry.js");
903
915
  await refreshFlagRegistrySnapshot(5_000);
904
916
  },
905
917
  readSetting: readPersonSetting,
906
- })),
918
+ })), home, env, () => log("warn", "hqd runtime revocation marker update failed")),
907
919
  startServer: async () => {
908
920
  const startConfig = readDaemonConfig(paths.configPath);
909
921
  return startHqdServer({
@@ -959,8 +971,9 @@ export async function runDaemon(opts) {
959
971
  releaseDaemonLock(paths.lockPath);
960
972
  resolve(exitCode);
961
973
  // Pipes, sockets and unrelated timers can outlive a clean shutdown.
962
- // Keep restart and signal exits bounded just like the handoff path.
963
- setTimeout(() => process.exit(exitCode), 5_000).unref();
974
+ // Services and the lock are already stopped; unrelated handles should
975
+ // not hold a restart or signal exit open for another service interval.
976
+ setTimeout(() => process.exit(exitCode), 1_000).unref();
964
977
  });
965
978
  };
966
979
  /** Exit for a restart but leave the services running for the next daemon to adopt. */
@@ -23,6 +23,7 @@ import { defaultMcpToken, HqMcpBridge, jsonResult, McpOpError, runWorker } from
23
23
  import { authenticateMachineCredentials, machineTokenExpiresAt, MachineLoginError, parseStdinMachineCredentials } from "./machine-login.js";
24
24
  import { HQ_MCP_TOOL_NAMES, HQD_NATIVE_TOOL_NAMES } from "../mcp/tools.js";
25
25
  import { HQ_ANYWHERE_RUNTIME_FLAG } from "../../commands/anywhere-runtime-flag.js";
26
+ import { updateAnywhereRuntimeRevocationMarker } from "../anywhere-runtime-revocation-marker.js";
26
27
  import { readHqAnywherePersonSetting } from "../anywhere-person-setting.js";
27
28
  import { createBoundedDaemonFlagMonitor } from "./flag-monitor.js";
28
29
  import { readDaemonFlag } from "./flag.js";
@@ -416,6 +417,12 @@ export async function startHqdServer(opts) {
416
417
  return personSetting === true;
417
418
  },
418
419
  onChange: (enabled) => {
420
+ try {
421
+ updateAnywhereRuntimeRevocationMarker(enabled, env.HOME?.trim() || undefined, env);
422
+ }
423
+ catch (error) {
424
+ log("warn", `hqd runtime revocation marker update failed (${error instanceof Error ? error.name : "UnknownError"})`);
425
+ }
419
426
  if (!enabled) {
420
427
  flagRevocationGeneration += 1;
421
428
  ephemeralMachineToken = undefined;
@@ -71,7 +71,18 @@ export interface RemoteUsageResponse {
71
71
  apps?: RemoteAppUsage[];
72
72
  }
73
73
  /** Answer for one app: its own numbers, or the company numbers when it shares the company cluster. */
74
- export type RemoteAppUsageResponse = (RemoteAppUsage & {
74
+ export interface RemoteRemovedAppUsage {
75
+ companyUid: string;
76
+ appId: string;
77
+ mode: "app-cluster" | "company-cluster";
78
+ status: "detached" | "purged";
79
+ removedAt: string;
80
+ retainUntil: string | null;
81
+ purgedAt: string | null;
82
+ billed: false;
83
+ exempt: boolean;
84
+ }
85
+ export type RemoteAppUsageResponse = RemoteRemovedAppUsage | (RemoteAppUsage & {
75
86
  companyUid: string;
76
87
  mode: "app-cluster";
77
88
  status?: string;
@@ -1,7 +1,8 @@
1
- import type { RemoteAppUsage, RemoteAppUsageResponse, RemoteUsageResponse } from "./control-plane.js";
1
+ import type { RemoteAppUsage, RemoteAppUsageResponse, RemoteRemovedAppUsage, RemoteUsageResponse } from "./control-plane.js";
2
2
  export declare function formatBytes(bytes: number): string;
3
3
  export declare function formatUsage(usage: RemoteUsageResponse): string;
4
4
  export declare function formatAppUsage(usage: RemoteAppUsage): string;
5
+ export declare function formatRemovedAppUsage(usage: RemoteRemovedAppUsage): string;
5
6
  export declare function formatAppUsageResponse(response: RemoteAppUsageResponse): string;
6
7
  export declare function formatCompanyApps(apps: RemoteAppUsage[]): string;
7
8
  //# sourceMappingURL=usage-format.d.ts.map
@@ -80,7 +80,26 @@ export function formatAppUsage(usage) {
80
80
  : `Last measured ${usage.measuredAt}. Numbers can lag by a few minutes.`);
81
81
  return lines.join("\n");
82
82
  }
83
+ function day(iso) {
84
+ return iso.slice(0, 10);
85
+ }
86
+ export function formatRemovedAppUsage(usage) {
87
+ const lines = [`The database for app ${usage.appId} was removed on ${day(usage.removedAt)}.`];
88
+ if (usage.purgedAt) {
89
+ lines.push(`Its data was deleted for good on ${day(usage.purgedAt)}.`);
90
+ }
91
+ else if (usage.retainUntil) {
92
+ lines.push(`Its data is kept until ${day(usage.retainUntil)}, then deleted for good.`);
93
+ }
94
+ lines.push("It is not billed.");
95
+ return lines.join("\n");
96
+ }
97
+ function isRemoved(response) {
98
+ return "status" in response && (response.status === "detached" || response.status === "purged");
99
+ }
83
100
  export function formatAppUsageResponse(response) {
101
+ if (isRemoved(response))
102
+ return formatRemovedAppUsage(response);
84
103
  if (response.mode === "company-cluster") {
85
104
  return [
86
105
  `App ${response.appId} uses the shared company database, so these are the company numbers.`,
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
+ import { HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER } from '../anywhere-runtime-flag-key.js';
4
5
  const marker = '# hq-anywhere provider sign-in wrapper';
5
6
  const PROVIDER_PATH_RC_FILES = ['.bashrc', '.bash_profile', '.zshrc', '.zprofile', '.profile'];
6
7
  const wrapperHeader = (provider) => `#!/bin/sh\n${marker}: ${provider}\n`;
@@ -76,6 +77,18 @@ function wrapperSource(provider) {
76
77
  ` ${provider === 'claude' ? '/login|auth' : 'login'}) exec "$real" "$@" ;;`,
77
78
  'esac',
78
79
  '',
80
+ '# The local marker mirrors registryDir: trim the override and use ~/.hq when empty.',
81
+ 'registry_dir=${HQ_REGISTRY_DIR:-}',
82
+ "registry_dir=$(printf '%s' \"$registry_dir\" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')",
83
+ 'if [ -z "$registry_dir" ]; then',
84
+ ' registry_dir=${HOME:-}',
85
+ ' [ -n "$registry_dir" ] && registry_dir="$registry_dir/.hq"',
86
+ 'fi',
87
+ 'if [ -n "$registry_dir" ]; then',
88
+ ' case "$registry_dir" in /*) ;; *) registry_dir="$PWD/$registry_dir" ;; esac',
89
+ ` if [ -e "$registry_dir/${HQ_ANYWHERE_RUNTIME_REVOCATION_MARKER}" ]; then exec "$real" "$@"; fi`,
90
+ 'fi',
91
+ '',
79
92
  'log_dir="$HOME/.hq/logs"',
80
93
  'mkdir -p "$log_dir" 2>/dev/null || :',
81
94
  'log_file="$log_dir/provider-auth.log"',
@@ -60,6 +60,13 @@ export interface LoopResult {
60
60
  session_id?: string;
61
61
  resumed?: boolean;
62
62
  fresh_fallback?: boolean;
63
+ question_id?: string;
64
+ decision?: {
65
+ question_id: string;
66
+ choice: string;
67
+ notes?: string;
68
+ at: string;
69
+ };
63
70
  }
64
71
  export interface Section8Handoff {
65
72
  schema: typeof PHASE_HANDOFF_SCHEMA;
@@ -103,6 +110,7 @@ export interface LoopRunnerOptions {
103
110
  envelopeId: string;
104
111
  attempt: number;
105
112
  error: string;
113
+ questionId: string;
106
114
  }): Promise<void> | void;
107
115
  defaultDeadlineSeconds?: number;
108
116
  now?: () => Date;
@@ -120,7 +128,6 @@ interface StoredEnvelope {
120
128
  envelope_id: string;
121
129
  queued_at: string;
122
130
  deadline_budget_seconds?: number;
123
- deadline_at_ms?: number;
124
131
  envelope: LoopEnvelope;
125
132
  }
126
133
  /** Validate both hq-cli phase envelopes and the conduct protocol §8 shape. */
@@ -148,6 +155,21 @@ export declare function interruptLoopQueue(runDir: string): {
148
155
  withdrawn: string[];
149
156
  already_picked_up: string[];
150
157
  };
158
+ export declare function interruptLoopQueueForStory(runDir: string, storyId: string, phase?: string): {
159
+ withdrawn: string[];
160
+ already_picked_up: string[];
161
+ };
162
+ export declare function applyLoopStallAnswer(input: {
163
+ runDir: string;
164
+ questionId: string;
165
+ choice: string;
166
+ notes?: string;
167
+ at: string;
168
+ }): {
169
+ lane_id: string;
170
+ envelope_id: string;
171
+ action: "requeued" | "parked";
172
+ } | undefined;
151
173
  export declare function loopStatus(runDir: string, laneId: string): LoopStatus;
152
174
  export declare function setLoopControllerPid(runDir: string, laneId: string, pid: number | undefined): void;
153
175
  export declare function clearLoopControllerPid(runDir: string, laneId: string, pid: number): void;