@remixhq/core 0.1.33 → 0.1.35

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CoreConfig } from './config.js';
2
2
  import { T as TokenProvider } from './tokenProvider-B9so5Pm3.js';
3
- import { T as TurnUsage } from './contracts-WCYiMqwB.js';
3
+ import { T as TurnUsage } from './contracts-BqC3Lj7W.js';
4
4
  import 'zod';
5
5
 
6
6
  type HistoryImportProvider = "claude_code" | "cursor";
package/dist/collab.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CollabApiClient, a as TurnUsagePayload, b as CollabFinalizeTurnResult, c as CollabRecordingPreflight, d as CollabApproveMode, e as CollabApprovalStrategy, f as CollabApproveResult, M as MergeRequestQueue, g as MergeRequest, I as InvitationScopeType, h as CollabMember, A as AppProfileInput, J as JsonObject, i as AppDeltaResponse, j as CollabStatus, k as MergeRequestReview } from './contracts-WCYiMqwB.js';
2
- export { l as AppHeadResponse, m as AppMember, n as AppMemberRole, o as AppReconcileResponse, p as CollabFinalizeTurnAutoSync, q as CollabFinalizeTurnMode, r as CollabPendingFinalizeState, s as CollabPendingFinalizeSummary, t as CollabRecordingPreflightStatus, u as CollabRepoStateKind, v as CollabStatusBlockedReason, w as CollabStatusRecommendedAction, x as CollabTurn, y as MembershipScopeType, z as MergeRequestStatus, B as ModelCall, O as OrganizationMember, D as OrganizationMemberRole, P as ProjectMember, E as ProjectMemberRole, R as ReconcilePreflightResponse, S as ServerToolUsage, F as SyncLocalResponse, G as SyncUpstreamResponse, T as TurnUsage, H as TurnUsageCaptureSource, K as TurnUsageConfidence, L as TurnUsagePreviousPatch } from './contracts-WCYiMqwB.js';
1
+ import { C as CollabApiClient, a as TurnUsagePayload, b as CollabFinalizeTurnResult, c as CollabRecordingPreflight, d as CollabApproveMode, e as CollabApprovalStrategy, f as CollabApproveResult, M as MergeRequestQueue, g as MergeRequest, I as InvitationScopeType, h as CollabMember, A as AppProfileInput, J as JsonObject, i as AppDeltaResponse, j as CollabStatus, k as MergeRequestReview } from './contracts-BqC3Lj7W.js';
2
+ export { l as AppHeadResponse, m as AppMember, n as AppMemberRole, o as AppReconcileResponse, p as CollabFinalizeTurnAutoSync, q as CollabFinalizeTurnMode, r as CollabPendingFinalizeState, s as CollabPendingFinalizeSummary, t as CollabRecordingPreflightStatus, u as CollabRepoStateKind, v as CollabStatusBlockedReason, w as CollabStatusRecommendedAction, x as CollabTurn, y as MembershipScopeType, z as MergeRequestStatus, B as ModelCall, O as OrganizationMember, D as OrganizationMemberRole, P as ProjectMember, E as ProjectMemberRole, R as ReconcilePreflightResponse, S as ServerToolUsage, F as SyncLocalResponse, G as SyncUpstreamResponse, T as TurnUsage, H as TurnUsageCaptureSource, K as TurnUsageConfidence, L as TurnUsagePreviousPatch } from './contracts-BqC3Lj7W.js';
3
3
  import { B as BranchBindingMode } from './binding-WiIRI2fl.js';
4
4
 
