@indigoai-us/hq-cli 5.343.0 → 5.343.1

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,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.343.1] — 2026-10-04
6
+
7
+ - Include the bounded hq-cli version in `cli_feature_used` telemetry so feature-use rows can be grouped by release.
8
+
9
+ - Release the HQ CLI package-use lease before hq daemon call waits for its daemon response, after preloading the request protocol.
10
+ - Release the package-use lease after Outpost lane message and resume requests are submitted and their result-wait imports are preloaded.
11
+ - Preserve human, agent, and both audience tags when CLI tools and local bots post or reply in channels.
12
+ - Wait for a live installer to finish before retrying a failed CLI import when the install tree is still in flight.
13
+ - Preserve bounded master-hook slow-phase and per-phase timeout attribution in Sentry reports.
14
+
5
15
  ## [5.343.0] — 2026-10-04
6
16
  - `hq company --company <slug> settings set --work-registry-enabled <true|false>` and `--work-mesh-live-enabled <true|false>` let a company owner turn work tracking on or off. Work tracking needs both switches; after either is set the command says whether work tracking is on and prints the exact command for the switch that is still off. `hq company --company <slug> settings get` now also shows both switches for an owner or admin.
7
17
  - `hq mesh context requeue` now also re-sends, by default, sessions that were refused while work tracking was off. `hq doctor` and `hq mesh daemon doctor` explain those refusals in plain words.
@@ -3204,7 +3204,7 @@ export declare const COMMAND_CATALOG: readonly [{
3204
3204
  readonly hidden: false;
3205
3205
  }, {
3206
3206
  readonly flags: "--audience <human|agent|both>";
3207
- readonly description: "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both (1:1 DMs only)";
3207
+ readonly description: "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both";
3208
3208
  readonly hidden: false;
3209
3209
  }];
3210
3210
  readonly subcommands: readonly [];
@@ -3874,7 +3874,7 @@ export const COMMAND_CATALOG = [
3874
3874
  },
3875
3875
  {
3876
3876
  "flags": "--audience <human|agent|both>",
3877
- "description": "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both (1:1 DMs only)",
3877
+ "description": "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both",
3878
3878
  "hidden": false
3879
3879
  }
3880
3880
  ],
@@ -29,6 +29,7 @@ import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "..
29
29
  import { daemonPaths } from "../lib/daemon/paths.js";
30
30
  import { processCommandLine } from "../lib/daemon/lock.js";
31
31
  import { writeControlRequest } from "../lib/daemon/requests.js";
32
+ import { runDaemonCall } from "../lib/daemon/command-call.js";
32
33
  import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
33
34
  import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
34
35
  import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
@@ -857,17 +858,7 @@ export function registerDaemonCommand(program, over = {}) {
857
858
  const timeoutMs = Number(opts.timeout ?? (op === "policy.check" ? "40000" : "20000"));
858
859
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
859
860
  throw new Error("--timeout must be a positive number of milliseconds");
860
- const { hqdRequest, hqdSocketPath, HqdClientError } = await import("../lib/daemon/protocol.js");
861
- try {
862
- const result = await hqdRequest(op, args, { socketPath: hqdSocketPath(d.home, d.env), timeoutMs });
863
- d.out(JSON.stringify(result, null, 2));
864
- }
865
- catch (err) {
866
- if (!(err instanceof HqdClientError))
867
- throw err;
868
- d.out(JSON.stringify({ error: { code: err.code, message: err.message } }, null, 2));
869
- process.exitCode = 1;
870
- }
861
+ await runResidentCommandWithPackageLease("daemon-call", () => runDaemonCall(op, args, timeoutMs, d));
871
862
  }));
872
863
  daemon
873
864
  .command("status")
