@indigoai-us/hq-cli 5.343.0 → 5.344.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/command-catalog.generated.d.ts +15 -4
- package/dist/command-catalog.generated.js +19 -4
- package/dist/commands/cloud.js +2 -2
- package/dist/commands/daemon.js +2 -11
- package/dist/commands/dm.js +10 -1
- package/dist/commands/lanes-shared.d.ts +1 -0
- package/dist/commands/lanes.d.ts +1 -0
- package/dist/commands/lanes.js +11 -0
- package/dist/commands/login.js +12 -1
- package/dist/commands/logout.js +10 -0
- package/dist/commands/mesh.d.ts +27 -1
- package/dist/commands/mesh.js +123 -12
- package/dist/lib/agent-kit/mcp/tools.js +1 -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 +12 -14
- package/dist/lib/bot/progress.js +29 -40
- package/dist/lib/bot/run.js +24 -24
- package/dist/lib/core-utils/sentry-report.d.ts +1 -1
- package/dist/lib/core-utils/sentry-report.js +18 -1
- package/dist/lib/daemon/command-call.d.ts +9 -0
- package/dist/lib/daemon/command-call.js +18 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/work-context/config.d.ts +66 -0
- package/dist/lib/work-context/config.js +142 -0
- package/dist/lib/work-context/person-identity.d.ts +16 -0
- package/dist/lib/work-context/person-identity.js +39 -0
- package/dist/utils/cli-telemetry.js +5 -1
- package/dist/utils/client-health.d.ts +7 -3
- package/dist/utils/client-health.js +30 -6
- package/dist/utils/install-tree-torn.d.ts +16 -13
- package/dist/utils/install-tree-torn.js +58 -26
- package/dist/utils/resident-package-preload.d.ts +1 -1
- package/dist/utils/resident-package-preload.js +18 -0
- package/package.json +1 -1
|
@@ -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
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
|
|
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
|
|
67
|
-
private readonly
|
|
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
|
|
113
|
-
*
|
|
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;
|
package/dist/lib/bot/progress.js
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
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.
|
|
73
|
-
this.
|
|
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.
|
|
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
|
|
151
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -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
|
-
|
|
770
|
-
|
|
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
|
|
867
|
-
const
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
1123
|
-
|
|
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",
|
|
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
|
|
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
|
|
1239
|
-
const
|
|
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
|
-
|
|
1276
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
1549
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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);
|
|
@@ -44,10 +44,41 @@ export interface RepoCompanyMapping {
|
|
|
44
44
|
updatedAt: string;
|
|
45
45
|
}
|
|
46
46
|
type RepoCompanyMap = Record<string, RepoCompanyMapping>;
|
|
47
|
+
/**
|
|
48
|
+
* Explicit opt-in to count usage from HQ sessions with no company toward the
|
|
49
|
+
* device default company. hq-cloud's usage sync reads this field and credits
|
|
50
|
+
* the default company only when mode is exactly "default-company", companyUid
|
|
51
|
+
* starts with "cmp_" and equals the default company's uid, and optedInAt
|
|
52
|
+
* parses as a date. Company owners and admins can see company-attributed
|
|
53
|
+
* usage, so this is never written without the person's explicit consent.
|
|
54
|
+
*/
|
|
55
|
+
export interface UsageAttributionOptIn {
|
|
56
|
+
mode: "default-company";
|
|
57
|
+
companyUid: string;
|
|
58
|
+
/**
|
|
59
|
+
* The signed-in person who consented: the `custom:entityUid` claim (prs_...)
|
|
60
|
+
* of the cached Cognito person session. Consent never carries to another
|
|
61
|
+
* account on the same OS user.
|
|
62
|
+
*/
|
|
63
|
+
personUid: string;
|
|
64
|
+
optedInAt: string;
|
|
65
|
+
}
|
|
66
|
+
/** A person principal uid, as carried in the `custom:entityUid` token claim. */
|
|
67
|
+
export declare function isPersonUid(value: unknown): value is string;
|
|
68
|
+
/** How `setDefaultCompany` treats the usage-attribution opt-in. */
|
|
69
|
+
export type UsageAttributionChoice =
|
|
70
|
+
/** Write an opt-in for the newly set company (requires its uid). */
|
|
71
|
+
"opt-in"
|
|
72
|
+
/** Remove any opt-in. */
|
|
73
|
+
| "opt-out"
|
|
74
|
+
/** Keep an existing opt-in only when it is for the same company uid. */
|
|
75
|
+
| "keep-if-same";
|
|
47
76
|
export interface WorkContextDeviceConfig {
|
|
48
77
|
schemaVersion: typeof DEVICE_CONFIG_SCHEMA_VERSION;
|
|
49
78
|
/** Explicit device default. Never copied from activeCompany. */
|
|
50
79
|
defaultCompany?: DeviceDefaultCompany | null;
|
|
80
|
+
/** Opt-in to count no-company HQ session usage toward the default company. */
|
|
81
|
+
usageAttribution?: UsageAttributionOptIn;
|
|
51
82
|
/** Marks a null default written by the automatic-activation implementation. */
|
|
52
83
|
defaultCompanyAutomaticEligible?: true;
|
|
53
84
|
/** Last-known migration capability across memberships (doctor reports this). */
|
|
@@ -75,12 +106,31 @@ export interface DeviceConfigDeps {
|
|
|
75
106
|
*/
|
|
76
107
|
validateMembership?: (slug: string) => Promise<MembershipValidationResult> | MembershipValidationResult;
|
|
77
108
|
now?: () => Date;
|
|
109
|
+
/**
|
|
110
|
+
* Usage-attribution opt-in handling for setDefaultCompany. Defaults to
|
|
111
|
+
* "keep-if-same": an existing opt-in survives only when it is for the same
|
|
112
|
+
* company uid, so changing the default never carries consent across companies.
|
|
113
|
+
*/
|
|
114
|
+
usageAttribution?: UsageAttributionChoice;
|
|
115
|
+
/**
|
|
116
|
+
* The signed-in person (prs_...). Required to write an opt-in; an existing
|
|
117
|
+
* opt-in is kept only when it was given by this person.
|
|
118
|
+
*/
|
|
119
|
+
personUid?: string | null;
|
|
78
120
|
}
|
|
79
121
|
/** Acquire an interprocess config lock; exported for direct lock regression tests. */
|
|
80
122
|
export declare function acquireDeviceConfigLock(root: string, options?: {
|
|
81
123
|
timeoutMs?: number;
|
|
82
124
|
staleMs?: number;
|
|
83
125
|
}): () => void;
|
|
126
|
+
/** Parse a raw usageAttribution value; anything malformed is dropped (fail closed). */
|
|
127
|
+
export declare function sanitizeUsageAttribution(raw: unknown): UsageAttributionOptIn | undefined;
|
|
128
|
+
/**
|
|
129
|
+
* The opt-in that is in effect: present, for the uid of the current enabled
|
|
130
|
+
* default company, and given by the signed-in person. Mirrors the hq-cloud
|
|
131
|
+
* reader. No signed-in person means no opt-in.
|
|
132
|
+
*/
|
|
133
|
+
export declare function effectiveUsageAttribution(cfg: Pick<WorkContextDeviceConfig, "defaultCompany" | "usageAttribution">, personUid: string | null | undefined): UsageAttributionOptIn | null;
|
|
84
134
|
/** Injection points for assertSafeConfigPath (tests only). */
|
|
85
135
|
export interface SafeConfigPathIo {
|
|
86
136
|
platform?: NodeJS.Platform;
|
|
@@ -104,6 +154,22 @@ export declare function getDefaultCompany(deps: Pick<DeviceConfigDeps, "root">):
|
|
|
104
154
|
/** A clear is a durable opt-out, not an invitation for automatic reactivation. */
|
|
105
155
|
export declare function isDefaultCompanyDisabled(deps: Pick<DeviceConfigDeps, "root">): boolean;
|
|
106
156
|
export declare function setDefaultCompany(slug: string, deps: DeviceConfigDeps): Promise<WorkContextDeviceConfig>;
|
|
157
|
+
/**
|
|
158
|
+
* Record or remove the usage-attribution opt-in for the current default
|
|
159
|
+
* company. Opting in requires an enabled, explicitly chosen default with a
|
|
160
|
+
* cmp_ uid that equals `companyUid` (the company the person was asked about);
|
|
161
|
+
* an automatic single-company default is never opted in.
|
|
162
|
+
*/
|
|
163
|
+
export declare function setUsageAttribution(optIn: {
|
|
164
|
+
companyUid: string;
|
|
165
|
+
personUid: string;
|
|
166
|
+
} | null, deps: Pick<DeviceConfigDeps, "root" | "now">): WorkContextDeviceConfig;
|
|
167
|
+
/**
|
|
168
|
+
* Drop the usage-attribution opt-in unless it was given by `personUid`. Called
|
|
169
|
+
* on login (identity may have changed) and logout (null: always drop). A
|
|
170
|
+
* config with no opt-in, or one already matching, is left untouched.
|
|
171
|
+
*/
|
|
172
|
+
export declare function retainUsageAttributionForPerson(personUid: string | null | undefined, deps: Pick<DeviceConfigDeps, "root" | "now">): boolean;
|
|
107
173
|
export declare function clearDefaultCompany(deps: DeviceConfigDeps): WorkContextDeviceConfig;
|
|
108
174
|
/**
|
|
109
175
|
* Read the persisted company mapping for a repo identity key (gap 4).
|