@indigoai-us/hq-cli 5.345.198 → 5.345.200
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 +71 -0
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +6 -4
- package/dist/command-catalog.generated.d.ts +150 -6
- package/dist/command-catalog.generated.js +182 -6
- package/dist/commands/bot-events.d.ts +19 -0
- package/dist/commands/bot-events.js +46 -0
- package/dist/commands/bot-jobs.d.ts +2 -0
- package/dist/commands/bot-jobs.js +43 -2
- package/dist/commands/bot-runtime.d.ts +35 -0
- package/dist/commands/bot-runtime.js +115 -0
- package/dist/commands/bot.d.ts +11 -0
- package/dist/commands/bot.js +72 -11
- package/dist/commands/creators.js +2 -1
- package/dist/commands/lanes-report.d.ts +2 -1
- package/dist/commands/lanes-report.js +44 -1
- package/dist/commands/lanes.d.ts +9 -0
- package/dist/commands/lanes.js +95 -9
- package/dist/commands/library-remove.js +2 -1
- package/dist/commands/library.js +5 -4
- package/dist/commands/meetings.js +1 -21
- package/dist/commands/mesh.d.ts +0 -7
- package/dist/commands/mesh.js +5 -1
- package/dist/commands/signals-run.d.ts +2 -0
- package/dist/commands/signals-run.js +6 -1
- package/dist/commands/sync-held.js +2 -2
- package/dist/lanes-drain-hook.js +9 -0
- package/dist/lib/bot/api.d.ts +5 -0
- package/dist/lib/bot/claude-login.d.ts +31 -0
- package/dist/lib/bot/claude-login.js +160 -0
- package/dist/lib/bot/events.d.ts +18 -0
- package/dist/lib/bot/events.js +160 -0
- package/dist/lib/bot/executive-settings.d.ts +10 -0
- package/dist/lib/bot/executive-settings.js +43 -0
- package/dist/lib/bot/fleet-cron.js +4 -0
- package/dist/lib/bot/fleet-download.d.ts +27 -0
- package/dist/lib/bot/fleet-download.js +162 -0
- package/dist/lib/bot/fleet-local.d.ts +51 -2
- package/dist/lib/bot/fleet-local.js +179 -76
- package/dist/lib/bot/fleet-python.d.ts +2 -0
- package/dist/lib/bot/fleet-python.js +29 -0
- package/dist/lib/bot/job-run.js +1 -1
- package/dist/lib/bot/jobs/hot-signal-sync.d.ts +8 -0
- package/dist/lib/bot/jobs/hot-signal-sync.js +2 -0
- package/dist/lib/bot/jobs/meeting-detect.d.ts +28 -0
- package/dist/lib/bot/jobs/meeting-detect.js +176 -0
- package/dist/lib/bot/jobs/meeting-rider.d.ts +53 -0
- package/dist/lib/bot/jobs/meeting-rider.js +284 -0
- package/dist/lib/bot/jobs/signal-to-lane.d.ts +32 -0
- package/dist/lib/bot/jobs/signal-to-lane.js +169 -0
- package/dist/lib/bot/local-jobs.d.ts +15 -2
- package/dist/lib/bot/local-jobs.js +24 -7
- package/dist/lib/bot/run.d.ts +7 -49
- package/dist/lib/bot/run.js +100 -4
- package/dist/lib/bot/status.d.ts +21 -0
- package/dist/lib/doctor/checks/bots.d.ts +6 -0
- package/dist/lib/doctor/checks/bots.js +47 -1
- package/dist/lib/hot-channels/adapters/meetings.d.ts +1 -0
- package/dist/lib/hot-channels/adapters/meetings.js +3 -1
- package/dist/lib/hot-channels/meeting-source-ref.d.ts +3 -0
- package/dist/lib/hot-channels/meeting-source-ref.js +7 -0
- package/dist/lib/hot-channels/schedule.js +2 -2
- package/dist/lib/hot-channels/signals-pass.d.ts +9 -32
- package/dist/lib/hot-channels/signals-pass.js +51 -17
- package/dist/lib/lanes/admission.d.ts +2 -0
- package/dist/lib/lanes/admission.js +4 -0
- package/dist/lib/lanes/approval-gate.js +4 -1
- package/dist/lib/lanes/links.d.ts +8 -1
- package/dist/lib/lanes/links.js +84 -3
- package/dist/lib/lanes/mesh-events.js +3 -1
- package/dist/lib/lanes/outcome.d.ts +154 -0
- package/dist/lib/lanes/outcome.js +151 -0
- package/dist/lib/lanes/reconcile.js +1 -0
- package/dist/lib/lanes/signal-brief-template.d.ts +7 -0
- package/dist/lib/lanes/signal-brief-template.js +40 -0
- package/dist/lib/lanes/signal-lane.d.ts +24 -0
- package/dist/lib/lanes/signal-lane.js +99 -0
- package/dist/lib/lanes/source-report.d.ts +37 -0
- package/dist/lib/lanes/source-report.js +66 -0
- package/dist/lib/lanes/spawn.d.ts +6 -0
- package/dist/lib/lanes/spawn.js +21 -0
- package/dist/lib/lanes/store.d.ts +3 -0
- package/dist/lib/lanes/store.js +64 -0
- package/dist/lib/lanes/story-ledger.d.ts +3 -0
- package/dist/lib/lanes/story-ledger.js +1 -0
- package/dist/lib/lanes/types.d.ts +137 -0
- package/dist/lib/lanes/types.js +17 -2
- package/dist/lib/lanes/watcher.js +4 -0
- package/dist/lib/live-meetings.d.ts +19 -0
- package/dist/lib/live-meetings.js +23 -0
- package/dist/lib/mesh/api.js +2 -1
- package/dist/lib/mesh/live/daemon/doctor.js +6 -7
- package/dist/lib/mesh/live/daemon/run.d.ts +0 -10
- package/dist/lib/mesh/live/daemon/run.js +3 -0
- package/dist/lib/mesh/live/emit.d.ts +2 -11
- package/dist/lib/mesh/live/emit.js +20 -2
- package/dist/lib/mesh/live/flush.d.ts +2 -0
- package/dist/lib/mesh/live/flush.js +34 -3
- package/dist/lib/mesh/live/reporting-privacy.d.ts +7 -0
- package/dist/lib/mesh/live/reporting-privacy.js +47 -0
- package/dist/lib/mesh/live/spool.d.ts +8 -0
- package/dist/lib/mesh/live/spool.js +59 -40
- package/dist/lib/plan-limit-nag.d.ts +30 -4
- package/dist/lib/plan-limit-nag.js +68 -17
- package/dist/lib/signals/engine.d.ts +4 -0
- package/dist/lib/signals/engine.js +25 -5
- package/dist/lib/signals/incremental.d.ts +42 -0
- package/dist/lib/signals/incremental.js +191 -0
- package/dist/lib/signals/prior-signals.d.ts +11 -0
- package/dist/lib/signals/prior-signals.js +44 -0
- package/dist/lib/signals/reply.js +6 -1
- package/dist/lib/signals/settings.d.ts +5 -0
- package/dist/lib/signals/settings.js +17 -0
- package/dist/lib/signals/steps/extract.d.ts +23 -2
- package/dist/lib/signals/steps/extract.js +142 -9
- package/dist/lib/signals/steps/types.d.ts +1 -0
- package/dist/lib/signals/write-guard.js +7 -0
- package/dist/lib/work-context/company.d.ts +2 -3
- package/dist/lib/work-context/company.js +4 -22
- package/dist/secrets-env-action.js +5 -2
- package/dist/utils/cognito-callback-port.d.ts +17 -0
- package/dist/utils/cognito-callback-port.js +56 -0
- package/dist/utils/cognito-config.js +7 -3
- package/dist/utils/cognito-session.d.ts +7 -6
- package/dist/utils/cognito-session.js +19 -14
- package/dist/utils/interactive-caller.d.ts +23 -0
- package/dist/utils/interactive-caller.js +80 -0
- package/dist/utils/login-browser.js +5 -2
- package/dist/utils/resident-package-preload.js +4 -0
- package/package.json +7 -7
|
@@ -24,6 +24,8 @@ export declare const HELD_MAX_LINES = 20000;
|
|
|
24
24
|
export declare const PROJECT_REBIND_INTERVAL_MS = 60000;
|
|
25
25
|
export type HeldReason = "STATE_ABSENT" | "NEEDS_COMPANY" | "COMPANY_CONFLICT" | "NEEDS_PROJECT" | "HELD_EXPIRED" | "HELD_OVERFLOW";
|
|
26
26
|
export interface FlushDeps {
|
|
27
|
+
/** Production callers re-read the shared person preference before each flush. */
|
|
28
|
+
reportingAllowed?: () => Promise<boolean>;
|
|
27
29
|
workMeshRoot: string;
|
|
28
30
|
workContextRoot: string;
|
|
29
31
|
poster: SessionEventsPoster;
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* 2xx responses are accounted per eventId (accepted / duplicates / rejected).
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
|
+
import { reportingLocallyDisabled } from "./reporting-privacy.js";
|
|
12
13
|
import * as path from "node:path";
|
|
13
14
|
import { readMetaCompanySlug, resolveCompany } from "../../work-context/company.js";
|
|
14
|
-
import { companyBindingChanged, reboundSessionProjection, } from "../../work-context/company-rebind.js";
|
|
15
|
+
import { companyBindingChanged, isNonCloudCompanySlug, reboundSessionProjection, } from "../../work-context/company-rebind.js";
|
|
15
16
|
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey, } from "../../work-context/repo-remote.js";
|
|
16
17
|
import { rebindNeedsProjectOnPrompt } from "../../work-context/prompt-rebind.js";
|
|
17
18
|
import { readSessionState, updateSessionState, writeSessionState } from "../../work-context/state.js";
|
|
@@ -172,6 +173,12 @@ async function postBatchWithRetries(poster, body, deps) {
|
|
|
172
173
|
* Recovers orphan `*.claimed` files first; restores unprocessed lines on throw.
|
|
173
174
|
*/
|
|
174
175
|
export async function flushSessionEvents(inputDeps) {
|
|
176
|
+
const env = inputDeps.env ?? process.env;
|
|
177
|
+
if (reportingLocallyDisabled(env) ||
|
|
178
|
+
(inputDeps.reportingAllowed && !(await inputDeps.reportingAllowed()))) {
|
|
179
|
+
return { claimed: 0, posted: 0, held: 0, dropped: 0, deadLettered: 0,
|
|
180
|
+
restored: 0, batches: 0, heldClaimed: false, heldOverflow: 0 };
|
|
181
|
+
}
|
|
175
182
|
// One resolver per flush: git lookups are cached per cwd for this flush only.
|
|
176
183
|
const deps = {
|
|
177
184
|
...inputDeps,
|
|
@@ -268,6 +275,30 @@ export async function flushSessionEvents(inputDeps) {
|
|
|
268
275
|
break sessionLoop;
|
|
269
276
|
let state = readSessionState(sessionId, deps.workContextRoot);
|
|
270
277
|
state = applySessionMetaCompanyRebind(sessionId, items, state, deps, now);
|
|
278
|
+
// Personal is the documented non-cloud scope, even if an old projection
|
|
279
|
+
// still contains a company UID or a pending presence leave.
|
|
280
|
+
if (isNonCloudCompanySlug(state?.companySlug) || items.some(item => isNonCloudCompanySlug(eventText(item.event, "companySlug")))) {
|
|
281
|
+
summary.dropped += items.length;
|
|
282
|
+
for (const item of items)
|
|
283
|
+
markDisposed(item.event);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (state?.companyConfidence === "device_default" && deps.authorizeDeviceDefaultTenant) {
|
|
287
|
+
await deps.authorizeDeviceDefaultTenant(state);
|
|
288
|
+
state = readSessionState(sessionId, deps.workContextRoot) ?? state;
|
|
289
|
+
}
|
|
290
|
+
if (state?.companyConfidence === "device_default") {
|
|
291
|
+
summary.held += appendHeldLinesIfAbsent(items.map(item => heldEnvelope(item.event, "NEEDS_COMPANY", item.heldAt ?? now().toISOString())), deps.workMeshRoot);
|
|
292
|
+
for (const item of items)
|
|
293
|
+
markDisposed(item.event);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (isNonCloudCompanySlug(state?.companySlug)) {
|
|
297
|
+
summary.dropped += items.length;
|
|
298
|
+
for (const item of items)
|
|
299
|
+
markDisposed(item.event);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
271
302
|
const leaveResult = await postPresenceLeaveIfNeeded({
|
|
272
303
|
sessionId,
|
|
273
304
|
items,
|
|
@@ -726,7 +757,7 @@ function bindHeldCompanyIfResolved(sessionId, items, state, deps, now, uidCache)
|
|
|
726
757
|
// Stay synchronous unless a uid lookup is needed: the flush loop's
|
|
727
758
|
// microtask ordering is relied on by retry/back-pressure timing.
|
|
728
759
|
const company = resolveHeldCompany(sessionId, items, state, deps);
|
|
729
|
-
if (!company)
|
|
760
|
+
if (!company || isNonCloudCompanySlug(company.slug))
|
|
730
761
|
return state;
|
|
731
762
|
if (company.uid) {
|
|
732
763
|
return writeHeldCompanyBinding(sessionId, state, { uid: company.uid, slug: company.slug, source: company.source }, deps, now);
|
|
@@ -996,7 +1027,7 @@ function classifySession(state) {
|
|
|
996
1027
|
state.companyUid === state.ownerMismatch.companyUid) {
|
|
997
1028
|
return { action: "drop", reason: "owner_mismatch" };
|
|
998
1029
|
}
|
|
999
|
-
if (state.contextStatus === "needs_company") {
|
|
1030
|
+
if (state.companyConfidence === "device_default" || state.contextStatus === "needs_company") {
|
|
1000
1031
|
return { action: "hold", heldReason: "NEEDS_COMPANY" };
|
|
1001
1032
|
}
|
|
1002
1033
|
if (state.contextStatus === "company_conflict") {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Match the off controls used by the hook and handoff command. */
|
|
2
|
+
export declare function reportingLocallyDisabled(env?: NodeJS.ProcessEnv): boolean;
|
|
3
|
+
/** Legacy emit must not let the server infer a company for conversation text. */
|
|
4
|
+
export declare function mayEmitConversation(event: Record<string, unknown>, env?: NodeJS.ProcessEnv): boolean;
|
|
5
|
+
/** Fleet identities have no person preference; operator and local controls still apply. */
|
|
6
|
+
export declare function reportingRuntimeAllowed(machine: boolean, env?: NodeJS.ProcessEnv): Promise<boolean>;
|
|
7
|
+
//# sourceMappingURL=reporting-privacy.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { anywhereRuntimeAllowed } from "../../anywhere-person-setting.js";
|
|
2
|
+
import { anywhereRuntimeEnabledFromSnapshot } from "../../anywhere-runtime-fast-flag.js";
|
|
3
|
+
import { hqAnywhereRuntimeLocallyDisabled } from "../../anywhere-runtime-flag-key.js";
|
|
4
|
+
import { isNonCloudCompanySlug } from "../../work-context/company-rebind.js";
|
|
5
|
+
import { readMetaCompanySlug, resolveCompany } from "../../work-context/company.js";
|
|
6
|
+
import { deriveRemoteOwnerSlug, deriveRepoIdentityKey } from "../../work-context/repo-remote.js";
|
|
7
|
+
import { workContextRoot } from "../../work-context/paths.js";
|
|
8
|
+
import { readSessionState } from "../../work-context/state.js";
|
|
9
|
+
/** Match the off controls used by the hook and handoff command. */
|
|
10
|
+
export function reportingLocallyDisabled(env = process.env) {
|
|
11
|
+
const hooks = (env.HQ_DISABLED_HOOKS ?? "").split(",").map(value => value.trim());
|
|
12
|
+
return ["1", "true", "TRUE", "yes", "YES", "on", "ON"].includes(env.HQ_WORK_MESH_DISABLED ?? "") ||
|
|
13
|
+
hooks.some(value => ["*", "work-mesh", "work-mesh-live"].includes(value)) ||
|
|
14
|
+
hqAnywhereRuntimeLocallyDisabled(env);
|
|
15
|
+
}
|
|
16
|
+
/** Legacy emit must not let the server infer a company for conversation text. */
|
|
17
|
+
export function mayEmitConversation(event, env = process.env) {
|
|
18
|
+
if (event.kind !== "session_end" && typeof event.summary !== "string")
|
|
19
|
+
return true;
|
|
20
|
+
if (typeof event.sessionId !== "string" || !event.sessionId.trim())
|
|
21
|
+
return false;
|
|
22
|
+
const evidence = event.evidence && typeof event.evidence === "object"
|
|
23
|
+
? event.evidence : {};
|
|
24
|
+
const text = (key) => typeof evidence[key] === "string" ? evidence[key] : undefined;
|
|
25
|
+
const root = workContextRoot(undefined, env);
|
|
26
|
+
const state = readSessionState(event.sessionId, root);
|
|
27
|
+
const meta = readMetaCompanySlug(event.sessionId, text("hqRoot"));
|
|
28
|
+
if (isNonCloudCompanySlug(meta) || isNonCloudCompanySlug(state?.companySlug) || isNonCloudCompanySlug(text("companySlug")) ||
|
|
29
|
+
state?.companyConfidence === "device_default" || state?.contextStatus === "untracked")
|
|
30
|
+
return false;
|
|
31
|
+
// The flushing process is not the originating session. Its spawn/company
|
|
32
|
+
// identity cannot authorize historical conversation text.
|
|
33
|
+
const resolved = resolveCompany({ root, sessionId: event.sessionId,
|
|
34
|
+
env: { HQ_AGENT_IDENTITY_FILE: "" },
|
|
35
|
+
cwd: text("cwd"), hqRoot: text("hqRoot"),
|
|
36
|
+
remoteOwnerSlug: deriveRemoteOwnerSlug({ cwd: text("cwd"), hqRoot: text("hqRoot") }) ?? undefined,
|
|
37
|
+
repoIdentityKey: text("cwd") ? deriveRepoIdentityKey({ cwd: text("cwd") }) ?? undefined : undefined,
|
|
38
|
+
trusted: { companySlug: text("companySlug") }, existingState: state });
|
|
39
|
+
return resolved.status === "resolved" && !isNonCloudCompanySlug(resolved.company.slug);
|
|
40
|
+
}
|
|
41
|
+
/** Fleet identities have no person preference; operator and local controls still apply. */
|
|
42
|
+
export async function reportingRuntimeAllowed(machine, env = process.env) {
|
|
43
|
+
if (reportingLocallyDisabled(env))
|
|
44
|
+
return false;
|
|
45
|
+
return machine ? anywhereRuntimeEnabledFromSnapshot(env) : anywhereRuntimeAllowed(env);
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=reporting-privacy.js.map
|
|
@@ -44,6 +44,14 @@ export declare function recoverOrphanClaims(root: string): {
|
|
|
44
44
|
};
|
|
45
45
|
export declare function removeClaimFile(claimPath: string | null): void;
|
|
46
46
|
export declare function deadLetterNonEmpty(root?: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Visit each line of a JSONL file without loading the whole file into one
|
|
49
|
+
* string. Queue files (dead-letter especially) can grow past V8's ~512 MB
|
|
50
|
+
* string ceiling, where readFileSync(..., "utf8") throws. Splits on the raw
|
|
51
|
+
* newline byte so multi-byte UTF-8 sequences are never cut. Returns false when
|
|
52
|
+
* the file cannot be opened.
|
|
53
|
+
*/
|
|
54
|
+
export declare function forEachJsonlLine(filePath: string, visit: (line: string) => void): boolean;
|
|
47
55
|
export declare function countJsonlLines(filePath: string): number;
|
|
48
56
|
/** Count held eventIds once; legacy rows without an eventId remain line-counted. */
|
|
49
57
|
export declare function countDistinctHeldEvents(filePath: string): number;
|
|
@@ -334,40 +334,71 @@ export function deadLetterNonEmpty(root) {
|
|
|
334
334
|
return false;
|
|
335
335
|
}
|
|
336
336
|
}
|
|
337
|
+
/** Chunk size for streaming JSONL reads. */
|
|
338
|
+
const JSONL_READ_CHUNK_BYTES = 1024 * 1024;
|
|
339
|
+
/**
|
|
340
|
+
* Visit each line of a JSONL file without loading the whole file into one
|
|
341
|
+
* string. Queue files (dead-letter especially) can grow past V8's ~512 MB
|
|
342
|
+
* string ceiling, where readFileSync(..., "utf8") throws. Splits on the raw
|
|
343
|
+
* newline byte so multi-byte UTF-8 sequences are never cut. Returns false when
|
|
344
|
+
* the file cannot be opened.
|
|
345
|
+
*/
|
|
346
|
+
export function forEachJsonlLine(filePath, visit) {
|
|
347
|
+
let fd;
|
|
348
|
+
try {
|
|
349
|
+
fd = fs.openSync(filePath, "r");
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
const chunk = Buffer.alloc(JSONL_READ_CHUNK_BYTES);
|
|
356
|
+
let carry = Buffer.alloc(0);
|
|
357
|
+
for (;;) {
|
|
358
|
+
const n = fs.readSync(fd, chunk, 0, chunk.length, null);
|
|
359
|
+
if (n <= 0)
|
|
360
|
+
break;
|
|
361
|
+
let data = carry.length > 0 ? Buffer.concat([carry, chunk.subarray(0, n)]) : chunk.subarray(0, n);
|
|
362
|
+
let start = 0;
|
|
363
|
+
for (let nl = data.indexOf(0x0a, start); nl !== -1; nl = data.indexOf(0x0a, start)) {
|
|
364
|
+
visit(data.toString("utf8", start, nl));
|
|
365
|
+
start = nl + 1;
|
|
366
|
+
}
|
|
367
|
+
carry = Buffer.from(data.subarray(start));
|
|
368
|
+
data = Buffer.alloc(0);
|
|
369
|
+
}
|
|
370
|
+
if (carry.length > 0)
|
|
371
|
+
visit(carry.toString("utf8"));
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
fs.closeSync(fd);
|
|
375
|
+
}
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
337
378
|
export function countJsonlLines(filePath) {
|
|
338
|
-
if (!fs.existsSync(filePath))
|
|
339
|
-
return 0;
|
|
340
|
-
const raw = fs.readFileSync(filePath, "utf8");
|
|
341
379
|
let n = 0;
|
|
342
|
-
|
|
380
|
+
forEachJsonlLine(filePath, (line) => {
|
|
343
381
|
if (line.trim())
|
|
344
382
|
n += 1;
|
|
345
|
-
}
|
|
383
|
+
});
|
|
346
384
|
return n;
|
|
347
385
|
}
|
|
348
386
|
/** Count held eventIds once; legacy rows without an eventId remain line-counted. */
|
|
349
387
|
export function countDistinctHeldEvents(filePath) {
|
|
350
|
-
let raw;
|
|
351
|
-
try {
|
|
352
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
353
|
-
}
|
|
354
|
-
catch {
|
|
355
|
-
// A concurrent claim can rename held.jsonl between existence and read checks.
|
|
356
|
-
return 0;
|
|
357
|
-
}
|
|
358
388
|
const seen = new Set();
|
|
359
389
|
let count = 0;
|
|
360
|
-
|
|
390
|
+
// A concurrent claim can rename held.jsonl away; a missing file counts as 0.
|
|
391
|
+
forEachJsonlLine(filePath, (line) => {
|
|
361
392
|
if (!line.trim())
|
|
362
|
-
|
|
393
|
+
return;
|
|
363
394
|
const eventId = heldEventId(line);
|
|
364
395
|
if (eventId) {
|
|
365
396
|
if (seen.has(eventId))
|
|
366
|
-
|
|
397
|
+
return;
|
|
367
398
|
seen.add(eventId);
|
|
368
399
|
}
|
|
369
400
|
count += 1;
|
|
370
|
-
}
|
|
401
|
+
});
|
|
371
402
|
return count;
|
|
372
403
|
}
|
|
373
404
|
function record(value) {
|
|
@@ -413,17 +444,12 @@ function isUnattributedLine(line) {
|
|
|
413
444
|
}
|
|
414
445
|
}
|
|
415
446
|
function countUnattributedJsonlLines(filePath) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
.filter((line) => line.trim() && isUnattributedLine(line)).length;
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
return 0;
|
|
426
|
-
}
|
|
447
|
+
let n = 0;
|
|
448
|
+
forEachJsonlLine(filePath, (line) => {
|
|
449
|
+
if (line.trim() && isUnattributedLine(line))
|
|
450
|
+
n += 1;
|
|
451
|
+
});
|
|
452
|
+
return n;
|
|
427
453
|
}
|
|
428
454
|
/**
|
|
429
455
|
* Count the actionable, unfiled events across all local delivery queues.
|
|
@@ -438,27 +464,20 @@ export function countUnattributedEvents(root) {
|
|
|
438
464
|
return { spool, held, deadLetter, total: spool + held + deadLetter };
|
|
439
465
|
}
|
|
440
466
|
function countUnattributedHeldEvents(filePath) {
|
|
441
|
-
let raw;
|
|
442
|
-
try {
|
|
443
|
-
raw = fs.readFileSync(filePath, "utf8");
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
// A concurrent held claim may rename this file between the read and open.
|
|
447
|
-
return 0;
|
|
448
|
-
}
|
|
449
467
|
const seen = new Set();
|
|
450
468
|
let count = 0;
|
|
451
|
-
|
|
469
|
+
// A concurrent held claim may rename this file away; a missing file counts as 0.
|
|
470
|
+
forEachJsonlLine(filePath, (line) => {
|
|
452
471
|
if (!line.trim() || !isUnattributedLine(line))
|
|
453
|
-
|
|
472
|
+
return;
|
|
454
473
|
const eventId = heldEventId(line);
|
|
455
474
|
if (eventId) {
|
|
456
475
|
if (seen.has(eventId))
|
|
457
|
-
|
|
476
|
+
return;
|
|
458
477
|
seen.add(eventId);
|
|
459
478
|
}
|
|
460
479
|
count += 1;
|
|
461
|
-
}
|
|
480
|
+
});
|
|
462
481
|
return count;
|
|
463
482
|
}
|
|
464
483
|
//# sourceMappingURL=spool.js.map
|
|
@@ -49,6 +49,28 @@ export declare const OVER_NAG_INTERVAL_MS: number;
|
|
|
49
49
|
* as the bare key rather than a guess.
|
|
50
50
|
*/
|
|
51
51
|
export declare const PLAN_LIMIT_DIMENSION_LABELS: Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* The account a nag payload is about, as hq-pro labels it (`account` beside
|
|
54
|
+
* `planLimits`). One command can touch a personal vault and a company vault,
|
|
55
|
+
* so the line names which one is over (feedback_b1867fe6).
|
|
56
|
+
*/
|
|
57
|
+
export type PlanLimitAccount = {
|
|
58
|
+
kind: "person";
|
|
59
|
+
} | {
|
|
60
|
+
kind: "company";
|
|
61
|
+
slug: string | null;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Render one amount of a resource. Storage arrives in bytes; printed raw it
|
|
65
|
+
* reads as "47012700666 of 10737418240". It is shown in GB with one decimal,
|
|
66
|
+
* so a 10 GB ceiling reads "10.0 GB", matching the plan's own wording.
|
|
67
|
+
*/
|
|
68
|
+
export declare function formatPlanLimitAmount(key: string, n: number): string;
|
|
69
|
+
/**
|
|
70
|
+
* The account as a noun phrase: "your personal account", "the acme company",
|
|
71
|
+
* or "this account" when an older hq-pro sent no label.
|
|
72
|
+
*/
|
|
73
|
+
export declare function planLimitAccountPhrase(account: PlanLimitAccount | null | undefined): string;
|
|
52
74
|
export interface PlanLimitEntry {
|
|
53
75
|
used: number;
|
|
54
76
|
limit: number;
|
|
@@ -75,10 +97,13 @@ export declare function recordPlanLimitStatus(body: unknown, opts?: {
|
|
|
75
97
|
/**
|
|
76
98
|
* The over-limit per-turn line (US-036 §2.2). One line, not the old box: it
|
|
77
99
|
* now repeats up to four times a day, and a multi-line box at that cadence
|
|
78
|
-
* reads as breakage rather than as a nag. It
|
|
79
|
-
*
|
|
100
|
+
* reads as breakage rather than as a nag. It names the account and states the
|
|
101
|
+
* facts. It claims no pause: being over is not being refused. A refusal comes
|
|
102
|
+
* back from hq-pro as its own error with its own sentence, and that is the
|
|
103
|
+
* only place a pause is reported (feedback_b1867fe6, where this line told a
|
|
104
|
+
* person their uploads were paused right after two uploads succeeded).
|
|
80
105
|
*/
|
|
81
|
-
export declare function buildOverLine(overEntries: Array<[string, PlanLimitEntry]>, upgradeUrl: string): string;
|
|
106
|
+
export declare function buildOverLine(overEntries: Array<[string, PlanLimitEntry]>, upgradeUrl: string, account?: PlanLimitAccount | null): string;
|
|
82
107
|
/**
|
|
83
108
|
* The notice a personal feature refusal prints. `upgradeUrl` is the server's
|
|
84
109
|
* own checkout target; an older hq-pro sends none, and the line then names the
|
|
@@ -95,7 +120,7 @@ export declare function renderPersonalPlanFeatureNotice(feature: string, upgrade
|
|
|
95
120
|
* their own story, and a notice that announces a pause nobody is experiencing
|
|
96
121
|
* reads as a bug.
|
|
97
122
|
*/
|
|
98
|
-
export declare function renderThresholdNotice(crossing: PlanLimitThresholdCrossing, upgradeUrl: string): string;
|
|
123
|
+
export declare function renderThresholdNotice(crossing: PlanLimitThresholdCrossing, upgradeUrl: string, account?: PlanLimitAccount | null): string;
|
|
99
124
|
/**
|
|
100
125
|
* Print every crossing this machine has not already printed for this episode.
|
|
101
126
|
*
|
|
@@ -106,6 +131,7 @@ export declare function renderThresholdNotice(crossing: PlanLimitThresholdCrossi
|
|
|
106
131
|
export declare function emitPlanLimitThresholdNotice(crossings: readonly PlanLimitThresholdCrossing[], upgradeUrl: string | null, opts?: {
|
|
107
132
|
write?: (s: string) => void;
|
|
108
133
|
statePath?: string;
|
|
134
|
+
account?: PlanLimitAccount | null;
|
|
109
135
|
}): void;
|
|
110
136
|
/**
|
|
111
137
|
* Emit a plan-limit nag to stderr at command completion.
|
|
@@ -63,6 +63,52 @@ export const PLAN_LIMIT_DIMENSION_LABELS = {
|
|
|
63
63
|
deployments: "deployments",
|
|
64
64
|
storageBytes: "storage",
|
|
65
65
|
};
|
|
66
|
+
/**
|
|
67
|
+
* Bytes in one GB as plan limits count them: 2^30. hq-pro stores the "10 GB"
|
|
68
|
+
* and "2 GB" storage ceilings as GiB and its limit emails print them as
|
|
69
|
+
* "10 GB" / "2 GB"; `lib/billing/usage-cost.ts` uses the same unit
|
|
70
|
+
* (BYTES_PER_GB). Kept local so this hot-path module loads no billing code.
|
|
71
|
+
*/
|
|
72
|
+
const STORAGE_BYTES_PER_GB = 1024 ** 3;
|
|
73
|
+
/**
|
|
74
|
+
* Render one amount of a resource. Storage arrives in bytes; printed raw it
|
|
75
|
+
* reads as "47012700666 of 10737418240". It is shown in GB with one decimal,
|
|
76
|
+
* so a 10 GB ceiling reads "10.0 GB", matching the plan's own wording.
|
|
77
|
+
*/
|
|
78
|
+
export function formatPlanLimitAmount(key, n) {
|
|
79
|
+
if (key !== "storageBytes")
|
|
80
|
+
return String(n);
|
|
81
|
+
return `${(n / STORAGE_BYTES_PER_GB).toFixed(1)} GB`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The account as a noun phrase: "your personal account", "the acme company",
|
|
85
|
+
* or "this account" when an older hq-pro sent no label.
|
|
86
|
+
*/
|
|
87
|
+
export function planLimitAccountPhrase(account) {
|
|
88
|
+
if (account?.kind === "person")
|
|
89
|
+
return "your personal account";
|
|
90
|
+
if (account?.kind === "company" && account.slug)
|
|
91
|
+
return `the ${account.slug} company`;
|
|
92
|
+
return "this account";
|
|
93
|
+
}
|
|
94
|
+
function parseAccount(body) {
|
|
95
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const raw = body.account;
|
|
99
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
100
|
+
return null;
|
|
101
|
+
const rec = raw;
|
|
102
|
+
if (rec.kind === "person")
|
|
103
|
+
return { kind: "person" };
|
|
104
|
+
if (rec.kind === "company") {
|
|
105
|
+
const slug = typeof rec.slug === "string" && rec.slug.trim().length > 0
|
|
106
|
+
? rec.slug.trim()
|
|
107
|
+
: null;
|
|
108
|
+
return { kind: "company", slug };
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
66
112
|
function dimensionSuffix(key) {
|
|
67
113
|
const label = PLAN_LIMIT_DIMENSION_LABELS[key];
|
|
68
114
|
return label && label !== key ? ` (${label})` : "";
|
|
@@ -252,14 +298,15 @@ export function recordPlanLimitStatus(body, opts = {}) {
|
|
|
252
298
|
legacyUrl: status?.upgradeUrl ?? PLAN_LIMIT_UPGRADE_URL,
|
|
253
299
|
});
|
|
254
300
|
}
|
|
301
|
+
const account = parseAccount(body);
|
|
255
302
|
if (status !== null) {
|
|
256
303
|
const anyOver = Object.values(status.limits).some((e) => e.over);
|
|
257
|
-
lastSeen = { ...status, anyOver, upgradeUrl: resolvedUpgradeUrl };
|
|
304
|
+
lastSeen = { ...status, anyOver, upgradeUrl: resolvedUpgradeUrl, account };
|
|
258
305
|
}
|
|
259
306
|
// Independent of `planLimits`: the server can announce a crossing on a
|
|
260
307
|
// response whose nag map this CLI could not parse, and the notice is the
|
|
261
308
|
// one surface that must land inside a session that is already running.
|
|
262
|
-
emitPlanLimitThresholdNotice(crossings, resolvedUpgradeUrl, opts);
|
|
309
|
+
emitPlanLimitThresholdNotice(crossings, resolvedUpgradeUrl, { ...opts, account });
|
|
263
310
|
}
|
|
264
311
|
catch {
|
|
265
312
|
// Never throw from record path.
|
|
@@ -287,7 +334,7 @@ function formatPct(entry) {
|
|
|
287
334
|
return `${Math.round((entry.used / entry.limit) * 100)}%`;
|
|
288
335
|
}
|
|
289
336
|
function formatEntryLine(key, entry) {
|
|
290
|
-
return `${key} at ${entry.used}/${entry.limit} (${formatPct(entry)})${dimensionSuffix(key)}`;
|
|
337
|
+
return `${key} at ${formatPlanLimitAmount(key, entry.used)}/${formatPlanLimitAmount(key, entry.limit)} (${formatPct(entry)})${dimensionSuffix(key)}`;
|
|
291
338
|
}
|
|
292
339
|
/**
|
|
293
340
|
* Read the whole state file. A missing, unreadable or malformed file is an
|
|
@@ -360,16 +407,18 @@ function withinNagWindow(shownAt, nowMs) {
|
|
|
360
407
|
/**
|
|
361
408
|
* The over-limit per-turn line (US-036 §2.2). One line, not the old box: it
|
|
362
409
|
* now repeats up to four times a day, and a multi-line box at that cadence
|
|
363
|
-
* reads as breakage rather than as a nag. It
|
|
364
|
-
*
|
|
410
|
+
* reads as breakage rather than as a nag. It names the account and states the
|
|
411
|
+
* facts. It claims no pause: being over is not being refused. A refusal comes
|
|
412
|
+
* back from hq-pro as its own error with its own sentence, and that is the
|
|
413
|
+
* only place a pause is reported (feedback_b1867fe6, where this line told a
|
|
414
|
+
* person their uploads were paused right after two uploads succeeded).
|
|
365
415
|
*/
|
|
366
|
-
export function buildOverLine(overEntries, upgradeUrl) {
|
|
416
|
+
export function buildOverLine(overEntries, upgradeUrl, account = null) {
|
|
367
417
|
const facts = overEntries
|
|
368
|
-
.map(([key, entry]) => `${PLAN_LIMIT_DIMENSION_LABELS[key] ?? key} ${entry.used} of ${entry.limit}`)
|
|
418
|
+
.map(([key, entry]) => `${PLAN_LIMIT_DIMENSION_LABELS[key] ?? key} ${formatPlanLimitAmount(key, entry.used)} of ${formatPlanLimitAmount(key, entry.limit)}`)
|
|
369
419
|
.join(", ");
|
|
370
420
|
// US-024: names no plan and offers no upgrade; the link is the Billing page.
|
|
371
|
-
return
|
|
372
|
-
`Billing: ${upgradeUrl}`);
|
|
421
|
+
return `⚠ Over ${planLimitAccountPhrase(account)}'s limits: ${facts}. Billing: ${upgradeUrl}`;
|
|
373
422
|
}
|
|
374
423
|
// ---------------------------------------------------------------------------
|
|
375
424
|
// Personal feature notice (US-054, copy per US-024)
|
|
@@ -424,12 +473,14 @@ function dimensionName(resource) {
|
|
|
424
473
|
* their own story, and a notice that announces a pause nobody is experiencing
|
|
425
474
|
* reads as a bug.
|
|
426
475
|
*/
|
|
427
|
-
export function renderThresholdNotice(crossing, upgradeUrl) {
|
|
476
|
+
export function renderThresholdNotice(crossing, upgradeUrl, account = null) {
|
|
428
477
|
const name = dimensionName(crossing.resource);
|
|
429
|
-
const counts = `${crossing.used} of ${crossing.limit}`;
|
|
478
|
+
const counts = `${formatPlanLimitAmount(crossing.resource, crossing.used)} of ${formatPlanLimitAmount(crossing.resource, crossing.limit)}`;
|
|
479
|
+
// An unlabelled payload (older hq-pro) keeps the original wording.
|
|
480
|
+
const where = account ? ` for ${planLimitAccountPhrase(account)}` : "";
|
|
430
481
|
const headline = crossing.band === "over"
|
|
431
|
-
? `You are past your ${name} limit (${counts}).`
|
|
432
|
-
: `You have reached 80% of your ${name} limit (${counts}).`;
|
|
482
|
+
? `You are past your ${name} limit${where} (${counts}).`
|
|
483
|
+
: `You have reached 80% of your ${name} limit${where} (${counts}).`;
|
|
433
484
|
return `${headline} Billing: ${upgradeUrl}`;
|
|
434
485
|
}
|
|
435
486
|
/**
|
|
@@ -461,7 +512,7 @@ export function emitPlanLimitThresholdNotice(crossings, upgradeUrl, opts = {}) {
|
|
|
461
512
|
continue;
|
|
462
513
|
thresholdsShownThisSession.set(key, crossing.firstSeenAt);
|
|
463
514
|
printed[key] = crossing.firstSeenAt;
|
|
464
|
-
write(chalk.yellow(renderThresholdNotice(crossing, resolvedUpgradeUrl)) + "\n");
|
|
515
|
+
write(chalk.yellow(renderThresholdNotice(crossing, resolvedUpgradeUrl, opts.account ?? null)) + "\n");
|
|
465
516
|
if (isCliLimitResourceKind(crossing.resource)) {
|
|
466
517
|
queueCliLimitNoticeExposure(crossing.resource, "advisory");
|
|
467
518
|
}
|
|
@@ -489,7 +540,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
489
540
|
const write = opts.write ?? ((s) => process.stderr.write(s));
|
|
490
541
|
const now = opts.now ?? (() => new Date());
|
|
491
542
|
const statePath = opts.statePath ?? defaultStatePath();
|
|
492
|
-
const { limits, anyOver, upgradeUrl } = lastSeen;
|
|
543
|
+
const { limits, anyOver, upgradeUrl, account } = lastSeen;
|
|
493
544
|
const resolvedUpgradeUrl = upgradeUrl ??
|
|
494
545
|
planLimitUpgradeFallbackUrl(resolveProcessFlagCompanyContext().companySlug, PLAN_LIMIT_UPGRADE_URL);
|
|
495
546
|
const entries = Object.entries(limits);
|
|
@@ -504,7 +555,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
504
555
|
return;
|
|
505
556
|
overShownThisSession = true;
|
|
506
557
|
const overEntries = entries.filter(([, e]) => e.over);
|
|
507
|
-
const line = buildOverLine(overEntries, resolvedUpgradeUrl);
|
|
558
|
+
const line = buildOverLine(overEntries, resolvedUpgradeUrl, account);
|
|
508
559
|
write(chalk.yellow(line) + "\n");
|
|
509
560
|
const resourceKind = overEntries[0]?.[0];
|
|
510
561
|
if (resourceKind && isCliLimitResourceKind(resourceKind)) {
|
|
@@ -520,7 +571,7 @@ export function emitPlanLimitNag(opts = {}) {
|
|
|
520
571
|
if (worst === null)
|
|
521
572
|
return;
|
|
522
573
|
warningShownThisSession = true;
|
|
523
|
-
const line = `⚠ Nearing
|
|
574
|
+
const line = `⚠ Nearing ${planLimitAccountPhrase(account)}'s limits: ${formatEntryLine(worst.key, worst.entry)}. Make room before you hit the cap. Billing: ${resolvedUpgradeUrl}`;
|
|
524
575
|
write(chalk.yellow(line) + "\n");
|
|
525
576
|
if (isCliLimitResourceKind(worst.key)) {
|
|
526
577
|
queueCliLimitNoticeExposure(worst.key, "advisory");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IncrementalOptions } from "./incremental.js";
|
|
1
2
|
import { localDate } from "./local-date.js";
|
|
2
3
|
import { type LockTakeover } from "./run-lock.js";
|
|
3
4
|
import { type UploadDeps } from "./steps/upload.js";
|
|
@@ -130,6 +131,9 @@ export interface RunSignalsResult {
|
|
|
130
131
|
alerts?: Record<string, RaiseSignalsFailureAlertsResult>;
|
|
131
132
|
}
|
|
132
133
|
export interface RunSignalsOptions {
|
|
134
|
+
source?: string;
|
|
135
|
+
incremental?: boolean;
|
|
136
|
+
incrementalOptions?: IncrementalOptions;
|
|
133
137
|
hqRoot: string;
|
|
134
138
|
company?: string;
|
|
135
139
|
dryRun?: boolean;
|
|
@@ -12,7 +12,8 @@ import { spawnSync } from "node:child_process";
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
14
|
import yaml from "js-yaml";
|
|
15
|
-
import { loadSignalsSettings } from "./settings.js";
|
|
15
|
+
import { loadSignalsSettings, loadIncrementalSettings } from "./settings.js";
|
|
16
|
+
import { createIncrementalStep } from "./incremental.js";
|
|
16
17
|
import { localDate } from "./local-date.js";
|
|
17
18
|
import { guardedWrite, WriteGuardError } from "./write-guard.js";
|
|
18
19
|
import { acquireCompanyLock, withSummaryLock } from "./run-lock.js";
|
|
@@ -46,6 +47,7 @@ export const defaultSpawn = (cmd, args, opts) => {
|
|
|
46
47
|
cwd: opts.cwd,
|
|
47
48
|
env: opts.env ?? process.env,
|
|
48
49
|
encoding: "utf8",
|
|
50
|
+
input: opts.input,
|
|
49
51
|
maxBuffer: 64 * 1024 * 1024,
|
|
50
52
|
...(opts.timeoutMs !== undefined ? { timeout: opts.timeoutMs, killSignal: "SIGKILL" } : {}),
|
|
51
53
|
});
|
|
@@ -246,9 +248,22 @@ export async function runSignals(opts) {
|
|
|
246
248
|
const stepChoice = opts.step ?? "all";
|
|
247
249
|
const dryRun = opts.dryRun === true;
|
|
248
250
|
const settings = loadSignalsSettings(hqRoot);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
+
if (opts.source !== undefined || opts.incremental) {
|
|
252
|
+
if (!opts.source || !opts.incremental || !opts.company)
|
|
253
|
+
throw new SignalsUsageError("--source and --incremental require each other and --company");
|
|
254
|
+
if ((opts.step && opts.step !== "all" && opts.step !== "extract") || opts.redo?.length)
|
|
255
|
+
throw new SignalsUsageError("--incremental cannot be combined with --redo or another step");
|
|
256
|
+
if (!/^[a-zA-Z0-9_-]+:[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(opts.source))
|
|
257
|
+
throw new SignalsUsageError("invalid --source: expected <channel>:<file>");
|
|
258
|
+
}
|
|
259
|
+
const limits = opts.incremental ? loadIncrementalSettings(hqRoot) : settings;
|
|
260
|
+
const maxTurns = opts.maxTurns ?? limits.maxTurns;
|
|
261
|
+
const maxBudgetUsd = opts.maxBudgetUsd ?? limits.maxBudgetUsd;
|
|
262
|
+
if (!Number.isInteger(maxTurns) || maxTurns < 1 || !Number.isFinite(maxBudgetUsd) || maxBudgetUsd < 0)
|
|
263
|
+
throw new SignalsUsageError("invalid model cost limits");
|
|
251
264
|
const stepImpls = { ...DEFAULT_STEPS, ...(opts.steps ?? {}) };
|
|
265
|
+
if (opts.incremental)
|
|
266
|
+
stepImpls.extract = createIncrementalStep(opts.source, opts.incrementalOptions);
|
|
252
267
|
if (opts.company !== undefined && !SLUG_RE.test(opts.company)) {
|
|
253
268
|
throw new SignalsUsageError(`invalid company slug: ${opts.company}`);
|
|
254
269
|
}
|
|
@@ -266,12 +281,14 @@ export async function runSignals(opts) {
|
|
|
266
281
|
const companies = opts.company !== undefined ? [opts.company] : listCompanies(hqRoot);
|
|
267
282
|
const start = now();
|
|
268
283
|
const date = localDate(start);
|
|
269
|
-
const lateStart = trigger === "job" && start.getHours() >= settings.lateStartHour;
|
|
284
|
+
const lateStart = !opts.incremental && trigger === "job" && start.getHours() >= settings.lateStartHour;
|
|
270
285
|
let plan = stepChoice === "all" ? STEP_ORDER : STEP_ORDER.filter((s) => s === stepChoice);
|
|
271
286
|
if (stepChoice !== "all")
|
|
272
287
|
plan = plan.slice(0, 1);
|
|
273
288
|
if (lateStart)
|
|
274
289
|
plan = plan.filter((s) => s === "garden").slice(0, 1);
|
|
290
|
+
if (opts.incremental)
|
|
291
|
+
plan = ["extract"];
|
|
275
292
|
const records = [];
|
|
276
293
|
const locked = [];
|
|
277
294
|
for (const company of companies) {
|
|
@@ -324,7 +341,10 @@ export async function runSignals(opts) {
|
|
|
324
341
|
};
|
|
325
342
|
const finish = () => {
|
|
326
343
|
const decidedSkip = record.skipReason;
|
|
344
|
+
const previousJobRanAt = record.jobRanAt;
|
|
327
345
|
finalizeRecord(record, { lateStart, lateStartHour: settings.lateStartHour, finishedAt: now() });
|
|
346
|
+
if (opts.incremental)
|
|
347
|
+
record.jobRanAt = previousJobRanAt;
|
|
328
348
|
if (dryRun) {
|
|
329
349
|
if (!decidedSkip) {
|
|
330
350
|
record.status = "dry-run";
|
|
@@ -343,7 +363,7 @@ export async function runSignals(opts) {
|
|
|
343
363
|
// 2. The day's job already ran: do nothing and leave the record as it is.
|
|
344
364
|
const prior = readJson(hqRoot, rel);
|
|
345
365
|
const priorJobRanAt = typeof prior?.jobRanAt === "string" ? prior.jobRanAt : null;
|
|
346
|
-
if (trigger === "job" && !opts.force && prior && priorJobRanAt) {
|
|
366
|
+
if (!opts.incremental && trigger === "job" && !opts.force && prior && priorJobRanAt) {
|
|
347
367
|
process.stderr.write(`hq signals: ${company}: already-ran (the job ran at ${priorJobRanAt})\n`);
|
|
348
368
|
records.push({ ...prior, alreadyRanToday: true });
|
|
349
369
|
continue;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type PriorSignal } from "./steps/extract.js";
|
|
2
|
+
import type { SignalsStep, StepContext } from "./steps/types.js";
|
|
3
|
+
export { meetingSignalSourceRef } from "../hot-channels/meeting-source-ref.js";
|
|
4
|
+
export interface IncrementalCursor {
|
|
5
|
+
revision: number;
|
|
6
|
+
segment_count: number;
|
|
7
|
+
last_run_at: string;
|
|
8
|
+
signal_ids: string[];
|
|
9
|
+
digest: string;
|
|
10
|
+
pending_events?: SignalWritten[];
|
|
11
|
+
}
|
|
12
|
+
export interface SourceSnapshot {
|
|
13
|
+
sourceRef: string;
|
|
14
|
+
text: string;
|
|
15
|
+
segmentCount: number;
|
|
16
|
+
revision?: number;
|
|
17
|
+
audience: string[];
|
|
18
|
+
}
|
|
19
|
+
export type SourceReader = (ctx: StepContext, id: string) => Promise<SourceSnapshot>;
|
|
20
|
+
export type SignalWritten = {
|
|
21
|
+
event: "signal.written";
|
|
22
|
+
signal_id: string;
|
|
23
|
+
type: string;
|
|
24
|
+
source_ref: string;
|
|
25
|
+
company: string;
|
|
26
|
+
key: string;
|
|
27
|
+
};
|
|
28
|
+
export interface IncrementalOptions {
|
|
29
|
+
readers?: Readonly<Record<string, SourceReader>>;
|
|
30
|
+
/** US-004/005 integration seam: one callback per newly persisted signal. */
|
|
31
|
+
onSignalWritten?: (event: SignalWritten) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export declare const SOURCE_READERS: Readonly<Record<string, SourceReader>>;
|
|
34
|
+
export declare function readIncrementalSource(ctx: StepContext, ref: string, readers?: Readonly<Record<string, SourceReader>>): Promise<SourceSnapshot>;
|
|
35
|
+
export declare function cursorPath(company: string, sourceRef: string): string;
|
|
36
|
+
/** Only read the current audience partition, never another audience's signals. */
|
|
37
|
+
export declare function priorSignalsFor(ctx: StepContext, sourceRef: string, audience: string[]): Array<PriorSignal & {
|
|
38
|
+
key: string;
|
|
39
|
+
live: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function createIncrementalStep(source: string, options?: IncrementalOptions): SignalsStep;
|
|
42
|
+
//# sourceMappingURL=incremental.d.ts.map
|