@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.
- package/CHANGELOG.md +47 -1
- package/dist/command-catalog.generated.d.ts +100 -0
- package/dist/command-catalog.generated.js +128 -0
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +6 -0
- package/dist/commands/agent-enroll.d.ts +2 -0
- package/dist/commands/agent-enroll.js +17 -2
- package/dist/commands/cloud.d.ts +10 -0
- package/dist/commands/cloud.js +52 -47
- package/dist/commands/lanes-checks.d.ts +2 -0
- package/dist/commands/lanes-checks.js +13 -11
- package/dist/commands/lanes-import.d.ts +1 -0
- package/dist/commands/lanes-import.js +12 -11
- package/dist/commands/lanes-report.js +21 -14
- package/dist/commands/lanes-tracker.d.ts +1 -0
- package/dist/commands/lanes-tracker.js +39 -31
- package/dist/commands/lanes.d.ts +8 -0
- package/dist/commands/lanes.js +818 -314
- package/dist/commands/staff/reset-test-account.d.ts +110 -0
- package/dist/commands/staff/reset-test-account.js +268 -0
- package/dist/commands/staff/test-account.d.ts +51 -0
- package/dist/commands/staff/test-account.js +196 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +4 -6
- package/dist/lib/checkpoint-ontology/plan.js +8 -26
- package/dist/lib/doctor/client-health-flag-token.d.ts +15 -0
- package/dist/lib/doctor/client-health-flag-token.js +66 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-reason-codes-flag.js +16 -5
- package/dist/lib/doctor/client-health-sentry-flag.d.ts +1 -0
- package/dist/lib/doctor/client-health-sentry-flag.js +17 -6
- package/dist/lib/lanes/dropbox.d.ts +9 -0
- package/dist/lib/lanes/dropbox.js +70 -1
- package/dist/lib/lanes/reconcile.d.ts +4 -0
- package/dist/lib/lanes/reconcile.js +28 -0
- package/dist/lib/lanes/store.js +9 -1
- package/dist/lib/lanes/tracker-mesh-questions.d.ts +54 -0
- package/dist/lib/lanes/tracker-mesh-questions.js +138 -0
- package/dist/lib/lanes/tracker-mesh.d.ts +19 -0
- package/dist/lib/lanes/tracker-mesh.js +228 -0
- package/dist/lib/lanes/tracker-store.d.ts +13 -2
- package/dist/lib/lanes/tracker-store.js +1 -1
- package/dist/lib/lanes/types.d.ts +4 -0
- package/dist/lib/lanes/watcher-main.js +16 -0
- package/dist/lib/lanes/watcher.d.ts +4 -1
- package/dist/lib/lanes/watcher.js +77 -3
- package/dist/utils/cognito-session.d.ts +8 -0
- package/dist/utils/cognito-session.js +198 -3
- package/dist/utils/upload-author.d.ts +15 -0
- package/dist/utils/upload-author.js +39 -0
- 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
|
-
*
|
|
167
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|
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" &&
|
|
237
|
-
.sort(([aRel, a], [bRel, b]) =>
|
|
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:
|
|
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
|
-
|
|
43
|
-
|
|
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:
|
|
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
|
-
|
|
35
|
-
|
|
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:
|
|
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) {
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -708,7 +708,15 @@ export function parseQuestionRecord(raw, filePath) {
|
|
|
708
708
|
value: delivered_at,
|
|
709
709
|
});
|
|
710
710
|
}
|
|
711
|
-
|
|
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
|