@indigoai-us/hq-cli 5.154.0 → 5.156.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 +48 -0
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/lanes.d.ts +3 -11
- package/dist/commands/lanes.js +162 -55
- package/dist/commands/mesh.js +35 -8
- package/dist/lib/lanes/accounts.d.ts +16 -11
- package/dist/lib/lanes/accounts.js +50 -8
- package/dist/lib/lanes/admission-host.d.ts +19 -0
- package/dist/lib/lanes/admission-host.js +33 -0
- package/dist/lib/lanes/config.js +25 -0
- package/dist/lib/lanes/mesh-events.d.ts +7 -3
- package/dist/lib/lanes/mesh-events.js +116 -20
- package/dist/lib/lanes/provider-rejection.d.ts +4 -4
- package/dist/lib/lanes/provider-rejection.js +24 -6
- package/dist/lib/lanes/reconcile.js +85 -2
- package/dist/lib/lanes/spawn.d.ts +9 -1
- package/dist/lib/lanes/spawn.js +420 -14
- package/dist/lib/lanes/types.d.ts +7 -3
- package/dist/lib/lanes/types.js +3 -2
- package/dist/lib/lanes/watcher.d.ts +8 -1
- package/dist/lib/lanes/watcher.js +66 -17
- package/dist/lib/mesh/api.d.ts +27 -1
- package/dist/lib/mesh/api.js +41 -12
- package/dist/lib/mesh/project-board.d.ts +1 -0
- package/dist/lib/mesh/project-board.js +1 -1
- package/package.json +1 -1
|
@@ -40,12 +40,13 @@ import { laneStories, storyStatusForEnvelope } from "./stories.js";
|
|
|
40
40
|
import { readLane, retryUpdateLane } from "./store.js";
|
|
41
41
|
import { readWorkerRunResult } from "./delivery.js";
|
|
42
42
|
import { applyDefaultQuotaHold, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
43
|
-
import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
43
|
+
import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, isProviderLoginFailure, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
|
|
44
44
|
import { readToolActivity, toolActivityPath } from "./dropbox.js";
|
|
45
45
|
import { loadLanesConfig, providerProgressEventMinIntervalMs, providerQuotaDefaultHoldMs } from "./config.js";
|
|
46
|
-
import {
|
|
46
|
+
import { resolveAdmissionHost as resolveProcessAdmissionHost } from "./admission-host.js";
|
|
47
|
+
import { createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, emitLaneTerminalStateEvent, progressEventDue, } from "./mesh-events.js";
|
|
47
48
|
import { envelopeQuestionId } from "./questions.js";
|
|
48
|
-
import { laneRunDir, missingPidProbe, probePidRecord, readPidRecord, readProcStarttime, readSpawnManifest, envelopeRepairMode, spawnEnvelopeRepair, } from "./spawn.js";
|
|
49
|
+
import { laneRunDir, missingPidProbe, probePidRecord, readPidRecord, readProcStarttime, readSpawnManifest, envelopeRepairMode, spawnEnvelopeRepair, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
49
50
|
/** Spec §5.7: no signal for 20 minutes while the process is still alive. */
|
|
50
51
|
export const SILENCE_MS = 20 * 60 * 1000;
|
|
51
52
|
export const WATCHER_POLL_MS = 5_000;
|
|
@@ -89,7 +90,12 @@ export function classifyLaneExit(input) {
|
|
|
89
90
|
throw new LanesError("unknown_envelope_decision", `Unknown envelope decision ${JSON.stringify(input.envelope.decision)}. Accepted: done, ask, blocked`, { accepted: ["done", "ask", "blocked"], value: input.envelope.decision });
|
|
90
91
|
}
|
|
91
92
|
if (input.providerRejection) {
|
|
92
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
state: "failed",
|
|
95
|
+
reason: isProviderLoginFailure(input.providerRejection)
|
|
96
|
+
? "account_not_logged_in"
|
|
97
|
+
: PROVIDER_REJECTED_REASON,
|
|
98
|
+
};
|
|
93
99
|
}
|
|
94
100
|
if (input.invalidEnvelope) {
|
|
95
101
|
return { state: "failed", reason: ENVELOPE_INVALID_REASON };
|
|
@@ -580,20 +586,27 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
580
586
|
const meshRequestTimeoutMs = deps.meshRequestTimeoutMs ?? 10_000;
|
|
581
587
|
const initialLane = readLaneSoft(hqRoot, laneId);
|
|
582
588
|
if (mesh && initialLane) {
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
589
|
+
// Every armed watcher refreshes the server descriptor. This migrates old
|
|
590
|
+
// shared project threads and picks up account/model changes after resume.
|
|
591
|
+
const company = initialLane.mesh_company ?? initialLane.company ?? undefined;
|
|
592
|
+
if (company &&
|
|
593
|
+
(mesh.ensureLaneThread || !initialLane.thread_id || initialLane.thread_id.startsWith("proj-"))) {
|
|
588
594
|
let timer;
|
|
589
595
|
try {
|
|
590
596
|
const threadId = await Promise.race([
|
|
591
|
-
mesh.
|
|
597
|
+
mesh.ensureLaneThread
|
|
598
|
+
? mesh.ensureLaneThread(company, initialLane)
|
|
599
|
+
: mesh.ensureProjectThread(company, initialLane.project_id),
|
|
592
600
|
new Promise((_resolve, reject) => {
|
|
593
601
|
timer = setTimeout(() => reject(new Error(`mesh thread ensure timed out after ${meshRequestTimeoutMs}ms`)), meshRequestTimeoutMs);
|
|
594
602
|
}),
|
|
595
603
|
]);
|
|
596
|
-
|
|
604
|
+
const updated = initialLane.thread_id === threadId
|
|
605
|
+
? initialLane
|
|
606
|
+
: retryUpdateLane(hqRoot, laneId, "spawn", (current) => current.thread_id === threadId ? current : { ...current, thread_id: threadId });
|
|
607
|
+
if (mesh.ensureLaneThread && updated.version !== initialLane.version) {
|
|
608
|
+
await mesh.ensureLaneThread(company, updated);
|
|
609
|
+
}
|
|
597
610
|
}
|
|
598
611
|
catch (err) {
|
|
599
612
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -601,7 +614,7 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
601
614
|
writeWatcherDecision(outPath, {
|
|
602
615
|
event: "mesh_event_failed",
|
|
603
616
|
at: new Date(now()).toISOString(),
|
|
604
|
-
mesh_event: "
|
|
617
|
+
mesh_event: "ensure_lane_thread",
|
|
605
618
|
error: message,
|
|
606
619
|
}, outMax);
|
|
607
620
|
}
|
|
@@ -613,6 +626,10 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
613
626
|
}
|
|
614
627
|
const progressIntervalMs = deps.progressEventMinIntervalMs ?? readProgressIntervalMs(hqRoot, readLaneSoft(hqRoot, laneId));
|
|
615
628
|
const spawnRepair = deps.spawnRepair ?? spawnEnvelopeRepair;
|
|
629
|
+
const retryAccount = deps.retryAccount ?? ((root, lane, reason) => retryLaneOnAlternateAccount(root, lane, reason, "watcher", {
|
|
630
|
+
...deps.retrySpawnDeps,
|
|
631
|
+
admissionHost: (deps.resolveAdmissionHost ?? resolveProcessAdmissionHost)(),
|
|
632
|
+
}));
|
|
616
633
|
let lastSignalMs = now();
|
|
617
634
|
let lastSeenMtime = 0;
|
|
618
635
|
let lastAlive = null;
|
|
@@ -836,6 +853,32 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
836
853
|
process.stderr.write(`hq lanes watcher: quota record failed for ${laneId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
837
854
|
}
|
|
838
855
|
}
|
|
856
|
+
const accountRetryReason = quota
|
|
857
|
+
? "usage_limit"
|
|
858
|
+
: isProviderLoginFailure(providerRejection)
|
|
859
|
+
? "account_unavailable"
|
|
860
|
+
: null;
|
|
861
|
+
if (accountRetryReason && quotaLane?.account) {
|
|
862
|
+
try {
|
|
863
|
+
const retry = await retryAccount(hqRoot, quotaLane, accountRetryReason);
|
|
864
|
+
logDecision({
|
|
865
|
+
event: "account_failover",
|
|
866
|
+
at: tick.emit.at,
|
|
867
|
+
from: quotaLane.account,
|
|
868
|
+
to: retry.account ?? null,
|
|
869
|
+
reason: accountRetryReason,
|
|
870
|
+
state: retry.state,
|
|
871
|
+
});
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
catch (err) {
|
|
875
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
876
|
+
process.stderr.write(`hq lanes watcher: account failover failed for ${laneId}: ${message}\n`);
|
|
877
|
+
logDecision({ event: "account_failover_failed", at: tick.emit.at, reason: accountRetryReason, error: message });
|
|
878
|
+
if (readLane(hqRoot, laneId).state !== "running")
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
839
882
|
const terminalWrite = applyTerminalToLaneWithResult(hqRoot, laneId, classification, {
|
|
840
883
|
envelope,
|
|
841
884
|
envelopeRef: latest?.ref,
|
|
@@ -853,11 +896,17 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
853
896
|
// Spec §8 terminal rows: done → lane_done + story review; blocked →
|
|
854
897
|
// lane_blocked; ask → lane_ask with the queue ids. After the state
|
|
855
898
|
// write, so a mesh failure can never leave the lane unclassified.
|
|
856
|
-
if (mesh &&
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
899
|
+
if (mesh && terminalWrite.changed) {
|
|
900
|
+
if (envelope && latest) {
|
|
901
|
+
const questionIds = envelope.decision === "ask"
|
|
902
|
+
? envelopeQuestionIds(laneId, latest.ref, envelope)
|
|
903
|
+
: undefined;
|
|
904
|
+
await emitMeshSoft(`lane_${envelope.decision}`, logDecision, tick.emit.at, (signal) => emitLaneDecisionEvents(mesh, updated, envelope, { questionIds, signal }), meshRequestTimeoutMs);
|
|
905
|
+
}
|
|
906
|
+
else if (updated.state === "failed" || updated.state === "interrupted" ||
|
|
907
|
+
updated.state === "cancelled" || updated.state === "killed") {
|
|
908
|
+
await emitMeshSoft(`lane_${updated.state}`, logDecision, tick.emit.at, (signal) => emitLaneTerminalStateEvent(mesh, updated, updated.state, classification.reason, signal), meshRequestTimeoutMs);
|
|
909
|
+
}
|
|
861
910
|
}
|
|
862
911
|
}
|
|
863
912
|
}
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -16,13 +16,35 @@ export interface MeshThread {
|
|
|
16
16
|
projectId?: string;
|
|
17
17
|
companyUid?: string;
|
|
18
18
|
threadStatus?: string;
|
|
19
|
+
kind?: "project" | "work-session" | "lane" | string;
|
|
20
|
+
lane?: MeshLaneDescriptor;
|
|
21
|
+
laneStatus?: MeshLaneStatus;
|
|
22
|
+
projectChannelId?: string;
|
|
19
23
|
ownerUid?: string;
|
|
24
|
+
ownerType?: "human" | "agent";
|
|
25
|
+
senderMachineUid?: string;
|
|
26
|
+
senderPrincipalType?: "agent" | "outpost";
|
|
27
|
+
senderAuthenticatedSub?: string;
|
|
20
28
|
progressSummary?: string;
|
|
21
29
|
sourceSignalSummary?: string;
|
|
22
30
|
blockedReason?: string;
|
|
23
31
|
lastActivityAt?: string;
|
|
24
32
|
createdAt?: string;
|
|
25
33
|
}
|
|
34
|
+
export type MeshLaneStatus = "queued" | "running" | "awaiting_input" | "blocked" | "done" | "failed" | "interrupted" | "cancelled" | "killed";
|
|
35
|
+
export interface MeshLaneDescriptor {
|
|
36
|
+
laneId: string;
|
|
37
|
+
storyId?: string;
|
|
38
|
+
workerProfile?: string;
|
|
39
|
+
model?: string;
|
|
40
|
+
account?: string;
|
|
41
|
+
parentLaneId?: string;
|
|
42
|
+
senior?: {
|
|
43
|
+
kind: "user" | "session" | "lane";
|
|
44
|
+
id?: string;
|
|
45
|
+
};
|
|
46
|
+
laneVersion: number;
|
|
47
|
+
}
|
|
26
48
|
export interface MeshProjectStoryInput {
|
|
27
49
|
id: string;
|
|
28
50
|
title: string;
|
|
@@ -101,6 +123,10 @@ export declare function ensureProjectThread(token: string, company: MeshCompany,
|
|
|
101
123
|
threadId: string;
|
|
102
124
|
created: boolean;
|
|
103
125
|
}>;
|
|
126
|
+
/** Ensure the server-owned, stable Work Mesh thread for one lane. */
|
|
127
|
+
export declare function ensureLaneMeshThread(token: string, company: MeshCompany, projectId: string, lane: MeshLaneDescriptor, laneStatus: MeshLaneStatus, signal?: AbortSignal): Promise<{
|
|
128
|
+
threadId: string;
|
|
129
|
+
}>;
|
|
104
130
|
export declare function appendThreadEvent(token: string, companyUid: string, threadId: string, eventKind: string, payload: Record<string, unknown>, signal?: AbortSignal): Promise<{
|
|
105
131
|
eventId?: string;
|
|
106
132
|
createdAt?: string;
|
|
@@ -138,7 +164,7 @@ export declare function putProjectView(token: string, companyUid: string, projec
|
|
|
138
164
|
expectedVersion?: number;
|
|
139
165
|
}): Promise<MeshProjectView>;
|
|
140
166
|
/** Register the project genesis thread and deterministic project channel. */
|
|
141
|
-
export declare function registerMeshProject(token: string, companyUid: string, projectId: string): Promise<MeshProjectRegistration>;
|
|
167
|
+
export declare function registerMeshProject(token: string, companyUid: string, projectId: string, channelId?: string): Promise<MeshProjectRegistration>;
|
|
142
168
|
export declare function pairDmUidsFromInbox(inbox: unknown): string[];
|
|
143
169
|
export interface WarmCacheResult {
|
|
144
170
|
directory: boolean;
|
package/dist/lib/mesh/api.js
CHANGED
|
@@ -229,21 +229,23 @@ async function listThreadsByStatus(token, companyUid) {
|
|
|
229
229
|
return collected;
|
|
230
230
|
}
|
|
231
231
|
/**
|
|
232
|
-
* Project
|
|
233
|
-
*
|
|
234
|
-
* back to `projectId` alone.
|
|
232
|
+
* Project facets contain all project-owned threads, including terminal lane
|
|
233
|
+
* threads that have been removed from the active-status index.
|
|
235
234
|
*/
|
|
236
235
|
async function listProjectThreads(token, companyUid, projectId) {
|
|
237
236
|
try {
|
|
238
237
|
return await listThreadsByQuery(token, {
|
|
239
238
|
companyUid,
|
|
240
239
|
projectId,
|
|
241
|
-
status: "open",
|
|
242
240
|
});
|
|
243
241
|
}
|
|
244
242
|
catch {
|
|
245
243
|
try {
|
|
246
|
-
return await listThreadsByQuery(token, {
|
|
244
|
+
return await listThreadsByQuery(token, {
|
|
245
|
+
companyUid,
|
|
246
|
+
projectId,
|
|
247
|
+
status: "open",
|
|
248
|
+
});
|
|
247
249
|
}
|
|
248
250
|
catch {
|
|
249
251
|
return [];
|
|
@@ -253,7 +255,10 @@ async function listProjectThreads(token, companyUid, projectId) {
|
|
|
253
255
|
function mergeProjectAndStatusThreads(projectThreads, statusThreads, projectId) {
|
|
254
256
|
const seen = new Set();
|
|
255
257
|
const merged = [];
|
|
256
|
-
for (const thread of sortThreadsByActivity(projectThreads.filter((t) => (!t.projectId || t.projectId === projectId) &&
|
|
258
|
+
for (const thread of sortThreadsByActivity(projectThreads.filter((t) => (!t.projectId || t.projectId === projectId) &&
|
|
259
|
+
(isActiveThread(t) ||
|
|
260
|
+
(t.kind === "lane" &&
|
|
261
|
+
["done", "failed", "interrupted", "cancelled", "killed"].includes(String(t.laneStatus))))))) {
|
|
257
262
|
const key = threadKey(thread);
|
|
258
263
|
if (key) {
|
|
259
264
|
if (seen.has(key))
|
|
@@ -312,7 +317,7 @@ export function eventPayload(kind, opts, claimedBy) {
|
|
|
312
317
|
export async function ensureProjectThread(token, company, projectId, opts) {
|
|
313
318
|
if (opts.threadId)
|
|
314
319
|
return { threadId: opts.threadId, created: false };
|
|
315
|
-
const existing = (await listActiveThreads(token, company.companyUid, projectId))
|
|
320
|
+
const existing = (await listActiveThreads(token, company.companyUid, projectId)).find((thread) => (thread.kind ?? "project") === "project" && !!thread.threadId);
|
|
316
321
|
if (existing?.threadId)
|
|
317
322
|
return { threadId: existing.threadId, created: false };
|
|
318
323
|
const created = asRecord(await meshJson(token, "/v1/work-mesh/threads", {
|
|
@@ -332,6 +337,27 @@ export async function ensureProjectThread(token, company, projectId, opts) {
|
|
|
332
337
|
throw new Error("Work mesh did not return a threadId");
|
|
333
338
|
return { threadId, created: true };
|
|
334
339
|
}
|
|
340
|
+
/** Ensure the server-owned, stable Work Mesh thread for one lane. */
|
|
341
|
+
export async function ensureLaneMeshThread(token, company, projectId, lane, laneStatus, signal) {
|
|
342
|
+
const result = asRecord(await meshJson(token, "/v1/work-mesh/threads", {
|
|
343
|
+
method: "POST",
|
|
344
|
+
body: {
|
|
345
|
+
companyUid: company.companyUid,
|
|
346
|
+
projectId,
|
|
347
|
+
routing: {
|
|
348
|
+
priority: "normal",
|
|
349
|
+
tags: ["hq-lane", `project:${projectId}`, `lane:${lane.laneId}`],
|
|
350
|
+
},
|
|
351
|
+
lane,
|
|
352
|
+
laneStatus,
|
|
353
|
+
},
|
|
354
|
+
signal,
|
|
355
|
+
}));
|
|
356
|
+
const threadId = typeof result?.threadId === "string" ? result.threadId.trim() : "";
|
|
357
|
+
if (!threadId)
|
|
358
|
+
throw new Error("Work mesh lane registration did not return a threadId");
|
|
359
|
+
return { threadId };
|
|
360
|
+
}
|
|
335
361
|
export async function appendThreadEvent(token, companyUid, threadId, eventKind, payload, signal) {
|
|
336
362
|
const data = asRecord(await meshJson(token, `/v1/work-mesh/threads/${encodeURIComponent(threadId)}/events`, {
|
|
337
363
|
method: "POST",
|
|
@@ -443,18 +469,21 @@ export async function putProjectView(token, companyUid, projectId, input) {
|
|
|
443
469
|
return result;
|
|
444
470
|
}
|
|
445
471
|
/** Register the project genesis thread and deterministic project channel. */
|
|
446
|
-
export async function registerMeshProject(token, companyUid, projectId) {
|
|
447
|
-
|
|
472
|
+
export async function registerMeshProject(token, companyUid, projectId, channelId) {
|
|
473
|
+
if (channelId !== undefined && !channelId.trim()) {
|
|
474
|
+
throw new Error("PROJECT_CHANNEL_INVALID: channelId must be non-empty");
|
|
475
|
+
}
|
|
476
|
+
const result = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/register`, { method: "POST", body: { companyUid, ...(channelId ? { channelId: channelId.trim() } : {}) } }));
|
|
448
477
|
const threadId = String(result?.threadId || "").trim();
|
|
449
|
-
const
|
|
450
|
-
if (!threadId || !
|
|
478
|
+
const registeredChannelId = String(result?.channelId || "").trim();
|
|
479
|
+
if (!threadId || !registeredChannelId) {
|
|
451
480
|
throw new Error("Work mesh registration did not return a threadId and channelId");
|
|
452
481
|
}
|
|
453
482
|
return {
|
|
454
483
|
companyUid: String(result?.companyUid || companyUid).trim(),
|
|
455
484
|
projectId: String(result?.projectId || projectId).trim(),
|
|
456
485
|
threadId,
|
|
457
|
-
channelId,
|
|
486
|
+
channelId: registeredChannelId,
|
|
458
487
|
channelCreated: result?.channelCreated === true,
|
|
459
488
|
};
|
|
460
489
|
}
|
|
@@ -125,7 +125,7 @@ export async function ensureProjectBoard(input) {
|
|
|
125
125
|
stories: input.stories,
|
|
126
126
|
repos: input.repos ?? [],
|
|
127
127
|
});
|
|
128
|
-
const registration = await registerMeshProject(input.token, input.company.companyUid, input.projectId);
|
|
128
|
+
const registration = await registerMeshProject(input.token, input.company.companyUid, input.projectId, input.channelId);
|
|
129
129
|
await closeSupersededProjectThread({
|
|
130
130
|
token: input.token,
|
|
131
131
|
companyUid: input.company.companyUid,
|