@indigoai-us/hq-cli 5.283.0 → 5.284.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 +10 -0
- package/dist/commands/lanes.js +11 -7
- package/dist/lib/lanes/outpost-dispatch.d.ts +8 -0
- package/dist/lib/lanes/outpost-dispatch.js +10 -0
- package/dist/lib/mesh/live/daemon/credentials.js +8 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +143 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.284.0] — 2026-09-29
|
|
6
|
+
- The existing Work Mesh MQTT subscriber can signal agent inbox wakes through a private local marker without persisting MQTT message contents.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- A remote `hq lanes list` read no longer dispatches owner hire summaries,
|
|
11
|
+
capacity probes, or mirror snapshots to other Outposts. Two Outposts that
|
|
12
|
+
each held a lane placed on the other ran `hq lanes list --json` on each other
|
|
13
|
+
without end, which filled both boxes with SSM commands.
|
|
14
|
+
|
|
5
15
|
## [5.283.0] — 2026-09-29
|
|
6
16
|
|
|
7
17
|
### Added
|
package/dist/commands/lanes.js
CHANGED
|
@@ -37,7 +37,7 @@ import { createOutpostProfileRepinContext, parseOutpostProfileRepinContext, } fr
|
|
|
37
37
|
import { createOutpostRegistryWorkerApprovalContext, parseOutpostRegistryWorkerApprovalContext, } from "../lib/lanes/outpost-registry-worker-approval.js";
|
|
38
38
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
39
39
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
40
|
-
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, outpostRejectedEffortOption, parseOutpostPickupPayload, PICKUP_WAIT_MS, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
40
|
+
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostQuestionListCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, REMOTE_LANE_READ_ENV, outpostRejectedEffortOption, parseOutpostPickupPayload, PICKUP_WAIT_MS, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
41
41
|
import { outpostHireSummaryCacheKey, LANE_OUTPOST_SNAPSHOT_CACHE_FLAG, OUTPOST_HIRE_SUMMARY_CACHE_TTL_MS, readOutpostHireSummaryCache, readOutpostHireSummaryCacheIncludingStale, writeOutpostHireSummaryCache, } from "../lib/lanes/outpost-hire-cache.js";
|
|
42
42
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
43
43
|
import { BALANCE_MOVABLE_STATES, selectClaudeAccount, } from "../lib/lanes/claude-balance.js";
|
|
@@ -4543,9 +4543,12 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4543
4543
|
});
|
|
4544
4544
|
return;
|
|
4545
4545
|
}
|
|
4546
|
-
const
|
|
4547
|
-
|
|
4548
|
-
|
|
4546
|
+
const remoteRead = ctx.env[REMOTE_LANE_READ_ENV] === "1";
|
|
4547
|
+
const remoteRefresh = remoteRead
|
|
4548
|
+
? { updated: new Map(), unverifiedRemote: new Map() }
|
|
4549
|
+
: await refreshRemoteLaneMirrors(ctx, hqRoot, laneSource, new Set(selectedLanes
|
|
4550
|
+
.filter((lane) => !isTerminalLaneState(lane.state))
|
|
4551
|
+
.map((lane) => lane.lane_id)));
|
|
4549
4552
|
for (const [laneId, updated] of remoteRefresh.updated) {
|
|
4550
4553
|
const index = laneSource.findIndex((lane) => lane.lane_id === laneId);
|
|
4551
4554
|
if (index >= 0)
|
|
@@ -4563,7 +4566,8 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4563
4566
|
}
|
|
4564
4567
|
const ownerEligibleLaneIds = new Set();
|
|
4565
4568
|
for (const lane of selectedLanes) {
|
|
4566
|
-
if (
|
|
4569
|
+
if (remoteRead ||
|
|
4570
|
+
lane.host.kind !== "outpost" ||
|
|
4567
4571
|
isTerminalLaneState(lane.state) ||
|
|
4568
4572
|
laneRunsHere(lane, host)) {
|
|
4569
4573
|
continue;
|
|
@@ -4713,8 +4717,8 @@ async function runList(ctx, opts, fastRouteFlags) {
|
|
|
4713
4717
|
};
|
|
4714
4718
|
});
|
|
4715
4719
|
const placementCompany = company ?? processLanePlacementCompany(ctx);
|
|
4716
|
-
const placementEnabled = fastRouteFlags?.lanePlacementEnabled ??
|
|
4717
|
-
await readLanePlacementEnabled(ctx, placementCompany);
|
|
4720
|
+
const placementEnabled = !remoteRead && (fastRouteFlags?.lanePlacementEnabled ??
|
|
4721
|
+
await readLanePlacementEnabled(ctx, placementCompany));
|
|
4718
4722
|
const host_capacity = placementEnabled
|
|
4719
4723
|
? (await loadHostCapacitySnapshots(ctx, hqRoot, undefined, allLanes)).entries
|
|
4720
4724
|
: undefined;
|
|
@@ -81,6 +81,14 @@ export declare function parseOutpostPickupPayload(raw: unknown): OutpostPickupPa
|
|
|
81
81
|
export declare function buildOutpostPickupCommand(payloadUrl: string): string;
|
|
82
82
|
/** Bounded, read-only capacity query executed by the target Outpost CLI. */
|
|
83
83
|
export declare function buildOutpostCapacityCommand(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Marks a lane list that runs on an Outpost for another host. Such a list
|
|
86
|
+
* answers from the box's own records and never dispatches to other Outposts:
|
|
87
|
+
* two boxes that each hold a lane placed on the other would otherwise run
|
|
88
|
+
* lists on each other without end. `env` keeps older CLIs working, since they
|
|
89
|
+
* ignore the variable instead of rejecting an unknown flag.
|
|
90
|
+
*/
|
|
91
|
+
export declare const REMOTE_LANE_READ_ENV = "HQ_LANES_REMOTE_READ";
|
|
84
92
|
/** Read lane records owned by an Outpost, using its configured HQ binary when set. */
|
|
85
93
|
export declare function buildOutpostLaneListCommand(input?: {
|
|
86
94
|
hqBinary?: string;
|
|
@@ -65,9 +65,19 @@ export function buildOutpostPickupCommand(payloadUrl) {
|
|
|
65
65
|
export function buildOutpostCapacityCommand() {
|
|
66
66
|
return withRemoteHqDir(joinCommandParts(["hq", "lanes", "capacity", "--json"]));
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Marks a lane list that runs on an Outpost for another host. Such a list
|
|
70
|
+
* answers from the box's own records and never dispatches to other Outposts:
|
|
71
|
+
* two boxes that each hold a lane placed on the other would otherwise run
|
|
72
|
+
* lists on each other without end. `env` keeps older CLIs working, since they
|
|
73
|
+
* ignore the variable instead of rejecting an unknown flag.
|
|
74
|
+
*/
|
|
75
|
+
export const REMOTE_LANE_READ_ENV = "HQ_LANES_REMOTE_READ";
|
|
68
76
|
/** Read lane records owned by an Outpost, using its configured HQ binary when set. */
|
|
69
77
|
export function buildOutpostLaneListCommand(input = {}) {
|
|
70
78
|
const args = [
|
|
79
|
+
"env",
|
|
80
|
+
`${REMOTE_LANE_READ_ENV}=1`,
|
|
71
81
|
input.hqBinary?.trim() || "hq",
|
|
72
82
|
"lanes",
|
|
73
83
|
"list",
|
|
@@ -562,13 +562,21 @@ export function normalizePersonalRealtimeBundle(raw) {
|
|
|
562
562
|
}
|
|
563
563
|
if (!topics.dm && typeof r.topic === "string")
|
|
564
564
|
topics.dm = r.topic;
|
|
565
|
+
const agentInboxTopic = typeof r.agentInboxTopic === "string" ? r.agentInboxTopic.trim() : "";
|
|
565
566
|
const actorUid = (typeof r.principalUid === "string" && /^(?:prs|agt)_[A-Za-z0-9]+$/.test(r.principalUid)
|
|
566
567
|
? r.principalUid
|
|
567
568
|
: undefined) ||
|
|
568
569
|
actorUidFromPersonalTopic(topics.dm) ||
|
|
570
|
+
actorUidFromPersonalTopic(agentInboxTopic) ||
|
|
569
571
|
"";
|
|
570
572
|
if (!actorUid)
|
|
571
573
|
throw new Error("contract-2 vend could not derive actorUid");
|
|
574
|
+
// hq-pro advertises the agent's own inbox topic as a top-level capability.
|
|
575
|
+
// Keep it only for the exact actor-owned topic; it reuses this personal MQTT
|
|
576
|
+
// session and never grants the subscriber a topic supplied by another actor.
|
|
577
|
+
if (actorUid.startsWith("agt_") && agentInboxTopic === `hq/${actorUid}/inbox`) {
|
|
578
|
+
topics.inbox = agentInboxTopic;
|
|
579
|
+
}
|
|
572
580
|
// Only keep topics that belong to the vended principal.
|
|
573
581
|
for (const [k, v] of Object.entries(topics)) {
|
|
574
582
|
if (actorUidFromPersonalTopic(v) !== actorUid)
|
|
@@ -14,6 +14,7 @@ export interface WorkCacheMqtt {
|
|
|
14
14
|
export interface WorkCacheSubscriber {
|
|
15
15
|
stop: () => Promise<void>;
|
|
16
16
|
}
|
|
17
|
+
export declare const AGENT_INBOX_PUSH_STATE_BASENAME = "agent-inbox-push.json";
|
|
17
18
|
export declare function startWorkCacheSubscriber(opts: {
|
|
18
19
|
meshRoot: string;
|
|
19
20
|
fetchCredentials?: PersonalCredentialsFetcher;
|
|
@@ -2,10 +2,57 @@
|
|
|
2
2
|
* Contract-2 subscriber for the personal work topic.
|
|
3
3
|
* Owns live-cache.json writes. Presence publish stays on PresenceClient.
|
|
4
4
|
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
5
7
|
import { createPersonalRealtimeFetcher, } from "./credentials.js";
|
|
6
8
|
import { presignIotWssUrl } from "./presign.js";
|
|
7
9
|
import { applyProjectViewPush, parseProjectViewPush } from "./live-cache.js";
|
|
8
10
|
import { backoffDelayMs, RECONNECT_MIN_DELAY_MS } from "./presence.js";
|
|
11
|
+
export const AGENT_INBOX_PUSH_STATE_BASENAME = "agent-inbox-push.json";
|
|
12
|
+
const INBOX_PUSH_HEARTBEAT_MS = 5_000;
|
|
13
|
+
function readInboxWakeSequence(meshRoot, onInfo) {
|
|
14
|
+
const target = path.join(meshRoot, AGENT_INBOX_PUSH_STATE_BASENAME);
|
|
15
|
+
try {
|
|
16
|
+
const info = fs.lstatSync(target);
|
|
17
|
+
if (!info.isFile() || info.isSymbolicLink()) {
|
|
18
|
+
onInfo?.("agent inbox push state read ignored (not a regular file)");
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
22
|
+
if (Number.isSafeInteger(parsed.wakeSequence) && Number(parsed.wakeSequence) >= 0) {
|
|
23
|
+
return Number(parsed.wakeSequence);
|
|
24
|
+
}
|
|
25
|
+
onInfo?.("agent inbox push state read ignored (invalid wake sequence)");
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== "ENOENT") {
|
|
30
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
31
|
+
onInfo?.(`agent inbox push state read failed (${name})`);
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function writeInboxPushState(meshRoot, state) {
|
|
37
|
+
fs.mkdirSync(meshRoot, { recursive: true, mode: 0o700 });
|
|
38
|
+
const target = path.join(meshRoot, AGENT_INBOX_PUSH_STATE_BASENAME);
|
|
39
|
+
const temporary = `${target}.${process.pid}.tmp`;
|
|
40
|
+
try {
|
|
41
|
+
fs.writeFileSync(temporary, `${JSON.stringify(state)}\n`, { mode: 0o600 });
|
|
42
|
+
fs.chmodSync(temporary, 0o600);
|
|
43
|
+
fs.renameSync(temporary, target);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
try {
|
|
47
|
+
fs.unlinkSync(temporary);
|
|
48
|
+
}
|
|
49
|
+
catch (cleanupError) {
|
|
50
|
+
if (cleanupError.code !== "ENOENT")
|
|
51
|
+
throw cleanupError;
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
9
56
|
export async function startWorkCacheSubscriber(opts) {
|
|
10
57
|
const fetchCredentials = opts.fetchCredentials ??
|
|
11
58
|
(opts.token
|
|
@@ -28,10 +75,48 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
28
75
|
let reconnectLogged = false;
|
|
29
76
|
let attempt = 0;
|
|
30
77
|
let timer = null;
|
|
78
|
+
let heartbeatTimer = null;
|
|
31
79
|
let generation = 0;
|
|
80
|
+
let inboxSubscribed = false;
|
|
81
|
+
// Carry the sequence forward from the last state marker so a daemon restart
|
|
82
|
+
// cannot reuse a value the watcher has already consumed.
|
|
83
|
+
let wakeSequence = readInboxWakeSequence(opts.meshRoot, opts.onInfo);
|
|
84
|
+
const clearInboxHeartbeat = () => {
|
|
85
|
+
if (heartbeatTimer !== null)
|
|
86
|
+
timers.clearTimeout(heartbeatTimer);
|
|
87
|
+
heartbeatTimer = null;
|
|
88
|
+
};
|
|
89
|
+
const persistInboxState = (connected) => {
|
|
90
|
+
try {
|
|
91
|
+
writeInboxPushState(opts.meshRoot, {
|
|
92
|
+
connected,
|
|
93
|
+
heartbeatAtMs: now().getTime(),
|
|
94
|
+
wakeSequence,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
99
|
+
opts.onInfo?.(`agent inbox push state write failed (${name})`);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const scheduleInboxHeartbeat = () => {
|
|
103
|
+
if (stopped || !inboxSubscribed)
|
|
104
|
+
return;
|
|
105
|
+
clearInboxHeartbeat();
|
|
106
|
+
heartbeatTimer = timers.setTimeout(() => {
|
|
107
|
+
heartbeatTimer = null;
|
|
108
|
+
if (!stopped && inboxSubscribed) {
|
|
109
|
+
persistInboxState(true);
|
|
110
|
+
scheduleInboxHeartbeat();
|
|
111
|
+
}
|
|
112
|
+
}, INBOX_PUSH_HEARTBEAT_MS);
|
|
113
|
+
};
|
|
32
114
|
const connect = async () => {
|
|
33
115
|
if (stopped)
|
|
34
116
|
return;
|
|
117
|
+
inboxSubscribed = false;
|
|
118
|
+
clearInboxHeartbeat();
|
|
119
|
+
persistInboxState(false);
|
|
35
120
|
const gen = ++generation;
|
|
36
121
|
let bundle;
|
|
37
122
|
try {
|
|
@@ -44,8 +129,12 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
44
129
|
if (stopped || gen !== generation)
|
|
45
130
|
return;
|
|
46
131
|
const topic = bundle.topics.work;
|
|
47
|
-
|
|
48
|
-
|
|
132
|
+
const inboxTopic = bundle.actorUid.startsWith("agt_") &&
|
|
133
|
+
bundle.topics.inbox === `hq/${bundle.actorUid}/inbox`
|
|
134
|
+
? bundle.topics.inbox
|
|
135
|
+
: undefined;
|
|
136
|
+
if (!topic && !inboxTopic) {
|
|
137
|
+
opts.onInfo?.("work cache subscribe skipped: credentials have no supported topic");
|
|
49
138
|
return;
|
|
50
139
|
}
|
|
51
140
|
const url = presignIotWssUrl(bundle.credentials, bundle.iotEndpoint, bundle.region, now());
|
|
@@ -60,12 +149,55 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
60
149
|
if (stopped || gen !== generation)
|
|
61
150
|
return;
|
|
62
151
|
reconnectLogged = false;
|
|
63
|
-
|
|
64
|
-
|
|
152
|
+
let pendingSubscriptions = Number(Boolean(topic)) + Number(Boolean(inboxTopic));
|
|
153
|
+
let subscriptionFailed = false;
|
|
154
|
+
const finishSubscription = (err) => {
|
|
155
|
+
if (err) {
|
|
156
|
+
if (subscriptionFailed)
|
|
157
|
+
return;
|
|
158
|
+
subscriptionFailed = true;
|
|
159
|
+
schedule(err);
|
|
160
|
+
next.end(true);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (subscriptionFailed)
|
|
164
|
+
return;
|
|
165
|
+
pendingSubscriptions -= 1;
|
|
166
|
+
if (pendingSubscriptions === 0)
|
|
167
|
+
attempt = 0;
|
|
168
|
+
};
|
|
169
|
+
if (topic) {
|
|
170
|
+
next.subscribe(topic, { qos: 1 }, (err) => finishSubscription(err));
|
|
171
|
+
}
|
|
172
|
+
if (inboxTopic) {
|
|
173
|
+
next.subscribe(inboxTopic, { qos: 1 }, (err) => {
|
|
174
|
+
if (stopped || gen !== generation)
|
|
175
|
+
return;
|
|
176
|
+
if (err) {
|
|
177
|
+
inboxSubscribed = false;
|
|
178
|
+
clearInboxHeartbeat();
|
|
179
|
+
persistInboxState(false);
|
|
180
|
+
opts.onInfo?.("agent inbox push subscribe failed");
|
|
181
|
+
finishSubscription(err);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
inboxSubscribed = true;
|
|
185
|
+
persistInboxState(true);
|
|
186
|
+
scheduleInboxHeartbeat();
|
|
187
|
+
finishSubscription();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
65
190
|
});
|
|
66
191
|
next.on("message", (_topic, payload) => {
|
|
67
192
|
if (stopped || gen !== generation)
|
|
68
193
|
return;
|
|
194
|
+
if (inboxTopic && _topic === inboxTopic && inboxSubscribed) {
|
|
195
|
+
// A topic message is a doorbell only. Never parse, persist, or log its
|
|
196
|
+
// content; the authenticated inbox GET remains the source of truth.
|
|
197
|
+
wakeSequence += 1;
|
|
198
|
+
persistInboxState(true);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
69
201
|
if (_topic !== topic)
|
|
70
202
|
return;
|
|
71
203
|
const push = parseProjectViewPush(payload);
|
|
@@ -79,11 +211,14 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
79
211
|
next.on("close", () => {
|
|
80
212
|
if (stopped || gen !== generation)
|
|
81
213
|
return;
|
|
214
|
+
inboxSubscribed = false;
|
|
215
|
+
clearInboxHeartbeat();
|
|
216
|
+
persistInboxState(false);
|
|
82
217
|
schedule();
|
|
83
218
|
});
|
|
84
219
|
};
|
|
85
220
|
const schedule = (err) => {
|
|
86
|
-
if (stopped)
|
|
221
|
+
if (stopped || timer !== null)
|
|
87
222
|
return;
|
|
88
223
|
if (!reconnectLogged) {
|
|
89
224
|
reconnectLogged = true;
|
|
@@ -106,6 +241,9 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
106
241
|
generation += 1;
|
|
107
242
|
if (timer !== null)
|
|
108
243
|
timers.clearTimeout(timer);
|
|
244
|
+
clearInboxHeartbeat();
|
|
245
|
+
inboxSubscribed = false;
|
|
246
|
+
persistInboxState(false);
|
|
109
247
|
const current = client;
|
|
110
248
|
client = null;
|
|
111
249
|
if (!current)
|