@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.
- 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 +536 -86
- package/dist/hook-stop-collab.cjs.map +1 -1
- package/dist/hook-user-prompt.cjs +1047 -514
- 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 +508 -519
- 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,
|
|
@@ -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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
9934
|
-
code: "
|
|
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
|
|
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: "
|
|
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
|
-
//
|
|
10028
|
-
|
|
10029
|
-
|
|
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.
|
|
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.
|
|
10480
|
-
"@remixhq/mcp": "^0.1.
|
|
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
|
-
//
|
|
10587
|
-
//
|
|
10588
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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, {
|
|
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, {
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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,
|