@remixhq/claude-plugin 0.1.22 → 0.1.23
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/.claude-plugin/plugin.json +1 -1
- package/agents/remix-collab.md +1 -1
- package/dist/hook-post-collab.cjs +5 -5
- package/dist/hook-post-collab.cjs.map +1 -1
- package/dist/hook-pre-git.cjs +2 -2
- package/dist/hook-pre-git.cjs.map +1 -1
- package/dist/hook-stop-collab.cjs +449 -65
- package/dist/hook-stop-collab.cjs.map +1 -1
- package/dist/hook-user-prompt.cjs +925 -487
- package/dist/hook-user-prompt.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +361 -486
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +3 -3
- package/skills/safe-collab-workflow/SKILL.md +3 -3
- package/skills/submit-change-step/SKILL.md +6 -5
- package/skills/sync-and-reconcile/SKILL.md +1 -1
|
@@ -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-
|
|
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-
|
|
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-
|
|
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
|
|
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:
|
|
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,
|
|
@@ -8801,6 +8815,8 @@ function buildBaseState() {
|
|
|
8801
8815
|
branchName: null,
|
|
8802
8816
|
localCommitHash: null,
|
|
8803
8817
|
currentSnapshotHash: null,
|
|
8818
|
+
currentServerRevisionId: null,
|
|
8819
|
+
currentServerTreeHash: null,
|
|
8804
8820
|
currentServerHeadHash: null,
|
|
8805
8821
|
currentServerHeadCommitId: null,
|
|
8806
8822
|
worktreeClean: false,
|
|
@@ -8834,6 +8850,8 @@ function buildBaseState() {
|
|
|
8834
8850
|
baseline: {
|
|
8835
8851
|
lastSnapshotId: null,
|
|
8836
8852
|
lastSnapshotHash: null,
|
|
8853
|
+
lastServerRevisionId: null,
|
|
8854
|
+
lastServerTreeHash: null,
|
|
8837
8855
|
lastServerHeadHash: null,
|
|
8838
8856
|
lastSeenLocalCommitHash: null
|
|
8839
8857
|
}
|
|
@@ -8960,6 +8978,8 @@ async function collabDetectRepoState(params) {
|
|
|
8960
8978
|
summarizeAsyncJobs({ repoRoot, branchName: binding.branchName ?? null })
|
|
8961
8979
|
]);
|
|
8962
8980
|
const appHead = unwrapResponseObject(headResp, "app head");
|
|
8981
|
+
detected.currentServerRevisionId = appHead.headRevisionId ?? null;
|
|
8982
|
+
detected.currentServerTreeHash = appHead.treeHash ?? null;
|
|
8963
8983
|
detected.currentServerHeadHash = appHead.headCommitHash;
|
|
8964
8984
|
detected.currentServerHeadCommitId = appHead.headCommitId;
|
|
8965
8985
|
detected.currentSnapshotHash = inspection.snapshotHash;
|
|
@@ -8968,6 +8988,8 @@ async function collabDetectRepoState(params) {
|
|
|
8968
8988
|
detected.baseline = {
|
|
8969
8989
|
lastSnapshotId: baseline?.lastSnapshotId ?? null,
|
|
8970
8990
|
lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
|
|
8991
|
+
lastServerRevisionId: baseline?.lastServerRevisionId ?? null,
|
|
8992
|
+
lastServerTreeHash: baseline?.lastServerTreeHash ?? null,
|
|
8971
8993
|
lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
|
|
8972
8994
|
lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
|
|
8973
8995
|
};
|
|
@@ -8977,6 +8999,7 @@ async function collabDetectRepoState(params) {
|
|
|
8977
8999
|
const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
|
|
8978
9000
|
baseHeadHash: localCommitHash,
|
|
8979
9001
|
targetHeadHash: appHead.headCommitHash,
|
|
9002
|
+
targetRevisionId: appHead.headRevisionId,
|
|
8980
9003
|
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
8981
9004
|
remoteUrl: binding.remoteUrl ?? void 0,
|
|
8982
9005
|
defaultBranch: binding.defaultBranch ?? void 0
|
|
@@ -8999,7 +9022,7 @@ async function collabDetectRepoState(params) {
|
|
|
8999
9022
|
}
|
|
9000
9023
|
}
|
|
9001
9024
|
detected.repoState = "external_local_base_changed";
|
|
9002
|
-
detected.hint = "No local Remix baseline exists for this lane yet. Run `remix collab
|
|
9025
|
+
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
9026
|
return detected;
|
|
9004
9027
|
}
|
|
9005
9028
|
const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
|
|
@@ -9018,7 +9041,30 @@ async function collabDetectRepoState(params) {
|
|
|
9018
9041
|
return detected;
|
|
9019
9042
|
}
|
|
9020
9043
|
const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
|
|
9021
|
-
const
|
|
9044
|
+
const serverHeadChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
|
|
9045
|
+
const revisionChanged = Boolean(
|
|
9046
|
+
baseline.lastServerRevisionId && (appHead.headRevisionId ?? null) !== baseline.lastServerRevisionId
|
|
9047
|
+
);
|
|
9048
|
+
const equivalentRevisionDrift = revisionChanged && !serverHeadChanged;
|
|
9049
|
+
if (equivalentRevisionDrift) {
|
|
9050
|
+
await writeLocalBaseline({
|
|
9051
|
+
repoRoot,
|
|
9052
|
+
repoFingerprint: binding.repoFingerprint,
|
|
9053
|
+
laneId: binding.laneId,
|
|
9054
|
+
currentAppId: binding.currentAppId,
|
|
9055
|
+
branchName: binding.branchName,
|
|
9056
|
+
lastSnapshotId: baseline.lastSnapshotId,
|
|
9057
|
+
lastSnapshotHash: baseline.lastSnapshotHash,
|
|
9058
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
9059
|
+
lastServerTreeHash: appHead.treeHash ?? baseline.lastServerTreeHash ?? null,
|
|
9060
|
+
lastServerHeadHash: appHead.headCommitHash,
|
|
9061
|
+
lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash
|
|
9062
|
+
});
|
|
9063
|
+
detected.baseline.lastServerRevisionId = appHead.headRevisionId ?? null;
|
|
9064
|
+
detected.baseline.lastServerTreeHash = appHead.treeHash ?? baseline.lastServerTreeHash ?? null;
|
|
9065
|
+
detected.baseline.lastServerHeadHash = appHead.headCommitHash;
|
|
9066
|
+
}
|
|
9067
|
+
const serverChanged = serverHeadChanged;
|
|
9022
9068
|
if (!localChanged && !serverChanged) {
|
|
9023
9069
|
detected.repoState = "idle";
|
|
9024
9070
|
return detected;
|
|
@@ -9442,6 +9488,7 @@ function buildWorkspaceMetadata(params) {
|
|
|
9442
9488
|
recordingMode: "boundary_delta",
|
|
9443
9489
|
baselineSnapshotId: params.baselineSnapshotId,
|
|
9444
9490
|
currentSnapshotId: params.currentSnapshotId,
|
|
9491
|
+
baselineServerRevisionId: params.baselineServerRevisionId ?? null,
|
|
9445
9492
|
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
9446
9493
|
currentSnapshotHash: params.currentSnapshotHash,
|
|
9447
9494
|
localCommitHash: params.localCommitHash,
|
|
@@ -9520,12 +9567,12 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9520
9567
|
throw buildFinalizeCliError({
|
|
9521
9568
|
message: "Local baseline is missing for this queued finalize job.",
|
|
9522
9569
|
exitCode: 2,
|
|
9523
|
-
hint: "Run `remix collab
|
|
9570
|
+
hint: "Run `remix collab init` to seed this checkout's revision baseline.",
|
|
9524
9571
|
disposition: "terminal",
|
|
9525
9572
|
reason: "baseline_missing"
|
|
9526
9573
|
});
|
|
9527
9574
|
}
|
|
9528
|
-
const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
|
|
9575
|
+
const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || (job.baselineServerRevisionId ? baseline.lastServerRevisionId !== job.baselineServerRevisionId : false) || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
|
|
9529
9576
|
const appHead = unwrapResponseObject(appHeadResp, "app head");
|
|
9530
9577
|
const remoteUrl = readMetadataString(job, "remoteUrl");
|
|
9531
9578
|
const defaultBranch = readMetadataString(job, "defaultBranch");
|
|
@@ -9548,12 +9595,13 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9548
9595
|
throw buildFinalizeCliError({
|
|
9549
9596
|
message: "Finalize queue baseline drifted before this job was processed.",
|
|
9550
9597
|
exitCode: 1,
|
|
9551
|
-
hint: "Process queued finalize jobs in capture order, or
|
|
9598
|
+
hint: "Process queued finalize jobs in capture order, or run `remix collab init` to refresh the revision baseline before retrying.",
|
|
9552
9599
|
disposition: "terminal",
|
|
9553
9600
|
reason: "baseline_drifted"
|
|
9554
9601
|
});
|
|
9555
9602
|
}
|
|
9556
|
-
|
|
9603
|
+
const serverStillAtBaseline = job.baselineServerRevisionId ? appHead.headRevisionId === job.baselineServerRevisionId : appHead.headCommitHash === job.baselineServerHeadHash;
|
|
9604
|
+
if (!serverStillAtBaseline) {
|
|
9557
9605
|
throw buildFinalizeCliError({
|
|
9558
9606
|
message: "Server lane changed before a no-diff turn could be recorded.",
|
|
9559
9607
|
exitCode: 2,
|
|
@@ -9575,6 +9623,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9575
9623
|
defaultBranch,
|
|
9576
9624
|
baselineSnapshotId: job.baselineSnapshotId,
|
|
9577
9625
|
currentSnapshotId: job.currentSnapshotId,
|
|
9626
|
+
baselineServerRevisionId: job.baselineServerRevisionId,
|
|
9578
9627
|
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
9579
9628
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
9580
9629
|
localCommitHash: snapshot.localCommitHash,
|
|
@@ -9595,6 +9644,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9595
9644
|
branchName: job.branchName,
|
|
9596
9645
|
lastSnapshotId: snapshot.id,
|
|
9597
9646
|
lastSnapshotHash: snapshot.snapshotHash,
|
|
9647
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
9648
|
+
lastServerTreeHash: appHead.treeHash ?? null,
|
|
9598
9649
|
lastServerHeadHash: appHead.headCommitHash,
|
|
9599
9650
|
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
9600
9651
|
});
|
|
@@ -9615,14 +9666,14 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9615
9666
|
};
|
|
9616
9667
|
}
|
|
9617
9668
|
const localBaselineAdvanced = baseline.lastSnapshotId !== job.baselineSnapshotId;
|
|
9618
|
-
const serverHeadAdvanced = appHead.headCommitHash !== job.baselineServerHeadHash;
|
|
9669
|
+
const serverHeadAdvanced = job.baselineServerRevisionId ? appHead.headRevisionId !== job.baselineServerRevisionId : appHead.headCommitHash !== job.baselineServerHeadHash;
|
|
9619
9670
|
if (baselineDrifted) {
|
|
9620
9671
|
const consistentAdvance = localBaselineAdvanced && serverHeadAdvanced;
|
|
9621
9672
|
if (!consistentAdvance) {
|
|
9622
9673
|
throw buildFinalizeCliError({
|
|
9623
9674
|
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
9675
|
exitCode: 1,
|
|
9625
|
-
hint: "Run `remix collab status` to inspect, then
|
|
9676
|
+
hint: "Run `remix collab status` to inspect, then sync or reconcile before retrying.",
|
|
9626
9677
|
disposition: "terminal",
|
|
9627
9678
|
reason: "baseline_drifted"
|
|
9628
9679
|
});
|
|
@@ -9630,6 +9681,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9630
9681
|
}
|
|
9631
9682
|
let submissionDiff = diffResult.diff;
|
|
9632
9683
|
let submissionBaseHeadHash = job.baselineServerHeadHash;
|
|
9684
|
+
let submissionBaseRevisionId = job.baselineServerRevisionId;
|
|
9633
9685
|
let replayedFromBaseHash = null;
|
|
9634
9686
|
if (!submissionBaseHeadHash) {
|
|
9635
9687
|
throw buildFinalizeCliError({
|
|
@@ -9647,7 +9699,9 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9647
9699
|
assistantResponse: job.assistantResponse,
|
|
9648
9700
|
diff: diffResult.diff,
|
|
9649
9701
|
baseCommitHash: submissionBaseHeadHash,
|
|
9702
|
+
baseRevisionId: job.baselineServerRevisionId,
|
|
9650
9703
|
targetHeadCommitHash: appHead.headCommitHash,
|
|
9704
|
+
targetRevisionId: appHead.headRevisionId,
|
|
9651
9705
|
expectedPaths: diffResult.changedPaths,
|
|
9652
9706
|
actor,
|
|
9653
9707
|
workspaceMetadata: buildWorkspaceMetadata({
|
|
@@ -9657,6 +9711,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9657
9711
|
defaultBranch,
|
|
9658
9712
|
baselineSnapshotId: job.baselineSnapshotId,
|
|
9659
9713
|
currentSnapshotId: job.currentSnapshotId,
|
|
9714
|
+
baselineServerRevisionId: job.baselineServerRevisionId,
|
|
9660
9715
|
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
9661
9716
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
9662
9717
|
localCommitHash: snapshot.localCommitHash,
|
|
@@ -9682,6 +9737,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9682
9737
|
submissionDiff = replayDiff.diff;
|
|
9683
9738
|
replayedFromBaseHash = submissionBaseHeadHash;
|
|
9684
9739
|
submissionBaseHeadHash = appHead.headCommitHash;
|
|
9740
|
+
submissionBaseRevisionId = appHead.headRevisionId;
|
|
9685
9741
|
} catch (error) {
|
|
9686
9742
|
if (error instanceof RemixError && error.finalizeDisposition === void 0) {
|
|
9687
9743
|
const detail = error.hint ? `${error.message} (${error.hint})` : error.message;
|
|
@@ -9703,6 +9759,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9703
9759
|
assistantResponse: job.assistantResponse,
|
|
9704
9760
|
diff: submissionDiff,
|
|
9705
9761
|
baseCommitHash: submissionBaseHeadHash,
|
|
9762
|
+
baseRevisionId: submissionBaseRevisionId,
|
|
9706
9763
|
headCommitHash: submissionBaseHeadHash,
|
|
9707
9764
|
changedFilesCount: diffResult.stats.changedFilesCount,
|
|
9708
9765
|
insertions: diffResult.stats.insertions,
|
|
@@ -9715,6 +9772,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9715
9772
|
defaultBranch,
|
|
9716
9773
|
baselineSnapshotId: job.baselineSnapshotId,
|
|
9717
9774
|
currentSnapshotId: job.currentSnapshotId,
|
|
9775
|
+
baselineServerRevisionId: job.baselineServerRevisionId,
|
|
9718
9776
|
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
9719
9777
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
9720
9778
|
localCommitHash: snapshot.localCommitHash,
|
|
@@ -9736,11 +9794,28 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9736
9794
|
throw buildFinalizeCliError({
|
|
9737
9795
|
message: "Backend returned a succeeded change step without a head commit hash.",
|
|
9738
9796
|
exitCode: 1,
|
|
9739
|
-
hint: "This is a backend invariant violation; retry will not help.
|
|
9797
|
+
hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again.",
|
|
9740
9798
|
disposition: "terminal",
|
|
9741
9799
|
reason: "missing_head_commit_hash"
|
|
9742
9800
|
});
|
|
9743
9801
|
}
|
|
9802
|
+
let nextServerRevisionId = typeof changeStep.resultRevisionId === "string" ? changeStep.resultRevisionId.trim() : "";
|
|
9803
|
+
let nextServerTreeHash = null;
|
|
9804
|
+
if (!nextServerRevisionId) {
|
|
9805
|
+
const freshHeadResp = await params.api.getAppHead(job.currentAppId);
|
|
9806
|
+
const freshHead = unwrapResponseObject(freshHeadResp, "app head");
|
|
9807
|
+
if (freshHead.headCommitHash !== nextServerHeadHash || !freshHead.headRevisionId) {
|
|
9808
|
+
throw buildFinalizeCliError({
|
|
9809
|
+
message: "Backend returned a succeeded change step without a matching result revision.",
|
|
9810
|
+
exitCode: 1,
|
|
9811
|
+
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`.",
|
|
9812
|
+
disposition: "terminal",
|
|
9813
|
+
reason: "missing_result_revision_id"
|
|
9814
|
+
});
|
|
9815
|
+
}
|
|
9816
|
+
nextServerRevisionId = freshHead.headRevisionId;
|
|
9817
|
+
nextServerTreeHash = freshHead.treeHash ?? null;
|
|
9818
|
+
}
|
|
9744
9819
|
await writeLocalBaseline({
|
|
9745
9820
|
repoRoot: job.repoRoot,
|
|
9746
9821
|
repoFingerprint: job.repoFingerprint,
|
|
@@ -9749,6 +9824,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
9749
9824
|
branchName: job.branchName,
|
|
9750
9825
|
lastSnapshotId: snapshot.id,
|
|
9751
9826
|
lastSnapshotHash: snapshot.snapshotHash,
|
|
9827
|
+
lastServerRevisionId: nextServerRevisionId,
|
|
9828
|
+
lastServerTreeHash: nextServerTreeHash,
|
|
9752
9829
|
lastServerHeadHash: nextServerHeadHash,
|
|
9753
9830
|
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
9754
9831
|
});
|
|
@@ -9780,6 +9857,7 @@ async function enqueueCapturedFinalizeTurn(params) {
|
|
|
9780
9857
|
prompt: params.prompt,
|
|
9781
9858
|
assistantResponse: params.assistantResponse,
|
|
9782
9859
|
baselineSnapshotId: params.baselineSnapshotId,
|
|
9860
|
+
baselineServerRevisionId: params.baselineServerRevisionId ?? null,
|
|
9783
9861
|
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
9784
9862
|
currentSnapshotId: params.currentSnapshotId,
|
|
9785
9863
|
idempotencyKey: params.idempotencyKey,
|
|
@@ -9878,17 +9956,6 @@ async function collabFinalizeTurn(params) {
|
|
|
9878
9956
|
});
|
|
9879
9957
|
}
|
|
9880
9958
|
}
|
|
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
9959
|
const detected = await collabDetectRepoState({
|
|
9893
9960
|
api: params.api,
|
|
9894
9961
|
cwd: repoRoot,
|
|
@@ -9929,9 +9996,16 @@ async function collabFinalizeTurn(params) {
|
|
|
9929
9996
|
hint: detected.hint
|
|
9930
9997
|
});
|
|
9931
9998
|
}
|
|
9999
|
+
if (detected.repoState === "both_changed") {
|
|
10000
|
+
throw new RemixError("Local and server changes must be reconciled before finalizing this turn.", {
|
|
10001
|
+
code: "reconcile_required",
|
|
10002
|
+
exitCode: 2,
|
|
10003
|
+
hint: detected.hint || "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying."
|
|
10004
|
+
});
|
|
10005
|
+
}
|
|
9932
10006
|
if (detected.repoState === "external_local_base_changed") {
|
|
9933
|
-
throw new RemixError("The local checkout
|
|
9934
|
-
code: "
|
|
10007
|
+
throw new RemixError("The local checkout is missing a Remix revision baseline for this lane.", {
|
|
10008
|
+
code: "baseline_missing",
|
|
9935
10009
|
exitCode: 2,
|
|
9936
10010
|
hint: detected.hint
|
|
9937
10011
|
});
|
|
@@ -9943,8 +10017,9 @@ async function collabFinalizeTurn(params) {
|
|
|
9943
10017
|
});
|
|
9944
10018
|
if (!baseline) {
|
|
9945
10019
|
throw new RemixError("Local Remix baseline is missing for this lane.", {
|
|
10020
|
+
code: "baseline_missing",
|
|
9946
10021
|
exitCode: 2,
|
|
9947
|
-
hint: "Run `remix collab
|
|
10022
|
+
hint: "Run `remix collab init` or sync this lane to create a fresh revision baseline."
|
|
9948
10023
|
});
|
|
9949
10024
|
}
|
|
9950
10025
|
const snapshot = await captureLocalSnapshot({
|
|
@@ -9955,10 +10030,11 @@ async function collabFinalizeTurn(params) {
|
|
|
9955
10030
|
});
|
|
9956
10031
|
const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
|
|
9957
10032
|
const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
|
|
9958
|
-
kind: "
|
|
10033
|
+
kind: "collab_finalize_turn_boundary_v2",
|
|
9959
10034
|
appId: binding.currentAppId,
|
|
9960
10035
|
laneId: binding.laneId,
|
|
9961
10036
|
baselineSnapshotId: baseline.lastSnapshotId,
|
|
10037
|
+
baselineServerRevisionId: baseline.lastServerRevisionId,
|
|
9962
10038
|
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
9963
10039
|
currentSnapshotId: snapshot.id,
|
|
9964
10040
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
@@ -9978,6 +10054,7 @@ async function collabFinalizeTurn(params) {
|
|
|
9978
10054
|
prompt,
|
|
9979
10055
|
assistantResponse,
|
|
9980
10056
|
baselineSnapshotId: baseline.lastSnapshotId,
|
|
10057
|
+
baselineServerRevisionId: baseline.lastServerRevisionId,
|
|
9981
10058
|
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
9982
10059
|
currentSnapshotId: snapshot.id,
|
|
9983
10060
|
idempotencyKey,
|
|
@@ -10024,9 +10101,10 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
|
|
|
10024
10101
|
// Server has commits we don't. Fix: `remix collab sync` (safe to
|
|
10025
10102
|
// auto-run for fast-forward; non-FF refused by the command itself).
|
|
10026
10103
|
"pull_required",
|
|
10027
|
-
//
|
|
10028
|
-
|
|
10029
|
-
|
|
10104
|
+
// Both local and server changed. Fix: inspect and apply reconcile.
|
|
10105
|
+
"reconcile_required",
|
|
10106
|
+
// Local revision baseline is missing. Fix: `remix collab init` or sync.
|
|
10107
|
+
"baseline_missing"
|
|
10030
10108
|
];
|
|
10031
10109
|
var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
|
|
10032
10110
|
function isFinalizePreflightFailureCode(value) {
|
|
@@ -10438,7 +10516,7 @@ async function clearPendingTurnState(sessionId) {
|
|
|
10438
10516
|
// package.json
|
|
10439
10517
|
var package_default = {
|
|
10440
10518
|
name: "@remixhq/claude-plugin",
|
|
10441
|
-
version: "0.1.
|
|
10519
|
+
version: "0.1.23",
|
|
10442
10520
|
description: "Claude Code plugin for Remix collaboration workflows",
|
|
10443
10521
|
homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
|
|
10444
10522
|
license: "MIT",
|
|
@@ -10476,8 +10554,8 @@ var package_default = {
|
|
|
10476
10554
|
prepack: "npm run build"
|
|
10477
10555
|
},
|
|
10478
10556
|
dependencies: {
|
|
10479
|
-
"@remixhq/core": "^0.1.
|
|
10480
|
-
"@remixhq/mcp": "^0.1.
|
|
10557
|
+
"@remixhq/core": "^0.1.18",
|
|
10558
|
+
"@remixhq/mcp": "^0.1.18"
|
|
10481
10559
|
},
|
|
10482
10560
|
devDependencies: {
|
|
10483
10561
|
"@types/node": "^25.4.0",
|
|
@@ -10583,11 +10661,9 @@ var AUTO_FIX_COMMAND = {
|
|
|
10583
10661
|
// include it here too so a finalize-time failure (e.g. binding got
|
|
10584
10662
|
// deleted between init and the next finalize) also self-heals.
|
|
10585
10663
|
branch_binding_missing: ["collab", "init"],
|
|
10586
|
-
//
|
|
10587
|
-
//
|
|
10588
|
-
|
|
10589
|
-
// are preserved on the server, so this is recoverable.
|
|
10590
|
-
re_anchor_required: ["collab", "re-anchor"],
|
|
10664
|
+
// Local revision baseline is missing. Init seeds the branch/lane baseline
|
|
10665
|
+
// without requiring the user to know about the recording internals.
|
|
10666
|
+
baseline_missing: ["collab", "init"],
|
|
10591
10667
|
// Server moved ahead. `collab sync` is fast-forward-safe by default;
|
|
10592
10668
|
// it refuses non-FF on its own, so we don't need to gate here.
|
|
10593
10669
|
pull_required: ["collab", "sync"]
|
|
@@ -10604,7 +10680,7 @@ var RECOMMENDED_USER_COMMAND = {
|
|
|
10604
10680
|
missing_head: "remix collab status",
|
|
10605
10681
|
remote_error: "remix collab status",
|
|
10606
10682
|
pull_required: "remix collab sync",
|
|
10607
|
-
|
|
10683
|
+
baseline_missing: "remix collab init"
|
|
10608
10684
|
};
|
|
10609
10685
|
var SPAWN_LOCK_REL = (cmdSlug) => import_node_path9.default.join(".remix", `.${cmdSlug}-spawning`);
|
|
10610
10686
|
var SPAWN_LOG_REL = (cmdSlug) => import_node_path9.default.join(".remix", `${cmdSlug}.log`);
|
|
@@ -10759,6 +10835,7 @@ var import_promises21 = __toESM(require("fs/promises"), 1);
|
|
|
10759
10835
|
var import_node_os6 = __toESM(require("os"), 1);
|
|
10760
10836
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
10761
10837
|
var DEFERRED_TURN_SCHEMA_VERSION = 1;
|
|
10838
|
+
var DEFERRED_TURN_MAX_ATTEMPTS = 10;
|
|
10762
10839
|
var DEFERRED_TURN_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
10763
10840
|
var DEFERRED_TURN_DIR = "deferred-turns";
|
|
10764
10841
|
function stateRoot2() {
|
|
@@ -10802,7 +10879,7 @@ async function readDeferredTurnFile(filePath) {
|
|
|
10802
10879
|
if (!parsed || typeof parsed !== "object") return null;
|
|
10803
10880
|
const record = parsed;
|
|
10804
10881
|
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") {
|
|
10882
|
+
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
10883
|
return null;
|
|
10807
10884
|
}
|
|
10808
10885
|
return {
|
|
@@ -10815,7 +10892,17 @@ async function readDeferredTurnFile(filePath) {
|
|
|
10815
10892
|
submittedAt: record.submittedAt,
|
|
10816
10893
|
deferredAt: record.deferredAt,
|
|
10817
10894
|
reason: record.reason,
|
|
10818
|
-
branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null
|
|
10895
|
+
branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null,
|
|
10896
|
+
// Additive fields: pre-appId-aware records on disk won't have these
|
|
10897
|
+
// keys at all. Coerce missing/invalid to `null` (drainer treats
|
|
10898
|
+
// null as "legacy, drain as today" — see drainer for the policy).
|
|
10899
|
+
appIdAtDefer: typeof record.appIdAtDefer === "string" ? record.appIdAtDefer : null,
|
|
10900
|
+
projectIdAtDefer: typeof record.projectIdAtDefer === "string" ? record.projectIdAtDefer : null,
|
|
10901
|
+
// Pre-attemptCount records coerce to 0 — they've never been
|
|
10902
|
+
// counted against the cap, so giving them the cap's full budget
|
|
10903
|
+
// is correct (we'd rather over-retry a legacy record than drop it
|
|
10904
|
+
// unexpectedly). Negative or non-finite values also coerce to 0.
|
|
10905
|
+
attemptCount: typeof record.attemptCount === "number" && Number.isFinite(record.attemptCount) && record.attemptCount >= 0 ? Math.floor(record.attemptCount) : 0
|
|
10819
10906
|
};
|
|
10820
10907
|
}
|
|
10821
10908
|
async function listDeferredTurnsForRepo(repoRoot) {
|
|
@@ -10878,16 +10965,35 @@ function buildDeferredTurnRecord(params) {
|
|
|
10878
10965
|
submittedAt: params.submittedAt,
|
|
10879
10966
|
deferredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10880
10967
|
reason: params.reason ?? "current_branch_unbound",
|
|
10881
|
-
branchAtDefer: params.branchAtDefer
|
|
10968
|
+
branchAtDefer: params.branchAtDefer,
|
|
10969
|
+
appIdAtDefer: params.appIdAtDefer ?? null,
|
|
10970
|
+
projectIdAtDefer: params.projectIdAtDefer ?? null,
|
|
10971
|
+
// Fresh records start at zero attempts — the next drain pass will
|
|
10972
|
+
// be the first attempt and bump this to 1 if it fails.
|
|
10973
|
+
attemptCount: 0
|
|
10882
10974
|
};
|
|
10883
10975
|
}
|
|
10976
|
+
async function recordDeferredTurnFailedAttempt(filePath) {
|
|
10977
|
+
const current = await readDeferredTurnFile(filePath);
|
|
10978
|
+
if (!current) {
|
|
10979
|
+
return { promoted: true, finalAttemptCount: DEFERRED_TURN_MAX_ATTEMPTS };
|
|
10980
|
+
}
|
|
10981
|
+
const newAttemptCount = current.attemptCount + 1;
|
|
10982
|
+
if (newAttemptCount >= DEFERRED_TURN_MAX_ATTEMPTS) {
|
|
10983
|
+
await deleteDeferredTurnFile(filePath);
|
|
10984
|
+
return { promoted: true, finalAttemptCount: newAttemptCount };
|
|
10985
|
+
}
|
|
10986
|
+
const next = { ...current, attemptCount: newAttemptCount };
|
|
10987
|
+
await writeDeferredTurn(next);
|
|
10988
|
+
return { promoted: false, newAttemptCount };
|
|
10989
|
+
}
|
|
10884
10990
|
|
|
10885
10991
|
// src/deferred-turn-drainer.ts
|
|
10886
10992
|
var import_promises23 = __toESM(require("fs/promises"), 1);
|
|
10887
10993
|
var import_node_path11 = __toESM(require("path"), 1);
|
|
10888
10994
|
var import_node_crypto3 = require("crypto");
|
|
10889
10995
|
|
|
10890
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
10996
|
+
// node_modules/@remixhq/core/dist/chunk-RCNOSZP6.js
|
|
10891
10997
|
async function readJsonSafe(res) {
|
|
10892
10998
|
const ct = res.headers.get("content-type") ?? "";
|
|
10893
10999
|
if (!ct.toLowerCase().includes("application/json")) return null;
|
|
@@ -10900,8 +11006,13 @@ async function readJsonSafe(res) {
|
|
|
10900
11006
|
function createApiClient(config, opts) {
|
|
10901
11007
|
const apiKey = (opts?.apiKey ?? "").trim();
|
|
10902
11008
|
const tokenProvider = opts?.tokenProvider;
|
|
11009
|
+
const defaultTimeoutMs = typeof opts?.defaultRequestTimeoutMs === "number" && opts.defaultRequestTimeoutMs > 0 ? opts.defaultRequestTimeoutMs : null;
|
|
10903
11010
|
const CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
10904
|
-
|
|
11011
|
+
function makeTimeoutSignal(timeoutMs) {
|
|
11012
|
+
const ms = typeof timeoutMs === "number" && timeoutMs > 0 ? timeoutMs : defaultTimeoutMs;
|
|
11013
|
+
return ms != null ? AbortSignal.timeout(ms) : void 0;
|
|
11014
|
+
}
|
|
11015
|
+
async function request(path16, init, opts2) {
|
|
10905
11016
|
if (!tokenProvider) {
|
|
10906
11017
|
throw new RemixError("API client is missing a token provider.", {
|
|
10907
11018
|
exitCode: 1,
|
|
@@ -10912,6 +11023,7 @@ function createApiClient(config, opts) {
|
|
|
10912
11023
|
const url = new URL(path16, config.apiUrl).toString();
|
|
10913
11024
|
const doFetch = async (bearer) => fetch(url, {
|
|
10914
11025
|
...init,
|
|
11026
|
+
signal: makeTimeoutSignal(opts2?.timeoutMs),
|
|
10915
11027
|
headers: {
|
|
10916
11028
|
Accept: "application/json",
|
|
10917
11029
|
"Content-Type": "application/json",
|
|
@@ -10928,12 +11040,16 @@ function createApiClient(config, opts) {
|
|
|
10928
11040
|
if (!res.ok) {
|
|
10929
11041
|
const body = await readJsonSafe(res);
|
|
10930
11042
|
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, {
|
|
11043
|
+
throw new RemixError(msg, {
|
|
11044
|
+
exitCode: 1,
|
|
11045
|
+
hint: body ? JSON.stringify(body, null, 2) : null,
|
|
11046
|
+
statusCode: res.status
|
|
11047
|
+
});
|
|
10932
11048
|
}
|
|
10933
11049
|
const json = await readJsonSafe(res);
|
|
10934
11050
|
return json ?? null;
|
|
10935
11051
|
}
|
|
10936
|
-
async function requestBinary(path16, init) {
|
|
11052
|
+
async function requestBinary(path16, init, opts2) {
|
|
10937
11053
|
if (!tokenProvider) {
|
|
10938
11054
|
throw new RemixError("API client is missing a token provider.", {
|
|
10939
11055
|
exitCode: 1,
|
|
@@ -10944,6 +11060,7 @@ function createApiClient(config, opts) {
|
|
|
10944
11060
|
const url = new URL(path16, config.apiUrl).toString();
|
|
10945
11061
|
const doFetch = async (bearer) => fetch(url, {
|
|
10946
11062
|
...init,
|
|
11063
|
+
signal: makeTimeoutSignal(opts2?.timeoutMs),
|
|
10947
11064
|
headers: {
|
|
10948
11065
|
Accept: "*/*",
|
|
10949
11066
|
...init?.headers ?? {},
|
|
@@ -10959,7 +11076,11 @@ function createApiClient(config, opts) {
|
|
|
10959
11076
|
if (!res.ok) {
|
|
10960
11077
|
const body = await readJsonSafe(res);
|
|
10961
11078
|
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, {
|
|
11079
|
+
throw new RemixError(msg, {
|
|
11080
|
+
exitCode: 1,
|
|
11081
|
+
hint: body ? JSON.stringify(body, null, 2) : null,
|
|
11082
|
+
statusCode: res.status
|
|
11083
|
+
});
|
|
10963
11084
|
}
|
|
10964
11085
|
const contentDisposition = res.headers.get("content-disposition") ?? "";
|
|
10965
11086
|
const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
|
|
@@ -15354,7 +15475,7 @@ var coerce = {
|
|
|
15354
15475
|
};
|
|
15355
15476
|
var NEVER = INVALID;
|
|
15356
15477
|
|
|
15357
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
15478
|
+
// node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
|
|
15358
15479
|
var import_promises22 = __toESM(require("fs/promises"), 1);
|
|
15359
15480
|
var import_os3 = __toESM(require("os"), 1);
|
|
15360
15481
|
var import_path7 = __toESM(require("path"), 1);
|
|
@@ -35299,7 +35420,7 @@ function shouldShowDeprecationWarning() {
|
|
|
35299
35420
|
}
|
|
35300
35421
|
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
35422
|
|
|
35302
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
35423
|
+
// node_modules/@remixhq/core/dist/chunk-XETDXVGM.js
|
|
35303
35424
|
var storedSessionSchema = external_exports.object({
|
|
35304
35425
|
access_token: external_exports.string().min(1),
|
|
35305
35426
|
refresh_token: external_exports.string().min(1),
|
|
@@ -35513,7 +35634,7 @@ function createSupabaseAuthHelpers(config) {
|
|
|
35513
35634
|
};
|
|
35514
35635
|
}
|
|
35515
35636
|
|
|
35516
|
-
// node_modules/@remixhq/core/dist/chunk-
|
|
35637
|
+
// node_modules/@remixhq/core/dist/chunk-XCZRNB35.js
|
|
35517
35638
|
var DEFAULT_API_URL = "https://api.remix.one";
|
|
35518
35639
|
var DEFAULT_SUPABASE_URL = "https://xtfxwbckjpfmqubnsusu.supabase.co";
|
|
35519
35640
|
var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inh0Znh3YmNranBmbXF1Ym5zdXN1Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA2MDEyMzAsImV4cCI6MjA3NjE3NzIzMH0.dzWGAWrK4CvrmHVHzf8w7JlUZohdap0ZPnLZnABMV8s";
|
|
@@ -35551,6 +35672,7 @@ async function resolveConfig(_opts) {
|
|
|
35551
35672
|
}
|
|
35552
35673
|
|
|
35553
35674
|
// src/hook-auth.ts
|
|
35675
|
+
var HOOK_API_REQUEST_TIMEOUT_MS = 6e4;
|
|
35554
35676
|
async function createHookCollabApiClient() {
|
|
35555
35677
|
const config = await resolveConfig();
|
|
35556
35678
|
const sessionStore = createLocalSessionStore();
|
|
@@ -35563,7 +35685,8 @@ async function createHookCollabApiClient() {
|
|
|
35563
35685
|
}
|
|
35564
35686
|
});
|
|
35565
35687
|
return createApiClient(config, {
|
|
35566
|
-
tokenProvider
|
|
35688
|
+
tokenProvider,
|
|
35689
|
+
defaultRequestTimeoutMs: HOOK_API_REQUEST_TIMEOUT_MS
|
|
35567
35690
|
});
|
|
35568
35691
|
}
|
|
35569
35692
|
|
|
@@ -35576,6 +35699,16 @@ var HOOK_ACTOR = {
|
|
|
35576
35699
|
version: pluginMetadata.version,
|
|
35577
35700
|
provider: "anthropic"
|
|
35578
35701
|
};
|
|
35702
|
+
function getDrainerErrorDetails(error) {
|
|
35703
|
+
if (error instanceof Error) {
|
|
35704
|
+
const hint = typeof error.hint === "string" ? String(error.hint) : null;
|
|
35705
|
+
const codeRaw = error.code;
|
|
35706
|
+
const preflightCode = isFinalizePreflightFailureCode(codeRaw) ? codeRaw : null;
|
|
35707
|
+
return { message: error.message || "Deferred turn recording failed.", hint, preflightCode };
|
|
35708
|
+
}
|
|
35709
|
+
const message = typeof error === "string" && error.trim() ? error.trim() : "Deferred turn recording failed.";
|
|
35710
|
+
return { message, hint: null, preflightCode: null };
|
|
35711
|
+
}
|
|
35579
35712
|
var DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS = 3e3;
|
|
35580
35713
|
var DEFERRED_TURN_DRAIN_MAX_WAIT_MS = 15 * 60 * 1e3;
|
|
35581
35714
|
var DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS = 3e4;
|
|
@@ -35741,6 +35874,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
|
|
|
35741
35874
|
let api = null;
|
|
35742
35875
|
let recordedTotal = 0;
|
|
35743
35876
|
let failedTotal = 0;
|
|
35877
|
+
let droppedTotal = 0;
|
|
35744
35878
|
let exitReason = "queue_empty";
|
|
35745
35879
|
try {
|
|
35746
35880
|
while (true) {
|
|
@@ -35771,7 +35905,49 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
|
|
|
35771
35905
|
const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
|
|
35772
35906
|
const currentBranch = bindingState?.currentBranch ?? null;
|
|
35773
35907
|
const isCurrentBranchBound = bindingState?.binding != null;
|
|
35774
|
-
const
|
|
35908
|
+
const currentAppId = bindingState?.binding?.currentAppId ?? null;
|
|
35909
|
+
const currentProjectId = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
|
|
35910
|
+
let droppedThisPass = 0;
|
|
35911
|
+
const liveEntries = [];
|
|
35912
|
+
for (const entry of entries) {
|
|
35913
|
+
const appIdMismatch = entry.record.appIdAtDefer != null && currentAppId != null && entry.record.appIdAtDefer !== currentAppId;
|
|
35914
|
+
const projectIdMismatch = entry.record.projectIdAtDefer != null && currentProjectId != null && entry.record.projectIdAtDefer !== currentProjectId;
|
|
35915
|
+
if (appIdMismatch || projectIdMismatch) {
|
|
35916
|
+
await deleteDeferredTurnFile(entry.filePath);
|
|
35917
|
+
droppedThisPass += 1;
|
|
35918
|
+
await appendHookDiagnosticsEvent({
|
|
35919
|
+
hook: "deferredTurnDrainer",
|
|
35920
|
+
sessionId: sessionMarker,
|
|
35921
|
+
stage: "deferred_turn_dropped",
|
|
35922
|
+
result: "info",
|
|
35923
|
+
reason: appIdMismatch ? "app_id_mismatch" : "project_id_mismatch",
|
|
35924
|
+
repoRoot,
|
|
35925
|
+
fields: {
|
|
35926
|
+
deferredTurnId: entry.record.turnId,
|
|
35927
|
+
deferredSessionId: entry.record.sessionId,
|
|
35928
|
+
appIdAtDefer: entry.record.appIdAtDefer,
|
|
35929
|
+
projectIdAtDefer: entry.record.projectIdAtDefer,
|
|
35930
|
+
currentAppId,
|
|
35931
|
+
currentProjectId
|
|
35932
|
+
}
|
|
35933
|
+
});
|
|
35934
|
+
continue;
|
|
35935
|
+
}
|
|
35936
|
+
liveEntries.push(entry);
|
|
35937
|
+
}
|
|
35938
|
+
if (droppedThisPass > 0) {
|
|
35939
|
+
droppedTotal += droppedThisPass;
|
|
35940
|
+
}
|
|
35941
|
+
if (liveEntries.length === 0) {
|
|
35942
|
+
const remaining = await listDeferredTurnsForRepo(repoRoot).catch(() => []);
|
|
35943
|
+
if (remaining.length === 0) {
|
|
35944
|
+
exitReason = "queue_empty";
|
|
35945
|
+
break;
|
|
35946
|
+
}
|
|
35947
|
+
await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
|
|
35948
|
+
continue;
|
|
35949
|
+
}
|
|
35950
|
+
const attemptable = liveEntries.filter(
|
|
35775
35951
|
(e) => isCurrentBranchBound && (!e.record.branchAtDefer || e.record.branchAtDefer === currentBranch)
|
|
35776
35952
|
);
|
|
35777
35953
|
if (attemptable.length === 0) {
|
|
@@ -35820,6 +35996,8 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
|
|
|
35820
35996
|
} else {
|
|
35821
35997
|
failedThisPass += 1;
|
|
35822
35998
|
failedTotal += 1;
|
|
35999
|
+
const outcome = await recordDeferredTurnFailedAttempt(entry.filePath).catch(() => null);
|
|
36000
|
+
const promoted = outcome?.promoted === true;
|
|
35823
36001
|
await appendHookDiagnosticsEvent({
|
|
35824
36002
|
hook: "deferredTurnDrainer",
|
|
35825
36003
|
sessionId: sessionMarker,
|
|
@@ -35830,9 +36008,43 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
|
|
|
35830
36008
|
message: result.error instanceof Error ? result.error.message : String(result.error ?? ""),
|
|
35831
36009
|
fields: {
|
|
35832
36010
|
deferredTurnId: entry.record.turnId,
|
|
35833
|
-
deferredSessionId: entry.record.sessionId
|
|
36011
|
+
deferredSessionId: entry.record.sessionId,
|
|
36012
|
+
attemptCount: outcome?.promoted === false ? outcome.newAttemptCount : outcome?.promoted === true ? outcome.finalAttemptCount : null,
|
|
36013
|
+
promoted
|
|
35834
36014
|
}
|
|
35835
36015
|
});
|
|
36016
|
+
if (promoted) {
|
|
36017
|
+
const errorDetails = getDrainerErrorDetails(result.error);
|
|
36018
|
+
await dispatchFinalizeFailure({
|
|
36019
|
+
// The dispatcher only knows about the two real Claude hook
|
|
36020
|
+
// entrypoints. The standalone drainer is logically a
|
|
36021
|
+
// post-Stop background process and the marker we're about
|
|
36022
|
+
// to write is consumed by the next prompt's UserPromptSubmit
|
|
36023
|
+
// hook, so attributing the failure to "Stop" matches what
|
|
36024
|
+
// the user will see.
|
|
36025
|
+
hook: "Stop",
|
|
36026
|
+
sessionId: sessionMarker,
|
|
36027
|
+
turnId: entry.record.turnId,
|
|
36028
|
+
repoRoot,
|
|
36029
|
+
preflightCode: errorDetails.preflightCode,
|
|
36030
|
+
message: `Deferred turn could not be recorded after ${outcome?.finalAttemptCount ?? "max"} attempts: ${errorDetails.message}`,
|
|
36031
|
+
hint: errorDetails.hint
|
|
36032
|
+
}).catch(async (dispatchErr) => {
|
|
36033
|
+
await appendHookDiagnosticsEvent({
|
|
36034
|
+
hook: "deferredTurnDrainer",
|
|
36035
|
+
sessionId: sessionMarker,
|
|
36036
|
+
stage: "deferred_turn_promotion_dispatch_failed",
|
|
36037
|
+
result: "error",
|
|
36038
|
+
reason: "exception",
|
|
36039
|
+
repoRoot,
|
|
36040
|
+
message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
|
|
36041
|
+
fields: {
|
|
36042
|
+
deferredTurnId: entry.record.turnId,
|
|
36043
|
+
deferredSessionId: entry.record.sessionId
|
|
36044
|
+
}
|
|
36045
|
+
});
|
|
36046
|
+
});
|
|
36047
|
+
}
|
|
35836
36048
|
}
|
|
35837
36049
|
}
|
|
35838
36050
|
if (recordedThisPass > 0) {
|
|
@@ -35885,6 +36097,7 @@ async function runStandaloneDeferredTurnDrainer(repoRoot) {
|
|
|
35885
36097
|
fields: {
|
|
35886
36098
|
recordedTotal,
|
|
35887
36099
|
failedTotal,
|
|
36100
|
+
droppedTotal,
|
|
35888
36101
|
elapsedMs: Date.now() - startedAt
|
|
35889
36102
|
}
|
|
35890
36103
|
});
|
|
@@ -35931,6 +36144,20 @@ function spawnDeferredTurnDrainer(repoRoot) {
|
|
|
35931
36144
|
child.unref();
|
|
35932
36145
|
}
|
|
35933
36146
|
|
|
36147
|
+
// src/transient-failure.ts
|
|
36148
|
+
function isTransientRecordingFailure(error) {
|
|
36149
|
+
if (!error || typeof error !== "object") return false;
|
|
36150
|
+
if (error instanceof Error) {
|
|
36151
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") return true;
|
|
36152
|
+
if (error instanceof TypeError && /fetch failed/i.test(error.message)) return true;
|
|
36153
|
+
}
|
|
36154
|
+
const candidate = error;
|
|
36155
|
+
if (typeof candidate.statusCode === "number" && candidate.statusCode >= 500 && candidate.statusCode < 600) {
|
|
36156
|
+
return true;
|
|
36157
|
+
}
|
|
36158
|
+
return false;
|
|
36159
|
+
}
|
|
36160
|
+
|
|
35934
36161
|
// node_modules/@remixhq/core/dist/history.js
|
|
35935
36162
|
var import_promises24 = __toESM(require("fs/promises"), 1);
|
|
35936
36163
|
async function readAndParseTranscript(transcriptPath) {
|
|
@@ -36618,11 +36845,12 @@ function getErrorDetails(error) {
|
|
|
36618
36845
|
return {
|
|
36619
36846
|
message: error.message || "Fallback Remix turn recording failed.",
|
|
36620
36847
|
hint,
|
|
36621
|
-
preflightCode
|
|
36848
|
+
preflightCode,
|
|
36849
|
+
isTransient: isTransientRecordingFailure(error)
|
|
36622
36850
|
};
|
|
36623
36851
|
}
|
|
36624
36852
|
const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
|
|
36625
|
-
return { message, hint: null, preflightCode: null };
|
|
36853
|
+
return { message, hint: null, preflightCode: null, isTransient: false };
|
|
36626
36854
|
}
|
|
36627
36855
|
function buildRepoIdempotencyKey(turnId, repo) {
|
|
36628
36856
|
const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
|
|
@@ -36934,7 +37162,12 @@ async function recordTouchedRepo(params) {
|
|
|
36934
37162
|
// Equivalent to the not_bound preflight code — the binding
|
|
36935
37163
|
// disappeared between touch-time and finalize-time. Reusing the
|
|
36936
37164
|
// code lets the dispatcher route this through the same recovery.
|
|
36937
|
-
preflightCode: "not_bound"
|
|
37165
|
+
preflightCode: "not_bound",
|
|
37166
|
+
// Missing-binding is a permanent state mismatch (the user
|
|
37167
|
+
// unbinded mid-flight); not transient. Spell it out so the
|
|
37168
|
+
// upstream loop routes via dispatchFinalizeFailure instead of
|
|
37169
|
+
// silent defer.
|
|
37170
|
+
isTransient: false
|
|
36938
37171
|
};
|
|
36939
37172
|
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
|
|
36940
37173
|
message: failure.message,
|
|
@@ -36994,7 +37227,11 @@ async function recordTouchedRepo(params) {
|
|
|
36994
37227
|
message: details.message,
|
|
36995
37228
|
fields: {
|
|
36996
37229
|
hint: details.hint,
|
|
36997
|
-
preflightCode: details.preflightCode
|
|
37230
|
+
preflightCode: details.preflightCode,
|
|
37231
|
+
// Logged so a hung backend or DNS hiccup is greppable in the
|
|
37232
|
+
// diagnostics file alongside the Cursor mirror — the next
|
|
37233
|
+
// prompt's drain log will pair with this for the recovery.
|
|
37234
|
+
isTransient: details.isTransient
|
|
36998
37235
|
}
|
|
36999
37236
|
});
|
|
37000
37237
|
return {
|
|
@@ -37004,7 +37241,8 @@ async function recordTouchedRepo(params) {
|
|
|
37004
37241
|
repoRoot: repo.repoRoot,
|
|
37005
37242
|
message: details.message,
|
|
37006
37243
|
hint: details.hint,
|
|
37007
|
-
preflightCode: details.preflightCode
|
|
37244
|
+
preflightCode: details.preflightCode,
|
|
37245
|
+
isTransient: details.isTransient
|
|
37008
37246
|
}
|
|
37009
37247
|
};
|
|
37010
37248
|
}
|
|
@@ -37042,6 +37280,8 @@ async function drainDeferredTurnsForRepo(params) {
|
|
|
37042
37280
|
const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
|
|
37043
37281
|
const currentBranch = bindingState?.currentBranch ?? null;
|
|
37044
37282
|
const isCurrentBranchBound = bindingState?.binding != null;
|
|
37283
|
+
const currentAppId = bindingState?.binding?.currentAppId ?? null;
|
|
37284
|
+
const currentProjectId = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
|
|
37045
37285
|
await appendHookDiagnosticsEvent({
|
|
37046
37286
|
hook,
|
|
37047
37287
|
sessionId,
|
|
@@ -37052,14 +37292,41 @@ async function drainDeferredTurnsForRepo(params) {
|
|
|
37052
37292
|
fields: {
|
|
37053
37293
|
candidateCount: entries.length,
|
|
37054
37294
|
currentBranch,
|
|
37055
|
-
currentBranchBound: isCurrentBranchBound
|
|
37295
|
+
currentBranchBound: isCurrentBranchBound,
|
|
37296
|
+
currentAppId,
|
|
37297
|
+
currentProjectId
|
|
37056
37298
|
}
|
|
37057
37299
|
});
|
|
37058
37300
|
let recordedCount = 0;
|
|
37059
37301
|
let skippedCount = 0;
|
|
37060
37302
|
let failedCount = 0;
|
|
37303
|
+
let droppedCount = 0;
|
|
37061
37304
|
for (const entry of entries) {
|
|
37062
37305
|
const { record, filePath } = entry;
|
|
37306
|
+
const appIdMismatch = record.appIdAtDefer != null && currentAppId != null && record.appIdAtDefer !== currentAppId;
|
|
37307
|
+
const projectIdMismatch = record.projectIdAtDefer != null && currentProjectId != null && record.projectIdAtDefer !== currentProjectId;
|
|
37308
|
+
if (appIdMismatch || projectIdMismatch) {
|
|
37309
|
+
droppedCount += 1;
|
|
37310
|
+
await deleteDeferredTurnFile(filePath);
|
|
37311
|
+
await appendHookDiagnosticsEvent({
|
|
37312
|
+
hook,
|
|
37313
|
+
sessionId,
|
|
37314
|
+
turnId: triggerTurnId,
|
|
37315
|
+
stage: "deferred_turn_dropped",
|
|
37316
|
+
result: "info",
|
|
37317
|
+
reason: appIdMismatch ? "app_id_mismatch" : "project_id_mismatch",
|
|
37318
|
+
repoRoot,
|
|
37319
|
+
fields: {
|
|
37320
|
+
deferredTurnId: record.turnId,
|
|
37321
|
+
deferredSessionId: record.sessionId,
|
|
37322
|
+
appIdAtDefer: record.appIdAtDefer,
|
|
37323
|
+
projectIdAtDefer: record.projectIdAtDefer,
|
|
37324
|
+
currentAppId,
|
|
37325
|
+
currentProjectId
|
|
37326
|
+
}
|
|
37327
|
+
});
|
|
37328
|
+
continue;
|
|
37329
|
+
}
|
|
37063
37330
|
if (!isCurrentBranchBound || record.branchAtDefer && record.branchAtDefer !== currentBranch) {
|
|
37064
37331
|
skippedCount += 1;
|
|
37065
37332
|
await appendHookDiagnosticsEvent({
|
|
@@ -37110,6 +37377,8 @@ async function drainDeferredTurnsForRepo(params) {
|
|
|
37110
37377
|
});
|
|
37111
37378
|
} catch (recordErr) {
|
|
37112
37379
|
failedCount += 1;
|
|
37380
|
+
const outcome = await recordDeferredTurnFailedAttempt(filePath).catch(() => null);
|
|
37381
|
+
const promoted = outcome?.promoted === true;
|
|
37113
37382
|
await appendHookDiagnosticsEvent({
|
|
37114
37383
|
hook,
|
|
37115
37384
|
sessionId,
|
|
@@ -37121,9 +37390,38 @@ async function drainDeferredTurnsForRepo(params) {
|
|
|
37121
37390
|
message: recordErr instanceof Error ? recordErr.message : String(recordErr),
|
|
37122
37391
|
fields: {
|
|
37123
37392
|
deferredTurnId: record.turnId,
|
|
37124
|
-
deferredSessionId: record.sessionId
|
|
37393
|
+
deferredSessionId: record.sessionId,
|
|
37394
|
+
attemptCount: outcome?.promoted === false ? outcome.newAttemptCount : outcome?.promoted === true ? outcome.finalAttemptCount : null,
|
|
37395
|
+
promoted
|
|
37125
37396
|
}
|
|
37126
37397
|
});
|
|
37398
|
+
if (promoted) {
|
|
37399
|
+
const errorDetails = getErrorDetails(recordErr);
|
|
37400
|
+
await dispatchFinalizeFailure({
|
|
37401
|
+
hook,
|
|
37402
|
+
sessionId,
|
|
37403
|
+
turnId: triggerTurnId,
|
|
37404
|
+
repoRoot,
|
|
37405
|
+
preflightCode: errorDetails.preflightCode,
|
|
37406
|
+
message: `Deferred turn could not be recorded after ${outcome?.finalAttemptCount ?? "max"} attempts: ${errorDetails.message}`,
|
|
37407
|
+
hint: errorDetails.hint
|
|
37408
|
+
}).catch(async (dispatchErr) => {
|
|
37409
|
+
await appendHookDiagnosticsEvent({
|
|
37410
|
+
hook,
|
|
37411
|
+
sessionId,
|
|
37412
|
+
turnId: triggerTurnId,
|
|
37413
|
+
stage: "deferred_turn_promotion_dispatch_failed",
|
|
37414
|
+
result: "error",
|
|
37415
|
+
reason: "exception",
|
|
37416
|
+
repoRoot,
|
|
37417
|
+
message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr),
|
|
37418
|
+
fields: {
|
|
37419
|
+
deferredTurnId: record.turnId,
|
|
37420
|
+
deferredSessionId: record.sessionId
|
|
37421
|
+
}
|
|
37422
|
+
});
|
|
37423
|
+
});
|
|
37424
|
+
}
|
|
37127
37425
|
}
|
|
37128
37426
|
}
|
|
37129
37427
|
try {
|
|
@@ -37167,14 +37465,75 @@ async function drainDeferredTurnsForRepo(params) {
|
|
|
37167
37465
|
fields: {
|
|
37168
37466
|
recordedCount,
|
|
37169
37467
|
skippedCount,
|
|
37170
|
-
failedCount
|
|
37468
|
+
failedCount,
|
|
37469
|
+
droppedCount
|
|
37171
37470
|
}
|
|
37172
37471
|
});
|
|
37173
37472
|
}
|
|
37473
|
+
async function deferTurnForTransientFailure(params) {
|
|
37474
|
+
const { hook, sessionId, turnId, repoRoot, prompt, assistantResponse, submittedAt, failureMessage } = params;
|
|
37475
|
+
const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
|
|
37476
|
+
const branchAtDefer = bindingState?.currentBranch ?? null;
|
|
37477
|
+
const appIdAtDefer = bindingState?.binding?.currentAppId ?? null;
|
|
37478
|
+
const projectIdAtDefer = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
|
|
37479
|
+
try {
|
|
37480
|
+
const deferredFilePath = await writeDeferredTurn(
|
|
37481
|
+
buildDeferredTurnRecord({
|
|
37482
|
+
sessionId,
|
|
37483
|
+
turnId,
|
|
37484
|
+
repoRoot,
|
|
37485
|
+
prompt,
|
|
37486
|
+
assistantResponse,
|
|
37487
|
+
submittedAt,
|
|
37488
|
+
branchAtDefer,
|
|
37489
|
+
appIdAtDefer,
|
|
37490
|
+
projectIdAtDefer,
|
|
37491
|
+
reason: "transient_recording_failure"
|
|
37492
|
+
})
|
|
37493
|
+
);
|
|
37494
|
+
await appendHookDiagnosticsEvent({
|
|
37495
|
+
hook,
|
|
37496
|
+
sessionId,
|
|
37497
|
+
turnId,
|
|
37498
|
+
stage: "turn_deferred",
|
|
37499
|
+
result: "success",
|
|
37500
|
+
reason: "transient_recording_failure",
|
|
37501
|
+
repoRoot,
|
|
37502
|
+
fields: {
|
|
37503
|
+
deferredFilePath,
|
|
37504
|
+
promptLength: prompt.length,
|
|
37505
|
+
assistantResponseLength: assistantResponse.length,
|
|
37506
|
+
branchAtDefer,
|
|
37507
|
+
// Forwarded so the diagnostics timeline pairs the defer with
|
|
37508
|
+
// the originating recording_failed event without needing a
|
|
37509
|
+
// join across stages.
|
|
37510
|
+
failureMessage
|
|
37511
|
+
}
|
|
37512
|
+
});
|
|
37513
|
+
return deferredFilePath;
|
|
37514
|
+
} catch (deferErr) {
|
|
37515
|
+
await appendHookDiagnosticsEvent({
|
|
37516
|
+
hook,
|
|
37517
|
+
sessionId,
|
|
37518
|
+
turnId,
|
|
37519
|
+
stage: "deferred_turn_write_failed",
|
|
37520
|
+
result: "error",
|
|
37521
|
+
reason: "exception",
|
|
37522
|
+
repoRoot,
|
|
37523
|
+
message: deferErr instanceof Error ? deferErr.message : String(deferErr),
|
|
37524
|
+
fields: {
|
|
37525
|
+
triggeredBy: "transient_recording_failure"
|
|
37526
|
+
}
|
|
37527
|
+
});
|
|
37528
|
+
return null;
|
|
37529
|
+
}
|
|
37530
|
+
}
|
|
37174
37531
|
async function deferTurnForRecoveryInProgress(params) {
|
|
37175
37532
|
const { hook, sessionId, turnId, repoRoot, prompt, assistantResponse, submittedAt, preflightCode } = params;
|
|
37176
37533
|
const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
|
|
37177
37534
|
const branchAtDefer = bindingState?.currentBranch ?? null;
|
|
37535
|
+
const appIdAtDefer = bindingState?.binding?.currentAppId ?? null;
|
|
37536
|
+
const projectIdAtDefer = bindingState?.binding?.projectId ?? bindingState?.projectId ?? null;
|
|
37178
37537
|
try {
|
|
37179
37538
|
const deferredFilePath = await writeDeferredTurn(
|
|
37180
37539
|
buildDeferredTurnRecord({
|
|
@@ -37185,6 +37544,8 @@ async function deferTurnForRecoveryInProgress(params) {
|
|
|
37185
37544
|
assistantResponse,
|
|
37186
37545
|
submittedAt,
|
|
37187
37546
|
branchAtDefer,
|
|
37547
|
+
appIdAtDefer,
|
|
37548
|
+
projectIdAtDefer,
|
|
37188
37549
|
reason: "recovery_in_progress"
|
|
37189
37550
|
})
|
|
37190
37551
|
);
|
|
@@ -37299,6 +37660,7 @@ async function runHookStopCollab(payload) {
|
|
|
37299
37660
|
let unboundBranchRepoRoot = null;
|
|
37300
37661
|
let unboundBranchName = null;
|
|
37301
37662
|
let unboundBranchKnownCount = 0;
|
|
37663
|
+
let unboundProjectIdAtDefer = null;
|
|
37302
37664
|
const candidateRepoRoot = await findBoundRepo(state.initialCwd).catch(() => null);
|
|
37303
37665
|
if (candidateRepoRoot) {
|
|
37304
37666
|
const bindingState = await readCollabBindingState(candidateRepoRoot).catch(() => null);
|
|
@@ -37308,6 +37670,7 @@ async function runHookStopCollab(payload) {
|
|
|
37308
37670
|
unboundBranchRepoRoot = candidateRepoRoot;
|
|
37309
37671
|
unboundBranchName = bindingState.currentBranch;
|
|
37310
37672
|
unboundBranchKnownCount = knownBoundBranches.length;
|
|
37673
|
+
unboundProjectIdAtDefer = bindingState.projectId ?? null;
|
|
37311
37674
|
}
|
|
37312
37675
|
}
|
|
37313
37676
|
const promptTextForDefer = state.prompt.trim();
|
|
@@ -37323,7 +37686,12 @@ async function runHookStopCollab(payload) {
|
|
|
37323
37686
|
prompt: promptTextForDefer,
|
|
37324
37687
|
assistantResponse: assistantResponseForDefer,
|
|
37325
37688
|
submittedAt: state.submittedAt,
|
|
37326
|
-
branchAtDefer: unboundBranchName
|
|
37689
|
+
branchAtDefer: unboundBranchName,
|
|
37690
|
+
// No appId for an unbound lane (the binding is null
|
|
37691
|
+
// by construction); project id still anchors against
|
|
37692
|
+
// `force-new`-style identity rotations.
|
|
37693
|
+
appIdAtDefer: null,
|
|
37694
|
+
projectIdAtDefer: unboundProjectIdAtDefer
|
|
37327
37695
|
})
|
|
37328
37696
|
);
|
|
37329
37697
|
} catch (deferErr) {
|
|
@@ -37539,6 +37907,22 @@ async function runHookStopCollab(payload) {
|
|
|
37539
37907
|
let deferredFailureCount = 0;
|
|
37540
37908
|
let dispatchFailureCount = 0;
|
|
37541
37909
|
for (const failure of failures) {
|
|
37910
|
+
if (failure.isTransient) {
|
|
37911
|
+
const deferredFilePath = await deferTurnForTransientFailure({
|
|
37912
|
+
hook,
|
|
37913
|
+
sessionId,
|
|
37914
|
+
turnId: state.turnId,
|
|
37915
|
+
repoRoot: failure.repoRoot,
|
|
37916
|
+
prompt,
|
|
37917
|
+
assistantResponse,
|
|
37918
|
+
submittedAt: state.submittedAt,
|
|
37919
|
+
failureMessage: failure.message
|
|
37920
|
+
});
|
|
37921
|
+
if (deferredFilePath) {
|
|
37922
|
+
deferredFailureCount += 1;
|
|
37923
|
+
}
|
|
37924
|
+
continue;
|
|
37925
|
+
}
|
|
37542
37926
|
const outcome = await dispatchFinalizeFailure({
|
|
37543
37927
|
hook: "Stop",
|
|
37544
37928
|
sessionId,
|