@remixhq/core 0.1.17 → 0.1.19
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/dist/api.d.ts +34 -1
- package/dist/api.js +2 -2
- package/dist/auth.js +2 -2
- package/dist/binding.js +3 -3
- package/dist/chunk-7XJGOKEO.js +23 -0
- package/dist/{chunk-US5SM7ZC.js → chunk-C2FOZ3O7.js} +28 -5
- package/dist/{chunk-YCFLOHJV.js → chunk-DBVN42RF.js} +2 -2
- package/dist/{chunk-WT6VRLXU.js → chunk-S4ECO35X.js} +1 -1
- package/dist/{chunk-VM3CGCNX.js → chunk-XCZRNB35.js} +1 -1
- package/dist/{chunk-P6JHXOV4.js → chunk-XETDXVGM.js} +1 -1
- package/dist/collab.d.ts +27 -127
- package/dist/collab.js +314 -355
- package/dist/config.js +2 -2
- package/dist/{contracts-CHmD-fMj.d.ts → contracts-DiVLvPTG.d.ts} +33 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -1
- package/dist/history.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/dist/repo.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-YZ34ICNN.js +0 -17
package/dist/collab.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
writeCollabBinding,
|
|
9
9
|
writeCollabBindingSnapshot,
|
|
10
10
|
writeJsonAtomic
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-DBVN42RF.js";
|
|
12
12
|
import {
|
|
13
13
|
applyUnifiedDiffToWorktree,
|
|
14
14
|
assertRepoSnapshotUnchanged,
|
|
@@ -30,13 +30,13 @@ import {
|
|
|
30
30
|
requireCurrentBranch,
|
|
31
31
|
setRemoteOriginUrl,
|
|
32
32
|
summarizeUnifiedDiff
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-S4ECO35X.js";
|
|
34
34
|
import {
|
|
35
35
|
REMIX_ERROR_CODES
|
|
36
36
|
} from "./chunk-GC2MOT3U.js";
|
|
37
37
|
import {
|
|
38
38
|
RemixError
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-7XJGOKEO.js";
|
|
40
40
|
|
|
41
41
|
// src/application/collab/appDeltaCache.ts
|
|
42
42
|
var APP_DELTA_CACHE_TTL_MS = 5e3;
|
|
@@ -47,6 +47,8 @@ function buildAppDeltaCacheKey(appId, payload) {
|
|
|
47
47
|
appId,
|
|
48
48
|
payload.baseHeadHash,
|
|
49
49
|
payload.targetHeadHash ?? "",
|
|
50
|
+
payload.baseRevisionId ?? "",
|
|
51
|
+
payload.targetRevisionId ?? "",
|
|
50
52
|
payload.localSnapshotHash ?? "",
|
|
51
53
|
payload.repoFingerprint ?? "",
|
|
52
54
|
payload.remoteUrl ?? "",
|
|
@@ -189,9 +191,6 @@ function getAsyncJobDir(jobId) {
|
|
|
189
191
|
function getAsyncJobFilePath(jobId) {
|
|
190
192
|
return path.join(getAsyncJobDir(jobId), "job.json");
|
|
191
193
|
}
|
|
192
|
-
function getAsyncJobBundlePath(jobId) {
|
|
193
|
-
return path.join(getAsyncJobDir(jobId), "bundle.bundle");
|
|
194
|
-
}
|
|
195
194
|
function getLogsRoot() {
|
|
196
195
|
return path.join(getCollabStateRoot(), "logs");
|
|
197
196
|
}
|
|
@@ -493,11 +492,11 @@ async function readLocalBaseline(params) {
|
|
|
493
492
|
const raw = await fs2.readFile(getBaselinePath(params), "utf8");
|
|
494
493
|
const parsed = JSON.parse(raw);
|
|
495
494
|
if (!parsed || typeof parsed !== "object") return null;
|
|
496
|
-
if (parsed.schemaVersion
|
|
495
|
+
if (![1, 2].includes(Number(parsed.schemaVersion)) || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
|
|
497
496
|
return null;
|
|
498
497
|
}
|
|
499
498
|
return {
|
|
500
|
-
schemaVersion: 1,
|
|
499
|
+
schemaVersion: Number(parsed.schemaVersion) === 2 ? 2 : 1,
|
|
501
500
|
key: parsed.key,
|
|
502
501
|
repoRoot: parsed.repoRoot,
|
|
503
502
|
repoFingerprint: parsed.repoFingerprint ?? null,
|
|
@@ -506,6 +505,8 @@ async function readLocalBaseline(params) {
|
|
|
506
505
|
branchName: parsed.branchName ?? null,
|
|
507
506
|
lastSnapshotId: parsed.lastSnapshotId ?? null,
|
|
508
507
|
lastSnapshotHash: parsed.lastSnapshotHash ?? null,
|
|
508
|
+
lastServerRevisionId: parsed.lastServerRevisionId ?? null,
|
|
509
|
+
lastServerTreeHash: parsed.lastServerTreeHash ?? null,
|
|
509
510
|
lastServerHeadHash: parsed.lastServerHeadHash ?? null,
|
|
510
511
|
lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
|
|
511
512
|
updatedAt: String(parsed.updatedAt ?? "")
|
|
@@ -517,7 +518,7 @@ async function readLocalBaseline(params) {
|
|
|
517
518
|
async function writeLocalBaseline(baseline) {
|
|
518
519
|
const key = buildLaneStateKey(baseline);
|
|
519
520
|
const normalized = {
|
|
520
|
-
schemaVersion:
|
|
521
|
+
schemaVersion: 2,
|
|
521
522
|
key,
|
|
522
523
|
repoRoot: baseline.repoRoot,
|
|
523
524
|
repoFingerprint: baseline.repoFingerprint ?? null,
|
|
@@ -526,6 +527,8 @@ async function writeLocalBaseline(baseline) {
|
|
|
526
527
|
branchName: baseline.branchName ?? null,
|
|
527
528
|
lastSnapshotId: baseline.lastSnapshotId ?? null,
|
|
528
529
|
lastSnapshotHash: baseline.lastSnapshotHash ?? null,
|
|
530
|
+
lastServerRevisionId: baseline.lastServerRevisionId ?? null,
|
|
531
|
+
lastServerTreeHash: baseline.lastServerTreeHash ?? null,
|
|
529
532
|
lastServerHeadHash: baseline.lastServerHeadHash ?? null,
|
|
530
533
|
lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash ?? null,
|
|
531
534
|
updatedAt: baseline.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -912,6 +915,7 @@ function normalizeJob2(input) {
|
|
|
912
915
|
prompt: input.prompt,
|
|
913
916
|
assistantResponse: input.assistantResponse,
|
|
914
917
|
baselineSnapshotId: input.baselineSnapshotId ?? null,
|
|
918
|
+
baselineServerRevisionId: input.baselineServerRevisionId ?? null,
|
|
915
919
|
baselineServerHeadHash: input.baselineServerHeadHash ?? null,
|
|
916
920
|
currentSnapshotId: input.currentSnapshotId,
|
|
917
921
|
capturedAt: input.capturedAt ?? now,
|
|
@@ -946,6 +950,7 @@ async function readPendingFinalizeJob(jobId) {
|
|
|
946
950
|
prompt: String(parsed.prompt ?? ""),
|
|
947
951
|
assistantResponse: String(parsed.assistantResponse ?? ""),
|
|
948
952
|
baselineSnapshotId: parsed.baselineSnapshotId ?? null,
|
|
953
|
+
baselineServerRevisionId: parsed.baselineServerRevisionId ?? null,
|
|
949
954
|
baselineServerHeadHash: parsed.baselineServerHeadHash ?? null,
|
|
950
955
|
currentSnapshotId: String(parsed.currentSnapshotId ?? ""),
|
|
951
956
|
capturedAt: parsed.capturedAt,
|
|
@@ -1462,6 +1467,15 @@ function shouldRequireRemoteLaneForCurrentBranch(params) {
|
|
|
1462
1467
|
if (params.currentBranch === defaultBranch) return false;
|
|
1463
1468
|
return !params.binding.laneId || params.binding.currentAppId === params.binding.upstreamAppId;
|
|
1464
1469
|
}
|
|
1470
|
+
function resolveLaneLookupProjectId(params) {
|
|
1471
|
+
const currentBranch = normalizeBranchName(params.currentBranch);
|
|
1472
|
+
const defaultBranch = normalizeBranchName(params.defaultBranch);
|
|
1473
|
+
const localProjectId = params.localBinding.projectId ?? null;
|
|
1474
|
+
if (currentBranch && currentBranch !== defaultBranch && localProjectId) {
|
|
1475
|
+
return localProjectId;
|
|
1476
|
+
}
|
|
1477
|
+
return params.explicitRootProjectId ?? (params.requireRemoteLane ? void 0 : localProjectId ?? params.fallbackProjectId ?? void 0);
|
|
1478
|
+
}
|
|
1465
1479
|
async function persistResolvedLane(repoRoot, binding) {
|
|
1466
1480
|
await writeCollabBinding(repoRoot, {
|
|
1467
1481
|
projectId: binding.projectId,
|
|
@@ -1540,7 +1554,14 @@ async function resolveActiveLaneBindingUncached(params, state) {
|
|
|
1540
1554
|
};
|
|
1541
1555
|
}
|
|
1542
1556
|
const laneResp2 = await params.api.resolveProjectLaneBinding({
|
|
1543
|
-
projectId:
|
|
1557
|
+
projectId: resolveLaneLookupProjectId({
|
|
1558
|
+
explicitRootProjectId: state.explicitRootBinding?.projectId,
|
|
1559
|
+
localBinding,
|
|
1560
|
+
currentBranch,
|
|
1561
|
+
defaultBranch: state.defaultBranch,
|
|
1562
|
+
requireRemoteLane,
|
|
1563
|
+
fallbackProjectId: state.projectId
|
|
1564
|
+
}),
|
|
1544
1565
|
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
1545
1566
|
remoteUrl: state.remoteUrl ?? void 0,
|
|
1546
1567
|
defaultBranch: state.defaultBranch ?? void 0,
|
|
@@ -1701,6 +1722,8 @@ function buildBaseState() {
|
|
|
1701
1722
|
branchName: null,
|
|
1702
1723
|
localCommitHash: null,
|
|
1703
1724
|
currentSnapshotHash: null,
|
|
1725
|
+
currentServerRevisionId: null,
|
|
1726
|
+
currentServerTreeHash: null,
|
|
1704
1727
|
currentServerHeadHash: null,
|
|
1705
1728
|
currentServerHeadCommitId: null,
|
|
1706
1729
|
worktreeClean: false,
|
|
@@ -1734,6 +1757,8 @@ function buildBaseState() {
|
|
|
1734
1757
|
baseline: {
|
|
1735
1758
|
lastSnapshotId: null,
|
|
1736
1759
|
lastSnapshotHash: null,
|
|
1760
|
+
lastServerRevisionId: null,
|
|
1761
|
+
lastServerTreeHash: null,
|
|
1737
1762
|
lastServerHeadHash: null,
|
|
1738
1763
|
lastSeenLocalCommitHash: null
|
|
1739
1764
|
}
|
|
@@ -1860,6 +1885,8 @@ async function collabDetectRepoState(params) {
|
|
|
1860
1885
|
summarizeAsyncJobs({ repoRoot, branchName: binding.branchName ?? null })
|
|
1861
1886
|
]);
|
|
1862
1887
|
const appHead = unwrapResponseObject(headResp, "app head");
|
|
1888
|
+
detected.currentServerRevisionId = appHead.headRevisionId ?? null;
|
|
1889
|
+
detected.currentServerTreeHash = appHead.treeHash ?? null;
|
|
1863
1890
|
detected.currentServerHeadHash = appHead.headCommitHash;
|
|
1864
1891
|
detected.currentServerHeadCommitId = appHead.headCommitId;
|
|
1865
1892
|
detected.currentSnapshotHash = inspection.snapshotHash;
|
|
@@ -1868,6 +1895,8 @@ async function collabDetectRepoState(params) {
|
|
|
1868
1895
|
detected.baseline = {
|
|
1869
1896
|
lastSnapshotId: baseline?.lastSnapshotId ?? null,
|
|
1870
1897
|
lastSnapshotHash: baseline?.lastSnapshotHash ?? null,
|
|
1898
|
+
lastServerRevisionId: baseline?.lastServerRevisionId ?? null,
|
|
1899
|
+
lastServerTreeHash: baseline?.lastServerTreeHash ?? null,
|
|
1871
1900
|
lastServerHeadHash: baseline?.lastServerHeadHash ?? null,
|
|
1872
1901
|
lastSeenLocalCommitHash: baseline?.lastSeenLocalCommitHash ?? null
|
|
1873
1902
|
};
|
|
@@ -1877,6 +1906,7 @@ async function collabDetectRepoState(params) {
|
|
|
1877
1906
|
const bootstrapResp = await params.api.getAppDelta(binding.currentAppId, {
|
|
1878
1907
|
baseHeadHash: localCommitHash,
|
|
1879
1908
|
targetHeadHash: appHead.headCommitHash,
|
|
1909
|
+
targetRevisionId: appHead.headRevisionId,
|
|
1880
1910
|
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
1881
1911
|
remoteUrl: binding.remoteUrl ?? void 0,
|
|
1882
1912
|
defaultBranch: binding.defaultBranch ?? void 0
|
|
@@ -1899,7 +1929,7 @@ async function collabDetectRepoState(params) {
|
|
|
1899
1929
|
}
|
|
1900
1930
|
}
|
|
1901
1931
|
detected.repoState = "external_local_base_changed";
|
|
1902
|
-
detected.hint = "No local Remix baseline exists for this lane yet. Run `remix collab
|
|
1932
|
+
detected.hint = "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
|
|
1903
1933
|
return detected;
|
|
1904
1934
|
}
|
|
1905
1935
|
const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
|
|
@@ -1918,7 +1948,30 @@ async function collabDetectRepoState(params) {
|
|
|
1918
1948
|
return detected;
|
|
1919
1949
|
}
|
|
1920
1950
|
const localChanged = inspection.snapshotHash !== baseline.lastSnapshotHash;
|
|
1921
|
-
const
|
|
1951
|
+
const serverHeadChanged = appHead.headCommitHash !== baseline.lastServerHeadHash;
|
|
1952
|
+
const revisionChanged = Boolean(
|
|
1953
|
+
baseline.lastServerRevisionId && (appHead.headRevisionId ?? null) !== baseline.lastServerRevisionId
|
|
1954
|
+
);
|
|
1955
|
+
const equivalentRevisionDrift = revisionChanged && !serverHeadChanged;
|
|
1956
|
+
if (equivalentRevisionDrift) {
|
|
1957
|
+
await writeLocalBaseline({
|
|
1958
|
+
repoRoot,
|
|
1959
|
+
repoFingerprint: binding.repoFingerprint,
|
|
1960
|
+
laneId: binding.laneId,
|
|
1961
|
+
currentAppId: binding.currentAppId,
|
|
1962
|
+
branchName: binding.branchName,
|
|
1963
|
+
lastSnapshotId: baseline.lastSnapshotId,
|
|
1964
|
+
lastSnapshotHash: baseline.lastSnapshotHash,
|
|
1965
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
1966
|
+
lastServerTreeHash: appHead.treeHash ?? baseline.lastServerTreeHash ?? null,
|
|
1967
|
+
lastServerHeadHash: appHead.headCommitHash,
|
|
1968
|
+
lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash
|
|
1969
|
+
});
|
|
1970
|
+
detected.baseline.lastServerRevisionId = appHead.headRevisionId ?? null;
|
|
1971
|
+
detected.baseline.lastServerTreeHash = appHead.treeHash ?? baseline.lastServerTreeHash ?? null;
|
|
1972
|
+
detected.baseline.lastServerHeadHash = appHead.headCommitHash;
|
|
1973
|
+
}
|
|
1974
|
+
const serverChanged = serverHeadChanged;
|
|
1922
1975
|
if (!localChanged && !serverChanged) {
|
|
1923
1976
|
detected.repoState = "idle";
|
|
1924
1977
|
return detected;
|
|
@@ -2347,6 +2400,7 @@ function buildWorkspaceMetadata(params) {
|
|
|
2347
2400
|
recordingMode: "boundary_delta",
|
|
2348
2401
|
baselineSnapshotId: params.baselineSnapshotId,
|
|
2349
2402
|
currentSnapshotId: params.currentSnapshotId,
|
|
2403
|
+
baselineServerRevisionId: params.baselineServerRevisionId ?? null,
|
|
2350
2404
|
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
2351
2405
|
currentSnapshotHash: params.currentSnapshotHash,
|
|
2352
2406
|
localCommitHash: params.localCommitHash,
|
|
@@ -2365,6 +2419,59 @@ function buildWorkspaceMetadata(params) {
|
|
|
2365
2419
|
}
|
|
2366
2420
|
return metadata;
|
|
2367
2421
|
}
|
|
2422
|
+
async function findExistingChangeStepByIdempotency(params) {
|
|
2423
|
+
const idempotencyKey = params.idempotencyKey?.trim();
|
|
2424
|
+
if (!idempotencyKey) return null;
|
|
2425
|
+
const resp = await params.api.listChangeSteps(params.appId, { limit: 1, idempotencyKey });
|
|
2426
|
+
const responseObject = unwrapResponseObject(
|
|
2427
|
+
resp,
|
|
2428
|
+
"change step list"
|
|
2429
|
+
);
|
|
2430
|
+
const steps = Array.isArray(responseObject) ? responseObject : Array.isArray(responseObject.items) ? responseObject.items : [];
|
|
2431
|
+
return steps.find((step) => step.idempotencyKey === idempotencyKey) ?? null;
|
|
2432
|
+
}
|
|
2433
|
+
async function writeBaselineFromSucceededChangeStep(params) {
|
|
2434
|
+
const nextServerHeadHash = typeof params.changeStep.headCommitHash === "string" ? params.changeStep.headCommitHash.trim() : "";
|
|
2435
|
+
if (!nextServerHeadHash) {
|
|
2436
|
+
throw buildFinalizeCliError({
|
|
2437
|
+
message: "Backend returned a succeeded change step without a head commit hash.",
|
|
2438
|
+
exitCode: 1,
|
|
2439
|
+
hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again.",
|
|
2440
|
+
disposition: "terminal",
|
|
2441
|
+
reason: "missing_head_commit_hash"
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
let nextServerRevisionId = typeof params.changeStep.resultRevisionId === "string" ? params.changeStep.resultRevisionId.trim() : "";
|
|
2445
|
+
let nextServerTreeHash = null;
|
|
2446
|
+
if (!nextServerRevisionId) {
|
|
2447
|
+
const freshHeadResp = await params.api.getAppHead(params.job.currentAppId);
|
|
2448
|
+
const freshHead = unwrapResponseObject(freshHeadResp, "app head");
|
|
2449
|
+
if (freshHead.headCommitHash !== nextServerHeadHash || !freshHead.headRevisionId) {
|
|
2450
|
+
throw buildFinalizeCliError({
|
|
2451
|
+
message: "Backend returned a succeeded change step without a matching result revision.",
|
|
2452
|
+
exitCode: 1,
|
|
2453
|
+
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`.",
|
|
2454
|
+
disposition: "terminal",
|
|
2455
|
+
reason: "missing_result_revision_id"
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
nextServerRevisionId = freshHead.headRevisionId;
|
|
2459
|
+
nextServerTreeHash = freshHead.treeHash ?? null;
|
|
2460
|
+
}
|
|
2461
|
+
await writeLocalBaseline({
|
|
2462
|
+
repoRoot: params.job.repoRoot,
|
|
2463
|
+
repoFingerprint: params.job.repoFingerprint,
|
|
2464
|
+
laneId: params.job.laneId,
|
|
2465
|
+
currentAppId: params.job.currentAppId,
|
|
2466
|
+
branchName: params.job.branchName,
|
|
2467
|
+
lastSnapshotId: params.snapshot.id,
|
|
2468
|
+
lastSnapshotHash: params.snapshot.snapshotHash,
|
|
2469
|
+
lastServerRevisionId: nextServerRevisionId,
|
|
2470
|
+
lastServerTreeHash: nextServerTreeHash,
|
|
2471
|
+
lastServerHeadHash: nextServerHeadHash,
|
|
2472
|
+
lastSeenLocalCommitHash: params.snapshot.localCommitHash
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2368
2475
|
async function harvestPreTurnEvents(repoRoot, fromCommit, toCommit) {
|
|
2369
2476
|
if (!toCommit) return null;
|
|
2370
2477
|
try {
|
|
@@ -2425,12 +2532,12 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2425
2532
|
throw buildFinalizeCliError({
|
|
2426
2533
|
message: "Local baseline is missing for this queued finalize job.",
|
|
2427
2534
|
exitCode: 2,
|
|
2428
|
-
hint: "Run `remix collab
|
|
2535
|
+
hint: "Run `remix collab init` to seed this checkout's revision baseline.",
|
|
2429
2536
|
disposition: "terminal",
|
|
2430
2537
|
reason: "baseline_missing"
|
|
2431
2538
|
});
|
|
2432
2539
|
}
|
|
2433
|
-
const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
|
|
2540
|
+
const baselineDrifted = baseline.lastSnapshotId !== job.baselineSnapshotId || (job.baselineServerRevisionId ? baseline.lastServerRevisionId !== job.baselineServerRevisionId : false) || baseline.lastServerHeadHash !== job.baselineServerHeadHash;
|
|
2434
2541
|
const appHead = unwrapResponseObject(appHeadResp, "app head");
|
|
2435
2542
|
const remoteUrl = readMetadataString(job, "remoteUrl");
|
|
2436
2543
|
const defaultBranch = readMetadataString(job, "defaultBranch");
|
|
@@ -2453,12 +2560,13 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2453
2560
|
throw buildFinalizeCliError({
|
|
2454
2561
|
message: "Finalize queue baseline drifted before this job was processed.",
|
|
2455
2562
|
exitCode: 1,
|
|
2456
|
-
hint: "Process queued finalize jobs in capture order, or
|
|
2563
|
+
hint: "Process queued finalize jobs in capture order, or run `remix collab init` to refresh the revision baseline before retrying.",
|
|
2457
2564
|
disposition: "terminal",
|
|
2458
2565
|
reason: "baseline_drifted"
|
|
2459
2566
|
});
|
|
2460
2567
|
}
|
|
2461
|
-
|
|
2568
|
+
const serverStillAtBaseline = job.baselineServerRevisionId ? appHead.headRevisionId === job.baselineServerRevisionId : appHead.headCommitHash === job.baselineServerHeadHash;
|
|
2569
|
+
if (!serverStillAtBaseline) {
|
|
2462
2570
|
throw buildFinalizeCliError({
|
|
2463
2571
|
message: "Server lane changed before a no-diff turn could be recorded.",
|
|
2464
2572
|
exitCode: 2,
|
|
@@ -2480,6 +2588,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2480
2588
|
defaultBranch,
|
|
2481
2589
|
baselineSnapshotId: job.baselineSnapshotId,
|
|
2482
2590
|
currentSnapshotId: job.currentSnapshotId,
|
|
2591
|
+
baselineServerRevisionId: job.baselineServerRevisionId,
|
|
2483
2592
|
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
2484
2593
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
2485
2594
|
localCommitHash: snapshot.localCommitHash,
|
|
@@ -2500,6 +2609,8 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2500
2609
|
branchName: job.branchName,
|
|
2501
2610
|
lastSnapshotId: snapshot.id,
|
|
2502
2611
|
lastSnapshotHash: snapshot.snapshotHash,
|
|
2612
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
2613
|
+
lastServerTreeHash: appHead.treeHash ?? null,
|
|
2503
2614
|
lastServerHeadHash: appHead.headCommitHash,
|
|
2504
2615
|
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
2505
2616
|
});
|
|
@@ -2520,14 +2631,14 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2520
2631
|
};
|
|
2521
2632
|
}
|
|
2522
2633
|
const localBaselineAdvanced = baseline.lastSnapshotId !== job.baselineSnapshotId;
|
|
2523
|
-
const serverHeadAdvanced = appHead.headCommitHash !== job.baselineServerHeadHash;
|
|
2634
|
+
const serverHeadAdvanced = job.baselineServerRevisionId ? appHead.headRevisionId !== job.baselineServerRevisionId : appHead.headCommitHash !== job.baselineServerHeadHash;
|
|
2524
2635
|
if (baselineDrifted) {
|
|
2525
2636
|
const consistentAdvance = localBaselineAdvanced && serverHeadAdvanced;
|
|
2526
2637
|
if (!consistentAdvance) {
|
|
2527
2638
|
throw buildFinalizeCliError({
|
|
2528
2639
|
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.`,
|
|
2529
2640
|
exitCode: 1,
|
|
2530
|
-
hint: "Run `remix collab status` to inspect, then
|
|
2641
|
+
hint: "Run `remix collab status` to inspect, then sync or reconcile before retrying.",
|
|
2531
2642
|
disposition: "terminal",
|
|
2532
2643
|
reason: "baseline_drifted"
|
|
2533
2644
|
});
|
|
@@ -2535,6 +2646,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2535
2646
|
}
|
|
2536
2647
|
let submissionDiff = diffResult.diff;
|
|
2537
2648
|
let submissionBaseHeadHash = job.baselineServerHeadHash;
|
|
2649
|
+
let submissionBaseRevisionId = job.baselineServerRevisionId;
|
|
2538
2650
|
let replayedFromBaseHash = null;
|
|
2539
2651
|
if (!submissionBaseHeadHash) {
|
|
2540
2652
|
throw buildFinalizeCliError({
|
|
@@ -2545,6 +2657,34 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2545
2657
|
});
|
|
2546
2658
|
}
|
|
2547
2659
|
const replayNeeded = appHead.headCommitHash !== submissionBaseHeadHash || baselineDrifted;
|
|
2660
|
+
if (replayNeeded) {
|
|
2661
|
+
const existingChangeStep = await findExistingChangeStepByIdempotency({
|
|
2662
|
+
api: params.api,
|
|
2663
|
+
appId: job.currentAppId,
|
|
2664
|
+
idempotencyKey: job.idempotencyKey
|
|
2665
|
+
});
|
|
2666
|
+
if (existingChangeStep) {
|
|
2667
|
+
const changeStep2 = existingChangeStep.status === "succeeded" ? existingChangeStep : await pollChangeStep(params.api, job.currentAppId, existingChangeStep.id);
|
|
2668
|
+
invalidateAppHeadCache(job.currentAppId);
|
|
2669
|
+
invalidateAppDeltaCacheForApp(job.currentAppId);
|
|
2670
|
+
await writeBaselineFromSucceededChangeStep({ api: params.api, job, snapshot, changeStep: changeStep2 });
|
|
2671
|
+
await updatePendingFinalizeJob(job.id, {
|
|
2672
|
+
status: "completed",
|
|
2673
|
+
metadata: { changeStepId: String(changeStep2.id ?? "") }
|
|
2674
|
+
});
|
|
2675
|
+
return {
|
|
2676
|
+
mode: "changed_turn",
|
|
2677
|
+
idempotencyKey: job.idempotencyKey ?? "",
|
|
2678
|
+
queued: false,
|
|
2679
|
+
jobId: job.id,
|
|
2680
|
+
repoState,
|
|
2681
|
+
changeStep: changeStep2,
|
|
2682
|
+
collabTurn: null,
|
|
2683
|
+
autoSync: null,
|
|
2684
|
+
warnings: []
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2548
2688
|
if (replayNeeded) {
|
|
2549
2689
|
try {
|
|
2550
2690
|
const replayResp = await params.api.startChangeStepReplay(job.currentAppId, {
|
|
@@ -2552,7 +2692,9 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2552
2692
|
assistantResponse: job.assistantResponse,
|
|
2553
2693
|
diff: diffResult.diff,
|
|
2554
2694
|
baseCommitHash: submissionBaseHeadHash,
|
|
2695
|
+
baseRevisionId: job.baselineServerRevisionId,
|
|
2555
2696
|
targetHeadCommitHash: appHead.headCommitHash,
|
|
2697
|
+
targetRevisionId: appHead.headRevisionId,
|
|
2556
2698
|
expectedPaths: diffResult.changedPaths,
|
|
2557
2699
|
actor,
|
|
2558
2700
|
workspaceMetadata: buildWorkspaceMetadata({
|
|
@@ -2562,6 +2704,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2562
2704
|
defaultBranch,
|
|
2563
2705
|
baselineSnapshotId: job.baselineSnapshotId,
|
|
2564
2706
|
currentSnapshotId: job.currentSnapshotId,
|
|
2707
|
+
baselineServerRevisionId: job.baselineServerRevisionId,
|
|
2565
2708
|
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
2566
2709
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
2567
2710
|
localCommitHash: snapshot.localCommitHash,
|
|
@@ -2587,6 +2730,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2587
2730
|
submissionDiff = replayDiff.diff;
|
|
2588
2731
|
replayedFromBaseHash = submissionBaseHeadHash;
|
|
2589
2732
|
submissionBaseHeadHash = appHead.headCommitHash;
|
|
2733
|
+
submissionBaseRevisionId = appHead.headRevisionId;
|
|
2590
2734
|
} catch (error) {
|
|
2591
2735
|
if (error instanceof RemixError && error.finalizeDisposition === void 0) {
|
|
2592
2736
|
const detail = error.hint ? `${error.message} (${error.hint})` : error.message;
|
|
@@ -2608,6 +2752,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2608
2752
|
assistantResponse: job.assistantResponse,
|
|
2609
2753
|
diff: submissionDiff,
|
|
2610
2754
|
baseCommitHash: submissionBaseHeadHash,
|
|
2755
|
+
baseRevisionId: submissionBaseRevisionId,
|
|
2611
2756
|
headCommitHash: submissionBaseHeadHash,
|
|
2612
2757
|
changedFilesCount: diffResult.stats.changedFilesCount,
|
|
2613
2758
|
insertions: diffResult.stats.insertions,
|
|
@@ -2620,6 +2765,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2620
2765
|
defaultBranch,
|
|
2621
2766
|
baselineSnapshotId: job.baselineSnapshotId,
|
|
2622
2767
|
currentSnapshotId: job.currentSnapshotId,
|
|
2768
|
+
baselineServerRevisionId: job.baselineServerRevisionId,
|
|
2623
2769
|
baselineServerHeadHash: job.baselineServerHeadHash,
|
|
2624
2770
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
2625
2771
|
localCommitHash: snapshot.localCommitHash,
|
|
@@ -2636,27 +2782,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2636
2782
|
const changeStep = await pollChangeStep(params.api, job.currentAppId, String(createdStep.id));
|
|
2637
2783
|
invalidateAppHeadCache(job.currentAppId);
|
|
2638
2784
|
invalidateAppDeltaCacheForApp(job.currentAppId);
|
|
2639
|
-
|
|
2640
|
-
if (!nextServerHeadHash) {
|
|
2641
|
-
throw buildFinalizeCliError({
|
|
2642
|
-
message: "Backend returned a succeeded change step without a head commit hash.",
|
|
2643
|
-
exitCode: 1,
|
|
2644
|
-
hint: "This is a backend invariant violation; retry will not help. Re-anchor and try again.",
|
|
2645
|
-
disposition: "terminal",
|
|
2646
|
-
reason: "missing_head_commit_hash"
|
|
2647
|
-
});
|
|
2648
|
-
}
|
|
2649
|
-
await writeLocalBaseline({
|
|
2650
|
-
repoRoot: job.repoRoot,
|
|
2651
|
-
repoFingerprint: job.repoFingerprint,
|
|
2652
|
-
laneId: job.laneId,
|
|
2653
|
-
currentAppId: job.currentAppId,
|
|
2654
|
-
branchName: job.branchName,
|
|
2655
|
-
lastSnapshotId: snapshot.id,
|
|
2656
|
-
lastSnapshotHash: snapshot.snapshotHash,
|
|
2657
|
-
lastServerHeadHash: nextServerHeadHash,
|
|
2658
|
-
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
2659
|
-
});
|
|
2785
|
+
await writeBaselineFromSucceededChangeStep({ api: params.api, job, snapshot, changeStep });
|
|
2660
2786
|
await updatePendingFinalizeJob(job.id, {
|
|
2661
2787
|
status: "completed",
|
|
2662
2788
|
metadata: { changeStepId: String(changeStep.id ?? "") }
|
|
@@ -2702,6 +2828,7 @@ async function enqueueCapturedFinalizeTurn(params) {
|
|
|
2702
2828
|
prompt: params.prompt,
|
|
2703
2829
|
assistantResponse: params.assistantResponse,
|
|
2704
2830
|
baselineSnapshotId: params.baselineSnapshotId,
|
|
2831
|
+
baselineServerRevisionId: params.baselineServerRevisionId ?? null,
|
|
2705
2832
|
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
2706
2833
|
currentSnapshotId: params.currentSnapshotId,
|
|
2707
2834
|
idempotencyKey: params.idempotencyKey,
|
|
@@ -2802,17 +2929,6 @@ async function collabFinalizeTurn(params) {
|
|
|
2802
2929
|
});
|
|
2803
2930
|
}
|
|
2804
2931
|
}
|
|
2805
|
-
const pendingReAnchor = await findPendingAsyncJob({
|
|
2806
|
-
repoRoot,
|
|
2807
|
-
branchName: binding.branchName ?? null,
|
|
2808
|
-
kind: "re_anchor"
|
|
2809
|
-
});
|
|
2810
|
-
if (pendingReAnchor) {
|
|
2811
|
-
throw new RemixError("Cannot finalize a turn while a re-anchor is still processing.", {
|
|
2812
|
-
exitCode: 2,
|
|
2813
|
-
hint: `Re-anchor job ${pendingReAnchor.id} is still in the background queue. Run \`remix collab status\` to check progress.`
|
|
2814
|
-
});
|
|
2815
|
-
}
|
|
2816
2932
|
const detected = await collabDetectRepoState({
|
|
2817
2933
|
api: params.api,
|
|
2818
2934
|
cwd: repoRoot,
|
|
@@ -2853,9 +2969,16 @@ async function collabFinalizeTurn(params) {
|
|
|
2853
2969
|
hint: detected.hint
|
|
2854
2970
|
});
|
|
2855
2971
|
}
|
|
2972
|
+
if (detected.repoState === "both_changed") {
|
|
2973
|
+
throw new RemixError("Local and server changes must be reconciled before finalizing this turn.", {
|
|
2974
|
+
code: "reconcile_required",
|
|
2975
|
+
exitCode: 2,
|
|
2976
|
+
hint: detected.hint || "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying."
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2856
2979
|
if (detected.repoState === "external_local_base_changed") {
|
|
2857
|
-
throw new RemixError("The local checkout
|
|
2858
|
-
code: "
|
|
2980
|
+
throw new RemixError("The local checkout is missing a Remix revision baseline for this lane.", {
|
|
2981
|
+
code: "baseline_missing",
|
|
2859
2982
|
exitCode: 2,
|
|
2860
2983
|
hint: detected.hint
|
|
2861
2984
|
});
|
|
@@ -2867,8 +2990,9 @@ async function collabFinalizeTurn(params) {
|
|
|
2867
2990
|
});
|
|
2868
2991
|
if (!baseline) {
|
|
2869
2992
|
throw new RemixError("Local Remix baseline is missing for this lane.", {
|
|
2993
|
+
code: "baseline_missing",
|
|
2870
2994
|
exitCode: 2,
|
|
2871
|
-
hint: "Run `remix collab
|
|
2995
|
+
hint: "Run `remix collab init` or sync this lane to create a fresh revision baseline."
|
|
2872
2996
|
});
|
|
2873
2997
|
}
|
|
2874
2998
|
const snapshot = await captureLocalSnapshot({
|
|
@@ -2879,10 +3003,11 @@ async function collabFinalizeTurn(params) {
|
|
|
2879
3003
|
});
|
|
2880
3004
|
const mode = snapshot.snapshotHash === baseline.lastSnapshotHash ? "no_diff_turn" : "changed_turn";
|
|
2881
3005
|
const idempotencyKey = params.idempotencyKey?.trim() || buildDeterministicIdempotencyKey({
|
|
2882
|
-
kind: "
|
|
3006
|
+
kind: "collab_finalize_turn_boundary_v2",
|
|
2883
3007
|
appId: binding.currentAppId,
|
|
2884
3008
|
laneId: binding.laneId,
|
|
2885
3009
|
baselineSnapshotId: baseline.lastSnapshotId,
|
|
3010
|
+
baselineServerRevisionId: baseline.lastServerRevisionId,
|
|
2886
3011
|
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
2887
3012
|
currentSnapshotId: snapshot.id,
|
|
2888
3013
|
currentSnapshotHash: snapshot.snapshotHash,
|
|
@@ -2902,6 +3027,7 @@ async function collabFinalizeTurn(params) {
|
|
|
2902
3027
|
prompt,
|
|
2903
3028
|
assistantResponse,
|
|
2904
3029
|
baselineSnapshotId: baseline.lastSnapshotId,
|
|
3030
|
+
baselineServerRevisionId: baseline.lastServerRevisionId,
|
|
2905
3031
|
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
2906
3032
|
currentSnapshotId: snapshot.id,
|
|
2907
3033
|
idempotencyKey,
|
|
@@ -2950,9 +3076,10 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
|
|
|
2950
3076
|
// Server has commits we don't. Fix: `remix collab sync` (safe to
|
|
2951
3077
|
// auto-run for fast-forward; non-FF refused by the command itself).
|
|
2952
3078
|
"pull_required",
|
|
2953
|
-
//
|
|
2954
|
-
|
|
2955
|
-
|
|
3079
|
+
// Both local and server changed. Fix: inspect and apply reconcile.
|
|
3080
|
+
"reconcile_required",
|
|
3081
|
+
// Local revision baseline is missing. Fix: `remix collab init` or sync.
|
|
3082
|
+
"baseline_missing"
|
|
2956
3083
|
];
|
|
2957
3084
|
var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
|
|
2958
3085
|
function isFinalizePreflightFailureCode(value) {
|
|
@@ -3011,7 +3138,7 @@ async function collabRecordingPreflight(params) {
|
|
|
3011
3138
|
if (detected.status === "branch_mismatch") return { status: "branch_mismatch", ...base };
|
|
3012
3139
|
if (detected.repoState === "server_only_changed") return { status: "pull_required", ...base };
|
|
3013
3140
|
if (detected.repoState === "both_changed") return { status: "reconcile_required", ...base };
|
|
3014
|
-
if (detected.repoState === "external_local_base_changed") return { status: "
|
|
3141
|
+
if (detected.repoState === "external_local_base_changed") return { status: "baseline_missing", ...base };
|
|
3015
3142
|
return { status: "ready", ...base };
|
|
3016
3143
|
}
|
|
3017
3144
|
|
|
@@ -3238,7 +3365,7 @@ async function ensureWorkspaceMatchesBaseline(params) {
|
|
|
3238
3365
|
if (!baseline?.lastSnapshotHash || !baseline.lastServerHeadHash) {
|
|
3239
3366
|
throw new RemixError("Local Remix baseline is missing for this lane.", {
|
|
3240
3367
|
exitCode: 2,
|
|
3241
|
-
hint: "Run `remix collab
|
|
3368
|
+
hint: "Run `remix collab init` or sync from a checkout with a valid revision baseline before applying server changes."
|
|
3242
3369
|
});
|
|
3243
3370
|
}
|
|
3244
3371
|
const inspection = await inspectLocalSnapshot({
|
|
@@ -3312,11 +3439,12 @@ async function collabSync(params) {
|
|
|
3312
3439
|
const repoSnapshot = await captureRepoSnapshot(repoRoot, { includeWorkspaceDiffHash: true });
|
|
3313
3440
|
const bootstrapFromLocalHead = !detected.baseline.lastSnapshotHash || !detected.baseline.lastServerHeadHash;
|
|
3314
3441
|
let baselineServerHeadHash;
|
|
3442
|
+
let baselineServerRevisionId = null;
|
|
3315
3443
|
if (bootstrapFromLocalHead) {
|
|
3316
3444
|
if (!headCommitHash) {
|
|
3317
3445
|
throw new RemixError("Failed to resolve local HEAD commit for the initial sync bootstrap.", {
|
|
3318
3446
|
exitCode: 1,
|
|
3319
|
-
hint: "Retry after Git HEAD is available, or run `remix collab
|
|
3447
|
+
hint: "Retry after Git HEAD is available, or run `remix collab init` to seed this checkout's revision baseline."
|
|
3320
3448
|
});
|
|
3321
3449
|
}
|
|
3322
3450
|
baselineServerHeadHash = headCommitHash;
|
|
@@ -3331,13 +3459,15 @@ async function collabSync(params) {
|
|
|
3331
3459
|
if (!baseline.lastServerHeadHash) {
|
|
3332
3460
|
throw new RemixError("Local Remix baseline is missing the last acknowledged server head.", {
|
|
3333
3461
|
exitCode: 2,
|
|
3334
|
-
hint: "Run `remix collab
|
|
3462
|
+
hint: "Run `remix collab init` or sync from a checkout with a valid revision baseline before pulling server changes."
|
|
3335
3463
|
});
|
|
3336
3464
|
}
|
|
3337
3465
|
baselineServerHeadHash = baseline.lastServerHeadHash;
|
|
3466
|
+
baselineServerRevisionId = baseline.lastServerRevisionId;
|
|
3338
3467
|
}
|
|
3339
3468
|
const deltaResp = await params.api.getAppDelta(binding.currentAppId, {
|
|
3340
3469
|
baseHeadHash: baselineServerHeadHash,
|
|
3470
|
+
baseRevisionId: baselineServerRevisionId,
|
|
3341
3471
|
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
3342
3472
|
remoteUrl: binding.remoteUrl ?? void 0,
|
|
3343
3473
|
defaultBranch: binding.defaultBranch ?? void 0
|
|
@@ -3361,13 +3491,54 @@ async function collabSync(params) {
|
|
|
3361
3491
|
applied: false,
|
|
3362
3492
|
dryRun: params.dryRun
|
|
3363
3493
|
};
|
|
3364
|
-
if (params.dryRun
|
|
3494
|
+
if (params.dryRun) {
|
|
3365
3495
|
return previewResult;
|
|
3366
3496
|
}
|
|
3497
|
+
if (delta.status === "up_to_date") {
|
|
3498
|
+
if (!bootstrapFromLocalHead) {
|
|
3499
|
+
return previewResult;
|
|
3500
|
+
}
|
|
3501
|
+
return withRepoMutationLock(
|
|
3502
|
+
{
|
|
3503
|
+
cwd: repoRoot,
|
|
3504
|
+
operation: "collabSync"
|
|
3505
|
+
},
|
|
3506
|
+
async ({ repoRoot: lockedRepoRoot, warnings }) => {
|
|
3507
|
+
await assertRepoSnapshotUnchanged(lockedRepoRoot, repoSnapshot, {
|
|
3508
|
+
operation: "`remix collab sync`",
|
|
3509
|
+
recoveryHint: "The repository changed before the first local Remix baseline could be created. Review the local changes and rerun `remix collab sync`."
|
|
3510
|
+
});
|
|
3511
|
+
const snapshot = await captureLocalSnapshot({
|
|
3512
|
+
repoRoot: lockedRepoRoot,
|
|
3513
|
+
repoFingerprint: binding.repoFingerprint,
|
|
3514
|
+
laneId: binding.laneId,
|
|
3515
|
+
branchName: binding.branchName
|
|
3516
|
+
});
|
|
3517
|
+
await writeLocalBaseline({
|
|
3518
|
+
repoRoot: lockedRepoRoot,
|
|
3519
|
+
repoFingerprint: binding.repoFingerprint,
|
|
3520
|
+
laneId: binding.laneId,
|
|
3521
|
+
currentAppId: binding.currentAppId,
|
|
3522
|
+
branchName: binding.branchName,
|
|
3523
|
+
lastSnapshotId: snapshot.id,
|
|
3524
|
+
lastSnapshotHash: snapshot.snapshotHash,
|
|
3525
|
+
lastServerRevisionId: delta.targetRevisionId ?? null,
|
|
3526
|
+
lastServerTreeHash: delta.targetTreeHash ?? null,
|
|
3527
|
+
lastServerHeadHash: delta.targetHeadHash,
|
|
3528
|
+
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
3529
|
+
});
|
|
3530
|
+
return {
|
|
3531
|
+
...previewResult,
|
|
3532
|
+
localCommitHash: snapshot.localCommitHash,
|
|
3533
|
+
...warnings.length > 0 ? { warnings } : {}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3367
3538
|
if (delta.status === "base_unknown") {
|
|
3368
3539
|
throw new RemixError("Direct pull is unavailable because Remix can no longer diff from the last acknowledged server head.", {
|
|
3369
3540
|
exitCode: 2,
|
|
3370
|
-
hint: "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying. If this checkout has no local Remix baseline yet for this lane, `remix collab
|
|
3541
|
+
hint: "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying. If this checkout has no local Remix baseline yet for this lane, run `remix collab init` to seed one."
|
|
3371
3542
|
});
|
|
3372
3543
|
}
|
|
3373
3544
|
if (delta.status !== "delta_ready") {
|
|
@@ -3411,6 +3582,8 @@ async function collabSync(params) {
|
|
|
3411
3582
|
branchName: binding.branchName,
|
|
3412
3583
|
lastSnapshotId: snapshot.id,
|
|
3413
3584
|
lastSnapshotHash: snapshot.snapshotHash,
|
|
3585
|
+
lastServerRevisionId: delta.targetRevisionId ?? null,
|
|
3586
|
+
lastServerTreeHash: delta.targetTreeHash ?? null,
|
|
3414
3587
|
lastServerHeadHash: delta.targetHeadHash,
|
|
3415
3588
|
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
3416
3589
|
});
|
|
@@ -3705,6 +3878,8 @@ async function collabCheckout(params) {
|
|
|
3705
3878
|
branchName: branchNameForBaseline,
|
|
3706
3879
|
lastSnapshotId: snapshot.id,
|
|
3707
3880
|
lastSnapshotHash: snapshot.snapshotHash,
|
|
3881
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
3882
|
+
lastServerTreeHash: appHead.treeHash ?? null,
|
|
3708
3883
|
lastServerHeadHash: appHead.headCommitHash,
|
|
3709
3884
|
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
3710
3885
|
});
|
|
@@ -4090,25 +4265,21 @@ async function trySeedEquivalentBranchBaseline(params) {
|
|
|
4090
4265
|
branchName: params.branchName,
|
|
4091
4266
|
persistBlobs: false
|
|
4092
4267
|
});
|
|
4093
|
-
if (inspection.snapshotHash !== defaultBaseline.lastSnapshotHash) {
|
|
4268
|
+
if (inspection.snapshotHash !== defaultBaseline.lastSnapshotHash && inspection.localCommitHash !== defaultBaseline.lastSeenLocalCommitHash) {
|
|
4094
4269
|
return null;
|
|
4095
4270
|
}
|
|
4096
|
-
const snapshot = await captureLocalSnapshot({
|
|
4097
|
-
repoRoot: params.repoRoot,
|
|
4098
|
-
repoFingerprint: params.repoFingerprint,
|
|
4099
|
-
laneId: params.laneId,
|
|
4100
|
-
branchName: params.branchName
|
|
4101
|
-
});
|
|
4102
4271
|
await writeLocalBaseline({
|
|
4103
4272
|
repoRoot: params.repoRoot,
|
|
4104
4273
|
repoFingerprint: params.repoFingerprint,
|
|
4105
4274
|
laneId: params.laneId,
|
|
4106
4275
|
currentAppId: params.currentAppId,
|
|
4107
4276
|
branchName: params.branchName,
|
|
4108
|
-
lastSnapshotId:
|
|
4109
|
-
lastSnapshotHash:
|
|
4277
|
+
lastSnapshotId: defaultBaseline.lastSnapshotId,
|
|
4278
|
+
lastSnapshotHash: defaultBaseline.lastSnapshotHash,
|
|
4279
|
+
lastServerRevisionId: params.appHeadRevisionId,
|
|
4280
|
+
lastServerTreeHash: params.appTreeHash,
|
|
4110
4281
|
lastServerHeadHash: params.appHeadHash,
|
|
4111
|
-
lastSeenLocalCommitHash:
|
|
4282
|
+
lastSeenLocalCommitHash: inspection.localCommitHash
|
|
4112
4283
|
});
|
|
4113
4284
|
return "seeded";
|
|
4114
4285
|
}
|
|
@@ -4118,11 +4289,11 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4118
4289
|
laneId: params.laneId,
|
|
4119
4290
|
repoRoot: params.repoRoot
|
|
4120
4291
|
});
|
|
4121
|
-
if (baseline?.lastSnapshotHash && baseline.lastServerHeadHash) {
|
|
4292
|
+
if (baseline?.lastSnapshotHash && (baseline.lastServerRevisionId || baseline.lastServerHeadHash)) {
|
|
4122
4293
|
return "existing";
|
|
4123
4294
|
}
|
|
4124
4295
|
const localHeadCommitHash = await getHeadCommitHash(params.repoRoot);
|
|
4125
|
-
if (!localHeadCommitHash) return "
|
|
4296
|
+
if (!localHeadCommitHash) return "baseline_missing";
|
|
4126
4297
|
const appHead = unwrapResponseObject(
|
|
4127
4298
|
await params.api.getAppHead(params.currentAppId),
|
|
4128
4299
|
"app head"
|
|
@@ -4149,6 +4320,7 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4149
4320
|
const deltaResp = await params.api.getAppDelta(params.currentAppId, {
|
|
4150
4321
|
baseHeadHash: localHeadCommitHash,
|
|
4151
4322
|
targetHeadHash: appHead.headCommitHash,
|
|
4323
|
+
targetRevisionId: appHead.headRevisionId,
|
|
4152
4324
|
localSnapshotHash,
|
|
4153
4325
|
repoFingerprint: params.repoFingerprint,
|
|
4154
4326
|
remoteUrl: params.remoteUrl ?? void 0,
|
|
@@ -4156,6 +4328,21 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4156
4328
|
});
|
|
4157
4329
|
const delta = unwrapResponseObject(deltaResp, "app delta");
|
|
4158
4330
|
if (delta.status === "up_to_date" || delta.status === "delta_ready") {
|
|
4331
|
+
const equivalentBaseline = await trySeedEquivalentBranchBaseline({
|
|
4332
|
+
repoRoot: params.repoRoot,
|
|
4333
|
+
repoFingerprint: params.repoFingerprint,
|
|
4334
|
+
laneId: params.laneId,
|
|
4335
|
+
currentAppId: params.currentAppId,
|
|
4336
|
+
upstreamAppId: params.upstreamAppId ?? null,
|
|
4337
|
+
branchName: params.branchName,
|
|
4338
|
+
defaultBranch: params.defaultBranch,
|
|
4339
|
+
appHeadHash: appHead.headCommitHash,
|
|
4340
|
+
appHeadRevisionId: appHead.headRevisionId ?? null,
|
|
4341
|
+
appTreeHash: appHead.treeHash ?? null
|
|
4342
|
+
});
|
|
4343
|
+
if (equivalentBaseline) {
|
|
4344
|
+
return equivalentBaseline;
|
|
4345
|
+
}
|
|
4159
4346
|
return "requires_sync";
|
|
4160
4347
|
}
|
|
4161
4348
|
if (delta.status === "content_equivalent") {
|
|
@@ -4178,7 +4365,9 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4178
4365
|
upstreamAppId: params.upstreamAppId ?? null,
|
|
4179
4366
|
branchName: params.branchName,
|
|
4180
4367
|
defaultBranch: params.defaultBranch,
|
|
4181
|
-
appHeadHash: appHead.headCommitHash
|
|
4368
|
+
appHeadHash: appHead.headCommitHash,
|
|
4369
|
+
appHeadRevisionId: appHead.headRevisionId ?? null,
|
|
4370
|
+
appTreeHash: appHead.treeHash ?? null
|
|
4182
4371
|
});
|
|
4183
4372
|
if (equivalentBaseline) {
|
|
4184
4373
|
return equivalentBaseline;
|
|
@@ -4186,7 +4375,7 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4186
4375
|
}
|
|
4187
4376
|
} catch {
|
|
4188
4377
|
}
|
|
4189
|
-
return "
|
|
4378
|
+
return "baseline_missing";
|
|
4190
4379
|
}
|
|
4191
4380
|
async function seedImportedInitBaseline(params) {
|
|
4192
4381
|
const appHead = unwrapResponseObject(
|
|
@@ -4207,6 +4396,8 @@ async function seedImportedInitBaseline(params) {
|
|
|
4207
4396
|
branchName: params.branchName,
|
|
4208
4397
|
lastSnapshotId: snapshot.id,
|
|
4209
4398
|
lastSnapshotHash: snapshot.snapshotHash,
|
|
4399
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
4400
|
+
lastServerTreeHash: appHead.treeHash ?? null,
|
|
4210
4401
|
lastServerHeadHash: appHead.headCommitHash,
|
|
4211
4402
|
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
4212
4403
|
});
|
|
@@ -4220,7 +4411,6 @@ async function collabInit(params) {
|
|
|
4220
4411
|
},
|
|
4221
4412
|
async ({ repoRoot, warnings }) => {
|
|
4222
4413
|
await ensureGitInfoExcludeEntries(repoRoot, [".remix/"]);
|
|
4223
|
-
await ensureCleanWorktree(repoRoot, "`remix collab init`");
|
|
4224
4414
|
if (params.path?.trim()) {
|
|
4225
4415
|
throw new RemixError("`remix collab init --path` is not supported.", {
|
|
4226
4416
|
exitCode: 2,
|
|
@@ -4228,6 +4418,10 @@ async function collabInit(params) {
|
|
|
4228
4418
|
});
|
|
4229
4419
|
}
|
|
4230
4420
|
const localBindingState = await readCollabBindingState(repoRoot, { persist: true });
|
|
4421
|
+
const hasExistingBinding = localBindingState != null && Object.keys(localBindingState.branchBindings ?? {}).length > 0;
|
|
4422
|
+
if (params.forceNew || !hasExistingBinding) {
|
|
4423
|
+
await ensureCleanWorktree(repoRoot, "`remix collab init`");
|
|
4424
|
+
}
|
|
4231
4425
|
const persistedRemoteUrl = normalizeGitRemote(localBindingState?.remoteUrl ?? null);
|
|
4232
4426
|
const currentBranch = await getCurrentBranch(repoRoot);
|
|
4233
4427
|
const defaultBranch = localBindingState?.defaultBranch ?? await getDefaultBranch(repoRoot) ?? currentBranch;
|
|
@@ -5081,10 +5275,11 @@ async function collabList(params) {
|
|
|
5081
5275
|
};
|
|
5082
5276
|
}
|
|
5083
5277
|
|
|
5084
|
-
// src/application/collab/
|
|
5085
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
5278
|
+
// src/application/collab/collabReconcile.ts
|
|
5086
5279
|
import fs11 from "fs/promises";
|
|
5280
|
+
import os5 from "os";
|
|
5087
5281
|
import path10 from "path";
|
|
5282
|
+
import { execa as execa3 } from "execa";
|
|
5088
5283
|
|
|
5089
5284
|
// src/application/collab/pendingFinalize.ts
|
|
5090
5285
|
function hasPendingFinalize(summary) {
|
|
@@ -5094,258 +5289,7 @@ function buildPendingFinalizeHint() {
|
|
|
5094
5289
|
return "Drain or await the local finalize queue first, then retry after the queued Remix turn finishes recording remotely.";
|
|
5095
5290
|
}
|
|
5096
5291
|
|
|
5097
|
-
// src/application/collab/collabReAnchor.ts
|
|
5098
|
-
async function collabReAnchor(params) {
|
|
5099
|
-
const repoRoot = await findGitRoot(params.cwd);
|
|
5100
|
-
const binding = await ensureActiveLaneBinding({
|
|
5101
|
-
repoRoot,
|
|
5102
|
-
api: params.api,
|
|
5103
|
-
operation: "`remix collab re-anchor`"
|
|
5104
|
-
});
|
|
5105
|
-
if (!binding) {
|
|
5106
|
-
throw new RemixError("Repository is not bound to Remix.", {
|
|
5107
|
-
exitCode: 2,
|
|
5108
|
-
hint: "Run `remix collab init` first."
|
|
5109
|
-
});
|
|
5110
|
-
}
|
|
5111
|
-
const detected = await collabDetectRepoState({
|
|
5112
|
-
api: params.api,
|
|
5113
|
-
cwd: repoRoot,
|
|
5114
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
5115
|
-
});
|
|
5116
|
-
if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
|
|
5117
|
-
throw new RemixError("Repository must be realigned before seeding a fresh local Remix baseline.", {
|
|
5118
|
-
exitCode: 2,
|
|
5119
|
-
hint: detected.hint
|
|
5120
|
-
});
|
|
5121
|
-
}
|
|
5122
|
-
if (detected.status !== "ready" || !detected.binding) {
|
|
5123
|
-
throw new RemixError(detected.hint || "Repository is not ready for re-anchor.", {
|
|
5124
|
-
exitCode: 2,
|
|
5125
|
-
hint: detected.hint
|
|
5126
|
-
});
|
|
5127
|
-
}
|
|
5128
|
-
if (detected.repoState === "server_only_changed") {
|
|
5129
|
-
throw new RemixError("This checkout is already on a server-known base and only needs a local pull.", {
|
|
5130
|
-
exitCode: 2,
|
|
5131
|
-
hint: "Run `remix collab sync` instead of `remix collab re-anchor`."
|
|
5132
|
-
});
|
|
5133
|
-
}
|
|
5134
|
-
if (detected.repoState === "both_changed") {
|
|
5135
|
-
throw new RemixError("Both the local workspace and the server lane changed since the last agreed baseline.", {
|
|
5136
|
-
exitCode: 2,
|
|
5137
|
-
hint: "Run `remix collab reconcile` to replay the local boundary onto the newer server head."
|
|
5138
|
-
});
|
|
5139
|
-
}
|
|
5140
|
-
if (detected.repoState === "local_only_changed") {
|
|
5141
|
-
if (hasPendingFinalize(detected.pendingFinalize)) {
|
|
5142
|
-
throw new RemixError("Re-anchor is not needed while queued Remix turn recording is still processing.", {
|
|
5143
|
-
exitCode: 2,
|
|
5144
|
-
hint: buildPendingFinalizeHint()
|
|
5145
|
-
});
|
|
5146
|
-
}
|
|
5147
|
-
throw new RemixError("Re-anchor is not the right command for local content changes.", {
|
|
5148
|
-
exitCode: 2,
|
|
5149
|
-
hint: "Remix is source-blind: any local content change since the last recorded turn \u2014 including manual commits, pulls, merges, and rebases \u2014 is recorded with `remix collab finalize-turn`. Use `remix collab re-anchor` only when no local Remix baseline exists yet for this lane (status reports `re_anchor`)."
|
|
5150
|
-
});
|
|
5151
|
-
}
|
|
5152
|
-
if (detected.repoState === "idle") {
|
|
5153
|
-
throw new RemixError("This checkout is already aligned with Remix.", {
|
|
5154
|
-
exitCode: 2,
|
|
5155
|
-
hint: "No re-anchor step is needed. Re-anchor only applies when no local Remix baseline exists yet for this lane."
|
|
5156
|
-
});
|
|
5157
|
-
}
|
|
5158
|
-
await ensureCleanWorktree(repoRoot, "`remix collab re-anchor`");
|
|
5159
|
-
const branch = await requireCurrentBranch(repoRoot);
|
|
5160
|
-
const headCommitHash = await getHeadCommitHash(repoRoot);
|
|
5161
|
-
if (!headCommitHash) {
|
|
5162
|
-
throw new RemixError("Failed to resolve local HEAD commit.", { exitCode: 1 });
|
|
5163
|
-
}
|
|
5164
|
-
if (params.asyncSubmit && !params.dryRun) {
|
|
5165
|
-
const pending = await findPendingAsyncJob({
|
|
5166
|
-
repoRoot,
|
|
5167
|
-
branchName: binding.branchName ?? branch,
|
|
5168
|
-
kind: "re_anchor"
|
|
5169
|
-
});
|
|
5170
|
-
if (pending) {
|
|
5171
|
-
return {
|
|
5172
|
-
status: "queued",
|
|
5173
|
-
queued: true,
|
|
5174
|
-
jobId: pending.id,
|
|
5175
|
-
repoRoot,
|
|
5176
|
-
branch,
|
|
5177
|
-
currentAppId: binding.currentAppId,
|
|
5178
|
-
dryRun: false,
|
|
5179
|
-
applied: false
|
|
5180
|
-
};
|
|
5181
|
-
}
|
|
5182
|
-
}
|
|
5183
|
-
const preflightResp = await params.api.preflightAppReconcile(binding.currentAppId, {
|
|
5184
|
-
localHeadCommitHash: headCommitHash,
|
|
5185
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
5186
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
5187
|
-
defaultBranch: binding.defaultBranch ?? void 0
|
|
5188
|
-
});
|
|
5189
|
-
const preflight = unwrapResponseObject(preflightResp, "reconcile preflight");
|
|
5190
|
-
if (preflight.status === "metadata_conflict") {
|
|
5191
|
-
throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
|
|
5192
|
-
exitCode: 2,
|
|
5193
|
-
hint: preflight.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
5194
|
-
});
|
|
5195
|
-
}
|
|
5196
|
-
const preview = {
|
|
5197
|
-
status: preflight.status === "up_to_date" ? "reanchored" : "re_anchor_required",
|
|
5198
|
-
repoRoot,
|
|
5199
|
-
branch,
|
|
5200
|
-
currentAppId: binding.currentAppId,
|
|
5201
|
-
localHeadCommitHash: headCommitHash,
|
|
5202
|
-
targetHeadCommitHash: preflight.targetHeadCommitHash,
|
|
5203
|
-
targetHeadCommitId: preflight.targetHeadCommitId,
|
|
5204
|
-
warnings: preflight.warnings,
|
|
5205
|
-
applied: false,
|
|
5206
|
-
dryRun: params.dryRun === true
|
|
5207
|
-
};
|
|
5208
|
-
if (params.dryRun) {
|
|
5209
|
-
return preview;
|
|
5210
|
-
}
|
|
5211
|
-
let anchoredServerHeadHash = preflight.targetHeadCommitHash;
|
|
5212
|
-
if (params.asyncSubmit && preflight.status === "ready_to_reconcile") {
|
|
5213
|
-
const failed = await findFailedAsyncJob({
|
|
5214
|
-
repoRoot,
|
|
5215
|
-
branchName: binding.branchName ?? branch,
|
|
5216
|
-
kind: "re_anchor"
|
|
5217
|
-
});
|
|
5218
|
-
if (failed) {
|
|
5219
|
-
await deleteAsyncJob(failed.id);
|
|
5220
|
-
}
|
|
5221
|
-
const { bundlePath: tmpBundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(
|
|
5222
|
-
repoRoot,
|
|
5223
|
-
"re-anchor.bundle"
|
|
5224
|
-
);
|
|
5225
|
-
const tmpBundleDir = path10.dirname(tmpBundlePath);
|
|
5226
|
-
try {
|
|
5227
|
-
const jobId = randomUUID5();
|
|
5228
|
-
const durableBundlePath = getAsyncJobBundlePath(jobId);
|
|
5229
|
-
await fs11.mkdir(getAsyncJobDir(jobId), { recursive: true });
|
|
5230
|
-
try {
|
|
5231
|
-
await fs11.rename(tmpBundlePath, durableBundlePath);
|
|
5232
|
-
} catch (error) {
|
|
5233
|
-
if (error?.code !== "EXDEV") throw error;
|
|
5234
|
-
await fs11.copyFile(tmpBundlePath, durableBundlePath);
|
|
5235
|
-
await fs11.unlink(tmpBundlePath).catch(() => void 0);
|
|
5236
|
-
}
|
|
5237
|
-
const bundleSha = await sha256FileHex(durableBundlePath);
|
|
5238
|
-
const job = await enqueueAsyncJob({
|
|
5239
|
-
id: jobId,
|
|
5240
|
-
kind: "re_anchor",
|
|
5241
|
-
status: "queued",
|
|
5242
|
-
repoRoot,
|
|
5243
|
-
repoFingerprint: binding.repoFingerprint,
|
|
5244
|
-
branchName: binding.branchName ?? branch,
|
|
5245
|
-
laneId: binding.laneId,
|
|
5246
|
-
retryCount: 0,
|
|
5247
|
-
error: null,
|
|
5248
|
-
idempotencyKey: null,
|
|
5249
|
-
payload: {
|
|
5250
|
-
bundlePath: durableBundlePath,
|
|
5251
|
-
bundleSha256: bundleSha,
|
|
5252
|
-
localHeadCommitHash: bundledHeadCommitHash,
|
|
5253
|
-
targetHeadCommitHash: preflight.targetHeadCommitHash,
|
|
5254
|
-
appId: binding.currentAppId
|
|
5255
|
-
}
|
|
5256
|
-
});
|
|
5257
|
-
await logDrainerEvent(job.id, "submitted", { kind: "re_anchor" });
|
|
5258
|
-
return {
|
|
5259
|
-
status: "queued",
|
|
5260
|
-
queued: true,
|
|
5261
|
-
jobId: job.id,
|
|
5262
|
-
repoRoot,
|
|
5263
|
-
branch,
|
|
5264
|
-
currentAppId: binding.currentAppId,
|
|
5265
|
-
localHeadCommitHash: bundledHeadCommitHash,
|
|
5266
|
-
targetHeadCommitHash: preflight.targetHeadCommitHash,
|
|
5267
|
-
warnings: preflight.warnings,
|
|
5268
|
-
dryRun: false,
|
|
5269
|
-
applied: false
|
|
5270
|
-
};
|
|
5271
|
-
} finally {
|
|
5272
|
-
await fs11.rm(tmpBundleDir, { recursive: true, force: true }).catch(() => void 0);
|
|
5273
|
-
}
|
|
5274
|
-
}
|
|
5275
|
-
if (preflight.status === "ready_to_reconcile") {
|
|
5276
|
-
const { bundlePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(repoRoot, "re-anchor.bundle");
|
|
5277
|
-
const bundleTempDir = path10.dirname(bundlePath);
|
|
5278
|
-
try {
|
|
5279
|
-
const bundleStat = await fs11.stat(bundlePath);
|
|
5280
|
-
const checksumSha256 = await sha256FileHex(bundlePath);
|
|
5281
|
-
const presignResp = await params.api.presignImportUploadFirstParty({
|
|
5282
|
-
file: {
|
|
5283
|
-
name: path10.basename(bundlePath),
|
|
5284
|
-
mimeType: "application/x-git-bundle",
|
|
5285
|
-
size: bundleStat.size,
|
|
5286
|
-
checksumSha256
|
|
5287
|
-
}
|
|
5288
|
-
});
|
|
5289
|
-
const uploadTarget = unwrapResponseObject(presignResp, "import upload target");
|
|
5290
|
-
await uploadPresigned({
|
|
5291
|
-
uploadUrl: String(uploadTarget.uploadUrl),
|
|
5292
|
-
filePath: bundlePath,
|
|
5293
|
-
headers: uploadTarget.headers ?? {}
|
|
5294
|
-
});
|
|
5295
|
-
const startResp = await params.api.startAppReconcile(binding.currentAppId, {
|
|
5296
|
-
uploadId: String(uploadTarget.uploadId),
|
|
5297
|
-
localHeadCommitHash: bundledHeadCommitHash,
|
|
5298
|
-
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
5299
|
-
remoteUrl: binding.remoteUrl ?? void 0,
|
|
5300
|
-
defaultBranch: binding.defaultBranch ?? void 0,
|
|
5301
|
-
idempotencyKey: buildDeterministicIdempotencyKey({
|
|
5302
|
-
kind: "collab_re_anchor_v1",
|
|
5303
|
-
appId: binding.currentAppId,
|
|
5304
|
-
localHeadCommitHash: bundledHeadCommitHash,
|
|
5305
|
-
targetHeadCommitHash: preflight.targetHeadCommitHash
|
|
5306
|
-
})
|
|
5307
|
-
});
|
|
5308
|
-
const started = unwrapResponseObject(startResp, "reconcile");
|
|
5309
|
-
const reconcile = await pollReconcile(params.api, binding.currentAppId, started.id);
|
|
5310
|
-
anchoredServerHeadHash = reconcile.reconciledHeadCommitHash ?? reconcile.targetHeadCommitHash ?? preflight.targetHeadCommitHash;
|
|
5311
|
-
} finally {
|
|
5312
|
-
await fs11.rm(bundleTempDir, { recursive: true, force: true });
|
|
5313
|
-
}
|
|
5314
|
-
}
|
|
5315
|
-
const snapshot = await captureLocalSnapshot({
|
|
5316
|
-
repoRoot,
|
|
5317
|
-
repoFingerprint: binding.repoFingerprint,
|
|
5318
|
-
laneId: binding.laneId,
|
|
5319
|
-
branchName: binding.branchName
|
|
5320
|
-
});
|
|
5321
|
-
await writeLocalBaseline({
|
|
5322
|
-
repoRoot,
|
|
5323
|
-
repoFingerprint: binding.repoFingerprint,
|
|
5324
|
-
laneId: binding.laneId,
|
|
5325
|
-
currentAppId: binding.currentAppId,
|
|
5326
|
-
branchName: binding.branchName,
|
|
5327
|
-
lastSnapshotId: snapshot.id,
|
|
5328
|
-
lastSnapshotHash: snapshot.snapshotHash,
|
|
5329
|
-
lastServerHeadHash: anchoredServerHeadHash,
|
|
5330
|
-
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
5331
|
-
});
|
|
5332
|
-
return {
|
|
5333
|
-
...preview,
|
|
5334
|
-
status: "reanchored",
|
|
5335
|
-
targetHeadCommitHash: anchoredServerHeadHash,
|
|
5336
|
-
applied: true,
|
|
5337
|
-
dryRun: false
|
|
5338
|
-
};
|
|
5339
|
-
}
|
|
5340
|
-
async function collabReAnchorSubmit(params) {
|
|
5341
|
-
return collabReAnchor({ ...params, asyncSubmit: true });
|
|
5342
|
-
}
|
|
5343
|
-
|
|
5344
5292
|
// src/application/collab/collabReconcile.ts
|
|
5345
|
-
import fs12 from "fs/promises";
|
|
5346
|
-
import os5 from "os";
|
|
5347
|
-
import path11 from "path";
|
|
5348
|
-
import { execa as execa3 } from "execa";
|
|
5349
5293
|
async function reconcileBothChanged(params) {
|
|
5350
5294
|
const repoRoot = await findGitRoot(params.cwd);
|
|
5351
5295
|
const binding = await ensureActiveLaneBinding({
|
|
@@ -5368,7 +5312,7 @@ async function reconcileBothChanged(params) {
|
|
|
5368
5312
|
if (!baseline?.lastSnapshotId || !baseline.lastServerHeadHash) {
|
|
5369
5313
|
throw new RemixError("Local Remix baseline is missing for this lane.", {
|
|
5370
5314
|
exitCode: 2,
|
|
5371
|
-
hint: "Run `remix collab
|
|
5315
|
+
hint: "Run `remix collab init` or sync from a checkout with a valid revision baseline first."
|
|
5372
5316
|
});
|
|
5373
5317
|
}
|
|
5374
5318
|
const currentSnapshot = await captureLocalSnapshot({
|
|
@@ -5391,6 +5335,7 @@ async function reconcileBothChanged(params) {
|
|
|
5391
5335
|
params.api.getAppHead(binding.currentAppId),
|
|
5392
5336
|
params.api.getAppDelta(binding.currentAppId, {
|
|
5393
5337
|
baseHeadHash: baseline.lastServerHeadHash,
|
|
5338
|
+
baseRevisionId: baseline.lastServerRevisionId,
|
|
5394
5339
|
repoFingerprint: binding.repoFingerprint ?? void 0,
|
|
5395
5340
|
remoteUrl: binding.remoteUrl ?? void 0,
|
|
5396
5341
|
defaultBranch: binding.defaultBranch ?? void 0
|
|
@@ -5408,7 +5353,7 @@ async function reconcileBothChanged(params) {
|
|
|
5408
5353
|
if (delta.status === "base_unknown") {
|
|
5409
5354
|
throw new RemixError("Reconcile cannot pull the newer server state from the last acknowledged baseline.", {
|
|
5410
5355
|
exitCode: 2,
|
|
5411
|
-
hint: "Run `remix collab
|
|
5356
|
+
hint: "Run `remix collab init` to seed a fresh revision baseline for this checkout before retrying."
|
|
5412
5357
|
});
|
|
5413
5358
|
}
|
|
5414
5359
|
if (delta.status !== "delta_ready" && delta.status !== "up_to_date") {
|
|
@@ -5439,7 +5384,9 @@ async function reconcileBothChanged(params) {
|
|
|
5439
5384
|
assistantResponse: "Replay the local boundary delta onto the latest server head without recording a new change step.",
|
|
5440
5385
|
diff: diffResult.diff,
|
|
5441
5386
|
baseCommitHash: baseline.lastServerHeadHash,
|
|
5387
|
+
baseRevisionId: baseline.lastServerRevisionId,
|
|
5442
5388
|
targetHeadCommitHash: appHead.headCommitHash,
|
|
5389
|
+
targetRevisionId: appHead.headRevisionId,
|
|
5443
5390
|
expectedPaths: diffResult.changedPaths,
|
|
5444
5391
|
workspaceMetadata: {
|
|
5445
5392
|
recordingMode: "boundary_delta",
|
|
@@ -5447,6 +5394,7 @@ async function reconcileBothChanged(params) {
|
|
|
5447
5394
|
branch,
|
|
5448
5395
|
baselineSnapshotId: baseline.lastSnapshotId,
|
|
5449
5396
|
currentSnapshotId: currentSnapshot.id,
|
|
5397
|
+
baselineServerRevisionId: baseline.lastServerRevisionId,
|
|
5450
5398
|
baselineServerHeadHash: baseline.lastServerHeadHash,
|
|
5451
5399
|
currentSnapshotHash: currentSnapshot.snapshotHash,
|
|
5452
5400
|
localCommitHash: currentSnapshot.localCommitHash,
|
|
@@ -5465,12 +5413,12 @@ async function reconcileBothChanged(params) {
|
|
|
5465
5413
|
const replay = await pollChangeStepReplay(params.api, binding.currentAppId, String(replayStart.id));
|
|
5466
5414
|
const replayDiffResp = await params.api.getChangeStepReplayDiff(binding.currentAppId, replay.id);
|
|
5467
5415
|
const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
|
|
5468
|
-
const tempRoot = await
|
|
5416
|
+
const tempRoot = await fs11.mkdtemp(path10.join(os5.tmpdir(), "remix-reconcile-"));
|
|
5469
5417
|
let serverHeadSnapshot = null;
|
|
5470
5418
|
let mergedSnapshot = null;
|
|
5471
5419
|
try {
|
|
5472
|
-
const tempRepoRoot =
|
|
5473
|
-
await
|
|
5420
|
+
const tempRepoRoot = path10.join(tempRoot, "repo");
|
|
5421
|
+
await fs11.mkdir(tempRepoRoot, { recursive: true });
|
|
5474
5422
|
await execa3("git", ["init"], { cwd: tempRepoRoot, stderr: "ignore" });
|
|
5475
5423
|
await materializeLocalSnapshot(baseline.lastSnapshotId, tempRepoRoot);
|
|
5476
5424
|
if (delta.status === "delta_ready" && delta.diff.trim()) {
|
|
@@ -5492,7 +5440,7 @@ async function reconcileBothChanged(params) {
|
|
|
5492
5440
|
branchName: binding.branchName
|
|
5493
5441
|
});
|
|
5494
5442
|
} finally {
|
|
5495
|
-
await
|
|
5443
|
+
await fs11.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
|
|
5496
5444
|
}
|
|
5497
5445
|
if (!serverHeadSnapshot || !mergedSnapshot) {
|
|
5498
5446
|
throw new RemixError("Failed to materialize the reconciled local workspace.", { exitCode: 1 });
|
|
@@ -5529,6 +5477,8 @@ async function reconcileBothChanged(params) {
|
|
|
5529
5477
|
branchName: binding.branchName,
|
|
5530
5478
|
lastSnapshotId: serverHeadSnapshot.id,
|
|
5531
5479
|
lastSnapshotHash: serverHeadSnapshot.snapshotHash,
|
|
5480
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
5481
|
+
lastServerTreeHash: appHead.treeHash ?? null,
|
|
5532
5482
|
lastServerHeadHash: appHead.headCommitHash,
|
|
5533
5483
|
lastSeenLocalCommitHash: restoredSnapshot.localCommitHash
|
|
5534
5484
|
});
|
|
@@ -5564,7 +5514,10 @@ async function collabReconcile(params) {
|
|
|
5564
5514
|
return reconcileBothChanged(params);
|
|
5565
5515
|
}
|
|
5566
5516
|
if (detected.repoState === "external_local_base_changed") {
|
|
5567
|
-
|
|
5517
|
+
throw new RemixError("This checkout needs a local Remix revision baseline before reconciliation.", {
|
|
5518
|
+
exitCode: 2,
|
|
5519
|
+
hint: detected.hint || "Run `remix collab init` or `remix collab sync` to seed the baseline."
|
|
5520
|
+
});
|
|
5568
5521
|
}
|
|
5569
5522
|
if (detected.repoState === "local_only_changed") {
|
|
5570
5523
|
if (hasPendingFinalize(detected.pendingFinalize)) {
|
|
@@ -5675,6 +5628,8 @@ async function collabRemix(params) {
|
|
|
5675
5628
|
branchName: branchNameForBaseline,
|
|
5676
5629
|
lastSnapshotId: snapshot.id,
|
|
5677
5630
|
lastSnapshotHash: snapshot.snapshotHash,
|
|
5631
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
5632
|
+
lastServerTreeHash: appHead.treeHash ?? null,
|
|
5678
5633
|
lastServerHeadHash: appHead.headCommitHash,
|
|
5679
5634
|
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
5680
5635
|
});
|
|
@@ -5799,11 +5754,15 @@ function createBaseStatus() {
|
|
|
5799
5754
|
baseline: {
|
|
5800
5755
|
lastSnapshotId: null,
|
|
5801
5756
|
lastSnapshotHash: null,
|
|
5757
|
+
lastServerRevisionId: null,
|
|
5758
|
+
lastServerTreeHash: null,
|
|
5802
5759
|
lastServerHeadHash: null,
|
|
5803
5760
|
lastSeenLocalCommitHash: null
|
|
5804
5761
|
},
|
|
5805
5762
|
current: {
|
|
5806
5763
|
snapshotHash: null,
|
|
5764
|
+
serverRevisionId: null,
|
|
5765
|
+
serverTreeHash: null,
|
|
5807
5766
|
serverHeadHash: null,
|
|
5808
5767
|
serverHeadCommitId: null,
|
|
5809
5768
|
localCommitHash: null
|
|
@@ -5890,6 +5849,8 @@ async function collabStatus(params) {
|
|
|
5890
5849
|
status.alignment.baseline = detected.baseline;
|
|
5891
5850
|
status.alignment.current = {
|
|
5892
5851
|
snapshotHash: detected.currentSnapshotHash,
|
|
5852
|
+
serverRevisionId: detected.currentServerRevisionId,
|
|
5853
|
+
serverTreeHash: detected.currentServerTreeHash,
|
|
5893
5854
|
serverHeadHash: detected.currentServerHeadHash,
|
|
5894
5855
|
serverHeadCommitId: detected.currentServerHeadCommitId,
|
|
5895
5856
|
localCommitHash: detected.localCommitHash
|
|
@@ -5953,7 +5914,7 @@ async function collabStatus(params) {
|
|
|
5953
5914
|
status.reconcile.canApply = !status.repo.branchMismatch;
|
|
5954
5915
|
status.recommendedAction = "reconcile";
|
|
5955
5916
|
} else if (detected.repoState === "external_local_base_changed") {
|
|
5956
|
-
status.recommendedAction = "
|
|
5917
|
+
status.recommendedAction = "init";
|
|
5957
5918
|
addBlockedReason(status.sync, "baseline_missing");
|
|
5958
5919
|
addBlockedReason(status.reconcile, "baseline_missing");
|
|
5959
5920
|
} else if (detected.repoState === "local_only_changed") {
|
|
@@ -6117,8 +6078,8 @@ async function collabView(params) {
|
|
|
6117
6078
|
}
|
|
6118
6079
|
|
|
6119
6080
|
// src/application/collab/collabAsyncProcessing.ts
|
|
6120
|
-
import
|
|
6121
|
-
import
|
|
6081
|
+
import fs12 from "fs/promises";
|
|
6082
|
+
import path11 from "path";
|
|
6122
6083
|
var MAX_TRANSIENT_RETRIES = 5;
|
|
6123
6084
|
var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
6124
6085
|
"ECONNREFUSED",
|
|
@@ -6214,10 +6175,10 @@ async function processInitJob(job, api) {
|
|
|
6214
6175
|
try {
|
|
6215
6176
|
await updateAsyncJob(job.id, { status: "submitting", error: null });
|
|
6216
6177
|
await logDrainerEvent(job.id, "claimed", { kind: "init" });
|
|
6217
|
-
const bundleStat = await
|
|
6178
|
+
const bundleStat = await fs12.stat(job.payload.bundlePath);
|
|
6218
6179
|
const presignResp = await api.presignImportUploadFirstParty({
|
|
6219
6180
|
file: {
|
|
6220
|
-
name:
|
|
6181
|
+
name: path11.basename(job.payload.bundlePath),
|
|
6221
6182
|
mimeType: "application/x-git-bundle",
|
|
6222
6183
|
size: bundleStat.size,
|
|
6223
6184
|
checksumSha256: job.payload.bundleSha256
|
|
@@ -6234,7 +6195,7 @@ async function processInitJob(job, api) {
|
|
|
6234
6195
|
await updateAsyncJob(job.id, { status: "server_processing" });
|
|
6235
6196
|
const importResp = await api.importFromUploadFirstParty({
|
|
6236
6197
|
uploadId: String(presign.uploadId),
|
|
6237
|
-
appName: job.payload.appName?.trim() ||
|
|
6198
|
+
appName: job.payload.appName?.trim() || path11.basename(job.repoRoot),
|
|
6238
6199
|
platform: "generic",
|
|
6239
6200
|
isPublic: false,
|
|
6240
6201
|
branch: job.payload.defaultBranch && job.branchName && job.branchName !== job.payload.defaultBranch ? job.payload.defaultBranch : job.branchName ?? void 0,
|
|
@@ -6428,7 +6389,7 @@ async function processInitPostJob(job, api) {
|
|
|
6428
6389
|
if (outcome.status === "failed") {
|
|
6429
6390
|
const bindingPath = getCollabBindingPath(job.repoRoot);
|
|
6430
6391
|
try {
|
|
6431
|
-
await
|
|
6392
|
+
await fs12.unlink(bindingPath);
|
|
6432
6393
|
await logDrainerEvent(job.id, "binding_cleared", {
|
|
6433
6394
|
kind: "init_post",
|
|
6434
6395
|
appId: job.payload.appId,
|
|
@@ -6469,10 +6430,10 @@ async function processReAnchorJob(job, api) {
|
|
|
6469
6430
|
}
|
|
6470
6431
|
let anchoredServerHeadHash = preflight.targetHeadCommitHash;
|
|
6471
6432
|
if (preflight.status === "ready_to_reconcile") {
|
|
6472
|
-
const bundleStat = await
|
|
6433
|
+
const bundleStat = await fs12.stat(job.payload.bundlePath);
|
|
6473
6434
|
const presignResp = await api.presignImportUploadFirstParty({
|
|
6474
6435
|
file: {
|
|
6475
|
-
name:
|
|
6436
|
+
name: path11.basename(job.payload.bundlePath),
|
|
6476
6437
|
mimeType: "application/x-git-bundle",
|
|
6477
6438
|
size: bundleStat.size,
|
|
6478
6439
|
checksumSha256: job.payload.bundleSha256
|
|
@@ -6576,9 +6537,9 @@ async function collabReAnchorProcess(jobId, opts) {
|
|
|
6576
6537
|
}
|
|
6577
6538
|
async function acquireDrainerPidLock() {
|
|
6578
6539
|
const pidPath = getDrainerPidPath();
|
|
6579
|
-
await
|
|
6540
|
+
await fs12.mkdir(path11.dirname(pidPath), { recursive: true });
|
|
6580
6541
|
try {
|
|
6581
|
-
const existing = await
|
|
6542
|
+
const existing = await fs12.readFile(pidPath, "utf8").catch(() => "");
|
|
6582
6543
|
const existingPid = parseInt(existing.trim(), 10);
|
|
6583
6544
|
if (Number.isFinite(existingPid) && existingPid > 0 && existingPid !== process.pid) {
|
|
6584
6545
|
try {
|
|
@@ -6588,13 +6549,13 @@ async function acquireDrainerPidLock() {
|
|
|
6588
6549
|
if (error?.code !== "ESRCH") return null;
|
|
6589
6550
|
}
|
|
6590
6551
|
}
|
|
6591
|
-
await
|
|
6552
|
+
await fs12.writeFile(pidPath, String(process.pid), "utf8");
|
|
6592
6553
|
return {
|
|
6593
6554
|
release: async () => {
|
|
6594
6555
|
try {
|
|
6595
|
-
const current = (await
|
|
6556
|
+
const current = (await fs12.readFile(pidPath, "utf8")).trim();
|
|
6596
6557
|
if (current === String(process.pid)) {
|
|
6597
|
-
await
|
|
6558
|
+
await fs12.unlink(pidPath).catch(() => void 0);
|
|
6598
6559
|
}
|
|
6599
6560
|
} catch {
|
|
6600
6561
|
}
|
|
@@ -6645,9 +6606,6 @@ export {
|
|
|
6645
6606
|
collabList,
|
|
6646
6607
|
collabListMembers,
|
|
6647
6608
|
collabListMergeRequests,
|
|
6648
|
-
collabReAnchor,
|
|
6649
|
-
collabReAnchorProcess,
|
|
6650
|
-
collabReAnchorSubmit,
|
|
6651
6609
|
collabReconcile,
|
|
6652
6610
|
collabRecordingPreflight,
|
|
6653
6611
|
collabReject,
|
|
@@ -6675,6 +6633,7 @@ export {
|
|
|
6675
6633
|
processPendingFinalizeJob,
|
|
6676
6634
|
pruneTerminalAsyncJobs,
|
|
6677
6635
|
readAsyncJob,
|
|
6636
|
+
readLocalSnapshot,
|
|
6678
6637
|
readPendingFinalizeJob,
|
|
6679
6638
|
requeuePendingFinalizeJob,
|
|
6680
6639
|
summarizeAsyncJobs,
|