@remixhq/core 0.1.31 → 0.1.33

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
- import { T as TokenProvider } from './tokenProvider-BWTusyj4.js';
3
- import { T as TurnUsage } from './contracts-BV67NVwV.js';
2
+ import { T as TokenProvider } from './tokenProvider-B9so5Pm3.js';
3
+ import { T as TurnUsage } from './contracts-WCYiMqwB.js';
4
4
  import 'zod';
5
5
 
6
6
  type HistoryImportProvider = "claude_code" | "cursor";
@@ -790,6 +790,7 @@ type ApiClient = {
790
790
  }): Promise<Json>;
791
791
  getAppContext(appId: string): Promise<Json>;
792
792
  getAppOverview(appId: string): Promise<Json>;
793
+ getAppMergePreview(appId: string): Promise<Json>;
793
794
  listAppTimeline(appId: string, params?: {
794
795
  limit?: number;
795
796
  cursor?: string;
@@ -997,6 +998,7 @@ type ApiClient = {
997
998
  title?: string;
998
999
  description?: string;
999
1000
  status?: string;
1001
+ approvalStrategy?: "ai_patch_apply" | "git_merge_auto_resolve" | "git_merge_manual_resolution";
1000
1002
  }): Promise<Json>;
1001
1003
  createOrganizationInvite(orgId: string, payload: {
1002
1004
  email: string;
package/dist/api.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-DOMO3LNV.js";
3
+ } from "./chunk-KURN7YZ5.js";
4
4
  import "./chunk-7XJGOKEO.js";
5
5
  export {
6
6
  createApiClient
package/dist/auth.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SessionStore, a as StoredSession } from './tokenProvider-BWTusyj4.js';
2
- export { R as RefreshStoredSession, d as ResolvedAuthToken, T as TokenProvider, c as createStoredSessionTokenProvider, s as shouldRefreshSoon, b as storedSessionSchema } from './tokenProvider-BWTusyj4.js';
1
+ import { W as WithRefreshLock, S as SessionStore, a as StoredSession } from './tokenProvider-B9so5Pm3.js';
2
+ export { R as RefreshLockOptions, e as RefreshStoredSession, f as ResolvedAuthToken, T as TokenProvider, c as createRefreshLock, b as createStoredSessionTokenProvider, s as shouldRefreshSoon, d as storedSessionSchema } from './tokenProvider-B9so5Pm3.js';
3
3
  import { CoreConfig } from './config.js';
4
4
  import 'zod';
5
5
 
@@ -8,6 +8,43 @@ declare function createLocalSessionStore(params?: {
8
8
  account?: string;
9
9
  filePath?: string;
10
10
  }): SessionStore;
11
+ /**
12
+ * Resolve the on-disk session file path that `createLocalSessionStore`
13
+ * uses by default. Exposed so callers can wire the matching refresh
14
+ * lock without duplicating the xdg lookup.
15
+ */
16
+ declare function defaultSessionFilePath(): string;
17
+ /**
18
+ * Build a `WithRefreshLock` bound to the session file's `.lock`
19
+ * sibling. Every process that touches a given session file should use
20
+ * the same lock so concurrent refreshes serialize correctly across
21
+ * the CLI, the desktop helper subprocess, and the MCP plugin host.
22
+ */
23
+ declare function createDefaultRefreshLock(filePath?: string): WithRefreshLock;
24
+
25
+ /**
26
+ * Classification helpers for refresh-token failures.
27
+ *
28
+ * The token provider needs to react very differently depending on
29
+ * WHY a refresh failed:
30
+ *
31
+ * - `isNetworkError`: transient — DNS / TCP / TLS / undici fetch
32
+ * dropped mid-flight. Keep the existing session (it may already
33
+ * be unusable but the *refresh token* is most likely still alive
34
+ * server-side, especially within Supabase's reuse interval). The
35
+ * next attempt will retry.
36
+ *
37
+ * - `isInvalidGrantError`: terminal — Supabase explicitly said the
38
+ * refresh token is dead (revoked, reused outside the grace
39
+ * window, user signed out elsewhere). No amount of retrying will
40
+ * fix it; the user must re-authenticate.
41
+ *
42
+ * Anything else (5xx, unexpected) is treated like a network error by
43
+ * default — better to keep the session and retry than to log the
44
+ * user out on a transient backend hiccup.
45
+ */
46
+ declare function isNetworkError(err: unknown): boolean;
47
+ declare function isInvalidGrantError(err: unknown): boolean;
11
48
 
12
49
  type SupabaseAuthHelpers = {
13
50
  startGoogleLogin(params: {
@@ -24,4 +61,4 @@ type SupabaseAuthHelpers = {
24
61
  };
25
62
  declare function createSupabaseAuthHelpers(config: CoreConfig): SupabaseAuthHelpers;
26
63
 
27
- export { SessionStore, StoredSession, type SupabaseAuthHelpers, createLocalSessionStore, createSupabaseAuthHelpers };
64
+ export { SessionStore, StoredSession, type SupabaseAuthHelpers, WithRefreshLock, createDefaultRefreshLock, createLocalSessionStore, createSupabaseAuthHelpers, defaultSessionFilePath, isInvalidGrantError, isNetworkError };
package/dist/auth.js CHANGED
@@ -1,15 +1,25 @@
1
1
  import {
2
+ createDefaultRefreshLock,
2
3
  createLocalSessionStore,
4
+ createRefreshLock,
3
5
  createStoredSessionTokenProvider,
4
6
  createSupabaseAuthHelpers,
7
+ defaultSessionFilePath,
8
+ isInvalidGrantError,
9
+ isNetworkError,
5
10
  shouldRefreshSoon,
6
11
  storedSessionSchema
7
- } from "./chunk-MAM4CGDF.js";
12
+ } from "./chunk-OMJ4JDME.js";
8
13
  import "./chunk-7XJGOKEO.js";
9
14
  export {
15
+ createDefaultRefreshLock,
10
16
  createLocalSessionStore,
17
+ createRefreshLock,
11
18
  createStoredSessionTokenProvider,
12
19
  createSupabaseAuthHelpers,
20
+ defaultSessionFilePath,
21
+ isInvalidGrantError,
22
+ isNetworkError,
13
23
  shouldRefreshSoon,
14
24
  storedSessionSchema
15
25
  };
@@ -366,6 +366,7 @@ function createApiClient(config, opts) {
366
366
  openApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/open`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
367
367
  getAppContext: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/context`, { method: "GET" }),
368
368
  getAppOverview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/overview`, { method: "GET" }),
369
+ getAppMergePreview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/merge-preview`, { method: "GET" }),
369
370
  listAppTimeline: (appId, params) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline${buildAppTimelineQuery(params)}`, { method: "GET" }),
370
371
  getAppTimelineEvent: (appId, eventId) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline/${encodeURIComponent(eventId)}`, { method: "GET" }),
371
372
  listAppEditQueue: (appId, params) => request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${buildAppEditQueueQuery(params)}`, { method: "GET" }),
@@ -16,14 +16,82 @@ var storedSessionSchema = z.object({
16
16
  });
17
17
 
18
18
  // src/auth/localSessionStore.ts
19
+ import fs2 from "fs/promises";
20
+ import os2 from "os";
21
+ import path2 from "path";
22
+
23
+ // src/auth/refreshLock.ts
19
24
  import fs from "fs/promises";
20
25
  import os from "os";
21
26
  import path from "path";
27
+ var DEFAULT_STALE_MS = 3e4;
28
+ var DEFAULT_MAX_WAIT_MS = 15e3;
29
+ var DEFAULT_POLL_MS = 50;
30
+ function createRefreshLock(opts) {
31
+ const staleMs = opts.staleMs ?? DEFAULT_STALE_MS;
32
+ const maxWaitMs = opts.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
33
+ const pollMs = opts.pollMs ?? DEFAULT_POLL_MS;
34
+ let inProcess = null;
35
+ return async function withRefreshLock(fn) {
36
+ if (inProcess) {
37
+ await inProcess.catch(() => {
38
+ });
39
+ }
40
+ const job = (async () => {
41
+ const start = Date.now();
42
+ while (true) {
43
+ try {
44
+ await fs.mkdir(path.dirname(opts.lockPath), { recursive: true });
45
+ const fh = await fs.open(opts.lockPath, "wx");
46
+ try {
47
+ await fh.writeFile(`${process.pid}
48
+ ${Date.now()}
49
+ ${os.hostname()}
50
+ `);
51
+ } finally {
52
+ await fh.close().catch(() => {
53
+ });
54
+ }
55
+ try {
56
+ return await fn();
57
+ } finally {
58
+ await fs.unlink(opts.lockPath).catch(() => {
59
+ });
60
+ }
61
+ } catch (err) {
62
+ const code = err?.code;
63
+ if (code !== "EEXIST") throw err;
64
+ const stat = await fs.stat(opts.lockPath).catch(() => null);
65
+ if (stat && Date.now() - stat.mtimeMs > staleMs) {
66
+ await fs.unlink(opts.lockPath).catch(() => {
67
+ });
68
+ continue;
69
+ }
70
+ if (Date.now() - start > maxWaitMs) {
71
+ return await fn();
72
+ }
73
+ await sleep(pollMs);
74
+ }
75
+ }
76
+ })();
77
+ inProcess = job;
78
+ try {
79
+ return await job;
80
+ } finally {
81
+ if (inProcess === job) inProcess = null;
82
+ }
83
+ };
84
+ }
85
+ function sleep(ms) {
86
+ return new Promise((resolve) => setTimeout(resolve, ms));
87
+ }
88
+
89
+ // src/auth/localSessionStore.ts
22
90
  var loadKeytarModule = () => new Function("return import('keytar')")();
23
91
  function xdgConfigHome() {
24
92
  const value = process.env.XDG_CONFIG_HOME;
25
93
  if (typeof value === "string" && value.trim()) return value;
26
- return path.join(os.homedir(), ".config");
94
+ return path2.join(os2.homedir(), ".config");
27
95
  }
28
96
  async function maybeLoadKeytar() {
29
97
  try {
@@ -41,22 +109,22 @@ async function maybeLoadKeytar() {
41
109
  return null;
42
110
  }
43
111
  async function ensurePathPermissions(filePath) {
44
- const dir = path.dirname(filePath);
45
- await fs.mkdir(dir, { recursive: true });
112
+ const dir = path2.dirname(filePath);
113
+ await fs2.mkdir(dir, { recursive: true });
46
114
  try {
47
- await fs.chmod(dir, 448);
115
+ await fs2.chmod(dir, 448);
48
116
  } catch {
49
117
  }
50
118
  try {
51
- await fs.chmod(filePath, 384);
119
+ await fs2.chmod(filePath, 384);
52
120
  } catch {
53
121
  }
54
122
  }
55
123
  async function writeJsonAtomic(filePath, value) {
56
- await fs.mkdir(path.dirname(filePath), { recursive: true });
124
+ await fs2.mkdir(path2.dirname(filePath), { recursive: true });
57
125
  const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
58
- await fs.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
59
- await fs.rename(tmpPath, filePath);
126
+ await fs2.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
127
+ await fs2.rename(tmpPath, filePath);
60
128
  }
61
129
  async function writeSessionFileFallback(filePath, session) {
62
130
  await writeJsonAtomic(filePath, session);
@@ -65,7 +133,7 @@ async function writeSessionFileFallback(filePath, session) {
65
133
  function createLocalSessionStore(params) {
66
134
  const service = params?.service?.trim() || "remix-cli";
67
135
  const account = params?.account?.trim() || "default";
68
- const filePath = params?.filePath?.trim() || path.join(xdgConfigHome(), "remix", "session.json");
136
+ const filePath = params?.filePath?.trim() || path2.join(xdgConfigHome(), "remix", "session.json");
69
137
  async function readKeytar() {
70
138
  const keytar = await maybeLoadKeytar();
71
139
  if (!keytar) return null;
@@ -79,7 +147,7 @@ function createLocalSessionStore(params) {
79
147
  }
80
148
  }
81
149
  async function readFile() {
82
- const raw = await fs.readFile(filePath, "utf8").catch(() => null);
150
+ const raw = await fs2.readFile(filePath, "utf8").catch(() => null);
83
151
  if (!raw) return null;
84
152
  try {
85
153
  const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
@@ -116,6 +184,42 @@ function createLocalSessionStore(params) {
116
184
  }
117
185
  };
118
186
  }
187
+ function defaultSessionFilePath() {
188
+ return path2.join(xdgConfigHome(), "remix", "session.json");
189
+ }
190
+ function createDefaultRefreshLock(filePath) {
191
+ const target = filePath?.trim() || defaultSessionFilePath();
192
+ return createRefreshLock({ lockPath: `${target}.lock` });
193
+ }
194
+
195
+ // src/auth/refreshErrors.ts
196
+ var NETWORK_CODE_PATTERN = /^(ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ENOTFOUND|EPIPE|EHOSTUNREACH|EADDRNOTAVAIL|UND_ERR_)/;
197
+ var NETWORK_MESSAGE_PATTERN = /fetch failed|failed to fetch|network request failed|socket hang up|network error|aborted/i;
198
+ function isNetworkError(err) {
199
+ if (!err || typeof err !== "object") return false;
200
+ const e = err;
201
+ const code = typeof e.code === "string" ? e.code : void 0;
202
+ const errno = typeof e.errno === "string" ? e.errno : void 0;
203
+ const msg = typeof e.message === "string" ? e.message : "";
204
+ if (code && NETWORK_CODE_PATTERN.test(code)) return true;
205
+ if (errno && NETWORK_CODE_PATTERN.test(errno)) return true;
206
+ if (e.name === "TypeError" && NETWORK_MESSAGE_PATTERN.test(msg)) return true;
207
+ if (e.name === "AbortError") return true;
208
+ if (NETWORK_MESSAGE_PATTERN.test(msg)) return true;
209
+ if (e.cause) return isNetworkError(e.cause);
210
+ return false;
211
+ }
212
+ var INVALID_GRANT_MESSAGE_PATTERN = /invalid[_ ]grant|invalid refresh token|refresh[_ ]token[_ ]not[_ ]found|token has expired or (been )?revoked|already used|user from sub claim in jwt does not exist/i;
213
+ function isInvalidGrantError(err) {
214
+ if (!err || typeof err !== "object") return false;
215
+ const e = err;
216
+ const code = typeof e.code === "string" ? e.code : "";
217
+ const msg = typeof e.message === "string" ? e.message : "";
218
+ if (code === "invalid_grant" || code === "refresh_token_not_found") return true;
219
+ if (INVALID_GRANT_MESSAGE_PATTERN.test(msg)) return true;
220
+ if (e.cause) return isInvalidGrantError(e.cause);
221
+ return false;
222
+ }
119
223
 
120
224
  // src/auth/tokenProvider.ts
121
225
  function shouldRefreshSoon(session, skewSeconds = 60) {
@@ -123,6 +227,7 @@ function shouldRefreshSoon(session, skewSeconds = 60) {
123
227
  return session.expires_at <= nowSec + skewSeconds;
124
228
  }
125
229
  function createStoredSessionTokenProvider(params) {
230
+ const withLock = params.withRefreshLock ?? (async (fn) => fn());
126
231
  return async (opts) => {
127
232
  const forceRefresh = Boolean(opts?.forceRefresh);
128
233
  const envToken = process.env.COMERGE_ACCESS_TOKEN;
@@ -137,11 +242,38 @@ function createStoredSessionTokenProvider(params) {
137
242
  });
138
243
  }
139
244
  if (forceRefresh || shouldRefreshSoon(session)) {
245
+ const sessionAtEntry = session;
140
246
  try {
141
- session = await params.refreshStoredSession({ config: params.config, session });
142
- await params.sessionStore.setSession(session);
247
+ session = await withLock(async () => {
248
+ const latest = await params.sessionStore.getSession();
249
+ const base = latest ?? sessionAtEntry;
250
+ if (!base) {
251
+ throw new RemixError("Not signed in.", {
252
+ exitCode: 2,
253
+ hint: "Run `remix login`, or set COMERGE_ACCESS_TOKEN for CI."
254
+ });
255
+ }
256
+ const siblingRefreshed = latest != null && latest.access_token !== sessionAtEntry.access_token;
257
+ if (siblingRefreshed && !forceRefresh && !shouldRefreshSoon(base, 5)) {
258
+ return base;
259
+ }
260
+ const refreshed = await params.refreshStoredSession({
261
+ config: params.config,
262
+ session: base
263
+ });
264
+ await params.sessionStore.setSession(refreshed);
265
+ return refreshed;
266
+ });
143
267
  } catch (err) {
144
- void err;
268
+ if (isInvalidGrantError(err)) {
269
+ throw new RemixError("Session expired. Please sign in again.", {
270
+ exitCode: 2,
271
+ hint: "Run `remix login` to re-authenticate."
272
+ });
273
+ }
274
+ if (isNetworkError(err)) {
275
+ } else {
276
+ }
145
277
  }
146
278
  }
147
279
  return { token: session.access_token, session, fromEnv: false };
@@ -217,8 +349,7 @@ function createSupabaseAuthHelpers(config) {
217
349
  return toStoredSession(data.session);
218
350
  },
219
351
  async refreshWithStoredSession(params) {
220
- const { data, error } = await supabase.auth.setSession({
221
- access_token: params.session.access_token,
352
+ const { data, error } = await supabase.auth.refreshSession({
222
353
  refresh_token: params.session.refresh_token
223
354
  });
224
355
  if (error) throw error;
@@ -230,7 +361,12 @@ function createSupabaseAuthHelpers(config) {
230
361
 
231
362
  export {
232
363
  storedSessionSchema,
364
+ createRefreshLock,
233
365
  createLocalSessionStore,
366
+ defaultSessionFilePath,
367
+ createDefaultRefreshLock,
368
+ isNetworkError,
369
+ isInvalidGrantError,
234
370
  shouldRefreshSoon,
235
371
  createStoredSessionTokenProvider,
236
372
  createSupabaseAuthHelpers
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 CollabApproveResult, M as MergeRequestQueue, f as MergeRequest, I as InvitationScopeType, g as CollabMember, A as AppProfileInput, J as JsonObject, h as AppDeltaResponse, i as CollabStatus, j as MergeRequestReview } from './contracts-BV67NVwV.js';
2
- export { k as AppHeadResponse, l as AppMember, m as AppMemberRole, n as AppReconcileResponse, o as CollabFinalizeTurnAutoSync, p as CollabFinalizeTurnMode, q as CollabPendingFinalizeState, r as CollabPendingFinalizeSummary, s as CollabRecordingPreflightStatus, t as CollabRepoStateKind, u as CollabStatusBlockedReason, v as CollabStatusRecommendedAction, w as CollabTurn, x as MembershipScopeType, y as MergeRequestStatus, z as ModelCall, O as OrganizationMember, B as OrganizationMemberRole, P as ProjectMember, D as ProjectMemberRole, R as ReconcilePreflightResponse, S as ServerToolUsage, E as SyncLocalResponse, F as SyncUpstreamResponse, T as TurnUsage, G as TurnUsageCaptureSource, H as TurnUsageConfidence, K as TurnUsagePreviousPatch } from './contracts-BV67NVwV.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-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';
3
3
  import { B as BranchBindingMode } from './binding-WiIRI2fl.js';
4
4
 
5
5
  declare function collabFinalizeTurn(params: {
@@ -149,6 +149,7 @@ declare function collabApprove(params: {
149
149
  api: CollabApiClient;
150
150
  mrId: string;
151
151
  mode: CollabApproveMode;
152
+ approvalStrategy?: CollabApprovalStrategy;
152
153
  cwd?: string;
153
154
  allowBranchMismatch?: boolean;
154
155
  }): Promise<CollabApproveResult>;
package/dist/collab.js CHANGED
@@ -1425,6 +1425,12 @@ async function pollMergeRequestCompletion(api, mrId, params) {
1425
1425
  const review = unwrapMergeRequestReview(reviewResp);
1426
1426
  const mergeRequest = review.mergeRequest;
1427
1427
  if (mergeRequest.status === "merged") return mergeRequest;
1428
+ if (mergeRequest.status === "manual_resolution_required") {
1429
+ throw new RemixError("Merge approval requires manual conflict resolution.", {
1430
+ exitCode: 2,
1431
+ hint: "Merge request ended in status=manual_resolution_required."
1432
+ });
1433
+ }
1428
1434
  if (mergeRequest.status === "rejected" || mergeRequest.status === "closed") {
1429
1435
  throw new RemixError("Merge approval did not complete successfully.", {
1430
1436
  exitCode: 1,
@@ -2500,6 +2506,7 @@ function buildWorkspaceMetadata(params) {
2500
2506
 
2501
2507
  // src/application/collab/finalizeDecision.ts
2502
2508
  var FINALIZE_AUTO_TERMINAL_THRESHOLD = 5;
2509
+ var FINALIZE_AUTO_TERMINAL_TOTAL_RETRY_THRESHOLD = 8;
2503
2510
  function classifyFinalizeError(error) {
2504
2511
  const tagged = error;
2505
2512
  return {
@@ -2512,12 +2519,20 @@ function computeFailureEscalation(job, reason) {
2512
2519
  const previousReason = job.metadata.consecutiveFailureReason;
2513
2520
  const previousCountRaw = job.metadata.consecutiveFailures;
2514
2521
  const previousCount = typeof previousCountRaw === "number" && Number.isFinite(previousCountRaw) && previousCountRaw > 0 ? Math.floor(previousCountRaw) : 0;
2522
+ const previousTotalRaw = job.metadata.totalFailures;
2523
+ const previousTotal = typeof previousTotalRaw === "number" && Number.isFinite(previousTotalRaw) && previousTotalRaw > 0 ? Math.floor(previousTotalRaw) : 0;
2515
2524
  const sameReason = previousReason === reason;
2516
2525
  const next = sameReason ? previousCount + 1 : 1;
2526
+ const totalFailures = previousTotal + 1;
2527
+ const consecutiveTripped = next >= FINALIZE_AUTO_TERMINAL_THRESHOLD;
2528
+ const totalTripped = totalFailures >= FINALIZE_AUTO_TERMINAL_TOTAL_RETRY_THRESHOLD;
2529
+ const escalationTrigger = consecutiveTripped ? "consecutive_same_reason" : totalTripped ? "total_retries" : "none";
2517
2530
  return {
2518
2531
  consecutiveFailures: next,
2519
2532
  consecutiveFailureReason: reason,
2520
- shouldEscalateToTerminal: next >= FINALIZE_AUTO_TERMINAL_THRESHOLD
2533
+ totalFailures,
2534
+ shouldEscalateToTerminal: consecutiveTripped || totalTripped,
2535
+ escalationTrigger
2521
2536
  };
2522
2537
  }
2523
2538
  function hasFinalizeBaselineDrifted(params) {
@@ -2697,15 +2712,25 @@ async function processClaimedPendingFinalizeJob(params) {
2697
2712
  const classified = classifyFinalizeError(error);
2698
2713
  const escalation = computeFailureEscalation(job, classified.reason);
2699
2714
  const finalDisposition = classified.disposition === "terminal" || escalation.shouldEscalateToTerminal ? "terminal" : "retryable";
2715
+ const autoEscalated = finalDisposition === "terminal" && escalation.shouldEscalateToTerminal && classified.disposition !== "terminal";
2716
+ let escalationSuffix = "";
2717
+ if (autoEscalated) {
2718
+ if (escalation.escalationTrigger === "consecutive_same_reason") {
2719
+ escalationSuffix = ` (auto-escalated to terminal after ${escalation.consecutiveFailures} consecutive ${classified.reason} failures)`;
2720
+ } else if (escalation.escalationTrigger === "total_retries") {
2721
+ escalationSuffix = ` (auto-escalated to terminal after ${escalation.totalFailures} total retries with mixed failure reasons; latest=${classified.reason})`;
2722
+ }
2723
+ }
2700
2724
  await updatePendingFinalizeJob(job.id, {
2701
2725
  status: finalDisposition === "terminal" ? "failed" : "queued",
2702
- error: finalDisposition === "terminal" && escalation.shouldEscalateToTerminal && classified.disposition !== "terminal" ? `${classified.message} (auto-escalated to terminal after ${escalation.consecutiveFailures} consecutive ${classified.reason} failures)` : classified.message,
2726
+ error: `${classified.message}${escalationSuffix}`,
2703
2727
  nextRetryAt: finalDisposition === "terminal" ? null : buildNextRetryAt(job.retryCount),
2704
2728
  metadata: {
2705
2729
  failureDisposition: finalDisposition,
2706
2730
  failureReason: classified.reason,
2707
2731
  consecutiveFailures: escalation.consecutiveFailures,
2708
- consecutiveFailureReason: escalation.consecutiveFailureReason
2732
+ consecutiveFailureReason: escalation.consecutiveFailureReason,
2733
+ totalFailures: escalation.totalFailures
2709
2734
  }
2710
2735
  });
2711
2736
  throw error;
@@ -3839,6 +3864,10 @@ async function collabSync(params) {
3839
3864
 
3840
3865
  // src/application/collab/collabApprove.ts
3841
3866
  async function collabApprove(params) {
3867
+ const approvePayload = {
3868
+ status: "approved",
3869
+ ...params.approvalStrategy ? { approvalStrategy: params.approvalStrategy } : {}
3870
+ };
3842
3871
  const transition = planApproveWorkflowTransition({
3843
3872
  mode: params.mode,
3844
3873
  hasCwd: Boolean(params.cwd?.trim())
@@ -3878,7 +3907,7 @@ async function collabApprove(params) {
3878
3907
  hint: `Run the command from the repository bound to app ${targetAppId}, or use --remote-only.`
3879
3908
  });
3880
3909
  }
3881
- const resp2 = await params.api.updateMergeRequest(params.mrId, { status: "approved" });
3910
+ const resp2 = await params.api.updateMergeRequest(params.mrId, approvePayload);
3882
3911
  const approvedMergeRequest2 = unwrapMergeRequest(resp2);
3883
3912
  const completedMergeRequest2 = await pollMergeRequestCompletion(params.api, params.mrId, {
3884
3913
  targetAppId: targetAppId ?? approvedMergeRequest2.targetAppId
@@ -3910,7 +3939,7 @@ async function collabApprove(params) {
3910
3939
  exitCode: 2
3911
3940
  });
3912
3941
  }
3913
- const resp = await params.api.updateMergeRequest(params.mrId, { status: "approved" });
3942
+ const resp = await params.api.updateMergeRequest(params.mrId, approvePayload);
3914
3943
  const approvedMergeRequest = unwrapMergeRequest(resp);
3915
3944
  const completedMergeRequest = await pollMergeRequestCompletion(params.api, params.mrId, {
3916
3945
  targetAppId: approvedMergeRequest.targetAppId
@@ -66,7 +66,7 @@ type TurnUsagePayload = {
66
66
  currentTurn: TurnUsage | null;
67
67
  previousTurn: TurnUsagePreviousPatch | null;
68
68
  };
69
- type MergeRequestStatus = "open" | "approved" | "rejected" | "merged" | "closed";
69
+ type MergeRequestStatus = "open" | "approved" | "rejected" | "merged" | "closed" | "manual_resolution_required";
70
70
  type MergeRequestQueue = "reviewable" | "created_by_me" | "app_reviewable" | "app_outgoing" | "app_related_visible";
71
71
  type MergeRequest = {
72
72
  id: string;
@@ -133,6 +133,7 @@ type MergeRequestReview = {
133
133
  };
134
134
  };
135
135
  type CollabApproveMode = "remote-only" | "sync-target-repo";
136
+ type CollabApprovalStrategy = "ai_patch_apply" | "git_merge_auto_resolve" | "git_merge_manual_resolution";
136
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";
137
138
  type CollabRepoStateKind = "idle" | "local_only_changed" | "server_only_changed" | "both_changed" | "external_local_base_changed" | "metadata_conflict" | "binding_problem";
138
139
  type CollabPendingFinalizeState = "idle" | "queued" | "processing" | "retry_scheduled" | "awaiting_usage" | "failed";
@@ -631,6 +632,7 @@ type CollabApiClient = {
631
632
  title?: string;
632
633
  description?: string;
633
634
  status?: string;
635
+ approvalStrategy?: CollabApprovalStrategy;
634
636
  }): Promise<unknown>;
635
637
  createOrganizationInvite(orgId: string, payload: {
636
638
  email: string;
@@ -732,4 +734,4 @@ type CollabApiClient = {
732
734
  }): Promise<unknown>;
733
735
  };
734
736
 
735
- export type { AppProfileInput as A, OrganizationMemberRole as B, CollabApiClient as C, ProjectMemberRole as D, SyncLocalResponse as E, SyncUpstreamResponse as F, TurnUsageCaptureSource as G, TurnUsageConfidence as H, InvitationScopeType as I, JsonObject as J, TurnUsagePreviousPatch as K, MergeRequestQueue as M, OrganizationMember as O, ProjectMember as P, ReconcilePreflightResponse as R, ServerToolUsage as S, TurnUsage as T, TurnUsagePayload as a, CollabFinalizeTurnResult as b, CollabRecordingPreflight as c, CollabApproveMode as d, CollabApproveResult as e, MergeRequest as f, CollabMember as g, AppDeltaResponse as h, CollabStatus as i, MergeRequestReview as j, AppHeadResponse as k, AppMember as l, AppMemberRole as m, AppReconcileResponse as n, CollabFinalizeTurnAutoSync as o, CollabFinalizeTurnMode as p, CollabPendingFinalizeState as q, CollabPendingFinalizeSummary as r, CollabRecordingPreflightStatus as s, CollabRepoStateKind as t, CollabStatusBlockedReason as u, CollabStatusRecommendedAction as v, CollabTurn as w, MembershipScopeType as x, MergeRequestStatus as y, ModelCall as z };
737
+ export type { AppProfileInput as A, ModelCall as B, CollabApiClient as C, OrganizationMemberRole as D, ProjectMemberRole as E, SyncLocalResponse as F, SyncUpstreamResponse as G, TurnUsageCaptureSource as H, InvitationScopeType as I, JsonObject as J, TurnUsageConfidence as K, TurnUsagePreviousPatch as L, MergeRequestQueue as M, OrganizationMember as O, ProjectMember as P, ReconcilePreflightResponse as R, ServerToolUsage as S, TurnUsage as T, TurnUsagePayload as a, CollabFinalizeTurnResult as b, CollabRecordingPreflight as c, CollabApproveMode as d, CollabApprovalStrategy as e, CollabApproveResult as f, MergeRequest as g, CollabMember as h, AppDeltaResponse as i, CollabStatus as j, MergeRequestReview as k, AppHeadResponse as l, AppMember as m, AppMemberRole as n, AppReconcileResponse as o, CollabFinalizeTurnAutoSync as p, CollabFinalizeTurnMode as q, CollabPendingFinalizeState as r, CollabPendingFinalizeSummary as s, CollabRecordingPreflightStatus as t, CollabRepoStateKind as u, CollabStatusBlockedReason as v, CollabStatusRecommendedAction as w, CollabTurn as x, MembershipScopeType as y, MergeRequestStatus as z };
package/dist/history.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as TurnUsage } from './contracts-BV67NVwV.js';
1
+ import { T as TurnUsage } from './contracts-WCYiMqwB.js';
2
2
 
3
3
  type TranscriptEvent = Record<string, unknown>;
4
4
  type ReadTranscriptResult = {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { CliError, REMIX_ERROR_CODES, CliError as RemixError, RemixErrorCode } from './errors.js';
2
2
  export { CoreConfig, ResolveConfigOptions, configSchema, resolveConfig } from './config.js';
3
- export { S as SessionStore, a as StoredSession, c as createStoredSessionTokenProvider, s as shouldRefreshSoon, b as storedSessionSchema } from './tokenProvider-BWTusyj4.js';
4
- export { createLocalSessionStore, createSupabaseAuthHelpers } from './auth.js';
3
+ export { S as SessionStore, a as StoredSession, W as WithRefreshLock, c as createRefreshLock, b as createStoredSessionTokenProvider, s as shouldRefreshSoon, d as storedSessionSchema } from './tokenProvider-B9so5Pm3.js';
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-BV67NVwV.js';
7
+ import './contracts-WCYiMqwB.js';
package/dist/index.js CHANGED
@@ -1,13 +1,18 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-DOMO3LNV.js";
3
+ } from "./chunk-KURN7YZ5.js";
4
4
  import {
5
+ createDefaultRefreshLock,
5
6
  createLocalSessionStore,
7
+ createRefreshLock,
6
8
  createStoredSessionTokenProvider,
7
9
  createSupabaseAuthHelpers,
10
+ defaultSessionFilePath,
11
+ isInvalidGrantError,
12
+ isNetworkError,
8
13
  shouldRefreshSoon,
9
14
  storedSessionSchema
10
- } from "./chunk-MAM4CGDF.js";
15
+ } from "./chunk-OMJ4JDME.js";
11
16
  import {
12
17
  configSchema,
13
18
  resolveConfig
@@ -25,9 +30,14 @@ export {
25
30
  RemixError,
26
31
  configSchema,
27
32
  createApiClient,
33
+ createDefaultRefreshLock,
28
34
  createLocalSessionStore,
35
+ createRefreshLock,
29
36
  createStoredSessionTokenProvider,
30
37
  createSupabaseAuthHelpers,
38
+ defaultSessionFilePath,
39
+ isInvalidGrantError,
40
+ isNetworkError,
31
41
  resolveConfig,
32
42
  shouldRefreshSoon,
33
43
  storedSessionSchema
@@ -0,0 +1,123 @@
1
+ import { CoreConfig } from './config.js';
2
+ import { z } from 'zod';
3
+
4
+ type RefreshLockOptions = {
5
+ /** Lock file path. Conventionally `<sessionFile>.lock`. */
6
+ lockPath: string;
7
+ /** Treat an existing lock as stale when older than this. */
8
+ staleMs?: number;
9
+ /** Give up waiting and proceed without the lock after this. */
10
+ maxWaitMs?: number;
11
+ /** Sleep between EEXIST retries while waiting. */
12
+ pollMs?: number;
13
+ };
14
+ type WithRefreshLock = <T>(fn: () => Promise<T>) => Promise<T>;
15
+ declare function createRefreshLock(opts: RefreshLockOptions): WithRefreshLock;
16
+
17
+ declare const storedSessionSchema: z.ZodObject<{
18
+ access_token: z.ZodString;
19
+ refresh_token: z.ZodString;
20
+ expires_at: z.ZodNumber;
21
+ token_type: z.ZodOptional<z.ZodString>;
22
+ user: z.ZodOptional<z.ZodObject<{
23
+ id: z.ZodString;
24
+ email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
25
+ }, "strip", z.ZodTypeAny, {
26
+ id: string;
27
+ email?: string | null | undefined;
28
+ }, {
29
+ id: string;
30
+ email?: string | null | undefined;
31
+ }>>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ access_token: string;
34
+ refresh_token: string;
35
+ expires_at: number;
36
+ token_type?: string | undefined;
37
+ user?: {
38
+ id: string;
39
+ email?: string | null | undefined;
40
+ } | undefined;
41
+ }, {
42
+ access_token: string;
43
+ refresh_token: string;
44
+ expires_at: number;
45
+ token_type?: string | undefined;
46
+ user?: {
47
+ id: string;
48
+ email?: string | null | undefined;
49
+ } | undefined;
50
+ }>;
51
+ type StoredSession = z.infer<typeof storedSessionSchema>;
52
+ type SessionStore = {
53
+ getSession(): Promise<StoredSession | null>;
54
+ setSession(session: StoredSession): Promise<void>;
55
+ };
56
+
57
+ type RefreshStoredSession = (params: {
58
+ config: CoreConfig;
59
+ session: StoredSession;
60
+ }) => Promise<StoredSession>;
61
+ type ResolvedAuthToken = {
62
+ token: string;
63
+ session: StoredSession | null;
64
+ fromEnv: boolean;
65
+ };
66
+ type TokenProvider = (opts?: {
67
+ forceRefresh?: boolean;
68
+ }) => Promise<ResolvedAuthToken>;
69
+ declare function shouldRefreshSoon(session: StoredSession, skewSeconds?: number): boolean;
70
+ /**
71
+ * Token provider that resolves a fresh access token from a stored
72
+ * Supabase session, refreshing it through `refreshStoredSession` when
73
+ * the cached token is at/near expiry.
74
+ *
75
+ * # Why this code is more careful than it looks
76
+ *
77
+ * Supabase rotates refresh tokens: every successful refresh server-
78
+ * side invalidates the old refresh token and issues a new one. If we
79
+ * lose the new token before persisting it (network drop after the
80
+ * server processed our request; multiple processes refreshing the
81
+ * same session in parallel; a `getUser` call after refresh that
82
+ * fails) the session is permanently dead even though the user did
83
+ * nothing wrong. Real-world incident: a laptop coming out of sleep
84
+ * during the 60-second drainer interval lost the rotated token and
85
+ * appeared as a "silent logout" to the user.
86
+ *
87
+ * Three properties protect against this:
88
+ *
89
+ * 1. **Single-flight via `withRefreshLock`** — at most one refresh
90
+ * runs at a time across the whole machine (lock file shared
91
+ * between CLI, desktop, MCP). Inside the lock we re-read the
92
+ * session so we don't double-refresh if another process beat us
93
+ * to it.
94
+ *
95
+ * 2. **Persist-before-return** — `refreshStoredSession` returns the
96
+ * new pair, we write it to disk via `setSession`, and only
97
+ * THEN return to the caller. If the write fails we throw and
98
+ * the caller will retry rather than acting on a token we
99
+ * couldn't durably store.
100
+ *
101
+ * 3. **Network-vs-auth error classification** — `fetch failed`
102
+ * keeps the existing session (the refresh may or may not have
103
+ * landed server-side; we'll find out on the next attempt, ideally
104
+ * within Supabase's reuse interval). Only `invalid_grant` /
105
+ * "refresh token not found" clears state and surfaces a clean
106
+ * sign-in prompt — by then the token is definitively dead and
107
+ * retrying won't help.
108
+ */
109
+ declare function createStoredSessionTokenProvider(params: {
110
+ config: CoreConfig;
111
+ sessionStore: SessionStore;
112
+ refreshStoredSession: RefreshStoredSession;
113
+ /**
114
+ * Optional cross-process lock around the refresh-and-persist step.
115
+ * Omitting this is safe for tests and single-process scenarios; in
116
+ * production every caller should provide one bound to the session
117
+ * file's `.lock` sibling so concurrent CLI / desktop / plugin
118
+ * processes serialize correctly.
119
+ */
120
+ withRefreshLock?: WithRefreshLock;
121
+ }): TokenProvider;
122
+
123
+ export { type RefreshLockOptions as R, type SessionStore as S, type TokenProvider as T, type WithRefreshLock as W, type StoredSession as a, createStoredSessionTokenProvider as b, createRefreshLock as c, storedSessionSchema as d, type RefreshStoredSession as e, type ResolvedAuthToken as f, shouldRefreshSoon as s };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixhq/core",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "Remix core library",
5
5
  "homepage": "https://github.com/RemixDotOne/remix-core",
6
6
  "license": "MIT",
@@ -1,63 +0,0 @@
1
- import { CoreConfig } from './config.js';
2
- import { z } from 'zod';
3
-
4
- declare const storedSessionSchema: z.ZodObject<{
5
- access_token: z.ZodString;
6
- refresh_token: z.ZodString;
7
- expires_at: z.ZodNumber;
8
- token_type: z.ZodOptional<z.ZodString>;
9
- user: z.ZodOptional<z.ZodObject<{
10
- id: z.ZodString;
11
- email: z.ZodNullable<z.ZodOptional<z.ZodString>>;
12
- }, "strip", z.ZodTypeAny, {
13
- id: string;
14
- email?: string | null | undefined;
15
- }, {
16
- id: string;
17
- email?: string | null | undefined;
18
- }>>;
19
- }, "strip", z.ZodTypeAny, {
20
- access_token: string;
21
- refresh_token: string;
22
- expires_at: number;
23
- token_type?: string | undefined;
24
- user?: {
25
- id: string;
26
- email?: string | null | undefined;
27
- } | undefined;
28
- }, {
29
- access_token: string;
30
- refresh_token: string;
31
- expires_at: number;
32
- token_type?: string | undefined;
33
- user?: {
34
- id: string;
35
- email?: string | null | undefined;
36
- } | undefined;
37
- }>;
38
- type StoredSession = z.infer<typeof storedSessionSchema>;
39
- type SessionStore = {
40
- getSession(): Promise<StoredSession | null>;
41
- setSession(session: StoredSession): Promise<void>;
42
- };
43
-
44
- type RefreshStoredSession = (params: {
45
- config: CoreConfig;
46
- session: StoredSession;
47
- }) => Promise<StoredSession>;
48
- type ResolvedAuthToken = {
49
- token: string;
50
- session: StoredSession | null;
51
- fromEnv: boolean;
52
- };
53
- type TokenProvider = (opts?: {
54
- forceRefresh?: boolean;
55
- }) => Promise<ResolvedAuthToken>;
56
- declare function shouldRefreshSoon(session: StoredSession, skewSeconds?: number): boolean;
57
- declare function createStoredSessionTokenProvider(params: {
58
- config: CoreConfig;
59
- sessionStore: SessionStore;
60
- refreshStoredSession: RefreshStoredSession;
61
- }): TokenProvider;
62
-
63
- export { type RefreshStoredSession as R, type SessionStore as S, type TokenProvider as T, type StoredSession as a, storedSessionSchema as b, createStoredSessionTokenProvider as c, type ResolvedAuthToken as d, shouldRefreshSoon as s };