@@ -614,11 +614,20 @@ async function runChannelSend(channelName, message, opts) {
614
614
  // Send the human-readable form: the quotes only existed for parsing.
615
615
  if (mentions.length > 0)
616
616
  body = stripMentionQuotes(body);
617
+ let audience;
618
+ if (opts.audience !== undefined && opts.audience !== "") {
619
+ if (!DM_AUDIENCE_VALUES.includes(opts.audience)) {
620
+ throw new Error(`Invalid --audience '${opts.audience}': must be human, agent, or both.`);
621
+ }
622
+ audience = opts.audience;
623
+ }
617
624
  const payload = { body };
618
625
  if (mentions.length > 0)
619
626
  payload.mentions = mentions;
620
627
  if (rootEventId)
621
628
  payload.rootEventId = rootEventId;
629
+ if (audience)
630
+ payload.audience = audience;
622
631
  const sendRes = await vaultApiFetch({
623
632
  token,
624
633
  path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/messages`,
@@ -1327,7 +1336,7 @@ export function registerDmCommand(program) {
1327
1336
  .option("--details-file <path>", "Read the details from a file (1:1 DMs only)")
1328
1337
  .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
1329
1338
  .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
1330
- .option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both (1:1 DMs only)")
1339
+ .option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both")
1331
1340
  .action(async (recipient, message, opts) => {
1332
1341
  await runDmSend(recipient, message, opts);
1333
1342
  });
@@ -272,6 +272,7 @@ export interface Ctx {
272
272
  command: string;
273
273
  timeoutSeconds?: number;
274
274
  waitMs?: number;
275
+ beforeWait?: () => Promise<void>;
275
276
  }) => Promise<OutpostDispatchResult>;
276
277
  backfillOutpostIdentity: (expectedId: string) => "created" | "unchanged";
277
278
  fetchText: (url: string) => Promise<string>;
@@ -86,6 +86,7 @@ export interface LanesCommandDeps {
86
86
  command: string;
87
87
  timeoutSeconds?: number;
88
88
  waitMs?: number;
89
+ beforeWait?: () => Promise<void>;
89
90
  }) => Promise<OutpostDispatchResult>;
90
91
  /** Test seam for the atomic, non-overwriting marker repair. */
91
92
  backfillOutpostIdentity?: (expectedId: string) => "created" | "unchanged";
@@ -4317,11 +4317,22 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
4317
4317
  let dispatched;
4318
4318
  let grantContextRejected = false;
4319
4319
  let failedResumeOptionRejected = false;
4320
+ let outpostWaitLeasePreparationStarted = false;
4320
4321
  try {
4321
4322
  const dispatchCommand = (contexts) => ctx.dispatchOutpostExec({
4322
4323
  outpostId,
4323
4324
  timeoutSeconds: Math.ceil(waitMs / 1000),
4324
4325
  waitMs,
4326
+ ...(action === "message" || action === "resume"
4327
+ ? {
4328
+ beforeWait: async () => {
4329
+ if (outpostWaitLeasePreparationStarted)
4330
+ return;
4331
+ outpostWaitLeasePreparationStarted = true;
4332
+ await runResidentCommandWithPackageLease("outpost-wait", async () => undefined);
4333
+ },
4334
+ }
4335
+ : {}),
4325
4336
  command: buildOutpostLaneCommand({
4326
4337
  action: action === "grant"
4327
4338
  ? "grant-target"
@@ -237,7 +237,7 @@ export function buildAgentMcpTools(clients) {
237
237
  const token = await clients.getToken();
238
238
  const res = await clients.apiJson(token, `/v1/notify/channels/${encodeURIComponent(to)}/messages`, {
239
239
  method: "POST",
240
- body: { body: message },
240
+ body: { body: message, ...(audience ? { audience } : {}) },
241
241
  });
242
242
  if (res.status < 200 || res.status >= 300) {
243
243
  return errorResult(`Channel send failed (${res.status}): ${describeError(res.body)}`);
@@ -239,6 +239,7 @@ export declare class BotApi {
239
239
  channelId: string;
240
240
  body: string;
241
241
  rootEventId?: string;
242
+ audience?: "human" | "agent" | "both";
242
243
  }): Promise<{
243
244
  eventId?: string;
244
245
  createdAt?: string;
@@ -195,6 +195,8 @@ export class BotApi {
195
195
  const payload = { body: input.body };
196
196
  if (input.rootEventId)
197
197
  payload.rootEventId = input.rootEventId;
198
+ if (input.audience)
199
+ payload.audience = input.audience;
198
200
  const { body } = await this.call(`/v1/notify/channels/${encodeURIComponent(input.channelId)}/messages`, { method: "POST", body: payload });
199
201
  return body;
200
202
  }
@@ -28,13 +28,10 @@ export declare const PROGRESS_MIN_GAP_MS = 4000;
28
28
  export declare const WORKING_NOTICE_MS = 60000;
29
29
  export declare const WORKING_NOTICE_TEXT = "Working on it \u2014 I'll reply here when it's done.";
30
30
  export interface ProgressPosterOptions {
31
- /** Post one message; resolve true when it was delivered. */
32
- send: (body: string) => Promise<boolean>;
33
- /**
34
- * If provided, the final reply is sent through this instead of `send`.
35
- * Use to tag the final reply differently from in-between progress messages.
36
- */
37
- sendFinal?: (body: string) => Promise<boolean>;
31
+ /** Post a live progress beat while the turn is still running. */
32
+ sendProgress: (body: string) => Promise<boolean>;
33
+ /** Post terminal content that the person needs to read. */
34
+ sendToPerson: (body: string) => Promise<boolean>;
38
35
  now?: () => number;
39
36
  setTimer?: (fn: () => void, ms: number) => unknown;
40
37
  clearTimer?: (handle: unknown) => void;
@@ -63,8 +60,8 @@ export declare function normalizeForCompare(text: string): string;
63
60
  /** Matches {"action":"no_reply"} anywhere in a model reply, with or without preamble text. */
64
61
  export declare function containsNoReplySentinel(text: string): boolean;
65
62
  export declare class ProgressPoster {
66
- private readonly send;
67
- private readonly sendFinalFn?;
63
+ private readonly sendProgress;
64
+ private readonly sendToPerson;
68
65
  private readonly now;
69
66
  private readonly setTimer;
70
67
  private readonly clearTimer;
@@ -95,6 +92,8 @@ export declare class ProgressPoster {
95
92
  * deliveries posted the placeholder right after a real message.
96
93
  */
97
94
  private queued;
95
+ /** Posts queued for the person, excluding agent-lane status beats. */
96
+ private personQueued;
98
97
  constructor(opts: ProgressPosterOptions);
99
98
  /** Arm the one-time "working on it" note. Call when the turn starts. */
100
99
  start(): void;
@@ -109,11 +108,9 @@ export declare class ProgressPoster {
109
108
  /** True once any message from the model was said this turn (not counting the notice). */
110
109
  get saidAnything(): boolean;
111
110
  /**
112
- * The turn succeeded: post whatever is pending, then the final answer unless
113
- * it repeats what was already said. Resolves once everything is sent.
114
- *
115
- * When `sendFinal` was provided, progress items flush through `send` and the
116
- * final reply goes through `sendFinal` — keeping their audience tags distinct.
111
+ * The turn succeeded: post pending human-facing text, then a human-lane
112
+ * terminal message when the final adds something. Agent-lane status beats
113
+ * alone still receive a human-lane fallback before the turn closes.
117
114
  */
118
115
  finish(finalText: string): Promise<void>;
119
116
  /**
@@ -125,6 +122,7 @@ export declare class ProgressPoster {
125
122
  private scheduleFlush;
126
123
  private flushNow;
127
124
  private enqueue;
125
+ private enqueueToPerson;
128
126
  private enqueueWith;
129
127
  private cancelFlush;
130
128
  private cancelNotice;
@@ -36,8 +36,8 @@ export function containsNoReplySentinel(text) {
36
36
  return /\{\s*"action"\s*:\s*"no_reply"\s*\}/.test(text);
37
37
  }
38
38
  export class ProgressPoster {
39
- send;
40
- sendFinalFn;
39
+ sendProgress;
40
+ sendToPerson;
41
41
  now;
42
42
  setTimer;
43
43
  clearTimer;
@@ -68,9 +68,11 @@ export class ProgressPoster {
68
68
  * deliveries posted the placeholder right after a real message.
69
69
  */
70
70
  queued = 0;
71
+ /** Posts queued for the person, excluding agent-lane status beats. */
72
+ personQueued = 0;
71
73
  constructor(opts) {
72
- this.send = opts.send;
73
- this.sendFinalFn = opts.sendFinal;
74
+ this.sendProgress = opts.sendProgress;
75
+ this.sendToPerson = opts.sendToPerson;
74
76
  this.now = opts.now ?? Date.now;
75
77
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
76
78
  this.clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -92,7 +94,7 @@ export class ProgressPoster {
92
94
  if (this.finished || this.said.length > 0 || this.noticeSent)
93
95
  return;
94
96
  this.noticeSent = true;
95
- this.enqueue(this.workingNoticeText);
97
+ this.enqueueWith(this.workingNoticeText, this.sendProgress);
96
98
  }, this.workingNoticeMs);
97
99
  }
98
100
  /** One finished assistant message from the model. */
@@ -147,11 +149,9 @@ export class ProgressPoster {
147
149
  return this.said.length > 0;
148
150
  }
149
151
  /**
150
- * The turn succeeded: post whatever is pending, then the final answer unless
151
- * it repeats what was already said. Resolves once everything is sent.
152
- *
153
- * When `sendFinal` was provided, progress items flush through `send` and the
154
- * final reply goes through `sendFinal` — keeping their audience tags distinct.
152
+ * The turn succeeded: post pending human-facing text, then a human-lane
153
+ * terminal message when the final adds something. Agent-lane status beats
154
+ * alone still receive a human-lane fallback before the turn closes.
155
155
  */
156
156
  async finish(finalText) {
157
157
  if (this.finished)
@@ -171,42 +171,25 @@ export class ProgressPoster {
171
171
  // Grok's final is every message concatenated, so it repeats and is skipped.
172
172
  // A silent turn (run.ts decides via sentinel/empty) never reaches here.
173
173
  if (this.buffer) {
174
+ const terminalParts = this.said.filter((s) => !containsNoReplySentinel(s));
174
175
  this.pending = [];
175
176
  this.heldQuestions = [];
176
- for (const s of this.said) {
177
- if (!containsNoReplySentinel(s))
178
- this.pending.push(s);
179
- }
180
177
  if (final && !containsNoReplySentinel(final) && !this.repeatsSaid(final)) {
181
- if (this.sendFinalFn) {
182
- // Keep the audience split: collected messages go through send, the
183
- // final reply through sendFinal.
184
- this.flushNow();
185
- this.enqueueWith(final, this.sendFinalFn);
186
- return this.chain;
187
- }
188
- this.pending.push(final);
178
+ terminalParts.push(final);
189
179
  }
190
- this.flushNow();
180
+ this.enqueueToPerson(terminalParts.join("\n\n") || "(no reply)");
191
181
  return this.chain;
192
182
  }
193
183
  const held = this.heldQuestions;
194
184
  this.heldQuestions = [];
195
- const shouldAddFinal = Boolean(final && !this.repeatsSaid(final));
196
- if (!shouldAddFinal && held.length > 0)
197
- this.pending.push(...held);
198
- if (this.pending.length === 0 && this.queued === 0 && !shouldAddFinal)
199
- this.pending.push("(no reply)");
200
- if (this.sendFinalFn && shouldAddFinal) {
201
- // Flush any remaining progress messages first, then the final reply separately.
202
- this.flushNow();
203
- this.enqueueWith(final, this.sendFinalFn);
204
- }
205
- else {
206
- if (shouldAddFinal)
207
- this.pending.push(final);
208
- this.flushNow();
209
- }
185
+ this.flushNow();
186
+ const shouldAddFinal = Boolean(final && !containsNoReplySentinel(final) && !this.repeatsSaid(final));
187
+ if (shouldAddFinal)
188
+ this.enqueueToPerson(final);
189
+ else if (held.length > 0)
190
+ this.enqueueToPerson(held.join("\n\n"));
191
+ else if (this.personQueued === 0)
192
+ this.enqueueToPerson("(no reply)");
210
193
  return this.chain;
211
194
  }
212
195
  /**
@@ -226,9 +209,11 @@ export class ProgressPoster {
226
209
  this.heldQuestions = [];
227
210
  return this.chain;
228
211
  }
229
- this.pending.push(...this.heldQuestions);
212
+ const held = this.heldQuestions;
230
213
  this.heldQuestions = [];
231
214
  this.flushNow();
215
+ if (held.length > 0)
216
+ this.enqueueToPerson(held.join("\n\n"));
232
217
  return this.chain;
233
218
  }
234
219
  repeatsSaid(final) {
@@ -256,7 +241,11 @@ export class ProgressPoster {
256
241
  this.enqueue(body);
257
242
  }
258
243
  enqueue(body) {
259
- this.enqueueWith(body, this.send);
244
+ this.enqueueToPerson(body);
245
+ }
246
+ enqueueToPerson(body) {
247
+ this.personQueued += 1;
248
+ this.enqueueWith(body, this.sendToPerson);
260
249
  }
261
250
  enqueueWith(body, sendFn) {
262
251
  // Defence in depth: never let the silence sentinel reach the chat, whatever
@@ -766,8 +766,8 @@ export async function runBot(deps) {
766
766
  ...deps.progress,
767
767
  ...(args.workingNotice === false || silenceable || isSetupBot ? { workingNoticeMs: 0 } : {}),
768
768
  ...(silenceable || args.suppressOutput ? { buffer: true } : {}),
769
- send: makeSendFn(args.send),
770
- ...(args.sendFinal ? { sendFinal: makeSendFn(args.sendFinal) } : {}),
769
+ sendProgress: makeSendFn(args.sendProgress),
770
+ sendToPerson: makeSendFn(args.sendToPerson),
771
771
  });
772
772
  const controller = new AbortController();
773
773
  currentTurns.add(controller);
@@ -827,7 +827,7 @@ export async function runBot(deps) {
827
827
  const root = rootEventId ? { rootEventId } : {};
828
828
  return target.kind === "dm"
829
829
  ? api.sendDm({ toPersonUid: target.peerUid, body, ...root, ...(audience ? { audience } : {}) })
830
- : api.sendChannelMessage({ channelId: target.channelId, body, ...root });
830
+ : api.sendChannelMessage({ channelId: target.channelId, body, ...root, ...(audience ? { audience } : {}) });
831
831
  };
832
832
  /**
833
833
  * Post `parts` in order, starting at `from`. A failure throws a
@@ -863,15 +863,15 @@ export async function runBot(deps) {
863
863
  throw err;
864
864
  }
865
865
  };
866
- const send = makeSend(target.kind === "dm" ? "agent" : undefined);
867
- const sendFinal = makeSend(target.kind === "dm" ? "human" : undefined);
866
+ const sendProgress = makeSend("agent");
867
+ const sendToPerson = makeSend("human");
868
868
  const saved = (reply) => {
869
869
  const parts = splitBody(reply);
870
870
  const partial = last.value;
871
871
  const sameBody = partial !== null && partial.parts.length === parts.length && partial.parts.every((p, i) => p === parts[i]);
872
872
  return { reply, replyParts: parts, replyPartsDelivered: sameBody ? partial.delivered : 0 };
873
873
  };
874
- return { send, sendFinal, saved };
874
+ return { sendProgress, sendToPerson, saved };
875
875
  };
876
876
  // ── Scheduled jobs ────────────────────────────────────────────────────────
877
877
  /** Newest pending run key already picked up, per jobId (one catch-up per wake). */
@@ -1113,14 +1113,14 @@ export async function runBot(deps) {
1113
1113
  }
1114
1114
  };
1115
1115
  const target = { kind: "dm", peerUid };
1116
- const { send, sendFinal, saved } = trackingSend(target);
1116
+ const { sendProgress, sendToPerson, saved } = trackingSend(target);
1117
1117
  const { outcome, poster } = await runTurnWithProgress({
1118
1118
  marker: { messageId: KICKOFF_MESSAGE_ID, target, ...(thinking && introEventId ? { reactionEventId: introEventId } : {}) },
1119
1119
  prompt,
1120
1120
  sessionScope: "dm",
1121
1121
  tReceived,
1122
- send,
1123
- sendFinal,
1122
+ sendProgress,
1123
+ sendToPerson,
1124
1124
  // The intro already said the bot is starting, and the app shows it thinking.
1125
1125
  workingNotice: false,
1126
1126
  });
@@ -1146,7 +1146,7 @@ export async function runBot(deps) {
1146
1146
  clearInflight(dir, KICKOFF_MESSAGE_ID);
1147
1147
  patchBotConfig(dir, { kickoffPendingSignIn: true });
1148
1148
  markSignInBroken(outcome.excerpt);
1149
- await tellSignInNeeded("dm", send, true);
1149
+ await tellSignInNeeded("dm", sendToPerson, true);
1150
1150
  return;
1151
1151
  }
1152
1152
  if (readBotConfig(dir)?.kickoffPendingSignIn)
@@ -1158,7 +1158,7 @@ export async function runBot(deps) {
1158
1158
  ? `${didNotFinishText(failureReason(outcome), poster.postedCount > 0)} Send me any message and I'll pick up from there.`
1159
1159
  : `Sorry — I couldn't get started on my own (${runtime.id} failed: ${outcome.excerpt.slice(0, 160)}). Send me any message and I'll pick up from there, or check that ${runtime.id} is signed in on this computer.`;
1160
1160
  try {
1161
- await send(body);
1161
+ await sendToPerson(body);
1162
1162
  clearInflight(dir, KICKOFF_MESSAGE_ID);
1163
1163
  }
1164
1164
  catch {
@@ -1235,8 +1235,8 @@ export async function runBot(deps) {
1235
1235
  throw err;
1236
1236
  }
1237
1237
  };
1238
- const send = makeSend(tracking.send);
1239
- const sendFinal = makeSend(tracking.sendFinal);
1238
+ const sendProgress = makeSend(tracking.sendProgress);
1239
+ const sendToPerson = makeSend(tracking.sendToPerson);
1240
1240
  const normalize = (text) => text.replace(/\s+/g, " ").trim();
1241
1241
  /**
1242
1242
  * True only when the final report itself reached the chat in full. With no
@@ -1272,8 +1272,8 @@ export async function runBot(deps) {
1272
1272
  prompt: buildJobTurnPrompt(job, { now: now(), timeZone }),
1273
1273
  sessionScope: target.sessionScope,
1274
1274
  tReceived,
1275
- send,
1276
- sendFinal,
1275
+ sendProgress,
1276
+ sendToPerson,
1277
1277
  // A scheduled run has nobody waiting in a chat: no "working on it" filler.
1278
1278
  workingNotice: false,
1279
1279
  ...(quietJob ? { suppressOutput: true } : {}),
@@ -1307,7 +1307,7 @@ export async function runBot(deps) {
1307
1307
  clearInflight(dir, id);
1308
1308
  attempts.delete(id);
1309
1309
  markSignInBroken(outcome.excerpt);
1310
- await tellSignInNeeded(`job:${jobId}`, (body) => sendToTarget(markerTarget, body));
1310
+ await tellSignInNeeded(`job:${jobId}`, (body) => sendToTarget(markerTarget, body, undefined, "human"));
1311
1311
  log("warn", `kept scheduled run ${id} of job ${jobId} until ${runtime.id} can sign in again`);
1312
1312
  return; // un-acked, un-reported: run once the sign-in works
1313
1313
  }
@@ -1320,7 +1320,7 @@ export async function runBot(deps) {
1320
1320
  return;
1321
1321
  }
1322
1322
  if (attempt >= MAX_ATTEMPTS_PER_MESSAGE) {
1323
- await send(`I couldn't run this one — ${runtime.id} kept failing: ${excerpt.slice(0, 160)}.`).catch(() => undefined);
1323
+ await sendToPerson(`I couldn't run this one — ${runtime.id} kept failing: ${excerpt.slice(0, 160)}.`).catch(() => undefined);
1324
1324
  await completeJobRun(jobId, id, { status: "failed", output: jobRunOutput(excerpt), disposition: `${runtime.id} could not be started` });
1325
1325
  markReported("failed");
1326
1326
  markProcessed(dir, id, now().toISOString());
@@ -1339,7 +1339,7 @@ export async function runBot(deps) {
1339
1339
  // reports and says this real reason, not "the bot was restarted".
1340
1340
  patchInflight(dir, id, { jobFailure: { reason, ...(outcome.excerpt ? { excerpt: outcome.excerpt } : {}) } });
1341
1341
  try {
1342
- await send(didNotFinishText(reason, poster.postedCount > 0));
1342
+ await sendToPerson(didNotFinishText(reason, poster.postedCount > 0));
1343
1343
  }
1344
1344
  catch (err) {
1345
1345
  // Leave it un-acked with the marker: the next poll reports it.
@@ -1534,7 +1534,7 @@ export async function runBot(deps) {
1534
1534
  const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? ownerText :
1535
1535
  `[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
1536
1536
  const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
1537
- const { send, sendFinal, saved } = trackingSend(markerTarget, item.rootEventId);
1537
+ const { sendProgress, sendToPerson, saved } = trackingSend(markerTarget, item.rootEventId);
1538
1538
  const { outcome, poster } = await runTurnWithProgress({
1539
1539
  marker: {
1540
1540
  messageId: id,
@@ -1545,8 +1545,8 @@ export async function runBot(deps) {
1545
1545
  prompt,
1546
1546
  sessionScope,
1547
1547
  tReceived,
1548
- send,
1549
- sendFinal,
1548
+ sendProgress,
1549
+ sendToPerson,
1550
1550
  silenceable: target.kind === "room" && !target.mentioned,
1551
1551
  ...(readDirs.length ? { readDirs } : {}),
1552
1552
  ...(target.kind === "room"
@@ -1605,7 +1605,7 @@ export async function runBot(deps) {
1605
1605
  attempts.delete(id);
1606
1606
  markSignInBroken(outcome.excerpt);
1607
1607
  const conversation = markerTarget.kind === "dm" ? sessionScope : `room:${markerTarget.channelId}:${item.rootEventId ?? ""}`;
1608
- await tellSignInNeeded(conversation, send);
1608
+ await tellSignInNeeded(conversation, sendToPerson);
1609
1609
  log("warn", `kept ${id} until ${runtime.id} can sign in again`);
1610
1610
  return; // un-acked: answered once the sign-in works
1611
1611
  }
@@ -1620,7 +1620,7 @@ export async function runBot(deps) {
1620
1620
  }
1621
1621
  if (attempt >= MAX_ATTEMPTS_PER_MESSAGE) {
1622
1622
  try {
1623
- await send(`Sorry — I couldn't answer that one (${runtime.id} kept failing: ${excerpt.slice(0, 160)}). Try again in a bit, or check that ${runtime.id} is signed in on this computer.`);
1623
+ await sendToPerson(`Sorry — I couldn't answer that one (${runtime.id} kept failing: ${excerpt.slice(0, 160)}). Try again in a bit, or check that ${runtime.id} is signed in on this computer.`);
1624
1624
  }
1625
1625
  catch {
1626
1626
  /* best-effort */
@@ -1660,7 +1660,7 @@ export async function runBot(deps) {
1660
1660
  // still say we did not finish — but with a plain reason, never the raw
1661
1661
  // internal error string.
1662
1662
  const reason = reasoningOnly ? "the model returned no answer" : failureReason(outcome);
1663
- await send(didNotFinishText(reason, poster.postedCount > 0));
1663
+ await sendToPerson(didNotFinishText(reason, poster.postedCount > 0));
1664
1664
  }
1665
1665
  catch (err) {
1666
1666
  // Leave it un-acked with the marker: the next poll says it did not finish.
@@ -26,7 +26,7 @@ export declare const MAX_SENTRY_REPORT_METADATA_VALUE_BYTES = 256;
26
26
  * is owner-approved telemetry for the exact hook script and may include a
27
27
  * company directory; it is never a substitute for hook input or a transcript.
28
28
  */
29
- export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "spawn_ms", "slow_child", "slow_child_ms", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "shell", "nproc", "hook_sequence", "hook_timeout_debug_context"];
29
+ export declare const ALLOWED_SENTRY_REPORT_METADATA_KEYS: readonly ["hook_name", "hook_path", "hook_event", "hook_script", "tool_name", "session_id", "declared_timeout_ms", "elapsed_ms", "remaining_ms", "watchdog_timeout_ms", "hook_timeout_ms", "hq_version", "platform", "load_average", "spawn_ms", "slow_child", "slow_child_ms", "slow_phase", "exit_code", "final_elapsed_ms", "bash_env_set", "cwd_kind", "shell", "nproc", "hook_sequence", "hook_timeout_debug_context"];
30
30
  export declare const MAX_SENTRY_REPORT_METADATA_KEYS: number;
31
31
  export type HookSequenceEntry = {
32
32
  script: string;
@@ -130,18 +130,25 @@ const DEBUG_CONTEXT_HOOK_EVENTS = new Set([
130
130
  "TaskCompleted",
131
131
  "TeammateIdle",
132
132
  ]);
133
- const DEBUG_CONTEXT_PHASES = new Set([
133
+ const SAFE_SLOW_PHASES = new Set([
134
134
  "startup",
135
135
  "source",
136
136
  "config_load",
137
137
  "policy_load",
138
138
  "external_command",
139
139
  "output_write",
140
+ "output_scan",
141
+ "output_merge",
142
+ "output_stdout",
143
+ "output_abandoned_scan",
144
+ "output_abandoned_merge",
145
+ "output_abandoned_stdout",
140
146
  "wait",
141
147
  "child_wait",
142
148
  "probe",
143
149
  "parse",
144
150
  ]);
151
+ const DEBUG_CONTEXT_PHASES = new Set(SAFE_SLOW_PHASES);
145
152
  /**
146
153
  * The only hook diagnostics that can be attached as metadata. The list omits
147
154
  * command text, prompts, payloads, company names, and repositories. `hook_path`
@@ -166,6 +173,7 @@ export const ALLOWED_SENTRY_REPORT_METADATA_KEYS = [
166
173
  "spawn_ms",
167
174
  "slow_child",
168
175
  "slow_child_ms",
176
+ "slow_phase",
169
177
  "exit_code",
170
178
  "final_elapsed_ms",
171
179
  "bash_env_set",
@@ -255,6 +263,11 @@ function boundedDebugName(value, known) {
255
263
  function boundedDebugPhase(value) {
256
264
  return typeof value === "string" && DEBUG_CONTEXT_PHASES.has(value) ? value : "other";
257
265
  }
266
+ function safeSlowPhase(value) {
267
+ if (typeof value !== "string")
268
+ return "unknown";
269
+ return SAFE_SLOW_PHASES.has(value) ? value : "other";
270
+ }
258
271
  function boundedDebugHookEvent(value) {
259
272
  return typeof value === "string" && DEBUG_CONTEXT_HOOK_EVENTS.has(value) ? value : "other";
260
273
  }
@@ -425,6 +438,9 @@ function parseSentryReportEvent(stdin) {
425
438
  if (typeof metadata.slow_child === "string") {
426
439
  metadata.slow_child = safeSlowChild(metadata);
427
440
  }
441
+ if ("slow_phase" in metadata) {
442
+ metadata.slow_phase = safeSlowPhase(metadata.slow_phase);
443
+ }
428
444
  return {
429
445
  code: 0,
430
446
  event: {
@@ -678,6 +694,7 @@ function hookTimeoutTags(event) {
678
694
  load_bucket: loadAverage === "unavailable" ? "unavailable" : loadBucket(loadAverage),
679
695
  spawn_ms_bucket: spawnMsBucket(metadata),
680
696
  slow_child: safeSlowChild(metadata),
697
+ slow_phase: safeSlowPhase(metadata.slow_phase),
681
698
  slow_child_ms_bucket: typeof slowChildMs === "number" && Number.isFinite(slowChildMs)
682
699
  ? elapsedBucket(slowChildMs)
683
700
  : "unknown",
@@ -0,0 +1,9 @@
1
+ interface DaemonCallDependencies {
2
+ home: string;
3
+ env: NodeJS.ProcessEnv;
4
+ out: (line: string) => void;
5
+ }
6
+ /** Send one CLI request after its protocol module has been preloaded. */
7
+ export declare function runDaemonCall(op: string, args: Record<string, unknown>, timeoutMs: number, deps: DaemonCallDependencies): Promise<void>;
8
+ export {};
9
+ //# sourceMappingURL=command-call.d.ts.map
@@ -0,0 +1,18 @@
1
+ /** Send one CLI request after its protocol module has been preloaded. */
2
+ export async function runDaemonCall(op, args, timeoutMs, deps) {
3
+ const { hqdRequest, hqdSocketPath, HqdClientError } = await import("./protocol.js");
4
+ try {
5
+ const result = await hqdRequest(op, args, {
6
+ socketPath: hqdSocketPath(deps.home, deps.env),
7
+ timeoutMs,
8
+ });
9
+ deps.out(JSON.stringify(result, null, 2));
10
+ }
11
+ catch (error) {
12
+ if (!(error instanceof HqdClientError))
13
+ throw error;
14
+ deps.out(JSON.stringify({ error: { code: error.code, message: error.message } }, null, 2));
15
+ process.exitCode = 1;
16
+ }
17
+ }
18
+ //# sourceMappingURL=command-call.js.map
@@ -146,6 +146,7 @@ export declare function dispatchOutpostPickup(port: OutpostExecPort, input: {
146
146
  command: string;
147
147
  timeoutSeconds?: number;
148
148
  waitMs?: number;
149
+ beforeWait?: () => Promise<void>;
149
150
  }): Promise<OutpostDispatchResult>;
150
151
  export declare function dispatchErrorMessage(err: unknown): string;
151
152
  /** Factory matching `hq outposts exec`'s client wiring. */
@@ -252,6 +252,7 @@ export async function stageOutpostPayload(port, outpostId, bytes, put = fetch) {
252
252
  }
253
253
  export async function dispatchOutpostPickup(port, input) {
254
254
  const submitted = await port.submitExec(input.command, input.outpostId, input.timeoutSeconds ?? PICKUP_EXEC_TIMEOUT_SECONDS);
255
+ await input.beforeWait?.();
255
256
  const ac = new AbortController();
256
257
  const waitMs = input.waitMs ?? PICKUP_WAIT_MS;
257
258
  const timer = setTimeout(() => ac.abort(), waitMs);
@@ -186,7 +186,11 @@ export async function emitCliFeatureUsed(usage, timeoutMs = TELEMETRY_TIMEOUT_MS
186
186
  consentBasis,
187
187
  schemaVersion: 1,
188
188
  ...(companyUid ? { companyUid } : {}),
189
- properties: { kind: usage.kind, outcome: usage.outcome },
189
+ properties: {
190
+ kind: usage.kind,
191
+ outcome: usage.outcome,
192
+ version: CLI_VERSION.slice(0, 40),
193
+ },
190
194
  },
191
195
  ],
192
196
  },
@@ -278,7 +278,8 @@ type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "v
278
278
  /**
279
279
  * Poll until the install tree has been continuously READY for `quietMs`, or the
280
280
  * wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
281
- * pid, (ii) the missing target is present, and — when `packageRoot` is known —
281
+ * pid and no live recorded updater while the tree is in flight, (ii) the missing
282
+ * target is present, and — when `packageRoot` is known —
282
283
  * (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and (iv) no
283
284
  * `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
284
285
  * its condition "not ready" rather than throwing.
@@ -295,20 +296,22 @@ type VersionGatePhase = "preflight" | "lock_acquired" | "installer_running" | "v
295
296
  * any installer before attempting a repair. Any observed signature change
296
297
  * keeps the ordinary settle deadline in force. `deadlineMs === 0` evaluates
297
298
  * readiness once.
298
- * - LOCKED time — wall time observed under a fresh foreign lock held by a live
299
- * pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
300
- * 10-minute staleness window) and does NOT consume the settle budget. When
301
- * that ceiling is reached with the lock still held, the result is unsettled
302
- * with `lockHeldAtEnd` true so the seam can report a distinct `lock-held`
303
- * outcome and remedy rather than telling the user to reinstall over a live
304
- * writer.
305
- * Each poll's REAL elapsed interval (now() deltas) is attributed to the locked or
306
- * unlocked bucket by whether a fresh foreign lock was held when the interval
307
- * began, so a starved event loop only makes the wait coarser, never unbounded. A
299
+ * - WRITER time — wall time observed under a fresh foreign lock, or while a
300
+ * recorded foreign updater pid is alive and the tree has an in-flight signal
301
+ * — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
302
+ * 10-minute staleness window) and does NOT consume the settle budget. This
303
+ * also covers a still-running updater whose lock became stale by age. When
304
+ * that ceiling is reached, the result is unsettled; `lockHeldAtEnd` remains
305
+ * reserved for a fresh lock held at the end.
306
+ * Each poll's REAL elapsed interval (now() deltas) is attributed to the writer or
307
+ * unlocked bucket by whether a fresh lock or qualifying live updater was observed
308
+ * when the interval began, so a starved event loop only makes the wait coarser,
309
+ * never unbounded. A
308
310
  * stale lock (dead pid, older than the staleness window, unparseable) or our own
309
- * pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
311
+ * pid never extends anything by itself. The whole wait is bounded (≤ `deadlineMs +
310
312
  * lockWaitMs`), and the unchanged-tree path normally returns after the much
311
- * shorter `staleProbeMs` observation window.
313
+ * shorter `staleProbeMs` observation window unless a live updater pid is still
314
+ * present with an in-flight tree signal.
312
315
  */
313
316
  export declare function waitForInstallTreeSettled(args: WaitForInstallTreeSettledArgs): Promise<WaitForInstallTreeSettledResult>;
314
317
  export interface InstallRewriteSignalArgs {
@@ -714,6 +714,28 @@ function updaterPidAlive(lockPath, fs, isPidAlive) {
714
714
  return "unknown";
715
715
  }
716
716
  }
717
+ /** Whether the lock names a live process other than this CLI invocation. */
718
+ function liveForeignUpdaterPid(lockPath, fs, isPidAlive) {
719
+ try {
720
+ const lock = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
721
+ if (typeof lock.startedAt !== "string" ||
722
+ Number.isNaN(Date.parse(lock.startedAt)) ||
723
+ typeof lock.tool !== "string" ||
724
+ lock.tool.length === 0 ||
725
+ typeof lock.version !== "string" ||
726
+ lock.version.length === 0) {
727
+ return false;
728
+ }
729
+ return typeof lock.pid === "number" &&
730
+ Number.isSafeInteger(lock.pid) &&
731
+ lock.pid > 0 &&
732
+ lock.pid !== process.pid &&
733
+ isPidAlive(lock.pid);
734
+ }
735
+ catch {
736
+ return false;
737
+ }
738
+ }
717
739
  /** Snapshot retired sibling trees so changes below their root stay observable. */
718
740
  function retiredSiblingActivitySignature(packageRoot, fs) {
719
741
  const { leaf } = cliNameParts();
@@ -917,7 +939,8 @@ const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
917
939
  /**
918
940
  * Poll until the install tree has been continuously READY for `quietMs`, or the
919
941
  * wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
920
- * pid, (ii) the missing target is present, and — when `packageRoot` is known —
942
+ * pid and no live recorded updater while the tree is in flight, (ii) the missing
943
+ * target is present, and — when `packageRoot` is known —
921
944
  * (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and (iv) no
922
945
  * `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
923
946
  * its condition "not ready" rather than throwing.
@@ -934,20 +957,22 @@ const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
934
957
  * any installer before attempting a repair. Any observed signature change
935
958
  * keeps the ordinary settle deadline in force. `deadlineMs === 0` evaluates
936
959
  * readiness once.
937
- * - LOCKED time — wall time observed under a fresh foreign lock held by a live
938
- * pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
939
- * 10-minute staleness window) and does NOT consume the settle budget. When
940
- * that ceiling is reached with the lock still held, the result is unsettled
941
- * with `lockHeldAtEnd` true so the seam can report a distinct `lock-held`
942
- * outcome and remedy rather than telling the user to reinstall over a live
943
- * writer.
944
- * Each poll's REAL elapsed interval (now() deltas) is attributed to the locked or
945
- * unlocked bucket by whether a fresh foreign lock was held when the interval
946
- * began, so a starved event loop only makes the wait coarser, never unbounded. A
960
+ * - WRITER time — wall time observed under a fresh foreign lock, or while a
961
+ * recorded foreign updater pid is alive and the tree has an in-flight signal
962
+ * — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
963
+ * 10-minute staleness window) and does NOT consume the settle budget. This
964
+ * also covers a still-running updater whose lock became stale by age. When
965
+ * that ceiling is reached, the result is unsettled; `lockHeldAtEnd` remains
966
+ * reserved for a fresh lock held at the end.
967
+ * Each poll's REAL elapsed interval (now() deltas) is attributed to the writer or
968
+ * unlocked bucket by whether a fresh lock or qualifying live updater was observed
969
+ * when the interval began, so a starved event loop only makes the wait coarser,
970
+ * never unbounded. A
947
971
  * stale lock (dead pid, older than the staleness window, unparseable) or our own
948
- * pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
972
+ * pid never extends anything by itself. The whole wait is bounded (≤ `deadlineMs +
949
973
  * lockWaitMs`), and the unchanged-tree path normally returns after the much
950
- * shorter `staleProbeMs` observation window.
974
+ * shorter `staleProbeMs` observation window unless a live updater pid is still
975
+ * present with an in-flight tree signal.
951
976
  */
952
977
  export async function waitForInstallTreeSettled(args) {
953
978
  const now = args.now ?? Date.now;
@@ -967,17 +992,21 @@ export async function waitForInstallTreeSettled(args) {
967
992
  let sawLock = false;
968
993
  let sawRetired = false;
969
994
  let lockedMs = 0;
995
+ let writerMs = 0;
970
996
  let unlockedMs = 0;
971
997
  let lockTool = "";
972
998
  let prevT = start;
973
999
  let prevForeignLockHeld = false;
1000
+ let prevWriterHeld = false;
974
1001
  let lockExtendedFired = false;
975
1002
  for (;;) {
976
1003
  const t = now();
977
- // Attribute the interval that just elapsed [prevT, t) to the bucket for the
978
- // lock state that held when it BEGAN (real now() deltas, never nominal pollMs).
1004
+ // Attribute the interval that just elapsed [prevT, t) using the state that
1005
+ // held when it BEGAN (real now() deltas, never nominal pollMs).
979
1006
  if (prevForeignLockHeld)
980
1007
  lockedMs += t - prevT;
1008
+ if (prevWriterHeld)
1009
+ writerMs += t - prevT;
981
1010
  else
982
1011
  unlockedMs += t - prevT;
983
1012
  const lock = freshForeignLock(args.lockPath, t, isPidAlive, fs);
@@ -1015,7 +1044,10 @@ export async function waitForInstallTreeSettled(args) {
1015
1044
  retiredCheckFailed = true;
1016
1045
  }
1017
1046
  }
1018
- let ready = !foreignLockHeld && installTargetPresent(args.target, fs);
1047
+ const inFlightTree = sawLock || sawRetired || sawTreeChange;
1048
+ const updaterPidHeld = inFlightTree && liveForeignUpdaterPid(args.lockPath, fs, isPidAlive);
1049
+ const writerHeld = foreignLockHeld || updaterPidHeld;
1050
+ let ready = !writerHeld && installTargetPresent(args.target, fs);
1019
1051
  if (ready && args.packageRoot) {
1020
1052
  try {
1021
1053
  if (!packageRootHealthy(args.packageRoot, fs))
@@ -1055,6 +1087,7 @@ export async function waitForInstallTreeSettled(args) {
1055
1087
  }
1056
1088
  if (!ready &&
1057
1089
  !foreignLockHeld &&
1090
+ !updaterPidHeld &&
1058
1091
  !retiredCheckFailed &&
1059
1092
  !sawLock &&
1060
1093
  !sawTreeChange &&
@@ -1078,14 +1111,13 @@ export async function waitForInstallTreeSettled(args) {
1078
1111
  };
1079
1112
  }
1080
1113
  // Give up when the TREE has had a full unlocked budget with no writer visible,
1081
- // OR a writer is STILL holding the lock and has held it past its own staleness
1082
- // ceiling. The lock ceiling is gated on a lock being held RIGHT NOW so that a
1083
- // ceiling of 0 (HQ_INSTALL_SETTLE_LOCK_TIMEOUT_MS=0, "do not extend under a
1084
- // lock") disables only the extension and does not collapse the ordinary
1085
- // deadlineMs settle when no lock exists — `lockedMs` is 0 ≥ 0 on every poll.
1114
+ // OR a qualifying writer has exceeded the shared writer ceiling. The writer
1115
+ // ceiling only extends while a fresh lock or in-flight live PID is present, so
1116
+ // a ceiling of 0 disables only that extension and does not collapse the normal
1117
+ // deadlineMs settle when no writer exists.
1086
1118
  // `deadlineMs` 0 makes the unlocked test fire on the first poll (evaluate-once),
1087
1119
  // matching the pre-lock-aware contract; `lockHeldAtEnd` distinguishes the causes.
1088
- if (unlockedMs >= deadlineMs || (foreignLockHeld && lockedMs >= lockWaitMs)) {
1120
+ if (unlockedMs >= deadlineMs || (writerHeld && writerMs >= lockWaitMs)) {
1089
1121
  return {
1090
1122
  settled: false,
1091
1123
  waitedMs,
@@ -1102,15 +1134,15 @@ export async function waitForInstallTreeSettled(args) {
1102
1134
  updaterPidAlive: updaterPidAlive(args.lockPath, fs, isPidAlive),
1103
1135
  };
1104
1136
  }
1105
- // The first poll at which the wait has reached `deadlineMs` while a lock is
1106
- // still held is exactly where the pre-lock-aware wait gave up — announce the
1107
- // extension once, and only when we are genuinely going to keep waiting.
1108
- if (!lockExtendedFired && foreignLockHeld && waitedMs >= deadlineMs) {
1137
+ // Announce once when the deadline has passed and a qualifying writer is why
1138
+ // the bounded wait continues.
1139
+ if (!lockExtendedFired && writerHeld && waitedMs >= deadlineMs) {
1109
1140
  lockExtendedFired = true;
1110
1141
  args.onLockExtended?.();
1111
1142
  }
1112
1143
  prevT = t;
1113
1144
  prevForeignLockHeld = foreignLockHeld;
1145
+ prevWriterHeld = writerHeld;
1114
1146
  await sleep(pollMs);
1115
1147
  }
1116
1148
  }
@@ -1,4 +1,4 @@
1
- export type ResidentPackagePath = "agent-kit" | "agent-mcp" | "auth-login" | "agents-terminal" | "bot" | "bot-logs-follow" | "childWrapper" | "coreJobRun" | "login" | "mcp" | "daemon" | "daemon-logs-follow" | "mesh-daemon" | "lanes-watch" | "monitor-wait" | "monitor-run" | "agents-message-wait" | "integrations-oauth-wait" | "secrets-sandbox" | "pkg-update" | "pack-install";
1
+ export type ResidentPackagePath = "agent-kit" | "agent-mcp" | "auth-login" | "agents-terminal" | "bot" | "bot-logs-follow" | "childWrapper" | "coreJobRun" | "daemon-call" | "login" | "mcp" | "daemon" | "daemon-logs-follow" | "mesh-daemon" | "lanes-watch" | "monitor-wait" | "monitor-run" | "agents-message-wait" | "integrations-oauth-wait" | "outpost-wait" | "secrets-sandbox" | "pkg-update" | "pack-install";
2
2
  /** Runtime modules that resident commands may need after a wait starts. */
3
3
  export declare const RESIDENT_PACKAGE_MODULES: Record<ResidentPackagePath, readonly string[]>;
4
4
  export declare class ResidentPackagePreloadError extends Error {
@@ -43,6 +43,7 @@ export const RESIDENT_PACKAGE_MODULES = {
43
43
  mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js"],
44
44
  childWrapper: ["../main.js"],
45
45
  coreJobRun: ["../lib/flag-registry.js"],
46
+ "daemon-call": ["../lib/daemon/protocol.js"],
46
47
  daemon: [
47
48
  "../sentry.js",
48
49
  "../utils/cognito-session.js",
@@ -63,6 +64,7 @@ export const RESIDENT_PACKAGE_MODULES = {
63
64
  "monitor-run": ["../commands/lanes.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "../sentry.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
64
65
  "agents-message-wait": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
65
66
  "integrations-oauth-wait": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
67
+ "outpost-wait": ["../commands/lanes.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "../sentry.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
66
68
  "secrets-sandbox": [],
67
69
  "pkg-update": ["../sentry.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud"],
68
70
  "pack-install": [
@@ -175,6 +177,9 @@ const LOADERS = {
175
177
  coreJobRun: [
176
178
  () => import("../lib/flag-registry.js"),
177
179
  ],
180
+ "daemon-call": [
181
+ () => import("../lib/daemon/protocol.js"),
182
+ ],
178
183
  daemon: [
179
184
  async () => {
180
185
  const { preloadSentrySdk } = await import("../sentry.js");
@@ -265,6 +270,19 @@ const LOADERS = {
265
270
  () => import("../lib/first-sync-next-step.js"),
266
271
  () => import("@indigoai-us/hq-cloud"),
267
272
  ],
273
+ "outpost-wait": [
274
+ () => import("../commands/lanes.js"),
275
+ () => import("../lib/plan-limit-nag.js"),
276
+ () => import("../utils/plan-gate-error.js"),
277
+ async () => {
278
+ const { preloadSentrySdk } = await import("../sentry.js");
279
+ preloadSentrySdk();
280
+ },
281
+ () => import("open"),
282
+ () => import("../utils/cognito-session.js"),
283
+ () => import("../lib/first-sync-next-step.js"),
284
+ () => import("@indigoai-us/hq-cloud"),
285
+ ],
268
286
  "secrets-sandbox": [],
269
287
  "pkg-update": [
270
288
  async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.343.0",
3
+ "version": "5.343.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {