@remixhq/core 0.1.24 → 0.1.26
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 +94 -8
- package/dist/api.js +1 -1
- package/dist/binding.js +2 -2
- package/dist/{chunk-BNY4BVH6.js → chunk-DOMO3LNV.js} +175 -179
- package/dist/{chunk-S4ECO35X.js → chunk-FA6PTZQI.js} +46 -6
- package/dist/{chunk-P4XIICU3.js → chunk-GNW6FA4J.js} +1 -1
- package/dist/collab.d.ts +96 -134
- package/dist/collab.js +1134 -755
- package/dist/{contracts-S3cbsIUV.d.ts → contracts-BV67NVwV.d.ts} +7 -0
- package/dist/history.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/repo.js +1 -1
- package/package.json +1 -1
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-GNW6FA4J.js";
|
|
12
12
|
import {
|
|
13
13
|
applyUnifiedDiffToWorktree,
|
|
14
14
|
assertRepoSnapshotUnchanged,
|
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
checkoutLocalBranch,
|
|
18
18
|
cloneGitBundleToDirectory,
|
|
19
19
|
createGitBundle,
|
|
20
|
+
createGitCommandPlan,
|
|
21
|
+
createSystemGitAdapter,
|
|
20
22
|
ensureCleanWorktree,
|
|
21
23
|
ensureGitInfoExcludeEntries,
|
|
22
24
|
findGitRoot,
|
|
@@ -30,7 +32,7 @@ import {
|
|
|
30
32
|
requireCurrentBranch,
|
|
31
33
|
setRemoteOriginUrl,
|
|
32
34
|
summarizeUnifiedDiff
|
|
33
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-FA6PTZQI.js";
|
|
34
36
|
import {
|
|
35
37
|
REMIX_ERROR_CODES
|
|
36
38
|
} from "./chunk-GC2MOT3U.js";
|
|
@@ -191,6 +193,9 @@ function getAsyncJobDir(jobId) {
|
|
|
191
193
|
function getAsyncJobFilePath(jobId) {
|
|
192
194
|
return path.join(getAsyncJobDir(jobId), "job.json");
|
|
193
195
|
}
|
|
196
|
+
function getAsyncJobBundlePath(jobId) {
|
|
197
|
+
return path.join(getAsyncJobDir(jobId), "bundle.bundle");
|
|
198
|
+
}
|
|
194
199
|
function getLogsRoot() {
|
|
195
200
|
return path.join(getCollabStateRoot(), "logs");
|
|
196
201
|
}
|
|
@@ -204,6 +209,7 @@ function getDrainerPidPath() {
|
|
|
204
209
|
// src/infrastructure/collab/asyncJobStore.ts
|
|
205
210
|
var ASYNC_JOB_LOCK_STALE_MS = 10 * 60 * 1e3;
|
|
206
211
|
var TERMINAL_ASYNC_JOB_RETENTION_MS = 24 * 60 * 60 * 1e3;
|
|
212
|
+
var REMOVED_ASYNC_JOB_KINDS = /* @__PURE__ */ new Set([["re", "anchor"].join("_")]);
|
|
207
213
|
function getJobLockPath(id) {
|
|
208
214
|
return path2.join(getAsyncJobDir(id), "lock");
|
|
209
215
|
}
|
|
@@ -249,7 +255,7 @@ function normalizeJob(input) {
|
|
|
249
255
|
if (input.kind === "init_post") {
|
|
250
256
|
return { ...base, kind: "init_post", payload: input.payload };
|
|
251
257
|
}
|
|
252
|
-
|
|
258
|
+
throw new Error(`Unsupported async job kind: ${input.kind ?? "unknown"}`);
|
|
253
259
|
}
|
|
254
260
|
async function enqueueAsyncJob(input) {
|
|
255
261
|
const job = normalizeJob(input);
|
|
@@ -262,7 +268,7 @@ function parseAsyncJob(raw) {
|
|
|
262
268
|
const data = raw;
|
|
263
269
|
if (data.schemaVersion !== 1) return null;
|
|
264
270
|
if (typeof data.id !== "string") return null;
|
|
265
|
-
if (data.kind !== "init" && data.kind !== "init_post"
|
|
271
|
+
if (data.kind !== "init" && data.kind !== "init_post") return null;
|
|
266
272
|
if (!data.payload || typeof data.payload !== "object") return null;
|
|
267
273
|
return data;
|
|
268
274
|
}
|
|
@@ -289,6 +295,30 @@ async function listAsyncJobs() {
|
|
|
289
295
|
);
|
|
290
296
|
return jobs.filter((job) => Boolean(job)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
291
297
|
}
|
|
298
|
+
async function pruneRemovedAsyncJobKinds() {
|
|
299
|
+
const root = getAsyncJobsRoot();
|
|
300
|
+
let entries;
|
|
301
|
+
try {
|
|
302
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error?.code === "ENOENT") return;
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
await Promise.all(
|
|
308
|
+
entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
const raw = await fs.readFile(getAsyncJobFilePath(entry.name), "utf8");
|
|
312
|
+
parsed = JSON.parse(raw);
|
|
313
|
+
} catch {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (parsed.schemaVersion === 1 && typeof parsed.id === "string" && typeof parsed.kind === "string" && REMOVED_ASYNC_JOB_KINDS.has(parsed.kind)) {
|
|
317
|
+
await deleteAsyncJob(entry.name);
|
|
318
|
+
}
|
|
319
|
+
})
|
|
320
|
+
);
|
|
321
|
+
}
|
|
292
322
|
async function listAsyncJobsForRepo(params) {
|
|
293
323
|
const jobs = await listAsyncJobs();
|
|
294
324
|
return jobs.filter((job) => {
|
|
@@ -319,8 +349,8 @@ async function updateAsyncJob(jobId, update) {
|
|
|
319
349
|
if (!existing) return null;
|
|
320
350
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
321
351
|
if (existing.kind === "init") {
|
|
322
|
-
const
|
|
323
|
-
const
|
|
352
|
+
const nextPayload = update.payload ?? existing.payload;
|
|
353
|
+
const next = {
|
|
324
354
|
...existing,
|
|
325
355
|
...update,
|
|
326
356
|
schemaVersion: 1,
|
|
@@ -328,14 +358,14 @@ async function updateAsyncJob(jobId, update) {
|
|
|
328
358
|
kind: "init",
|
|
329
359
|
createdAt: existing.createdAt,
|
|
330
360
|
updatedAt: now,
|
|
331
|
-
payload:
|
|
361
|
+
payload: nextPayload
|
|
332
362
|
};
|
|
333
|
-
await writeJsonAtomic(getAsyncJobFilePath(jobId),
|
|
334
|
-
return
|
|
363
|
+
await writeJsonAtomic(getAsyncJobFilePath(jobId), next);
|
|
364
|
+
return next;
|
|
335
365
|
}
|
|
336
366
|
if (existing.kind === "init_post") {
|
|
337
|
-
const
|
|
338
|
-
const
|
|
367
|
+
const nextPayload = update.payload ?? existing.payload;
|
|
368
|
+
const next = {
|
|
339
369
|
...existing,
|
|
340
370
|
...update,
|
|
341
371
|
schemaVersion: 1,
|
|
@@ -343,24 +373,12 @@ async function updateAsyncJob(jobId, update) {
|
|
|
343
373
|
kind: "init_post",
|
|
344
374
|
createdAt: existing.createdAt,
|
|
345
375
|
updatedAt: now,
|
|
346
|
-
payload:
|
|
376
|
+
payload: nextPayload
|
|
347
377
|
};
|
|
348
|
-
await writeJsonAtomic(getAsyncJobFilePath(jobId),
|
|
349
|
-
return
|
|
378
|
+
await writeJsonAtomic(getAsyncJobFilePath(jobId), next);
|
|
379
|
+
return next;
|
|
350
380
|
}
|
|
351
|
-
|
|
352
|
-
const next = {
|
|
353
|
-
...existing,
|
|
354
|
-
...update,
|
|
355
|
-
schemaVersion: 1,
|
|
356
|
-
id: existing.id,
|
|
357
|
-
kind: "re_anchor",
|
|
358
|
-
createdAt: existing.createdAt,
|
|
359
|
-
updatedAt: now,
|
|
360
|
-
payload: nextPayload
|
|
361
|
-
};
|
|
362
|
-
await writeJsonAtomic(getAsyncJobFilePath(jobId), next);
|
|
363
|
-
return next;
|
|
381
|
+
throw new Error(`Unsupported async job kind: ${existing.kind ?? "unknown"}`);
|
|
364
382
|
}
|
|
365
383
|
async function deleteAsyncJob(jobId) {
|
|
366
384
|
await fs.rm(getAsyncJobDir(jobId), { recursive: true, force: true });
|
|
@@ -434,14 +452,15 @@ async function summarizeAsyncJobs(params) {
|
|
|
434
452
|
}
|
|
435
453
|
summary.activeJobCount = summary.queuedJobCount + summary.processingJobCount;
|
|
436
454
|
summary.kinds = Array.from(kinds);
|
|
437
|
-
summary.oldestCreatedAt = active[0]
|
|
438
|
-
summary.newestCreatedAt = active[active.length - 1]
|
|
455
|
+
summary.oldestCreatedAt = active[0].createdAt;
|
|
456
|
+
summary.newestCreatedAt = active[active.length - 1].createdAt;
|
|
439
457
|
if (summary.processingJobCount > 0) summary.state = "processing";
|
|
440
458
|
else if (summary.queuedJobCount > 0) summary.state = "queued";
|
|
441
459
|
else summary.state = "failed";
|
|
442
460
|
return summary;
|
|
443
461
|
}
|
|
444
462
|
async function pruneTerminalAsyncJobs() {
|
|
463
|
+
await pruneRemovedAsyncJobKinds();
|
|
445
464
|
const jobs = await listAsyncJobs();
|
|
446
465
|
const now = Date.now();
|
|
447
466
|
await Promise.all(
|
|
@@ -453,6 +472,9 @@ async function pruneTerminalAsyncJobs() {
|
|
|
453
472
|
}).map((job) => deleteAsyncJob(job.id))
|
|
454
473
|
);
|
|
455
474
|
}
|
|
475
|
+
function getAsyncJobBundleFilePath(jobId) {
|
|
476
|
+
return getAsyncJobBundlePath(jobId);
|
|
477
|
+
}
|
|
456
478
|
async function awaitAsyncJob(params) {
|
|
457
479
|
const pollIntervalMs = Math.max(50, params.pollIntervalMs ?? 500);
|
|
458
480
|
const deadline = Date.now() + params.timeoutMs;
|
|
@@ -536,6 +558,15 @@ async function writeLocalBaseline(baseline) {
|
|
|
536
558
|
await writeJsonAtomic(getBaselinePath(baseline), normalized);
|
|
537
559
|
return normalized;
|
|
538
560
|
}
|
|
561
|
+
async function clearLocalBaseline(params) {
|
|
562
|
+
try {
|
|
563
|
+
await fs2.unlink(getBaselinePath(params));
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error?.code !== "ENOENT") {
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
539
570
|
|
|
540
571
|
// src/infrastructure/collab/localSnapshotStore.ts
|
|
541
572
|
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
@@ -1207,7 +1238,9 @@ async function resolveProjectLaneIfAuthoritative(api, params) {
|
|
|
1207
1238
|
repoFingerprint: params.repoFingerprint ?? void 0,
|
|
1208
1239
|
remoteUrl: params.remoteUrl ?? void 0,
|
|
1209
1240
|
defaultBranch: params.defaultBranch ?? void 0,
|
|
1210
|
-
branchName
|
|
1241
|
+
branchName,
|
|
1242
|
+
currentAppId: params.currentAppId ?? void 0,
|
|
1243
|
+
upstreamAppId: params.expectedUpstreamAppId ?? void 0
|
|
1211
1244
|
});
|
|
1212
1245
|
return unwrapResponseObject(laneResp, "project lane binding");
|
|
1213
1246
|
};
|
|
@@ -1222,6 +1255,8 @@ async function resolveProjectLaneIfAuthoritative(api, params) {
|
|
|
1222
1255
|
remoteUrl: params.remoteUrl ?? void 0,
|
|
1223
1256
|
defaultBranch: params.defaultBranch ?? void 0,
|
|
1224
1257
|
branchName,
|
|
1258
|
+
currentAppId: params.currentAppId ?? void 0,
|
|
1259
|
+
upstreamAppId: params.expectedUpstreamAppId ?? void 0,
|
|
1225
1260
|
seedAppId: params.seedAppId
|
|
1226
1261
|
});
|
|
1227
1262
|
lane = unwrapResponseObject(ensuredResp, "project lane binding");
|
|
@@ -1233,6 +1268,8 @@ async function resolveProjectLaneIfAuthoritative(api, params) {
|
|
|
1233
1268
|
remoteUrl: params.remoteUrl ?? void 0,
|
|
1234
1269
|
defaultBranch: params.defaultBranch ?? void 0,
|
|
1235
1270
|
branchName,
|
|
1271
|
+
currentAppId: params.currentAppId ?? void 0,
|
|
1272
|
+
upstreamAppId: params.expectedUpstreamAppId ?? void 0,
|
|
1236
1273
|
seedAppId: params.seedAppId
|
|
1237
1274
|
});
|
|
1238
1275
|
lane = unwrapResponseObject(ensuredResp, "project lane binding");
|
|
@@ -1243,10 +1280,12 @@ async function resolveProjectLaneIfAuthoritative(api, params) {
|
|
|
1243
1280
|
if (params.expectedUpstreamAppId && lane.upstreamAppId && lane.upstreamAppId !== params.expectedUpstreamAppId) return null;
|
|
1244
1281
|
return lane;
|
|
1245
1282
|
}
|
|
1246
|
-
async function pollAppReady(api, appId) {
|
|
1247
|
-
const
|
|
1283
|
+
async function pollAppReady(api, appId, deps = {}) {
|
|
1284
|
+
const now = deps.clock?.now ?? Date.now;
|
|
1285
|
+
const sleepFn = deps.timers?.sleep ?? sleep;
|
|
1286
|
+
const started = now();
|
|
1248
1287
|
let delay = 2e3;
|
|
1249
|
-
while (
|
|
1288
|
+
while (now() - started < 20 * 60 * 1e3) {
|
|
1250
1289
|
const appResp = await api.getApp(appId);
|
|
1251
1290
|
const app = unwrapResponseObject(appResp, "app");
|
|
1252
1291
|
const status = typeof app.status === "string" ? app.status : "";
|
|
@@ -1257,7 +1296,7 @@ async function pollAppReady(api, appId) {
|
|
|
1257
1296
|
hint: typeof app.statusError === "string" ? app.statusError : null
|
|
1258
1297
|
});
|
|
1259
1298
|
}
|
|
1260
|
-
await
|
|
1299
|
+
await sleepFn(delay);
|
|
1261
1300
|
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
1262
1301
|
}
|
|
1263
1302
|
throw new RemixError("Timed out waiting for app to become ready.", { exitCode: 1 });
|
|
@@ -1327,30 +1366,6 @@ async function pollChangeStep(api, appId, changeStepId) {
|
|
|
1327
1366
|
}
|
|
1328
1367
|
throw new RemixError("Timed out waiting for change step.", { exitCode: 1 });
|
|
1329
1368
|
}
|
|
1330
|
-
async function pollReconcile(api, appId, reconcileId) {
|
|
1331
|
-
const started = Date.now();
|
|
1332
|
-
let delay = 1500;
|
|
1333
|
-
while (Date.now() - started < 30 * 60 * 1e3) {
|
|
1334
|
-
const resp = await api.getAppReconcile(appId, reconcileId);
|
|
1335
|
-
const reconcile = unwrapResponseObject(resp, "reconcile");
|
|
1336
|
-
if (reconcile.status === "succeeded") return reconcile;
|
|
1337
|
-
if (reconcile.status === "manual_reconcile_required") {
|
|
1338
|
-
throw new RemixError("Reconciliation requires manual intervention.", {
|
|
1339
|
-
exitCode: 2,
|
|
1340
|
-
hint: reconcile.statusError || "The server could not safely replay the local-only commits."
|
|
1341
|
-
});
|
|
1342
|
-
}
|
|
1343
|
-
if (reconcile.status === "failed" || reconcile.status === "cancelled") {
|
|
1344
|
-
throw new RemixError("Reconciliation failed.", {
|
|
1345
|
-
exitCode: 1,
|
|
1346
|
-
hint: reconcile.statusError || null
|
|
1347
|
-
});
|
|
1348
|
-
}
|
|
1349
|
-
await sleep(delay);
|
|
1350
|
-
delay = Math.min(1e4, Math.floor(delay * 1.4));
|
|
1351
|
-
}
|
|
1352
|
-
throw new RemixError("Timed out waiting for reconcile job.", { exitCode: 1 });
|
|
1353
|
-
}
|
|
1354
1369
|
async function pollChangeStepReplay(api, appId, replayId) {
|
|
1355
1370
|
const started = Date.now();
|
|
1356
1371
|
let delay = 1500;
|
|
@@ -1507,7 +1522,14 @@ var RESOLUTION_CACHE_TTL_MS = 5e3;
|
|
|
1507
1522
|
var resolutionCache = /* @__PURE__ */ new Map();
|
|
1508
1523
|
var cacheClock3 = () => Date.now();
|
|
1509
1524
|
function buildResolutionCacheKey(params) {
|
|
1510
|
-
return
|
|
1525
|
+
return [
|
|
1526
|
+
params.repoRoot,
|
|
1527
|
+
params.currentBranch ?? "",
|
|
1528
|
+
params.hasApi ? "1" : "0",
|
|
1529
|
+
params.currentAppId ?? "",
|
|
1530
|
+
params.upstreamAppId ?? "",
|
|
1531
|
+
params.laneId ?? ""
|
|
1532
|
+
].join("|");
|
|
1511
1533
|
}
|
|
1512
1534
|
function readResolutionCache(key) {
|
|
1513
1535
|
const entry = resolutionCache.get(key);
|
|
@@ -1527,7 +1549,10 @@ async function resolveActiveLaneBinding(params) {
|
|
|
1527
1549
|
const cacheKey = buildResolutionCacheKey({
|
|
1528
1550
|
repoRoot: params.repoRoot,
|
|
1529
1551
|
currentBranch: state ? normalizeBranchName(state.currentBranch) : null,
|
|
1530
|
-
hasApi: Boolean(params.api)
|
|
1552
|
+
hasApi: Boolean(params.api),
|
|
1553
|
+
currentAppId: state?.binding?.currentAppId ?? null,
|
|
1554
|
+
upstreamAppId: state?.binding?.upstreamAppId ?? null,
|
|
1555
|
+
laneId: state?.binding?.laneId ?? null
|
|
1531
1556
|
});
|
|
1532
1557
|
const cached = readResolutionCache(cacheKey);
|
|
1533
1558
|
if (cached) return cached;
|
|
@@ -1567,7 +1592,9 @@ async function resolveActiveLaneBindingUncached(params, state) {
|
|
|
1567
1592
|
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
1568
1593
|
remoteUrl: state.remoteUrl ?? void 0,
|
|
1569
1594
|
defaultBranch: state.defaultBranch ?? void 0,
|
|
1570
|
-
branchName: currentBranch
|
|
1595
|
+
branchName: currentBranch,
|
|
1596
|
+
currentAppId: localBinding.currentAppId,
|
|
1597
|
+
upstreamAppId: localBinding.upstreamAppId
|
|
1571
1598
|
});
|
|
1572
1599
|
const lane2 = unwrapResponseObject(laneResp2, "project lane binding");
|
|
1573
1600
|
if (lane2.status === "ambiguous_family_selection") {
|
|
@@ -1647,7 +1674,9 @@ async function resolveActiveLaneBindingUncached(params, state) {
|
|
|
1647
1674
|
repoFingerprint: state.repoFingerprint ?? void 0,
|
|
1648
1675
|
remoteUrl: state.remoteUrl ?? void 0,
|
|
1649
1676
|
defaultBranch: state.defaultBranch ?? void 0,
|
|
1650
|
-
branchName: currentBranch
|
|
1677
|
+
branchName: currentBranch,
|
|
1678
|
+
currentAppId: state.binding?.currentAppId,
|
|
1679
|
+
upstreamAppId: state.binding?.upstreamAppId
|
|
1651
1680
|
});
|
|
1652
1681
|
const lane = unwrapResponseObject(laneResp, "project lane binding");
|
|
1653
1682
|
if (lane.status === "ambiguous_family_selection") {
|
|
@@ -1768,6 +1797,21 @@ function buildBaseState() {
|
|
|
1768
1797
|
}
|
|
1769
1798
|
};
|
|
1770
1799
|
}
|
|
1800
|
+
async function refreshEquivalentRevisionBaseline(params) {
|
|
1801
|
+
return writeLocalBaseline({
|
|
1802
|
+
repoRoot: params.repoRoot,
|
|
1803
|
+
repoFingerprint: params.binding.repoFingerprint,
|
|
1804
|
+
laneId: params.binding.laneId,
|
|
1805
|
+
currentAppId: params.binding.currentAppId,
|
|
1806
|
+
branchName: params.binding.branchName,
|
|
1807
|
+
lastSnapshotId: params.baseline.lastSnapshotId,
|
|
1808
|
+
lastSnapshotHash: params.baseline.lastSnapshotHash,
|
|
1809
|
+
lastServerRevisionId: params.appHead.headRevisionId ?? null,
|
|
1810
|
+
lastServerTreeHash: params.appHead.treeHash ?? params.baseline.lastServerTreeHash ?? null,
|
|
1811
|
+
lastServerHeadHash: params.appHead.headCommitHash,
|
|
1812
|
+
lastSeenLocalCommitHash: params.baseline.lastSeenLocalCommitHash
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1771
1815
|
async function collabDetectRepoState(params) {
|
|
1772
1816
|
const detected = buildBaseState();
|
|
1773
1817
|
let repoRoot;
|
|
@@ -1963,22 +2007,15 @@ async function collabDetectRepoState(params) {
|
|
|
1963
2007
|
);
|
|
1964
2008
|
const equivalentRevisionDrift = revisionChanged && !serverHeadChanged;
|
|
1965
2009
|
if (equivalentRevisionDrift) {
|
|
1966
|
-
await
|
|
2010
|
+
const refreshedBaseline = await refreshEquivalentRevisionBaseline({
|
|
1967
2011
|
repoRoot,
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
branchName: binding.branchName,
|
|
1972
|
-
lastSnapshotId: baseline.lastSnapshotId,
|
|
1973
|
-
lastSnapshotHash: baseline.lastSnapshotHash,
|
|
1974
|
-
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
1975
|
-
lastServerTreeHash: appHead.treeHash ?? baseline.lastServerTreeHash ?? null,
|
|
1976
|
-
lastServerHeadHash: appHead.headCommitHash,
|
|
1977
|
-
lastSeenLocalCommitHash: baseline.lastSeenLocalCommitHash
|
|
2012
|
+
binding,
|
|
2013
|
+
baseline,
|
|
2014
|
+
appHead
|
|
1978
2015
|
});
|
|
1979
|
-
detected.baseline.lastServerRevisionId =
|
|
1980
|
-
detected.baseline.lastServerTreeHash =
|
|
1981
|
-
detected.baseline.lastServerHeadHash =
|
|
2016
|
+
detected.baseline.lastServerRevisionId = refreshedBaseline.lastServerRevisionId;
|
|
2017
|
+
detected.baseline.lastServerTreeHash = refreshedBaseline.lastServerTreeHash;
|
|
2018
|
+
detected.baseline.lastServerHeadHash = refreshedBaseline.lastServerHeadHash;
|
|
1982
2019
|
}
|
|
1983
2020
|
const serverChanged = serverHeadChanged;
|
|
1984
2021
|
if (!localChanged && !serverChanged) {
|
|
@@ -2005,13 +2042,38 @@ async function collabDetectRepoState(params) {
|
|
|
2005
2042
|
}
|
|
2006
2043
|
|
|
2007
2044
|
// src/infrastructure/git/gitEventHarvester.ts
|
|
2008
|
-
import { execa as execa2 } from "execa";
|
|
2009
2045
|
var GIT_TIMEOUT_MS = 1e4;
|
|
2010
2046
|
var GIT_MAX_BUFFER = 64 * 1024 * 1024;
|
|
2011
2047
|
var MAX_COMMITS = 100;
|
|
2012
2048
|
var RECORD_SEPARATOR = "";
|
|
2013
2049
|
var FIELD_SEPARATOR = "";
|
|
2014
2050
|
var LOG_FORMAT = `${RECORD_SEPARATOR}%H${FIELD_SEPARATOR}%P${FIELD_SEPARATOR}%an${FIELD_SEPARATOR}%ae${FIELD_SEPARATOR}%cn${FIELD_SEPARATOR}%ce${FIELD_SEPARATOR}%aI${FIELD_SEPARATOR}%cI${FIELD_SEPARATOR}%s`;
|
|
2051
|
+
function buildGitEventStatusArgs() {
|
|
2052
|
+
return ["status", "--porcelain=v1"];
|
|
2053
|
+
}
|
|
2054
|
+
function buildGitEventDiffNumstatArgs(params) {
|
|
2055
|
+
if (params.kind === "uncommitted") {
|
|
2056
|
+
return ["diff", "--numstat", "HEAD"];
|
|
2057
|
+
}
|
|
2058
|
+
return ["diff", "--numstat", "--no-renames", params.fromCommit, params.toCommit];
|
|
2059
|
+
}
|
|
2060
|
+
function buildGitEventReachabilityArgs(fromCommit, toCommit) {
|
|
2061
|
+
return ["merge-base", "--is-ancestor", fromCommit, toCommit];
|
|
2062
|
+
}
|
|
2063
|
+
function buildGitEventLogArgs(fromCommit, toCommit) {
|
|
2064
|
+
return [
|
|
2065
|
+
"log",
|
|
2066
|
+
"--first-parent",
|
|
2067
|
+
"-m",
|
|
2068
|
+
"--numstat",
|
|
2069
|
+
"--no-renames",
|
|
2070
|
+
`--format=${LOG_FORMAT}`,
|
|
2071
|
+
`${fromCommit}..${toCommit}`
|
|
2072
|
+
];
|
|
2073
|
+
}
|
|
2074
|
+
function buildGitEventNameRevArgs(secondParent) {
|
|
2075
|
+
return ["name-rev", "--name-only", "--no-undefined", secondParent];
|
|
2076
|
+
}
|
|
2015
2077
|
function sanitizedEnv() {
|
|
2016
2078
|
const env = { ...process.env };
|
|
2017
2079
|
delete env.GIT_DIR;
|
|
@@ -2020,21 +2082,16 @@ function sanitizedEnv() {
|
|
|
2020
2082
|
delete env.GIT_INDEX_FILE;
|
|
2021
2083
|
return env;
|
|
2022
2084
|
}
|
|
2023
|
-
async function runGitEvent(args, cwd) {
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
exitCode: res.exitCode ?? -1,
|
|
2034
|
-
stdout: String(res.stdout ?? ""),
|
|
2035
|
-
stderr: String(res.stderr ?? ""),
|
|
2036
|
-
timedOut: res.timedOut === true
|
|
2037
|
-
};
|
|
2085
|
+
async function runGitEvent(args, cwd, adapter = createSystemGitAdapter()) {
|
|
2086
|
+
return adapter.run(
|
|
2087
|
+
createGitCommandPlan({
|
|
2088
|
+
args,
|
|
2089
|
+
cwd,
|
|
2090
|
+
env: sanitizedEnv(),
|
|
2091
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
2092
|
+
maxBuffer: GIT_MAX_BUFFER
|
|
2093
|
+
})
|
|
2094
|
+
);
|
|
2038
2095
|
}
|
|
2039
2096
|
function parseNumstatLine(line) {
|
|
2040
2097
|
const tab1 = line.indexOf(" ");
|
|
@@ -2123,10 +2180,11 @@ function parseBranchFromMergeMessage(subject) {
|
|
|
2123
2180
|
}
|
|
2124
2181
|
return null;
|
|
2125
2182
|
}
|
|
2126
|
-
async function resolveBranchFromNameRev(secondParent, repoRoot) {
|
|
2183
|
+
async function resolveBranchFromNameRev(secondParent, repoRoot, adapter) {
|
|
2127
2184
|
const res = await runGitEvent(
|
|
2128
|
-
|
|
2129
|
-
repoRoot
|
|
2185
|
+
buildGitEventNameRevArgs(secondParent),
|
|
2186
|
+
repoRoot,
|
|
2187
|
+
adapter
|
|
2130
2188
|
);
|
|
2131
2189
|
if (res.exitCode !== 0 || res.timedOut) return null;
|
|
2132
2190
|
let name = res.stdout.trim();
|
|
@@ -2136,7 +2194,35 @@ async function resolveBranchFromNameRev(secondParent, repoRoot) {
|
|
|
2136
2194
|
if (name.includes("~") || name.includes("^") || name.startsWith("tags/") || name === "undefined") return null;
|
|
2137
2195
|
return name;
|
|
2138
2196
|
}
|
|
2139
|
-
|
|
2197
|
+
function summarizePorcelainStatus(stdout) {
|
|
2198
|
+
const summary = {
|
|
2199
|
+
filesChanged: 0,
|
|
2200
|
+
staged: 0,
|
|
2201
|
+
unstaged: 0,
|
|
2202
|
+
untracked: 0
|
|
2203
|
+
};
|
|
2204
|
+
const uniquePaths = /* @__PURE__ */ new Set();
|
|
2205
|
+
for (const rawLine of stdout.split("\n")) {
|
|
2206
|
+
if (!rawLine) continue;
|
|
2207
|
+
if (rawLine.startsWith("??")) {
|
|
2208
|
+
summary.untracked += 1;
|
|
2209
|
+
uniquePaths.add(rawLine.slice(3));
|
|
2210
|
+
continue;
|
|
2211
|
+
}
|
|
2212
|
+
if (rawLine.length < 3) continue;
|
|
2213
|
+
const indexCode = rawLine.charCodeAt(0);
|
|
2214
|
+
const worktreeCode = rawLine.charCodeAt(1);
|
|
2215
|
+
const indexCh = rawLine[0];
|
|
2216
|
+
const worktreeCh = rawLine[1];
|
|
2217
|
+
const pathPart = rawLine.slice(3);
|
|
2218
|
+
if (indexCh !== " " && indexCode !== 63) summary.staged += 1;
|
|
2219
|
+
if ((worktreeCh === "M" || worktreeCh === "D") && worktreeCode !== 63) summary.unstaged += 1;
|
|
2220
|
+
uniquePaths.add(pathPart);
|
|
2221
|
+
}
|
|
2222
|
+
summary.filesChanged = uniquePaths.size;
|
|
2223
|
+
return summary;
|
|
2224
|
+
}
|
|
2225
|
+
async function collectUncommittedSummary(repoRoot, adapter) {
|
|
2140
2226
|
const summary = {
|
|
2141
2227
|
filesChanged: 0,
|
|
2142
2228
|
linesAdded: 0,
|
|
@@ -2145,29 +2231,11 @@ async function collectUncommittedSummary(repoRoot) {
|
|
|
2145
2231
|
unstaged: 0,
|
|
2146
2232
|
untracked: 0
|
|
2147
2233
|
};
|
|
2148
|
-
const statusRes = await runGitEvent(
|
|
2234
|
+
const statusRes = await runGitEvent(buildGitEventStatusArgs(), repoRoot, adapter);
|
|
2149
2235
|
if (statusRes.exitCode === 0 && !statusRes.timedOut) {
|
|
2150
|
-
|
|
2151
|
-
for (const rawLine of statusRes.stdout.split("\n")) {
|
|
2152
|
-
if (!rawLine) continue;
|
|
2153
|
-
if (rawLine.startsWith("??")) {
|
|
2154
|
-
summary.untracked += 1;
|
|
2155
|
-
uniquePaths.add(rawLine.slice(3));
|
|
2156
|
-
continue;
|
|
2157
|
-
}
|
|
2158
|
-
if (rawLine.length < 3) continue;
|
|
2159
|
-
const indexCode = rawLine.charCodeAt(0);
|
|
2160
|
-
const worktreeCode = rawLine.charCodeAt(1);
|
|
2161
|
-
const indexCh = rawLine[0];
|
|
2162
|
-
const worktreeCh = rawLine[1];
|
|
2163
|
-
const pathPart = rawLine.slice(3);
|
|
2164
|
-
if (indexCh !== " " && indexCode !== 63) summary.staged += 1;
|
|
2165
|
-
if ((worktreeCh === "M" || worktreeCh === "D") && worktreeCode !== 63) summary.unstaged += 1;
|
|
2166
|
-
uniquePaths.add(pathPart);
|
|
2167
|
-
}
|
|
2168
|
-
summary.filesChanged = uniquePaths.size;
|
|
2236
|
+
Object.assign(summary, summarizePorcelainStatus(statusRes.stdout));
|
|
2169
2237
|
}
|
|
2170
|
-
const diffRes = await runGitEvent(
|
|
2238
|
+
const diffRes = await runGitEvent(buildGitEventDiffNumstatArgs({ kind: "uncommitted" }), repoRoot, adapter);
|
|
2171
2239
|
if (diffRes.exitCode === 0 && !diffRes.timedOut) {
|
|
2172
2240
|
for (const rawLine of diffRes.stdout.split("\n")) {
|
|
2173
2241
|
if (!rawLine) continue;
|
|
@@ -2200,7 +2268,8 @@ function buildEmptyPreTurnEvents(params) {
|
|
|
2200
2268
|
}
|
|
2201
2269
|
async function collectPreTurnGitEvents(params) {
|
|
2202
2270
|
try {
|
|
2203
|
-
const
|
|
2271
|
+
const git = params.git ?? createSystemGitAdapter();
|
|
2272
|
+
const uncommitted = await collectUncommittedSummary(params.repoRoot, git);
|
|
2204
2273
|
if (!params.fromCommit || params.fromCommit === params.toCommit) {
|
|
2205
2274
|
return buildEmptyPreTurnEvents({
|
|
2206
2275
|
fromCommit: params.fromCommit,
|
|
@@ -2210,8 +2279,9 @@ async function collectPreTurnGitEvents(params) {
|
|
|
2210
2279
|
});
|
|
2211
2280
|
}
|
|
2212
2281
|
const reachabilityRes = await runGitEvent(
|
|
2213
|
-
|
|
2214
|
-
params.repoRoot
|
|
2282
|
+
buildGitEventReachabilityArgs(params.fromCommit, params.toCommit),
|
|
2283
|
+
params.repoRoot,
|
|
2284
|
+
git
|
|
2215
2285
|
);
|
|
2216
2286
|
if (reachabilityRes.timedOut) return null;
|
|
2217
2287
|
if (reachabilityRes.exitCode === 1) {
|
|
@@ -2224,16 +2294,9 @@ async function collectPreTurnGitEvents(params) {
|
|
|
2224
2294
|
}
|
|
2225
2295
|
if (reachabilityRes.exitCode !== 0) return null;
|
|
2226
2296
|
const logRes = await runGitEvent(
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
"-m",
|
|
2231
|
-
"--numstat",
|
|
2232
|
-
"--no-renames",
|
|
2233
|
-
`--format=${LOG_FORMAT}`,
|
|
2234
|
-
`${params.fromCommit}..${params.toCommit}`
|
|
2235
|
-
],
|
|
2236
|
-
params.repoRoot
|
|
2297
|
+
buildGitEventLogArgs(params.fromCommit, params.toCommit),
|
|
2298
|
+
params.repoRoot,
|
|
2299
|
+
git
|
|
2237
2300
|
);
|
|
2238
2301
|
if (logRes.timedOut || logRes.exitCode !== 0) return null;
|
|
2239
2302
|
const rawCommits = parseLogOutput(logRes.stdout);
|
|
@@ -2248,7 +2311,7 @@ async function collectPreTurnGitEvents(params) {
|
|
|
2248
2311
|
if (raw.parents.length < 2) continue;
|
|
2249
2312
|
const secondParent = raw.parents[1];
|
|
2250
2313
|
const fromBranchFromMessage = parseBranchFromMergeMessage(raw.subject);
|
|
2251
|
-
const fromBranchFromNameRev = await resolveBranchFromNameRev(secondParent, params.repoRoot);
|
|
2314
|
+
const fromBranchFromNameRev = await resolveBranchFromNameRev(secondParent, params.repoRoot, git);
|
|
2252
2315
|
merges.push({
|
|
2253
2316
|
hash: raw.hash,
|
|
2254
2317
|
shortHash: raw.hash.slice(0, 7),
|
|
@@ -2265,8 +2328,9 @@ async function collectPreTurnGitEvents(params) {
|
|
|
2265
2328
|
let totalLinesAdded = 0;
|
|
2266
2329
|
let totalLinesRemoved = 0;
|
|
2267
2330
|
const diffRes = await runGitEvent(
|
|
2268
|
-
|
|
2269
|
-
params.repoRoot
|
|
2331
|
+
buildGitEventDiffNumstatArgs({ kind: "range", fromCommit: params.fromCommit, toCommit: params.toCommit }),
|
|
2332
|
+
params.repoRoot,
|
|
2333
|
+
git
|
|
2270
2334
|
);
|
|
2271
2335
|
if (diffRes.exitCode === 0 && !diffRes.timedOut) {
|
|
2272
2336
|
const uniquePaths = /* @__PURE__ */ new Set();
|
|
@@ -2317,13 +2381,184 @@ async function collectPreTurnGitEvents(params) {
|
|
|
2317
2381
|
}
|
|
2318
2382
|
}
|
|
2319
2383
|
|
|
2384
|
+
// src/application/collab/jobRunner.ts
|
|
2385
|
+
async function drainClaimedJobs(params) {
|
|
2386
|
+
await params.prune();
|
|
2387
|
+
const jobs = await params.list();
|
|
2388
|
+
const results = [];
|
|
2389
|
+
for (const listedJob of jobs) {
|
|
2390
|
+
if (params.isTerminal(listedJob)) continue;
|
|
2391
|
+
const claimed = await params.claim(listedJob);
|
|
2392
|
+
if (!claimed) continue;
|
|
2393
|
+
try {
|
|
2394
|
+
const result = await params.process(claimed);
|
|
2395
|
+
results.push(result);
|
|
2396
|
+
await params.afterSuccess?.({ listedJob, job: claimed.job, result });
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
await claimed.release();
|
|
2399
|
+
if (params.onError === "throw") throw error;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
return results;
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// src/application/collab/workflowState.ts
|
|
2406
|
+
function asyncJobWorkflowState(status) {
|
|
2407
|
+
if (status === "completed" || status === "failed" || status === "queued") return status;
|
|
2408
|
+
return "processing";
|
|
2409
|
+
}
|
|
2410
|
+
function planAsyncJobDrainTransition(job) {
|
|
2411
|
+
const state = asyncJobWorkflowState(job.status);
|
|
2412
|
+
if (state === "completed" || state === "failed") {
|
|
2413
|
+
return { workflow: "async_job_drain", state, action: "skip_terminal", terminal: true };
|
|
2414
|
+
}
|
|
2415
|
+
return {
|
|
2416
|
+
workflow: "async_job_drain",
|
|
2417
|
+
state,
|
|
2418
|
+
action: job.kind === "init" ? "process_init" : "process_init_post",
|
|
2419
|
+
terminal: false
|
|
2420
|
+
};
|
|
2421
|
+
}
|
|
2422
|
+
function planFinalizeQueueTransition(job) {
|
|
2423
|
+
if (job.status === "completed" || job.status === "failed" && job.metadata.failureDisposition === "terminal") {
|
|
2424
|
+
return { workflow: "finalize", state: job.status, action: "skip_terminal", terminal: true };
|
|
2425
|
+
}
|
|
2426
|
+
if (job.status === "failed") {
|
|
2427
|
+
return { workflow: "finalize", state: "retryable_failed", action: "claim_and_process", terminal: false };
|
|
2428
|
+
}
|
|
2429
|
+
return { workflow: "finalize", state: job.status, action: "claim_and_process", terminal: false };
|
|
2430
|
+
}
|
|
2431
|
+
function planCheckoutWorkflowTransition(params) {
|
|
2432
|
+
if (!params.appId?.trim()) {
|
|
2433
|
+
return { workflow: "checkout", state: "failed", action: "require_app", terminal: true };
|
|
2434
|
+
}
|
|
2435
|
+
return { workflow: "checkout", state: "selected", action: "checkout", terminal: false };
|
|
2436
|
+
}
|
|
2437
|
+
function planApproveWorkflowTransition(params) {
|
|
2438
|
+
if (params.mode === "sync-target-repo") {
|
|
2439
|
+
return params.hasCwd ? { workflow: "approve", state: "sync_target_repo", action: "sync_target_repo", terminal: false } : { workflow: "approve", state: "failed", action: "require_cwd", terminal: true };
|
|
2440
|
+
}
|
|
2441
|
+
return { workflow: "approve", state: "remote_only", action: "approve_remote", terminal: false };
|
|
2442
|
+
}
|
|
2443
|
+
function planRequestMergeWorkflowTransition(params) {
|
|
2444
|
+
if (!params.hasBinding) {
|
|
2445
|
+
return { workflow: "request_merge", state: "failed", action: "require_binding", terminal: true };
|
|
2446
|
+
}
|
|
2447
|
+
if (params.hasPendingFinalize) {
|
|
2448
|
+
return { workflow: "request_merge", state: "pending_finalize", action: "require_finalize_drain", terminal: true };
|
|
2449
|
+
}
|
|
2450
|
+
return { workflow: "request_merge", state: "bound", action: "open_merge_request", terminal: false };
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/application/collab/finalizeQueueRunner.ts
|
|
2454
|
+
async function drainPendingFinalizeQueueJobs(params) {
|
|
2455
|
+
return drainClaimedJobs({
|
|
2456
|
+
prune: prunePendingFinalizeJobs,
|
|
2457
|
+
list: listPendingFinalizeJobs,
|
|
2458
|
+
isTerminal: (job) => planFinalizeQueueTransition(job).terminal,
|
|
2459
|
+
claim: (job) => claimPendingFinalizeJob(job.id),
|
|
2460
|
+
process: (claimed) => params.processClaimedJob({
|
|
2461
|
+
api: params.api,
|
|
2462
|
+
job: claimed.job,
|
|
2463
|
+
release: claimed.release
|
|
2464
|
+
}),
|
|
2465
|
+
afterSuccess: async ({ listedJob }) => {
|
|
2466
|
+
await removePendingFinalizeJob(listedJob.id);
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// src/application/collab/finalizeWorkspaceMetadata.ts
|
|
2472
|
+
function buildWorkspaceMetadata(params) {
|
|
2473
|
+
const metadata = {
|
|
2474
|
+
branch: params.branchName,
|
|
2475
|
+
repoRoot: params.repoRoot,
|
|
2476
|
+
remoteUrl: params.remoteUrl,
|
|
2477
|
+
defaultBranch: params.defaultBranch,
|
|
2478
|
+
recordingMode: "boundary_delta",
|
|
2479
|
+
baselineSnapshotId: params.baselineSnapshotId,
|
|
2480
|
+
currentSnapshotId: params.currentSnapshotId,
|
|
2481
|
+
baselineServerRevisionId: params.baselineServerRevisionId ?? null,
|
|
2482
|
+
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
2483
|
+
currentSnapshotHash: params.currentSnapshotHash,
|
|
2484
|
+
localCommitHash: params.localCommitHash,
|
|
2485
|
+
repoStateAtCapture: params.repoState,
|
|
2486
|
+
replayedFromBaseHash: params.replayedFromBaseHash ?? null,
|
|
2487
|
+
previousLocalCommitHash: params.previousLocalCommitHash ?? null
|
|
2488
|
+
};
|
|
2489
|
+
if (params.preTurnEvents) {
|
|
2490
|
+
metadata.preTurnEvents = params.preTurnEvents;
|
|
2491
|
+
}
|
|
2492
|
+
if (params.turnUsage) {
|
|
2493
|
+
metadata.turnUsage = params.turnUsage;
|
|
2494
|
+
}
|
|
2495
|
+
if (typeof params.promptedAt === "string" && params.promptedAt.trim()) {
|
|
2496
|
+
metadata.promptedAt = params.promptedAt.trim();
|
|
2497
|
+
}
|
|
2498
|
+
return metadata;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// src/application/collab/finalizeDecision.ts
|
|
2502
|
+
var FINALIZE_AUTO_TERMINAL_THRESHOLD = 5;
|
|
2503
|
+
function classifyFinalizeError(error) {
|
|
2504
|
+
const tagged = error;
|
|
2505
|
+
return {
|
|
2506
|
+
disposition: tagged.finalizeDisposition ?? "retryable",
|
|
2507
|
+
reason: tagged.finalizeReason ?? "unknown",
|
|
2508
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2509
|
+
};
|
|
2510
|
+
}
|
|
2511
|
+
function computeFailureEscalation(job, reason) {
|
|
2512
|
+
const previousReason = job.metadata.consecutiveFailureReason;
|
|
2513
|
+
const previousCountRaw = job.metadata.consecutiveFailures;
|
|
2514
|
+
const previousCount = typeof previousCountRaw === "number" && Number.isFinite(previousCountRaw) && previousCountRaw > 0 ? Math.floor(previousCountRaw) : 0;
|
|
2515
|
+
const sameReason = previousReason === reason;
|
|
2516
|
+
const next = sameReason ? previousCount + 1 : 1;
|
|
2517
|
+
return {
|
|
2518
|
+
consecutiveFailures: next,
|
|
2519
|
+
consecutiveFailureReason: reason,
|
|
2520
|
+
shouldEscalateToTerminal: next >= FINALIZE_AUTO_TERMINAL_THRESHOLD
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
function hasFinalizeBaselineDrifted(params) {
|
|
2524
|
+
return params.liveSnapshotId !== params.jobSnapshotId || (params.jobServerRevisionId ? params.liveServerRevisionId !== params.jobServerRevisionId : false) || params.liveServerHeadHash !== params.jobServerHeadHash;
|
|
2525
|
+
}
|
|
2526
|
+
function canSalvageNoDiffFinalize(params) {
|
|
2527
|
+
if (params.baselineDrifted && params.currentSnapshotHash !== params.liveBaselineSnapshotHash) return false;
|
|
2528
|
+
return params.serverStillAtBaseline;
|
|
2529
|
+
}
|
|
2530
|
+
function classifyChangedFinalizeDrift(params) {
|
|
2531
|
+
if (!params.baselineDrifted) return "no_drift";
|
|
2532
|
+
return params.localBaselineAdvanced && params.serverHeadAdvanced ? "consistent_advance" : "inconsistent_drift";
|
|
2533
|
+
}
|
|
2534
|
+
function shouldReplayChangedFinalize(params) {
|
|
2535
|
+
return params.currentServerHeadHash !== params.submissionBaseHeadHash || params.baselineDrifted;
|
|
2536
|
+
}
|
|
2537
|
+
function shouldAdoptExistingChangeStepBeforeReplay(params) {
|
|
2538
|
+
return params.replayNeeded && Boolean(params.idempotencyKey?.trim());
|
|
2539
|
+
}
|
|
2540
|
+
function validateSucceededChangeStepForBaseline(params) {
|
|
2541
|
+
const nextServerHeadHash = typeof params.headCommitHash === "string" ? params.headCommitHash.trim() : "";
|
|
2542
|
+
if (!nextServerHeadHash) {
|
|
2543
|
+
throw new RemixError("Backend returned a succeeded change step without a head commit hash.", {
|
|
2544
|
+
exitCode: 1,
|
|
2545
|
+
hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again."
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
const nextServerRevisionId = typeof params.resultRevisionId === "string" ? params.resultRevisionId.trim() : "";
|
|
2549
|
+
return {
|
|
2550
|
+
nextServerHeadHash,
|
|
2551
|
+
nextServerRevisionId: nextServerRevisionId || null,
|
|
2552
|
+
needsFreshHeadVerification: !nextServerRevisionId
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2320
2556
|
// src/application/collab/collabFinalizeProcessing.ts
|
|
2321
2557
|
var FINALIZE_RETRY_BASE_DELAY_MS = 15e3;
|
|
2322
2558
|
var FINALIZE_RETRY_MAX_DELAY_MS = 5 * 60 * 1e3;
|
|
2323
2559
|
var FINALIZE_JOB_PROCESSING_TIMEOUT_MS_DEFAULT = 90 * 1e3;
|
|
2324
2560
|
var finalizeJobProcessingTimeoutMs = FINALIZE_JOB_PROCESSING_TIMEOUT_MS_DEFAULT;
|
|
2325
2561
|
var FINALIZE_LOCK_HEARTBEAT_MS = Math.max(15e3, Math.floor(FINALIZE_JOB_LOCK_STALE_MS / 3));
|
|
2326
|
-
var FINALIZE_AUTO_TERMINAL_THRESHOLD = 5;
|
|
2327
2562
|
function readMetadataString(job, key) {
|
|
2328
2563
|
const value = job.metadata[key];
|
|
2329
2564
|
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
@@ -2350,14 +2585,6 @@ function buildFinalizeCliError(params) {
|
|
|
2350
2585
|
error.finalizeReason = params.reason;
|
|
2351
2586
|
return error;
|
|
2352
2587
|
}
|
|
2353
|
-
function classifyFinalizeError(error) {
|
|
2354
|
-
const tagged = error;
|
|
2355
|
-
return {
|
|
2356
|
-
disposition: tagged.finalizeDisposition ?? "retryable",
|
|
2357
|
-
reason: tagged.finalizeReason ?? "unknown",
|
|
2358
|
-
message: error instanceof Error ? error.message : String(error)
|
|
2359
|
-
};
|
|
2360
|
-
}
|
|
2361
2588
|
function runWithFinalizeTimeout(work, jobId) {
|
|
2362
2589
|
let timer;
|
|
2363
2590
|
const cap = finalizeJobProcessingTimeoutMs;
|
|
@@ -2388,46 +2615,6 @@ function startLockHeartbeat(jobId) {
|
|
|
2388
2615
|
handle.unref?.();
|
|
2389
2616
|
return () => clearInterval(handle);
|
|
2390
2617
|
}
|
|
2391
|
-
function computeFailureEscalation(job, reason) {
|
|
2392
|
-
const previousReason = job.metadata.consecutiveFailureReason;
|
|
2393
|
-
const previousCountRaw = job.metadata.consecutiveFailures;
|
|
2394
|
-
const previousCount = typeof previousCountRaw === "number" && Number.isFinite(previousCountRaw) && previousCountRaw > 0 ? Math.floor(previousCountRaw) : 0;
|
|
2395
|
-
const sameReason = previousReason === reason;
|
|
2396
|
-
const next = sameReason ? previousCount + 1 : 1;
|
|
2397
|
-
return {
|
|
2398
|
-
consecutiveFailures: next,
|
|
2399
|
-
consecutiveFailureReason: reason,
|
|
2400
|
-
shouldEscalateToTerminal: next >= FINALIZE_AUTO_TERMINAL_THRESHOLD
|
|
2401
|
-
};
|
|
2402
|
-
}
|
|
2403
|
-
function buildWorkspaceMetadata(params) {
|
|
2404
|
-
const metadata = {
|
|
2405
|
-
branch: params.branchName,
|
|
2406
|
-
repoRoot: params.repoRoot,
|
|
2407
|
-
remoteUrl: params.remoteUrl,
|
|
2408
|
-
defaultBranch: params.defaultBranch,
|
|
2409
|
-
recordingMode: "boundary_delta",
|
|
2410
|
-
baselineSnapshotId: params.baselineSnapshotId,
|
|
2411
|
-
currentSnapshotId: params.currentSnapshotId,
|
|
2412
|
-
baselineServerRevisionId: params.baselineServerRevisionId ?? null,
|
|
2413
|
-
baselineServerHeadHash: params.baselineServerHeadHash,
|
|
2414
|
-
currentSnapshotHash: params.currentSnapshotHash,
|
|
2415
|
-
localCommitHash: params.localCommitHash,
|
|
2416
|
-
repoStateAtCapture: params.repoState,
|
|
2417
|
-
replayedFromBaseHash: params.replayedFromBaseHash ?? null,
|
|
2418
|
-
previousLocalCommitHash: params.previousLocalCommitHash ?? null
|
|
2419
|
-
};
|
|
2420
|
-
if (params.preTurnEvents) {
|
|
2421
|
-
metadata.preTurnEvents = params.preTurnEvents;
|
|
2422
|
-
}
|
|
2423
|
-
if (params.turnUsage) {
|
|
2424
|
-
metadata.turnUsage = params.turnUsage;
|
|
2425
|
-
}
|
|
2426
|
-
if (typeof params.promptedAt === "string" && params.promptedAt.trim()) {
|
|
2427
|
-
metadata.promptedAt = params.promptedAt.trim();
|
|
2428
|
-
}
|
|
2429
|
-
return metadata;
|
|
2430
|
-
}
|
|
2431
2618
|
async function findExistingChangeStepByIdempotency(params) {
|
|
2432
2619
|
const idempotencyKey = params.idempotencyKey?.trim();
|
|
2433
2620
|
if (!idempotencyKey) return null;
|
|
@@ -2440,19 +2627,28 @@ async function findExistingChangeStepByIdempotency(params) {
|
|
|
2440
2627
|
return steps.find((step) => step.idempotencyKey === idempotencyKey) ?? null;
|
|
2441
2628
|
}
|
|
2442
2629
|
async function writeBaselineFromSucceededChangeStep(params) {
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
hint: "This is a backend invariant violation; retry will not help. Run `remix collab status` before trying again.",
|
|
2449
|
-
disposition: "terminal",
|
|
2450
|
-
reason: "missing_head_commit_hash"
|
|
2630
|
+
let baselineAdvance;
|
|
2631
|
+
try {
|
|
2632
|
+
baselineAdvance = validateSucceededChangeStepForBaseline({
|
|
2633
|
+
headCommitHash: params.changeStep.headCommitHash,
|
|
2634
|
+
resultRevisionId: params.changeStep.resultRevisionId
|
|
2451
2635
|
});
|
|
2636
|
+
} catch (error) {
|
|
2637
|
+
if (error instanceof RemixError) {
|
|
2638
|
+
throw buildFinalizeCliError({
|
|
2639
|
+
message: error.message,
|
|
2640
|
+
exitCode: error.exitCode,
|
|
2641
|
+
hint: error.hint ?? void 0,
|
|
2642
|
+
disposition: "terminal",
|
|
2643
|
+
reason: "missing_head_commit_hash"
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
throw error;
|
|
2452
2647
|
}
|
|
2453
|
-
|
|
2648
|
+
const nextServerHeadHash = baselineAdvance.nextServerHeadHash;
|
|
2649
|
+
let nextServerRevisionId = baselineAdvance.nextServerRevisionId ?? "";
|
|
2454
2650
|
let nextServerTreeHash = null;
|
|
2455
|
-
if (
|
|
2651
|
+
if (baselineAdvance.needsFreshHeadVerification) {
|
|
2456
2652
|
const freshHeadResp = await params.api.getAppHead(params.job.currentAppId);
|
|
2457
2653
|
const freshHead = unwrapResponseObject(freshHeadResp, "app head");
|
|
2458
2654
|
if (freshHead.headCommitHash !== nextServerHeadHash || !freshHead.headRevisionId) {
|
|
@@ -2546,7 +2742,14 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2546
2742
|
reason: "baseline_missing"
|
|
2547
2743
|
});
|
|
2548
2744
|
}
|
|
2549
|
-
const baselineDrifted =
|
|
2745
|
+
const baselineDrifted = hasFinalizeBaselineDrifted({
|
|
2746
|
+
liveSnapshotId: baseline.lastSnapshotId,
|
|
2747
|
+
jobSnapshotId: job.baselineSnapshotId,
|
|
2748
|
+
liveServerRevisionId: baseline.lastServerRevisionId,
|
|
2749
|
+
jobServerRevisionId: job.baselineServerRevisionId,
|
|
2750
|
+
liveServerHeadHash: baseline.lastServerHeadHash,
|
|
2751
|
+
jobServerHeadHash: job.baselineServerHeadHash
|
|
2752
|
+
});
|
|
2550
2753
|
const appHead = unwrapResponseObject(appHeadResp, "app head");
|
|
2551
2754
|
const remoteUrl = readMetadataString(job, "remoteUrl");
|
|
2552
2755
|
const defaultBranch = readMetadataString(job, "defaultBranch");
|
|
@@ -2565,7 +2768,13 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2565
2768
|
targetSnapshotId: job.currentSnapshotId
|
|
2566
2769
|
});
|
|
2567
2770
|
if (!diffResult.diff.trim()) {
|
|
2568
|
-
|
|
2771
|
+
const serverStillAtBaseline = job.baselineServerRevisionId ? appHead.headRevisionId === job.baselineServerRevisionId : appHead.headCommitHash === job.baselineServerHeadHash;
|
|
2772
|
+
if (baselineDrifted && !canSalvageNoDiffFinalize({
|
|
2773
|
+
baselineDrifted,
|
|
2774
|
+
currentSnapshotHash: snapshot.snapshotHash,
|
|
2775
|
+
liveBaselineSnapshotHash: baseline.lastSnapshotHash,
|
|
2776
|
+
serverStillAtBaseline: true
|
|
2777
|
+
})) {
|
|
2569
2778
|
throw buildFinalizeCliError({
|
|
2570
2779
|
message: "Finalize queue baseline drifted before this job was processed.",
|
|
2571
2780
|
exitCode: 1,
|
|
@@ -2574,8 +2783,7 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2574
2783
|
reason: "baseline_drifted"
|
|
2575
2784
|
});
|
|
2576
2785
|
}
|
|
2577
|
-
|
|
2578
|
-
if (!serverStillAtBaseline) {
|
|
2786
|
+
if (!canSalvageNoDiffFinalize({ baselineDrifted: false, currentSnapshotHash: "", liveBaselineSnapshotHash: "", serverStillAtBaseline })) {
|
|
2579
2787
|
throw buildFinalizeCliError({
|
|
2580
2788
|
message: "Server lane changed before a no-diff turn could be recorded.",
|
|
2581
2789
|
exitCode: 2,
|
|
@@ -2641,9 +2849,9 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2641
2849
|
}
|
|
2642
2850
|
const localBaselineAdvanced = baseline.lastSnapshotId !== job.baselineSnapshotId;
|
|
2643
2851
|
const serverHeadAdvanced = job.baselineServerRevisionId ? appHead.headRevisionId !== job.baselineServerRevisionId : appHead.headCommitHash !== job.baselineServerHeadHash;
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
if (
|
|
2852
|
+
const changedDrift = classifyChangedFinalizeDrift({ baselineDrifted, localBaselineAdvanced, serverHeadAdvanced });
|
|
2853
|
+
if (changedDrift !== "no_drift") {
|
|
2854
|
+
if (changedDrift === "inconsistent_drift") {
|
|
2647
2855
|
throw buildFinalizeCliError({
|
|
2648
2856
|
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.`,
|
|
2649
2857
|
exitCode: 1,
|
|
@@ -2665,8 +2873,12 @@ async function processClaimedPendingFinalizeJobInner(params) {
|
|
|
2665
2873
|
reason: "baseline_server_head_missing"
|
|
2666
2874
|
});
|
|
2667
2875
|
}
|
|
2668
|
-
const replayNeeded =
|
|
2669
|
-
|
|
2876
|
+
const replayNeeded = shouldReplayChangedFinalize({
|
|
2877
|
+
currentServerHeadHash: appHead.headCommitHash,
|
|
2878
|
+
submissionBaseHeadHash,
|
|
2879
|
+
baselineDrifted
|
|
2880
|
+
});
|
|
2881
|
+
if (shouldAdoptExistingChangeStepBeforeReplay({ replayNeeded, idempotencyKey: job.idempotencyKey })) {
|
|
2670
2882
|
const existingChangeStep = await findExistingChangeStepByIdempotency({
|
|
2671
2883
|
api: params.api,
|
|
2672
2884
|
appId: job.currentAppId,
|
|
@@ -2853,24 +3065,10 @@ async function enqueueCapturedFinalizeTurn(params) {
|
|
|
2853
3065
|
});
|
|
2854
3066
|
}
|
|
2855
3067
|
async function drainPendingFinalizeQueue(params) {
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
const claimed = await claimPendingFinalizeJob(job.id);
|
|
2861
|
-
if (!claimed) continue;
|
|
2862
|
-
try {
|
|
2863
|
-
const result = await processClaimedPendingFinalizeJob({
|
|
2864
|
-
api: params.api,
|
|
2865
|
-
job: claimed.job,
|
|
2866
|
-
release: claimed.release
|
|
2867
|
-
});
|
|
2868
|
-
results.push(result);
|
|
2869
|
-
await removePendingFinalizeJob(job.id);
|
|
2870
|
-
} catch {
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
return results;
|
|
3068
|
+
return drainPendingFinalizeQueueJobs({
|
|
3069
|
+
api: params.api,
|
|
3070
|
+
processClaimedJob: processClaimedPendingFinalizeJob
|
|
3071
|
+
});
|
|
2874
3072
|
}
|
|
2875
3073
|
|
|
2876
3074
|
// src/application/collab/collabFinalizeTurn.ts
|
|
@@ -3123,12 +3321,7 @@ function isFinalizePreflightFailureCode(value) {
|
|
|
3123
3321
|
}
|
|
3124
3322
|
|
|
3125
3323
|
// src/application/collab/recordingPreflight.ts
|
|
3126
|
-
|
|
3127
|
-
const detected = await collabDetectRepoState({
|
|
3128
|
-
api: params.api,
|
|
3129
|
-
cwd: params.cwd,
|
|
3130
|
-
allowBranchMismatch: params.allowBranchMismatch
|
|
3131
|
-
});
|
|
3324
|
+
function mapDetectedRepoStateToRecordingPreflight(detected) {
|
|
3132
3325
|
if (detected.status === "not_git_repo") {
|
|
3133
3326
|
return {
|
|
3134
3327
|
status: "not_git_repo",
|
|
@@ -3177,6 +3370,14 @@ async function collabRecordingPreflight(params) {
|
|
|
3177
3370
|
if (detected.repoState === "external_local_base_changed") return { status: "baseline_missing", ...base };
|
|
3178
3371
|
return { status: "ready", ...base };
|
|
3179
3372
|
}
|
|
3373
|
+
async function collabRecordingPreflight(params) {
|
|
3374
|
+
const detected = await collabDetectRepoState({
|
|
3375
|
+
api: params.api,
|
|
3376
|
+
cwd: params.cwd,
|
|
3377
|
+
allowBranchMismatch: params.allowBranchMismatch
|
|
3378
|
+
});
|
|
3379
|
+
return mapDetectedRepoStateToRecordingPreflight(detected);
|
|
3380
|
+
}
|
|
3180
3381
|
|
|
3181
3382
|
// src/infrastructure/locking/repoMutationLock.ts
|
|
3182
3383
|
import fs5 from "fs/promises";
|
|
@@ -3638,15 +3839,14 @@ async function collabSync(params) {
|
|
|
3638
3839
|
|
|
3639
3840
|
// src/application/collab/collabApprove.ts
|
|
3640
3841
|
async function collabApprove(params) {
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
}
|
|
3842
|
+
const transition = planApproveWorkflowTransition({
|
|
3843
|
+
mode: params.mode,
|
|
3844
|
+
hasCwd: Boolean(params.cwd?.trim())
|
|
3845
|
+
});
|
|
3846
|
+
if (transition.action === "sync_target_repo") {
|
|
3647
3847
|
return withRepoMutationLock(
|
|
3648
3848
|
{
|
|
3649
|
-
cwd: params.cwd,
|
|
3849
|
+
cwd: params.cwd ?? "",
|
|
3650
3850
|
operation: "collabApproveSyncTarget"
|
|
3651
3851
|
},
|
|
3652
3852
|
async ({ repoRoot, warnings }) => {
|
|
@@ -3705,6 +3905,11 @@ async function collabApprove(params) {
|
|
|
3705
3905
|
}
|
|
3706
3906
|
);
|
|
3707
3907
|
}
|
|
3908
|
+
if (transition.action === "require_cwd") {
|
|
3909
|
+
throw new RemixError("Working directory is required for `--sync-target-repo`.", {
|
|
3910
|
+
exitCode: 2
|
|
3911
|
+
});
|
|
3912
|
+
}
|
|
3708
3913
|
const resp = await params.api.updateMergeRequest(params.mrId, { status: "approved" });
|
|
3709
3914
|
const approvedMergeRequest = unwrapMergeRequest(resp);
|
|
3710
3915
|
const completedMergeRequest = await pollMergeRequestCompletion(params.api, params.mrId, {
|
|
@@ -3841,13 +4046,14 @@ async function materializeAppCheckout(params) {
|
|
|
3841
4046
|
|
|
3842
4047
|
// src/application/collab/collabCheckout.ts
|
|
3843
4048
|
async function collabCheckout(params) {
|
|
3844
|
-
const
|
|
3845
|
-
if (
|
|
4049
|
+
const transition = planCheckoutWorkflowTransition({ appId: params.appId });
|
|
4050
|
+
if (transition.action === "require_app") {
|
|
3846
4051
|
throw new RemixError("No app selected.", {
|
|
3847
4052
|
exitCode: 2,
|
|
3848
4053
|
hint: "Pass the app id to checkout."
|
|
3849
4054
|
});
|
|
3850
4055
|
}
|
|
4056
|
+
const appId = params.appId?.trim() ?? "";
|
|
3851
4057
|
const app = await pollAppReady(params.api, appId);
|
|
3852
4058
|
const collab = app.collab && typeof app.collab === "object" ? app.collab : null;
|
|
3853
4059
|
const source = app.source && typeof app.source === "object" ? app.source : null;
|
|
@@ -4101,38 +4307,16 @@ async function collabUpdateMemberRole(params) {
|
|
|
4101
4307
|
}
|
|
4102
4308
|
|
|
4103
4309
|
// src/application/collab/collabInit.ts
|
|
4104
|
-
import
|
|
4105
|
-
import
|
|
4106
|
-
import path9 from "path";
|
|
4107
|
-
|
|
4108
|
-
// src/infrastructure/collab/drainerLog.ts
|
|
4109
|
-
import fs7 from "fs/promises";
|
|
4110
|
-
import path8 from "path";
|
|
4111
|
-
async function logDrainerEvent(jobId, event, fields = {}) {
|
|
4112
|
-
const entry = {
|
|
4113
|
-
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4114
|
-
jobId,
|
|
4115
|
-
event,
|
|
4116
|
-
...fields
|
|
4117
|
-
};
|
|
4118
|
-
try {
|
|
4119
|
-
await fs7.mkdir(getLogsRoot(), { recursive: true });
|
|
4120
|
-
await fs7.appendFile(getDrainerLogPath(), `${JSON.stringify(entry)}
|
|
4121
|
-
`, "utf8");
|
|
4122
|
-
} catch {
|
|
4123
|
-
}
|
|
4124
|
-
}
|
|
4125
|
-
function drainerLogPath() {
|
|
4126
|
-
return getDrainerLogPath();
|
|
4127
|
-
}
|
|
4310
|
+
import fs11 from "fs/promises";
|
|
4311
|
+
import path10 from "path";
|
|
4128
4312
|
|
|
4129
4313
|
// src/shared/hash.ts
|
|
4130
4314
|
import crypto from "crypto";
|
|
4131
|
-
import
|
|
4315
|
+
import fs7 from "fs";
|
|
4132
4316
|
async function sha256FileHex(filePath) {
|
|
4133
4317
|
const hash = crypto.createHash("sha256");
|
|
4134
4318
|
await new Promise((resolve, reject) => {
|
|
4135
|
-
const stream =
|
|
4319
|
+
const stream = fs7.createReadStream(filePath);
|
|
4136
4320
|
stream.on("data", (chunk) => hash.update(chunk));
|
|
4137
4321
|
stream.on("error", reject);
|
|
4138
4322
|
stream.on("end", () => resolve());
|
|
@@ -4140,145 +4324,52 @@ async function sha256FileHex(filePath) {
|
|
|
4140
4324
|
return hash.digest("hex");
|
|
4141
4325
|
}
|
|
4142
4326
|
|
|
4143
|
-
// src/
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
async function uploadPresigned(params) {
|
|
4147
|
-
const stats = await fs9.promises.stat(params.filePath).catch(() => null);
|
|
4148
|
-
if (!stats || !stats.isFile()) {
|
|
4149
|
-
throw new RemixError("Upload file not found.", { exitCode: 2 });
|
|
4150
|
-
}
|
|
4151
|
-
const totalBytes = stats.size;
|
|
4152
|
-
const fileStream = fs9.createReadStream(params.filePath);
|
|
4153
|
-
const pass = new PassThrough();
|
|
4154
|
-
let sentBytes = 0;
|
|
4155
|
-
fileStream.on("data", (chunk) => {
|
|
4156
|
-
sentBytes += chunk.length;
|
|
4157
|
-
params.onProgress?.({ sentBytes, totalBytes });
|
|
4158
|
-
});
|
|
4159
|
-
fileStream.on("error", (err) => pass.destroy(err));
|
|
4160
|
-
fileStream.pipe(pass);
|
|
4161
|
-
const response = await fetch(params.uploadUrl, {
|
|
4162
|
-
method: "PUT",
|
|
4163
|
-
headers: params.headers,
|
|
4164
|
-
body: pass,
|
|
4165
|
-
duplex: "half"
|
|
4166
|
-
});
|
|
4167
|
-
if (!response.ok) {
|
|
4168
|
-
const text = await response.text().catch(() => "");
|
|
4169
|
-
throw new RemixError("Upload failed.", {
|
|
4170
|
-
exitCode: 1,
|
|
4171
|
-
hint: `Status: ${response.status}
|
|
4172
|
-
${text}`.trim() || null
|
|
4173
|
-
});
|
|
4174
|
-
}
|
|
4175
|
-
}
|
|
4176
|
-
|
|
4177
|
-
// src/application/collab/collabInit.ts
|
|
4178
|
-
function requireResolvedLaneBinding(lane, params) {
|
|
4179
|
-
if (lane.status === "resolved" && lane.currentAppId && lane.upstreamAppId) {
|
|
4180
|
-
return lane;
|
|
4181
|
-
}
|
|
4182
|
-
const branchLabel = params.branchName ?? "the current branch";
|
|
4183
|
-
const laneStatus = String(lane.status ?? "");
|
|
4184
|
-
throw new RemixError(`Failed to resolve a Remix lane for ${branchLabel}.`, {
|
|
4185
|
-
exitCode: 1,
|
|
4186
|
-
hint: laneStatus === "binding_not_found" ? `Run ${params.operation} again after the repository has been initialized.` : laneStatus === "ambiguous_family_selection" ? "Multiple canonical Remix families match this repository. Continue from a checkout already bound to the intended family, or rerun with `--force-new` to create a new family." : `Remix did not return complete lane metadata for ${branchLabel}.`
|
|
4187
|
-
});
|
|
4327
|
+
// src/application/collab/collabInitDecision.ts
|
|
4328
|
+
function shouldRequireCleanWorktreeForInit(params) {
|
|
4329
|
+
return Boolean(params.forceNew) || !params.hasExistingBinding;
|
|
4188
4330
|
}
|
|
4189
|
-
function
|
|
4190
|
-
|
|
4191
|
-
if (!payload || typeof payload !== "object") {
|
|
4192
|
-
return { status: "not_found" };
|
|
4193
|
-
}
|
|
4194
|
-
if (payload.status === "ambiguous_family_selection") {
|
|
4195
|
-
const projectIds = Array.isArray(payload.projectIds) ? payload.projectIds.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
4196
|
-
return {
|
|
4197
|
-
status: "ambiguous_family_selection",
|
|
4198
|
-
candidateCount: typeof payload.candidateCount === "number" && Number.isFinite(payload.candidateCount) ? payload.candidateCount : projectIds.length,
|
|
4199
|
-
projectIds
|
|
4200
|
-
};
|
|
4201
|
-
}
|
|
4202
|
-
if (payload.projectId && payload.appId) {
|
|
4203
|
-
return {
|
|
4204
|
-
status: "resolved",
|
|
4205
|
-
projectId: String(payload.projectId),
|
|
4206
|
-
appId: String(payload.appId),
|
|
4207
|
-
upstreamAppId: String(payload.upstreamAppId ?? payload.appId),
|
|
4208
|
-
threadId: payload.threadId ? String(payload.threadId) : null
|
|
4209
|
-
};
|
|
4210
|
-
}
|
|
4211
|
-
return { status: "not_found" };
|
|
4331
|
+
function classifyExistingInitBaseline(baseline) {
|
|
4332
|
+
return baseline?.lastSnapshotHash && (baseline.lastServerRevisionId || baseline.lastServerHeadHash) ? "existing" : null;
|
|
4212
4333
|
}
|
|
4213
|
-
function
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
exitCode: 2,
|
|
4219
|
-
hint: `Plain \`remix collab init\` cannot safely choose among ${familyCount} matching canonical families for repo fingerprint ${params.repoFingerprint}${remoteHint}.${projectHint} Run \`remix collab init --force-new\` to create a new canonical family, or continue from a checkout already bound to the intended family.`
|
|
4220
|
-
});
|
|
4221
|
-
}
|
|
4222
|
-
async function resolveOrEnsureLaneBinding(params) {
|
|
4223
|
-
const resolvePayload = {
|
|
4224
|
-
projectId: params.projectId ?? void 0,
|
|
4225
|
-
repoFingerprint: params.repoFingerprint,
|
|
4226
|
-
remoteUrl: params.remoteUrl ?? void 0,
|
|
4227
|
-
defaultBranch: params.defaultBranch ?? void 0,
|
|
4228
|
-
branchName: params.branchName
|
|
4229
|
-
};
|
|
4230
|
-
let lane = unwrapResponseObject(
|
|
4231
|
-
await params.api.resolveProjectLaneBinding(resolvePayload),
|
|
4232
|
-
"project lane binding"
|
|
4233
|
-
);
|
|
4234
|
-
if (lane.status !== "resolved") {
|
|
4235
|
-
lane = unwrapResponseObject(
|
|
4236
|
-
await params.api.ensureProjectLaneBinding({
|
|
4237
|
-
...resolvePayload,
|
|
4238
|
-
seedAppId: params.seedAppId ?? void 0,
|
|
4239
|
-
seedHeadCommitHash: params.seedHeadCommitHash ?? void 0
|
|
4240
|
-
}),
|
|
4241
|
-
"project lane binding"
|
|
4242
|
-
);
|
|
4243
|
-
}
|
|
4244
|
-
return requireResolvedLaneBinding(lane, {
|
|
4245
|
-
branchName: params.branchName,
|
|
4246
|
-
operation: params.operation
|
|
4247
|
-
});
|
|
4334
|
+
function classifyInitBaselineDelta(params) {
|
|
4335
|
+
if (params.equivalentBaselineStatus) return params.equivalentBaselineStatus;
|
|
4336
|
+
if (params.deltaStatus === "content_equivalent") return "seeded";
|
|
4337
|
+
if (params.deltaStatus === "up_to_date" || params.deltaStatus === "delta_ready") return "requires_sync";
|
|
4338
|
+
return null;
|
|
4248
4339
|
}
|
|
4249
|
-
function
|
|
4250
|
-
return
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
bindingMode: mode
|
|
4257
|
-
};
|
|
4340
|
+
function canSeedEquivalentBranchBaseline(params) {
|
|
4341
|
+
if (!params.branchName || !params.defaultBranch || params.branchName === params.defaultBranch) return false;
|
|
4342
|
+
if (!params.defaultBranchCurrentAppId) return false;
|
|
4343
|
+
if (params.upstreamAppId && params.defaultBranchCurrentAppId !== params.upstreamAppId) return false;
|
|
4344
|
+
if (!params.defaultBaselineSnapshotHash || !params.defaultBaselineServerHeadHash) return false;
|
|
4345
|
+
if (params.defaultBaselineServerHeadHash !== params.appHeadHash) return false;
|
|
4346
|
+
return params.localSnapshotHash === params.defaultBaselineSnapshotHash || params.localCommitHash === params.defaultBaselineLocalCommitHash;
|
|
4258
4347
|
}
|
|
4259
4348
|
function resolveStoredBindingAnchor(params) {
|
|
4260
4349
|
const currentBranchBinding = params.currentBranch ? params.state.branchBindings[params.currentBranch] ?? null : null;
|
|
4261
4350
|
if (currentBranchBinding?.currentAppId) {
|
|
4262
|
-
return {
|
|
4263
|
-
binding: currentBranchBinding,
|
|
4264
|
-
branchName: params.currentBranch
|
|
4265
|
-
};
|
|
4351
|
+
return { binding: currentBranchBinding, branchName: params.currentBranch };
|
|
4266
4352
|
}
|
|
4267
4353
|
const defaultBranchBinding = params.defaultBranch ? params.state.branchBindings[params.defaultBranch] ?? null : null;
|
|
4268
4354
|
if (defaultBranchBinding?.currentAppId) {
|
|
4269
|
-
return {
|
|
4270
|
-
binding: defaultBranchBinding,
|
|
4271
|
-
branchName: params.defaultBranch
|
|
4272
|
-
};
|
|
4355
|
+
return { binding: defaultBranchBinding, branchName: params.defaultBranch };
|
|
4273
4356
|
}
|
|
4274
4357
|
return null;
|
|
4275
4358
|
}
|
|
4359
|
+
function shouldReplaceAsyncInitJobs(params) {
|
|
4360
|
+
return Boolean(params.forceNew);
|
|
4361
|
+
}
|
|
4362
|
+
function shouldReturnPendingAsyncInit(params) {
|
|
4363
|
+
return !params.forceNew && params.pendingJobFound;
|
|
4364
|
+
}
|
|
4365
|
+
|
|
4366
|
+
// src/application/collab/collabInitBaseline.ts
|
|
4276
4367
|
async function trySeedEquivalentBranchBaseline(params) {
|
|
4277
4368
|
if (!params.branchName || !params.defaultBranch || params.branchName === params.defaultBranch) {
|
|
4278
4369
|
return null;
|
|
4279
4370
|
}
|
|
4280
4371
|
const bindingState = await readCollabBindingState(params.repoRoot);
|
|
4281
|
-
const defaultBranchBinding = bindingState?.branchBindings[params.defaultBranch] ?? null;
|
|
4372
|
+
const defaultBranchBinding = params.defaultBranch ? bindingState?.branchBindings[params.defaultBranch] ?? null : null;
|
|
4282
4373
|
if (!defaultBranchBinding?.currentAppId) {
|
|
4283
4374
|
return null;
|
|
4284
4375
|
}
|
|
@@ -4303,7 +4394,18 @@ async function trySeedEquivalentBranchBaseline(params) {
|
|
|
4303
4394
|
branchName: params.branchName,
|
|
4304
4395
|
persistBlobs: false
|
|
4305
4396
|
});
|
|
4306
|
-
if (
|
|
4397
|
+
if (!canSeedEquivalentBranchBaseline({
|
|
4398
|
+
branchName: params.branchName,
|
|
4399
|
+
defaultBranch: params.defaultBranch,
|
|
4400
|
+
upstreamAppId: params.upstreamAppId,
|
|
4401
|
+
defaultBranchCurrentAppId: defaultBranchBinding?.currentAppId ?? null,
|
|
4402
|
+
defaultBaselineSnapshotHash: defaultBaseline?.lastSnapshotHash ?? null,
|
|
4403
|
+
defaultBaselineServerHeadHash: defaultBaseline?.lastServerHeadHash ?? null,
|
|
4404
|
+
defaultBaselineLocalCommitHash: defaultBaseline?.lastSeenLocalCommitHash ?? null,
|
|
4405
|
+
appHeadHash: params.appHeadHash,
|
|
4406
|
+
localSnapshotHash: inspection.snapshotHash,
|
|
4407
|
+
localCommitHash: inspection.localCommitHash
|
|
4408
|
+
})) {
|
|
4307
4409
|
return null;
|
|
4308
4410
|
}
|
|
4309
4411
|
await writeLocalBaseline({
|
|
@@ -4327,9 +4429,8 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4327
4429
|
laneId: params.laneId,
|
|
4328
4430
|
repoRoot: params.repoRoot
|
|
4329
4431
|
});
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
}
|
|
4432
|
+
const existingBaselineStatus = classifyExistingInitBaseline(baseline);
|
|
4433
|
+
if (existingBaselineStatus) return existingBaselineStatus;
|
|
4333
4434
|
const localHeadCommitHash = await getHeadCommitHash(params.repoRoot);
|
|
4334
4435
|
if (!localHeadCommitHash) return "baseline_missing";
|
|
4335
4436
|
const appHead = unwrapResponseObject(
|
|
@@ -4381,7 +4482,7 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4381
4482
|
if (equivalentBaseline) {
|
|
4382
4483
|
return equivalentBaseline;
|
|
4383
4484
|
}
|
|
4384
|
-
return "
|
|
4485
|
+
return classifyInitBaselineDelta({ deltaStatus: delta.status }) ?? "baseline_missing";
|
|
4385
4486
|
}
|
|
4386
4487
|
if (delta.status === "content_equivalent") {
|
|
4387
4488
|
await seedImportedInitBaseline({
|
|
@@ -4392,7 +4493,7 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4392
4493
|
currentAppId: params.currentAppId,
|
|
4393
4494
|
branchName: params.branchName
|
|
4394
4495
|
});
|
|
4395
|
-
return "
|
|
4496
|
+
return classifyInitBaselineDelta({ deltaStatus: delta.status }) ?? "baseline_missing";
|
|
4396
4497
|
}
|
|
4397
4498
|
if (delta.status === "base_unknown") {
|
|
4398
4499
|
const equivalentBaseline = await trySeedEquivalentBranchBaseline({
|
|
@@ -4413,34 +4514,317 @@ async function resolveInitBaselineStatus(params) {
|
|
|
4413
4514
|
}
|
|
4414
4515
|
} catch {
|
|
4415
4516
|
}
|
|
4416
|
-
return "baseline_missing";
|
|
4517
|
+
return "baseline_missing";
|
|
4518
|
+
}
|
|
4519
|
+
async function seedImportedInitBaseline(params) {
|
|
4520
|
+
const appHead = unwrapResponseObject(
|
|
4521
|
+
await params.api.getAppHead(params.currentAppId),
|
|
4522
|
+
"app head"
|
|
4523
|
+
);
|
|
4524
|
+
const snapshot = await captureLocalSnapshot({
|
|
4525
|
+
repoRoot: params.repoRoot,
|
|
4526
|
+
repoFingerprint: params.repoFingerprint,
|
|
4527
|
+
laneId: params.laneId,
|
|
4528
|
+
branchName: params.branchName
|
|
4529
|
+
});
|
|
4530
|
+
await writeLocalBaseline({
|
|
4531
|
+
repoRoot: params.repoRoot,
|
|
4532
|
+
repoFingerprint: params.repoFingerprint,
|
|
4533
|
+
laneId: params.laneId,
|
|
4534
|
+
currentAppId: params.currentAppId,
|
|
4535
|
+
branchName: params.branchName,
|
|
4536
|
+
lastSnapshotId: snapshot.id,
|
|
4537
|
+
lastSnapshotHash: snapshot.snapshotHash,
|
|
4538
|
+
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
4539
|
+
lastServerTreeHash: appHead.treeHash ?? null,
|
|
4540
|
+
lastServerHeadHash: appHead.headCommitHash,
|
|
4541
|
+
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
4542
|
+
});
|
|
4543
|
+
return "seeded";
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
// src/application/collab/collabInitBinding.ts
|
|
4547
|
+
function requireResolvedLaneBinding(lane, params) {
|
|
4548
|
+
if (lane.status === "resolved" && lane.currentAppId && lane.upstreamAppId) {
|
|
4549
|
+
return lane;
|
|
4550
|
+
}
|
|
4551
|
+
const branchLabel = params.branchName ?? "the current branch";
|
|
4552
|
+
const laneStatus = String(lane.status ?? "");
|
|
4553
|
+
throw new RemixError(`Failed to resolve a Remix lane for ${branchLabel}.`, {
|
|
4554
|
+
exitCode: 1,
|
|
4555
|
+
hint: laneStatus === "binding_not_found" ? `Run ${params.operation} again after the repository has been initialized.` : laneStatus === "ambiguous_family_selection" ? "Multiple canonical Remix families match this repository. Continue from a checkout already bound to the intended family, or rerun with `--force-new` to create a new family." : `Remix did not return complete lane metadata for ${branchLabel}.`
|
|
4556
|
+
});
|
|
4557
|
+
}
|
|
4558
|
+
function resolveProjectBindingResult(response) {
|
|
4559
|
+
const payload = response?.responseObject;
|
|
4560
|
+
if (!payload || typeof payload !== "object") {
|
|
4561
|
+
return { status: "not_found" };
|
|
4562
|
+
}
|
|
4563
|
+
if (payload.status === "ambiguous_family_selection") {
|
|
4564
|
+
const projectIds = Array.isArray(payload.projectIds) ? payload.projectIds.filter((value) => typeof value === "string" && value.trim().length > 0) : [];
|
|
4565
|
+
return {
|
|
4566
|
+
status: "ambiguous_family_selection",
|
|
4567
|
+
candidateCount: typeof payload.candidateCount === "number" && Number.isFinite(payload.candidateCount) ? payload.candidateCount : projectIds.length,
|
|
4568
|
+
projectIds
|
|
4569
|
+
};
|
|
4570
|
+
}
|
|
4571
|
+
if (payload.projectId && payload.appId) {
|
|
4572
|
+
return {
|
|
4573
|
+
status: "resolved",
|
|
4574
|
+
projectId: String(payload.projectId),
|
|
4575
|
+
appId: String(payload.appId),
|
|
4576
|
+
upstreamAppId: String(payload.upstreamAppId ?? payload.appId),
|
|
4577
|
+
threadId: payload.threadId ? String(payload.threadId) : null
|
|
4578
|
+
};
|
|
4579
|
+
}
|
|
4580
|
+
return { status: "not_found" };
|
|
4581
|
+
}
|
|
4582
|
+
function throwAmbiguousFamilyError(params) {
|
|
4583
|
+
const familyCount = params.candidateCount || params.projectIds.length;
|
|
4584
|
+
const projectHint = params.projectIds.length > 0 ? ` Matching project ids: ${params.projectIds.join(", ")}.` : "";
|
|
4585
|
+
const remoteHint = params.remoteUrl ? ` (${params.remoteUrl})` : "";
|
|
4586
|
+
throw new RemixError("Multiple canonical Remix families already match this repository.", {
|
|
4587
|
+
exitCode: 2,
|
|
4588
|
+
hint: `Plain \`remix collab init\` cannot safely choose among ${familyCount} matching canonical families for repo fingerprint ${params.repoFingerprint}${remoteHint}.${projectHint} Run \`remix collab init --force-new\` to create a new canonical family, or continue from a checkout already bound to the intended family.`
|
|
4589
|
+
});
|
|
4590
|
+
}
|
|
4591
|
+
async function resolveOrEnsureLaneBinding(params) {
|
|
4592
|
+
const resolvePayload = {
|
|
4593
|
+
projectId: params.projectId ?? void 0,
|
|
4594
|
+
repoFingerprint: params.repoFingerprint,
|
|
4595
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
4596
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
4597
|
+
branchName: params.branchName
|
|
4598
|
+
};
|
|
4599
|
+
let lane = unwrapResponseObject(
|
|
4600
|
+
await params.api.resolveProjectLaneBinding(resolvePayload),
|
|
4601
|
+
"project lane binding"
|
|
4602
|
+
);
|
|
4603
|
+
if (lane.status !== "resolved") {
|
|
4604
|
+
lane = unwrapResponseObject(
|
|
4605
|
+
await params.api.ensureProjectLaneBinding({
|
|
4606
|
+
...resolvePayload,
|
|
4607
|
+
seedAppId: params.seedAppId ?? void 0,
|
|
4608
|
+
seedHeadCommitHash: params.seedHeadCommitHash ?? void 0
|
|
4609
|
+
}),
|
|
4610
|
+
"project lane binding"
|
|
4611
|
+
);
|
|
4612
|
+
}
|
|
4613
|
+
return requireResolvedLaneBinding(lane, {
|
|
4614
|
+
branchName: params.branchName,
|
|
4615
|
+
operation: params.operation
|
|
4616
|
+
});
|
|
4617
|
+
}
|
|
4618
|
+
function branchBindingFromLane(lane, mode, fallback) {
|
|
4619
|
+
return {
|
|
4620
|
+
projectId: lane.projectId ?? fallback.projectId,
|
|
4621
|
+
currentAppId: lane.currentAppId ?? fallback.currentAppId,
|
|
4622
|
+
upstreamAppId: lane.upstreamAppId ?? fallback.upstreamAppId,
|
|
4623
|
+
threadId: lane.threadId ?? fallback.threadId,
|
|
4624
|
+
laneId: lane.laneId ?? null,
|
|
4625
|
+
bindingMode: mode
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
// src/application/collab/collabInitAsync.ts
|
|
4630
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
4631
|
+
|
|
4632
|
+
// src/infrastructure/collab/drainerLog.ts
|
|
4633
|
+
import fs8 from "fs/promises";
|
|
4634
|
+
import path8 from "path";
|
|
4635
|
+
async function logDrainerEvent(jobId, event, fields = {}) {
|
|
4636
|
+
const entry = {
|
|
4637
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4638
|
+
jobId,
|
|
4639
|
+
event,
|
|
4640
|
+
...fields
|
|
4641
|
+
};
|
|
4642
|
+
try {
|
|
4643
|
+
await fs8.mkdir(getLogsRoot(), { recursive: true });
|
|
4644
|
+
await fs8.appendFile(getDrainerLogPath(), `${JSON.stringify(entry)}
|
|
4645
|
+
`, "utf8");
|
|
4646
|
+
} catch {
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
function drainerLogPath() {
|
|
4650
|
+
return getDrainerLogPath();
|
|
4651
|
+
}
|
|
4652
|
+
|
|
4653
|
+
// src/application/collab/collabInitAsync.ts
|
|
4654
|
+
async function resolvePendingAsyncInitResult(params) {
|
|
4655
|
+
if (shouldReplaceAsyncInitJobs({ forceNew: params.forceNew })) {
|
|
4656
|
+
for (const kind of ["init", "init_post"]) {
|
|
4657
|
+
const existing = await listAsyncJobsForRepo({ repoRoot: params.repoRoot, branchName: params.branchName, kind });
|
|
4658
|
+
for (const job of existing) {
|
|
4659
|
+
await deleteAsyncJob(job.id);
|
|
4660
|
+
await logDrainerEvent(job.id, "replaced", {
|
|
4661
|
+
kind,
|
|
4662
|
+
reason: "force_new",
|
|
4663
|
+
previousStatus: job.status,
|
|
4664
|
+
previousError: job.error
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
return null;
|
|
4669
|
+
}
|
|
4670
|
+
const pendingPost = await findPendingAsyncJob({ repoRoot: params.repoRoot, branchName: params.branchName, kind: "init_post" });
|
|
4671
|
+
const pending = pendingPost ?? await findPendingAsyncJob({ repoRoot: params.repoRoot, branchName: params.branchName, kind: "init" });
|
|
4672
|
+
if (!shouldReturnPendingAsyncInit({ forceNew: params.forceNew, pendingJobFound: Boolean(pending) }) || !pending) {
|
|
4673
|
+
return null;
|
|
4674
|
+
}
|
|
4675
|
+
const existingBinding = await readCollabBinding(params.repoRoot);
|
|
4676
|
+
if (existingBinding && existingBinding.currentAppId) {
|
|
4677
|
+
return {
|
|
4678
|
+
queued: true,
|
|
4679
|
+
jobId: pending.id,
|
|
4680
|
+
repoRoot: params.repoRoot,
|
|
4681
|
+
projectId: existingBinding.projectId ?? "",
|
|
4682
|
+
appId: existingBinding.currentAppId,
|
|
4683
|
+
upstreamAppId: existingBinding.upstreamAppId ?? existingBinding.currentAppId,
|
|
4684
|
+
dashboardUrl: buildDashboardAppUrl(existingBinding.currentAppId),
|
|
4685
|
+
bindingPath: getCollabBindingPath(params.repoRoot),
|
|
4686
|
+
repoFingerprint: existingBinding.repoFingerprint ?? params.repoFingerprint,
|
|
4687
|
+
bindingMode: existingBinding.bindingMode,
|
|
4688
|
+
createdCanonicalFamily: false,
|
|
4689
|
+
remoteUrl: existingBinding.remoteUrl ?? params.remoteUrl,
|
|
4690
|
+
defaultBranch: existingBinding.defaultBranch ?? params.defaultBranch ?? null,
|
|
4691
|
+
...params.warnings.length > 0 ? { warnings: params.warnings } : {}
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
return {
|
|
4695
|
+
queued: true,
|
|
4696
|
+
jobId: pending.id,
|
|
4697
|
+
repoRoot: params.repoRoot,
|
|
4698
|
+
projectId: "",
|
|
4699
|
+
appId: "",
|
|
4700
|
+
upstreamAppId: "",
|
|
4701
|
+
dashboardUrl: "",
|
|
4702
|
+
bindingPath: getCollabBindingPath(params.repoRoot),
|
|
4703
|
+
repoFingerprint: params.repoFingerprint,
|
|
4704
|
+
bindingMode: "lane",
|
|
4705
|
+
createdCanonicalFamily: false,
|
|
4706
|
+
remoteUrl: params.remoteUrl,
|
|
4707
|
+
defaultBranch: params.defaultBranch ?? null,
|
|
4708
|
+
...params.warnings.length > 0 ? { warnings: params.warnings } : {}
|
|
4709
|
+
};
|
|
4710
|
+
}
|
|
4711
|
+
async function clearFailedAsyncInitJobs(params) {
|
|
4712
|
+
if (params.forceNew) return;
|
|
4713
|
+
const failed = await findFailedAsyncJob({ repoRoot: params.repoRoot, branchName: params.branchName, kind: "init" });
|
|
4714
|
+
if (failed) {
|
|
4715
|
+
await deleteAsyncJob(failed.id);
|
|
4716
|
+
}
|
|
4717
|
+
const failedPost = await findFailedAsyncJob({ repoRoot: params.repoRoot, branchName: params.branchName, kind: "init_post" });
|
|
4718
|
+
if (failedPost) {
|
|
4719
|
+
await deleteAsyncJob(failedPost.id);
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
async function enqueueInitPostJob(params) {
|
|
4723
|
+
const job = await enqueueAsyncJob({
|
|
4724
|
+
id: randomUUID4(),
|
|
4725
|
+
kind: "init_post",
|
|
4726
|
+
status: "queued",
|
|
4727
|
+
repoRoot: params.repoRoot,
|
|
4728
|
+
repoFingerprint: params.repoFingerprint,
|
|
4729
|
+
branchName: params.branchName,
|
|
4730
|
+
laneId: params.laneId,
|
|
4731
|
+
retryCount: 0,
|
|
4732
|
+
error: null,
|
|
4733
|
+
idempotencyKey: null,
|
|
4734
|
+
payload: {
|
|
4735
|
+
appId: params.appId,
|
|
4736
|
+
syncPhaseStartedAt: params.syncPhaseStartedAt,
|
|
4737
|
+
syncPhaseFinishedAt: params.syncPhaseFinishedAt
|
|
4738
|
+
}
|
|
4739
|
+
});
|
|
4740
|
+
await logDrainerEvent(job.id, "submitted", {
|
|
4741
|
+
kind: "init_post",
|
|
4742
|
+
appId: params.appId,
|
|
4743
|
+
["init.sync_phase_ms"]: params.syncPhaseFinishedAt - params.syncPhaseStartedAt
|
|
4744
|
+
});
|
|
4745
|
+
return job;
|
|
4746
|
+
}
|
|
4747
|
+
|
|
4748
|
+
// src/application/collab/collabInitImport.ts
|
|
4749
|
+
import fs10 from "fs/promises";
|
|
4750
|
+
import path9 from "path";
|
|
4751
|
+
|
|
4752
|
+
// src/shared/upload.ts
|
|
4753
|
+
import fs9 from "fs";
|
|
4754
|
+
import { PassThrough } from "stream";
|
|
4755
|
+
async function uploadPresigned(params) {
|
|
4756
|
+
const stats = await fs9.promises.stat(params.filePath).catch(() => null);
|
|
4757
|
+
if (!stats || !stats.isFile()) {
|
|
4758
|
+
throw new RemixError("Upload file not found.", { exitCode: 2 });
|
|
4759
|
+
}
|
|
4760
|
+
const totalBytes = stats.size;
|
|
4761
|
+
const fileStream = fs9.createReadStream(params.filePath);
|
|
4762
|
+
const pass = new PassThrough();
|
|
4763
|
+
let sentBytes = 0;
|
|
4764
|
+
fileStream.on("data", (chunk) => {
|
|
4765
|
+
sentBytes += chunk.length;
|
|
4766
|
+
params.onProgress?.({ sentBytes, totalBytes });
|
|
4767
|
+
});
|
|
4768
|
+
fileStream.on("error", (err) => pass.destroy(err));
|
|
4769
|
+
fileStream.pipe(pass);
|
|
4770
|
+
const response = await fetch(params.uploadUrl, {
|
|
4771
|
+
method: "PUT",
|
|
4772
|
+
headers: params.headers,
|
|
4773
|
+
body: pass,
|
|
4774
|
+
duplex: "half"
|
|
4775
|
+
});
|
|
4776
|
+
if (!response.ok) {
|
|
4777
|
+
const text = await response.text().catch(() => "");
|
|
4778
|
+
throw new RemixError("Upload failed.", {
|
|
4779
|
+
exitCode: 1,
|
|
4780
|
+
hint: `Status: ${response.status}
|
|
4781
|
+
${text}`.trim() || null
|
|
4782
|
+
});
|
|
4783
|
+
}
|
|
4417
4784
|
}
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
);
|
|
4423
|
-
const
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4785
|
+
|
|
4786
|
+
// src/application/collab/collabInitImport.ts
|
|
4787
|
+
async function uploadInitBundle(params) {
|
|
4788
|
+
const bundleName = params.bundleName ?? path9.basename(params.bundlePath);
|
|
4789
|
+
const bundleSize = (await fs10.stat(params.bundlePath)).size;
|
|
4790
|
+
const presignResp = await params.api.presignImportUploadFirstParty({
|
|
4791
|
+
file: {
|
|
4792
|
+
name: bundleName,
|
|
4793
|
+
mimeType: "application/x-git-bundle",
|
|
4794
|
+
size: bundleSize,
|
|
4795
|
+
checksumSha256: params.bundleSha
|
|
4796
|
+
}
|
|
4428
4797
|
});
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
branchName: params.branchName,
|
|
4435
|
-
lastSnapshotId: snapshot.id,
|
|
4436
|
-
lastSnapshotHash: snapshot.snapshotHash,
|
|
4437
|
-
lastServerRevisionId: appHead.headRevisionId ?? null,
|
|
4438
|
-
lastServerTreeHash: appHead.treeHash ?? null,
|
|
4439
|
-
lastServerHeadHash: appHead.headCommitHash,
|
|
4440
|
-
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
4798
|
+
const presign = unwrapResponseObject(presignResp, "upload");
|
|
4799
|
+
await uploadPresigned({
|
|
4800
|
+
uploadUrl: String(presign.uploadUrl),
|
|
4801
|
+
headers: presign.headers ?? {},
|
|
4802
|
+
filePath: params.bundlePath
|
|
4441
4803
|
});
|
|
4442
|
-
return
|
|
4804
|
+
return String(presign.uploadId);
|
|
4805
|
+
}
|
|
4806
|
+
async function importUploadedInitBundle(params) {
|
|
4807
|
+
const importPayload = {
|
|
4808
|
+
uploadId: params.uploadId,
|
|
4809
|
+
appName: params.appName,
|
|
4810
|
+
platform: "generic",
|
|
4811
|
+
profile: params.profile ?? void 0,
|
|
4812
|
+
isPublic: false,
|
|
4813
|
+
branch: params.branch ?? void 0,
|
|
4814
|
+
remoteUrl: params.remoteUrl ?? void 0,
|
|
4815
|
+
defaultBranch: params.defaultBranch ?? void 0,
|
|
4816
|
+
repoFingerprint: params.repoFingerprint,
|
|
4817
|
+
headCommitHash: params.headCommitHash,
|
|
4818
|
+
forceNew: params.forceNew
|
|
4819
|
+
};
|
|
4820
|
+
if ("path" in params) {
|
|
4821
|
+
importPayload.path = params.path?.trim() || void 0;
|
|
4822
|
+
}
|
|
4823
|
+
const importResp = await params.api.importFromUploadFirstParty(importPayload);
|
|
4824
|
+
return unwrapResponseObject(importResp, "import");
|
|
4443
4825
|
}
|
|
4826
|
+
|
|
4827
|
+
// src/application/collab/collabInit.ts
|
|
4444
4828
|
async function collabInit(params) {
|
|
4445
4829
|
return withRepoMutationLock(
|
|
4446
4830
|
{
|
|
@@ -4457,7 +4841,7 @@ async function collabInit(params) {
|
|
|
4457
4841
|
}
|
|
4458
4842
|
const localBindingState = await readCollabBindingState(repoRoot, { persist: true });
|
|
4459
4843
|
const hasExistingBinding = localBindingState != null && Object.keys(localBindingState.branchBindings ?? {}).length > 0;
|
|
4460
|
-
if (params.forceNew
|
|
4844
|
+
if (shouldRequireCleanWorktreeForInit({ forceNew: params.forceNew, hasExistingBinding })) {
|
|
4461
4845
|
await ensureCleanWorktree(repoRoot, "`remix collab init`");
|
|
4462
4846
|
}
|
|
4463
4847
|
const persistedRemoteUrl = normalizeGitRemote(localBindingState?.remoteUrl ?? null);
|
|
@@ -4469,59 +4853,17 @@ async function collabInit(params) {
|
|
|
4469
4853
|
const repoFingerprint = localBindingState?.repoFingerprint ?? await buildRepoFingerprint({ gitRoot: repoRoot, remoteUrl, defaultBranch });
|
|
4470
4854
|
const repoSnapshot = await captureRepoSnapshot(repoRoot);
|
|
4471
4855
|
if (params.asyncSubmit) {
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
}
|
|
4484
|
-
}
|
|
4485
|
-
} else {
|
|
4486
|
-
const pendingPost = await findPendingAsyncJob({ repoRoot, branchName, kind: "init_post" });
|
|
4487
|
-
const pending = pendingPost ?? await findPendingAsyncJob({ repoRoot, branchName, kind: "init" });
|
|
4488
|
-
if (pending) {
|
|
4489
|
-
const existingBinding = await readCollabBinding(repoRoot);
|
|
4490
|
-
if (existingBinding && existingBinding.currentAppId) {
|
|
4491
|
-
return {
|
|
4492
|
-
queued: true,
|
|
4493
|
-
jobId: pending.id,
|
|
4494
|
-
repoRoot,
|
|
4495
|
-
projectId: existingBinding.projectId ?? "",
|
|
4496
|
-
appId: existingBinding.currentAppId,
|
|
4497
|
-
upstreamAppId: existingBinding.upstreamAppId ?? existingBinding.currentAppId,
|
|
4498
|
-
dashboardUrl: buildDashboardAppUrl(existingBinding.currentAppId),
|
|
4499
|
-
bindingPath: getCollabBindingPath(repoRoot),
|
|
4500
|
-
repoFingerprint: existingBinding.repoFingerprint ?? repoFingerprint,
|
|
4501
|
-
bindingMode: existingBinding.bindingMode,
|
|
4502
|
-
createdCanonicalFamily: false,
|
|
4503
|
-
remoteUrl: existingBinding.remoteUrl ?? remoteUrl,
|
|
4504
|
-
defaultBranch: existingBinding.defaultBranch ?? defaultBranch ?? null,
|
|
4505
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
4506
|
-
};
|
|
4507
|
-
}
|
|
4508
|
-
return {
|
|
4509
|
-
queued: true,
|
|
4510
|
-
jobId: pending.id,
|
|
4511
|
-
repoRoot,
|
|
4512
|
-
projectId: "",
|
|
4513
|
-
appId: "",
|
|
4514
|
-
upstreamAppId: "",
|
|
4515
|
-
dashboardUrl: "",
|
|
4516
|
-
bindingPath: getCollabBindingPath(repoRoot),
|
|
4517
|
-
repoFingerprint,
|
|
4518
|
-
bindingMode: "lane",
|
|
4519
|
-
createdCanonicalFamily: false,
|
|
4520
|
-
remoteUrl,
|
|
4521
|
-
defaultBranch: defaultBranch ?? null,
|
|
4522
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
4523
|
-
};
|
|
4524
|
-
}
|
|
4856
|
+
const pendingResult = await resolvePendingAsyncInitResult({
|
|
4857
|
+
forceNew: params.forceNew,
|
|
4858
|
+
repoRoot,
|
|
4859
|
+
branchName,
|
|
4860
|
+
repoFingerprint,
|
|
4861
|
+
remoteUrl,
|
|
4862
|
+
defaultBranch: defaultBranch ?? null,
|
|
4863
|
+
warnings
|
|
4864
|
+
});
|
|
4865
|
+
if (pendingResult) {
|
|
4866
|
+
return pendingResult;
|
|
4525
4867
|
}
|
|
4526
4868
|
}
|
|
4527
4869
|
if (!params.forceNew && localBindingState?.explicitRootBinding && branchName) {
|
|
@@ -4613,7 +4955,7 @@ async function collabInit(params) {
|
|
|
4613
4955
|
appId: boundCurrentAppId2,
|
|
4614
4956
|
dashboardUrl: buildDashboardAppUrl(boundCurrentAppId2),
|
|
4615
4957
|
upstreamAppId: boundUpstreamAppId2,
|
|
4616
|
-
bindingPath:
|
|
4958
|
+
bindingPath: path10.join(repoRoot, ".remix", "config.json"),
|
|
4617
4959
|
repoRoot,
|
|
4618
4960
|
repoFingerprint,
|
|
4619
4961
|
bindingMode: defaultBranch && branchName !== defaultBranch ? "lane" : "explicit_root",
|
|
@@ -4878,53 +5220,33 @@ async function collabInit(params) {
|
|
|
4878
5220
|
}
|
|
4879
5221
|
}
|
|
4880
5222
|
if (params.asyncSubmit) {
|
|
4881
|
-
|
|
4882
|
-
const failed = await findFailedAsyncJob({ repoRoot, branchName, kind: "init" });
|
|
4883
|
-
if (failed) {
|
|
4884
|
-
await deleteAsyncJob(failed.id);
|
|
4885
|
-
}
|
|
4886
|
-
const failedPost = await findFailedAsyncJob({ repoRoot, branchName, kind: "init_post" });
|
|
4887
|
-
if (failedPost) {
|
|
4888
|
-
await deleteAsyncJob(failedPost.id);
|
|
4889
|
-
}
|
|
4890
|
-
}
|
|
5223
|
+
await clearFailedAsyncInitJobs({ forceNew: params.forceNew, repoRoot, branchName });
|
|
4891
5224
|
const syncPhaseStartedAt = Date.now();
|
|
4892
5225
|
const { bundlePath: bundleFilePath, headCommitHash: bundledHeadCommitHash } = await createGitBundle(
|
|
4893
5226
|
repoRoot,
|
|
4894
5227
|
"repository.bundle"
|
|
4895
5228
|
);
|
|
4896
|
-
const tmpBundleDir =
|
|
5229
|
+
const tmpBundleDir = path10.dirname(bundleFilePath);
|
|
4897
5230
|
try {
|
|
4898
5231
|
const bundleSha2 = await sha256FileHex(bundleFilePath);
|
|
4899
|
-
const
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
size: bundleSize2,
|
|
4905
|
-
checksumSha256: bundleSha2
|
|
4906
|
-
}
|
|
4907
|
-
});
|
|
4908
|
-
const presign2 = unwrapResponseObject(presignResp2, "upload");
|
|
4909
|
-
await uploadPresigned({
|
|
4910
|
-
uploadUrl: String(presign2.uploadUrl),
|
|
4911
|
-
headers: presign2.headers ?? {},
|
|
4912
|
-
filePath: bundleFilePath
|
|
5232
|
+
const uploadId2 = await uploadInitBundle({
|
|
5233
|
+
api: params.api,
|
|
5234
|
+
bundlePath: bundleFilePath,
|
|
5235
|
+
bundleSha: bundleSha2,
|
|
5236
|
+
bundleName: "repository.bundle"
|
|
4913
5237
|
});
|
|
4914
|
-
const
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
5238
|
+
const imported2 = await importUploadedInitBundle({
|
|
5239
|
+
api: params.api,
|
|
5240
|
+
uploadId: uploadId2,
|
|
5241
|
+
appName: params.appName?.trim() || path10.basename(repoRoot),
|
|
4918
5242
|
profile: params.profile ?? void 0,
|
|
4919
|
-
isPublic: false,
|
|
4920
5243
|
branch: defaultBranch && branchName && branchName !== defaultBranch ? defaultBranch : currentBranch ?? void 0,
|
|
4921
|
-
remoteUrl
|
|
4922
|
-
defaultBranch: defaultBranch ??
|
|
5244
|
+
remoteUrl,
|
|
5245
|
+
defaultBranch: defaultBranch ?? null,
|
|
4923
5246
|
repoFingerprint,
|
|
4924
5247
|
headCommitHash: bundledHeadCommitHash,
|
|
4925
5248
|
forceNew: params.forceNew
|
|
4926
5249
|
});
|
|
4927
|
-
const imported2 = unwrapResponseObject(importResp2, "import");
|
|
4928
5250
|
const importedAppId = String(imported2.appId);
|
|
4929
5251
|
let boundProjectId2 = String(imported2.projectId ?? "");
|
|
4930
5252
|
let boundCurrentAppId2 = importedAppId;
|
|
@@ -5049,28 +5371,14 @@ async function collabInit(params) {
|
|
|
5049
5371
|
});
|
|
5050
5372
|
}
|
|
5051
5373
|
const syncPhaseFinishedAt = Date.now();
|
|
5052
|
-
const
|
|
5053
|
-
const job = await enqueueAsyncJob({
|
|
5054
|
-
id: jobId,
|
|
5055
|
-
kind: "init_post",
|
|
5056
|
-
status: "queued",
|
|
5374
|
+
const job = await enqueueInitPostJob({
|
|
5057
5375
|
repoRoot,
|
|
5058
5376
|
repoFingerprint,
|
|
5059
5377
|
branchName,
|
|
5060
5378
|
laneId: boundLaneId2,
|
|
5061
|
-
retryCount: 0,
|
|
5062
|
-
error: null,
|
|
5063
|
-
idempotencyKey: null,
|
|
5064
|
-
payload: {
|
|
5065
|
-
appId: boundCurrentAppId2,
|
|
5066
|
-
syncPhaseStartedAt,
|
|
5067
|
-
syncPhaseFinishedAt
|
|
5068
|
-
}
|
|
5069
|
-
});
|
|
5070
|
-
await logDrainerEvent(job.id, "submitted", {
|
|
5071
|
-
kind: "init_post",
|
|
5072
5379
|
appId: boundCurrentAppId2,
|
|
5073
|
-
|
|
5380
|
+
syncPhaseStartedAt,
|
|
5381
|
+
syncPhaseFinishedAt
|
|
5074
5382
|
});
|
|
5075
5383
|
return {
|
|
5076
5384
|
queued: true,
|
|
@@ -5089,41 +5397,30 @@ async function collabInit(params) {
|
|
|
5089
5397
|
...warnings.length > 0 ? { warnings } : {}
|
|
5090
5398
|
};
|
|
5091
5399
|
} finally {
|
|
5092
|
-
await
|
|
5400
|
+
await fs11.rm(tmpBundleDir, { recursive: true, force: true }).catch(() => void 0);
|
|
5093
5401
|
}
|
|
5094
5402
|
}
|
|
5095
5403
|
const { bundlePath, headCommitHash } = await createGitBundle(repoRoot, "repository.bundle");
|
|
5096
5404
|
const bundleSha = await sha256FileHex(bundlePath);
|
|
5097
|
-
const
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
size: bundleSize,
|
|
5103
|
-
checksumSha256: bundleSha
|
|
5104
|
-
}
|
|
5105
|
-
});
|
|
5106
|
-
const presign = unwrapResponseObject(presignResp, "upload");
|
|
5107
|
-
await uploadPresigned({
|
|
5108
|
-
uploadUrl: String(presign.uploadUrl),
|
|
5109
|
-
headers: presign.headers ?? {},
|
|
5110
|
-
filePath: bundlePath
|
|
5405
|
+
const uploadId = await uploadInitBundle({
|
|
5406
|
+
api: params.api,
|
|
5407
|
+
bundlePath,
|
|
5408
|
+
bundleSha,
|
|
5409
|
+
bundleName: "repository.bundle"
|
|
5111
5410
|
});
|
|
5112
|
-
const
|
|
5113
|
-
|
|
5114
|
-
|
|
5411
|
+
const imported = await importUploadedInitBundle({
|
|
5412
|
+
api: params.api,
|
|
5413
|
+
uploadId,
|
|
5414
|
+
appName: params.appName?.trim() || path10.basename(repoRoot),
|
|
5115
5415
|
path: params.path?.trim() || void 0,
|
|
5116
|
-
platform: "generic",
|
|
5117
5416
|
profile: params.profile ?? void 0,
|
|
5118
|
-
isPublic: false,
|
|
5119
5417
|
branch: defaultBranch && branchName && branchName !== defaultBranch ? defaultBranch : currentBranch ?? void 0,
|
|
5120
|
-
remoteUrl
|
|
5121
|
-
defaultBranch: defaultBranch ??
|
|
5418
|
+
remoteUrl,
|
|
5419
|
+
defaultBranch: defaultBranch ?? null,
|
|
5122
5420
|
repoFingerprint,
|
|
5123
5421
|
headCommitHash,
|
|
5124
5422
|
forceNew: params.forceNew
|
|
5125
5423
|
});
|
|
5126
|
-
const imported = unwrapResponseObject(importResp, "import");
|
|
5127
5424
|
const app = await pollAppImported(params.api, String(imported.appId));
|
|
5128
5425
|
let boundProjectId = String(app.projectId);
|
|
5129
5426
|
let boundCurrentAppId = String(app.id);
|
|
@@ -5323,10 +5620,10 @@ async function collabList(params) {
|
|
|
5323
5620
|
}
|
|
5324
5621
|
|
|
5325
5622
|
// src/application/collab/collabReconcile.ts
|
|
5326
|
-
import
|
|
5623
|
+
import fs12 from "fs/promises";
|
|
5327
5624
|
import os5 from "os";
|
|
5328
|
-
import
|
|
5329
|
-
import { execa as
|
|
5625
|
+
import path11 from "path";
|
|
5626
|
+
import { execa as execa2 } from "execa";
|
|
5330
5627
|
|
|
5331
5628
|
// src/application/collab/pendingFinalize.ts
|
|
5332
5629
|
function hasPendingFinalize(summary) {
|
|
@@ -5336,6 +5633,50 @@ function buildPendingFinalizeHint() {
|
|
|
5336
5633
|
return "Drain or await the local finalize queue first, then retry after the queued Remix turn finishes recording remotely.";
|
|
5337
5634
|
}
|
|
5338
5635
|
|
|
5636
|
+
// src/application/collab/reconcileDecision.ts
|
|
5637
|
+
function classifyReconcileDelta(delta) {
|
|
5638
|
+
if (delta.status === "conflict_risk") {
|
|
5639
|
+
return {
|
|
5640
|
+
action: "metadata_conflict",
|
|
5641
|
+
message: "Local repository metadata conflicts with the bound Remix app.",
|
|
5642
|
+
exitCode: 2,
|
|
5643
|
+
hint: delta.warnings.join("\n") || "Run the command from the correct bound repository."
|
|
5644
|
+
};
|
|
5645
|
+
}
|
|
5646
|
+
if (delta.status === "base_unknown") {
|
|
5647
|
+
return {
|
|
5648
|
+
action: "base_unknown",
|
|
5649
|
+
message: "Reconcile cannot pull the newer server state from the last acknowledged baseline.",
|
|
5650
|
+
exitCode: 2,
|
|
5651
|
+
hint: "Run `remix collab init` to seed a fresh revision baseline for this checkout before retrying."
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
if (delta.status !== "delta_ready" && delta.status !== "up_to_date") {
|
|
5655
|
+
return {
|
|
5656
|
+
action: "unsupported_delta",
|
|
5657
|
+
message: "Reconcile could not determine the server delta for this lane.",
|
|
5658
|
+
exitCode: 1,
|
|
5659
|
+
hint: delta.warnings.join("\n") || "Run `remix collab status` and retry once the lane is healthy."
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
return { action: "ready" };
|
|
5663
|
+
}
|
|
5664
|
+
function classifyReconcileDetectedState(detected) {
|
|
5665
|
+
if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
|
|
5666
|
+
return { action: "realign", hint: detected.hint };
|
|
5667
|
+
}
|
|
5668
|
+
if (detected.repoState === "server_only_changed" || detected.repoState === "idle") return { action: "sync_instead" };
|
|
5669
|
+
if (detected.repoState === "both_changed") return { action: "reconcile" };
|
|
5670
|
+
if (detected.repoState === "external_local_base_changed") {
|
|
5671
|
+
return { action: "external_local_base_changed", hint: detected.hint };
|
|
5672
|
+
}
|
|
5673
|
+
if (detected.repoState === "local_only_changed") return { action: "local_only_finalize" };
|
|
5674
|
+
return { action: "not_reconcilable", hint: detected.hint };
|
|
5675
|
+
}
|
|
5676
|
+
function shouldApplyServerDeltaBeforeReplay(params) {
|
|
5677
|
+
return params.deltaStatus === "delta_ready" && params.diff.trim().length > 0;
|
|
5678
|
+
}
|
|
5679
|
+
|
|
5339
5680
|
// src/application/collab/collabReconcile.ts
|
|
5340
5681
|
async function reconcileBothChanged(params) {
|
|
5341
5682
|
const repoRoot = await findGitRoot(params.cwd);
|
|
@@ -5391,22 +5732,11 @@ async function reconcileBothChanged(params) {
|
|
|
5391
5732
|
]);
|
|
5392
5733
|
const appHead = unwrapResponseObject(appHeadResp, "app head");
|
|
5393
5734
|
const delta = unwrapResponseObject(deltaResp, "app delta");
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
}
|
|
5400
|
-
if (delta.status === "base_unknown") {
|
|
5401
|
-
throw new RemixError("Reconcile cannot pull the newer server state from the last acknowledged baseline.", {
|
|
5402
|
-
exitCode: 2,
|
|
5403
|
-
hint: "Run `remix collab init` to seed a fresh revision baseline for this checkout before retrying."
|
|
5404
|
-
});
|
|
5405
|
-
}
|
|
5406
|
-
if (delta.status !== "delta_ready" && delta.status !== "up_to_date") {
|
|
5407
|
-
throw new RemixError("Reconcile could not determine the server delta for this lane.", {
|
|
5408
|
-
exitCode: 1,
|
|
5409
|
-
hint: delta.warnings.join("\n") || "Run `remix collab status` and retry once the lane is healthy."
|
|
5735
|
+
const deltaDecision = classifyReconcileDelta(delta);
|
|
5736
|
+
if (deltaDecision.action !== "ready") {
|
|
5737
|
+
throw new RemixError(deltaDecision.message, {
|
|
5738
|
+
exitCode: deltaDecision.exitCode,
|
|
5739
|
+
hint: deltaDecision.hint
|
|
5410
5740
|
});
|
|
5411
5741
|
}
|
|
5412
5742
|
const preview = {
|
|
@@ -5460,15 +5790,15 @@ async function reconcileBothChanged(params) {
|
|
|
5460
5790
|
const replay = await pollChangeStepReplay(params.api, binding.currentAppId, String(replayStart.id));
|
|
5461
5791
|
const replayDiffResp = await params.api.getChangeStepReplayDiff(binding.currentAppId, replay.id);
|
|
5462
5792
|
const replayDiff = unwrapResponseObject(replayDiffResp, "change step replay diff");
|
|
5463
|
-
const tempRoot = await
|
|
5793
|
+
const tempRoot = await fs12.mkdtemp(path11.join(os5.tmpdir(), "remix-reconcile-"));
|
|
5464
5794
|
let serverHeadSnapshot = null;
|
|
5465
5795
|
let mergedSnapshot = null;
|
|
5466
5796
|
try {
|
|
5467
|
-
const tempRepoRoot =
|
|
5468
|
-
await
|
|
5469
|
-
await
|
|
5797
|
+
const tempRepoRoot = path11.join(tempRoot, "repo");
|
|
5798
|
+
await fs12.mkdir(tempRepoRoot, { recursive: true });
|
|
5799
|
+
await execa2("git", ["init"], { cwd: tempRepoRoot, stderr: "ignore" });
|
|
5470
5800
|
await materializeLocalSnapshot(baseline.lastSnapshotId, tempRepoRoot);
|
|
5471
|
-
if (delta.status
|
|
5801
|
+
if (shouldApplyServerDeltaBeforeReplay({ deltaStatus: delta.status, diff: delta.diff })) {
|
|
5472
5802
|
await applyUnifiedDiffToWorktree(tempRepoRoot, delta.diff, "`remix collab reconcile`");
|
|
5473
5803
|
}
|
|
5474
5804
|
serverHeadSnapshot = await captureLocalSnapshot({
|
|
@@ -5487,7 +5817,7 @@ async function reconcileBothChanged(params) {
|
|
|
5487
5817
|
branchName: binding.branchName
|
|
5488
5818
|
});
|
|
5489
5819
|
} finally {
|
|
5490
|
-
await
|
|
5820
|
+
await fs12.rm(tempRoot, { recursive: true, force: true }).catch(() => void 0);
|
|
5491
5821
|
}
|
|
5492
5822
|
if (!serverHeadSnapshot || !mergedSnapshot) {
|
|
5493
5823
|
throw new RemixError("Failed to materialize the reconciled local workspace.", { exitCode: 1 });
|
|
@@ -5548,25 +5878,30 @@ async function collabReconcile(params) {
|
|
|
5548
5878
|
cwd: params.cwd,
|
|
5549
5879
|
allowBranchMismatch: params.allowBranchMismatch
|
|
5550
5880
|
});
|
|
5551
|
-
|
|
5881
|
+
const detectedDecision = classifyReconcileDetectedState({
|
|
5882
|
+
status: detected.status,
|
|
5883
|
+
repoState: detected.repoState,
|
|
5884
|
+
hint: detected.hint
|
|
5885
|
+
});
|
|
5886
|
+
if (detectedDecision.action === "realign") {
|
|
5552
5887
|
throw new RemixError("Repository must be realigned before reconciliation.", {
|
|
5553
5888
|
exitCode: 2,
|
|
5554
|
-
hint:
|
|
5889
|
+
hint: detectedDecision.hint
|
|
5555
5890
|
});
|
|
5556
5891
|
}
|
|
5557
|
-
if (
|
|
5892
|
+
if (detectedDecision.action === "sync_instead") {
|
|
5558
5893
|
return collabSync(params);
|
|
5559
5894
|
}
|
|
5560
|
-
if (
|
|
5895
|
+
if (detectedDecision.action === "reconcile") {
|
|
5561
5896
|
return reconcileBothChanged(params);
|
|
5562
5897
|
}
|
|
5563
|
-
if (
|
|
5898
|
+
if (detectedDecision.action === "external_local_base_changed") {
|
|
5564
5899
|
throw new RemixError("This checkout needs a local Remix revision baseline before reconciliation.", {
|
|
5565
5900
|
exitCode: 2,
|
|
5566
|
-
hint:
|
|
5901
|
+
hint: detectedDecision.hint || "Run `remix collab init` or `remix collab sync` to seed the baseline."
|
|
5567
5902
|
});
|
|
5568
5903
|
}
|
|
5569
|
-
if (
|
|
5904
|
+
if (detectedDecision.action === "local_only_finalize") {
|
|
5570
5905
|
if (hasPendingFinalize(detected.pendingFinalize)) {
|
|
5571
5906
|
throw new RemixError("Reconcile is not needed while queued Remix turn recording is still processing.", {
|
|
5572
5907
|
exitCode: 2,
|
|
@@ -5690,7 +6025,85 @@ async function collabRemix(params) {
|
|
|
5690
6025
|
};
|
|
5691
6026
|
}
|
|
5692
6027
|
|
|
6028
|
+
// src/infrastructure/collab/localStateRepository.ts
|
|
6029
|
+
var defaultLocalStateRepository = {
|
|
6030
|
+
baselines: {
|
|
6031
|
+
read: readLocalBaseline,
|
|
6032
|
+
write: writeLocalBaseline,
|
|
6033
|
+
clear: clearLocalBaseline
|
|
6034
|
+
},
|
|
6035
|
+
snapshots: {
|
|
6036
|
+
inspect: inspectLocalSnapshot,
|
|
6037
|
+
capture: captureLocalSnapshot,
|
|
6038
|
+
read: readLocalSnapshot,
|
|
6039
|
+
materialize: materializeLocalSnapshot,
|
|
6040
|
+
restoreToWorktree: restoreLocalSnapshotToWorktree,
|
|
6041
|
+
diff: diffLocalSnapshots
|
|
6042
|
+
},
|
|
6043
|
+
asyncJobs: {
|
|
6044
|
+
enqueue: enqueueAsyncJob,
|
|
6045
|
+
read: readAsyncJob,
|
|
6046
|
+
list: listAsyncJobs,
|
|
6047
|
+
listForRepo: listAsyncJobsForRepo,
|
|
6048
|
+
findPending: findPendingAsyncJob,
|
|
6049
|
+
findFailed: findFailedAsyncJob,
|
|
6050
|
+
update: updateAsyncJob,
|
|
6051
|
+
delete: deleteAsyncJob,
|
|
6052
|
+
claim: claimAsyncJob,
|
|
6053
|
+
summarize: summarizeAsyncJobs,
|
|
6054
|
+
pruneTerminal: pruneTerminalAsyncJobs,
|
|
6055
|
+
getBundleFilePath: getAsyncJobBundleFilePath,
|
|
6056
|
+
await: awaitAsyncJob
|
|
6057
|
+
},
|
|
6058
|
+
finalizeJobs: {
|
|
6059
|
+
enqueue: enqueuePendingFinalizeJob,
|
|
6060
|
+
read: readPendingFinalizeJob,
|
|
6061
|
+
list: listPendingFinalizeJobs,
|
|
6062
|
+
update: updatePendingFinalizeJob,
|
|
6063
|
+
claim: claimPendingFinalizeJob,
|
|
6064
|
+
remove: removePendingFinalizeJob,
|
|
6065
|
+
forget: forgetPendingFinalizeJob,
|
|
6066
|
+
requeue: requeuePendingFinalizeJob,
|
|
6067
|
+
heartbeatLock: heartbeatJobLock,
|
|
6068
|
+
cleanStaleLocks: cleanStaleFinalizeJobLocks,
|
|
6069
|
+
prune: prunePendingFinalizeJobs,
|
|
6070
|
+
summarize: summarizePendingFinalizeJobs
|
|
6071
|
+
},
|
|
6072
|
+
bindings: {
|
|
6073
|
+
getPath: getCollabBindingPath,
|
|
6074
|
+
read: readCollabBinding,
|
|
6075
|
+
readState: readCollabBindingState,
|
|
6076
|
+
write: writeCollabBinding,
|
|
6077
|
+
writeSnapshot: writeCollabBindingSnapshot
|
|
6078
|
+
}
|
|
6079
|
+
};
|
|
6080
|
+
function createLocalStateRepository(overrides = {}) {
|
|
6081
|
+
return {
|
|
6082
|
+
baselines: {
|
|
6083
|
+
...defaultLocalStateRepository.baselines,
|
|
6084
|
+
...overrides.baselines
|
|
6085
|
+
},
|
|
6086
|
+
snapshots: {
|
|
6087
|
+
...defaultLocalStateRepository.snapshots,
|
|
6088
|
+
...overrides.snapshots
|
|
6089
|
+
},
|
|
6090
|
+
asyncJobs: {
|
|
6091
|
+
...defaultLocalStateRepository.asyncJobs,
|
|
6092
|
+
...overrides.asyncJobs
|
|
6093
|
+
},
|
|
6094
|
+
finalizeJobs: {
|
|
6095
|
+
...defaultLocalStateRepository.finalizeJobs,
|
|
6096
|
+
...overrides.finalizeJobs
|
|
6097
|
+
},
|
|
6098
|
+
bindings: {
|
|
6099
|
+
...defaultLocalStateRepository.bindings,
|
|
6100
|
+
...overrides.bindings
|
|
6101
|
+
}
|
|
6102
|
+
};
|
|
6103
|
+
}
|
|
6104
|
+
|
|
5693
6105
|
// src/application/collab/collabRequestMerge.ts
|
|
6106
|
+
var localState = createLocalStateRepository();
|
|
5694
6107
|
async function collabRequestMerge(params) {
|
|
5695
6108
|
const repoRoot = await findGitRoot(params.cwd);
|
|
5696
6109
|
const binding = await ensureActiveLaneBinding({
|
|
@@ -5698,14 +6111,24 @@ async function collabRequestMerge(params) {
|
|
|
5698
6111
|
api: params.api,
|
|
5699
6112
|
operation: "`remix collab request-merge`"
|
|
5700
6113
|
});
|
|
5701
|
-
if (!binding)
|
|
5702
|
-
|
|
6114
|
+
if (!binding) {
|
|
6115
|
+
const transition2 = planRequestMergeWorkflowTransition({ hasBinding: false, hasPendingFinalize: false });
|
|
6116
|
+
if (transition2.action === "require_binding") {
|
|
6117
|
+
throw new RemixError("Repository is not bound to Remix.", { exitCode: 2 });
|
|
6118
|
+
}
|
|
6119
|
+
throw new RemixError("Repository is not bound to Remix.", { exitCode: 2 });
|
|
6120
|
+
}
|
|
6121
|
+
const pendingFinalize = await localState.finalizeJobs.summarize({
|
|
5703
6122
|
repoRoot,
|
|
5704
6123
|
repoFingerprint: binding.repoFingerprint,
|
|
5705
6124
|
currentAppId: binding.currentAppId,
|
|
5706
6125
|
laneId: binding.laneId
|
|
5707
6126
|
});
|
|
5708
|
-
|
|
6127
|
+
const transition = planRequestMergeWorkflowTransition({
|
|
6128
|
+
hasBinding: true,
|
|
6129
|
+
hasPendingFinalize: hasPendingFinalize(pendingFinalize)
|
|
6130
|
+
});
|
|
6131
|
+
if (transition.action === "require_finalize_drain") {
|
|
5709
6132
|
throw new RemixError("Pending Remix turn recording still processing.", {
|
|
5710
6133
|
exitCode: 2,
|
|
5711
6134
|
hint: buildPendingFinalizeHint()
|
|
@@ -6125,9 +6548,34 @@ async function collabView(params) {
|
|
|
6125
6548
|
}
|
|
6126
6549
|
|
|
6127
6550
|
// src/application/collab/collabAsyncProcessing.ts
|
|
6128
|
-
import
|
|
6129
|
-
import
|
|
6130
|
-
|
|
6551
|
+
import fs13 from "fs/promises";
|
|
6552
|
+
import path12 from "path";
|
|
6553
|
+
|
|
6554
|
+
// src/application/collab/collabDependencies.ts
|
|
6555
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
6556
|
+
function isFullLocalStateRepository(value) {
|
|
6557
|
+
return typeof value.bindings?.getPath === "function" && typeof value.bindings.read === "function" && typeof value.bindings.readState === "function" && typeof value.bindings.write === "function" && typeof value.bindings.writeSnapshot === "function" && typeof value.baselines?.read === "function" && typeof value.baselines.write === "function" && typeof value.baselines.clear === "function" && typeof value.snapshots?.inspect === "function" && typeof value.snapshots.capture === "function" && typeof value.snapshots.read === "function" && typeof value.snapshots.materialize === "function" && typeof value.snapshots.restoreToWorktree === "function" && typeof value.snapshots.diff === "function" && typeof value.asyncJobs?.enqueue === "function" && typeof value.asyncJobs.read === "function" && typeof value.asyncJobs.list === "function" && typeof value.asyncJobs.listForRepo === "function" && typeof value.asyncJobs.findPending === "function" && typeof value.asyncJobs.findFailed === "function" && typeof value.asyncJobs.update === "function" && typeof value.asyncJobs.delete === "function" && typeof value.asyncJobs.claim === "function" && typeof value.asyncJobs.summarize === "function" && typeof value.asyncJobs.pruneTerminal === "function" && typeof value.asyncJobs.getBundleFilePath === "function" && typeof value.asyncJobs.await === "function" && typeof value.finalizeJobs?.enqueue === "function" && typeof value.finalizeJobs.read === "function" && typeof value.finalizeJobs.list === "function" && typeof value.finalizeJobs.update === "function" && typeof value.finalizeJobs.claim === "function" && typeof value.finalizeJobs.remove === "function" && typeof value.finalizeJobs.forget === "function" && typeof value.finalizeJobs.requeue === "function" && typeof value.finalizeJobs.heartbeatLock === "function" && typeof value.finalizeJobs.cleanStaleLocks === "function" && typeof value.finalizeJobs.prune === "function" && typeof value.finalizeJobs.summarize === "function";
|
|
6558
|
+
}
|
|
6559
|
+
function createCollabDependencies(overrides = {}) {
|
|
6560
|
+
const defaultClock = {
|
|
6561
|
+
now: () => Date.now(),
|
|
6562
|
+
isoNow: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
6563
|
+
};
|
|
6564
|
+
const defaultTimers = {
|
|
6565
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
6566
|
+
};
|
|
6567
|
+
const localState3 = overrides.localState ? isFullLocalStateRepository(overrides.localState) ? overrides.localState : createLocalStateRepository(overrides.localState) : createLocalStateRepository();
|
|
6568
|
+
return {
|
|
6569
|
+
clock: { ...defaultClock, ...overrides.clock },
|
|
6570
|
+
ids: { randomUUID: randomUUID5, ...overrides.ids },
|
|
6571
|
+
timers: { ...defaultTimers, ...overrides.timers },
|
|
6572
|
+
git: overrides.git ?? createSystemGitAdapter(),
|
|
6573
|
+
localState: localState3,
|
|
6574
|
+
uploadPresigned: overrides.uploadPresigned ?? uploadPresigned
|
|
6575
|
+
};
|
|
6576
|
+
}
|
|
6577
|
+
|
|
6578
|
+
// src/application/collab/errorTaxonomy.ts
|
|
6131
6579
|
var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
6132
6580
|
"ECONNREFUSED",
|
|
6133
6581
|
"ECONNRESET",
|
|
@@ -6139,9 +6587,10 @@ var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
|
6139
6587
|
"EPIPE",
|
|
6140
6588
|
"UND_ERR_SOCKET",
|
|
6141
6589
|
"UND_ERR_CONNECT_TIMEOUT",
|
|
6142
|
-
"UND_ERR_HEADERS_TIMEOUT"
|
|
6590
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
6591
|
+
"UND_ERR_BODY_TIMEOUT"
|
|
6143
6592
|
]);
|
|
6144
|
-
function
|
|
6593
|
+
function formatWorkflowError(error) {
|
|
6145
6594
|
if (error instanceof RemixError) {
|
|
6146
6595
|
return [error.message, error.hint].filter(Boolean).join("\n\n") || error.message || "Unknown error.";
|
|
6147
6596
|
}
|
|
@@ -6167,8 +6616,15 @@ function readStringCandidate(error, key) {
|
|
|
6167
6616
|
if (typeof value === "string" && value.length > 0) return value;
|
|
6168
6617
|
return null;
|
|
6169
6618
|
}
|
|
6170
|
-
function
|
|
6171
|
-
|
|
6619
|
+
function buildClassification(category, retryable) {
|
|
6620
|
+
return {
|
|
6621
|
+
category,
|
|
6622
|
+
retryable,
|
|
6623
|
+
terminal: !retryable,
|
|
6624
|
+
disposition: retryable ? "transient" : "jobSpecific"
|
|
6625
|
+
};
|
|
6626
|
+
}
|
|
6627
|
+
function classifyWorkflowError(error) {
|
|
6172
6628
|
const candidates = [error];
|
|
6173
6629
|
if (error && typeof error === "object" && "cause" in error) {
|
|
6174
6630
|
candidates.push(error.cause);
|
|
@@ -6176,31 +6632,42 @@ function classifyError(error) {
|
|
|
6176
6632
|
for (const candidate of candidates) {
|
|
6177
6633
|
const status = readNumberCandidate(candidate, "status") ?? readNumberCandidate(candidate, "statusCode");
|
|
6178
6634
|
if (status !== null) {
|
|
6179
|
-
if (status === 401 || status === 403) return "
|
|
6180
|
-
if (status >= 500 && status < 600) return "
|
|
6181
|
-
if (status >= 400 && status < 500) return "
|
|
6635
|
+
if (status === 401 || status === 403) return buildClassification("transient_backend", true);
|
|
6636
|
+
if (status >= 500 && status < 600) return buildClassification("transient_backend", true);
|
|
6637
|
+
if (status >= 400 && status < 500) return buildClassification("backend_contract_failure", false);
|
|
6182
6638
|
}
|
|
6183
6639
|
const code = readStringCandidate(candidate, "code");
|
|
6184
|
-
if (code && TRANSIENT_NETWORK_CODES.has(code)) return "
|
|
6185
|
-
if (code === "ENOENT" || code === "EACCES") return "
|
|
6640
|
+
if (code && TRANSIENT_NETWORK_CODES.has(code)) return buildClassification("transient_network", true);
|
|
6641
|
+
if (code === "ENOENT" || code === "EACCES") return buildClassification("persisted_state_corruption", false);
|
|
6186
6642
|
const name = readStringCandidate(candidate, "name");
|
|
6187
|
-
if (name === "AbortError" || name === "TimeoutError") return "
|
|
6188
|
-
if (name === "SyntaxError") return "
|
|
6643
|
+
if (name === "AbortError" || name === "TimeoutError") return buildClassification("transient_network", true);
|
|
6644
|
+
if (name === "SyntaxError") return buildClassification("backend_contract_failure", false);
|
|
6189
6645
|
}
|
|
6190
|
-
return "
|
|
6646
|
+
if (error instanceof RemixError) return buildClassification("user_action_needed", false);
|
|
6647
|
+
return buildClassification("backend_contract_failure", false);
|
|
6648
|
+
}
|
|
6649
|
+
|
|
6650
|
+
// src/application/collab/workflowEvents.ts
|
|
6651
|
+
async function logDrainerWorkflowEvent(jobId, event, fields = {}) {
|
|
6652
|
+
await logDrainerEvent(jobId, event, fields);
|
|
6191
6653
|
}
|
|
6654
|
+
|
|
6655
|
+
// src/application/collab/collabAsyncProcessing.ts
|
|
6656
|
+
var MAX_TRANSIENT_RETRIES = 5;
|
|
6657
|
+
var collabDependencies = createCollabDependencies();
|
|
6658
|
+
var localState2 = collabDependencies.localState;
|
|
6192
6659
|
async function handleProcessError(job, error) {
|
|
6193
|
-
const message =
|
|
6194
|
-
const kind =
|
|
6660
|
+
const message = formatWorkflowError(error);
|
|
6661
|
+
const { disposition: kind } = classifyWorkflowError(error);
|
|
6195
6662
|
if (kind === "transient") {
|
|
6196
6663
|
const nextRetry = (job.retryCount ?? 0) + 1;
|
|
6197
6664
|
if (nextRetry < MAX_TRANSIENT_RETRIES) {
|
|
6198
|
-
await
|
|
6665
|
+
await localState2.asyncJobs.update(job.id, {
|
|
6199
6666
|
status: "queued",
|
|
6200
6667
|
retryCount: nextRetry,
|
|
6201
6668
|
error: message
|
|
6202
6669
|
});
|
|
6203
|
-
await
|
|
6670
|
+
await logDrainerWorkflowEvent(job.id, "retrying", {
|
|
6204
6671
|
kind: job.kind,
|
|
6205
6672
|
retryCount: nextRetry,
|
|
6206
6673
|
error: message
|
|
@@ -6208,41 +6675,41 @@ async function handleProcessError(job, error) {
|
|
|
6208
6675
|
return { status: "retry_scheduled", jobId: job.id, error: message, retryCount: nextRetry };
|
|
6209
6676
|
}
|
|
6210
6677
|
const finalMessage = `exceeded retry limit (${MAX_TRANSIENT_RETRIES} attempts). Last error: ${message}`;
|
|
6211
|
-
await
|
|
6212
|
-
await
|
|
6678
|
+
await localState2.asyncJobs.update(job.id, { status: "failed", error: finalMessage, retryCount: nextRetry });
|
|
6679
|
+
await logDrainerWorkflowEvent(job.id, "failed", { kind: job.kind, error: finalMessage, retryCount: nextRetry });
|
|
6213
6680
|
return { status: "failed", jobId: job.id, error: finalMessage };
|
|
6214
6681
|
}
|
|
6215
|
-
await
|
|
6216
|
-
await
|
|
6682
|
+
await localState2.asyncJobs.update(job.id, { status: "failed", error: message });
|
|
6683
|
+
await logDrainerWorkflowEvent(job.id, "failed", { kind: job.kind, error: message });
|
|
6217
6684
|
return { status: "failed", jobId: job.id, error: message };
|
|
6218
6685
|
}
|
|
6219
6686
|
async function processInitJob(job, api) {
|
|
6220
6687
|
if (job.kind !== "init") return { status: "skipped", jobId: job.id };
|
|
6221
|
-
const startedAt =
|
|
6688
|
+
const startedAt = collabDependencies.clock.now();
|
|
6222
6689
|
try {
|
|
6223
|
-
await
|
|
6224
|
-
await
|
|
6225
|
-
const bundleStat = await
|
|
6690
|
+
await localState2.asyncJobs.update(job.id, { status: "submitting", error: null });
|
|
6691
|
+
await logDrainerWorkflowEvent(job.id, "claimed", { kind: "init" });
|
|
6692
|
+
const bundleStat = await fs13.stat(job.payload.bundlePath);
|
|
6226
6693
|
const presignResp = await api.presignImportUploadFirstParty({
|
|
6227
6694
|
file: {
|
|
6228
|
-
name:
|
|
6695
|
+
name: path12.basename(job.payload.bundlePath),
|
|
6229
6696
|
mimeType: "application/x-git-bundle",
|
|
6230
6697
|
size: bundleStat.size,
|
|
6231
6698
|
checksumSha256: job.payload.bundleSha256
|
|
6232
6699
|
}
|
|
6233
6700
|
});
|
|
6234
6701
|
const presign = unwrapResponseObject(presignResp, "upload");
|
|
6235
|
-
await
|
|
6236
|
-
await
|
|
6237
|
-
await uploadPresigned({
|
|
6702
|
+
await localState2.asyncJobs.update(job.id, { status: "uploading" });
|
|
6703
|
+
await logDrainerWorkflowEvent(job.id, "uploading", { kind: "init" });
|
|
6704
|
+
await collabDependencies.uploadPresigned({
|
|
6238
6705
|
uploadUrl: String(presign.uploadUrl),
|
|
6239
6706
|
headers: presign.headers ?? {},
|
|
6240
6707
|
filePath: job.payload.bundlePath
|
|
6241
6708
|
});
|
|
6242
|
-
await
|
|
6709
|
+
await localState2.asyncJobs.update(job.id, { status: "server_processing" });
|
|
6243
6710
|
const importResp = await api.importFromUploadFirstParty({
|
|
6244
6711
|
uploadId: String(presign.uploadId),
|
|
6245
|
-
appName: job.payload.appName?.trim() ||
|
|
6712
|
+
appName: job.payload.appName?.trim() || path12.basename(job.repoRoot),
|
|
6246
6713
|
platform: "generic",
|
|
6247
6714
|
isPublic: false,
|
|
6248
6715
|
branch: job.payload.defaultBranch && job.branchName && job.branchName !== job.payload.defaultBranch ? job.payload.defaultBranch : job.branchName ?? void 0,
|
|
@@ -6252,11 +6719,14 @@ async function processInitJob(job, api) {
|
|
|
6252
6719
|
headCommitHash: job.payload.headCommitHash
|
|
6253
6720
|
});
|
|
6254
6721
|
const imported = unwrapResponseObject(importResp, "import");
|
|
6255
|
-
await
|
|
6722
|
+
await logDrainerWorkflowEvent(job.id, "server_accepted", {
|
|
6256
6723
|
kind: "init",
|
|
6257
6724
|
appId: String(imported.appId ?? "")
|
|
6258
6725
|
});
|
|
6259
|
-
const app = await pollAppReady(api, String(imported.appId)
|
|
6726
|
+
const app = await pollAppReady(api, String(imported.appId), {
|
|
6727
|
+
clock: collabDependencies.clock,
|
|
6728
|
+
timers: collabDependencies.timers
|
|
6729
|
+
});
|
|
6260
6730
|
let boundProjectId = String(app.projectId);
|
|
6261
6731
|
let boundCurrentAppId = String(app.id);
|
|
6262
6732
|
let boundUpstreamAppId = String(app.id);
|
|
@@ -6316,7 +6786,10 @@ async function processInitJob(job, api) {
|
|
|
6316
6786
|
}
|
|
6317
6787
|
}
|
|
6318
6788
|
if (boundCurrentAppId) {
|
|
6319
|
-
const readyApp = await pollAppReady(api, boundCurrentAppId
|
|
6789
|
+
const readyApp = await pollAppReady(api, boundCurrentAppId, {
|
|
6790
|
+
clock: collabDependencies.clock,
|
|
6791
|
+
timers: collabDependencies.timers
|
|
6792
|
+
});
|
|
6320
6793
|
boundProjectId = String(readyApp.projectId ?? boundProjectId);
|
|
6321
6794
|
boundThreadId = readyApp.threadId ? String(readyApp.threadId) : boundThreadId;
|
|
6322
6795
|
}
|
|
@@ -6339,7 +6812,7 @@ async function processInitJob(job, api) {
|
|
|
6339
6812
|
bindingMode: "lane"
|
|
6340
6813
|
};
|
|
6341
6814
|
}
|
|
6342
|
-
await
|
|
6815
|
+
await localState2.bindings.writeSnapshot({
|
|
6343
6816
|
repoRoot,
|
|
6344
6817
|
repoFingerprint: repoFingerprint ?? "",
|
|
6345
6818
|
remoteUrl,
|
|
@@ -6349,7 +6822,7 @@ async function processInitJob(job, api) {
|
|
|
6349
6822
|
});
|
|
6350
6823
|
} else {
|
|
6351
6824
|
if (canonicalLane && defaultBranch && branchName && branchName !== defaultBranch) {
|
|
6352
|
-
await
|
|
6825
|
+
await localState2.bindings.write(repoRoot, {
|
|
6353
6826
|
projectId: canonicalLane.projectId ?? null,
|
|
6354
6827
|
currentAppId: canonicalLane.currentAppId ?? boundCurrentAppId,
|
|
6355
6828
|
upstreamAppId: canonicalLane.upstreamAppId ?? canonicalLane.currentAppId ?? boundCurrentAppId,
|
|
@@ -6367,7 +6840,7 @@ async function processInitJob(job, api) {
|
|
|
6367
6840
|
bindingMode: "explicit_root"
|
|
6368
6841
|
});
|
|
6369
6842
|
}
|
|
6370
|
-
await
|
|
6843
|
+
await localState2.bindings.write(repoRoot, {
|
|
6371
6844
|
projectId: boundProjectId,
|
|
6372
6845
|
currentAppId: boundCurrentAppId,
|
|
6373
6846
|
upstreamAppId: boundUpstreamAppId,
|
|
@@ -6388,14 +6861,14 @@ async function processInitJob(job, api) {
|
|
|
6388
6861
|
currentAppId: boundCurrentAppId,
|
|
6389
6862
|
branchName
|
|
6390
6863
|
});
|
|
6391
|
-
await
|
|
6864
|
+
await logDrainerWorkflowEvent(job.id, "server_completed", {
|
|
6392
6865
|
kind: "init",
|
|
6393
6866
|
appId: boundCurrentAppId,
|
|
6394
6867
|
dashboardUrl: buildDashboardAppUrl(boundCurrentAppId),
|
|
6395
|
-
durationMs:
|
|
6868
|
+
durationMs: collabDependencies.clock.now() - startedAt
|
|
6396
6869
|
});
|
|
6397
|
-
await
|
|
6398
|
-
await
|
|
6870
|
+
await localState2.asyncJobs.update(job.id, { status: "completed", error: null });
|
|
6871
|
+
await logDrainerWorkflowEvent(job.id, "completed", { kind: "init", durationMs: collabDependencies.clock.now() - startedAt });
|
|
6399
6872
|
return { status: "completed", jobId: job.id };
|
|
6400
6873
|
} catch (error) {
|
|
6401
6874
|
return await handleProcessError(job, error);
|
|
@@ -6403,16 +6876,19 @@ async function processInitJob(job, api) {
|
|
|
6403
6876
|
}
|
|
6404
6877
|
async function processInitPostJob(job, api) {
|
|
6405
6878
|
if (job.kind !== "init_post") return { status: "skipped", jobId: job.id };
|
|
6406
|
-
const startedAt =
|
|
6879
|
+
const startedAt = collabDependencies.clock.now();
|
|
6407
6880
|
const syncPhaseMs = Math.max(0, job.payload.syncPhaseFinishedAt - job.payload.syncPhaseStartedAt);
|
|
6408
6881
|
try {
|
|
6409
|
-
await
|
|
6410
|
-
await
|
|
6882
|
+
await localState2.asyncJobs.update(job.id, { status: "server_processing", error: null });
|
|
6883
|
+
await logDrainerWorkflowEvent(job.id, "claimed", {
|
|
6411
6884
|
kind: "init_post",
|
|
6412
6885
|
appId: job.payload.appId,
|
|
6413
6886
|
syncPhaseMs
|
|
6414
6887
|
});
|
|
6415
|
-
await pollAppReady(api, job.payload.appId
|
|
6888
|
+
await pollAppReady(api, job.payload.appId, {
|
|
6889
|
+
clock: collabDependencies.clock,
|
|
6890
|
+
timers: collabDependencies.timers
|
|
6891
|
+
});
|
|
6416
6892
|
await seedImportedInitBaseline({
|
|
6417
6893
|
api,
|
|
6418
6894
|
repoRoot: job.repoRoot,
|
|
@@ -6421,9 +6897,9 @@ async function processInitPostJob(job, api) {
|
|
|
6421
6897
|
currentAppId: job.payload.appId,
|
|
6422
6898
|
branchName: job.branchName
|
|
6423
6899
|
});
|
|
6424
|
-
const asyncPhaseMs =
|
|
6425
|
-
await
|
|
6426
|
-
await
|
|
6900
|
+
const asyncPhaseMs = collabDependencies.clock.now() - startedAt;
|
|
6901
|
+
await localState2.asyncJobs.update(job.id, { status: "completed", error: null });
|
|
6902
|
+
await logDrainerWorkflowEvent(job.id, "completed", {
|
|
6427
6903
|
kind: "init_post",
|
|
6428
6904
|
appId: job.payload.appId,
|
|
6429
6905
|
durationMs: asyncPhaseMs,
|
|
@@ -6437,10 +6913,10 @@ async function processInitPostJob(job, api) {
|
|
|
6437
6913
|
} catch (error) {
|
|
6438
6914
|
const outcome = await handleProcessError(job, error);
|
|
6439
6915
|
if (outcome.status === "failed") {
|
|
6440
|
-
const bindingPath =
|
|
6916
|
+
const bindingPath = localState2.bindings.getPath(job.repoRoot);
|
|
6441
6917
|
try {
|
|
6442
|
-
await
|
|
6443
|
-
await
|
|
6918
|
+
await fs13.unlink(bindingPath);
|
|
6919
|
+
await logDrainerWorkflowEvent(job.id, "binding_cleared", {
|
|
6444
6920
|
kind: "init_post",
|
|
6445
6921
|
appId: job.payload.appId,
|
|
6446
6922
|
repoRoot: job.repoRoot,
|
|
@@ -6448,105 +6924,23 @@ async function processInitPostJob(job, api) {
|
|
|
6448
6924
|
reason: "init_post_terminal_failure"
|
|
6449
6925
|
});
|
|
6450
6926
|
} catch (cleanupError) {
|
|
6451
|
-
await
|
|
6927
|
+
await logDrainerWorkflowEvent(job.id, "binding_cleared", {
|
|
6452
6928
|
kind: "init_post",
|
|
6453
6929
|
appId: job.payload.appId,
|
|
6454
6930
|
repoRoot: job.repoRoot,
|
|
6455
6931
|
bindingPath,
|
|
6456
6932
|
reason: "init_post_terminal_failure",
|
|
6457
|
-
cleanupError:
|
|
6933
|
+
cleanupError: formatWorkflowError(cleanupError)
|
|
6458
6934
|
});
|
|
6459
6935
|
}
|
|
6460
6936
|
}
|
|
6461
6937
|
return outcome;
|
|
6462
6938
|
}
|
|
6463
6939
|
}
|
|
6464
|
-
async function processReAnchorJob(job, api) {
|
|
6465
|
-
if (job.kind !== "re_anchor") return { status: "skipped", jobId: job.id };
|
|
6466
|
-
const startedAt = Date.now();
|
|
6467
|
-
try {
|
|
6468
|
-
await updateAsyncJob(job.id, { status: "submitting", error: null });
|
|
6469
|
-
await logDrainerEvent(job.id, "claimed", { kind: "re_anchor" });
|
|
6470
|
-
const preflightResp = await api.preflightAppReconcile(job.payload.appId, {
|
|
6471
|
-
localHeadCommitHash: job.payload.localHeadCommitHash,
|
|
6472
|
-
repoFingerprint: job.repoFingerprint ?? void 0
|
|
6473
|
-
});
|
|
6474
|
-
const preflight = unwrapResponseObject(preflightResp, "reconcile preflight");
|
|
6475
|
-
if (preflight.status === "metadata_conflict") {
|
|
6476
|
-
throw new RemixError("Local repository metadata conflicts with the bound Remix app.", {
|
|
6477
|
-
exitCode: 2,
|
|
6478
|
-
hint: preflight.warnings.join("\n") || null
|
|
6479
|
-
});
|
|
6480
|
-
}
|
|
6481
|
-
let anchoredServerHeadHash = preflight.targetHeadCommitHash;
|
|
6482
|
-
if (preflight.status === "ready_to_reconcile") {
|
|
6483
|
-
const bundleStat = await fs12.stat(job.payload.bundlePath);
|
|
6484
|
-
const presignResp = await api.presignImportUploadFirstParty({
|
|
6485
|
-
file: {
|
|
6486
|
-
name: path11.basename(job.payload.bundlePath),
|
|
6487
|
-
mimeType: "application/x-git-bundle",
|
|
6488
|
-
size: bundleStat.size,
|
|
6489
|
-
checksumSha256: job.payload.bundleSha256
|
|
6490
|
-
}
|
|
6491
|
-
});
|
|
6492
|
-
const uploadTarget = unwrapResponseObject(presignResp, "import upload target");
|
|
6493
|
-
await updateAsyncJob(job.id, { status: "uploading" });
|
|
6494
|
-
await logDrainerEvent(job.id, "uploading", { kind: "re_anchor" });
|
|
6495
|
-
await uploadPresigned({
|
|
6496
|
-
uploadUrl: String(uploadTarget.uploadUrl),
|
|
6497
|
-
filePath: job.payload.bundlePath,
|
|
6498
|
-
headers: uploadTarget.headers ?? {}
|
|
6499
|
-
});
|
|
6500
|
-
await updateAsyncJob(job.id, { status: "server_processing" });
|
|
6501
|
-
const startResp = await api.startAppReconcile(job.payload.appId, {
|
|
6502
|
-
uploadId: String(uploadTarget.uploadId),
|
|
6503
|
-
localHeadCommitHash: job.payload.localHeadCommitHash,
|
|
6504
|
-
repoFingerprint: job.repoFingerprint ?? void 0,
|
|
6505
|
-
idempotencyKey: buildDeterministicIdempotencyKey({
|
|
6506
|
-
kind: "collab_re_anchor_v1",
|
|
6507
|
-
appId: job.payload.appId,
|
|
6508
|
-
localHeadCommitHash: job.payload.localHeadCommitHash,
|
|
6509
|
-
targetHeadCommitHash: job.payload.targetHeadCommitHash
|
|
6510
|
-
})
|
|
6511
|
-
});
|
|
6512
|
-
const started = unwrapResponseObject(startResp, "reconcile");
|
|
6513
|
-
await logDrainerEvent(job.id, "server_accepted", { kind: "re_anchor", appId: job.payload.appId });
|
|
6514
|
-
const reconcile = await pollReconcile(api, job.payload.appId, started.id);
|
|
6515
|
-
anchoredServerHeadHash = reconcile.reconciledHeadCommitHash ?? reconcile.targetHeadCommitHash ?? preflight.targetHeadCommitHash;
|
|
6516
|
-
}
|
|
6517
|
-
const snapshot = await captureLocalSnapshot({
|
|
6518
|
-
repoRoot: job.repoRoot,
|
|
6519
|
-
repoFingerprint: job.repoFingerprint,
|
|
6520
|
-
laneId: job.laneId,
|
|
6521
|
-
branchName: job.branchName
|
|
6522
|
-
});
|
|
6523
|
-
await writeLocalBaseline({
|
|
6524
|
-
repoRoot: job.repoRoot,
|
|
6525
|
-
repoFingerprint: job.repoFingerprint,
|
|
6526
|
-
laneId: job.laneId,
|
|
6527
|
-
currentAppId: job.payload.appId,
|
|
6528
|
-
branchName: job.branchName,
|
|
6529
|
-
lastSnapshotId: snapshot.id,
|
|
6530
|
-
lastSnapshotHash: snapshot.snapshotHash,
|
|
6531
|
-
lastServerHeadHash: anchoredServerHeadHash,
|
|
6532
|
-
lastSeenLocalCommitHash: snapshot.localCommitHash
|
|
6533
|
-
});
|
|
6534
|
-
await logDrainerEvent(job.id, "server_completed", {
|
|
6535
|
-
kind: "re_anchor",
|
|
6536
|
-
appId: job.payload.appId,
|
|
6537
|
-
durationMs: Date.now() - startedAt
|
|
6538
|
-
});
|
|
6539
|
-
await updateAsyncJob(job.id, { status: "completed", error: null });
|
|
6540
|
-
await logDrainerEvent(job.id, "completed", { kind: "re_anchor", durationMs: Date.now() - startedAt });
|
|
6541
|
-
return { status: "completed", jobId: job.id };
|
|
6542
|
-
} catch (error) {
|
|
6543
|
-
return await handleProcessError(job, error);
|
|
6544
|
-
}
|
|
6545
|
-
}
|
|
6546
6940
|
async function collabInitProcess(jobId, opts) {
|
|
6547
|
-
const claim = await
|
|
6941
|
+
const claim = await localState2.asyncJobs.claim(jobId);
|
|
6548
6942
|
if (!claim) {
|
|
6549
|
-
const job = await
|
|
6943
|
+
const job = await localState2.asyncJobs.read(jobId);
|
|
6550
6944
|
if (!job) return { status: "skipped", jobId };
|
|
6551
6945
|
return { status: "skipped", jobId };
|
|
6552
6946
|
}
|
|
@@ -6558,9 +6952,9 @@ async function collabInitProcess(jobId, opts) {
|
|
|
6558
6952
|
}
|
|
6559
6953
|
}
|
|
6560
6954
|
async function collabInitPostProcess(jobId, opts) {
|
|
6561
|
-
const claim = await
|
|
6955
|
+
const claim = await localState2.asyncJobs.claim(jobId);
|
|
6562
6956
|
if (!claim) {
|
|
6563
|
-
const job = await
|
|
6957
|
+
const job = await localState2.asyncJobs.read(jobId);
|
|
6564
6958
|
if (!job) return { status: "skipped", jobId };
|
|
6565
6959
|
return { status: "skipped", jobId };
|
|
6566
6960
|
}
|
|
@@ -6571,25 +6965,11 @@ async function collabInitPostProcess(jobId, opts) {
|
|
|
6571
6965
|
await claim.release();
|
|
6572
6966
|
}
|
|
6573
6967
|
}
|
|
6574
|
-
async function collabReAnchorProcess(jobId, opts) {
|
|
6575
|
-
const claim = await claimAsyncJob(jobId);
|
|
6576
|
-
if (!claim) {
|
|
6577
|
-
const job = await readAsyncJob(jobId);
|
|
6578
|
-
if (!job) return { status: "skipped", jobId };
|
|
6579
|
-
return { status: "skipped", jobId };
|
|
6580
|
-
}
|
|
6581
|
-
try {
|
|
6582
|
-
if (claim.job.kind !== "re_anchor") return { status: "skipped", jobId };
|
|
6583
|
-
return await processReAnchorJob(claim.job, opts.api);
|
|
6584
|
-
} finally {
|
|
6585
|
-
await claim.release();
|
|
6586
|
-
}
|
|
6587
|
-
}
|
|
6588
6968
|
async function acquireDrainerPidLock() {
|
|
6589
6969
|
const pidPath = getDrainerPidPath();
|
|
6590
|
-
await
|
|
6970
|
+
await fs13.mkdir(path12.dirname(pidPath), { recursive: true });
|
|
6591
6971
|
try {
|
|
6592
|
-
const existing = await
|
|
6972
|
+
const existing = await fs13.readFile(pidPath, "utf8").catch(() => "");
|
|
6593
6973
|
const existingPid = parseInt(existing.trim(), 10);
|
|
6594
6974
|
if (Number.isFinite(existingPid) && existingPid > 0 && existingPid !== process.pid) {
|
|
6595
6975
|
try {
|
|
@@ -6599,13 +6979,13 @@ async function acquireDrainerPidLock() {
|
|
|
6599
6979
|
if (error?.code !== "ESRCH") return null;
|
|
6600
6980
|
}
|
|
6601
6981
|
}
|
|
6602
|
-
await
|
|
6982
|
+
await fs13.writeFile(pidPath, String(process.pid), "utf8");
|
|
6603
6983
|
return {
|
|
6604
6984
|
release: async () => {
|
|
6605
6985
|
try {
|
|
6606
|
-
const current = (await
|
|
6986
|
+
const current = (await fs13.readFile(pidPath, "utf8")).trim();
|
|
6607
6987
|
if (current === String(process.pid)) {
|
|
6608
|
-
await
|
|
6988
|
+
await fs13.unlink(pidPath).catch(() => void 0);
|
|
6609
6989
|
}
|
|
6610
6990
|
} catch {
|
|
6611
6991
|
}
|
|
@@ -6618,31 +6998,30 @@ async function acquireDrainerPidLock() {
|
|
|
6618
6998
|
async function drainAsyncJobs(opts) {
|
|
6619
6999
|
const lockHandle = opts.respectPidLock === false ? null : await acquireDrainerPidLock();
|
|
6620
7000
|
if (opts.respectPidLock !== false && !lockHandle) return [];
|
|
6621
|
-
const outcomes = [];
|
|
6622
7001
|
try {
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
}
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
7002
|
+
return await drainClaimedJobs({
|
|
7003
|
+
prune: async () => {
|
|
7004
|
+
try {
|
|
7005
|
+
await localState2.asyncJobs.pruneTerminal();
|
|
7006
|
+
} catch {
|
|
7007
|
+
}
|
|
7008
|
+
},
|
|
7009
|
+
list: () => localState2.asyncJobs.list(),
|
|
7010
|
+
isTerminal: (job) => planAsyncJobDrainTransition(job).terminal,
|
|
7011
|
+
claim: async (job) => ({
|
|
7012
|
+
job,
|
|
7013
|
+
/* v8 ignore next -- async job processors perform their own claim/release cycle. */
|
|
7014
|
+
release: async () => void 0
|
|
7015
|
+
}),
|
|
7016
|
+
process: async ({ job }) => {
|
|
7017
|
+
const transition = planAsyncJobDrainTransition(job);
|
|
7018
|
+
return transition.action === "process_init" ? collabInitProcess(job.id, { api: opts.api }) : collabInitPostProcess(job.id, { api: opts.api });
|
|
7019
|
+
},
|
|
7020
|
+
onError: "throw"
|
|
7021
|
+
});
|
|
6642
7022
|
} finally {
|
|
6643
7023
|
if (lockHandle) await lockHandle.release();
|
|
6644
7024
|
}
|
|
6645
|
-
return outcomes;
|
|
6646
7025
|
}
|
|
6647
7026
|
export {
|
|
6648
7027
|
FINALIZE_JOB_LOCK_STALE_MS,
|