@indigoai-us/hq-cli 5.159.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 +22 -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/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
|
@@ -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" };
|