@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.
@@ -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 { createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, progressEventDue, } from "./mesh-events.js";
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 { state: "failed", reason: PROVIDER_REJECTED_REASON };
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
- // Explicit mesh attribution is the repair signal. Ordinary company lanes
584
- // already receive their thread during create; legacy records without one
585
- // stay quiet until an operator or the owning flow repairs them.
586
- const company = initialLane.mesh_company;
587
- if (company && !initialLane.thread_id) {
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.ensureProjectThread(company, initialLane.project_id),
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
- retryUpdateLane(hqRoot, laneId, "spawn", (current) => current.thread_id ? current : { ...current, thread_id: threadId });
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: "ensure_project_thread",
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 && envelope && latest && terminalWrite.changed) {
857
- const questionIds = envelope.decision === "ask"
858
- ? envelopeQuestionIds(laneId, latest.ref, envelope)
859
- : undefined;
860
- await emitMeshSoft(`lane_${envelope.decision}`, logDecision, tick.emit.at, (signal) => emitLaneDecisionEvents(mesh, updated, envelope, { questionIds, signal }), meshRequestTimeoutMs);
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
  }
@@ -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;
@@ -229,21 +229,23 @@ async function listThreadsByStatus(token, companyUid) {
229
229
  return collected;
230
230
  }
231
231
  /**
232
- * Project threads are often missing from the status index. Prefer
233
- * `projectId` + `status=open`; if that combined query is rejected, fall
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, { companyUid, projectId });
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) && isActiveThread(t)))) {
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))[0];
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
- const result = asRecord(await meshJson(token, `/v1/work-mesh/projects/${encodeURIComponent(projectId)}/register`, { method: "POST", body: { companyUid } }));
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 channelId = String(result?.channelId || "").trim();
450
- if (!threadId || !channelId) {
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
  }
@@ -41,6 +41,7 @@ export declare function ensureProjectBoard(input: {
41
41
  description?: string;
42
42
  stories: MeshProjectStoryInput[];
43
43
  repos?: MeshProjectRepo[];
44
+ channelId?: string;
44
45
  }): Promise<{
45
46
  view: MeshProjectView;
46
47
  registration: MeshProjectRegistration;
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.154.0",
3
+ "version": "5.156.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {