@indigoai-us/hq-cli 5.301.0 → 5.303.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 +26 -0
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +99 -12
- package/assets/scaffold/core/scripts/token-usage-report.sh +72 -20
- package/dist/command-catalog.generated.d.ts +4 -1
- package/dist/command-catalog.generated.js +5 -1
- package/dist/commands/bot.js +1 -0
- package/dist/commands/core-checkpoint.js +1 -0
- package/dist/commands/core-handoff-fast.js +1 -0
- package/dist/commands/core-handoff.js +2 -0
- package/dist/commands/deploy-comments.js +5 -2
- package/dist/commands/integrations-connect.d.ts +3 -0
- package/dist/commands/integrations-connect.js +21 -2
- package/dist/commands/lanes-message.js +33 -3
- package/dist/commands/lanes-questions.d.ts +4 -2
- package/dist/commands/lanes-questions.js +123 -2
- package/dist/commands/lanes-resume.js +25 -4
- package/dist/commands/lanes-shared.d.ts +13 -1
- package/dist/commands/lanes-watch.js +5 -1
- package/dist/commands/lanes.js +91 -9
- package/dist/commands/monitor.js +17 -2
- package/dist/commands/report.js +5 -2
- package/dist/commands/rescue.d.ts +4 -0
- package/dist/commands/rescue.js +27 -17
- package/dist/lib/agent-kit/fallback.d.ts +1 -0
- package/dist/lib/agent-kit/fallback.js +1 -0
- package/dist/lib/bot/cli-update.d.ts +0 -8
- package/dist/lib/bot/cli-update.js +5 -5
- package/dist/lib/bot/daemon.d.ts +5 -1
- package/dist/lib/bot/daemon.js +4 -4
- package/dist/lib/bot/local-jobs.d.ts +1 -21
- package/dist/lib/bot/local-jobs.js +12 -12
- package/dist/lib/core-utils/soft-timeout.js +1 -1
- package/dist/lib/daemon/config.d.ts +0 -1
- package/dist/lib/daemon/config.js +1 -1
- package/dist/lib/deploy-apps.d.ts +13 -0
- package/dist/lib/deploy-apps.js +24 -0
- package/dist/lib/index-render/projects.js +82 -5
- package/dist/lib/lanes/delivery.d.ts +7 -3
- package/dist/lib/lanes/delivery.js +18 -8
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.d.ts +13 -3
- package/dist/lib/lanes/keep-alive.js +29 -5
- package/dist/lib/lanes/outpost-dispatch.d.ts +5 -0
- package/dist/lib/lanes/outpost-dispatch.js +12 -0
- package/dist/lib/lanes/reconcile.d.ts +1 -0
- package/dist/lib/lanes/reconcile.js +358 -38
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +20 -10
- package/dist/lib/lanes/store.js +24 -0
- package/dist/lib/lanes/story-ledger.d.ts +2 -0
- package/dist/lib/lanes/story-ledger.js +8 -0
- package/dist/lib/lanes/types.d.ts +26 -4
- package/dist/lib/lanes/types.js +33 -1
- package/dist/lib/lanes/watch.d.ts +1 -0
- package/dist/lib/lanes/watch.js +19 -6
- package/dist/lib/lanes/watcher.js +139 -9
- package/dist/lib/mesh/live/daemon/run.js +36 -2
- package/dist/lib/report-snapshot.d.ts +0 -2
- package/dist/lib/report-snapshot.js +2 -2
- package/dist/lib/search-index/background.d.ts +1 -0
- package/dist/lib/search-index/background.js +1 -0
- package/dist/utils/run-bundled-script.js +103 -1
- package/dist/utils/update-command-supervisor.cjs +1 -0
- package/dist/utils/version-gate.js +1 -1
- package/package.json +1 -1
package/dist/lib/lanes/spawn.js
CHANGED
|
@@ -333,17 +333,25 @@ function archivedResumeMessageText(hqRoot, laneId, messageId) {
|
|
|
333
333
|
return undefined;
|
|
334
334
|
throw new LanesError("account_retry_context_unreadable", `Could not read the archived message needed to resume lane ${laneId}`, { lane_id: laneId, message_id: messageId });
|
|
335
335
|
}
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
336
|
+
const records = [];
|
|
337
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
338
|
+
if (line.trim().length === 0)
|
|
339
|
+
continue;
|
|
340
|
+
let parsed;
|
|
341
|
+
try {
|
|
342
|
+
parsed = JSON.parse(line);
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
348
|
+
continue;
|
|
349
|
+
records.push(parsed);
|
|
343
350
|
}
|
|
344
|
-
|
|
351
|
+
if (records.length !== 1) {
|
|
345
352
|
throw new LanesError("account_retry_context_invalid", `The archived message needed to resume lane ${laneId} is malformed`, { lane_id: laneId, message_id: messageId });
|
|
346
353
|
}
|
|
354
|
+
const value = records[0];
|
|
347
355
|
if (value === null ||
|
|
348
356
|
typeof value !== "object" ||
|
|
349
357
|
value.message_id !== messageId ||
|
|
@@ -972,7 +980,7 @@ export function nextEnvelopePath(runDir) {
|
|
|
972
980
|
}
|
|
973
981
|
export function envelopeInstruction(envelopePath) {
|
|
974
982
|
return (`When finished, write a JSON object to ${envelopePath} using these key names exactly: ` +
|
|
975
|
-
`schema_version, decision, summary, story_status, artifacts, story_complete, story_complete_reason, questions, blocked_reason. ` +
|
|
983
|
+
`schema_version, decision, summary, story_status, artifacts, story_complete, story_complete_reason, questions, blocked_reason, next_check_at. ` +
|
|
976
984
|
`schema_version must be the exact string "hq-lane-envelope.v1". ` +
|
|
977
985
|
`decision must be one of "done", "ask", "blocked". ` +
|
|
978
986
|
`summary is a string. ` +
|
|
@@ -982,7 +990,8 @@ export function envelopeInstruction(envelopePath) {
|
|
|
982
990
|
`story_complete true requires story_status done and a reason stating what was delivered and how it was verified. ` +
|
|
983
991
|
`story_complete false requires story_status other than done and a reason stating what is still missing. ` +
|
|
984
992
|
`questions is present and non-empty exactly when decision is "ask". ` +
|
|
985
|
-
`blocked_reason is present exactly when decision is "blocked"
|
|
993
|
+
`blocked_reason is present exactly when decision is "blocked". ` +
|
|
994
|
+
`next_check_at is optional; when present, use an ISO date-time for the next time-based keep-alive check.`);
|
|
986
995
|
}
|
|
987
996
|
/**
|
|
988
997
|
* Standing authorization carried by every brief and every resume turn. A
|
|
@@ -1382,6 +1391,7 @@ function spawnDetached(spawnProcess, command, args, env, cwd, stdoutPath, stderr
|
|
|
1382
1391
|
errFd = openLog(stderrPath);
|
|
1383
1392
|
const child = spawnProcess(command, args, {
|
|
1384
1393
|
detached: true,
|
|
1394
|
+
windowsHide: true,
|
|
1385
1395
|
stdio: ["ignore", outFd, errFd],
|
|
1386
1396
|
env,
|
|
1387
1397
|
cwd,
|
package/dist/lib/lanes/store.js
CHANGED
|
@@ -536,6 +536,30 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
536
536
|
raw.envelope_validation_errors.some((error) => typeof error !== "string" || error.trim() === ""))) {
|
|
537
537
|
throw new LaneSchemaError("envelope_validation_errors must be a non-empty array of non-empty strings", { path: filePath, field: "envelope_validation_errors" });
|
|
538
538
|
}
|
|
539
|
+
if (raw.fallback_question_cancellation_failures !== undefined) {
|
|
540
|
+
if (!Array.isArray(raw.fallback_question_cancellation_failures)) {
|
|
541
|
+
throw new LaneSchemaError("fallback_question_cancellation_failures must be an array", {
|
|
542
|
+
path: filePath,
|
|
543
|
+
field: "fallback_question_cancellation_failures",
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
for (const [index, entry] of raw.fallback_question_cancellation_failures.entries()) {
|
|
547
|
+
const field = `fallback_question_cancellation_failures[${index}]`;
|
|
548
|
+
if (!isPlainObject(entry) ||
|
|
549
|
+
Object.keys(entry).some((key) => !["at", "caller", "trigger", "question_ids", "error"].includes(key)) ||
|
|
550
|
+
(entry.trigger !== "message" && entry.trigger !== "resume") ||
|
|
551
|
+
!Array.isArray(entry.question_ids) ||
|
|
552
|
+
entry.question_ids.some((id) => typeof id !== "string" || id.trim() === "")) {
|
|
553
|
+
throw new LaneSchemaError(`${field} must be a fallback question-cancellation failure`, {
|
|
554
|
+
path: filePath,
|
|
555
|
+
field,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
requireTimestamp(entry.at, `${field}.at`, filePath);
|
|
559
|
+
requireString(entry.caller, `${field}.caller`);
|
|
560
|
+
requireString(entry.error, `${field}.error`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
539
563
|
requireTimestamp(raw.created_at, "created_at", filePath);
|
|
540
564
|
requireTimestamp(raw.updated_at, "updated_at", filePath);
|
|
541
565
|
if (raw.queued_reason !== undefined && !isQueuedReason(raw.queued_reason)) {
|
|
@@ -64,6 +64,8 @@ export declare function laneStoryLedgerEnabled(): boolean;
|
|
|
64
64
|
export declare function resolveProjectStoryLedger(hqRoot: string, projectId: string, companySlug?: string): ProjectStoryLedger;
|
|
65
65
|
export declare function validateProjectStories(hqRoot: string, projectId: string, storyIds: readonly string[], companySlug?: string): ProjectStoryLedger;
|
|
66
66
|
export declare function syncLaneToProjectStoryLedger(hqRoot: string, lane: LaneRecord, questionRefs?: readonly StoryLedgerQuestionRef[], deps?: StoryLedgerWriteDeps): void;
|
|
67
|
+
/** Read canonical ledger states for the story ids currently carried by a lane. */
|
|
68
|
+
export declare function readProjectStoryStates(hqRoot: string, projectId: string, storyIds: readonly string[], companySlug?: string): Map<string, string>;
|
|
67
69
|
export declare function buildLaneStatusSnapshot(input: {
|
|
68
70
|
hqRoot: string;
|
|
69
71
|
projectId: string;
|
|
@@ -667,6 +667,14 @@ function storyState(story) {
|
|
|
667
667
|
}
|
|
668
668
|
return story.passes === true ? "done" : "todo";
|
|
669
669
|
}
|
|
670
|
+
/** Read canonical ledger states for the story ids currently carried by a lane. */
|
|
671
|
+
export function readProjectStoryStates(hqRoot, projectId, storyIds, companySlug) {
|
|
672
|
+
const parsed = readLedger(resolveProjectStoryLedger(hqRoot, projectId, companySlug));
|
|
673
|
+
const wanted = new Set(storyIds);
|
|
674
|
+
return new Map(parsed.userStories
|
|
675
|
+
.filter((story) => wanted.has(story.id.trim()))
|
|
676
|
+
.map((story) => [story.id.trim(), storyState(story)]));
|
|
677
|
+
}
|
|
670
678
|
function laneStoryRefs(lane, projectId) {
|
|
671
679
|
return laneStoriesForLedger(lane)
|
|
672
680
|
.filter((story) => story.project_id === projectId)
|
|
@@ -31,6 +31,10 @@ export declare const LANE_ENVELOPE_JSON_SCHEMA: {
|
|
|
31
31
|
readonly type: "string";
|
|
32
32
|
readonly minLength: 1;
|
|
33
33
|
};
|
|
34
|
+
readonly next_check_at: {
|
|
35
|
+
readonly type: "string";
|
|
36
|
+
readonly description: "Optional ISO date-time for a clock-based keep-alive check; invalid values are ignored.";
|
|
37
|
+
};
|
|
34
38
|
readonly questions: {
|
|
35
39
|
readonly type: "array";
|
|
36
40
|
readonly items: {
|
|
@@ -380,6 +384,7 @@ export interface LastEnvelopeRef {
|
|
|
380
384
|
/** PR and merge references carried by the worker envelope. */
|
|
381
385
|
artifacts?: unknown[];
|
|
382
386
|
blocked_reason?: string;
|
|
387
|
+
next_check_at?: string;
|
|
383
388
|
}
|
|
384
389
|
export interface ManagerStatusGate {
|
|
385
390
|
turn_started_at: string;
|
|
@@ -404,9 +409,12 @@ export interface BlockerFreshnessState {
|
|
|
404
409
|
stale_labels: string[];
|
|
405
410
|
/** Hash of the last stale set already escalated to the senior. */
|
|
406
411
|
escalated_fingerprint?: string;
|
|
412
|
+
/** The same escalation was recorded locally because a session inbox was unavailable. */
|
|
413
|
+
undeliverable_fingerprint?: string;
|
|
414
|
+
undeliverable_reason?: string;
|
|
407
415
|
}
|
|
408
416
|
export interface BlockerFreshnessHistoryEntry {
|
|
409
|
-
event: "blocker_stale" | "blocker_stale_escalated";
|
|
417
|
+
event: "blocker_stale" | "blocker_stale_escalated" | "blocker_stale_escalation_undeliverable";
|
|
410
418
|
turn_id: string;
|
|
411
419
|
path: string;
|
|
412
420
|
at: string;
|
|
@@ -416,6 +424,8 @@ export interface BlockerFreshnessHistoryEntry {
|
|
|
416
424
|
resume_queued?: boolean;
|
|
417
425
|
resume_skipped_reason?: "open_question" | "blocked" | "miss_limit" | "no_provider_session";
|
|
418
426
|
delivery_failed?: true;
|
|
427
|
+
reason?: string;
|
|
428
|
+
delivery_recovered?: true;
|
|
419
429
|
}
|
|
420
430
|
export interface KrFreshnessState {
|
|
421
431
|
/** Stable fingerprint of the last stalled KPI set, which is resumed once. */
|
|
@@ -441,6 +451,13 @@ export interface LaneResumeHistoryEntry {
|
|
|
441
451
|
/** HQ lane id for an agent caller, or `user` for an explicit human CLI action. */
|
|
442
452
|
caller?: string;
|
|
443
453
|
}
|
|
454
|
+
export interface FallbackQuestionCancellationFailure {
|
|
455
|
+
at: string;
|
|
456
|
+
caller: string;
|
|
457
|
+
trigger: "message" | "resume";
|
|
458
|
+
question_ids: string[];
|
|
459
|
+
error: string;
|
|
460
|
+
}
|
|
444
461
|
export interface KrFreshnessHistoryEntry {
|
|
445
462
|
event: "kr_stalled";
|
|
446
463
|
turn_id: string;
|
|
@@ -694,6 +711,8 @@ export interface LaneRecord {
|
|
|
694
711
|
last_resume?: LaneResumeRecord;
|
|
695
712
|
/** Last 50 lane worker starts after the initial start, including actor provenance. */
|
|
696
713
|
resume_history?: LaneResumeHistoryEntry[];
|
|
714
|
+
/** Bounded failures while closing escalated questions after a fallback resume. */
|
|
715
|
+
fallback_question_cancellation_failures?: FallbackQuestionCancellationFailure[];
|
|
697
716
|
/** Cache counters from the last exited run's result. Nothing else from the payload. */
|
|
698
717
|
last_run_usage?: LaneRunUsage;
|
|
699
718
|
/** Completion time paired with last_run_usage; written by the watcher. */
|
|
@@ -750,7 +769,7 @@ export interface PendingResume {
|
|
|
750
769
|
trigger: "answered_questions" | "message" | "resume";
|
|
751
770
|
/** True when this queued resume was initiated automatically by keep-alive. */
|
|
752
771
|
keep_alive?: true;
|
|
753
|
-
resumed_from?: "interrupted" | "done" | "blocked" | "failed";
|
|
772
|
+
resumed_from?: "interrupted" | "done" | "blocked" | "failed" | "awaiting_input";
|
|
754
773
|
from_failed?: true;
|
|
755
774
|
/** Senior that authorized a failed-lane resume, rechecked at the claim CAS. */
|
|
756
775
|
expected_senior?: SeniorRef;
|
|
@@ -785,9 +804,10 @@ export interface LaneResumeRecord {
|
|
|
785
804
|
monitor_event?: true;
|
|
786
805
|
/**
|
|
787
806
|
* `hq lanes resume` only: the state the lane was in when the operator
|
|
788
|
-
* resumed it (`interrupted`, `done` or `
|
|
807
|
+
* resumed it (`interrupted`, `done`, `blocked`, `failed` or `awaiting_input`)
|
|
808
|
+
* and the text they sent.
|
|
789
809
|
*/
|
|
790
|
-
resumed_from?: "interrupted" | "done" | "blocked" | "failed";
|
|
810
|
+
resumed_from?: "interrupted" | "done" | "blocked" | "failed" | "awaiting_input";
|
|
791
811
|
from_failed?: true;
|
|
792
812
|
text?: string;
|
|
793
813
|
/** False when the continued turn explicitly skipped compaction. */
|
|
@@ -888,6 +908,7 @@ export interface LaneEnvelope {
|
|
|
888
908
|
schema_version: typeof ENVELOPE_SCHEMA_VERSION;
|
|
889
909
|
decision: EnvelopeDecision;
|
|
890
910
|
summary: string;
|
|
911
|
+
next_check_at?: string;
|
|
891
912
|
questions?: EnvelopeQuestion[];
|
|
892
913
|
story_status: StoryStatus;
|
|
893
914
|
artifacts: unknown[];
|
|
@@ -993,6 +1014,7 @@ export interface LanesConfig {
|
|
|
993
1014
|
/** Age in hours after which a manager KPI is considered stalled. */
|
|
994
1015
|
kr_freshness_hours: number;
|
|
995
1016
|
}
|
|
1017
|
+
export declare function isIsoDateTime(value: string): boolean;
|
|
996
1018
|
export declare function parseMajorSchema(schemaVersion: string, expectedPrefix: string, expectedMajor: number): void;
|
|
997
1019
|
export declare function parseLaneEnvelope(raw: unknown): LaneEnvelope;
|
|
998
1020
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -25,6 +25,10 @@ export const LANE_ENVELOPE_JSON_SCHEMA = {
|
|
|
25
25
|
schema_version: { const: "hq-lane-envelope.v1" },
|
|
26
26
|
decision: { enum: ["done", "ask", "blocked"] },
|
|
27
27
|
summary: { type: "string", minLength: 1 },
|
|
28
|
+
next_check_at: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Optional ISO date-time for a clock-based keep-alive check; invalid values are ignored.",
|
|
31
|
+
},
|
|
28
32
|
questions: {
|
|
29
33
|
type: "array",
|
|
30
34
|
items: {
|
|
@@ -226,7 +230,7 @@ export const LANE_FIELD_WRITERS = {
|
|
|
226
230
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
227
231
|
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], registry_worker: ["creator", "command", "spawn"], registry_worker_approval_history: ["command", "watcher", "reconcile"], worker_profile_repin_history: ["command"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
228
232
|
keep_alive: ["creator", "command"], keep_alive_standing: ["command"], keep_alive_opted_in_at: ["creator", "command"], keep_alive_blocked_reason: ["command", "reconcile"], keep_alive_resume_count: ["command", "reconcile"], keep_alive_history: ["reconcile"], keep_alive_blocked_resume_streak: ["reconcile", "spawn", "watcher"], keep_alive_last_resume_at: ["command", "reconcile"], keep_alive_next_eligible_at: ["command", "reconcile"], keep_alive_exhausted_at: ["command", "reconcile"],
|
|
229
|
-
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
233
|
+
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], account_selection: ["creator", "command"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], failed_resume_history: ["command"], fallback_question_cancellation_failures: ["command"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
230
234
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], ended_at: ["reconcile"], envelope_summary: ["reconcile"], attribution_history: ["command", "watcher", "reconcile"], story_switch_pending: ["command", "watcher", "reconcile"], story_switch_unacknowledged: ["watcher", "reconcile"],
|
|
231
235
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn", "command"], effort_source: ["creator", "spawn", "command"], fast_mode: ["spawn"],
|
|
232
236
|
last_message_at: ["command"], status_gate: ["spawn", "watcher"], status_gate_history: ["watcher", "reconcile"], blocker_freshness_state: ["watcher"], kr_freshness_state: ["watcher"], last_compaction: ["spawn"], story_complete: ["watcher", "reconcile"], story_complete_reason: ["watcher", "reconcile"], story_status: ["watcher", "reconcile"],
|
|
@@ -246,6 +250,24 @@ function requireString(value, field) {
|
|
|
246
250
|
}
|
|
247
251
|
return value;
|
|
248
252
|
}
|
|
253
|
+
export function isIsoDateTime(value) {
|
|
254
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-](\d{2}):(\d{2}))$/.exec(value);
|
|
255
|
+
if (!match)
|
|
256
|
+
return false;
|
|
257
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, , offsetHourText, offsetMinuteText] = match;
|
|
258
|
+
const year = Number(yearText);
|
|
259
|
+
const month = Number(monthText);
|
|
260
|
+
const day = Number(dayText);
|
|
261
|
+
const hour = Number(hourText);
|
|
262
|
+
const minute = Number(minuteText);
|
|
263
|
+
const second = Number(secondText);
|
|
264
|
+
const offsetHour = offsetHourText === undefined ? 0 : Number(offsetHourText);
|
|
265
|
+
const offsetMinute = offsetMinuteText === undefined ? 0 : Number(offsetMinuteText);
|
|
266
|
+
const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
267
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth &&
|
|
268
|
+
hour <= 23 && minute <= 59 && second <= 59 && offsetHour <= 23 && offsetMinute <= 59 &&
|
|
269
|
+
Number.isFinite(Date.parse(value));
|
|
270
|
+
}
|
|
249
271
|
export function parseMajorSchema(schemaVersion, expectedPrefix, expectedMajor) {
|
|
250
272
|
const match = new RegExp(`^${expectedPrefix}\\.v(\\d+)$`).exec(schemaVersion);
|
|
251
273
|
if (!match) {
|
|
@@ -264,6 +286,7 @@ export function parseLaneEnvelope(raw) {
|
|
|
264
286
|
"schema_version",
|
|
265
287
|
"decision",
|
|
266
288
|
"summary",
|
|
289
|
+
"next_check_at",
|
|
267
290
|
"questions",
|
|
268
291
|
"story_status",
|
|
269
292
|
"artifacts",
|
|
@@ -280,6 +303,13 @@ export function parseLaneEnvelope(raw) {
|
|
|
280
303
|
throw new LaneSchemaError(`Unknown envelope decision ${JSON.stringify(raw.decision)}. Accepted: ${ENVELOPE_DECISIONS.join(", ")}`, { accepted: [...ENVELOPE_DECISIONS], value: raw.decision });
|
|
281
304
|
}
|
|
282
305
|
const summary = requireString(raw.summary, "summary");
|
|
306
|
+
let nextCheckAt;
|
|
307
|
+
if (raw.next_check_at !== undefined) {
|
|
308
|
+
if (typeof raw.next_check_at !== "string") {
|
|
309
|
+
throw new LaneSchemaError("next_check_at must be a string", { field: "next_check_at" });
|
|
310
|
+
}
|
|
311
|
+
nextCheckAt = raw.next_check_at;
|
|
312
|
+
}
|
|
283
313
|
if (typeof raw.story_status !== "string" ||
|
|
284
314
|
!STORY_STATUSES.includes(raw.story_status)) {
|
|
285
315
|
throw new LaneSchemaError(`Unknown story_status ${JSON.stringify(raw.story_status)}. Accepted: ${STORY_STATUSES.join(", ")}`, { accepted: [...STORY_STATUSES], value: raw.story_status });
|
|
@@ -344,6 +374,8 @@ export function parseLaneEnvelope(raw) {
|
|
|
344
374
|
story_status: storyStatus,
|
|
345
375
|
artifacts: raw.artifacts,
|
|
346
376
|
};
|
|
377
|
+
if (nextCheckAt !== undefined)
|
|
378
|
+
envelope.next_check_at = nextCheckAt;
|
|
347
379
|
if (storyComplete !== undefined) {
|
|
348
380
|
envelope.story_complete = storyComplete;
|
|
349
381
|
envelope.story_complete_reason = storyCompleteReason;
|
|
@@ -23,6 +23,7 @@ export interface LaneWatchDeps {
|
|
|
23
23
|
readQuestion?: (hqRoot: string, questionId: string) => QuestionRecord | null;
|
|
24
24
|
readAccountStatuses?: (lanes: readonly LaneRecord[]) => Promise<readonly LaneWatchAccountStatus[]>;
|
|
25
25
|
keepAliveEnabled?: (lane: LaneRecord) => boolean | Promise<boolean>;
|
|
26
|
+
keepAliveNextCheckAtEnabled?: (lane: LaneRecord) => boolean | Promise<boolean>;
|
|
26
27
|
sleep?: (ms: number) => Promise<void>;
|
|
27
28
|
intervalMs?: number;
|
|
28
29
|
maxOutputLines?: number;
|
package/dist/lib/lanes/watch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { listQuestionIds, readLane, readQuestion, } from "./store.js";
|
|
2
2
|
import { LaneNotFoundError, LanesError } from "./errors.js";
|
|
3
3
|
import { providerSupportsSessionResume } from "./delivery.js";
|
|
4
|
-
import { canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, } from "./keep-alive.js";
|
|
4
|
+
import { canResumeDoneKeepAliveLane, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, } from "./keep-alive.js";
|
|
5
5
|
import { isTerminalLaneState, } from "./types.js";
|
|
6
6
|
export const DEFAULT_LANE_WATCH_INTERVAL_MS = 5_000;
|
|
7
7
|
export const MAX_LANE_WATCH_INTERVAL_MS = 30_000;
|
|
@@ -32,7 +32,7 @@ function validateLaneIds(laneIds) {
|
|
|
32
32
|
}
|
|
33
33
|
return unique;
|
|
34
34
|
}
|
|
35
|
-
function canResumeKeepAliveLane(hqRoot, lane) {
|
|
35
|
+
function canResumeKeepAliveLane(hqRoot, lane, nextCheckAtEnabled = false, nowMs = Date.now()) {
|
|
36
36
|
if (lane.keep_alive !== true ||
|
|
37
37
|
lane.keep_alive_opted_in_at === undefined ||
|
|
38
38
|
!Number.isFinite(Date.parse(lane.keep_alive_opted_in_at)) ||
|
|
@@ -49,7 +49,15 @@ function canResumeKeepAliveLane(hqRoot, lane) {
|
|
|
49
49
|
return lane.terminal_reason === "exited_with_partial_output";
|
|
50
50
|
}
|
|
51
51
|
if (lane.state === "done") {
|
|
52
|
-
|
|
52
|
+
const hasPendingWork = lane.keep_alive_standing === true || nextCheckAtEnabled
|
|
53
|
+
? hasPendingKeepAliveWork(hqRoot, lane)
|
|
54
|
+
: false;
|
|
55
|
+
const nextCheckAtMs = nextCheckAtEnabled && !hasPendingWork
|
|
56
|
+
? keepAliveNextCheckAtMs(lane.last_envelope?.next_check_at, nowMs, Date.parse(lane.last_envelope?.at ?? lane.updated_at))
|
|
57
|
+
: undefined;
|
|
58
|
+
if (nextCheckAtMs !== undefined && nowMs < nextCheckAtMs)
|
|
59
|
+
return false;
|
|
60
|
+
return canResumeDoneKeepAliveLane(lane, { decision: lane.last_envelope?.decision, story_complete: lane.story_complete, next_check_at: lane.last_envelope?.next_check_at }, lane.keep_alive_standing === true && hasPendingWork) &&
|
|
53
61
|
lane.terminal_reason !== "provider_rejected" &&
|
|
54
62
|
lane.terminal_reason !== "account_not_logged_in";
|
|
55
63
|
}
|
|
@@ -131,9 +139,14 @@ export async function runLaneWatch(hqRoot, laneIds, deps = {}) {
|
|
|
131
139
|
const keepAliveWaiting = new Set();
|
|
132
140
|
if (deps.keepAliveEnabled !== undefined) {
|
|
133
141
|
for (const record of records) {
|
|
134
|
-
if (isTerminalLaneState(record.state)
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
if (!isTerminalLaneState(record.state) || !canResumeKeepAliveLane(hqRoot, record))
|
|
143
|
+
continue;
|
|
144
|
+
if (!await deps.keepAliveEnabled(record))
|
|
145
|
+
continue;
|
|
146
|
+
const nextCheckAtEnabled = deps.keepAliveNextCheckAtEnabled === undefined
|
|
147
|
+
? false
|
|
148
|
+
: await deps.keepAliveNextCheckAtEnabled(record);
|
|
149
|
+
if (canResumeKeepAliveLane(hqRoot, record, nextCheckAtEnabled))
|
|
137
150
|
keepAliveWaiting.add(record.lane_id);
|
|
138
151
|
}
|
|
139
152
|
}
|
|
@@ -424,6 +424,7 @@ export function applyTerminalToLaneWithResult(hqRoot, laneId, classification, op
|
|
|
424
424
|
at,
|
|
425
425
|
artifacts: envelope.artifacts,
|
|
426
426
|
...(envelope.blocked_reason !== undefined ? { blocked_reason: envelope.blocked_reason } : {}),
|
|
427
|
+
...(envelope.next_check_at !== undefined ? { next_check_at: envelope.next_check_at } : {}),
|
|
427
428
|
};
|
|
428
429
|
if (storySwitch.clearPending) {
|
|
429
430
|
next.story_switch_pending = undefined;
|
|
@@ -769,15 +770,30 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
769
770
|
const turnId = manifestTurnId ?? lane.status_gate?.turn_id ?? `unknown-${at}`;
|
|
770
771
|
const resumeText = `These open blockers in ${resolution.statusPath} were not re-verified in the last ${resolution.windowHours}h: ${staleLabels.join(", ")}. Check each one now; set verified_at on the ones that still hold and close the rest, then end the turn.`;
|
|
771
772
|
let deliverEscalation = false;
|
|
773
|
+
let deliverFallback = false;
|
|
774
|
+
let retryUndeliverable = false;
|
|
775
|
+
let undeliverableReason;
|
|
772
776
|
let escalationText = "";
|
|
777
|
+
const sessionInbox = lane.senior.kind === "session"
|
|
778
|
+
? lane.senior.id
|
|
779
|
+
? findBlockerFreshnessSessionInbox(hqRoot, lane.senior.id)
|
|
780
|
+
: { reason: "The session senior has no session id" }
|
|
781
|
+
: undefined;
|
|
773
782
|
let updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
|
|
774
783
|
deliverEscalation = false;
|
|
784
|
+
deliverFallback = false;
|
|
785
|
+
retryUndeliverable = false;
|
|
786
|
+
undeliverableReason = undefined;
|
|
775
787
|
const prior = current.blocker_freshness_state;
|
|
776
788
|
const repeated = prior !== undefined && staleKeys.some((key) => prior.stale_keys.includes(key));
|
|
777
789
|
const history = current.status_gate_history ?? [];
|
|
778
790
|
const alreadyEscalatedThisTurn = history.some((entry) => entry.event === "blocker_stale_escalated" && entry.turn_id === turnId && entry.fingerprint === fingerprint);
|
|
779
791
|
const escalate = repeated && prior?.escalated_fingerprint !== fingerprint && !alreadyEscalatedThisTurn;
|
|
780
792
|
const alreadyEscalatedSet = prior?.escalated_fingerprint === fingerprint;
|
|
793
|
+
const pendingUndeliverable = prior?.undeliverable_fingerprint === fingerprint;
|
|
794
|
+
const failedFallback = history.find((entry) => entry.event === "blocker_stale_escalation_undeliverable" &&
|
|
795
|
+
entry.fingerprint === fingerprint && entry.delivery_failed === true &&
|
|
796
|
+
entry.delivery_recovered !== true);
|
|
781
797
|
const statusResumeQueued = history.some((entry) => entry.event === "status_missed" && entry.turn_id === turnId && entry.resume_queued === true);
|
|
782
798
|
let resumeQueued = false;
|
|
783
799
|
let resumeSkippedReason;
|
|
@@ -819,6 +835,37 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
819
835
|
escalatedFingerprint = fingerprint;
|
|
820
836
|
deliverEscalation = true;
|
|
821
837
|
escalationText = `Blocker freshness escalation for ${current.lane_id}: these blockers remain stale after consecutive turns: ${staleLabels.join(", ")}. Status file: ${resolution.statusPath}.`;
|
|
838
|
+
if (lane.senior.kind === "session" && sessionInbox !== undefined && !("inbox" in sessionInbox)) {
|
|
839
|
+
deliverFallback = true;
|
|
840
|
+
undeliverableReason = sessionInbox.reason;
|
|
841
|
+
nextHistory = appendCappedHistory(nextHistory, {
|
|
842
|
+
event: "blocker_stale_escalation_undeliverable",
|
|
843
|
+
turn_id: turnId,
|
|
844
|
+
path: resolution.statusPath,
|
|
845
|
+
at,
|
|
846
|
+
labels: staleLabels,
|
|
847
|
+
count: resolution.stale.length,
|
|
848
|
+
fingerprint,
|
|
849
|
+
reason: sessionInbox.reason,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
else if (pendingUndeliverable && sessionInbox !== undefined && "inbox" in sessionInbox) {
|
|
854
|
+
// A monitor inbox registered after the local fallback appeared; retry delivery once.
|
|
855
|
+
deliverEscalation = true;
|
|
856
|
+
retryUndeliverable = true;
|
|
857
|
+
escalationText = `Blocker freshness escalation for ${current.lane_id}: these blockers remain stale after consecutive turns: ${staleLabels.join(", ")}. Status file: ${resolution.statusPath}.`;
|
|
858
|
+
}
|
|
859
|
+
else if (failedFallback !== undefined && sessionInbox !== undefined && "inbox" in sessionInbox) {
|
|
860
|
+
// Prefer the newly available senior inbox over retrying a failed local fallback.
|
|
861
|
+
deliverEscalation = true;
|
|
862
|
+
retryUndeliverable = true;
|
|
863
|
+
escalationText = `Blocker freshness escalation for ${current.lane_id}: these blockers remain stale after consecutive turns: ${staleLabels.join(", ")}. Status file: ${resolution.statusPath}.`;
|
|
864
|
+
}
|
|
865
|
+
else if (failedFallback !== undefined) {
|
|
866
|
+
deliverFallback = true;
|
|
867
|
+
undeliverableReason = failedFallback.reason ?? prior?.undeliverable_reason ?? "The session senior inbox is unavailable";
|
|
868
|
+
escalationText = `Blocker freshness escalation for ${current.lane_id}: these blockers remain stale after consecutive turns: ${staleLabels.join(", ")}. Status file: ${resolution.statusPath}.`;
|
|
822
869
|
}
|
|
823
870
|
else if (prior !== undefined && blockerFreshnessFingerprint(prior.stale_keys.map((key, index) => ({ key, label: prior.stale_labels[index] ?? "" }))) !== fingerprint) {
|
|
824
871
|
escalatedFingerprint = undefined;
|
|
@@ -829,13 +876,68 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
829
876
|
stale_keys: staleKeys,
|
|
830
877
|
stale_labels: staleLabels,
|
|
831
878
|
...(escalatedFingerprint !== undefined ? { escalated_fingerprint: escalatedFingerprint } : {}),
|
|
879
|
+
...(undeliverableReason !== undefined
|
|
880
|
+
? { undeliverable_fingerprint: fingerprint, undeliverable_reason: undeliverableReason }
|
|
881
|
+
: prior?.undeliverable_fingerprint === fingerprint
|
|
882
|
+
? { undeliverable_fingerprint: fingerprint, undeliverable_reason: prior.undeliverable_reason }
|
|
883
|
+
: {}),
|
|
832
884
|
},
|
|
833
885
|
status_gate_history: nextHistory,
|
|
834
886
|
};
|
|
835
887
|
});
|
|
836
888
|
logDecision({ event: "blocker_stale", at, turn_id: turnId, path: resolution.statusPath, labels: staleLabels, count: staleLabels.length });
|
|
837
|
-
const escalated = updated.status_gate_history?.find((entry) => entry.event === "blocker_stale_escalated" && entry.
|
|
838
|
-
|
|
889
|
+
const escalated = updated.status_gate_history?.find((entry) => entry.event === "blocker_stale_escalated" && entry.fingerprint === fingerprint &&
|
|
890
|
+
(entry.turn_id === turnId || retryUndeliverable || deliverFallback));
|
|
891
|
+
if (escalated && deliverFallback) {
|
|
892
|
+
const reason = undeliverableReason ?? "The session senior has no unique monitor-session inbox";
|
|
893
|
+
try {
|
|
894
|
+
appendDropboxMessageAtPath(path.join(laneRunDir(hqRoot, lane.lane_id), "dropbox"), {
|
|
895
|
+
at,
|
|
896
|
+
message_id: `blocker-freshness-undeliverable:${lane.lane_id}:${fingerprint}`,
|
|
897
|
+
kind: "blocker_stale_escalation_undeliverable",
|
|
898
|
+
text: `${escalationText} The session senior inbox is unavailable (${reason}); this escalation is recorded in the lane inbox for its next turn.`,
|
|
899
|
+
});
|
|
900
|
+
updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
|
|
901
|
+
const state = current.blocker_freshness_state;
|
|
902
|
+
if (state === undefined)
|
|
903
|
+
return current;
|
|
904
|
+
return {
|
|
905
|
+
...current,
|
|
906
|
+
blocker_freshness_state: {
|
|
907
|
+
...state,
|
|
908
|
+
undeliverable_fingerprint: fingerprint,
|
|
909
|
+
undeliverable_reason: reason,
|
|
910
|
+
},
|
|
911
|
+
status_gate_history: (current.status_gate_history ?? []).map((entry) => entry.event === "blocker_stale_escalation_undeliverable" &&
|
|
912
|
+
entry.fingerprint === fingerprint && entry.delivery_failed === true
|
|
913
|
+
? { ...entry, delivery_recovered: true }
|
|
914
|
+
: entry),
|
|
915
|
+
};
|
|
916
|
+
});
|
|
917
|
+
logDecision({ event: "blocker_stale_escalation_undeliverable", at, turn_id: turnId, fingerprint, reason, fallback: "lane_dropbox" });
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
921
|
+
process.stderr.write(`hq lanes watcher: blocker freshness fallback delivery failed for ${lane.lane_id}: ${message}\n`);
|
|
922
|
+
logDecision({ event: "blocker_stale_escalation_fallback_failed", at, turn_id: turnId, fingerprint, error: message });
|
|
923
|
+
updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
|
|
924
|
+
const state = current.blocker_freshness_state;
|
|
925
|
+
const retryableState = state === undefined ? undefined : { ...state };
|
|
926
|
+
if (retryableState !== undefined) {
|
|
927
|
+
delete retryableState.undeliverable_fingerprint;
|
|
928
|
+
delete retryableState.undeliverable_reason;
|
|
929
|
+
}
|
|
930
|
+
return {
|
|
931
|
+
...current,
|
|
932
|
+
...(retryableState !== undefined ? { blocker_freshness_state: retryableState } : {}),
|
|
933
|
+
status_gate_history: (current.status_gate_history ?? []).map((entry) => entry.event === "blocker_stale_escalation_undeliverable" && entry.fingerprint === fingerprint
|
|
934
|
+
? { ...entry, delivery_failed: true }
|
|
935
|
+
: entry),
|
|
936
|
+
};
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if ((escalated || retryUndeliverable) && deliverEscalation && !deliverFallback) {
|
|
839
941
|
try {
|
|
840
942
|
if (lane.senior.kind === "lane" && lane.senior.id) {
|
|
841
943
|
if (deps.deliverLaneSeniorMessage === undefined) {
|
|
@@ -844,7 +946,9 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
844
946
|
await deps.deliverLaneSeniorMessage(lane.senior.id, escalationText);
|
|
845
947
|
}
|
|
846
948
|
else if (lane.senior.kind === "session" && lane.senior.id) {
|
|
847
|
-
const inbox =
|
|
949
|
+
const inbox = sessionInbox !== undefined && "inbox" in sessionInbox
|
|
950
|
+
? sessionInbox.inbox
|
|
951
|
+
: resolveBlockerFreshnessSessionInbox(hqRoot, lane.senior.id);
|
|
848
952
|
appendDropboxMessageAtPath(inbox, {
|
|
849
953
|
at,
|
|
850
954
|
message_id: `blocker-freshness:${lane.lane_id}:${fingerprint}:${turnId}`,
|
|
@@ -856,6 +960,26 @@ async function settleBlockerFreshness(hqRoot, lane, manifestTurnId, at, deps, me
|
|
|
856
960
|
throw new LanesError("blocker_freshness_delivery_unavailable", "No durable senior inbox is available for this blocker escalation", { lane_id: lane.lane_id, senior_kind: lane.senior.kind });
|
|
857
961
|
}
|
|
858
962
|
logDecision({ event: "blocker_stale_escalation_delivered", at, turn_id: turnId, senior_kind: lane.senior.kind });
|
|
963
|
+
if (retryUndeliverable) {
|
|
964
|
+
updated = retryUpdateLane(hqRoot, lane.lane_id, "watcher", (current) => {
|
|
965
|
+
const state = current.blocker_freshness_state;
|
|
966
|
+
const failedFallbackStillOpen = (current.status_gate_history ?? []).some((entry) => entry.event === "blocker_stale_escalation_undeliverable" &&
|
|
967
|
+
entry.fingerprint === fingerprint && entry.delivery_failed === true &&
|
|
968
|
+
entry.delivery_recovered !== true);
|
|
969
|
+
if (state?.undeliverable_fingerprint !== fingerprint && !failedFallbackStillOpen)
|
|
970
|
+
return current;
|
|
971
|
+
const recoveredHistory = (current.status_gate_history ?? []).map((entry) => entry.event === "blocker_stale_escalation_undeliverable" && entry.fingerprint === fingerprint
|
|
972
|
+
? { ...entry, delivery_recovered: true }
|
|
973
|
+
: entry);
|
|
974
|
+
if (state?.undeliverable_fingerprint === fingerprint) {
|
|
975
|
+
const recoveredState = { ...state };
|
|
976
|
+
delete recoveredState.undeliverable_fingerprint;
|
|
977
|
+
delete recoveredState.undeliverable_reason;
|
|
978
|
+
return { ...current, blocker_freshness_state: recoveredState, status_gate_history: recoveredHistory };
|
|
979
|
+
}
|
|
980
|
+
return { ...current, status_gate_history: recoveredHistory };
|
|
981
|
+
});
|
|
982
|
+
}
|
|
859
983
|
}
|
|
860
984
|
catch (error) {
|
|
861
985
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -950,7 +1074,7 @@ async function settleKrFreshness(hqRoot, lane, manifestTurnId, at, deps, logDeci
|
|
|
950
1074
|
};
|
|
951
1075
|
});
|
|
952
1076
|
}
|
|
953
|
-
function
|
|
1077
|
+
function findBlockerFreshnessSessionInbox(hqRoot, sessionId) {
|
|
954
1078
|
let candidates;
|
|
955
1079
|
try {
|
|
956
1080
|
candidates = LANE_PROVIDERS
|
|
@@ -958,14 +1082,20 @@ function resolveBlockerFreshnessSessionInbox(hqRoot, sessionId) {
|
|
|
958
1082
|
.filter((sessionDir) => fs.existsSync(sessionDir));
|
|
959
1083
|
}
|
|
960
1084
|
catch (error) {
|
|
961
|
-
|
|
1085
|
+
return { reason: `Could not resolve the monitor inbox for session senior: ${error instanceof Error ? error.message : String(error)}` };
|
|
962
1086
|
}
|
|
963
1087
|
if (candidates.length !== 1) {
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1088
|
+
return { reason: candidates.length === 0
|
|
1089
|
+
? "No monitor-session inbox is registered for the session senior"
|
|
1090
|
+
: "The session senior matches more than one monitor-session inbox" };
|
|
967
1091
|
}
|
|
968
|
-
return path.join(candidates[0], "dropbox");
|
|
1092
|
+
return { inbox: path.join(candidates[0], "dropbox") };
|
|
1093
|
+
}
|
|
1094
|
+
function resolveBlockerFreshnessSessionInbox(hqRoot, sessionId) {
|
|
1095
|
+
const result = findBlockerFreshnessSessionInbox(hqRoot, sessionId);
|
|
1096
|
+
if ("inbox" in result)
|
|
1097
|
+
return result.inbox;
|
|
1098
|
+
throw new LanesError("blocker_freshness_session_inbox_unavailable", result.reason, { session_id: sessionId });
|
|
969
1099
|
}
|
|
970
1100
|
/**
|
|
971
1101
|
* Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
|
|
@@ -449,6 +449,40 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
449
449
|
})).status === "authorized",
|
|
450
450
|
});
|
|
451
451
|
});
|
|
452
|
+
let boardRefreshFailureActive = false;
|
|
453
|
+
const refreshBoardsWithDiagnostics = async () => {
|
|
454
|
+
try {
|
|
455
|
+
const result = await refreshFn();
|
|
456
|
+
if (boardRefreshFailureActive) {
|
|
457
|
+
log(dir, "mesh-board-refresh.recovered");
|
|
458
|
+
boardRefreshFailureActive = false;
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
if (!boardRefreshFailureActive) {
|
|
464
|
+
const rawName = error instanceof Error ? error.name : typeof error;
|
|
465
|
+
const errorName = rawName.replace(/[^A-Za-z0-9_.-]/g, "").slice(0, 80) || "Error";
|
|
466
|
+
const rawMessage = error instanceof Error
|
|
467
|
+
? error.message
|
|
468
|
+
: typeof error === "string"
|
|
469
|
+
? error
|
|
470
|
+
: "non-Error rejection";
|
|
471
|
+
const message = rawMessage
|
|
472
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
473
|
+
.replace(/Bearer\s+\S+/gi, "Bearer [REDACTED]")
|
|
474
|
+
.replace(/op:\/\/[^\s"]+/gi, "op://[REDACTED]")
|
|
475
|
+
.replace(/https?:\/\/[^\s"<>]+/gi, "[URL]")
|
|
476
|
+
.replace(/\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]+|AKIA[0-9A-Z]{16}|eyJ[A-Za-z0-9_.-]{10,})\b/gi, "[REDACTED]")
|
|
477
|
+
.replace(/\b(?:access[_-]?token|refresh[_-]?token|id[_-]?token|token|cookie|authorization)\b\s*[:=]\s*[^\s,;&]+/gi, "credential=[REDACTED]")
|
|
478
|
+
.replace(/\b(?:cmp|prs|prj|sess?)_[A-Za-z0-9_-]+\b/gi, "[ID]")
|
|
479
|
+
.slice(0, 160);
|
|
480
|
+
log(dir, `mesh-board-refresh.failed errorName=${errorName} message=${message || "[empty]"}`);
|
|
481
|
+
}
|
|
482
|
+
boardRefreshFailureActive = true;
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
452
486
|
let presence = null;
|
|
453
487
|
let workCache = null;
|
|
454
488
|
if (deps.enablePresence !== false) {
|
|
@@ -743,7 +777,7 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
743
777
|
boardIntervalHandle = timers.setTimeout(function boardTick() {
|
|
744
778
|
if (stopped)
|
|
745
779
|
return;
|
|
746
|
-
void
|
|
780
|
+
void refreshBoardsWithDiagnostics()
|
|
747
781
|
.catch(() => undefined)
|
|
748
782
|
.finally(() => {
|
|
749
783
|
if (stopped)
|
|
@@ -810,7 +844,7 @@ export async function runMeshDaemon(deps = {}) {
|
|
|
810
844
|
else {
|
|
811
845
|
void activateDefaultCompany();
|
|
812
846
|
}
|
|
813
|
-
void
|
|
847
|
+
void refreshBoardsWithDiagnostics().catch(() => undefined);
|
|
814
848
|
if (emitPath) {
|
|
815
849
|
void runTranscriptTick();
|
|
816
850
|
}
|