@indigoai-us/hq-cli 5.343.0 → 5.343.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/dist/command-catalog.generated.d.ts +1 -1
- package/dist/command-catalog.generated.js +1 -1
- 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/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/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 +18 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.343.1] — 2026-10-04
|
|
6
|
+
|
|
7
|
+
- Include the bounded hq-cli version in `cli_feature_used` telemetry so feature-use rows can be grouped by release.
|
|
8
|
+
|
|
9
|
+
- Release the HQ CLI package-use lease before hq daemon call waits for its daemon response, after preloading the request protocol.
|
|
10
|
+
- Release the package-use lease after Outpost lane message and resume requests are submitted and their result-wait imports are preloaded.
|
|
11
|
+
- Preserve human, agent, and both audience tags when CLI tools and local bots post or reply in channels.
|
|
12
|
+
- Wait for a live installer to finish before retrying a failed CLI import when the install tree is still in flight.
|
|
13
|
+
- Preserve bounded master-hook slow-phase and per-phase timeout attribution in Sentry reports.
|
|
14
|
+
|
|
5
15
|
## [5.343.0] — 2026-10-04
|
|
6
16
|
- `hq company --company <slug> settings set --work-registry-enabled <true|false>` and `--work-mesh-live-enabled <true|false>` let a company owner turn work tracking on or off. Work tracking needs both switches; after either is set the command says whether work tracking is on and prints the exact command for the switch that is still off. `hq company --company <slug> settings get` now also shows both switches for an owner or admin.
|
|
7
17
|
- `hq mesh context requeue` now also re-sends, by default, sessions that were refused while work tracking was off. `hq doctor` and `hq mesh daemon doctor` explain those refusals in plain words.
|
|
@@ -3204,7 +3204,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
3204
3204
|
readonly hidden: false;
|
|
3205
3205
|
}, {
|
|
3206
3206
|
readonly flags: "--audience <human|agent|both>";
|
|
3207
|
-
readonly description: "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both
|
|
3207
|
+
readonly description: "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both";
|
|
3208
3208
|
readonly hidden: false;
|
|
3209
3209
|
}];
|
|
3210
3210
|
readonly subcommands: readonly [];
|
|
@@ -3874,7 +3874,7 @@ export const COMMAND_CATALOG = [
|
|
|
3874
3874
|
},
|
|
3875
3875
|
{
|
|
3876
3876
|
"flags": "--audience <human|agent|both>",
|
|
3877
|
-
"description": "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both
|
|
3877
|
+
"description": "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both",
|
|
3878
3878
|
"hidden": false
|
|
3879
3879
|
}
|
|
3880
3880
|
],
|
package/dist/commands/daemon.js
CHANGED
|
@@ -29,6 +29,7 @@ import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "..
|
|
|
29
29
|
import { daemonPaths } from "../lib/daemon/paths.js";
|
|
30
30
|
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
31
31
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
|
+
import { runDaemonCall } from "../lib/daemon/command-call.js";
|
|
32
33
|
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
33
34
|
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
34
35
|
import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
|
|
@@ -857,17 +858,7 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
857
858
|
const timeoutMs = Number(opts.timeout ?? (op === "policy.check" ? "40000" : "20000"));
|
|
858
859
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
859
860
|
throw new Error("--timeout must be a positive number of milliseconds");
|
|
860
|
-
|
|
861
|
-
try {
|
|
862
|
-
const result = await hqdRequest(op, args, { socketPath: hqdSocketPath(d.home, d.env), timeoutMs });
|
|
863
|
-
d.out(JSON.stringify(result, null, 2));
|
|
864
|
-
}
|
|
865
|
-
catch (err) {
|
|
866
|
-
if (!(err instanceof HqdClientError))
|
|
867
|
-
throw err;
|
|
868
|
-
d.out(JSON.stringify({ error: { code: err.code, message: err.message } }, null, 2));
|
|
869
|
-
process.exitCode = 1;
|
|
870
|
-
}
|
|
861
|
+
await runResidentCommandWithPackageLease("daemon-call", () => runDaemonCall(op, args, timeoutMs, d));
|
|
871
862
|
}));
|
|
872
863
|
daemon
|
|
873
864
|
.command("status")
|
package/dist/commands/dm.js
CHANGED
|
@@ -614,11 +614,20 @@ async function runChannelSend(channelName, message, opts) {
|
|
|
614
614
|
// Send the human-readable form: the quotes only existed for parsing.
|
|
615
615
|
if (mentions.length > 0)
|
|
616
616
|
body = stripMentionQuotes(body);
|
|
617
|
+
let audience;
|
|
618
|
+
if (opts.audience !== undefined && opts.audience !== "") {
|
|
619
|
+
if (!DM_AUDIENCE_VALUES.includes(opts.audience)) {
|
|
620
|
+
throw new Error(`Invalid --audience '${opts.audience}': must be human, agent, or both.`);
|
|
621
|
+
}
|
|
622
|
+
audience = opts.audience;
|
|
623
|
+
}
|
|
617
624
|
const payload = { body };
|
|
618
625
|
if (mentions.length > 0)
|
|
619
626
|
payload.mentions = mentions;
|
|
620
627
|
if (rootEventId)
|
|
621
628
|
payload.rootEventId = rootEventId;
|
|
629
|
+
if (audience)
|
|
630
|
+
payload.audience = audience;
|
|
622
631
|
const sendRes = await vaultApiFetch({
|
|
623
632
|
token,
|
|
624
633
|
path: `/v1/notify/channels/${encodeURIComponent(channel.channelId)}/messages`,
|
|
@@ -1327,7 +1336,7 @@ export function registerDmCommand(program) {
|
|
|
1327
1336
|
.option("--details-file <path>", "Read the details from a file (1:1 DMs only)")
|
|
1328
1337
|
.option("--at <iso>", "Schedule delivery at an ISO8601 time (store-and-forward; delivered within ~60s of the time; 1:1 DMs only)")
|
|
1329
1338
|
.option("--in <duration>", "Schedule delivery after a relative delay: 30s, 10m, 2h, 1d (1:1 DMs only)")
|
|
1330
|
-
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both
|
|
1339
|
+
.option("--audience <human|agent|both>", "Tag who this message is for: human (a person must read it), agent (progress/status, hidden from humans by default), or both")
|
|
1331
1340
|
.action(async (recipient, message, opts) => {
|
|
1332
1341
|
await runDmSend(recipient, message, opts);
|
|
1333
1342
|
});
|
|
@@ -272,6 +272,7 @@ export interface Ctx {
|
|
|
272
272
|
command: string;
|
|
273
273
|
timeoutSeconds?: number;
|
|
274
274
|
waitMs?: number;
|
|
275
|
+
beforeWait?: () => Promise<void>;
|
|
275
276
|
}) => Promise<OutpostDispatchResult>;
|
|
276
277
|
backfillOutpostIdentity: (expectedId: string) => "created" | "unchanged";
|
|
277
278
|
fetchText: (url: string) => Promise<string>;
|
package/dist/commands/lanes.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export interface LanesCommandDeps {
|
|
|
86
86
|
command: string;
|
|
87
87
|
timeoutSeconds?: number;
|
|
88
88
|
waitMs?: number;
|
|
89
|
+
beforeWait?: () => Promise<void>;
|
|
89
90
|
}) => Promise<OutpostDispatchResult>;
|
|
90
91
|
/** Test seam for the atomic, non-overwriting marker repair. */
|
|
91
92
|
backfillOutpostIdentity?: (expectedId: string) => "created" | "unchanged";
|
package/dist/commands/lanes.js
CHANGED
|
@@ -4317,11 +4317,22 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options, behavior =
|
|
|
4317
4317
|
let dispatched;
|
|
4318
4318
|
let grantContextRejected = false;
|
|
4319
4319
|
let failedResumeOptionRejected = false;
|
|
4320
|
+
let outpostWaitLeasePreparationStarted = false;
|
|
4320
4321
|
try {
|
|
4321
4322
|
const dispatchCommand = (contexts) => ctx.dispatchOutpostExec({
|
|
4322
4323
|
outpostId,
|
|
4323
4324
|
timeoutSeconds: Math.ceil(waitMs / 1000),
|
|
4324
4325
|
waitMs,
|
|
4326
|
+
...(action === "message" || action === "resume"
|
|
4327
|
+
? {
|
|
4328
|
+
beforeWait: async () => {
|
|
4329
|
+
if (outpostWaitLeasePreparationStarted)
|
|
4330
|
+
return;
|
|
4331
|
+
outpostWaitLeasePreparationStarted = true;
|
|
4332
|
+
await runResidentCommandWithPackageLease("outpost-wait", async () => undefined);
|
|
4333
|
+
},
|
|
4334
|
+
}
|
|
4335
|
+
: {}),
|
|
4325
4336
|
command: buildOutpostLaneCommand({
|
|
4326
4337
|
action: action === "grant"
|
|
4327
4338
|
? "grant-target"
|
|
@@ -237,7 +237,7 @@ export function buildAgentMcpTools(clients) {
|
|
|
237
237
|
const token = await clients.getToken();
|
|
238
238
|
const res = await clients.apiJson(token, `/v1/notify/channels/${encodeURIComponent(to)}/messages`, {
|
|
239
239
|
method: "POST",
|
|
240
|
-
body: { body: message },
|
|
240
|
+
body: { body: message, ...(audience ? { audience } : {}) },
|
|
241
241
|
});
|
|
242
242
|
if (res.status < 200 || res.status >= 300) {
|
|
243
243
|
return errorResult(`Channel send failed (${res.status}): ${describeError(res.body)}`);
|
package/dist/lib/bot/api.d.ts
CHANGED
package/dist/lib/bot/api.js
CHANGED
|
@@ -195,6 +195,8 @@ export class BotApi {
|
|
|
195
195
|
const payload = { body: input.body };
|
|
196
196
|
if (input.rootEventId)
|
|
197
197
|
payload.rootEventId = input.rootEventId;
|
|
198
|
+
if (input.audience)
|
|
199
|
+
payload.audience = input.audience;
|
|
198
200
|
const { body } = await this.call(`/v1/notify/channels/${encodeURIComponent(input.channelId)}/messages`, { method: "POST", body: payload });
|
|
199
201
|
return body;
|
|
200
202
|
}
|
|
@@ -28,13 +28,10 @@ export declare const PROGRESS_MIN_GAP_MS = 4000;
|
|
|
28
28
|
export declare const WORKING_NOTICE_MS = 60000;
|
|
29
29
|
export declare const WORKING_NOTICE_TEXT = "Working on it \u2014 I'll reply here when it's done.";
|
|
30
30
|
export interface ProgressPosterOptions {
|
|
31
|
-
/** Post
|
|
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);
|
|
@@ -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 {
|
|
@@ -714,6 +714,28 @@ function updaterPidAlive(lockPath, fs, isPidAlive) {
|
|
|
714
714
|
return "unknown";
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
|
+
/** Whether the lock names a live process other than this CLI invocation. */
|
|
718
|
+
function liveForeignUpdaterPid(lockPath, fs, isPidAlive) {
|
|
719
|
+
try {
|
|
720
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
721
|
+
if (typeof lock.startedAt !== "string" ||
|
|
722
|
+
Number.isNaN(Date.parse(lock.startedAt)) ||
|
|
723
|
+
typeof lock.tool !== "string" ||
|
|
724
|
+
lock.tool.length === 0 ||
|
|
725
|
+
typeof lock.version !== "string" ||
|
|
726
|
+
lock.version.length === 0) {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
return typeof lock.pid === "number" &&
|
|
730
|
+
Number.isSafeInteger(lock.pid) &&
|
|
731
|
+
lock.pid > 0 &&
|
|
732
|
+
lock.pid !== process.pid &&
|
|
733
|
+
isPidAlive(lock.pid);
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
717
739
|
/** Snapshot retired sibling trees so changes below their root stay observable. */
|
|
718
740
|
function retiredSiblingActivitySignature(packageRoot, fs) {
|
|
719
741
|
const { leaf } = cliNameParts();
|
|
@@ -917,7 +939,8 @@ const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
|
917
939
|
/**
|
|
918
940
|
* Poll until the install tree has been continuously READY for `quietMs`, or the
|
|
919
941
|
* wait exhausts its bounds. READY means: (i) no FRESH update lock held by another
|
|
920
|
-
* pid
|
|
942
|
+
* pid and no live recorded updater while the tree is in flight, (ii) the missing
|
|
943
|
+
* target is present, and — when `packageRoot` is known —
|
|
921
944
|
* (iii) `<packageRoot>/package.json` parses with `name === CLI_NAME` and (iv) no
|
|
922
945
|
* `.<leaf>-<rand>` retired sibling remains beside it. Every filesystem error makes
|
|
923
946
|
* its condition "not ready" rather than throwing.
|
|
@@ -934,20 +957,22 @@ const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
|
934
957
|
* any installer before attempting a repair. Any observed signature change
|
|
935
958
|
* keeps the ordinary settle deadline in force. `deadlineMs === 0` evaluates
|
|
936
959
|
* readiness once.
|
|
937
|
-
* -
|
|
938
|
-
* pid
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
* Each poll's REAL elapsed interval (now() deltas) is attributed to the
|
|
945
|
-
* unlocked bucket by whether a fresh
|
|
946
|
-
* began, so a starved event loop only makes the wait coarser,
|
|
960
|
+
* - WRITER time — wall time observed under a fresh foreign lock, or while a
|
|
961
|
+
* recorded foreign updater pid is alive and the tree has an in-flight signal
|
|
962
|
+
* — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
|
|
963
|
+
* 10-minute staleness window) and does NOT consume the settle budget. This
|
|
964
|
+
* also covers a still-running updater whose lock became stale by age. When
|
|
965
|
+
* that ceiling is reached, the result is unsettled; `lockHeldAtEnd` remains
|
|
966
|
+
* reserved for a fresh lock held at the end.
|
|
967
|
+
* Each poll's REAL elapsed interval (now() deltas) is attributed to the writer or
|
|
968
|
+
* unlocked bucket by whether a fresh lock or qualifying live updater was observed
|
|
969
|
+
* when the interval began, so a starved event loop only makes the wait coarser,
|
|
970
|
+
* never unbounded. A
|
|
947
971
|
* stale lock (dead pid, older than the staleness window, unparseable) or our own
|
|
948
|
-
* pid never extends anything. The whole wait is bounded (≤ `deadlineMs +
|
|
972
|
+
* pid never extends anything by itself. The whole wait is bounded (≤ `deadlineMs +
|
|
949
973
|
* lockWaitMs`), and the unchanged-tree path normally returns after the much
|
|
950
|
-
* shorter `staleProbeMs` observation window
|
|
974
|
+
* shorter `staleProbeMs` observation window unless a live updater pid is still
|
|
975
|
+
* present with an in-flight tree signal.
|
|
951
976
|
*/
|
|
952
977
|
export async function waitForInstallTreeSettled(args) {
|
|
953
978
|
const now = args.now ?? Date.now;
|
|
@@ -967,17 +992,21 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
967
992
|
let sawLock = false;
|
|
968
993
|
let sawRetired = false;
|
|
969
994
|
let lockedMs = 0;
|
|
995
|
+
let writerMs = 0;
|
|
970
996
|
let unlockedMs = 0;
|
|
971
997
|
let lockTool = "";
|
|
972
998
|
let prevT = start;
|
|
973
999
|
let prevForeignLockHeld = false;
|
|
1000
|
+
let prevWriterHeld = false;
|
|
974
1001
|
let lockExtendedFired = false;
|
|
975
1002
|
for (;;) {
|
|
976
1003
|
const t = now();
|
|
977
|
-
// Attribute the interval that just elapsed [prevT, t)
|
|
978
|
-
//
|
|
1004
|
+
// Attribute the interval that just elapsed [prevT, t) using the state that
|
|
1005
|
+
// held when it BEGAN (real now() deltas, never nominal pollMs).
|
|
979
1006
|
if (prevForeignLockHeld)
|
|
980
1007
|
lockedMs += t - prevT;
|
|
1008
|
+
if (prevWriterHeld)
|
|
1009
|
+
writerMs += t - prevT;
|
|
981
1010
|
else
|
|
982
1011
|
unlockedMs += t - prevT;
|
|
983
1012
|
const lock = freshForeignLock(args.lockPath, t, isPidAlive, fs);
|
|
@@ -1015,7 +1044,10 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
1015
1044
|
retiredCheckFailed = true;
|
|
1016
1045
|
}
|
|
1017
1046
|
}
|
|
1018
|
-
|
|
1047
|
+
const inFlightTree = sawLock || sawRetired || sawTreeChange;
|
|
1048
|
+
const updaterPidHeld = inFlightTree && liveForeignUpdaterPid(args.lockPath, fs, isPidAlive);
|
|
1049
|
+
const writerHeld = foreignLockHeld || updaterPidHeld;
|
|
1050
|
+
let ready = !writerHeld && installTargetPresent(args.target, fs);
|
|
1019
1051
|
if (ready && args.packageRoot) {
|
|
1020
1052
|
try {
|
|
1021
1053
|
if (!packageRootHealthy(args.packageRoot, fs))
|
|
@@ -1055,6 +1087,7 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
1055
1087
|
}
|
|
1056
1088
|
if (!ready &&
|
|
1057
1089
|
!foreignLockHeld &&
|
|
1090
|
+
!updaterPidHeld &&
|
|
1058
1091
|
!retiredCheckFailed &&
|
|
1059
1092
|
!sawLock &&
|
|
1060
1093
|
!sawTreeChange &&
|
|
@@ -1078,14 +1111,13 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
1078
1111
|
};
|
|
1079
1112
|
}
|
|
1080
1113
|
// Give up when the TREE has had a full unlocked budget with no writer visible,
|
|
1081
|
-
// OR a writer
|
|
1082
|
-
// ceiling
|
|
1083
|
-
// ceiling of 0
|
|
1084
|
-
//
|
|
1085
|
-
// deadlineMs settle when no lock exists — `lockedMs` is 0 ≥ 0 on every poll.
|
|
1114
|
+
// OR a qualifying writer has exceeded the shared writer ceiling. The writer
|
|
1115
|
+
// ceiling only extends while a fresh lock or in-flight live PID is present, so
|
|
1116
|
+
// a ceiling of 0 disables only that extension and does not collapse the normal
|
|
1117
|
+
// deadlineMs settle when no writer exists.
|
|
1086
1118
|
// `deadlineMs` 0 makes the unlocked test fire on the first poll (evaluate-once),
|
|
1087
1119
|
// matching the pre-lock-aware contract; `lockHeldAtEnd` distinguishes the causes.
|
|
1088
|
-
if (unlockedMs >= deadlineMs || (
|
|
1120
|
+
if (unlockedMs >= deadlineMs || (writerHeld && writerMs >= lockWaitMs)) {
|
|
1089
1121
|
return {
|
|
1090
1122
|
settled: false,
|
|
1091
1123
|
waitedMs,
|
|
@@ -1102,15 +1134,15 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
1102
1134
|
updaterPidAlive: updaterPidAlive(args.lockPath, fs, isPidAlive),
|
|
1103
1135
|
};
|
|
1104
1136
|
}
|
|
1105
|
-
//
|
|
1106
|
-
//
|
|
1107
|
-
|
|
1108
|
-
if (!lockExtendedFired && foreignLockHeld && waitedMs >= deadlineMs) {
|
|
1137
|
+
// Announce once when the deadline has passed and a qualifying writer is why
|
|
1138
|
+
// the bounded wait continues.
|
|
1139
|
+
if (!lockExtendedFired && writerHeld && waitedMs >= deadlineMs) {
|
|
1109
1140
|
lockExtendedFired = true;
|
|
1110
1141
|
args.onLockExtended?.();
|
|
1111
1142
|
}
|
|
1112
1143
|
prevT = t;
|
|
1113
1144
|
prevForeignLockHeld = foreignLockHeld;
|
|
1145
|
+
prevWriterHeld = writerHeld;
|
|
1114
1146
|
await sleep(pollMs);
|
|
1115
1147
|
}
|
|
1116
1148
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ResidentPackagePath = "agent-kit" | "agent-mcp" | "auth-login" | "agents-terminal" | "bot" | "bot-logs-follow" | "childWrapper" | "coreJobRun" | "login" | "mcp" | "daemon" | "daemon-logs-follow" | "mesh-daemon" | "lanes-watch" | "monitor-wait" | "monitor-run" | "agents-message-wait" | "integrations-oauth-wait" | "secrets-sandbox" | "pkg-update" | "pack-install";
|
|
1
|
+
export type ResidentPackagePath = "agent-kit" | "agent-mcp" | "auth-login" | "agents-terminal" | "bot" | "bot-logs-follow" | "childWrapper" | "coreJobRun" | "daemon-call" | "login" | "mcp" | "daemon" | "daemon-logs-follow" | "mesh-daemon" | "lanes-watch" | "monitor-wait" | "monitor-run" | "agents-message-wait" | "integrations-oauth-wait" | "outpost-wait" | "secrets-sandbox" | "pkg-update" | "pack-install";
|
|
2
2
|
/** Runtime modules that resident commands may need after a wait starts. */
|
|
3
3
|
export declare const RESIDENT_PACKAGE_MODULES: Record<ResidentPackagePath, readonly string[]>;
|
|
4
4
|
export declare class ResidentPackagePreloadError extends Error {
|
|
@@ -43,6 +43,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
43
43
|
mcp: ["../lib/mcp/tools.js", "../lib/mcp/prompts.js"],
|
|
44
44
|
childWrapper: ["../main.js"],
|
|
45
45
|
coreJobRun: ["../lib/flag-registry.js"],
|
|
46
|
+
"daemon-call": ["../lib/daemon/protocol.js"],
|
|
46
47
|
daemon: [
|
|
47
48
|
"../sentry.js",
|
|
48
49
|
"../utils/cognito-session.js",
|
|
@@ -63,6 +64,7 @@ export const RESIDENT_PACKAGE_MODULES = {
|
|
|
63
64
|
"monitor-run": ["../commands/lanes.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "../sentry.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
|
|
64
65
|
"agents-message-wait": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
|
|
65
66
|
"integrations-oauth-wait": ["../sentry.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
|
|
67
|
+
"outpost-wait": ["../commands/lanes.js", "../lib/plan-limit-nag.js", "../utils/plan-gate-error.js", "../sentry.js", "open", "../utils/cognito-session.js", "../lib/first-sync-next-step.js", "@indigoai-us/hq-cloud"],
|
|
66
68
|
"secrets-sandbox": [],
|
|
67
69
|
"pkg-update": ["../sentry.js", "../utils/cognito-session.js", "@indigoai-us/hq-cloud"],
|
|
68
70
|
"pack-install": [
|
|
@@ -175,6 +177,9 @@ const LOADERS = {
|
|
|
175
177
|
coreJobRun: [
|
|
176
178
|
() => import("../lib/flag-registry.js"),
|
|
177
179
|
],
|
|
180
|
+
"daemon-call": [
|
|
181
|
+
() => import("../lib/daemon/protocol.js"),
|
|
182
|
+
],
|
|
178
183
|
daemon: [
|
|
179
184
|
async () => {
|
|
180
185
|
const { preloadSentrySdk } = await import("../sentry.js");
|
|
@@ -265,6 +270,19 @@ const LOADERS = {
|
|
|
265
270
|
() => import("../lib/first-sync-next-step.js"),
|
|
266
271
|
() => import("@indigoai-us/hq-cloud"),
|
|
267
272
|
],
|
|
273
|
+
"outpost-wait": [
|
|
274
|
+
() => import("../commands/lanes.js"),
|
|
275
|
+
() => import("../lib/plan-limit-nag.js"),
|
|
276
|
+
() => import("../utils/plan-gate-error.js"),
|
|
277
|
+
async () => {
|
|
278
|
+
const { preloadSentrySdk } = await import("../sentry.js");
|
|
279
|
+
preloadSentrySdk();
|
|
280
|
+
},
|
|
281
|
+
() => import("open"),
|
|
282
|
+
() => import("../utils/cognito-session.js"),
|
|
283
|
+
() => import("../lib/first-sync-next-step.js"),
|
|
284
|
+
() => import("@indigoai-us/hq-cloud"),
|
|
285
|
+
],
|
|
268
286
|
"secrets-sandbox": [],
|
|
269
287
|
"pkg-update": [
|
|
270
288
|
async () => {
|