@indigoai-us/hq-cli 5.159.0 → 5.161.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.
@@ -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
- // Exactly one repair turn, then failed with both errors. A provider
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
- latestFile &&
735
- latestFile.envelope === null) {
736
- const repair = decideEnvelopeRepair(hqRoot, laneId, latestFile, now(), spawnRepair, logDecision);
737
- if (repair.action === "spawned") {
738
- lastSignalMs = now();
739
- lastAlive = null;
740
- if (progressed)
741
- lastSeenMtime = newestMtime;
742
- await sleep(pollMs);
743
- continue;
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 file does not
923
- * parse. First failure on a resumable provider with a recorded session id:
924
- * record `envelope_repair` on the lane and spawn the repair turn. A provider
925
- * with no session resume gets the same single attempt as a fresh turn. Anything
926
- * else (already attempted, no session id on a resumable provider, spawn
927
- * failure): give up with every error collected, and the caller classifies
928
- * the lane `failed` with reason `envelope_invalid`.
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
- if (prior && prior.attempted) {
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, readSpawnManifest(runDir).worker_out_offset ?? 0);
963
- sessionId = result?.session_id ?? null;
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: latest.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" };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.159.0",
3
+ "version": "5.161.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {