@indigoai-us/hq-cli 5.225.0 → 5.227.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/dist/command-catalog.generated.d.ts +100 -0
  3. package/dist/command-catalog.generated.js +128 -0
  4. package/dist/command-registration-plan.d.ts +18 -0
  5. package/dist/command-registration-plan.js +6 -0
  6. package/dist/commands/agent-enroll.d.ts +2 -0
  7. package/dist/commands/agent-enroll.js +17 -2
  8. package/dist/commands/cloud.d.ts +10 -0
  9. package/dist/commands/cloud.js +52 -47
  10. package/dist/commands/lanes-checks.d.ts +2 -0
  11. package/dist/commands/lanes-checks.js +13 -11
  12. package/dist/commands/lanes-import.d.ts +1 -0
  13. package/dist/commands/lanes-import.js +12 -11
  14. package/dist/commands/lanes-report.js +21 -14
  15. package/dist/commands/lanes-tracker.d.ts +1 -0
  16. package/dist/commands/lanes-tracker.js +39 -31
  17. package/dist/commands/lanes.d.ts +8 -0
  18. package/dist/commands/lanes.js +818 -314
  19. package/dist/commands/staff/reset-test-account.d.ts +110 -0
  20. package/dist/commands/staff/reset-test-account.js +268 -0
  21. package/dist/commands/staff/test-account.d.ts +51 -0
  22. package/dist/commands/staff/test-account.js +196 -0
  23. package/dist/lib/checkpoint-ontology/plan.d.ts +4 -6
  24. package/dist/lib/checkpoint-ontology/plan.js +8 -26
  25. package/dist/lib/doctor/client-health-flag-token.d.ts +15 -0
  26. package/dist/lib/doctor/client-health-flag-token.js +66 -0
  27. package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +1 -0
  28. package/dist/lib/doctor/client-health-reason-codes-flag.js +16 -5
  29. package/dist/lib/doctor/client-health-sentry-flag.d.ts +1 -0
  30. package/dist/lib/doctor/client-health-sentry-flag.js +17 -6
  31. package/dist/lib/lanes/dropbox.d.ts +9 -0
  32. package/dist/lib/lanes/dropbox.js +70 -1
  33. package/dist/lib/lanes/reconcile.d.ts +4 -0
  34. package/dist/lib/lanes/reconcile.js +28 -0
  35. package/dist/lib/lanes/store.js +9 -1
  36. package/dist/lib/lanes/tracker-mesh-questions.d.ts +54 -0
  37. package/dist/lib/lanes/tracker-mesh-questions.js +138 -0
  38. package/dist/lib/lanes/tracker-mesh.d.ts +19 -0
  39. package/dist/lib/lanes/tracker-mesh.js +228 -0
  40. package/dist/lib/lanes/tracker-store.d.ts +13 -2
  41. package/dist/lib/lanes/tracker-store.js +1 -1
  42. package/dist/lib/lanes/types.d.ts +4 -0
  43. package/dist/lib/lanes/watcher-main.js +16 -0
  44. package/dist/lib/lanes/watcher.d.ts +4 -1
  45. package/dist/lib/lanes/watcher.js +77 -3
  46. package/dist/utils/cognito-session.d.ts +8 -0
  47. package/dist/utils/cognito-session.js +198 -3
  48. package/dist/utils/upload-author.d.ts +15 -0
  49. package/dist/utils/upload-author.js +39 -0
  50. package/package.json +1 -1
@@ -15,7 +15,6 @@ import { resolveOntologyCompany, safeWriteFile, stateRoot } from "./company.js";
15
15
  import { startMultiWriterRun } from "./multi-writer.js";
16
16
  import { buildEntityIndex, listKnowledgeFiles, listSignalFiles, readSignal } from "./tree.js";
17
17
  export const ONTOLOGY_FLAG = "checkpoint.ontology";
18
- export const SHARED_INPUTS_FLAG = "checkpoint.ontology.shared-inputs";
19
18
  export const COMPANY_BRIEF_FLAG = "checkpoint.ontology.company-brief";