5
5
  declare function collabFinalizeTurn(params: {
package/dist/collab.js CHANGED
@@ -509,6 +509,13 @@ import path3 from "path";
509
509
  function getBaselinePath(params) {
510
510
  return path3.join(getBaselinesRoot(), `${buildLaneStateKey(params)}.json`);
511
511
  }
512
+ function normalizeNullableString(value) {
513
+ const normalized = String(value ?? "").trim();
514
+ return normalized || null;
515
+ }
516
+ function isSameBaselineIdentity(left, right) {
517
+ return left.repoRoot === right.repoRoot && (left.repoFingerprint ?? null) === (right.repoFingerprint ?? null) && (left.laneId ?? null) === (right.laneId ?? null) && left.currentAppId === right.currentAppId && (left.branchName ?? null) === (right.branchName ?? null);
518
+ }
512
519
  async function readLocalBaseline(params) {
513
520
  try {
514
521
  const raw = await fs2.readFile(getBaselinePath(params), "utf8");
@@ -558,6 +565,105 @@ async function writeLocalBaseline(baseline) {
558
565
  await writeJsonAtomic(getBaselinePath(baseline), normalized);
559
566
  return normalized;
560
567
  }
568
+ async function readAllLocalBaselines() {
569
+ let entries;
570
+ try {
571
+ entries = await fs2.readdir(getBaselinesRoot());
572
+ } catch (error) {
573
+ if (error?.code === "ENOENT") return [];
574
+ throw error;
575
+ }
576
+ const baselines = [];
577
+ for (const entry of entries) {
578
+ if (!entry.endsWith(".json")) continue;
579
+ try {
580
+ const raw = await fs2.readFile(path3.join(getBaselinesRoot(), entry), "utf8");
581
+ const parsed = JSON.parse(raw);
582
+ if (!parsed || typeof parsed !== "object") continue;
583
+ if (![1, 2].includes(Number(parsed.schemaVersion)) || typeof parsed.key !== "string" || typeof parsed.repoRoot !== "string") {
584
+ continue;
585
+ }
586
+ baselines.push({
587
+ schemaVersion: Number(parsed.schemaVersion) === 2 ? 2 : 1,
588
+ key: parsed.key,
589
+ repoRoot: parsed.repoRoot,
590
+ repoFingerprint: parsed.repoFingerprint ?? null,
591
+ laneId: parsed.laneId ?? null,
592
+ currentAppId: String(parsed.currentAppId ?? ""),
593
+ branchName: parsed.branchName ?? null,
594
+ lastSnapshotId: parsed.lastSnapshotId ?? null,
595
+ lastSnapshotHash: parsed.lastSnapshotHash ?? null,
596
+ lastServerRevisionId: parsed.lastServerRevisionId ?? null,
597
+ lastServerTreeHash: parsed.lastServerTreeHash ?? null,
598
+ lastServerHeadHash: parsed.lastServerHeadHash ?? null,
599
+ lastSeenLocalCommitHash: parsed.lastSeenLocalCommitHash ?? null,
600
+ updatedAt: String(parsed.updatedAt ?? "")
601
+ });
602
+ } catch {
603
+ continue;
604
+ }
605
+ }
606
+ return baselines;
607
+ }
608
+ async function migrateLocalBaselineForLaneChange(params) {
609
+ if ((params.fromLaneId ?? null) === (params.toLaneId ?? null)) {
610
+ return { status: "same_lane" };
611
+ }
612
+ const target = await readLocalBaseline({
613
+ repoRoot: params.repoRoot,
614
+ repoFingerprint: params.repoFingerprint,
615
+ laneId: params.toLaneId
616
+ });
617
+ if (target) {
618
+ return { status: "target_exists" };
619
+ }
620
+ const source = await readLocalBaseline({
621
+ repoRoot: params.repoRoot,
622
+ repoFingerprint: params.repoFingerprint,
623
+ laneId: params.fromLaneId
624
+ });
625
+ if (!source) {
626
+ return { status: "source_missing" };
627
+ }
628
+ const expectedSource = {
629
+ repoRoot: params.repoRoot,
630
+ repoFingerprint: params.repoFingerprint,
631
+ laneId: params.fromLaneId,
632
+ currentAppId: params.currentAppId,
633
+ branchName: params.branchName
634
+ };
635
+ const actualSource = {
636
+ repoRoot: source.repoRoot,
637
+ repoFingerprint: source.repoFingerprint,
638
+ laneId: source.laneId,
639
+ currentAppId: source.currentAppId,
640
+ branchName: source.branchName
641
+ };
642
+ if (!isSameBaselineIdentity(actualSource, expectedSource)) {
643
+ return { status: "source_missing" };
644
+ }
645
+ const baseline = await writeLocalBaseline({
646
+ ...source,
647
+ laneId: params.toLaneId
648
+ });
649
+ return { status: "migrated", baseline };
650
+ }
651
+ async function findCompatibleLocalBaseline(params) {
652
+ const targetKey = buildLaneStateKey(params);
653
+ const branchName = normalizeNullableString(params.branchName);
654
+ const candidates = (await readAllLocalBaselines()).filter((baseline) => {
655
+ if (baseline.key === targetKey) return false;
656
+ if (baseline.repoRoot !== params.repoRoot) return false;
657
+ if ((baseline.repoFingerprint ?? null) !== (params.repoFingerprint ?? null)) return false;
658
+ if (baseline.currentAppId !== params.currentAppId) return false;
659
+ return true;
660
+ });
661
+ const branchMatches = candidates.filter((baseline) => normalizeNullableString(baseline.branchName) === branchName);
662
+ const compatible = branchMatches.length > 0 ? branchMatches : candidates.filter((baseline) => normalizeNullableString(baseline.branchName) === null);
663
+ if (compatible.length === 0) return { status: "none", candidateCount: 0 };
664
+ if (compatible.length > 1) return { status: "ambiguous", candidateCount: compatible.length };
665
+ return { status: "found", baseline: compatible[0] };
666
+ }
561
667
  async function clearLocalBaseline(params) {
562
668
  try {
563
669
  await fs2.unlink(getBaselinePath(params));
@@ -1500,6 +1606,7 @@ function resolveLaneLookupProjectId(params) {
1500
1606
  return params.explicitRootProjectId ?? (params.requireRemoteLane ? void 0 : localProjectId ?? params.fallbackProjectId ?? void 0);
1501
1607
  }
1502
1608
  async function persistResolvedLane(repoRoot, binding) {
1609
+ const previousBinding = await readCollabBinding(repoRoot);
1503
1610
  await writeCollabBinding(repoRoot, {
1504
1611
  projectId: binding.projectId,
1505
1612
  currentAppId: binding.currentAppId,
@@ -1512,6 +1619,16 @@ async function persistResolvedLane(repoRoot, binding) {
1512
1619
  branchName: binding.branchName,
1513
1620
  bindingMode: binding.bindingMode
1514
1621
  });
1622
+ if (previousBinding && (previousBinding.laneId ?? null) !== (binding.laneId ?? null)) {
1623
+ await migrateLocalBaselineForLaneChange({
1624
+ repoRoot,
1625
+ repoFingerprint: binding.repoFingerprint,
1626
+ currentAppId: binding.currentAppId,
1627
+ branchName: binding.branchName,
1628
+ fromLaneId: previousBinding.laneId,
1629
+ toLaneId: binding.laneId
1630
+ });
1631
+ }
1515
1632
  return readCollabBinding(repoRoot);
1516
1633
  }
1517
1634
  function buildAmbiguousResolution(params) {
@@ -1911,11 +2028,36 @@ async function collabDetectRepoState(params) {
1911
2028
  return detected;
1912
2029
  }
1913
2030
  try {
1914
- const baseline = await readLocalBaseline({
2031
+ let baseline = await readLocalBaseline({
1915
2032
  repoFingerprint: binding.repoFingerprint,
1916
2033
  laneId: binding.laneId,
1917
2034
  repoRoot
1918
2035
  });
2036
+ let ambiguousBaselineCandidateCount = null;
2037
+ if (!baseline) {
2038
+ const compatibleBaseline = await findCompatibleLocalBaseline({
2039
+ repoRoot,
2040
+ repoFingerprint: binding.repoFingerprint,
2041
+ laneId: binding.laneId,
2042
+ currentAppId: binding.currentAppId,
2043
+ branchName: binding.branchName
2044
+ });
2045
+ if (compatibleBaseline.status === "found") {
2046
+ baseline = await writeLocalBaseline({
2047
+ ...compatibleBaseline.baseline,
2048
+ repoRoot,
2049
+ repoFingerprint: binding.repoFingerprint,
2050
+ laneId: binding.laneId,
2051
+ currentAppId: binding.currentAppId,
2052
+ branchName: binding.branchName
2053
+ });
2054
+ detected.warnings.push(
2055
+ `Recovered local Remix baseline after lane id changed from ${compatibleBaseline.baseline.laneId ?? "null"} to ${binding.laneId ?? "null"} for app ${binding.currentAppId} on branch ${binding.branchName ?? "(unknown)"}.`
2056
+ );
2057
+ } else if (compatibleBaseline.status === "ambiguous") {
2058
+ ambiguousBaselineCandidateCount = compatibleBaseline.candidateCount;
2059
+ }
2060
+ }
1919
2061
  const hasFullBaseline = Boolean(baseline?.lastSnapshotHash && baseline?.lastServerHeadHash);
1920
2062
  const metadataIdentityPromise = hasFullBaseline ? getAppDeltaCached(params.api, binding.currentAppId, {
1921
2063
  baseHeadHash: baseline.lastServerHeadHash,
@@ -1988,7 +2130,7 @@ async function collabDetectRepoState(params) {
1988
2130
  }
1989
2131
  }
1990
2132
  detected.repoState = "external_local_base_changed";
1991
- detected.hint = "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
2133
+ detected.hint = ambiguousBaselineCandidateCount !== null ? `No local Remix revision baseline exists for this lane, and multiple compatible local baselines were found (${ambiguousBaselineCandidateCount}). Run \`remix collab status\` or sync this lane to seed an unambiguous baseline.` : "No local Remix revision baseline exists for this lane yet. Run `remix collab init` or sync this lane to seed the baseline.";
1992
2134
  return detected;
1993
2135
  }
1994
2136
  const localHeadMovedSinceBaseline = Boolean(baseline.lastSeenLocalCommitHash) && localCommitHash !== baseline.lastSeenLocalCommitHash;
@@ -3098,11 +3240,174 @@ async function drainPendingFinalizeQueue(params) {
3098
3240
 
3099
3241
  // src/application/collab/collabFinalizeTurn.ts
3100
3242
  var FINALIZE_AWAIT_INIT_POST_TIMEOUT_MS = 6e4;
3243
+ var FINALIZE_UNALIGNED_DRAIN_TIMEOUT_MS = 1e4;
3101
3244
  function collectWarnings(value) {
3102
3245
  if (!Array.isArray(value)) return [];
3103
3246
  return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
3104
3247
  }
3248
+ function throwBlockingDetectedStatus(detected) {
3249
+ if (detected.status === "not_bound") {
3250
+ throw new RemixError("Repository is not bound to Remix.", {
3251
+ code: "not_bound",
3252
+ exitCode: 2,
3253
+ hint: detected.hint
3254
+ });
3255
+ }
3256
+ if (detected.status === "branch_binding_missing") {
3257
+ throw new RemixError(detected.hint || "Current branch is not ready for Remix recording.", {
3258
+ code: detected.status,
3259
+ exitCode: 2,
3260
+ hint: detected.hint
3261
+ });
3262
+ }
3263
+ if (detected.status === "family_ambiguous") {
3264
+ throw new RemixError(detected.hint || "Current branch is not ready for Remix recording.", {
3265
+ code: detected.status,
3266
+ exitCode: 2,
3267
+ hint: detected.hint
3268
+ });
3269
+ }
3270
+ if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
3271
+ throw new RemixError("Repository must be realigned before finalizing the turn.", {
3272
+ code: detected.status,
3273
+ exitCode: 2,
3274
+ hint: detected.hint
3275
+ });
3276
+ }
3277
+ if (detected.status === "remote_error") {
3278
+ const message = detected.hint || "Failed to determine the current repo state.";
3279
+ throw new RemixError(message, {
3280
+ code: detected.status,
3281
+ exitCode: 1,
3282
+ hint: detected.hint
3283
+ });
3284
+ }
3285
+ }
3286
+ function withTimeout(work, timeoutMs, message) {
3287
+ let timer;
3288
+ const timeout = new Promise((_, reject) => {
3289
+ timer = setTimeout(() => reject(new Error(message)), timeoutMs);
3290
+ timer.unref?.();
3291
+ });
3292
+ return Promise.race([work, timeout]).finally(() => {
3293
+ if (timer) clearTimeout(timer);
3294
+ });
3295
+ }
3296
+ async function drainPendingFinalizeBeforeUnalignedFallback(params) {
3297
+ const summary = await summarizePendingFinalizeJobs({
3298
+ repoRoot: params.repoRoot,
3299
+ repoFingerprint: params.repoFingerprint,
3300
+ currentAppId: params.currentAppId,
3301
+ laneId: params.laneId
3302
+ });
3303
+ if (summary.activeJobCount <= 0) {
3304
+ return { attempted: false, warnings: [] };
3305
+ }
3306
+ const warnings = [];
3307
+ try {
3308
+ await withTimeout(
3309
+ drainPendingFinalizeQueue({ api: params.api }),
3310
+ FINALIZE_UNALIGNED_DRAIN_TIMEOUT_MS,
3311
+ `Timed out waiting ${Math.round(
3312
+ FINALIZE_UNALIGNED_DRAIN_TIMEOUT_MS / 1e3
3313
+ )}s for pending Remix turn recording to advance the local baseline.`
3314
+ );
3315
+ } catch (error) {
3316
+ const message = error instanceof Error ? error.message : String(error);
3317
+ warnings.push(`Pending Remix turn recording could not be drained before this turn was captured: ${message}`);
3318
+ } finally {
3319
+ invalidateAppHeadCache(params.currentAppId);
3320
+ invalidateAppDeltaCacheForApp(params.currentAppId);
3321
+ }
3322
+ return { attempted: true, warnings };
3323
+ }
3105
3324
  var FINALIZE_QUEUED_WARNING = "Queued only: the local Remix turn was captured, but no remote change step or collab turn exists yet. Drain or await finalize before merge-related flows.";
3325
+ var CONVERSATION_ONLY_WARNING = "Recorded conversation only: this turn was not materialized into a change step.";
3326
+ function buildConversationOnlyWorkspaceMetadata(params) {
3327
+ const metadata = {
3328
+ branch: params.branchName,
3329
+ repoRoot: params.repoRoot,
3330
+ remoteUrl: params.remoteUrl,
3331
+ defaultBranch: params.defaultBranch,
3332
+ recordingMode: "conversation_only",
3333
+ materializationStatus: params.materializationStatus,
3334
+ alignmentRequired: params.alignmentRequired,
3335
+ baselineSnapshotId: params.baselineSnapshotId,
3336
+ baselineServerRevisionId: params.baselineServerRevisionId,
3337
+ baselineServerHeadHash: params.baselineServerHeadHash,
3338
+ currentServerRevisionId: params.currentServerRevisionId,
3339
+ currentServerHeadHash: params.currentServerHeadHash,
3340
+ currentServerHeadCommitId: params.currentServerHeadCommitId,
3341
+ localCommitHash: params.localCommitHash,
3342
+ repoStateAtCapture: params.repoState,
3343
+ actor: params.actor ?? null
3344
+ };
3345
+ if (params.turnUsage) {
3346
+ metadata.turnUsage = params.turnUsage;
3347
+ }
3348
+ if (typeof params.promptedAt === "string" && params.promptedAt.trim()) {
3349
+ metadata.promptedAt = params.promptedAt.trim();
3350
+ }
3351
+ return metadata;
3352
+ }
3353
+ async function recordConversationOnlyTurn(params) {
3354
+ const idempotencyKey = params.explicitIdempotencyKey?.trim() || buildDeterministicIdempotencyKey({
3355
+ kind: "collab_finalize_turn_conversation_only_v1",
3356
+ appId: params.binding.currentAppId,
3357
+ laneId: params.binding.laneId,
3358
+ materializationStatus: params.materializationStatus,
3359
+ alignmentRequired: params.alignmentRequired,
3360
+ baselineSnapshotId: params.baselineSnapshotId ?? null,
3361
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
3362
+ baselineServerHeadHash: params.baselineServerHeadHash ?? null,
3363
+ currentServerRevisionId: params.currentServerRevisionId ?? null,
3364
+ currentServerHeadHash: params.currentServerHeadHash ?? null,
3365
+ currentServerHeadCommitId: params.currentServerHeadCommitId ?? null,
3366
+ localCommitHash: params.localCommitHash ?? null,
3367
+ repoState: params.repoState,
3368
+ prompt: params.prompt,
3369
+ assistantResponse: params.assistantResponse
3370
+ });
3371
+ const collabTurnResp = await params.api.createCollabTurn(params.binding.currentAppId, {
3372
+ threadId: params.binding.threadId ?? void 0,
3373
+ collabLaneId: params.binding.laneId ?? void 0,
3374
+ prompt: params.prompt,
3375
+ assistantResponse: params.assistantResponse,
3376
+ actor: params.actor,
3377
+ workspaceMetadata: buildConversationOnlyWorkspaceMetadata({
3378
+ repoRoot: params.repoRoot,
3379
+ branchName: params.binding.branchName,
3380
+ remoteUrl: params.binding.remoteUrl,
3381
+ defaultBranch: params.binding.defaultBranch,
3382
+ baselineSnapshotId: params.baselineSnapshotId ?? null,
3383
+ baselineServerRevisionId: params.baselineServerRevisionId ?? null,
3384
+ baselineServerHeadHash: params.baselineServerHeadHash ?? null,
3385
+ currentServerRevisionId: params.currentServerRevisionId ?? null,
3386
+ currentServerHeadHash: params.currentServerHeadHash ?? null,
3387
+ currentServerHeadCommitId: params.currentServerHeadCommitId ?? null,
3388
+ localCommitHash: params.localCommitHash ?? null,
3389
+ repoState: params.repoState,
3390
+ materializationStatus: params.materializationStatus,
3391
+ alignmentRequired: params.alignmentRequired,
3392
+ actor: params.actor ?? null,
3393
+ turnUsage: params.turnUsage ?? null,
3394
+ promptedAt: params.promptedAt
3395
+ }),
3396
+ idempotencyKey
3397
+ });
3398
+ const collabTurn = unwrapResponseObject(collabTurnResp, "collab turn");
3399
+ return {
3400
+ mode: "conversation_only",
3401
+ idempotencyKey,
3402
+ queued: false,
3403
+ jobId: null,
3404
+ repoState: params.repoState,
3405
+ changeStep: null,
3406
+ collabTurn,
3407
+ autoSync: null,
3408
+ warnings: [CONVERSATION_ONLY_WARNING, ...collectWarnings(params.warnings ?? [])]
3409
+ };
3410
+ }
3106
3411
  async function collabFinalizeTurn(params) {
3107
3412
  const repoRoot = await findGitRoot(params.cwd);
3108
3413
  const binding = await ensureActiveLaneBinding({
@@ -3171,6 +3476,24 @@ async function collabFinalizeTurn(params) {
3171
3476
  ` : "") + "Run `remix collab init` again to retry \u2014 the post-init drainer cleared the local binding so this is safe."
3172
3477
  });
3173
3478
  }
3479
+ try {
3480
+ return await recordConversationOnlyTurn({
3481
+ api: params.api,
3482
+ binding,
3483
+ repoRoot,
3484
+ prompt,
3485
+ assistantResponse,
3486
+ explicitIdempotencyKey: params.idempotencyKey,
3487
+ actor: params.actor,
3488
+ turnUsage: params.turnUsage,
3489
+ promptedAt: params.promptedAt,
3490
+ repoState: "init_post_pending",
3491
+ materializationStatus: "skipped_init_post_pending",
3492
+ alignmentRequired: "init_post",
3493
+ warnings: [`Init post job ${pendingInitPost.id} is still pending; recorded conversation only.`]
3494
+ });
3495
+ } catch {
3496
+ }
3174
3497
  throw new RemixError("Timed out waiting for the initial Remix import to finish provisioning.", {
3175
3498
  exitCode: 2,
3176
3499
  hint: `Init post job ${pendingInitPost.id} did not complete within ${Math.round(
@@ -3179,67 +3502,95 @@ async function collabFinalizeTurn(params) {
3179
3502
  });
3180
3503
  }
3181
3504
  }
3182
- const detected = await collabDetectRepoState({
3505
+ let detected = await collabDetectRepoState({
3183
3506
  api: params.api,
3184
3507
  cwd: repoRoot,
3185
3508
  allowBranchMismatch: params.allowBranchMismatch
3186
3509
  });
3187
- if (detected.status === "not_bound") {
3188
- throw new RemixError("Repository is not bound to Remix.", {
3189
- code: "not_bound",
3190
- exitCode: 2,
3191
- hint: detected.hint
3192
- });
3193
- }
3194
- if (detected.status === "branch_binding_missing") {
3195
- throw new RemixError(detected.hint || "Current branch is not ready for Remix recording.", {
3196
- code: detected.status,
3197
- exitCode: 2,
3198
- hint: detected.hint
3199
- });
3200
- }
3201
- if (detected.status === "family_ambiguous") {
3202
- throw new RemixError(detected.hint || "Current branch is not ready for Remix recording.", {
3203
- code: detected.status,
3204
- exitCode: 2,
3205
- hint: detected.hint
3206
- });
3207
- }
3208
- if (detected.status === "metadata_conflict" || detected.status === "branch_mismatch") {
3209
- throw new RemixError("Repository must be realigned before finalizing the turn.", {
3210
- code: detected.status,
3211
- exitCode: 2,
3212
- hint: detected.hint
3213
- });
3214
- }
3215
- if (detected.status === "missing_head" || detected.status === "remote_error") {
3216
- const message = detected.hint || "Failed to determine the current repo state.";
3217
- throw new RemixError(message, {
3218
- code: detected.status,
3219
- exitCode: 1,
3220
- hint: detected.hint
3510
+ throwBlockingDetectedStatus(detected);
3511
+ let pendingFinalizeDrainWarnings = [];
3512
+ if (detected.repoState === "server_only_changed" || detected.repoState === "both_changed") {
3513
+ const recovery = await drainPendingFinalizeBeforeUnalignedFallback({
3514
+ api: params.api,
3515
+ repoRoot,
3516
+ repoFingerprint: binding.repoFingerprint,
3517
+ currentAppId: binding.currentAppId,
3518
+ laneId: binding.laneId
3221
3519
  });
3520
+ if (recovery.attempted) {
3521
+ pendingFinalizeDrainWarnings = recovery.warnings;
3522
+ detected = await collabDetectRepoState({
3523
+ api: params.api,
3524
+ cwd: repoRoot,
3525
+ allowBranchMismatch: params.allowBranchMismatch
3526
+ });
3527
+ throwBlockingDetectedStatus(detected);
3528
+ }
3222
3529
  }
3223
- if (detected.repoState === "server_only_changed") {
3224
- throw new RemixError("Server changes must be pulled locally before finalizing this turn.", {
3225
- code: "pull_required",
3226
- exitCode: 2,
3227
- hint: detected.hint
3530
+ if (detected.status === "missing_head") {
3531
+ return recordConversationOnlyTurn({
3532
+ api: params.api,
3533
+ binding,
3534
+ repoRoot,
3535
+ prompt,
3536
+ assistantResponse,
3537
+ explicitIdempotencyKey: params.idempotencyKey,
3538
+ actor: params.actor,
3539
+ turnUsage: params.turnUsage,
3540
+ promptedAt: params.promptedAt,
3541
+ repoState: "missing_head",
3542
+ materializationStatus: "skipped_missing_head",
3543
+ alignmentRequired: "head",
3544
+ localCommitHash: null,
3545
+ warnings: [...pendingFinalizeDrainWarnings, ...detected.warnings]
3228
3546
  });
3229
3547
  }
3230
- if (detected.repoState === "both_changed") {
3231
- const hint = detected.hint || "Run `remix collab reconcile --dry-run` to inspect recovery options before retrying.";
3232
- throw new RemixError("Local and server changes must be reconciled before finalizing this turn.", {
3233
- code: "reconcile_required",
3234
- exitCode: 2,
3235
- hint
3548
+ if (detected.repoState === "server_only_changed" || detected.repoState === "both_changed") {
3549
+ return recordConversationOnlyTurn({
3550
+ api: params.api,
3551
+ binding,
3552
+ repoRoot,
3553
+ prompt,
3554
+ assistantResponse,
3555
+ explicitIdempotencyKey: params.idempotencyKey,
3556
+ actor: params.actor,
3557
+ turnUsage: params.turnUsage,
3558
+ promptedAt: params.promptedAt,
3559
+ repoState: detected.repoState,
3560
+ materializationStatus: "skipped_unaligned",
3561
+ alignmentRequired: detected.repoState === "both_changed" ? "reconcile" : "sync",
3562
+ baselineSnapshotId: detected.baseline.lastSnapshotId,
3563
+ baselineServerRevisionId: detected.baseline.lastServerRevisionId,
3564
+ baselineServerHeadHash: detected.baseline.lastServerHeadHash,
3565
+ currentServerRevisionId: detected.currentServerRevisionId,
3566
+ currentServerHeadHash: detected.currentServerHeadHash,
3567
+ currentServerHeadCommitId: detected.currentServerHeadCommitId,
3568
+ localCommitHash: detected.localCommitHash,
3569
+ warnings: [...pendingFinalizeDrainWarnings, ...detected.warnings]
3236
3570
  });
3237
3571
  }
3238
3572
  if (detected.repoState === "external_local_base_changed") {
3239
- throw new RemixError("The local checkout is missing a Remix revision baseline for this lane.", {
3240
- code: "baseline_missing",
3241
- exitCode: 2,
3242
- hint: detected.hint
3573
+ return recordConversationOnlyTurn({
3574
+ api: params.api,
3575
+ binding,
3576
+ repoRoot,
3577
+ prompt,
3578
+ assistantResponse,
3579
+ explicitIdempotencyKey: params.idempotencyKey,
3580
+ actor: params.actor,
3581
+ turnUsage: params.turnUsage,
3582
+ promptedAt: params.promptedAt,
3583
+ repoState: "external_local_base_changed",
3584
+ materializationStatus: "skipped_baseline_missing",
3585
+ alignmentRequired: "baseline",
3586
+ baselineSnapshotId: detected.baseline.lastSnapshotId,
3587
+ baselineServerRevisionId: detected.baseline.lastServerRevisionId,
3588
+ baselineServerHeadHash: detected.baseline.lastServerHeadHash,
3589
+ currentServerRevisionId: detected.currentServerRevisionId,
3590
+ currentServerHeadHash: detected.currentServerHeadHash,
3591
+ currentServerHeadCommitId: detected.currentServerHeadCommitId,
3592
+ localCommitHash: detected.localCommitHash,
3593
+ warnings: [...pendingFinalizeDrainWarnings, ...detected.warnings]
3243
3594
  });
3244
3595
  }
3245
3596
  const baseline = await readLocalBaseline({
@@ -3248,10 +3599,27 @@ async function collabFinalizeTurn(params) {
3248
3599
  repoRoot
3249
3600
  });
3250
3601
  if (!baseline) {
3251
- throw new RemixError("Local Remix baseline is missing for this lane.", {
3252
- code: "baseline_missing",
3253
- exitCode: 2,
3254
- hint: "Run `remix collab init` or sync this lane to create a fresh revision baseline."
3602
+ return recordConversationOnlyTurn({
3603
+ api: params.api,
3604
+ binding,
3605
+ repoRoot,
3606
+ prompt,
3607
+ assistantResponse,
3608
+ explicitIdempotencyKey: params.idempotencyKey,
3609
+ actor: params.actor,
3610
+ turnUsage: params.turnUsage,
3611
+ promptedAt: params.promptedAt,
3612
+ repoState: "baseline_missing",
3613
+ materializationStatus: "skipped_baseline_missing",
3614
+ alignmentRequired: "baseline",
3615
+ baselineSnapshotId: detected.baseline.lastSnapshotId,
3616
+ baselineServerRevisionId: detected.baseline.lastServerRevisionId,
3617
+ baselineServerHeadHash: detected.baseline.lastServerHeadHash,
3618
+ currentServerRevisionId: detected.currentServerRevisionId,
3619
+ currentServerHeadHash: detected.currentServerHeadHash,
3620
+ currentServerHeadCommitId: detected.currentServerHeadCommitId,
3621
+ localCommitHash: detected.localCommitHash,
3622
+ warnings: [...pendingFinalizeDrainWarnings, ...detected.warnings]
3255
3623
  });
3256
3624
  }
3257
3625
  const snapshot = await captureLocalSnapshot({
@@ -3309,7 +3677,7 @@ async function collabFinalizeTurn(params) {
3309
3677
  changeStep: null,
3310
3678
  collabTurn: null,
3311
3679
  autoSync: null,
3312
- warnings: [FINALIZE_QUEUED_WARNING, ...collectWarnings(detected.warnings)]
3680
+ warnings: [FINALIZE_QUEUED_WARNING, ...collectWarnings([...pendingFinalizeDrainWarnings, ...detected.warnings])]
3313
3681
  };
3314
3682
  }
3315
3683
 
@@ -135,7 +135,7 @@ type MergeRequestReview = {
135
135
  type CollabApproveMode = "remote-only" | "sync-target-repo";
136
136
  type CollabApprovalStrategy = "ai_patch_apply" | "git_merge_auto_resolve" | "git_merge_manual_resolution";
137
137
  type CollabStatusBlockedReason = "not_git_repo" | "not_bound" | "branch_binding_missing" | "family_ambiguous" | "missing_head" | "detached_head" | "branch_mismatch" | "dirty_worktree" | "baseline_missing" | "metadata_conflict" | "remote_error";
138
- type CollabRepoStateKind = "idle" | "local_only_changed" | "server_only_changed" | "both_changed" | "external_local_base_changed" | "metadata_conflict" | "binding_problem";
138
+ type CollabRepoStateKind = "idle" | "local_only_changed" | "server_only_changed" | "both_changed" | "external_local_base_changed" | "baseline_missing" | "missing_head" | "init_post_pending" | "metadata_conflict" | "binding_problem";
139
139
  type CollabPendingFinalizeState = "idle" | "queued" | "processing" | "retry_scheduled" | "awaiting_usage" | "failed";
140
140
  type CollabPendingFinalizeSummary = {
141
141
  state: CollabPendingFinalizeState;
@@ -382,7 +382,7 @@ type CollabTurn = {
382
382
  prompt: string | null;
383
383
  assistantResponse: string | null;
384
384
  };
385
- type CollabFinalizeTurnMode = "changed_turn" | "no_diff_turn";
385
+ type CollabFinalizeTurnMode = "changed_turn" | "no_diff_turn" | "conversation_only";
386
386
  type CollabFinalizeTurnAutoSync = {
387
387
  requested: boolean;
388
388
  eligible: boolean;
package/dist/history.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as TurnUsage } from './contracts-WCYiMqwB.js';
1
+ import { T as TurnUsage } from './contracts-BqC3Lj7W.js';
2
2
 
3
3
  type TranscriptEvent = Record<string, unknown>;
4
4
  type ReadTranscriptResult = {
package/dist/index.d.ts CHANGED
@@ -4,4 +4,4 @@ export { S as SessionStore, a as StoredSession, W as WithRefreshLock, c as creat
4
4
  export { createDefaultRefreshLock, createLocalSessionStore, createSupabaseAuthHelpers, defaultSessionFilePath, isInvalidGrantError, isNetworkError } from './auth.js';
5
5
  export { AgentMemoryItem, AgentMemoryKind, AgentMemorySearchItem, AgentMemorySearchResponse, AgentMemorySummary, AgentMemoryTimelineResponse, ApiClient, AppContext, AppContextAccessPath, AppPreviewExpectedKind, AppPreviewProcessStatus, AppPreviewQuery, AppPreviewResponse, AppReconcileResponse, AppSandboxCommandRun, Bundle, BundlePlatform, BundleStatus, ChangeStepDiffResponse, DetectProjectRuntimeTargetsPayload, ImportProjectRuntimeEnvPayload, InitiateBundleRequest, InvitationRecord, MergeRequest, MergeRequestReview, MergeRequestStatus, ProjectRuntimeEnvMetadata, ProjectRuntimeEnvScope, ProjectRuntimeTargetMetadata, ProjectRuntimeTargetScope, ProjectTrigger, ProjectTriggerPayload, ProjectTriggerScope, ProjectTriggerStep, ProjectTriggerStepPayload, ReconcilePreflightResponse, ResolveProjectRuntimeEnvForLocalPullResponse, RunAppRuntimeTargetPayload, RunAppSandboxCommandPayload, RunAppTriggerEventPayload, RunAppTriggerPayload, RuntimeCommandKind, RuntimeTargetKind, SetProjectRuntimeEnvPayload, SetProjectRuntimeTargetPayload, SyncLocalResponse, SyncUpstreamResponse, TriggerEventMetadata, TriggerEventSource, TriggerIntegrationStatus, TriggerMode, TriggerRun, TriggerStepRun, TriggerStepType, UpdateProjectTriggerPayload, createApiClient } from './api.js';
6
6
  import 'zod';
7
- import './contracts-WCYiMqwB.js';
7
+ import './contracts-BqC3Lj7W.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixhq/core",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "Remix core library",
5
5
  "homepage": "https://github.com/RemixDotOne/remix-core",
6
6
  "license": "MIT",