@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 +15 -1
- package/dist/command-catalog.generated.d.ts +3 -0
- package/dist/command-catalog.generated.js +4 -0
- package/dist/commands/dm.d.ts +5 -0
- package/dist/commands/dm.js +15 -2
- package/dist/commands/integrations-connect.d.ts +9 -0
- package/dist/commands/integrations-connect.js +24 -0
- package/dist/lib/agent-kit/mcp/tools.js +8 -1
- package/dist/lib/bot/api.d.ts +1 -0
- package/dist/lib/bot/api.js +2 -0
- package/dist/lib/bot/progress.d.ts +10 -0
- package/dist/lib/bot/progress.js +22 -6
- package/dist/lib/bot/prompt.js +6 -0
- package/dist/lib/bot/run.js +35 -26
- package/package.json +1 -1
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": []
|
package/dist/commands/dm.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/commands/dm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/lib/bot/api.d.ts
CHANGED
package/dist/lib/bot/api.js
CHANGED
|
@@ -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
|
}
|
package/dist/lib/bot/progress.js
CHANGED
|
@@ -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
|
-
|
|
113
|
-
|
|
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 && !
|
|
120
|
+
if (this.pending.length === 0 && this.delivered === 0 && !shouldAddFinal)
|
|
117
121
|
this.pending.push("(no reply)");
|
|
118
|
-
this.
|
|
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
|
|
180
|
+
if (await sendFn(body))
|
|
165
181
|
this.delivered += 1;
|
|
166
182
|
}
|
|
167
183
|
catch {
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -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
|
/**
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -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:
|
|
653
|
-
|
|
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
|
|
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
|
|
1077
|
+
const makeSend = (fn) => async (body) => {
|
|
1073
1078
|
const full = withHeader(body);
|
|
1074
1079
|
try {
|
|
1075
|
-
await
|
|
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"
|