20
19
  /**
21
20
  * Applies only when the payload's sibling backend is grok. On gold-v2, Claude
@@ -163,14 +162,13 @@ function missingRefs(companyDir, rel) {
163
162
  }
164
163
  /**
165
164
  * Update the ledger with the current tree and return it. New or changed files
166
- * become pending when this machine should process them (design 14.1) and
167
- * seen otherwise. The first run seeds from the entity boundary (design 14.3).
165
+ * the session touched become pending; others are recorded as seen. The first
166
+ * run records the entity boundary (design 14.3).
168
167
  */
169
- export function refreshLedger(company, existing, touched, sharedInputs) {
168
+ export function refreshLedger(company, existing, touched) {
170
169
  const firstRun = existing === null;
171
170
  const index = firstRun ? buildEntityIndex(company.dir) : null;
172
171
  const boundary = existing ? existing.boundary : entityBoundary(index);
173
- const boundaryMs = boundary ? Date.parse(boundary) : Number.NaN;
174
172
  const entries = {};
175
173
  const files = [...listKnowledgeFiles(company.dir), ...listSignalFiles(company.dir)];
176
174
  for (const rel of files) {
@@ -188,24 +186,10 @@ export function refreshLedger(company, existing, touched, sharedInputs) {
188
186
  : { ...previous, ...base };
189
187
  continue;
190
188
  }
191
- let reason = null;
192
- if (touched.has(rel))
193
- reason = "session-touched";
194
- else if (sharedInputs && firstRun) {
195
- if (Number.isFinite(boundaryMs) && stat.mtimeMs > boundaryMs)
196
- reason = "first-run-gap";
197
- else if (rel.startsWith("signals/") && missingRefs(company.dir, rel).length > 0)
198
- reason = "missing-refs";
199
- }
200
- else if (sharedInputs)
201
- reason = "shared";
202
- entries[rel] = reason ? { ...base, state: "pending", reason } : { ...base, state: "seen" };
189
+ entries[rel] = touched.has(rel) ? { ...base, state: "pending", reason: "session-touched" } : { ...base, state: "seen" };
203
190
  }
204
191
  return { ledger: { schema: LEDGER_SCHEMA, company: company.slug, boundary, entries }, firstRun };
205
192
  }
206
- function reasonRank(reason) {
207
- return reason === "session-touched" ? 0 : 1;
208
- }
209
193
  /** Plan this run. Never throws for a skip; returns the reason instead. */
210
194
  export async function planOntologyRun(options) {
211
195
  const { hqRoot, runDir, payload } = options;
@@ -218,7 +202,6 @@ export async function planOntologyRun(options) {
218
202
  fs.rmSync(backendPreferencePath(hqRoot, company), { force: true });
219
203
  return { status: "skipped", reason: "flag off" };
220
204
  }
221
- const sharedInputs = await options.readFlag(SHARED_INPUTS_FLAG, company);
222
205
  const briefOn = await options.readFlag(COMPANY_BRIEF_FLAG, company);
223
206
  const ignoreAnnotations = (await options.readFlag(IGNORE_ANNOTATIONS_FLAG, company)) && payload.backend === "grok";
224
207
  recordBackendPreference(hqRoot, company, await options.readFlag(GROK_BACKEND_FLAG, company));
@@ -226,15 +209,15 @@ export async function planOntologyRun(options) {
226
209
  const touched = sessionTouched(hqRoot, company, payload.files);
227
210
  // A first run takes its boundary from the entity tree, so the ledger is
228
211
  // refreshed before merge-back moves any `last_updated` forward.
229
- const { ledger, firstRun } = refreshLedger(company, readLedger(hqRoot, company), touched, sharedInputs);
212
+ const { ledger, firstRun } = refreshLedger(company, readLedger(hqRoot, company), touched);
230
213
  // Merge-back and the brief come before the nothing-changed return, so such
231
214
  // a plan still merges set-aside versions and records the brief; the brief
232
215
  // follows merge-back so its counts include merged entities.
233
216
  const multiWriter = multiWriterOn ? startMultiWriterRun(hqRoot, company, options.now ?? new Date()) : undefined;
234
217
  const companyBrief = briefOn ? writeCompanyBrief(hqRoot, company, options.now ?? new Date()) : undefined;
235
218
  const pending = Object.entries(ledger.entries)
236
- .filter(([, entry]) => entry.state === "pending" && (sharedInputs || entry.reason === "session-touched"))
237
- .sort(([aRel, a], [bRel, b]) => reasonRank(a.reason) - reasonRank(b.reason) || a.mtime_ms - b.mtime_ms || aRel.localeCompare(bRel));
219
+ .filter(([, entry]) => entry.state === "pending" && entry.reason === "session-touched")
220
+ .sort(([aRel, a], [bRel, b]) => a.mtime_ms - b.mtime_ms || aRel.localeCompare(bRel));
238
221
  const inputs = [];
239
222
  let tokens = 0;
240
223
  let remaining = 0;
@@ -264,7 +247,7 @@ export async function planOntologyRun(options) {
264
247
  kind,
265
248
  path: rel,
266
249
  audience: "company",
267
- reason: entry.reason ?? "shared",
250
+ reason: "session-touched",
268
251
  sha256: entry.sha256,
269
252
  est_tokens: cost,
270
253
  lines: chunks.length > 1 ? [start, end] : null,
@@ -313,7 +296,6 @@ export async function planOntologyRun(options) {
313
296
  created_at: (options.now ?? new Date()).toISOString(),
314
297
  first_run: firstRun,
315
298
  boundary: ledger.boundary,
316
- shared_inputs: sharedInputs,
317
299
  ...(companyBrief
318
300
  ? {
319
301
  company_brief: companyBrief.reason
@@ -0,0 +1,15 @@
1
+ export declare const CLIENT_HEALTH_FLAG_TOKEN_TIMEOUT_MS = 1000;
2
+ export declare class MissingFlagTokenError extends Error {
3
+ constructor();
4
+ }
5
+ export declare class ClientHealthFlagTokenTimeoutError extends Error {
6
+ constructor(timeoutMs: number);
7
+ }
8
+ export type ClientHealthFlagTokenProvider = (signal: AbortSignal) => string | Promise<string>;
9
+ /**
10
+ * Obtain a non-interactive ID token, separately bounded from the flag read.
11
+ * ensureCognitoIdToken refreshes expired person sessions and selects machine
12
+ * identity credentials when the current installation is a machine.
13
+ */
14
+ export declare function getClientHealthFlagToken(provider?: ClientHealthFlagTokenProvider, timeoutMs?: number, callerSignal?: AbortSignal): Promise<string>;
15
+ //# sourceMappingURL=client-health-flag-token.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { ensureCognitoIdToken } from "../../utils/cognito-session.js";
2
+ export const CLIENT_HEALTH_FLAG_TOKEN_TIMEOUT_MS = 1_000;
3
+ export class MissingFlagTokenError extends Error {
4
+ constructor() {
5
+ super("No cached HQ identity token is available");
6
+ this.name = "MissingFlagTokenError";
7
+ }
8
+ }
9
+ export class ClientHealthFlagTokenTimeoutError extends Error {
10
+ constructor(timeoutMs) {
11
+ super(`Client-health flag token acquisition exceeded its ${timeoutMs} ms bound`);
12
+ this.name = "ClientHealthFlagTokenTimeoutError";
13
+ }
14
+ }
15
+ /**
16
+ * Obtain a non-interactive ID token, separately bounded from the flag read.
17
+ * ensureCognitoIdToken refreshes expired person sessions and selects machine
18
+ * identity credentials when the current installation is a machine.
19
+ */
20
+ export async function getClientHealthFlagToken(provider, timeoutMs = CLIENT_HEALTH_FLAG_TOKEN_TIMEOUT_MS, callerSignal) {
21
+ if (callerSignal?.aborted) {
22
+ throw callerSignal.reason ?? new Error("Token acquisition was aborted");
23
+ }
24
+ const controller = new AbortController();
25
+ const signal = callerSignal ? AbortSignal.any([callerSignal, controller.signal]) : controller.signal;
26
+ const timeout = new ClientHealthFlagTokenTimeoutError(timeoutMs);
27
+ const timer = setTimeout(() => controller.abort(timeout), timeoutMs);
28
+ let onAbort;
29
+ const aborted = new Promise((_resolve, reject) => {
30
+ onAbort = () => reject(signal.reason ?? new Error("Token acquisition was aborted"));
31
+ if (signal.aborted)
32
+ onAbort();
33
+ else
34
+ signal.addEventListener("abort", onAbort, { once: true });
35
+ });
36
+ try {
37
+ const acquire = provider ?? ((requestSignal) => ensureCognitoIdToken({ interactive: false, signal: requestSignal }));
38
+ const acquisition = Promise.resolve().then(() => {
39
+ if (signal.aborted) {
40
+ throw signal.reason ?? new Error("Token acquisition was aborted");
41
+ }
42
+ return acquire(signal);
43
+ });
44
+ const token = await Promise.race([acquisition, aborted]);
45
+ if (signal.aborted) {
46
+ throw signal.reason ?? new Error("Token acquisition was aborted");
47
+ }
48
+ if (typeof token !== "string" || token.trim().length === 0) {
49
+ throw new MissingFlagTokenError();
50
+ }
51
+ return token;
52
+ }
53
+ catch (error) {
54
+ if (error instanceof Error &&
55
+ error.message.startsWith("No valid HQ session and interactive login is disabled")) {
56
+ throw new MissingFlagTokenError();
57
+ }
58
+ throw error;
59
+ }
60
+ finally {
61
+ clearTimeout(timer);
62
+ if (onAbort)
63
+ signal.removeEventListener("abort", onAbort);
64
+ }
65
+ }
66
+ //# sourceMappingURL=client-health-flag-token.js.map
@@ -7,6 +7,7 @@ export interface ClientHealthReasonCodesFlagDependencies {
7
7
  env?: NodeJS.ProcessEnv;
8
8
  createClient?: (options: FlagClientOptions) => ClientHealthFlagClient;
9
9
  getToken?: FlagClientOptions["getToken"];
10
+ tokenTimeoutMs?: number;
10
11
  fetch?: typeof fetch;
11
12
  reportFailure?: (error: unknown) => void;
12
13
  }
@@ -1,6 +1,6 @@
1
- import { loadCachedTokens } from "@indigoai-us/hq-cloud";
2
1
  import { createFlagClient, } from "@indigoai-us/hq-flags-client";
3
2
  import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
3
+ import { getClientHealthFlagToken } from "./client-health-flag-token.js";
4
4
  export const CLIENT_HEALTH_REASON_CODES_FLAG = "client-health-sync-reason-codes";
5
5
  export const CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK = false;
6
6
  export const CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS = 150;
@@ -39,9 +39,8 @@ export async function clientHealthReasonCodesEnabledForCompany(company, dependen
39
39
  return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
40
40
  }
41
41
  const companySlug = company.companySlug?.trim() || "";
42
- const deadline = AbortSignal.timeout(CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS);
43
- const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
44
- const doFetch = dependencies.fetch ?? globalThis.fetch;
42
+ if (callerSignal?.aborted)
43
+ return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
45
44
  let client;
46
45
  let failed = false;
47
46
  let failureReported = false;
@@ -53,6 +52,18 @@ export async function clientHealthReasonCodesEnabledForCompany(company, dependen
53
52
  failureReported = true;
54
53
  (dependencies.reportFailure ?? reportFlagFailure)(error);
55
54
  };
55
+ let token;
56
+ try {
57
+ token = await getClientHealthFlagToken(dependencies.getToken, dependencies.tokenTimeoutMs, callerSignal);
58
+ }
59
+ catch (error) {
60
+ if (!callerSignal?.aborted)
61
+ reportFailureOnce(error);
62
+ return CLIENT_HEALTH_REASON_CODES_FLAG_FALLBACK;
63
+ }
64
+ const deadline = AbortSignal.timeout(CLIENT_HEALTH_REASON_CODES_FLAG_TIMEOUT_MS);
65
+ const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
66
+ const doFetch = dependencies.fetch ?? globalThis.fetch;
56
67
  try {
57
68
  client = (dependencies.createClient ?? createFlagClient)({
58
69
  endpoint,
@@ -61,7 +72,7 @@ export async function clientHealthReasonCodesEnabledForCompany(company, dependen
61
72
  ? { companyIdentifiers: [companySlug] }
62
73
  : {}),
63
74
  env: {},
64
- getToken: dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? ""),
75
+ getToken: () => token,
65
76
  fetch: (input, init) => doFetch(input, {
66
77
  ...init,
67
78
  signal: combineSignals(init?.signal ?? undefined, signal),
@@ -7,6 +7,7 @@ export interface ClientHealthSentryFlagDependencies {
7
7
  env?: NodeJS.ProcessEnv;
8
8
  createClient?: (options: FlagClientOptions) => ClientHealthFlagClient;
9
9
  getToken?: FlagClientOptions["getToken"];
10
+ tokenTimeoutMs?: number;
10
11
  fetch?: typeof fetch;
11
12
  reportFailure?: (error: unknown) => void;
12
13
  }
@@ -1,6 +1,6 @@
1
- import { loadCachedTokens } from "@indigoai-us/hq-cloud";
2
1
  import { createFlagClient, } from "@indigoai-us/hq-flags-client";
3
2
  import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
3
+ import { getClientHealthFlagToken } from "./client-health-flag-token.js";
4
4
  export const CLIENT_HEALTH_SENTRY_DIRECT_FLAG = "client-health.sentry-direct";
5
5
  export const CLIENT_HEALTH_SENTRY_DIRECT_FALLBACK = false;
6
6
  export const CLIENT_HEALTH_SENTRY_DIRECT_TIMEOUT_MS = 150;
@@ -31,10 +31,8 @@ export async function clientHealthSentryDirectEnabledForCompany(company, depende
31
31
  if (!/^cmp_[A-Za-z0-9]{3,128}$/.test(companyUid)) {
32
32
  return CLIENT_HEALTH_SENTRY_DIRECT_FALLBACK;
33
33
  }
34
- const companySlug = company.companySlug?.trim() || "";
35
- const deadline = AbortSignal.timeout(CLIENT_HEALTH_SENTRY_DIRECT_TIMEOUT_MS);
36
- const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
37
- const doFetch = dependencies.fetch ?? globalThis.fetch;
34
+ if (callerSignal?.aborted)
35
+ return CLIENT_HEALTH_SENTRY_DIRECT_FALLBACK;
38
36
  let client;
39
37
  let failed = false;
40
38
  let failureReported = false;
@@ -46,6 +44,19 @@ export async function clientHealthSentryDirectEnabledForCompany(company, depende
46
44
  failureReported = true;
47
45
  (dependencies.reportFailure ?? reportFlagFailure)(error);
48
46
  };
47
+ let token;
48
+ try {
49
+ token = await getClientHealthFlagToken(dependencies.getToken, dependencies.tokenTimeoutMs, callerSignal);
50
+ }
51
+ catch (error) {
52
+ if (!callerSignal?.aborted)
53
+ reportFailureOnce(error);
54
+ return CLIENT_HEALTH_SENTRY_DIRECT_FALLBACK;
55
+ }
56
+ const companySlug = company.companySlug?.trim() || "";
57
+ const deadline = AbortSignal.timeout(CLIENT_HEALTH_SENTRY_DIRECT_TIMEOUT_MS);
58
+ const signal = callerSignal ? AbortSignal.any([callerSignal, deadline]) : deadline;
59
+ const doFetch = dependencies.fetch ?? globalThis.fetch;
49
60
  try {
50
61
  client = (dependencies.createClient ?? createFlagClient)({
51
62
  endpoint: resolveFlagRegistryEndpoint(undefined, env),
@@ -54,7 +65,7 @@ export async function clientHealthSentryDirectEnabledForCompany(company, depende
54
65
  ? { companyIdentifiers: [companySlug] }
55
66
  : {}),
56
67
  env: {},
57
- getToken: dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? ""),
68
+ getToken: () => token,
58
69
  fetch: (input, init) => doFetch(input, {
59
70
  ...init,
60
71
  signal: combineSignals(init?.signal ?? undefined, signal),
@@ -50,6 +50,8 @@ export interface DrainDeps {
50
50
  afterRead?: () => void;
51
51
  /** Prefix marks archives that belong to one provider turn. */
52
52
  archivePrefix?: string;
53
+ /** Terminal messages with no permitted delivery path are kept separately. */
54
+ archiveDirectory?: "delivered" | "undeliverable";
53
55
  }
54
56
  export interface LaneDrainHookInput {
55
57
  transcript_path?: string | null;
@@ -68,6 +70,13 @@ export declare function mintClaimId(): string;
68
70
  export declare function assertSafeLaneId(laneId: string): void;
69
71
  export declare function dropboxDir(hqRoot: string, laneId: string): string;
70
72
  export declare function dropboxInboxPath(hqRoot: string, laneId: string): string;
73
+ /** A recovery drain can claim the inbox, draining files, or dead inflight files. */
74
+ export declare function dropboxInboxHasMessages(hqRoot: string, laneId: string): boolean;
75
+ /**
76
+ * Return a failed recovery claim to the append-only inbox without replacing
77
+ * messages appended while resume validation or spawn was in progress.
78
+ */
79
+ export declare function requeueDropboxMessages(hqRoot: string, laneId: string, messages: readonly DropboxMessage[]): void;
71
80
  /**
72
81
  * Worker tool-call liveness (watcher.ts reads it). A Claude worker run with
73
82
  * `--output-format json` writes nothing to worker.out until it exits, so the
@@ -65,6 +65,75 @@ export function dropboxDir(hqRoot, laneId) {
65
65
  export function dropboxInboxPath(hqRoot, laneId) {
66
66
  return path.join(dropboxDir(hqRoot, laneId), INBOX_NAME);
67
67
  }
68
+ function directoryHasRecoverableFiles(dir, nestedInboxOnly = false) {
69
+ let names;
70
+ try {
71
+ names = fs.readdirSync(dir).filter((name) => !name.startsWith("."));
72
+ }
73
+ catch (err) {
74
+ if (err.code === "ENOENT")
75
+ return false;
76
+ throw err;
77
+ }
78
+ for (const name of names) {
79
+ const candidate = path.join(dir, name);
80
+ let stat;
81
+ try {
82
+ stat = fs.statSync(candidate);
83
+ }
84
+ catch (err) {
85
+ if (err.code === "ENOENT")
86
+ continue;
87
+ throw err;
88
+ }
89
+ if (nestedInboxOnly) {
90
+ if (!stat.isDirectory())
91
+ continue;
92
+ try {
93
+ const inbox = fs.statSync(path.join(candidate, INBOX_NAME));
94
+ if (inbox.isFile() && inbox.size > 0)
95
+ return true;
96
+ }
97
+ catch (err) {
98
+ if (err.code !== "ENOENT")
99
+ throw err;
100
+ }
101
+ }
102
+ else if (stat.isFile() && stat.size > 0) {
103
+ return true;
104
+ }
105
+ }
106
+ return false;
107
+ }
108
+ function fileHasBytes(filePath) {
109
+ try {
110
+ const stat = fs.statSync(filePath);
111
+ return stat.isFile() && stat.size > 0;
112
+ }
113
+ catch (err) {
114
+ if (err.code === "ENOENT")
115
+ return false;
116
+ throw err;
117
+ }
118
+ }
119
+ /** A recovery drain can claim the inbox, draining files, or dead inflight files. */
120
+ export function dropboxInboxHasMessages(hqRoot, laneId) {
121
+ const box = dropboxDir(hqRoot, laneId);
122
+ return fileHasBytes(path.join(box, INBOX_NAME)) ||
123
+ directoryHasRecoverableFiles(path.join(box, DRAINING_DIR)) ||
124
+ directoryHasRecoverableFiles(path.join(box, INFLIGHT_DIR), true);
125
+ }
126
+ /**
127
+ * Return a failed recovery claim to the append-only inbox without replacing
128
+ * messages appended while resume validation or spawn was in progress.
129
+ */
130
+ export function requeueDropboxMessages(hqRoot, laneId, messages) {
131
+ if (messages.length === 0)
132
+ return;
133
+ const inbox = dropboxInboxPath(hqRoot, laneId);
134
+ fs.mkdirSync(path.dirname(inbox), { recursive: true });
135
+ fs.appendFileSync(inbox, messages.map((message) => `${JSON.stringify(message)}\n`).join(""), "utf8");
136
+ }
68
137
  /**
69
138
  * Worker tool-call liveness (watcher.ts reads it). A Claude worker run with
70
139
  * `--output-format json` writes nothing to worker.out until it exits, so the
@@ -220,7 +289,7 @@ export function drainDropboxAtPath(box, depsInput = {}) {
220
289
  return [];
221
290
  const inbox = path.join(box, INBOX_NAME);
222
291
  const draining = path.join(box, DRAINING_DIR);
223
- const delivered = path.join(box, DELIVERED_DIR);
292
+ const delivered = path.join(box, deps.archiveDirectory ?? DELIVERED_DIR);
224
293
  const inflightRoot = path.join(box, INFLIGHT_DIR);
225
294
  try {
226
295
  deps.mkdirSync(box, { recursive: true });
@@ -81,6 +81,10 @@ export interface ReconcileDeps {
81
81
  readRegistryFlag?: typeof readLanesRegistryFlag;
82
82
  /** Active-company fallback for questions raised from an unscoped lane. */
83
83
  resolveQuestionActiveCompany?: () => Promise<LaneCompanyRef>;
84
+ /** Index local questions and poll remote answers before the normal answer-wake pass. */
85
+ syncTrackerQuestions?: (hqRoot: string) => Promise<void>;
86
+ /** Resume queued messages after this pass wins a local dead-worker CAS. */
87
+ recoverQueuedMessages?: (lane: LaneRecord) => Promise<void>;
84
88
  /** Detached mesh seam for dead-watcher recovery. */
85
89
  mesh?: LaneMesh | null;
86
90
  /** Bound on one terminal mesh emit during dead-host recovery. Default 10 s. */
@@ -632,6 +632,14 @@ export async function runPassBody(hqRoot, deps) {
632
632
  const livenessDeps = deps.livenessDeps ?? defaultLivenessDeps();
633
633
  let lanes;
634
634
  let questions;
635
+ if (!deps.dryRun) {
636
+ try {
637
+ await deps.syncTrackerQuestions?.(hqRoot);
638
+ }
639
+ catch (err) {
640
+ markError(summary, err);
641
+ }
642
+ }
635
643
  try {
636
644
  lanes = loadLaneRecords(hqRoot, summary);
637
645
  questions = loadQuestionRecords(hqRoot, summary);
@@ -845,6 +853,14 @@ export async function runPassBody(hqRoot, deps) {
845
853
  await admitQueued(hqRoot, drainLanes, summary, deps, now);
846
854
  await resumeAwaiting(hqRoot, drainLanes, freshQuestions, summary, deps);
847
855
  });
856
+ if (!deps.dryRun && deps.syncTrackerQuestions) {
857
+ try {
858
+ await deps.syncTrackerQuestions(hqRoot);
859
+ }
860
+ catch (err) {
861
+ markError(summary, err);
862
+ }
863
+ }
848
864
  if (!deps.dryRun && summary.errors.length === 0) {
849
865
  try {
850
866
  writeHeartbeatFile(hqRoot, summary, nowIso);
@@ -1172,6 +1188,18 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
1172
1188
  }
1173
1189
  }
1174
1190
  }
1191
+ if (!deps.dryRun && deps.recoverQueuedMessages) {
1192
+ try {
1193
+ await deps.recoverQueuedMessages(result.record);
1194
+ }
1195
+ catch (err) {
1196
+ markDirty(summary, {
1197
+ kind: "lane",
1198
+ id: lane.lane_id,
1199
+ why: `queued message recovery failed: ${err instanceof Error ? err.message : String(err)}`,
1200
+ });
1201
+ }
1202
+ }
1175
1203
  }
1176
1204
  }
1177
1205
  catch (err) {
@@ -708,7 +708,15 @@ export function parseQuestionRecord(raw, filePath) {
708
708
  value: delivered_at,
709
709
  });
710
710
  }
711
- return { ...raw, delivered_at };
711
+ const mesh_question_id = raw.mesh_question_id;
712
+ if (mesh_question_id !== undefined && (typeof mesh_question_id !== "string" || !mesh_question_id.trim())) {
713
+ throw new LaneSchemaError("mesh_question_id must be a non-empty string when present", { path: filePath, value: mesh_question_id });
714
+ }
715
+ const mesh_delivered_at = raw.mesh_delivered_at === undefined ? null : raw.mesh_delivered_at;
716
+ if (mesh_delivered_at !== null && (typeof mesh_delivered_at !== "string" || !Number.isFinite(Date.parse(mesh_delivered_at)))) {
717
+ throw new LaneSchemaError("mesh_delivered_at must be a valid timestamp or null", { path: filePath, value: mesh_delivered_at });
718
+ }
719
+ return { ...raw, delivered_at, mesh_delivered_at };
712
720
  }
713
721
  /**
714
722
  * HQ Sync sibling-twin grammar from `@indigoai-us/hq-cloud` `buildConflictPath`:
@@ -0,0 +1,54 @@
1
+ import type { LaneCompanyRef, QuestionRecord } from "./types.js";
2
+ import { type TrackerMeshRequest } from "./tracker-mesh.js";
3
+ export type TrackerMeshQuestionAudience = "owner" | "operator" | "lane";
4
+ export type TrackerMeshQuestionStatus = "pending" | "answered" | "cancelled";
5
+ export interface TrackerMeshQuestionRow {
6
+ questionId: string;
7
+ threadId: string;
8
+ companyUid: string;
9
+ projectId: string;
10
+ laneId: string;
11
+ host: {
12
+ kind: string;
13
+ id: string;
14
+ };
15
+ audience: TrackerMeshQuestionAudience;
16
+ audienceChain: string[];
17
+ routingHistory: Array<Record<string, string>>;
18
+ question: string;
19
+ context?: string;
20
+ options: string[];
21
+ recommended?: string;
22
+ askedAt: string;
23
+ status: TrackerMeshQuestionStatus;
24
+ choice?: string;
25
+ notes?: string;
26
+ answeredBy?: string;
27
+ answeredAt?: string;
28
+ deliveredAt?: string;
29
+ }
30
+ export interface TrackerMeshQuestionInput {
31
+ laneId: string;
32
+ threadId?: string;
33
+ projectId: string;
34
+ audience: TrackerMeshQuestionAudience;
35
+ audienceChain: string[];
36
+ routingHistory: Array<Record<string, string>>;
37
+ question: string;
38
+ context: string;
39
+ options: string[];
40
+ recommended?: string;
41
+ }
42
+ export interface TrackerMeshQuestionClient {
43
+ create(input: TrackerMeshQuestionInput): Promise<TrackerMeshQuestionRow>;
44
+ list(statuses?: readonly TrackerMeshQuestionStatus[]): Promise<TrackerMeshQuestionRow[]>;
45
+ answer(questionId: string, choice: string, notes?: string): Promise<TrackerMeshQuestionRow>;
46
+ delivered(questionId: string): Promise<TrackerMeshQuestionRow>;
47
+ }
48
+ /** Persist a local delivery checkpoint only after the Work Mesh stamp succeeds. */
49
+ export declare function deliverTrackerMeshQuestionOnce(question: QuestionRecord, company: LaneCompanyRef, client: Pick<TrackerMeshQuestionClient, "delivered">, persist: (deliveredAt: string) => QuestionRecord): Promise<QuestionRecord>;
50
+ export declare function createTrackerMeshQuestionClient(options: {
51
+ company: LaneCompanyRef;
52
+ request?: TrackerMeshRequest;
53
+ }): TrackerMeshQuestionClient;
54
+ //# sourceMappingURL=tracker-mesh-questions.d.ts.map