@indigoai-us/hq-cli 5.179.0 → 5.179.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,8 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.179.1] — 2026-09-24
6
+
7
+ ### Added
8
+
9
+ - `hq dm send` now takes `--audience <human|agent|both>` so a bot or operator
10
+ can declare who a DM is for, and the `hq_dm_send` bot tool accepts the same
11
+ optional field. Local bot turns tag in-between progress messages as `agent`
12
+ and the final reply as `human` by default, so status chatter stays out of a
13
+ person's inbox while the answer they need still reaches them. `hq dm inbox`
14
+ and `hq dm thread` mark agent-audience messages with an `[agent]` tag.
15
+
5
16
  ## [5.179.0] — 2026-09-24
6
17
 
18
+ ### Fixed
19
+
20
+ - `hq integrations connect --client-id` with a Google OAuth client (`*.apps.googleusercontent.com`) now warns that the client must be of type "Web application" and prints the redirect URI to register. A "Desktop app" client cannot accept HQ's hosted callback, and Google reports that only after the browser is open.
21
+
7
22
  ## [5.178.0] — 2026-09-24
8
23
 
9
24
  ### Fixed
@@ -20,7 +35,6 @@
20
35
  show include grant and alive-child counts. Terminal child resume, message, and
21
36
  account-move paths recheck occupancy and remaining grant allocation. The
22
37
  backfill defaults to dry-run.
23
-
24
38
  - When `lanes.hire-grants` is enabled, `hq lanes list` and `hq lanes show` use counts
25
39
  from the owning Outpost for active remote lanes. If the Outpost does not return
26
40
  counts, both commands show local counts and a warning.
@@ -2255,6 +2255,9 @@ export declare const COMMAND_CATALOG: readonly [{
2255
2255
  }, {
2256
2256
  readonly flags: "--in <duration>";
2257
2257
  readonly description: "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)";
2258
+ }, {
2259
+ readonly flags: "--audience <human|agent|both>";
2260
+ 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)";
2258
2261
  }];
2259
2262
  readonly subcommands: readonly [];
