@indigoai-us/hq-cli 5.158.0 → 5.160.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 +33 -0
- package/dist/command-catalog.generated.d.ts +29 -3
- package/dist/command-catalog.generated.js +36 -3
- package/dist/commands/agents.d.ts +5 -0
- package/dist/commands/agents.js +114 -3
- package/dist/commands/lanes.js +128 -11
- package/dist/lib/core-utils/sentry-report.d.ts +1 -1
- package/dist/lib/core-utils/sentry-report.js +107 -4
- package/dist/lib/lanes/errors.d.ts +1 -1
- package/dist/lib/lanes/profile.d.ts +5 -5
- package/dist/lib/lanes/profile.js +50 -13
- package/dist/lib/lanes/reconcile.js +20 -9
- package/dist/lib/lanes/spawn.d.ts +12 -1
- package/dist/lib/lanes/spawn.js +79 -16
- package/dist/lib/lanes/store.js +5 -0
- package/dist/lib/lanes/types.d.ts +5 -3
- package/dist/lib/lanes/types.js +2 -2
- package/dist/lib/lanes/watcher.d.ts +14 -8
- package/dist/lib/lanes/watcher.js +67 -25
- package/package.json +1 -1
package/dist/lib/lanes/store.js
CHANGED
|
@@ -421,6 +421,11 @@ export function parseLaneRecord(raw, filePath) {
|
|
|
421
421
|
if (!isLaneProvider(raw.provider)) {
|
|
422
422
|
throw new LaneSchemaError(`Unknown provider ${JSON.stringify(raw.provider)}`, { path: filePath, value: raw.provider });
|
|
423
423
|
}
|
|
424
|
+
if (raw.worker_policy_delivery !== undefined &&
|
|
425
|
+
raw.worker_policy_delivery !== "hook" &&
|
|
426
|
+
raw.worker_policy_delivery !== "prompt") {
|
|
427
|
+
throw new LaneSchemaError(`Unknown worker policy delivery mode ${JSON.stringify(raw.worker_policy_delivery)}`, { path: filePath, field: "worker_policy_delivery", value: raw.worker_policy_delivery });
|
|
428
|
+
}
|
|
424
429
|
parseLaneStories(raw, filePath);
|
|
425
430
|
parseOptionalCompanyField(raw, filePath);
|
|
426
431
|
parseOptionalMeshCompanyField(raw, filePath);
|
|
@@ -296,6 +296,8 @@ export interface LaneRecord {
|
|
|
296
296
|
worker_profile_hash?: string;
|
|
297
297
|
/** Profile fields intentionally unsupported by the selected provider. */
|
|
298
298
|
worker_profile_ignored?: string[];
|
|
299
|
+
/** How policy files were delivered on the most recent profile spawn. */
|
|
300
|
+
worker_policy_delivery?: "hook" | "prompt";
|
|
299
301
|
provider: LaneProvider;
|
|
300
302
|
account?: string;
|
|
301
303
|
/** Eligible accounts for a queue entry created without an explicit account. */
|
|
@@ -400,9 +402,9 @@ export interface LaneRecord {
|
|
|
400
402
|
/** Cache counters from the last exited run's result. Nothing else from the payload. */
|
|
401
403
|
last_run_usage?: LaneRunUsage;
|
|
402
404
|
/**
|
|
403
|
-
* Spec §4: one repair turn when the final output
|
|
404
|
-
*
|
|
405
|
-
*
|
|
405
|
+
* Spec §4: one repair turn per expected envelope slot when the final output
|
|
406
|
+
* is invalid or missing. Written before repair spawn and updated on repair
|
|
407
|
+
* exit. Absent when each slot's first envelope parsed successfully.
|
|
406
408
|
*/
|
|
407
409
|
envelope_repair?: LaneEnvelopeRepair;
|
|
408
410
|
/** Last successful lane_progress mesh delivery, preserved across watcher restarts. */
|
package/dist/lib/lanes/types.js
CHANGED
|
@@ -214,12 +214,12 @@ export const LANE_WRITERS = ["creator", "command", "watcher", "reconcile", "spaw
|
|
|
214
214
|
/** Field ownership at the durable-record mutation boundary (spec §10.1). */
|
|
215
215
|
export const LANE_FIELD_WRITERS = {
|
|
216
216
|
schema_version: ["creator"], lane_id: ["creator"], company: ["creator"],
|
|
217
|
-
project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator"],
|
|
217
|
+
project_id: ["command"], story_id: ["command"], stories: ["creator", "command", "watcher", "reconcile", "spawn"], thread_id: ["creator", "command", "spawn"], mesh_company: ["creator", "command"],
|
|
218
218
|
depth: ["command"], senior: ["command"], senior_history: ["command"],
|
|
219
219
|
state: ["creator", "command", "watcher", "reconcile", "spawn"], queued_reason: ["creator", "command", "spawn", "watcher", "reconcile"], queued_at: ["creator", "command", "spawn", "watcher", "reconcile"], admission_reserved_at: ["creator", "command", "reconcile", "spawn", "watcher"],
|
|
220
220
|
pickup_validation_pending: ["creator", "command", "spawn", "reconcile"],
|
|
221
221
|
pickup_validation_pending_since: ["creator", "command", "spawn", "reconcile"],
|
|
222
|
-
priority: ["command"], worker: ["creator"], worker_path: ["creator"], worker_pack: ["creator"], worker_profile: ["creator"], worker_profile_hash: ["creator", "spawn"], worker_profile_ignored: ["creator", "spawn"],
|
|
222
|
+
priority: ["command"], worker: ["creator", "command"], worker_path: ["creator", "command"], worker_pack: ["creator", "command"], worker_profile: ["creator", "command"], worker_profile_hash: ["creator", "command", "spawn"], worker_profile_ignored: ["creator", "command", "spawn"], worker_policy_delivery: ["command", "spawn"],
|
|
223
223
|
provider: ["creator"], account: ["creator", "spawn", "command", "watcher", "reconcile"], admission_accounts: ["creator", "command"], account_history: ["command", "spawn", "watcher", "reconcile"], pending_resume: ["command", "spawn", "watcher", "reconcile"], placement_default: ["creator"], host: ["creator", "command"], origin_host: ["creator"], origin_host_source: ["creator"],
|
|
224
224
|
version: ["creator"], created_at: ["creator"], updated_at: ["creator"], attribution_history: ["command"],
|
|
225
225
|
last_envelope: ["watcher", "reconcile"], model: ["creator", "spawn", "command"], model_source: ["creator", "spawn", "command"], effort: ["creator", "spawn"], effort_source: ["creator", "spawn"], fast_mode: ["spawn"],
|
|
@@ -174,8 +174,10 @@ export interface LatestEnvelopeFile {
|
|
|
174
174
|
path: string;
|
|
175
175
|
text: string;
|
|
176
176
|
envelope: LaneEnvelope | null;
|
|
177
|
-
/** Parser message when `envelope` is null. */
|
|
177
|
+
/** Parser message, or the missing-slot reason, when `envelope` is null. */
|
|
178
178
|
error: string | null;
|
|
179
|
+
/** True when the manifest's expected envelope slot was never created. */
|
|
180
|
+
missing?: boolean;
|
|
179
181
|
}
|
|
180
182
|
export declare function readLatestEnvelope(hqRoot: string, laneId: string, minName?: string): LaneEnvelope | null;
|
|
181
183
|
/** Valid-only view over `readLatestEnvelopeFile`; an unparseable file reads as absent. */
|
|
@@ -187,6 +189,8 @@ export declare function readLatestEnvelopeRecord(hqRoot: string, laneId: string,
|
|
|
187
189
|
* would otherwise be marked done again from the previous one.
|
|
188
190
|
*/
|
|
189
191
|
export declare function readLatestEnvelopeFile(hqRoot: string, laneId: string, minName?: string): LatestEnvelopeFile | null;
|
|
192
|
+
/** A descriptor for this run's manifest slot when no envelope file was written. */
|
|
193
|
+
export declare function missingExpectedEnvelopeFile(hqRoot: string, laneId: string, envelopePath: string | undefined): LatestEnvelopeFile | null;
|
|
190
194
|
export interface WatcherLoopDeps {
|
|
191
195
|
sleep?: (ms: number) => Promise<void>;
|
|
192
196
|
now?: () => number;
|
|
@@ -230,13 +234,15 @@ export type EnvelopeRepairDecision = {
|
|
|
230
234
|
errors: string[];
|
|
231
235
|
};
|
|
232
236
|
/**
|
|
233
|
-
* Spec §4 repair decision for an exited worker whose envelope
|
|
234
|
-
*
|
|
235
|
-
* record `envelope_repair` on the lane and spawn the repair turn.
|
|
236
|
-
* with no session resume gets the same single attempt as a fresh
|
|
237
|
-
* else (already attempted, no session id on a
|
|
238
|
-
* failure): give up with every
|
|
239
|
-
*
|
|
237
|
+
* Spec §4 repair decision for an exited worker whose expected envelope is
|
|
238
|
+
* invalid or missing. First failure on a resumable provider with a recorded
|
|
239
|
+
* session id: record `envelope_repair` on the lane and spawn the repair turn.
|
|
240
|
+
* A provider with no session resume gets the same single attempt as a fresh
|
|
241
|
+
* turn for invalid files. Anything else (already attempted, no session id on a
|
|
242
|
+
* resumable provider, non-running lane, spawn failure): give up with every
|
|
243
|
+
* error collected. The caller preserves the normal interrupted classification
|
|
244
|
+
* for a missing file and uses `envelope_invalid` only for an existing invalid
|
|
245
|
+
* file.
|
|
240
246
|
*/
|
|
241
247
|
export declare function decideEnvelopeRepair(hqRoot: string, laneId: string, latest: LatestEnvelopeFile, nowMs: number, spawnRepair: NonNullable<WatcherLoopDeps["spawnRepair"]>, logDecision: (record: Record<string, unknown>) => void): EnvelopeRepairDecision;
|
|
242
248
|
/** Exported so tests can prove the watcher reads the live record, not spawn env. */
|
|
@@ -499,6 +499,25 @@ export function readLatestEnvelopeFile(hqRoot, laneId, minName) {
|
|
|
499
499
|
};
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
|
+
/** A descriptor for this run's manifest slot when no envelope file was written. */
|
|
503
|
+
export function missingExpectedEnvelopeFile(hqRoot, laneId, envelopePath) {
|
|
504
|
+
if (!envelopePath || !path.isAbsolute(envelopePath))
|
|
505
|
+
return null;
|
|
506
|
+
const runDir = laneRunDir(hqRoot, laneId);
|
|
507
|
+
const expectedPath = path.resolve(envelopePath);
|
|
508
|
+
const expectedDir = path.resolve(runDir, "envelopes");
|
|
509
|
+
const filename = path.basename(expectedPath);
|
|
510
|
+
if (path.dirname(expectedPath) !== expectedDir || !filename.endsWith(".json"))
|
|
511
|
+
return null;
|
|
512
|
+
return {
|
|
513
|
+
ref: path.posix.join("envelopes", filename),
|
|
514
|
+
path: expectedPath,
|
|
515
|
+
text: "",
|
|
516
|
+
envelope: null,
|
|
517
|
+
error: `Missing expected envelope file: ${expectedPath}`,
|
|
518
|
+
missing: true,
|
|
519
|
+
};
|
|
520
|
+
}
|
|
502
521
|
/**
|
|
503
522
|
* Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
|
|
504
523
|
* the event name and the error message, and never propagates: the lane's
|
|
@@ -677,6 +696,9 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
677
696
|
}
|
|
678
697
|
}
|
|
679
698
|
const latestFile = readLatestEnvelopeFile(hqRoot, laneId, minEnvelopeName);
|
|
699
|
+
const missingExpected = !processAlive && latestFile === null
|
|
700
|
+
? missingExpectedEnvelopeFile(hqRoot, laneId, manifest.envelope_path)
|
|
701
|
+
: null;
|
|
680
702
|
const latest = latestFile && latestFile.envelope
|
|
681
703
|
? { envelope: latestFile.envelope, ref: latestFile.ref }
|
|
682
704
|
: null;
|
|
@@ -719,30 +741,37 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
719
741
|
process.stderr.write(`hq lanes watcher: quota read failed for ${laneId}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
720
742
|
}
|
|
721
743
|
}
|
|
722
|
-
// Spec §4: the process exited and its envelope file does not parse
|
|
723
|
-
//
|
|
744
|
+
// Spec §4: the process exited and its envelope file does not parse, or
|
|
745
|
+
// this run never wrote its expected slot. Exactly one repair turn. A provider
|
|
724
746
|
// rejection skips the repair: the provider refused the turn, so a
|
|
725
747
|
// second turn on the same model and account would be refused the same
|
|
726
748
|
// way. A quota signal skips it too: a repair turn on an exhausted account
|
|
727
749
|
// fails the same way, and would end the lane envelope_invalid without
|
|
728
750
|
// ever recording the hold. The quota result applies immediately instead.
|
|
729
751
|
let invalidEnvelope = null;
|
|
752
|
+
const repairCandidate = latestFile ?? missingExpected;
|
|
730
753
|
if (!processAlive &&
|
|
731
754
|
!envelope &&
|
|
732
755
|
!providerRejection &&
|
|
733
756
|
!quota &&
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
const
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
757
|
+
repairCandidate &&
|
|
758
|
+
repairCandidate.envelope === null) {
|
|
759
|
+
const lane = repairCandidate.missing ? readLaneSoft(hqRoot, laneId) : null;
|
|
760
|
+
const missingRepairAllowed = !repairCandidate.missing ||
|
|
761
|
+
(lane !== null && envelopeRepairMode(lane.provider) === "resume");
|
|
762
|
+
if (missingRepairAllowed) {
|
|
763
|
+
const repair = decideEnvelopeRepair(hqRoot, laneId, repairCandidate, now(), spawnRepair, logDecision);
|
|
764
|
+
if (repair.action === "spawned") {
|
|
765
|
+
lastSignalMs = now();
|
|
766
|
+
lastAlive = null;
|
|
767
|
+
if (progressed)
|
|
768
|
+
lastSeenMtime = newestMtime;
|
|
769
|
+
await sleep(pollMs);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
if (!repairCandidate.missing)
|
|
773
|
+
invalidEnvelope = { errors: repair.errors };
|
|
744
774
|
}
|
|
745
|
-
invalidEnvelope = { errors: repair.errors };
|
|
746
775
|
}
|
|
747
776
|
if (processAlive === false && envelope && latestFile) {
|
|
748
777
|
markRepairSucceeded(hqRoot, laneId, latestFile.ref);
|
|
@@ -919,13 +948,15 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
|
|
|
919
948
|
}
|
|
920
949
|
}
|
|
921
950
|
/**
|
|
922
|
-
* Spec §4 repair decision for an exited worker whose envelope
|
|
923
|
-
*
|
|
924
|
-
* record `envelope_repair` on the lane and spawn the repair turn.
|
|
925
|
-
* with no session resume gets the same single attempt as a fresh
|
|
926
|
-
* else (already attempted, no session id on a
|
|
927
|
-
* failure): give up with every
|
|
928
|
-
*
|
|
951
|
+
* Spec §4 repair decision for an exited worker whose expected envelope is
|
|
952
|
+
* invalid or missing. First failure on a resumable provider with a recorded
|
|
953
|
+
* session id: record `envelope_repair` on the lane and spawn the repair turn.
|
|
954
|
+
* A provider with no session resume gets the same single attempt as a fresh
|
|
955
|
+
* turn for invalid files. Anything else (already attempted, no session id on a
|
|
956
|
+
* resumable provider, non-running lane, spawn failure): give up with every
|
|
957
|
+
* error collected. The caller preserves the normal interrupted classification
|
|
958
|
+
* for a missing file and uses `envelope_invalid` only for an existing invalid
|
|
959
|
+
* file.
|
|
929
960
|
*/
|
|
930
961
|
export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair, logDecision) {
|
|
931
962
|
const at = new Date(nowMs).toISOString();
|
|
@@ -933,8 +964,18 @@ export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair,
|
|
|
933
964
|
const lane = readLaneSoft(hqRoot, laneId);
|
|
934
965
|
if (!lane)
|
|
935
966
|
return { action: "give_up", errors: [error] };
|
|
967
|
+
if (lane.state !== "running") {
|
|
968
|
+
logDecision({ event: "envelope_repair", at, outcome: "skipped", skipped_reason: "lane_not_running", error });
|
|
969
|
+
return { action: "give_up", errors: [error] };
|
|
970
|
+
}
|
|
971
|
+
const runDir = laneRunDir(hqRoot, laneId);
|
|
972
|
+
const manifest = readSpawnManifest(runDir);
|
|
936
973
|
const prior = lane.envelope_repair;
|
|
937
|
-
|
|
974
|
+
const repairRef = missingExpectedEnvelopeFile(hqRoot, laneId, manifest.envelope_path)?.ref ??
|
|
975
|
+
prior?.envelope_ref ??
|
|
976
|
+
latest.ref;
|
|
977
|
+
const priorAppliesToSlot = prior?.envelope_ref === undefined || prior.envelope_ref === repairRef;
|
|
978
|
+
if (prior && prior.attempted && priorAppliesToSlot) {
|
|
938
979
|
// The repair turn ran and its output does not parse either.
|
|
939
980
|
if (prior.attempted) {
|
|
940
981
|
const errors = [prior.first_error, error];
|
|
@@ -952,21 +993,21 @@ export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair,
|
|
|
952
993
|
}
|
|
953
994
|
return { action: "give_up", errors: [prior.first_error] };
|
|
954
995
|
}
|
|
955
|
-
const runDir = laneRunDir(hqRoot, laneId);
|
|
956
996
|
// `resume` (claude, codex) continues the recorded session; `fresh_turn`
|
|
957
997
|
// (grok) runs the repair prompt as a new single turn on the same account.
|
|
958
998
|
const mode = envelopeRepairMode(lane.provider);
|
|
959
999
|
let skipped;
|
|
960
1000
|
let sessionId = null;
|
|
961
1001
|
if (mode === "resume") {
|
|
962
|
-
const result = readWorkerRunResult(runDir, lane.provider,
|
|
963
|
-
sessionId = result?.session_id ??
|
|
1002
|
+
const result = readWorkerRunResult(runDir, lane.provider, manifest.worker_out_offset ?? 0);
|
|
1003
|
+
sessionId = result?.session_id ??
|
|
1004
|
+
[manifest.resume?.session_id, lane.provider_session_id, lane.last_resume?.session_id].find((candidate) => typeof candidate === "string" && candidate.trim().length > 0) ?? null;
|
|
964
1005
|
if (sessionId === null)
|
|
965
1006
|
skipped = result?.session_id_absent ?? "no_result_json";
|
|
966
1007
|
}
|
|
967
1008
|
const base = {
|
|
968
1009
|
first_error: error,
|
|
969
|
-
envelope_ref:
|
|
1010
|
+
envelope_ref: repairRef,
|
|
970
1011
|
at,
|
|
971
1012
|
};
|
|
972
1013
|
if (skipped !== undefined) {
|
|
@@ -981,6 +1022,7 @@ export function decideEnvelopeRepair(hqRoot, laneId, latest, nowMs, spawnRepair,
|
|
|
981
1022
|
envelopePath: latest.path,
|
|
982
1023
|
error,
|
|
983
1024
|
rawText: latest.text,
|
|
1025
|
+
missing: latest.missing,
|
|
984
1026
|
});
|
|
985
1027
|
logDecision({ event: "envelope_repair", at, outcome: "spawned", mode, pid, session_id: sessionId, error });
|
|
986
1028
|
return { action: "spawned" };
|