@remixhq/claude-plugin 0.1.22 → 0.1.24

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.
@@ -540,17 +540,23 @@ __export(hook_stop_collab_exports, {
540
540
  module.exports = __toCommonJS(hook_stop_collab_exports);
541
541
  var import_node_child_process9 = require("child_process");
542
542
 
543
- // node_modules/@remixhq/core/dist/chunk-YZ34ICNN.js
543
+ // node_modules/@remixhq/core/dist/chunk-7XJGOKEO.js
544
544
  var RemixError = class extends Error {
545
545
  code;
546
546
  exitCode;
547
547
  hint;
548
+ // HTTP status code when this error originates from an API response.
549
+ // null for non-HTTP errors (validation, local IO, programming bugs).
550
+ // Callers use this to distinguish transient (5xx) from permanent (4xx)
551
+ // API failures without resorting to error-message string matching.
552
+ statusCode;
548
553
  constructor(message, opts) {
549
554
  super(message);
550
555
  this.name = "RemixError";
551
556
  this.code = opts?.code ?? null;
552
557
  this.exitCode = opts?.exitCode ?? 1;
553
558
  this.hint = opts?.hint ?? null;
559
+ this.statusCode = opts?.statusCode ?? null;
554
560
  }
555
561
  };
556
562
 
@@ -7329,7 +7335,7 @@ var {
7329
7335
  getCancelSignal: getCancelSignal2
7330
7336
  } = getIpcExport();
7331
7337
 
7332
- // node_modules/@remixhq/core/dist/chunk-WT6VRLXU.js
7338
+ // node_modules/@remixhq/core/dist/chunk-S4ECO35X.js
7333
7339
  async function runGit(args, cwd) {
7334
7340
  const res = await execa("git", args, { cwd, stderr: "ignore" });
7335
7341
  return String(res.stdout || "").trim();
@@ -7384,7 +7390,7 @@ function summarizeUnifiedDiff(diff) {
7384
7390
  return { changedFilesCount, insertions, deletions };
7385
7391
  }
7386
7392
 
7387
- // node_modules/@remixhq/core/dist/chunk-YCFLOHJV.js
7393
+ // node_modules/@remixhq/core/dist/chunk-DBVN42RF.js
7388
7394
  var import_promises12 = __toESM(require("fs/promises"), 1);
7389
7395
  var import_path = __toESM(require("path"), 1);
7390
7396
  var import_promises13 = __toESM(require("fs/promises"), 1);
@@ -7683,6 +7689,8 @@ function buildAppDeltaCacheKey(appId, payload) {
7683
7689
  appId,
7684
7690
  payload.baseHeadHash,
7685
7691
  payload.targetHeadHash ?? "",
7692
+ payload.baseRevisionId ?? "",
7693
+ payload.targetRevisionId ?? "",
7686
7694
  payload.localSnapshotHash ?? "",
7687
7695
  payload.repoFingerprint ?? "",
7688
7696
  payload.remoteUrl ?? "",
@@ -7929,11 +7937,11 @@ async function readLocalBaseline(params) {
7929
7937
  const raw = await import_promises15.default.readFile(getBaselinePath(params), "utf8");
7930
7938
  const parsed = JSON.parse(raw);
7931
7939
  if (!parsed || typeof parsed !== "object") return null;
7932
- if (parsed.schemaVersion !== 1 || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
7940
+ if (![1, 2].includes(Number(parsed.schemaVersion)) || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
7933
7941
  return null;
7934
7942
  }
7935
7943
  return {
7936
- schemaVersion: 1,
7944
+ schemaVersion: Number(parsed.schemaVersion) === 2 ? 2 : 1,
7937
7945
  key: parsed.key,
7938
7946
  repoRoot: parsed.repoRoot,
7939
7947
  repoFingerprint: parsed.repoFingerprint ?? null,
@@ -7942,6 +7950,8 @@ async function readLocalBaseline(params) {
7942
7950
  branchName: parsed.branchName ?? null,
7943
7951
  lastSnapshotId: parsed.lastSnapshotId ?? null,
7944
7952
  lastSnapshotHash: parsed.lastSnapshotHash ?? null,
7953
+ lastServerRevisionId: parsed.lastServerRevisionId ?? null,
7954
+ lastServerTreeHash: parsed.lastServerTreeHash ?? null,
7945
7955
  lastServerHeadHash: parsed.lastServerHeadHash ?? null,
7946
7956
  lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
7947
7957
  updatedAt: String(parsed.updatedAt ?? "")
@@ -7953,7 +7963,7 @@ async function readLocalBaseline(params) {
7953
7963
  async function writeLocalBaseline(baseline) {
7954
7964
  const key = buildLaneStateKey(baseline);
7955
7965
  const normalized = {
7956
- schemaVersion: 1,
7966
+ schemaVersion: 2,
7957
7967
  key,
7958
7968
  repoRoot: baseline.repoRoot,
7959
7969
  repoFingerprint: baseline.repoFingerprint ?? null,
@@ -7962,6 +7972,8 @@ async function writeLocalBaseline(baseline) {
7962
7972
  branchName: baseline.branchName ?? null,
7963
7973
  lastSnapshotId: baseline.lastSnapshotId ?? null,
7964
7974
  lastSnapshotHash: baseline.lastSnapshotHash ?? null,
7975
+ lastServerRevisionId: baseline.lastServerRevisionId ?? null,
7976
+ lastServerTreeHash: baseline.lastServerTreeHash ?? null,
7965
7977
  lastServerHeadHash: baseline.lastServerHeadHash ?? null,
7966
7978
  lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash ?? null,
7967
7979
  updatedAt: baseline.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
@@ -8266,6 +8278,7 @@ function normalizeJob2(input) {
8266
8278
  prompt: input.prompt,
8267
8279
  assistantResponse: input.assistantResponse,
8268
8280
  baselineSnapshotId: input.baselineSnapshotId ?? null,
8281
+ baselineServerRevisionId: input.baselineServerRevisionId ?? null,
8269
8282
  baselineServerHeadHash: input.baselineServerHeadHash ?? null,
8270
8283
  currentSnapshotId: input.currentSnapshotId,
8271
8284
  capturedAt: input.capturedAt ?? now,
@@ -8300,6 +8313,7 @@ async function readPendingFinalizeJob(jobId) {
8300
8313
  prompt: String(parsed.prompt ?? ""),
8301
8314
  assistantResponse: String(parsed.assistantResponse ?? ""),
8302
8315
  baselineSnapshotId: parsed.baselineSnapshotId ?? null,
8316
+ baselineServerRevisionId: parsed.baselineServerRevisionId ?? null,
8303
8317
  baselineServerHeadHash: parsed.baselineServerHeadHash ?? null,
8304
8318
  currentSnapshotId: String(parsed.currentSnapshotId ?? ""),
8305
8319
  capturedAt: parsed.capturedAt,
@@ -8564,6 +8578,15 @@ function shouldRequireRemoteLaneForCurrentBranch(params) {
8564
8578
  if (params.currentBranch === defaultBranch) return false;
8565
8579
  return !params.binding.laneId || params.binding.currentAppId === params.binding.upstreamAppId;
8566
8580
  }
8581
+ function resolveLaneLookupProjectId(params) {
8582
+ const currentBranch = normalizeBranchName2(params.currentBranch);
8583
+ const defaultBranch = normalizeBranchName2(params.defaultBranch);
8584
+ const localProjectId = params.localBinding.projectId ?? null;
8585
+ if (currentBranch && currentBranch !== defaultBranch && localProjectId) {
8586
+ return localProjectId;
8587
+ }
8588
+ return params.explicitRootProjectId ?? (params.requireRemoteLane ? void 0 : localProjectId ?? params.fallbackProjectId ?? void 0);
8589
+ }
8567
8590
  async function persistResolvedLane(repoRoot, binding) {
8568
8591
  await writeCollabBinding(repoRoot, {
8569
8592
  projectId: binding.projectId,
@@ -8642,7 +8665,14 @@ async function resolveActiveLaneBindingUncached(params, state) {
8642
8665
  };
8643
8666
  }
8644
8667
  const laneResp2 = await params.api.resolveProjectLaneBinding({
8645
- projectId: state.explicitRootBinding?.projectId ?? (requireRemoteLane ? void 0 : localBinding.projectId ?? state.projectId ?? void 0),
8668
+ projectId: resolveLaneLookupProjectId({
8669
+ explicitRootProjectId: state.explicitRootBinding?.projectId,
8670
+ localBinding,
8671
+ currentBranch,
8672
+ defaultBranch: state.defaultBranch,
8673
+ requireRemoteLane,
8674
+ fallbackProjectId: state.projectId
8675
+ }),
8646
8676
  repoFingerprint: state.repoFingerprint ?? void 0,
8647
8677
  remoteUrl: state.remoteUrl ?? void 0,
8648
8678
  defaultBranch: state.defaultBranch ?? void 0,
@@ -8801,6 +8831,8 @@ function buildBaseState() {
8801
8831
  branchName: null,
8802
8832
  localCommitHash: null,
8803
8833
  currentSnapshotHash: null,
8834
+ currentServerRevisionId: null,
8835
+ currentServerTreeHash: null,
8804
8836
  currentServerHeadHash: null,
8805
8837
  currentServerHeadCommitId: null,
8806
8838
  worktreeClean: false,
@@ -8834,6 +8866,8 @@ function buildBaseState() {
8834
8866
  baseline: {
8835
8867
  lastSnapshotId: null,
8836
8868
  lastSnapshotHash: null,
8869
+ lastServerRevisionId: null,
8870
+ lastServerTreeHash: null,
8837
8871
  lastServerHeadHash: null,
8838
8872
  lastSeenLocalCommitHash: null
8839
8873
  }
@@ -8960,6 +8994,8 @@ async function collabDetectRepoState(params) {
8960
8994
  summarizeAsyncJobs({ repoRoot, branchName: binding.branchName ?? null })
8961
8995
  ]);
8962
8996
  const appHead = unwrapResponseObject(headResp, "app head");
8997
+ detected.currentServerRevisionId = appHead.headRevisionId ?? null;
8998
+ detected.currentServerTreeHash = appHead.treeHash ?? null;
8963
8999
  detected.currentServerHeadHash = appHead.headCommitHash;
8964
9000
  detected.currentServerHeadCommitId = appHead.headCommitId;
8965
9001
  detected.currentSnapshotHash = inspection.snapshotHash;
@@ -8968,6 +9004,8 @@ async function collabDetectRepoState(params) {
8968
9004
  detected.baseline = {
8969
9005
  lastSnapshotId: baseline?.lastSnapshotId ?? null,
8970
9006
  lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
9007
+ lastServerRevisionId: baseline?.lastServerRevisionId ?? null,
9008
+ lastServerTreeHash: baseline?.lastServerTreeHash ?? null,
8971
9009
  lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
8972
9010
  lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
8973
9011
  };
@@ -8977,6 +9015,7 @@ async function collabDetectRepoState(params) {
8977
9015
  const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
8978
9016
  baseHeadHash: localCommitHash,
8979
9017
  targetHeadHash: appHead.headCommitHash,
9018
+ targetRevisionId: appHead.headRevisionId,
8980
9019
  repoFingerprint: binding.repoFingerprint ?? void 0,
8981
9020
  remoteUrl: binding.remoteUrl ?? void 0,
8982
9021
  defaultBranch: binding.defaultBranch ?? void 0
@@ -8999,7 +9038,7 @@ async function collabDetectRepoState(params) {
8999
9038
  }
9000
9039
  }
9001
9040
  detected.repoState = "external_local_base_changed";
9002
- detected.hint = "No local Remix baseline exists for this lane yet. Run `remix collab re-anchor` to anchor this checkout.";
9041
+ detected.hint = "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
9003
9042
  return detected;
9004
9043
  }
9005
9044
  const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
@@ -9018,7 +9057,30 @@ async function collabDetectRepoState(params) {
9018
9057
  return detected;
9019
9058
  }
9020
9059
  const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
9021
- const serverChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
9060
+ const serverHeadChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
9061
+ const revisionChanged = Boolean(
9062
+ baseline.lastServerRevisionId && (appHead.headRevisionId ?? null) !== baseline.lastServerRevisionId
9063
+ );
9064
+ const equivalentRevisionDrift = revisionChanged && !serverHeadChanged;
9065
+ if (equivalentRevisionDrift) {
9066
+ await writeLocalBaseline({
9067
+ repoRoot,
9068
+ repoFingerprint: binding.repoFingerprint,
9069
+ laneId: binding.laneId,
9070
+ currentAppId: binding.currentAppId,
9071
+ branchName: binding.branchName,
9072
+ lastSnapshotId: baseline.lastSnapshotId,
9073
+ lastSnapshotHash: baseline.lastSnapshotHash,
9074
+ lastServerRevisionId: appHead.headRevisionId ?? null,
9075
+ lastServerTreeHash: appHead.treeHash ?? baseline.lastServerTreeHash ?? null,
9076
+ lastServerHeadHash: appHead.headCommitHash,
9077
+ lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash
9078
+ });
9079
+ detected.baseline.lastServerRevisionId = appHead.headRevisionId ?? null;
9080
+ detected.baseline.lastServerTreeHash = appHead.treeHash ?? baseline.lastServerTreeHash ?? null;
9081
+ detected.baseline.lastServerHeadHash = appHead.headCommitHash;
9082
+ }
9083
+ const serverChanged = serverHeadChanged;
9022
9084
  if (!localChanged && !serverChanged) {
9023
9085
  detected.repoState = "idle";
9024
9086
  return detected;
@@ -9442,6 +9504,7 @@ function buildWorkspaceMetadata(params) {
9442
9504
  recordingMode: "boundary_delta",
9443
9505
  baselineSnapshotId: params.baselineSnapshotId,
9444
9506
  currentSnapshotId: params.currentSnapshotId,
9507
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
9445
9508
  baselineServerHeadHash: params.baselineServerHeadHash,
9446
9509
  currentSnapshotHash: params.currentSnapshotHash,
9447
9510
  localCommitHash: params.localCommitHash,
@@ -9460,6 +9523,59 @@ function buildWorkspaceMetadata(params) {
9460
9523
  }
9461
9524
  return metadata;
9462
9525
  }
9526
+ async function findExistingChangeStepByIdempotency(params) {
9527
+ const idempotencyKey = params.idempotencyKey?.trim();
9528
+ if (!idempotencyKey) return null;
9529
+ const resp = await params.api.listChangeSteps(params.appId, { limit: 1, idempotencyKey });
9530
+ const responseObject = unwrapResponseObject(
9531
+ resp,
9532
+ "change step list"
9533
+ );
9534
+ const steps = Array.isArray(responseObject) ? responseObject : Array.isArray(responseObject.items) ? responseObject.items : [];
9535
+ return steps.find((step) => step.idempotencyKey === idempotencyKey) ?? null;
9536
+ }
9537
+ async function writeBaselineFromSucceededChangeStep(params) {
9538
+ const nextServerHeadHash = typeof params.changeStep.headCommitHash === "string" ? params.changeStep.headCommitHash.trim() : "";
9539
+ if (!nextServerHeadHash) {
9540
+ throw buildFinalizeCliError({
9541
+ message: "Backend returned a succeeded change step without a head commit hash.",
9542
+ exitCode: 1,
9543
+ hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again.",
9544
+ disposition: "terminal",
9545
+ reason: "missing_head_commit_hash"
9546
+ });
9547
+ }
9548
+ let nextServerRevisionId = typeof params.changeStep.resultRevisionId === "string" ? params.changeStep.resultRevisionId.trim() : "";
9549
+ let nextServerTreeHash = null;
9550
+ if (!nextServerRevisionId) {
9551
+ const freshHeadResp = await params.api.getAppHead(params.job.currentAppId);
9552
+ const freshHead = unwrapResponseObject(freshHeadResp, "app head");
9553
+ if (freshHead.headCommitHash !== nextServerHeadHash || !freshHead.headRevisionId) {
9554
+ throw buildFinalizeCliError({
9555
+ message: "Backend returned a succeeded change step without a matching result revision.",
9556
+ exitCode: 1,
9557
+ hint: "The local baseline was not advanced because the post-step revision could not be verified. Restart the backend/CLI and retry after checking `remix collab status`.",
9558
+ disposition: "terminal",
9559
+ reason: "missing_result_revision_id"
9560
+ });
9561
+ }
9562
+ nextServerRevisionId = freshHead.headRevisionId;
9563
+ nextServerTreeHash = freshHead.treeHash ?? null;
9564
+ }
9565
+ await writeLocalBaseline({
9566
+ repoRoot: params.job.repoRoot,
9567
+ repoFingerprint: params.job.repoFingerprint,
9568
+ laneId: params.job.laneId,
9569
+ currentAppId: params.job.currentAppId,
9570
+ branchName: params.job.branchName,
9571
+ lastSnapshotId: params.snapshot.id,
9572
+ lastSnapshotHash: params.snapshot.snapshotHash,
9573
+ lastServerRevisionId: nextServerRevisionId,
9574
+ lastServerTreeHash: nextServerTreeHash,
9575
+ lastServerHeadHash: nextServerHeadHash,
9576
+ lastSeenLocalCommitHash: params.snapshot.localCommitHash
9577
+ });
9578
+ }
9463
9579
  async function harvestPreTurnEvents(repoRoot, fromCommit, toCommit) {
9464
9580
  if (!toCommit) return null;
9465
9581
  try {
@@ -9520,12 +9636,12 @@ async function processClaimedPendingFinalizeJobInner(params) {
9520
9636
  throw buildFinalizeCliError({
9521
9637
  message: "Local baseline is missing for this queued finalize job.",
9522
9638
  exitCode: 2,
9523
- hint: "Run `remix collab re-anchor` to anchor the repository again.",
9639
+ hint: "Run `remix collab init` to seed this checkout's revision baseline.",
9524
9640
  disposition: "terminal",
9525
9641
  reason: "baseline_missing"
9526
9642
  });
9527
9643
  }
9528
- const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
9644
+ const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || (job.baselineServerRevisionId ? baseline.lastServerRevisionId !== job.baselineServerRevisionId : false) || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
9529
9645
  const appHead = unwrapResponseObject(appHeadResp, "app head");
9530
9646
  const remoteUrl = readMetadataString(job, "remoteUrl");
9531
9647
  const defaultBranch = readMetadataString(job, "defaultBranch");
@@ -9548,12 +9664,13 @@ async function processClaimedPendingFinalizeJobInner(params) {
9548
9664
  throw buildFinalizeCliError({
9549
9665
  message: "Finalize queue baseline drifted before this job was processed.",
9550
9666
  exitCode: 1,
9551
- hint: "Process queued finalize jobs in capture order, or re-anchor the repository before retrying.",
9667
+ hint: "Process queued finalize jobs in capture order, or run `remix collab init` to refresh the revision baseline before retrying.",
9552
9668
  disposition: "terminal",
9553
9669
  reason: "baseline_drifted"
9554
9670
  });
9555
9671
  }
9556
- if (appHead.headCommitHash !== job.baselineServerHeadHash) {
9672
+ const serverStillAtBaseline = job.baselineServerRevisionId ? appHead.headRevisionId === job.baselineServerRevisionId : appHead.headCommitHash === job.baselineServerHeadHash;
9673
+ if (!serverStillAtBaseline) {
9557
9674
  throw buildFinalizeCliError({
9558
9675
  message: "Server lane changed before a no-diff turn could be recorded.",
9559
9676
  exitCode: 2,
@@ -9575,6 +9692,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9575
9692
  defaultBranch,
9576
9693
  baselineSnapshotId: job.baselineSnapshotId,
9577
9694
  currentSnapshotId: job.currentSnapshotId,
9695
+ baselineServerRevisionId: job.baselineServerRevisionId,
9578
9696
  baselineServerHeadHash: job.baselineServerHeadHash,
9579
9697
  currentSnapshotHash: snapshot.snapshotHash,
9580
9698
  localCommitHash: snapshot.localCommitHash,
@@ -9595,6 +9713,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
9595
9713
  branchName: job.branchName,
9596
9714
  lastSnapshotId: snapshot.id,
9597
9715
  lastSnapshotHash: snapshot.snapshotHash,
9716
+ lastServerRevisionId: appHead.headRevisionId ?? null,
9717
+ lastServerTreeHash: appHead.treeHash ?? null,
9598
9718
  lastServerHeadHash: appHead.headCommitHash,
9599
9719
  lastSeenLocalCommitHash: snapshot.localCommitHash
9600
9720
  });
@@ -9615,14 +9735,14 @@ async function processClaimedPendingFinalizeJobInner(params) {
9615
9735
  };
9616
9736
  }
9617
9737
  const localBaselineAdvanced = baseline.lastSnapshotId !== job.baselineSnapshotId;
9618
- const serverHeadAdvanced = appHead.headCommitHash !== job.baselineServerHeadHash;
9738
+ const serverHeadAdvanced = job.baselineServerRevisionId ? appHead.headRevisionId !== job.baselineServerRevisionId : appHead.headCommitHash !== job.baselineServerHeadHash;
9619
9739
  if (baselineDrifted) {
9620
9740
  const consistentAdvance = localBaselineAdvanced && serverHeadAdvanced;
9621
9741
  if (!consistentAdvance) {
9622
9742
  throw buildFinalizeCliError({
9623
9743
  message: `Finalize queue baseline advanced inconsistently before this job was processed (localBaselineAdvanced=${localBaselineAdvanced}, serverHeadAdvanced=${serverHeadAdvanced}, jobBaselineSnapshotId=${job.baselineSnapshotId ?? "null"}, liveBaselineSnapshotId=${baseline.lastSnapshotId ?? "null"}, jobBaselineServerHeadHash=${job.baselineServerHeadHash ?? "null"}, liveBaselineServerHeadHash=${baseline.lastServerHeadHash ?? "null"}, currentAppHeadHash=${appHead.headCommitHash}). This indicates local Remix state diverged from the backend in a way that should not be reachable in normal operation; please report this as a bug.`,
9624
9744
  exitCode: 1,
9625
- hint: "Run `remix collab status` to inspect, then `remix collab re-anchor` only if the lane has no valid baseline.",
9745
+ hint: "Run `remix collab status` to inspect, then sync or reconcile before retrying.",
9626
9746
  disposition: "terminal",
9627
9747
  reason: "baseline_drifted"
9628
9748
  });
@@ -9630,6 +9750,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9630
9750
  }
9631
9751
  let submissionDiff = diffResult.diff;
9632
9752
  let submissionBaseHeadHash = job.baselineServerHeadHash;
9753
+ let submissionBaseRevisionId = job.baselineServerRevisionId;
9633
9754
  let replayedFromBaseHash = null;
9634
9755
  if (!submissionBaseHeadHash) {
9635
9756
  throw buildFinalizeCliError({
@@ -9640,6 +9761,34 @@ async function processClaimedPendingFinalizeJobInner(params) {
9640
9761
  });
9641
9762
  }
9642
9763
  const replayNeeded = appHead.headCommitHash !== submissionBaseHeadHash || baselineDrifted;
9764
+ if (replayNeeded) {
9765
+ const existingChangeStep = await findExistingChangeStepByIdempotency({
9766
+ api: params.api,
9767
+ appId: job.currentAppId,
9768
+ idempotencyKey: job.idempotencyKey
9769
+ });
9770
+ if (existingChangeStep) {
9771
+ const changeStep2 = existingChangeStep.status === "succeeded" ? existingChangeStep : await pollChangeStep(params.api, job.currentAppId, existingChangeStep.id);
9772
+ invalidateAppHeadCache(job.currentAppId);
9773
+ invalidateAppDeltaCacheForApp(job.currentAppId);
9774
+ await writeBaselineFromSucceededChangeStep({ api: params.api, job, snapshot, changeStep: changeStep2 });
9775
+ await updatePendingFinalizeJob(job.id, {
9776
+ status: "completed",
9777
+ metadata: { changeStepId: String(changeStep2.id ?? "") }
9778
+ });
9779
+ return {
9780
+ mode: "changed_turn",
9781
+ idempotencyKey: job.idempotencyKey ?? "",
9782
+ queued: false,
9783
+ jobId: job.id,
9784
+ repoState,
9785
+ changeStep: changeStep2,
9786
+ collabTurn: null,
9787
+ autoSync: null,
9788
+ warnings: []
9789
+ };
9790
+ }
9791
+ }
9643
9792
  if (replayNeeded) {
9644
9793
  try {
9645
9794
  const replayResp = await params.api.startChangeStepReplay(job.currentAppId, {
@@ -9647,7 +9796,9 @@ async function processClaimedPendingFinalizeJobInner(params) {
9647
9796
  assistantResponse: job.assistantResponse,
9648
9797
  diff: diffResult.diff,
9649
9798
  baseCommitHash: submissionBaseHeadHash,
9799
+ baseRevisionId: job.baselineServerRevisionId,
9650
9800
  targetHeadCommitHash: appHead.headCommitHash,
9801
+ targetRevisionId: appHead.headRevisionId,
9651
9802
  expectedPaths: diffResult.changedPaths,
9652
9803
  actor,
9653
9804
  workspaceMetadata: buildWorkspaceMetadata({
@@ -9657,6 +9808,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9657
9808
  defaultBranch,
9658
9809
  baselineSnapshotId: job.baselineSnapshotId,
9659
9810
  currentSnapshotId: job.currentSnapshotId,
9811
+ baselineServerRevisionId: job.baselineServerRevisionId,
9660
9812
  baselineServerHeadHash: job.baselineServerHeadHash,
9661
9813
  currentSnapshotHash: snapshot.snapshotHash,
9662
9814
  localCommitHash: snapshot.localCommitHash,
@@ -9682,6 +9834,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9682
9834
  submissionDiff = replayDiff.diff;
9683
9835
  replayedFromBaseHash = submissionBaseHeadHash;
9684
9836
  submissionBaseHeadHash = appHead.headCommitHash;
9837
+ submissionBaseRevisionId = appHead.headRevisionId;
9685
9838
  } catch (error) {
9686
9839
  if (error instanceof RemixError && error.finalizeDisposition === void 0) {
9687
9840
  const detail = error.hint ? `${error.message} (${error.hint})` : error.message;
@@ -9703,6 +9856,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9703
9856
  assistantResponse: job.assistantResponse,
9704
9857
  diff: submissionDiff,
9705
9858
  baseCommitHash: submissionBaseHeadHash,
9859
+ baseRevisionId: submissionBaseRevisionId,
9706
9860
  headCommitHash: submissionBaseHeadHash,
9707
9861
  changedFilesCount: diffResult.stats.changedFilesCount,
9708
9862
  insertions: diffResult.stats.insertions,
@@ -9715,6 +9869,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9715
9869
  defaultBranch,
9716
9870
  baselineSnapshotId: job.baselineSnapshotId,
9717
9871
  currentSnapshotId: job.currentSnapshotId,
9872
+ baselineServerRevisionId: job.baselineServerRevisionId,
9718
9873
  baselineServerHeadHash: job.baselineServerHeadHash,
9719
9874
  currentSnapshotHash: snapshot.snapshotHash,
9720
9875
  localCommitHash: snapshot.localCommitHash,
@@ -9731,27 +9886,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
9731
9886
  const changeStep = await pollChangeStep(params.api, job.currentAppId, String(createdStep.id));
9732
9887
  invalidateAppHeadCache(job.currentAppId);
9733
9888
  invalidateAppDeltaCacheForApp(job.currentAppId);
9734
- const nextServerHeadHash = typeof changeStep.headCommitHash === "string" ? changeStep.headCommitHash.trim() : "";
9735
- if (!nextServerHeadHash) {
9736
- throw buildFinalizeCliError({
9737
- message: "Backend returned a succeeded change step without a head commit hash.",
9738
- exitCode: 1,
9739
- hint: "This is a backend invariant violation; retry will not help. Re-anchor and try again.",
9740
- disposition: "terminal",
9741
- reason: "missing_head_commit_hash"
9742
- });
9743
- }
9744
- await writeLocalBaseline({
9745
- repoRoot: job.repoRoot,
9746
- repoFingerprint: job.repoFingerprint,
9747
- laneId: job.laneId,
9748
- currentAppId: job.currentAppId,
9749
- branchName: job.branchName,
9750
- lastSnapshotId: snapshot.id,
9751
- lastSnapshotHash: snapshot.snapshotHash,
9752
- lastServerHeadHash: nextServerHeadHash,
9753
- lastSeenLocalCommitHash: snapshot.localCommitHash
9754
- });
9889
+ await writeBaselineFromSucceededChangeStep({ api: params.api, job, snapshot, changeStep });
9755
9890
  await updatePendingFinalizeJob(job.id, {
9756
9891
  status: "completed",
9757
9892
  metadata: { changeStepId: String(changeStep.id ?? "") }
@@ -9780,6 +9915,7 @@ async function enqueueCapturedFinalizeTurn(params) {
9780
9915
  prompt: params.prompt,
9781
9916
  assistantResponse: params.assistantResponse,
9782
9917
  baselineSnapshotId: params.baselineSnapshotId,
9918
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
9783
9919
  baselineServerHeadHash: params.baselineServerHeadHash,
9784
9920
  currentSnapshotId: params.currentSnapshotId,
9785
9921
  idempotencyKey: params.idempotencyKey,
@@ -9878,17 +10014,6 @@ async function collabFinalizeTurn(params) {
9878
10014
  });
9879
10015
  }
9880
10016
  }
9881
- const pendingReAnchor = await findPendingAsyncJob({
9882
- repoRoot,
9883
- branchName: binding.branchName ?? null,
9884
- kind: "re_anchor"
9885
- });
9886
- if (pendingReAnchor) {
9887
- throw new RemixError("Cannot finalize a turn while a re-anchor is still processing.", {
9888
- exitCode: 2,
9889
- hint: `Re-anchor job ${pendingReAnchor.id} is still in the background queue. Run \`remix collab status\` to check progress.`
9890
- });
9891
- }
9892
10017
  const detected = await collabDetectRepoState({
9893
10018
  api: params.api,
9894
10019
  cwd: repoRoot,
@@ -9929,9 +10054,16 @@ async function collabFinalizeTurn(params) {
9929
10054
  hint: detected.hint
9930
10055
  });
9931
10056
  }
10057
+ if (detected.repoState === "both_changed") {
10058
+ throw new RemixError("Local and server changes must be reconciled before finalizing this turn.", {
10059
+ code: "reconcile_required",
10060
+ exitCode: 2,
10061
+ hint: detected.hint || "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying."
10062
+ });
10063
+ }
9932
10064
  if (detected.repoState === "external_local_base_changed") {
9933
- throw new RemixError("The local checkout must be re-anchored before finalizing this turn.", {
9934
- code: "re_anchor_required",
10065
+ throw new RemixError("The local checkout is missing a Remix revision baseline for this lane.", {
10066
+ code: "baseline_missing",
9935
10067
  exitCode: 2,
9936
10068
  hint: detected.hint
9937
10069
  });
@@ -9943,8 +10075,9 @@ async function collabFinalizeTurn(params) {
9943
10075
  });
9944
10076
  if (!baseline) {
9945
10077
  throw new RemixError("Local Remix baseline is missing for this lane.", {
10078
+ code: "baseline_missing",
9946
10079
  exitCode: 2,
9947
- hint: "Run `remix collab re-anchor` to create a fresh baseline."
10080
+ hint: "Run `remix collab init` or sync this lane to create a fresh revision baseline."
9948
10081
  });
9949
10082
  }
9950
10083
  const snapshot = await captureLocalSnapshot({
@@ -9955,10 +10088,11 @@ async function collabFinalizeTurn(params) {
9955
10088
  });
9956
10089
  const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
9957
10090
  const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
9958
- kind: "collab_finalize_turn_boundary_v1",
10091
+ kind: "collab_finalize_turn_boundary_v2",
9959
10092
  appId: binding.currentAppId,
9960
10093
  laneId: binding.laneId,
9961
10094
  baselineSnapshotId: baseline.lastSnapshotId,
10095
+ baselineServerRevisionId: baseline.lastServerRevisionId,
9962
10096
  baselineServerHeadHash: baseline.lastServerHeadHash,
9963
10097
  currentSnapshotId: snapshot.id,
9964
10098
  currentSnapshotHash: snapshot.snapshotHash,
@@ -9978,6 +10112,7 @@ async function collabFinalizeTurn(params) {
9978
10112
  prompt,
9979
10113
  assistantResponse,
9980
10114
  baselineSnapshotId: baseline.lastSnapshotId,
10115
+ baselineServerRevisionId: baseline.lastServerRevisionId,
9981
10116
  baselineServerHeadHash: baseline.lastServerHeadHash,
9982
10117
  currentSnapshotId: snapshot.id,
9983
10118
  idempotencyKey,
@@ -10024,9 +10159,10 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
10024
10159
  // Server has commits we don't. Fix: `remix collab sync` (safe to
10025
10160
  // auto-run for fast-forward; non-FF refused by the command itself).
10026
10161
  "pull_required",
10027
- // Local base hash doesn't match the recorded baseline (force-push,
10028
- // hard reset, rebase). Fix: `remix collab re-anchor`.
10029
- "re_anchor_required"
10162
+ // Both local and server changed. Fix: inspect and apply reconcile.
10163
+ "reconcile_required",
10164
+ // Local revision baseline is missing. Fix: `remix collab init` or sync.
10165
+ "baseline_missing"
10030
10166
  ];
10031
10167
  var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
10032
10168
  function isFinalizePreflightFailureCode(value) {
@@ -10438,7 +10574,7 @@ async function clearPendingTurnState(sessionId) {
10438
10574
  // package.json
10439
10575
  var package_default = {
10440
10576
  name: "@remixhq/claude-plugin",
10441
- version: "0.1.22",
10577
+ version: "0.1.24",
10442
10578
  description: "Claude Code plugin for Remix collaboration workflows",
10443
10579
  homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
10444
10580
  license: "MIT",
@@ -10476,8 +10612,8 @@ var package_default = {
10476
10612
  prepack: "npm run build"
10477
10613
  },
10478
10614
  dependencies: {
10479
- "@remixhq/core": "^0.1.17",
10480
- "@remixhq/mcp": "^0.1.17"
10615
+ "@remixhq/core": "^0.1.19",
10616
+ "@remixhq/mcp": "^0.1.19"
10481
10617
  },
10482
10618
  devDependencies: {
10483
10619
  "@types/node": "^25.4.0",
@@ -10583,11 +10719,9 @@ var AUTO_FIX_COMMAND = {
10583
10719
  // include it here too so a finalize-time failure (e.g. binding got
10584
10720
  // deleted between init and the next finalize) also self-heals.
10585
10721
  branch_binding_missing: ["collab", "init"],
10586
- // External base diverged. Re-anchor declares the new local state as
10587
- // truth. Risky if the user hard-reset to the wrong commit, but the
10588
- // worst case is "Remix forgets the previous anchor" — recorded turns
10589
- // are preserved on the server, so this is recoverable.
10590
- re_anchor_required: ["collab", "re-anchor"],
10722
+ // Local revision baseline is missing. Init seeds the branch/lane baseline
10723
+ // without requiring the user to know about the recording internals.
10724
+ baseline_missing: ["collab", "init"],
10591
10725
  // Server moved ahead. `collab sync` is fast-forward-safe by default;
10592
10726
  // it refuses non-FF on its own, so we don't need to gate here.
10593
10727
  pull_required: ["collab", "sync"]
@@ -10604,7 +10738,7 @@ var RECOMMENDED_USER_COMMAND = {
10604
10738
  missing_head: "remix collab status",
10605
10739
  remote_error: "remix collab status",
10606
10740
  pull_required: "remix collab sync",
10607
- re_anchor_required: "remix collab re-anchor"
10741
+ baseline_missing: "remix collab init"
10608
10742
  };
10609
10743
  var SPAWN_LOCK_REL = (cmdSlug) => import_node_path9.default.join(".remix", `.${cmdSlug}-spawning`);
10610
10744
  var SPAWN_LOG_REL = (cmdSlug) => import_node_path9.default.join(".remix", `${cmdSlug}.log`);
@@ -10759,6 +10893,7 @@ var import_promises21 = __toESM(require("fs/promises"), 1);
10759
10893
  var import_node_os6 = __toESM(require("os"), 1);
10760
10894
  var import_node_path10 = __toESM(require("path"), 1);
10761
10895
  var DEFERRED_TURN_SCHEMA_VERSION = 1;
10896
+ var DEFERRED_TURN_MAX_ATTEMPTS = 10;
10762
10897
  var DEFERRED_TURN_TTL_MS = 24 * 60 * 60 * 1e3;
10763
10898
  var DEFERRED_TURN_DIR = "deferred-turns";
10764
10899
  function stateRoot2() {
@@ -10802,7 +10937,7 @@ async function readDeferredTurnFile(filePath) {
10802
10937
  if (!parsed || typeof parsed !== "object") return null;
10803
10938
  const record = parsed;
10804
10939
  if (record.schemaVersion !== DEFERRED_TURN_SCHEMA_VERSION) return null;
10805
- if (typeof record.sessionId !== "string" || typeof record.turnId !== "string" || typeof record.repoRoot !== "string" || typeof record.prompt !== "string" || typeof record.assistantResponse !== "string" || typeof record.submittedAt !== "string" || typeof record.deferredAt !== "string" || record.reason !== "current_branch_unbound" && record.reason !== "recovery_in_progress") {
10940
+ if (typeof record.sessionId !== "string" || typeof record.turnId !== "string" || typeof record.repoRoot !== "string" || typeof record.prompt !== "string" || typeof record.assistantResponse !== "string" || typeof record.submittedAt !== "string" || typeof record.deferredAt !== "string" || record.reason !== "current_branch_unbound" && record.reason !== "recovery_in_progress" && record.reason !== "transient_recording_failure") {
10806
10941
  return null;
10807
10942
  }
10808
10943
  return {
@@ -10815,7 +10950,17 @@ async function readDeferredTurnFile(filePath) {
10815
10950
  submittedAt: record.submittedAt,
10816
10951
  deferredAt: record.deferredAt,
10817
10952
  reason: record.reason,
10818
- branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null
10953
+ branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null,
10954
+ // Additive fields: pre-appId-aware records on disk won't have these
10955
+ // keys at all. Coerce missing/invalid to `null` (drainer treats
10956
+ // null as "legacy, drain as today" — see drainer for the policy).
10957
+ appIdAtDefer: typeof record.appIdAtDefer === "string" ? record.appIdAtDefer : null,
10958
+ projectIdAtDefer: typeof record.projectIdAtDefer === "string" ? record.projectIdAtDefer : null,
10959
+ // Pre-attemptCount records coerce to 0 — they've never been
10960
+ // counted against the cap, so giving them the cap's full budget
10961
+ // is correct (we'd rather over-retry a legacy record than drop it
10962
+ // unexpectedly). Negative or non-finite values also coerce to 0.
10963
+ attemptCount: typeof record.attemptCount === "number" && Number.isFinite(record.attemptCount) && record.attemptCount >= 0 ? Math.floor(record.attemptCount) : 0
10819
10964
  };
10820
10965
  }
10821
10966
  async function listDeferredTurnsForRepo(repoRoot) {
@@ -10878,16 +11023,35 @@ function buildDeferredTurnRecord(params) {
10878
11023
  submittedAt: params.submittedAt,
10879
11024
  deferredAt: (/* @__PURE__ */ new Date()).toISOString(),
10880
11025
  reason: params.reason ?? "current_branch_unbound",
10881
- branchAtDefer: params.branchAtDefer
11026
+ branchAtDefer: params.branchAtDefer,
11027
+ appIdAtDefer: params.appIdAtDefer ?? null,
11028
+ projectIdAtDefer: params.projectIdAtDefer ?? null,
11029
+ // Fresh records start at zero attempts — the next drain pass will
11030
+ // be the first attempt and bump this to 1 if it fails.
11031
+ attemptCount: 0
10882
11032
  };
10883
11033
  }
11034
+ async function recordDeferredTurnFailedAttempt(filePath) {
11035
+ const current = await readDeferredTurnFile(filePath);
11036
+ if (!current) {
11037
+ return { promoted: true, finalAttemptCount: DEFERRED_TURN_MAX_ATTEMPTS };
11038
+ }
11039
+ const newAttemptCount = current.attemptCount + 1;
11040
+ if (newAttemptCount >= DEFERRED_TURN_MAX_ATTEMPTS) {
11041
+ await deleteDeferredTurnFile(filePath);
11042
+ return { promoted: true, finalAttemptCount: newAttemptCount };
11043
+ }
11044
+ const next = { ...current, attemptCount: newAttemptCount };
11045
+ await writeDeferredTurn(next);
11046
+ return { promoted: false, newAttemptCount };
11047
+ }
10884
11048
 
10885
11049
  // src/deferred-turn-drainer.ts
10886
11050
  var import_promises23 = __toESM(require("fs/promises"), 1);
10887
11051
  var import_node_path11 = __toESM(require("path"), 1);
10888
11052
  var import_node_crypto3 = require("crypto");
10889
11053
 
10890
- // node_modules/@remixhq/core/dist/chunk-US5SM7ZC.js
11054
+ // node_modules/@remixhq/core/dist/chunk-C2FOZ3O7.js
10891
11055
  async function readJsonSafe(res) {
10892
11056
  const ct = res.headers.get("content-type") ?? "";
10893
11057
  if (!ct.toLowerCase().includes("application/json")) return null;
@@ -10900,8 +11064,13 @@ async function readJsonSafe(res) {
10900
11064
  function createApiClient(config, opts) {
10901
11065
  const apiKey = (opts?.apiKey ?? "").trim();
10902
11066
  const tokenProvider = opts?.tokenProvider;
11067
+ const defaultTimeoutMs = typeof opts?.defaultRequestTimeoutMs === "number" && opts.defaultRequestTimeoutMs > 0 ? opts.defaultRequestTimeoutMs : null;
10903
11068
  const CLIENT_KEY_HEADER = "x-comerge-api-key";
10904
- async function request(path16, init) {
11069
+ function makeTimeoutSignal(timeoutMs) {
11070
+ const ms = typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
11071
+ return ms != null ? AbortSignal.timeout(ms) : void 0;
11072
+ }
11073
+ async function request(path16, init, opts2) {
10905
11074
  if (!tokenProvider) {
10906
11075
  throw new RemixError("API client is missing a token provider.", {
10907
11076
  exitCode: 1,
@@ -10912,6 +11081,7 @@ function createApiClient(config, opts) {
10912
11081
  const url = new URL(path16, config.apiUrl).toString();
10913
11082
  const doFetch = async (bearer) => fetch(url, {
10914
11083
  ...init,
11084
+ signal: makeTimeoutSignal(opts2?.timeoutMs),
10915
11085
  headers: {
10916
11086
  Accept: "application/json",
10917
11087
  "Content-Type": "application/json",
@@ -10928,12 +11098,16 @@ function createApiClient(config, opts) {
10928
11098
  if (!res.ok) {
10929
11099
  const body = await readJsonSafe(res);
10930
11100
  const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10931
- throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
11101
+ throw new RemixError(msg, {
11102
+ exitCode: 1,
11103
+ hint: body ? JSON.stringify(body, null, 2) : null,
11104
+ statusCode: res.status
11105
+ });
10932
11106
  }
10933
11107
  const json = await readJsonSafe(res);
10934
11108
  return json ?? null;
10935
11109
  }
10936
- async function requestBinary(path16, init) {
11110
+ async function requestBinary(path16, init, opts2) {
10937
11111
  if (!tokenProvider) {
10938
11112
  throw new RemixError("API client is missing a token provider.", {
10939
11113
  exitCode: 1,
@@ -10944,6 +11118,7 @@ function createApiClient(config, opts) {
10944
11118
  const url = new URL(path16, config.apiUrl).toString();
10945
11119
  const doFetch = async (bearer) => fetch(url, {
10946
11120
  ...init,
11121
+ signal: makeTimeoutSignal(opts2?.timeoutMs),
10947
11122
  headers: {
10948
11123
  Accept: "*/*",
10949
11124
  ...init?.headers ?? {},
@@ -10959,7 +11134,11 @@ function createApiClient(config, opts) {
10959
11134
  if (!res.ok) {
10960
11135
  const body = await readJsonSafe(res);
10961
11136
  const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
10962
- throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
11137
+ throw new RemixError(msg, {
11138
+ exitCode: 1,
11139
+ hint: body ? JSON.stringify(body, null, 2) : null,
11140
+ statusCode: res.status
11141
+ });
10963
11142
  }
10964
11143
  const contentDisposition = res.headers.get("content-disposition") ?? "";
10965
11144
  const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
@@ -11066,6 +11245,14 @@ function createApiClient(config, opts) {
11066
11245
  method: "POST",
11067
11246
  body: JSON.stringify(payload)
11068
11247
  }),
11248
+ listChangeSteps: (appId, params) => {
11249
+ const qs = new URLSearchParams();
11250
+ if (params?.limit !== void 0) qs.set("limit", String(params.limit));
11251
+ if (params?.offset !== void 0) qs.set("offset", String(params.offset));
11252
+ if (params?.idempotencyKey) qs.set("idempotencyKey", params.idempotencyKey);
11253
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
11254
+ return request(`/v1/apps/${encodeURIComponent(appId)}/change-steps${suffix}`, { method: "GET" });
11255
+ },
11069
11256
  createCollabTurn: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns`, {
11070
11257
  method: "POST",
11071
11258
  body: JSON.stringify(payload)
@@ -15354,7 +15541,7 @@ var coerce = {
15354
15541
  };
15355
15542
  var NEVER = INVALID;
15356
15543
 
15357
- // node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
15544
+ // node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
15358
15545
  var import_promises22 = __toESM(require("fs/promises"), 1);
15359
15546
  var import_os3 = __toESM(require("os"), 1);
15360
15547
  var import_path7 = __toESM(require("path"), 1);
@@ -35299,7 +35486,7 @@ function shouldShowDeprecationWarning() {
35299
35486
  }
35300
35487
  if (shouldShowDeprecationWarning()) console.warn("\u26A0\uFE0F Node.js 18 and below are deprecated and will no longer be supported in future versions of @supabase/supabase-js. Please upgrade to Node.js 20 or later. For more information, visit: https://github.com/orgs/supabase/discussions/37217");
35301
35488
 
35302
- // node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
35489
+ // node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
35303
35490
  var storedSessionSchema = external_exports.object({
35304
35491
  access_token: external_exports.string().min(1),
35305
35492
  refresh_token: external_exports.string().min(1),
@@ -35513,7 +35700,7 @@ function createSupabaseAuthHelpers(config) {
35513
35700
  };
35514
35701
  }
35515
35702
 
35516
- // node_modules/@remixhq/core/dist/chunk-VM3CGCNX.js
35703
+ // node_modules/@remixhq/core/dist/chunk-XCZRNB35.js
35517
35704
  var DEFAULT_API_URL = "https://api.remix.one";
35518
35705
  var DEFAULT_SUPABASE_URL = "https://xtfxwbckjpfmqubnsusu.supabase.co";
35519
35706
  var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s";
@@ -35551,6 +35738,7 @@ async function resolveConfig(_opts) {
35551
35738
  }
35552
35739
 
35553
35740
  // src/hook-auth.ts
35741
+ var HOOK_API_REQUEST_TIMEOUT_MS = 6e4;
35554
35742
  async function createHookCollabApiClient() {
35555
35743
  const config = await resolveConfig();
35556
35744
  const sessionStore = createLocalSessionStore();
@@ -35563,7 +35751,8 @@ async function createHookCollabApiClient() {
35563
35751
  }
35564
35752
  });
35565
35753
  return createApiClient(config, {
35566
- tokenProvider
35754
+ tokenProvider,
35755
+ defaultRequestTimeoutMs: HOOK_API_REQUEST_TIMEOUT_MS
35567
35756
  });
35568
35757
  }
35569
35758
 
@@ -35576,6 +35765,16 @@ var HOOK_ACTOR = {
35576
35765
  version: pluginMetadata.version,
35577
35766
  provider: "anthropic"
35578
35767
  };
35768
+ function getDrainerErrorDetails(error) {
35769
+ if (error instanceof Error) {
35770
+ const hint = typeof error.hint === "string" ? String(error.hint) : null;
35771
+ const codeRaw = error.code;
35772
+ const preflightCode = isFinalizePreflightFailureCode(codeRaw) ? codeRaw : null;
35773
+ return { message: error.message || "Deferred turn recording failed.", hint, preflightCode };
35774
+ }
35775
+ const message = typeof error === "string" && error.trim() ? error.trim() : "Deferred turn recording failed.";
35776
+ return { message, hint: null, preflightCode: null };
35777
+ }
35579
35778
  var DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS = 3e3;
35580
35779
  var DEFERRED_TURN_DRAIN_MAX_WAIT_MS = 15 * 60 * 1e3;
35581
35780
  var DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS = 3e4;
@@ -35741,6 +35940,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35741
35940
  let api = null;
35742
35941
  let recordedTotal = 0;
35743
35942
  let failedTotal = 0;
35943
+ let droppedTotal = 0;
35744
35944
  let exitReason = "queue_empty";
35745
35945
  try {
35746
35946
  while (true) {
@@ -35771,7 +35971,49 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35771
35971
  const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
35772
35972
  const currentBranch = bindingState?.currentBranch ?? null;
35773
35973
  const isCurrentBranchBound = bindingState?.binding != null;
35774
- const attemptable = entries.filter(
35974
+ const currentAppId = bindingState?.binding?.currentAppId ?? null;
35975
+ const currentProjectId = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
35976
+ let droppedThisPass = 0;
35977
+ const liveEntries = [];
35978
+ for (const entry of entries) {
35979
+ const appIdMismatch = entry.record.appIdAtDefer != null && currentAppId != null && entry.record.appIdAtDefer !== currentAppId;
35980
+ const projectIdMismatch = entry.record.projectIdAtDefer != null && currentProjectId != null && entry.record.projectIdAtDefer !== currentProjectId;
35981
+ if (appIdMismatch || projectIdMismatch) {
35982
+ await deleteDeferredTurnFile(entry.filePath);
35983
+ droppedThisPass += 1;
35984
+ await appendHookDiagnosticsEvent({
35985
+ hook: "deferredTurnDrainer",
35986
+ sessionId: sessionMarker,
35987
+ stage: "deferred_turn_dropped",
35988
+ result: "info",
35989
+ reason: appIdMismatch ? "app_id_mismatch" : "project_id_mismatch",
35990
+ repoRoot,
35991
+ fields: {
35992
+ deferredTurnId: entry.record.turnId,
35993
+ deferredSessionId: entry.record.sessionId,
35994
+ appIdAtDefer: entry.record.appIdAtDefer,
35995
+ projectIdAtDefer: entry.record.projectIdAtDefer,
35996
+ currentAppId,
35997
+ currentProjectId
35998
+ }
35999
+ });
36000
+ continue;
36001
+ }
36002
+ liveEntries.push(entry);
36003
+ }
36004
+ if (droppedThisPass > 0) {
36005
+ droppedTotal += droppedThisPass;
36006
+ }
36007
+ if (liveEntries.length === 0) {
36008
+ const remaining = await listDeferredTurnsForRepo(repoRoot).catch(() => []);
36009
+ if (remaining.length === 0) {
36010
+ exitReason = "queue_empty";
36011
+ break;
36012
+ }
36013
+ await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
36014
+ continue;
36015
+ }
36016
+ const attemptable = liveEntries.filter(
35775
36017
  (e) => isCurrentBranchBound && (!e.record.branchAtDefer || e.record.branchAtDefer === currentBranch)
35776
36018
  );
35777
36019
  if (attemptable.length === 0) {
@@ -35820,6 +36062,8 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35820
36062
  } else {
35821
36063
  failedThisPass += 1;
35822
36064
  failedTotal += 1;
36065
+ const outcome = await recordDeferredTurnFailedAttempt(entry.filePath).catch(() => null);
36066
+ const promoted = outcome?.promoted === true;
35823
36067
  await appendHookDiagnosticsEvent({
35824
36068
  hook: "deferredTurnDrainer",
35825
36069
  sessionId: sessionMarker,
@@ -35830,9 +36074,43 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35830
36074
  message: result.error instanceof Error ? result.error.message : String(result.error ?? ""),
35831
36075
  fields: {
35832
36076
  deferredTurnId: entry.record.turnId,
35833
- deferredSessionId: entry.record.sessionId
36077
+ deferredSessionId: entry.record.sessionId,
36078
+ attemptCount: outcome?.promoted === false ? outcome.newAttemptCount : outcome?.promoted === true ? outcome.finalAttemptCount : null,
36079
+ promoted
35834
36080
  }
35835
36081
  });
36082
+ if (promoted) {
36083
+ const errorDetails = getDrainerErrorDetails(result.error);
36084
+ await dispatchFinalizeFailure({
36085
+ // The dispatcher only knows about the two real Claude hook
36086
+ // entrypoints. The standalone drainer is logically a
36087
+ // post-Stop background process and the marker we're about
36088
+ // to write is consumed by the next prompt's UserPromptSubmit
36089
+ // hook, so attributing the failure to "Stop" matches what
36090
+ // the user will see.
36091
+ hook: "Stop",
36092
+ sessionId: sessionMarker,
36093
+ turnId: entry.record.turnId,
36094
+ repoRoot,
36095
+ preflightCode: errorDetails.preflightCode,
36096
+ message: `Deferred turn could not be recorded after ${outcome?.finalAttemptCount ?? "max"} attempts: ${errorDetails.message}`,
36097
+ hint: errorDetails.hint
36098
+ }).catch(async (dispatchErr) => {
36099
+ await appendHookDiagnosticsEvent({
36100
+ hook: "deferredTurnDrainer",
36101
+ sessionId: sessionMarker,
36102
+ stage: "deferred_turn_promotion_dispatch_failed",
36103
+ result: "error",
36104
+ reason: "exception",
36105
+ repoRoot,
36106
+ message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
36107
+ fields: {
36108
+ deferredTurnId: entry.record.turnId,
36109
+ deferredSessionId: entry.record.sessionId
36110
+ }
36111
+ });
36112
+ });
36113
+ }
35836
36114
  }
35837
36115
  }
35838
36116
  if (recordedThisPass > 0) {
@@ -35885,6 +36163,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
35885
36163
  fields: {
35886
36164
  recordedTotal,
35887
36165
  failedTotal,
36166
+ droppedTotal,
35888
36167
  elapsedMs: Date.now() - startedAt
35889
36168
  }
35890
36169
  });
@@ -35931,6 +36210,20 @@ function spawnDeferredTurnDrainer(repoRoot) {
35931
36210
  child.unref();
35932
36211
  }
35933
36212
 
36213
+ // src/transient-failure.ts
36214
+ function isTransientRecordingFailure(error) {
36215
+ if (!error || typeof error !== "object") return false;
36216
+ if (error instanceof Error) {
36217
+ if (error.name === "AbortError" || error.name === "TimeoutError") return true;
36218
+ if (error instanceof TypeError && /fetch failed/i.test(error.message)) return true;
36219
+ }
36220
+ const candidate = error;
36221
+ if (typeof candidate.statusCode === "number" && candidate.statusCode >= 500 && candidate.statusCode < 600) {
36222
+ return true;
36223
+ }
36224
+ return false;
36225
+ }
36226
+
35934
36227
  // node_modules/@remixhq/core/dist/history.js
35935
36228
  var import_promises24 = __toESM(require("fs/promises"), 1);
35936
36229
  async function readAndParseTranscript(transcriptPath) {
@@ -36618,11 +36911,12 @@ function getErrorDetails(error) {
36618
36911
  return {
36619
36912
  message: error.message || "Fallback Remix turn recording failed.",
36620
36913
  hint,
36621
- preflightCode
36914
+ preflightCode,
36915
+ isTransient: isTransientRecordingFailure(error)
36622
36916
  };
36623
36917
  }
36624
36918
  const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
36625
- return { message, hint: null, preflightCode: null };
36919
+ return { message, hint: null, preflightCode: null, isTransient: false };
36626
36920
  }
36627
36921
  function buildRepoIdempotencyKey(turnId, repo) {
36628
36922
  const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
@@ -36934,7 +37228,12 @@ async function recordTouchedRepo(params) {
36934
37228
  // Equivalent to the not_bound preflight code — the binding
36935
37229
  // disappeared between touch-time and finalize-time. Reusing the
36936
37230
  // code lets the dispatcher route this through the same recovery.
36937
- preflightCode: "not_bound"
37231
+ preflightCode: "not_bound",
37232
+ // Missing-binding is a permanent state mismatch (the user
37233
+ // unbinded mid-flight); not transient. Spell it out so the
37234
+ // upstream loop routes via dispatchFinalizeFailure instead of
37235
+ // silent defer.
37236
+ isTransient: false
36938
37237
  };
36939
37238
  await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
36940
37239
  message: failure.message,
@@ -36994,7 +37293,11 @@ async function recordTouchedRepo(params) {
36994
37293
  message: details.message,
36995
37294
  fields: {
36996
37295
  hint: details.hint,
36997
- preflightCode: details.preflightCode
37296
+ preflightCode: details.preflightCode,
37297
+ // Logged so a hung backend or DNS hiccup is greppable in the
37298
+ // diagnostics file alongside the Cursor mirror — the next
37299
+ // prompt's drain log will pair with this for the recovery.
37300
+ isTransient: details.isTransient
36998
37301
  }
36999
37302
  });
37000
37303
  return {
@@ -37004,7 +37307,8 @@ async function recordTouchedRepo(params) {
37004
37307
  repoRoot: repo.repoRoot,
37005
37308
  message: details.message,
37006
37309
  hint: details.hint,
37007
- preflightCode: details.preflightCode
37310
+ preflightCode: details.preflightCode,
37311
+ isTransient: details.isTransient
37008
37312
  }
37009
37313
  };
37010
37314
  }
@@ -37042,6 +37346,8 @@ async function drainDeferredTurnsForRepo(params) {
37042
37346
  const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
37043
37347
  const currentBranch = bindingState?.currentBranch ?? null;
37044
37348
  const isCurrentBranchBound = bindingState?.binding != null;
37349
+ const currentAppId = bindingState?.binding?.currentAppId ?? null;
37350
+ const currentProjectId = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
37045
37351
  await appendHookDiagnosticsEvent({
37046
37352
  hook,
37047
37353
  sessionId,
@@ -37052,14 +37358,41 @@ async function drainDeferredTurnsForRepo(params) {
37052
37358
  fields: {
37053
37359
  candidateCount: entries.length,
37054
37360
  currentBranch,
37055
- currentBranchBound: isCurrentBranchBound
37361
+ currentBranchBound: isCurrentBranchBound,
37362
+ currentAppId,
37363
+ currentProjectId
37056
37364
  }
37057
37365
  });
37058
37366
  let recordedCount = 0;
37059
37367
  let skippedCount = 0;
37060
37368
  let failedCount = 0;
37369
+ let droppedCount = 0;
37061
37370
  for (const entry of entries) {
37062
37371
  const { record, filePath } = entry;
37372
+ const appIdMismatch = record.appIdAtDefer != null && currentAppId != null && record.appIdAtDefer !== currentAppId;
37373
+ const projectIdMismatch = record.projectIdAtDefer != null && currentProjectId != null && record.projectIdAtDefer !== currentProjectId;
37374
+ if (appIdMismatch || projectIdMismatch) {
37375
+ droppedCount += 1;
37376
+ await deleteDeferredTurnFile(filePath);
37377
+ await appendHookDiagnosticsEvent({
37378
+ hook,
37379
+ sessionId,
37380
+ turnId: triggerTurnId,
37381
+ stage: "deferred_turn_dropped",
37382
+ result: "info",
37383
+ reason: appIdMismatch ? "app_id_mismatch" : "project_id_mismatch",
37384
+ repoRoot,
37385
+ fields: {
37386
+ deferredTurnId: record.turnId,
37387
+ deferredSessionId: record.sessionId,
37388
+ appIdAtDefer: record.appIdAtDefer,
37389
+ projectIdAtDefer: record.projectIdAtDefer,
37390
+ currentAppId,
37391
+ currentProjectId
37392
+ }
37393
+ });
37394
+ continue;
37395
+ }
37063
37396
  if (!isCurrentBranchBound || record.branchAtDefer && record.branchAtDefer !== currentBranch) {
37064
37397
  skippedCount += 1;
37065
37398
  await appendHookDiagnosticsEvent({
@@ -37110,6 +37443,8 @@ async function drainDeferredTurnsForRepo(params) {
37110
37443
  });
37111
37444
  } catch (recordErr) {
37112
37445
  failedCount += 1;
37446
+ const outcome = await recordDeferredTurnFailedAttempt(filePath).catch(() => null);
37447
+ const promoted = outcome?.promoted === true;
37113
37448
  await appendHookDiagnosticsEvent({
37114
37449
  hook,
37115
37450
  sessionId,
@@ -37121,9 +37456,38 @@ async function drainDeferredTurnsForRepo(params) {
37121
37456
  message: recordErr instanceof Error ? recordErr.message : String(recordErr),
37122
37457
  fields: {
37123
37458
  deferredTurnId: record.turnId,
37124
- deferredSessionId: record.sessionId
37459
+ deferredSessionId: record.sessionId,
37460
+ attemptCount: outcome?.promoted === false ? outcome.newAttemptCount : outcome?.promoted === true ? outcome.finalAttemptCount : null,
37461
+ promoted
37125
37462
  }
37126
37463
  });
37464
+ if (promoted) {
37465
+ const errorDetails = getErrorDetails(recordErr);
37466
+ await dispatchFinalizeFailure({
37467
+ hook,
37468
+ sessionId,
37469
+ turnId: triggerTurnId,
37470
+ repoRoot,
37471
+ preflightCode: errorDetails.preflightCode,
37472
+ message: `Deferred turn could not be recorded after ${outcome?.finalAttemptCount ?? "max"} attempts: ${errorDetails.message}`,
37473
+ hint: errorDetails.hint
37474
+ }).catch(async (dispatchErr) => {
37475
+ await appendHookDiagnosticsEvent({
37476
+ hook,
37477
+ sessionId,
37478
+ turnId: triggerTurnId,
37479
+ stage: "deferred_turn_promotion_dispatch_failed",
37480
+ result: "error",
37481
+ reason: "exception",
37482
+ repoRoot,
37483
+ message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
37484
+ fields: {
37485
+ deferredTurnId: record.turnId,
37486
+ deferredSessionId: record.sessionId
37487
+ }
37488
+ });
37489
+ });
37490
+ }
37127
37491
  }
37128
37492
  }
37129
37493
  try {
@@ -37167,14 +37531,75 @@ async function drainDeferredTurnsForRepo(params) {
37167
37531
  fields: {
37168
37532
  recordedCount,
37169
37533
  skippedCount,
37170
- failedCount
37534
+ failedCount,
37535
+ droppedCount
37171
37536
  }
37172
37537
  });
37173
37538
  }
37539
+ async function deferTurnForTransientFailure(params) {
37540
+ const { hook, sessionId, turnId, repoRoot, prompt, assistantResponse, submittedAt, failureMessage } = params;
37541
+ const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
37542
+ const branchAtDefer = bindingState?.currentBranch ?? null;
37543
+ const appIdAtDefer = bindingState?.binding?.currentAppId ?? null;
37544
+ const projectIdAtDefer = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
37545
+ try {
37546
+ const deferredFilePath = await writeDeferredTurn(
37547
+ buildDeferredTurnRecord({
37548
+ sessionId,
37549
+ turnId,
37550
+ repoRoot,
37551
+ prompt,
37552
+ assistantResponse,
37553
+ submittedAt,
37554
+ branchAtDefer,
37555
+ appIdAtDefer,
37556
+ projectIdAtDefer,
37557
+ reason: "transient_recording_failure"
37558
+ })
37559
+ );
37560
+ await appendHookDiagnosticsEvent({
37561
+ hook,
37562
+ sessionId,
37563
+ turnId,
37564
+ stage: "turn_deferred",
37565
+ result: "success",
37566
+ reason: "transient_recording_failure",
37567
+ repoRoot,
37568
+ fields: {
37569
+ deferredFilePath,
37570
+ promptLength: prompt.length,
37571
+ assistantResponseLength: assistantResponse.length,
37572
+ branchAtDefer,
37573
+ // Forwarded so the diagnostics timeline pairs the defer with
37574
+ // the originating recording_failed event without needing a
37575
+ // join across stages.
37576
+ failureMessage
37577
+ }
37578
+ });
37579
+ return deferredFilePath;
37580
+ } catch (deferErr) {
37581
+ await appendHookDiagnosticsEvent({
37582
+ hook,
37583
+ sessionId,
37584
+ turnId,
37585
+ stage: "deferred_turn_write_failed",
37586
+ result: "error",
37587
+ reason: "exception",
37588
+ repoRoot,
37589
+ message: deferErr instanceof Error ? deferErr.message : String(deferErr),
37590
+ fields: {
37591
+ triggeredBy: "transient_recording_failure"
37592
+ }
37593
+ });
37594
+ return null;
37595
+ }
37596
+ }
37174
37597
  async function deferTurnForRecoveryInProgress(params) {
37175
37598
  const { hook, sessionId, turnId, repoRoot, prompt, assistantResponse, submittedAt, preflightCode } = params;
37176
37599
  const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
37177
37600
  const branchAtDefer = bindingState?.currentBranch ?? null;
37601
+ const appIdAtDefer = bindingState?.binding?.currentAppId ?? null;
37602
+ const projectIdAtDefer = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
37178
37603
  try {
37179
37604
  const deferredFilePath = await writeDeferredTurn(
37180
37605
  buildDeferredTurnRecord({
@@ -37185,6 +37610,8 @@ async function deferTurnForRecoveryInProgress(params) {
37185
37610
  assistantResponse,
37186
37611
  submittedAt,
37187
37612
  branchAtDefer,
37613
+ appIdAtDefer,
37614
+ projectIdAtDefer,
37188
37615
  reason: "recovery_in_progress"
37189
37616
  })
37190
37617
  );
@@ -37299,6 +37726,7 @@ async function runHookStopCollab(payload) {
37299
37726
  let unboundBranchRepoRoot = null;
37300
37727
  let unboundBranchName = null;
37301
37728
  let unboundBranchKnownCount = 0;
37729
+ let unboundProjectIdAtDefer = null;
37302
37730
  const candidateRepoRoot = await findBoundRepo(state.initialCwd).catch(() => null);
37303
37731
  if (candidateRepoRoot) {
37304
37732
  const bindingState = await readCollabBindingState(candidateRepoRoot).catch(() => null);
@@ -37308,6 +37736,7 @@ async function runHookStopCollab(payload) {
37308
37736
  unboundBranchRepoRoot = candidateRepoRoot;
37309
37737
  unboundBranchName = bindingState.currentBranch;
37310
37738
  unboundBranchKnownCount = knownBoundBranches.length;
37739
+ unboundProjectIdAtDefer = bindingState.projectId ?? null;
37311
37740
  }
37312
37741
  }
37313
37742
  const promptTextForDefer = state.prompt.trim();
@@ -37323,7 +37752,12 @@ async function runHookStopCollab(payload) {
37323
37752
  prompt: promptTextForDefer,
37324
37753
  assistantResponse: assistantResponseForDefer,
37325
37754
  submittedAt: state.submittedAt,
37326
- branchAtDefer: unboundBranchName
37755
+ branchAtDefer: unboundBranchName,
37756
+ // No appId for an unbound lane (the binding is null
37757
+ // by construction); project id still anchors against
37758
+ // `force-new`-style identity rotations.
37759
+ appIdAtDefer: null,
37760
+ projectIdAtDefer: unboundProjectIdAtDefer
37327
37761
  })
37328
37762
  );
37329
37763
  } catch (deferErr) {
@@ -37539,6 +37973,22 @@ async function runHookStopCollab(payload) {
37539
37973
  let deferredFailureCount = 0;
37540
37974
  let dispatchFailureCount = 0;
37541
37975
  for (const failure of failures) {
37976
+ if (failure.isTransient) {
37977
+ const deferredFilePath = await deferTurnForTransientFailure({
37978
+ hook,
37979
+ sessionId,
37980
+ turnId: state.turnId,
37981
+ repoRoot: failure.repoRoot,
37982
+ prompt,
37983
+ assistantResponse,
37984
+ submittedAt: state.submittedAt,
37985
+ failureMessage: failure.message
37986
+ });
37987
+ if (deferredFilePath) {
37988
+ deferredFailureCount += 1;
37989
+ }
37990
+ continue;
37991
+ }
37542
37992
  const outcome = await dispatchFinalizeFailure({
37543
37993
  hook: "Stop",
37544
37994
  sessionId,