2260
2263
  }, {
@@ -2896,6 +2896,10 @@ export const COMMAND_CATALOG = [
2896
2896
  {
2897
2897
  "flags": "--in <duration>",
2898
2898
  "description": "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)"
2899
+ },
2900
+ {
2901
+ "flags": "--audience <human|agent|both>",
2902
+ "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)"
2899
2903
  }
2900
2904
  ],
2901
2905
  "subcommands": []
@@ -133,6 +133,7 @@ export declare function resolveMentions(tokens: string[], members: ChannelMember
133
133
  * Returns null on anything that doesn't match. Pure → unit-testable.
134
134
  */
135
135
  export declare function parseDuration(input: string): number | null;
136
+ export type DmAudience = "human" | "agent" | "both";
136
137
  export interface DmSendBody {
137
138
  toEmail?: string;
138
139
  toPersonUid?: string;
@@ -140,6 +141,7 @@ export interface DmSendBody {
140
141
  prompt?: string;
141
142
  details?: string;
142
143
  deliverAt?: string;
144
+ audience?: DmAudience;
143
145
  }
144
146
  /**
145
147
  * Build the POST /v1/notify/dm request body from CLI inputs. Pure (no I/O,
@@ -156,6 +158,7 @@ export declare function buildDmBody(args: {
156
158
  at?: string;
157
159
  inDelay?: string;
158
160
  now: number;
161
+ audience?: string;
159
162
  }): DmSendBody;
160
163
  /** A pending incoming connection request, as returned by the backend. */
161
164
  export interface ConnectionRequest {
@@ -204,6 +207,7 @@ export interface DmInboxEvent {
204
207
  details?: string;
205
208
  prompt?: string;
206
209
  acknowledgedAt?: string;
210
+ audience?: DmAudience;
207
211
  /** US-007 file references. Singular `attachment` is the legacy form. */
208
212
  attachments?: DmAttachment[];
209
213
  attachment?: DmAttachment;
@@ -219,6 +223,7 @@ export interface DmThreadMessage {
219
223
  direction: "in" | "out";
220
224
  details?: string;
221
225
  prompt?: string;
226
+ audience?: DmAudience;
222
227
  attachments?: DmAttachment[];
223
228
  attachment?: DmAttachment;
224
229
  }
@@ -276,6 +276,7 @@ export function parseDuration(input) {
276
276
  };
277
277
  return n * mult[m[2]];
278
278
  }
279
+ const DM_AUDIENCE_VALUES = ["human", "agent", "both"];
279
280
  /**
280
281
  * Build the POST /v1/notify/dm request body from CLI inputs. Pure (no I/O,
281
282
  * no clock) so the option-resolution logic is unit-testable; the caller
@@ -312,12 +313,20 @@ export function buildDmBody(args) {
312
313
  }
313
314
  const prompt = args.prompt?.trim();
314
315
  const details = args.details?.trim();
316
+ let audience;
317
+ if (args.audience !== undefined && args.audience !== "") {
318
+ if (!DM_AUDIENCE_VALUES.includes(args.audience)) {
319
+ throw new Error(`Invalid --audience '${args.audience}': must be human, agent, or both.`);
320
+ }
321
+ audience = args.audience;
322
+ }
315
323
  return {
316
324
  ...rcpt,
317
325
  body,
318
326
  ...(prompt ? { prompt } : {}),
319
327
  ...(details ? { details } : {}),
320
328
  ...(deliverAt ? { deliverAt } : {}),
329
+ ...(audience ? { audience } : {}),
321
330
  };
322
331
  }
323
332
  function friendlyDmError(status, code, fallback) {
@@ -670,6 +679,7 @@ async function runDmSend(recipient, message, opts) {
670
679
  at: opts.at,
671
680
  inDelay: opts.in,
672
681
  now: Date.now(),
682
+ audience: opts.audience,
673
683
  });
674
684
  const token = await ensureCognitoToken();
675
685
  const res = await vaultApiFetch({
@@ -831,14 +841,16 @@ export function formatInboxEvent(e, nowMs) {
831
841
  const when = chalk.dim(formatRelativeTime(e.createdAt, nowMs));
832
842
  const who = chalk.bold(senderLabel(e));
833
843
  const email = e.fromEmail && e.fromDisplayName ? chalk.dim(` <${e.fromEmail}>`) : "";
834
- return `${marker} ${when} ${who}${email}\n ${firstLine(e.body)}${formatAttachments(e)}`;
844
+ const audienceTag = e.audience === "agent" ? chalk.dim(" [agent]") : "";
845
+ return `${marker} ${when} ${who}${email}${audienceTag}\n ${firstLine(e.body)}${formatAttachments(e)}`;
835
846
  }
836
847
  /** Render one 1:1 thread line, tagged by direction. */
837
848
  export function formatThreadMessage(m, nowMs) {
838
849
  const arrow = m.direction === "out" ? chalk.dim("→") : chalk.cyan("←");
839
850
  const who = m.direction === "out" ? "you" : senderLabel(m);
840
851
  const when = chalk.dim(formatRelativeTime(m.createdAt, nowMs));
841
- return `${arrow} ${chalk.bold(who)} ${when}\n ${firstLine(m.body)}${formatAttachments(m)}`;
852
+ const audienceTag = m.audience === "agent" ? chalk.dim(" [agent]") : "";
853
+ return `${arrow} ${chalk.bold(who)} ${when}${audienceTag}\n ${firstLine(m.body)}${formatAttachments(m)}`;
842
854
  }
843
855
  /** Render one channel/group message line. */
844
856
  export function formatChannelMessage(m, nowMs, opts = {}) {
@@ -1280,6 +1292,7 @@ export function registerDmCommand(program) {
1280
1292
  .option("--details-file <path>", "Read the details from a file (1:1 DMs only)")
1281
1293
  .option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
1282
1294
  .option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
1295
+ .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)")
1283
1296
  .action(async (recipient, message, opts) => {
1284
1297
  await runDmSend(recipient, message, opts);
1285
1298
  });
@@ -47,6 +47,15 @@ export declare function consoleOrigin(vaultApiUrl?: string): string | undefined;
47
47
  * by slug), or undefined when the console for this control plane is unknown.
48
48
  */
49
49
  export declare function consoleIntegrationsUrl(companySlug: string | undefined, origin: string | undefined): string | undefined;
50
+ /**
51
+ * Google issues "Desktop app" and "Web application" OAuth clients with the
52
+ * same client-id shape, but only a Web application client accepts HQ's hosted
53
+ * https callback. A Desktop client fails at Google with `redirect_uri_mismatch`
54
+ * after the user has already opened the browser, so say it up front
55
+ * (Undrdog, 2026-09-24). The callback is read from the authorize URL hq-pro
56
+ * built, so the hint always names the URL Google will actually be sent.
57
+ */
58
+ export declare function googleClientTypeHint(clientId: string, authorizationUrl: string): string | null;
50
59
  export declare function findAlreadyConnected(connections: readonly AdminConnection[], app: string): AdminConnection | undefined;
51
60
  export declare function registerConnectCommands(integrations: Command): void;
52
61
  //# sourceMappingURL=integrations-connect.d.ts.map
@@ -851,12 +851,36 @@ async function userClientSignIn(token, companyUid, target, opts, client, request
851
851
  });
852
852
  console.error(`Open this URL to sign in to ${started.displayName || target.label}:`);
853
853
  console.error(` ${started.authorizationUrl}`);
854
+ const googleHint = googleClientTypeHint(client.clientId, started.authorizationUrl);
855
+ if (googleHint)
856
+ console.error(chalk.yellow(googleHint));
854
857
  if (opts.browser !== false)
855
858
  await open(started.authorizationUrl).catch(() => { });
856
859
  console.error(chalk.dim("The browser finishes on the HQ console. When it says connected, " +
857
860
  "run `hq integrations list` to confirm."));
858
861
  return null;
859
862
  }
863
+ /**
864
+ * Google issues "Desktop app" and "Web application" OAuth clients with the
865
+ * same client-id shape, but only a Web application client accepts HQ's hosted
866
+ * https callback. A Desktop client fails at Google with `redirect_uri_mismatch`
867
+ * after the user has already opened the browser, so say it up front
868
+ * (Undrdog, 2026-09-24). The callback is read from the authorize URL hq-pro
869
+ * built, so the hint always names the URL Google will actually be sent.
870
+ */
871
+ export function googleClientTypeHint(clientId, authorizationUrl) {
872
+ if (!clientId.toLowerCase().endsWith(".apps.googleusercontent.com"))
873
+ return null;
874
+ let redirectUri = null;
875
+ try {
876
+ redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
877
+ }
878
+ catch {
879
+ // An unparseable URL still gets the client-type half of the hint.
880
+ }
881
+ return ("This Google OAuth client must be of type \"Web application\" (a \"Desktop app\" client will not work)" +
882
+ (redirectUri ? `, with this authorized redirect URI:\n ${redirectUri}` : "."));
883
+ }
860
884
  /**
861
885
  * Second chance at a credential when the target's auth mode wasn't known up
862
886
  * front.
@@ -213,12 +213,13 @@ export function buildAgentMcpTools(clients) {
213
213
  },
214
214
  {
215
215
  name: "hq_dm_send",
216
- description: "Send an HQ direct message as this agent to a person (email or prs_…) or agent (agt_…).",
216
+ description: "Send an HQ direct message as this agent to a person (email or prs_…) or agent (agt_…). Use audience='agent' for progress/status messages that only agents need to see; audience='human' for messages a person must read; audience='both' when unsure.",
217
217
  inputSchema: {
218
218
  type: "object",
219
219
  properties: {
220
220
  to: { type: "string", description: "Email, prs_… or agt_… uid" },
221
221
  message: { type: "string" },
222
+ audience: { type: "string", enum: ["human", "agent", "both"], description: "Who this message is for (default: human)" },
222
223
  },
223
224
  required: ["to", "message"],
224
225
  additionalProperties: false,
@@ -226,6 +227,10 @@ export function buildAgentMcpTools(clients) {
226
227
  handler: async (args) => {
227
228
  const to = str(args, "to", { required: true, max: 320 }).trim();
228
229
  const message = str(args, "message", { required: true, max: 20_000 }).trim();
230
+ const audience = str(args, "audience");
231
+ if (audience !== undefined && !["human", "agent", "both"].includes(audience)) {
232
+ throw new McpToolInputError('"audience" must be human, agent, or both');
233
+ }
229
234
  const body = /^[^\s]+@[^\s]+$/.test(to)
230
235
  ? { toEmail: to.toLowerCase(), body: message }
231
236
  : isRecipientUid(to)
@@ -233,6 +238,8 @@ export function buildAgentMcpTools(clients) {
233
238
  : (() => {
234
239
  throw new McpToolInputError('"to" must be an email, prs_… or agt_… uid');
235
240
  })();
241
+ if (audience)
242
+ body.audience = audience;
236
243
  const token = await clients.getToken();
237
244
  const res = await clients.apiJson(token, "/v1/notify/dm", { method: "POST", body });
238
245
  if (res.status < 200 || res.status >= 300) {
@@ -216,6 +216,7 @@ export declare class BotApi {
216
216
  toPersonUid: string;
217
217
  body: string;
218
218
  rootEventId?: string;
219
+ audience?: "human" | "agent" | "both";
219
220
  }): Promise<{
220
221
  eventId?: string;
221
222
  createdAt?: string;
@@ -156,6 +156,8 @@ export class BotApi {
156
156
  const payload = { toPersonUid: recipient, body: input.body };
157
157
  if (input.rootEventId)
158
158
  payload.rootEventId = input.rootEventId;
159
+ if (input.audience)
160
+ payload.audience = input.audience;
159
161
  const { body } = await this.call("/v1/notify/dm", { method: "POST", body: payload });
160
162
  return body;
161
163
  }
@@ -30,6 +30,11 @@ export declare const WORKING_NOTICE_TEXT = "Working on it \u2014 I'll reply here
30
30
  export interface ProgressPosterOptions {
31
31
  /** Post one message; resolve true when it was delivered. */
32
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>;
33
38
  now?: () => number;
34
39
  setTimer?: (fn: () => void, ms: number) => unknown;
35
40
  clearTimer?: (handle: unknown) => void;
@@ -42,6 +47,7 @@ export interface ProgressPosterOptions {
42
47
  export declare function normalizeForCompare(text: string): string;
43
48
  export declare class ProgressPoster {
44
49
  private readonly send;
50
+ private readonly sendFinalFn?;
45
51
  private readonly now;
46
52
  private readonly setTimer;
47
53
  private readonly clearTimer;
@@ -73,6 +79,9 @@ export declare class ProgressPoster {
73
79
  /**
74
80
  * The turn succeeded: post whatever is pending, then the final answer unless
75
81
  * it repeats what was already said. Resolves once everything is sent.
82
+ *
83
+ * When `sendFinal` was provided, progress items flush through `send` and the
84
+ * final reply goes through `sendFinal` — keeping their audience tags distinct.
76
85
  */
77
86
  finish(finalText: string): Promise<void>;
78
87
  /**
@@ -84,6 +93,7 @@ export declare class ProgressPoster {
84
93
  private scheduleFlush;
85
94
  private flushNow;
86
95
  private enqueue;
96
+ private enqueueWith;
87
97
  private cancelFlush;
88
98
  private cancelNotice;
89
99
  }
@@ -33,6 +33,7 @@ export function normalizeForCompare(text) {
33
33
  }
34
34
  export class ProgressPoster {
35
35
  send;
36
+ sendFinalFn;
36
37
  now;
37
38
  setTimer;
38
39
  clearTimer;
@@ -54,6 +55,7 @@ export class ProgressPoster {
54
55
  delivered = 0;
55
56
  constructor(opts) {
56
57
  this.send = opts.send;
58
+ this.sendFinalFn = opts.sendFinal;
57
59
  this.now = opts.now ?? Date.now;
58
60
  this.setTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
59
61
  this.clearTimer = opts.clearTimer ?? ((h) => clearTimeout(h));
@@ -99,6 +101,9 @@ export class ProgressPoster {
99
101
  /**
100
102
  * The turn succeeded: post whatever is pending, then the final answer unless
101
103
  * it repeats what was already said. Resolves once everything is sent.
104
+ *
105
+ * When `sendFinal` was provided, progress items flush through `send` and the
106
+ * final reply goes through `sendFinal` — keeping their audience tags distinct.
102
107
  */
103
108
  async finish(finalText) {
104
109
  if (this.finished)
@@ -109,13 +114,21 @@ export class ProgressPoster {
109
114
  const final = finalText.trim();
110
115
  const held = this.heldQuestions;
111
116
  this.heldQuestions = [];
112
- if (final && !this.repeatsSaid(final))
113
- this.pending.push(final);
114
- else if (held.length > 0)
117
+ const shouldAddFinal = Boolean(final && !this.repeatsSaid(final));
118
+ if (!shouldAddFinal && held.length > 0)
115
119
  this.pending.push(...held);
116
- if (this.pending.length === 0 && this.delivered === 0 && !final)
120
+ if (this.pending.length === 0 && this.delivered === 0 && !shouldAddFinal)
117
121
  this.pending.push("(no reply)");
118
- this.flushNow();
122
+ if (this.sendFinalFn && shouldAddFinal) {
123
+ // Flush any remaining progress messages first, then the final reply separately.
124
+ this.flushNow();
125
+ this.enqueueWith(final, this.sendFinalFn);
126
+ }
127
+ else {
128
+ if (shouldAddFinal)
129
+ this.pending.push(final);
130
+ this.flushNow();
131
+ }
119
132
  return this.chain;
120
133
  }
121
134
  /**
@@ -158,10 +171,13 @@ export class ProgressPoster {
158
171
  this.enqueue(body);
159
172
  }
160
173
  enqueue(body) {
174
+ this.enqueueWith(body, this.send);
175
+ }
176
+ enqueueWith(body, sendFn) {
161
177
  this.lastPostAt = this.now();
162
178
  this.chain = this.chain.then(async () => {
163
179
  try {
164
- if (await this.send(body))
180
+ if (await sendFn(body))
165
181
  this.delivered += 1;
166
182
  }
167
183
  catch {
@@ -337,6 +337,12 @@ export function buildSystemPrompt(input) {
337
337
  const blocks = input.sources.memory.map((m) => `### ${m.name}\n\n${m.body.trim()}`);
338
338
  parts.push(`## Memory (${memoryDir}/)\n\n${blocks.join("\n\n")}`);
339
339
  }
340
+ parts.push(`## Message audience\n\n` +
341
+ `When sending a DM, tag each message with the right audience using the \`audience\` field:\n` +
342
+ `- \`agent\`: status pings, tool handoffs, and in-progress notes that only another agent needs to read.\n` +
343
+ `- \`human\`: anything a person must read — answers, results, questions, summaries.\n` +
344
+ `- \`both\`: when the content is relevant to both a person and a downstream agent.\n` +
345
+ `When unsure, use \`human\`. Progress messages sent before the final answer should use \`agent\`; the final answer should use \`human\`.`);
340
346
  return `${parts.join("\n\n")}\n`;
341
347
  }
342
348
  /**
@@ -646,23 +646,25 @@ export async function runBot(deps) {
646
646
  writeInflight(dir, { v: 1, ...args.marker, startedAt: now().toISOString() });
647
647
  let running = true;
648
648
  let posted = 0;
649
+ const makeSendFn = (fn) => async (body) => {
650
+ try {
651
+ await fn(body);
652
+ }
653
+ catch (err) {
654
+ log("warn", `post for ${messageId} failed: ${err instanceof Error ? err.message : String(err)}`);
655
+ return false;
656
+ }
657
+ posted += 1;
658
+ patchInflight(dir, messageId, { progressPosted: posted });
659
+ if (running && args.afterProgressPost)
660
+ await args.afterProgressPost().catch(() => undefined);
661
+ return true;
662
+ };
649
663
  const poster = new ProgressPoster({
650
664
  ...deps.progress,
651
665
  ...(args.workingNotice === false ? { workingNoticeMs: 0 } : {}),
652
- send: async (body) => {
653
- try {
654
- await args.send(body);
655
- }
656
- catch (err) {
657
- log("warn", `post for ${messageId} failed: ${err instanceof Error ? err.message : String(err)}`);
658
- return false;
659
- }
660
- posted += 1;
661
- patchInflight(dir, messageId, { progressPosted: posted });
662
- if (running && args.afterProgressPost)
663
- await args.afterProgressPost().catch(() => undefined);
664
- return true;
665
- },
666
+ send: makeSendFn(args.send),
667
+ ...(args.sendFinal ? { sendFinal: makeSendFn(args.sendFinal) } : {}),
666
668
  });
667
669
  const controller = new AbortController();
668
670
  currentTurns.add(controller);
@@ -693,10 +695,10 @@ export async function runBot(deps) {
693
695
  }
694
696
  };
695
697
  const failureReason = (outcome) => outcome.aborted ? STOPPED_BY_OWNER_REASON : `${runtime.id} stopped with an error: ${outcome.excerpt.slice(0, 160)}`;
696
- const postOne = (target, body, rootEventId) => {
698
+ const postOne = (target, body, rootEventId, audience) => {
697
699
  const root = rootEventId ? { rootEventId } : {};
698
700
  return target.kind === "dm"
699
- ? api.sendDm({ toPersonUid: target.peerUid, body, ...root })
701
+ ? api.sendDm({ toPersonUid: target.peerUid, body, ...root, ...(audience ? { audience } : {}) })
700
702
  : api.sendChannelMessage({ channelId: target.channelId, body, ...root });
701
703
  };
702
704
  /**
@@ -704,10 +706,10 @@ export async function runBot(deps) {
704
706
  * PartialPostError naming how many parts got through, so the caller can save
705
707
  * exactly the remainder.
706
708
  */
707
- const sendParts = async (target, parts, from, rootEventId, onDelivered) => {
709
+ const sendParts = async (target, parts, from, rootEventId, onDelivered, audience) => {
708
710
  for (let i = Math.max(0, from); i < parts.length; i += 1) {
709
711
  try {
710
- await postOne(target, parts[i], rootEventId);
712
+ await postOne(target, parts[i], rootEventId, audience);
711
713
  }
712
714
  catch (err) {
713
715
  throw new PartialPostError(parts, i, err);
@@ -716,16 +718,16 @@ export async function runBot(deps) {
716
718
  }
717
719
  };
718
720
  /** Every outbound body goes through here, so nothing over the limit is ever posted whole. */
719
- const sendToTarget = (target, body, rootEventId) => sendParts(target, splitBody(body), 0, rootEventId);
721
+ const sendToTarget = (target, body, rootEventId, audience) => sendParts(target, splitBody(body), 0, rootEventId, undefined, audience);
720
722
  /**
721
723
  * A `send` for one conversation that remembers how much of its last failed
722
724
  * body got through, so the in-flight marker can record the remainder.
723
725
  */
724
726
  const trackingSend = (target, rootEventId) => {
725
727
  const last = { value: null };
726
- const send = async (body) => {
728
+ const makeSend = (audience) => async (body) => {
727
729
  try {
728
- await sendToTarget(target, body, rootEventId);
730
+ await sendToTarget(target, body, rootEventId, audience);
729
731
  last.value = null;
730
732
  }
731
733
  catch (err) {
@@ -733,13 +735,15 @@ export async function runBot(deps) {
733
735
  throw err;
734
736
  }
735
737
  };
738
+ const send = makeSend(target.kind === "dm" ? "agent" : undefined);
739
+ const sendFinal = makeSend(target.kind === "dm" ? "human" : undefined);
736
740
  const saved = (reply) => {
737
741
  const parts = splitBody(reply);
738
742
  const partial = last.value;
739
743
  const sameBody = partial !== null && partial.parts.length === parts.length && partial.parts.every((p, i) => p === parts[i]);
740
744
  return { reply, replyParts: parts, replyPartsDelivered: sameBody ? partial.delivered : 0 };
741
745
  };
742
- return { send, saved };
746
+ return { send, sendFinal, saved };
743
747
  };
744
748
  // ── Scheduled jobs ────────────────────────────────────────────────────────
745
749
  /** Newest pending run key already picked up, per jobId (one catch-up per wake). */
@@ -967,13 +971,14 @@ export async function runBot(deps) {
967
971
  }
968
972
  };
969
973
  const target = { kind: "dm", peerUid };
970
- const { send, saved } = trackingSend(target);
974
+ const { send, sendFinal, saved } = trackingSend(target);
971
975
  const { outcome, poster } = await runTurnWithProgress({
972
976
  marker: { messageId: KICKOFF_MESSAGE_ID, target, ...(thinking && introEventId ? { reactionEventId: introEventId } : {}) },
973
977
  prompt,
974
978
  sessionScope: "dm",
975
979
  tReceived,
976
980
  send,
981
+ sendFinal,
977
982
  // The intro already said the bot is starting, and the app shows it thinking.
978
983
  workingNotice: false,
979
984
  });
@@ -1069,10 +1074,10 @@ export async function runBot(deps) {
1069
1074
  /** The body of the LAST send, when that send failed (the resume point). */
1070
1075
  let lastFailedBody = null;
1071
1076
  const withHeader = (body) => (headerOut ? body : `${header}${body}`);
1072
- const send = async (body) => {
1077
+ const makeSend = (fn) => async (body) => {
1073
1078
  const full = withHeader(body);
1074
1079
  try {
1075
- await tracking.send(full);
1080
+ await fn(full);
1076
1081
  headerOut = true;
1077
1082
  deliveredBodies.push(body);
1078
1083
  lastFailedBody = null;
@@ -1085,6 +1090,8 @@ export async function runBot(deps) {
1085
1090
  throw err;
1086
1091
  }
1087
1092
  };
1093
+ const send = makeSend(tracking.send);
1094
+ const sendFinal = makeSend(tracking.sendFinal);
1088
1095
  const normalize = (text) => text.replace(/\s+/g, " ").trim();
1089
1096
  /**
1090
1097
  * True only when the final report itself reached the chat in full. With no
@@ -1121,6 +1128,7 @@ export async function runBot(deps) {
1121
1128
  sessionScope: target.sessionScope,
1122
1129
  tReceived,
1123
1130
  send,
1131
+ sendFinal,
1124
1132
  // A scheduled run has nobody waiting in a chat: no "working on it" filler.
1125
1133
  workingNotice: false,
1126
1134
  });
@@ -1312,7 +1320,7 @@ export async function runBot(deps) {
1312
1320
  const prompt = target.kind === "room" ? await buildRoomTurn(item, target, text) : target.peerUid === config.ownerUid ? text :
1313
1321
  `[HQ DM sender: ${target.peerUid}; owner: ${config.ownerUid}; sender is not the owner.]\n\n${text}`;
1314
1322
  const markerTarget = target.kind === "dm" ? { kind: "dm", peerUid: target.peerUid } : { kind: "room", channelId: target.channelId };
1315
- const { send, saved } = trackingSend(markerTarget, item.rootEventId);
1323
+ const { send, sendFinal, saved } = trackingSend(markerTarget, item.rootEventId);
1316
1324
  const { outcome, poster } = await runTurnWithProgress({
1317
1325
  marker: {
1318
1326
  messageId: id,
@@ -1324,6 +1332,7 @@ export async function runBot(deps) {
1324
1332
  sessionScope,
1325
1333
  tReceived,
1326
1334
  send,
1335
+ sendFinal,
1327
1336
  workingNotice: !isSetupBot,
1328
1337
  ...(readDirs.length ? { readDirs } : {}),
1329
1338
  ...(target.kind === "room"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.179.0",
3
+ "version": "5.179.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {