@indigoai-us/hq-cli 5.342.18 → 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 +15 -0
- package/dist/command-catalog.generated.d.ts +12 -4
- package/dist/command-catalog.generated.js +14 -4
- package/dist/commands/company.d.ts +12 -0
- package/dist/commands/company.js +103 -6
- 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/mesh.js +6 -2
- package/dist/commands/pack-install.d.ts +2 -0
- package/dist/commands/pack-install.js +7 -0
- package/dist/commands/pkg-update.d.ts +2 -0
- package/dist/commands/pkg-update.js +5 -1
- package/dist/commands/secrets.js +2 -3
- 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/doctor/checks/work-context.js +5 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/mesh/client.js +11 -0
- package/dist/lib/mesh/denial-codes.d.ts +13 -2
- package/dist/lib/mesh/denial-codes.js +25 -1
- package/dist/lib/mesh/live/daemon/doctor.js +4 -0
- package/dist/lib/work-context/outbox.js +8 -2
- package/dist/utils/cli-telemetry.js +5 -1
- 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 +62 -0
- package/dist/utils/secrets-sandbox-resident.d.ts +5 -0
- package/dist/utils/secrets-sandbox-resident.js +8 -0
- package/package.json +1 -1
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
|
|
@@ -13,6 +13,7 @@ import { workContextRoot, workContextSessionsDir } from "../../work-context/path
|
|
|
13
13
|
import { countDistinctHeldEvents, countJsonlLines, deadLetterNonEmpty, } from "../../mesh/live/spool.js";
|
|
14
14
|
import { workMeshDeadLetterPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "../../mesh/live/paths.js";
|
|
15
15
|
import { collectDaemonDoctor, formatDaemonDoctor, } from "../../mesh/live/daemon/doctor.js";
|
|
16
|
+
import { workSessionDenialReason } from "../../mesh/denial-codes.js";
|
|
16
17
|
export const WORK_CONTEXT_FAMILY_ID = "work-context";
|
|
17
18
|
export const WORK_CONTEXT_FAMILY_TITLE = "Work context";
|
|
18
19
|
const DEFAULT_DEPS = {
|
|
@@ -97,6 +98,9 @@ function runChecks(deps) {
|
|
|
97
98
|
]
|
|
98
99
|
.filter(Boolean)
|
|
99
100
|
.join(" ");
|
|
101
|
+
const quarantineDenialReason = showQuarantineReason
|
|
102
|
+
? workSessionDenialReason(stats.lastQuarantineServerCode)
|
|
103
|
+
: undefined;
|
|
100
104
|
results.push({
|
|
101
105
|
status: stats.quarantined > 0 ? "WARN" : "PASS",
|
|
102
106
|
checkId: "work-context.outbox-quarantined",
|
|
@@ -104,6 +108,7 @@ function runChecks(deps) {
|
|
|
104
108
|
message: `Outbox quarantined count=${stats.quarantined}${showQuarantineReason
|
|
105
109
|
? ` (last reason=${stats.lastQuarantineReason ?? "unknown"}${quarantineDetails ? `; ${quarantineDetails}` : ""})`
|
|
106
110
|
: ""}.`,
|
|
111
|
+
...(quarantineDenialReason ? { remediation: quarantineDenialReason } : {}),
|
|
107
112
|
});
|
|
108
113
|
// Default-company mode + migration capability (US-017B)
|
|
109
114
|
try {
|
|
@@ -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);
|
package/dist/lib/mesh/client.js
CHANGED
|
@@ -79,6 +79,17 @@ export function createWorkSessionDeliverer(opts) {
|
|
|
79
79
|
if (status === 429 || (status >= 500 && status <= 599)) {
|
|
80
80
|
return { ok: false, retryable: true, code: `HTTP_${status}` };
|
|
81
81
|
}
|
|
82
|
+
if (status === 404 && isWorkSessionDenialCode(errorFields.code)) {
|
|
83
|
+
// Keep the allowlisted code (FEATURE_DISABLED) so the refusal can be
|
|
84
|
+
// explained and picked up by the default requeue filter.
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
retryable: false,
|
|
88
|
+
code: "HTTP_404",
|
|
89
|
+
httpStatus: 404,
|
|
90
|
+
serverCode: errorFields.code,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
82
93
|
if (status === 404 ||
|
|
83
94
|
status === 409 ||
|
|
84
95
|
status === 410 ||
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Explicit refusal codes returned by hq-pro's work-session registration path:
|
|
3
|
+
* the 401/403 denials, plus the 404 FEATURE_DISABLED answer given while the
|
|
4
|
+
* company's Work Mesh Live setting is off.
|
|
5
|
+
*/
|
|
6
|
+
export declare const WORK_SESSION_DENIAL_CODES: readonly ["AUTH_REQUIRED", "AGENT_IDENTITY_UNVERIFIED", "IMPERSONATION_FORBIDDEN", "NO_PERSON_ENTITY", "INVALID_COMPANY", "WORK_REGISTRY_DISABLED", "FEATURE_DISABLED"];
|
|
7
|
+
/**
|
|
8
|
+
* Refusals that end once the company owner turns a setting on. The default
|
|
9
|
+
* `hq mesh context requeue` filter re-sends operations stored with these.
|
|
10
|
+
*/
|
|
11
|
+
export declare const WORK_TRACKING_OFF_DENIAL_CODES: readonly WorkSessionDenialCode[];
|
|
3
12
|
export type WorkSessionDenialCode = (typeof WORK_SESSION_DENIAL_CODES)[number];
|
|
4
13
|
export declare function isWorkSessionDenialCode(value: unknown): value is WorkSessionDenialCode;
|
|
14
|
+
/** Plain reason for a server denial code, or undefined when there is none. */
|
|
15
|
+
export declare function workSessionDenialReason(value: unknown): string | undefined;
|
|
5
16
|
//# sourceMappingURL=denial-codes.d.ts.map
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Explicit refusal codes returned by hq-pro's work-session registration path:
|
|
3
|
+
* the 401/403 denials, plus the 404 FEATURE_DISABLED answer given while the
|
|
4
|
+
* company's Work Mesh Live setting is off.
|
|
5
|
+
*/
|
|
2
6
|
export const WORK_SESSION_DENIAL_CODES = [
|
|
3
7
|
"AUTH_REQUIRED",
|
|
4
8
|
"AGENT_IDENTITY_UNVERIFIED",
|
|
@@ -6,9 +10,29 @@ export const WORK_SESSION_DENIAL_CODES = [
|
|
|
6
10
|
"NO_PERSON_ENTITY",
|
|
7
11
|
"INVALID_COMPANY",
|
|
8
12
|
"WORK_REGISTRY_DISABLED",
|
|
13
|
+
"FEATURE_DISABLED",
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Refusals that end once the company owner turns a setting on. The default
|
|
17
|
+
* `hq mesh context requeue` filter re-sends operations stored with these.
|
|
18
|
+
*/
|
|
19
|
+
export const WORK_TRACKING_OFF_DENIAL_CODES = [
|
|
20
|
+
"WORK_REGISTRY_DISABLED",
|
|
21
|
+
"FEATURE_DISABLED",
|
|
9
22
|
];
|
|
10
23
|
export function isWorkSessionDenialCode(value) {
|
|
11
24
|
return typeof value === "string" &&
|
|
12
25
|
WORK_SESSION_DENIAL_CODES.includes(value);
|
|
13
26
|
}
|
|
27
|
+
/** Plain-language reasons for denial codes the user can act on. */
|
|
28
|
+
const WORK_SESSION_DENIAL_REASONS = {
|
|
29
|
+
WORK_REGISTRY_DISABLED: "Work tracking is turned off for this company. The company owner can turn it on with: hq company --company <slug> settings set --work-registry-enabled true",
|
|
30
|
+
FEATURE_DISABLED: "Live work tracking is turned off for this company. The company owner can turn it on with: hq company --company <slug> settings set --work-mesh-live-enabled true",
|
|
31
|
+
};
|
|
32
|
+
/** Plain reason for a server denial code, or undefined when there is none. */
|
|
33
|
+
export function workSessionDenialReason(value) {
|
|
34
|
+
return isWorkSessionDenialCode(value)
|
|
35
|
+
? WORK_SESSION_DENIAL_REASONS[value]
|
|
36
|
+
: undefined;
|
|
37
|
+
}
|
|
14
38
|
//# sourceMappingURL=denial-codes.js.map
|
|
@@ -23,6 +23,7 @@ import { readDaemonState } from "./state.js";
|
|
|
23
23
|
import { CLI_VERSION } from "../../../../cli-version.js";
|
|
24
24
|
import { resolveRunningCliEntry } from "./install.js";
|
|
25
25
|
import { readInstalledCliVersion } from "./version-drift.js";
|
|
26
|
+
import { workSessionDenialReason } from "../../denial-codes.js";
|
|
26
27
|
import { LEGACY_LISTENER_DOCTOR_LINE, legacyListenInstalled, } from "./legacy-listen.js";
|
|
27
28
|
export const UNHEALTHY_SPOOL_AGE_MS = 60_000;
|
|
28
29
|
function oldestJsonlAgeMs(filePath, nowMs) {
|
|
@@ -321,6 +322,9 @@ export function formatDaemonDoctor(report) {
|
|
|
321
322
|
.filter(Boolean)
|
|
322
323
|
.join(" ");
|
|
323
324
|
lines.push(`outbox quarantined: ${report.outboxQuarantined} (last reason=${report.outboxLastQuarantineReason ?? "unknown"}${quarantineDetails ? `; ${quarantineDetails}` : ""})`);
|
|
325
|
+
const denialReason = workSessionDenialReason(report.outboxLastQuarantineServerCode);
|
|
326
|
+
if (denialReason)
|
|
327
|
+
lines.push(denialReason);
|
|
324
328
|
}
|
|
325
329
|
if (report.emitMode !== "legacy" && report.heldCount > 0) {
|
|
326
330
|
lines.push(`held: ${report.heldCount} (needs_company; not sent as NONE)`);
|
|
@@ -8,7 +8,7 @@ import * as path from "node:path";
|
|
|
8
8
|
import { WORK_CONTEXT_CONTRACT_VERSION } from "./contract.js";
|
|
9
9
|
import { atomicWriteJson, ensureOwnerDir } from "./atomic.js";
|
|
10
10
|
import { NotTrackingError } from "./errors.js";
|
|
11
|
-
import { isWorkSessionDenialCode } from "../mesh/denial-codes.js";
|
|
11
|
+
import { isWorkSessionDenialCode, WORK_TRACKING_OFF_DENIAL_CODES, } from "../mesh/denial-codes.js";
|
|
12
12
|
import { readSessionState, stripRegisterReceiptEpisodes, updateSessionState, writeAcknowledgedRegisterBinding, } from "./state.js";
|
|
13
13
|
import { isSafeWorkContextSegment, workContextOutboxDir, workContextOutboxPath, } from "./paths.js";
|
|
14
14
|
/** Base delay for outbox retry backoff (attempt 1 → 30s). */
|
|
@@ -390,8 +390,14 @@ export function requeueQuarantinedOutbox(root, opts = {}) {
|
|
|
390
390
|
for (const op of listOutboxOperations(root)) {
|
|
391
391
|
if (op.delivery !== "quarantined")
|
|
392
392
|
continue;
|
|
393
|
-
|
|
393
|
+
// Default filter: AUTH_DENIED, plus refusals stored with a server code
|
|
394
|
+
// that means work tracking was off for the company (a 404 FEATURE_DISABLED
|
|
395
|
+
// is stored as HTTP_404). An explicit errorCode matches lastErrorCode only.
|
|
396
|
+
const workTrackingWasOff = opts.errorCode === undefined &&
|
|
397
|
+
WORK_TRACKING_OFF_DENIAL_CODES.includes(op.lastServerErrorCode ?? "");
|
|
398
|
+
if (errorCode !== null && op.lastErrorCode !== errorCode && !workTrackingWasOff) {
|
|
394
399
|
continue;
|
|
400
|
+
}
|
|
395
401
|
op.delivery = "queued";
|
|
396
402
|
op.attemptCount = 0;
|
|
397
403
|
op.lastErrorCode = undefined;
|
|
@@ -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: {
|
|
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
|
|
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
|
-
* -
|
|
299
|
-
* pid
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* Each poll's REAL elapsed interval (now() deltas) is attributed to the
|
|
306
|
-
* unlocked bucket by whether a fresh
|
|
307
|
-
* began, so a starved event loop only makes the wait coarser,
|
|
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 {
|