@cmmd-center/forge 0.13.12 → 0.13.13

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.
Files changed (24) hide show
  1. package/dist/bin.cjs +454 -71
  2. package/dist/bin.mjs +478 -95
  3. package/dist/client/assets/{CanvasPanel-BjHxCGlb.js → CanvasPanel-RpD9Eh_S.js} +2 -2
  4. package/dist/client/assets/{CompositeItem-D4LER3__.js → CompositeItem-DEIBUHJR.js} +2 -2
  5. package/dist/client/assets/{DiffPanel-D3btZGjW.js → DiffPanel-cwm7LEtZ.js} +2 -2
  6. package/dist/client/assets/{DiffPanel.logic-DWcwE9Wj.js → DiffPanel.logic-B9AWYFxE.js} +2 -2
  7. package/dist/client/assets/{DiffPanelShell-oXHh--ae.js → DiffPanelShell-BhokrrDZ.js} +103 -103
  8. package/dist/client/assets/{DiffWorkerPoolProvider-R9YPaaNy.js → DiffWorkerPoolProvider-BZjEmEzL.js} +2 -2
  9. package/dist/client/assets/{PullRequestCodePanel-DTCc2TBl.js → PullRequestCodePanel-BPJINzCp.js} +2 -2
  10. package/dist/client/assets/{Virtualizer-arKGxq_z.js → Virtualizer-i6HiQ7Qm.js} +2 -2
  11. package/dist/client/assets/{canvasStateStore-B5zd1u9r.js → canvasStateStore-NrAxDqvM.js} +2 -2
  12. package/dist/client/assets/{cmmdAuthRedirect-DeFAIQpj.js → cmmdAuthRedirect-BahlX0gF.js} +2 -2
  13. package/dist/client/assets/index-DMWfw0Ih.js +154 -0
  14. package/dist/client/assets/index-dEPiAWVG.css +1 -0
  15. package/dist/client/assets/{popover-DM7IA0zJ.js → popover-Z8Tvl-OD.js} +2 -2
  16. package/dist/client/assets/src-BU7yHzO9.js +3 -0
  17. package/dist/client/assets/{toggle-DVtS0BJJ.js → toggle-CWYdWYBn.js} +2 -2
  18. package/dist/client/index.html +11 -11
  19. package/dist/forge-build.json +1 -1
  20. package/dist/{open-D8mGyWmt.mjs → open-FIgTDosf.mjs} +3 -3
  21. package/package.json +1 -1
  22. package/dist/client/assets/index-Bn-ASQRG.css +0 -1
  23. package/dist/client/assets/index-C7YKDHIe.js +0 -154
  24. package/dist/client/assets/src-LDQtPKQt.js +0 -3
package/dist/bin.mjs CHANGED
@@ -22,7 +22,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicK
22
22
  import * as FS$1 from "node:fs";
23
23
  import fs, { accessSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, mkdtempSync, promises, readFile, readFileSync, readdir, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
24
24
  import * as NodeOS from "node:os";
25
- import $U, { homedir, networkInterfaces, tmpdir } from "node:os";
25
+ import os, { homedir, networkInterfaces, tmpdir } from "node:os";
26
26
  import * as Path$1 from "node:path";
27
27
  import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
28
28
  import { pathToFileURL } from "node:url";
@@ -17358,7 +17358,8 @@ const ORCHESTRATION_WS_METHODS = {
17358
17358
  getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
17359
17359
  subscribeShell: "orchestration.subscribeShell",
17360
17360
  subscribeThread: "orchestration.subscribeThread",
17361
- searchThreads: "orchestration.searchThreads"
17361
+ searchThreads: "orchestration.searchThreads",
17362
+ getThreadDetailPage: "orchestration.getThreadDetailPage"
17362
17363
  };
17363
17364
  const ProviderKind = Literals([
17364
17365
  "codex",
@@ -17878,6 +17879,21 @@ Struct({
17878
17879
  turnLimit: optionalKey(PositiveInt),
17879
17880
  beforeCursor: optionalKey(TrimmedNonEmptyString)
17880
17881
  });
17882
+ /**
17883
+ * Reads one windowed page of a thread's detail directly over the RPC socket.
17884
+ *
17885
+ * The HTTP snapshot route and `subscribeThread`'s fallback snapshot both take
17886
+ * the same `turnLimit`/`beforeCursor` window, but neither can serve "load
17887
+ * earlier" on a connection with no HTTP endpoint: the fallback snapshot only
17888
+ * fires once, on subscribe. This method exists for exactly that case (a
17889
+ * relay-only connection), gated on the server's `threadDetailPageRpc`
17890
+ * capability so older servers are never sent a method they do not know.
17891
+ */
17892
+ const OrchestrationGetThreadDetailPageInput = Struct({
17893
+ threadId: ThreadId,
17894
+ turnLimit: optionalKey(PositiveInt),
17895
+ beforeCursor: optionalKey(TrimmedNonEmptyString)
17896
+ });
17881
17897
  const OrchestrationThreadDetailSnapshot = Struct({
17882
17898
  snapshotSequence: NonNegativeInt,
17883
17899
  thread: OrchestrationThread,
@@ -18842,6 +18858,10 @@ const OrchestrationRpcSchemas = {
18842
18858
  searchThreads: {
18843
18859
  input: OrchestrationSearchThreadsInput,
18844
18860
  output: OrchestrationSearchThreadsResult
18861
+ },
18862
+ getThreadDetailPage: {
18863
+ input: OrchestrationGetThreadDetailPageInput,
18864
+ output: OrchestrationThreadDetailSnapshot
18845
18865
  }
18846
18866
  };
18847
18867
  var OrchestrationGetSnapshotError = class extends TaggedErrorClass()("OrchestrationGetSnapshotError", {
@@ -22812,6 +22832,7 @@ const ServerConfig$1 = Struct({
22812
22832
  shellRevealInFileManagerKind: optionalKey(FileManagerRevealKind),
22813
22833
  threadResumeCompletionMarker: optionalKey(Boolean$1),
22814
22834
  threadSnapshotPagination: optionalKey(Boolean$1),
22835
+ threadDetailPageRpc: optionalKey(Boolean$1),
22815
22836
  observability: ServerObservability,
22816
22837
  settings: ServerSettings
22817
22838
  });
@@ -26678,6 +26699,17 @@ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchT
26678
26699
  error: OrchestrationSearchThreadsError
26679
26700
  });
26680
26701
  /**
26702
+ * The socket-only path for "load earlier turns": a relay-only connection has
26703
+ * no HTTP endpoint, so this reads the same windowed page the HTTP snapshot
26704
+ * route and `subscribeThread`'s fallback snapshot serve, over the RPC socket
26705
+ * instead.
26706
+ */
26707
+ const WsOrchestrationGetThreadDetailPageRpc = make$30(ORCHESTRATION_WS_METHODS.getThreadDetailPage, {
26708
+ payload: OrchestrationRpcSchemas.getThreadDetailPage.input,
26709
+ success: OrchestrationRpcSchemas.getThreadDetailPage.output,
26710
+ error: OrchestrationGetSnapshotError
26711
+ });
26712
+ /**
26681
26713
  * Resolve `owner/repo` to the repository the caller is allowed to clone.
26682
26714
  *
26683
26715
  * Both of these run against the same CMMD-brokered GitHub credential the web
@@ -26790,7 +26822,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
26790
26822
  payload: ProviderUploadFeedbackInput,
26791
26823
  success: ProviderUploadFeedbackResult,
26792
26824
  error: ProviderUploadFeedbackError
26793
- }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
26825
+ }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsOrchestrationGetThreadDetailPageRpc);
26794
26826
  const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
26795
26827
  /**
26796
26828
  * The direct-WebSocket mount of the same group, with per-method scope
@@ -26863,6 +26895,32 @@ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
26863
26895
  }
26864
26896
  };
26865
26897
  }
26898
+ const WORKTREE_LAUNCH_ACTIVITY_KINDS = new Set([
26899
+ "worktree.requested",
26900
+ "worktree.stage",
26901
+ "worktree.output",
26902
+ "worktree.created",
26903
+ "worktree.failed",
26904
+ "setup-script.requested",
26905
+ "setup-script.started",
26906
+ "setup-script.skipped",
26907
+ "setup-script.failed",
26908
+ "launch.agent-starting",
26909
+ "launch.cancelled"
26910
+ ]);
26911
+ function encodeCmmdRuntimeWorktreeLaunchActivityPayload(kind, payload) {
26912
+ return {
26913
+ ...isRecord$23(payload) ? payload : { sourcePayload: payload },
26914
+ forgeWorktreeLaunchActivity: {
26915
+ schemaVersion: 1,
26916
+ source: "native",
26917
+ kind
26918
+ }
26919
+ };
26920
+ }
26921
+ function isCmmdRuntimeWorktreeLaunchActivityKind(kind) {
26922
+ return WORKTREE_LAUNCH_ACTIVITY_KINDS.has(kind);
26923
+ }
26866
26924
  //#endregion
26867
26925
  //#region ../../packages/contracts/src/cmmdProjectPatch.ts
26868
26926
  const CMMD_PROJECT_PATCH_FIELDS = {
@@ -31965,6 +32023,11 @@ const RELAY_METHOD_POLICY_BASES = [
31965
32023
  requiredScope: "thread.read",
31966
32024
  deliveryClass: "subscription"
31967
32025
  },
32026
+ {
32027
+ method: ORCHESTRATION_WS_METHODS.getThreadDetailPage,
32028
+ requiredScope: "thread.read",
32029
+ deliveryClass: "read"
32030
+ },
31968
32031
  {
31969
32032
  method: WS_METHODS.assetsCreateUrl,
31970
32033
  requiredScope: "thread.read",
@@ -50634,7 +50697,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
50634
50697
  const min = forceColor || 0;
50635
50698
  if (env.TERM === "dumb") return min;
50636
50699
  if (process$1.platform === "win32") {
50637
- const osRelease = $U.release().split(".");
50700
+ const osRelease = os.release().split(".");
50638
50701
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
50639
50702
  return 1;
50640
50703
  }
@@ -53077,7 +53140,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
53077
53140
  exports.processDetector = void 0;
53078
53141
  const api_1 = (init_esm$1(), __toCommonJS(esm_exports$1));
53079
53142
  const semconv_1 = require_semconv$1();
53080
- const os$3 = __require("os");
53143
+ const os$4 = __require("os");
53081
53144
  /**
53082
53145
  * ProcessDetector will be used to detect the resources related current process running
53083
53146
  * and being instrumented from the NodeJS Process module.
@@ -53099,7 +53162,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
53099
53162
  };
53100
53163
  if (process.argv.length > 1) attributes[semconv_1.ATTR_PROCESS_COMMAND] = process.argv[1];
53101
53164
  try {
53102
- const userInfo = os$3.userInfo();
53165
+ const userInfo = os$4.userInfo();
53103
53166
  attributes[semconv_1.ATTR_PROCESS_OWNER] = userInfo.username;
53104
53167
  } catch (e) {
53105
53168
  api_1.diag.debug(`error obtaining process owner: ${e}`);
@@ -66022,7 +66085,7 @@ function normalizeNumberish(value) {
66022
66085
  }
66023
66086
  //#endregion
66024
66087
  //#region package.json
66025
- var version$1 = "0.13.12";
66088
+ var version$1 = "0.13.13";
66026
66089
  //#endregion
66027
66090
  //#region src/sentry.ts
66028
66091
  const SERVER_APP_NAME = "forge-server";
@@ -66424,6 +66487,129 @@ const respondToAuthError = (error) => gen(function* () {
66424
66487
  }));
66425
66488
  return response;
66426
66489
  });
66490
+ //#endregion
66491
+ //#region src/http/rateLimit.ts
66492
+ /**
66493
+ * Per-client, per-route-class rate limiting for Forge's own HTTP surface.
66494
+ *
66495
+ * Scoped to the routes a broken or looping client can hammer before it ever
66496
+ * has an authenticated session — the CMMD device-code poll, the bootstrap and
66497
+ * pairing-token exchanges — plus the authenticated /api/cmmd proxy, which
66498
+ * every desktop UI interaction runs through. None of these had any limit
66499
+ * before FOR-36706: a stuck pairing loop or a poll that never saw `slow_down`
66500
+ * could retry as fast as the network allowed, with nothing to stop it and
66501
+ * nothing telling the client to back off.
66502
+ *
66503
+ * Defaults are generous on purpose. This exists to bound a runaway client,
66504
+ * not to throttle a working one — see `defaultRateLimitPolicies` for the
66505
+ * reasoning behind each number and how to override it.
66506
+ *
66507
+ * Deliberately plain module state, not an Effect service: every route this
66508
+ * guards is a synchronous decision (count, compare, maybe reset a window),
66509
+ * and threading a new Context requirement through the whole server's layer
66510
+ * graph for that would touch every test file that stands up any of these
66511
+ * routes. `cmmdJwtResolver.ts`'s in-flight-refresh map is the same call.
66512
+ */
66513
+ function envInt(name, fallback) {
66514
+ const raw = process.env[name];
66515
+ if (!raw) return fallback;
66516
+ const parsed = Number(raw);
66517
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
66518
+ }
66519
+ /**
66520
+ * One reasoning line per class, so a future change to a default has to
66521
+ * confront why the old one was picked:
66522
+ *
66523
+ * - `auth-device-poll` (`/api/auth/cmmd-oauth/device` and `/token`): RFC 8628
66524
+ * polls every `interval` seconds, 5 by default, so a well-behaved client
66525
+ * sits under 12/min. 120/min leaves room for several concurrent sign-in
66526
+ * attempts and CMMD's own `slow_down` handling without ever tripping on
66527
+ * normal use, while still bounding a client that ignores `slow_down`.
66528
+ * - `auth-bootstrap` (`/api/auth/bootstrap*`, the bearer bootstrap routes):
66529
+ * a working client bootstraps once per sign-in. 30/min is generous headroom
66530
+ * for retries after a network blip, not a budget anyone should approach.
66531
+ * - `auth-token-exchange` (`/oauth/token`, the vendored pairing exchange):
66532
+ * same shape as bootstrap — one pairing attempt, occasionally retried.
66533
+ * - `cmmd-proxy` (every `/api/cmmd/*` route): this is the one real desktop
66534
+ * traffic actually leans on — task lists, project browsing, comments, all
66535
+ * day. 1200/min (20/sec) is set high enough that no normal session should
66536
+ * ever see it; it exists for a retry loop or a runaway resync, not for
66537
+ * ordinary use.
66538
+ */
66539
+ function buildPoliciesFromEnv() {
66540
+ return {
66541
+ "auth-device-poll": {
66542
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_MAX", 120),
66543
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_WINDOW_SECONDS", 60) * 1e3
66544
+ },
66545
+ "auth-bootstrap": {
66546
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_MAX", 30),
66547
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_WINDOW_SECONDS", 60) * 1e3
66548
+ },
66549
+ "auth-token-exchange": {
66550
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_MAX", 30),
66551
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_WINDOW_SECONDS", 60) * 1e3
66552
+ },
66553
+ "cmmd-proxy": {
66554
+ maxRequests: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_MAX", 1200),
66555
+ windowMs: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_WINDOW_SECONDS", 60) * 1e3
66556
+ }
66557
+ };
66558
+ }
66559
+ let policies = buildPoliciesFromEnv();
66560
+ const windows = /* @__PURE__ */ new Map();
66561
+ function windowKey(routeClass, clientKey) {
66562
+ return `${routeClass}\0${clientKey}`;
66563
+ }
66564
+ /**
66565
+ * Fixed-window counting: cheap, and precise enough for a loop-guard rather
66566
+ * than a security boundary. The one imprecision this trades away — up to 2x
66567
+ * the configured rate right at a window boundary — matters for an attacker
66568
+ * budgeting requests against the limiter's clock, not for the runaway
66569
+ * clients this exists to catch.
66570
+ */
66571
+ function checkHttpRateLimit(input) {
66572
+ const policy = policies[input.routeClass];
66573
+ const nowMs = Date.now();
66574
+ const key = windowKey(input.routeClass, input.clientKey);
66575
+ const existing = windows.get(key);
66576
+ const windowIsLive = existing !== void 0 && nowMs - existing.windowStartMs < policy.windowMs;
66577
+ const windowStartMs = windowIsLive ? existing.windowStartMs : nowMs;
66578
+ const count = (windowIsLive ? existing.count : 0) + 1;
66579
+ windows.set(key, {
66580
+ windowStartMs,
66581
+ count
66582
+ });
66583
+ return {
66584
+ allowed: count <= policy.maxRequests,
66585
+ retryAfterSeconds: Math.max(0, Math.ceil((windowStartMs + policy.windowMs - nowMs) / 1e3))
66586
+ };
66587
+ }
66588
+ /**
66589
+ * Checks the limiter for one request and, when the caller is over budget,
66590
+ * returns the 429 to send instead of running the route — `null` means
66591
+ * proceed. Every route this guards is expected to call this first and return
66592
+ * early on a non-null result, before doing any other work.
66593
+ */
66594
+ function rateLimitOrRespond(input) {
66595
+ const decision = checkHttpRateLimit(input);
66596
+ if (decision.allowed) return null;
66597
+ return jsonUnsafe({ error: "Too many requests. Try again later." }, {
66598
+ status: 429,
66599
+ headers: { "retry-after": String(decision.retryAfterSeconds) }
66600
+ });
66601
+ }
66602
+ /**
66603
+ * The client identity for a route with no session yet — the device-code
66604
+ * poll, bootstrap and pairing-token exchange all run before Forge has issued
66605
+ * one. IP address is the only stable-enough signal available at that point;
66606
+ * the authenticated `/api/cmmd` proxy keys on the session instead (see its
66607
+ * call site), so one desktop and one phone sharing a NAT never share a
66608
+ * budget there.
66609
+ */
66610
+ function clientKeyFromRequest(request) {
66611
+ return normalizeIpAddress(getOrNull(request.remoteAddress)) ?? "unknown";
66612
+ }
66427
66613
  const OAUTH_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
66428
66614
  /**
66429
66615
  * The one exchange this endpoint performs, or the reason it will not.
@@ -68271,6 +68457,11 @@ function commandOAuthProxyStatus(path, status, body) {
68271
68457
  }
68272
68458
  const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", gen(function* () {
68273
68459
  const request = yield* HttpServerRequest;
68460
+ const limited = rateLimitOrRespond({
68461
+ routeClass: "auth-bootstrap",
68462
+ clientKey: clientKeyFromRequest(request)
68463
+ });
68464
+ if (limited) return limited;
68274
68465
  const serverAuth = yield* ServerAuth;
68275
68466
  const sessions = yield* SessionCredentialService;
68276
68467
  const payload = yield* schemaBodyJson(AuthBootstrapInput).pipe(mapError((cause) => new AuthError({
@@ -68288,6 +68479,11 @@ const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", gen(functio
68288
68479
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68289
68480
  const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command", gen(function* () {
68290
68481
  const request = yield* HttpServerRequest;
68482
+ const limited = rateLimitOrRespond({
68483
+ routeClass: "auth-bootstrap",
68484
+ clientKey: clientKeyFromRequest(request)
68485
+ });
68486
+ if (limited) return limited;
68291
68487
  const serverAuth = yield* ServerAuth;
68292
68488
  const sessions = yield* SessionCredentialService;
68293
68489
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68336,6 +68532,11 @@ const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/comman
68336
68532
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68337
68533
  const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command/bearer", gen(function* () {
68338
68534
  const request = yield* HttpServerRequest;
68535
+ const limited = rateLimitOrRespond({
68536
+ routeClass: "auth-bootstrap",
68537
+ clientKey: clientKeyFromRequest(request)
68538
+ });
68539
+ if (limited) return limited;
68339
68540
  const serverAuth = yield* ServerAuth;
68340
68541
  const sessions = yield* SessionCredentialService;
68341
68542
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68384,6 +68585,11 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68384
68585
  }, { status: 200 });
68385
68586
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68386
68587
  const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device", gen(function* () {
68588
+ const limited = rateLimitOrRespond({
68589
+ routeClass: "auth-device-poll",
68590
+ clientKey: clientKeyFromRequest(yield* HttpServerRequest)
68591
+ });
68592
+ if (limited) return limited;
68387
68593
  const payload = yield* schemaBodyJson(CommandOAuthDeviceInput).pipe(mapError((cause) => new AuthError({
68388
68594
  message: "Invalid CMMD OAuth device payload.",
68389
68595
  status: 400,
@@ -68395,7 +68601,17 @@ const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device
68395
68601
  ...payload.scope ? { scope: payload.scope } : {}
68396
68602
  });
68397
68603
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68604
+ /**
68605
+ * The device-code poll. Called every `interval` seconds by design (RFC
68606
+ * 8628), which is exactly why FOR-36706 puts a limiter on it: a client that
68607
+ * ignores `slow_down` — a bug, not a person — has no other backstop here.
68608
+ */
68398
68609
  const authCmmdOAuthTokenRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/token", gen(function* () {
68610
+ const limited = rateLimitOrRespond({
68611
+ routeClass: "auth-device-poll",
68612
+ clientKey: clientKeyFromRequest(yield* HttpServerRequest)
68613
+ });
68614
+ if (limited) return limited;
68399
68615
  const payload = yield* schemaBodyJson(CommandOAuthTokenInput).pipe(mapError((cause) => new AuthError({
68400
68616
  message: "Invalid CMMD OAuth token payload.",
68401
68617
  status: 400,
@@ -68690,6 +68906,11 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
68690
68906
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68691
68907
  const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer", gen(function* () {
68692
68908
  const request = yield* HttpServerRequest;
68909
+ const limited = rateLimitOrRespond({
68910
+ routeClass: "auth-bootstrap",
68911
+ clientKey: clientKeyFromRequest(request)
68912
+ });
68913
+ if (limited) return limited;
68693
68914
  const serverAuth = yield* ServerAuth;
68694
68915
  const payload = yield* schemaBodyJson(AuthBootstrapInput).pipe(mapError((cause) => new AuthError({
68695
68916
  message: "Invalid bootstrap payload.",
@@ -68721,6 +68942,11 @@ const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer"
68721
68942
  */
68722
68943
  const authOAuthTokenExchangeRouteLayer = add$1("POST", "/oauth/token", gen(function* () {
68723
68944
  const request = yield* HttpServerRequest;
68945
+ const limited = rateLimitOrRespond({
68946
+ routeClass: "auth-token-exchange",
68947
+ clientKey: clientKeyFromRequest(request)
68948
+ });
68949
+ if (limited) return limited;
68724
68950
  const serverAuth = yield* ServerAuth;
68725
68951
  const payload = yield* schemaBodyUrlParams(OAuthTokenExchangeInput).pipe(mapError((cause) => new AuthError({
68726
68952
  message: "Invalid token exchange payload.",
@@ -70923,9 +71149,12 @@ function trySubjectFallback(sessionId, subject, logPrefix) {
70923
71149
  * Fail-closed: never returns a non-user / malformed Bearer. Callers must map
70924
71150
  * null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
70925
71151
  */
70926
- function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false }) {
71152
+ function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
70927
71153
  const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
70928
- if (!forceRefresh && cached && !isJwtExpiredForSession(sessionId)) return succeed(cached);
71154
+ if (cached && !isJwtExpiredForSession(sessionId)) {
71155
+ if (!forceRefresh) return succeed(cached);
71156
+ if (staleAccessToken && cached !== staleAccessToken) return succeed(cached);
71157
+ }
70929
71158
  const normalizedSubject = subject?.trim() || null;
70930
71159
  if (!normalizedSubject && !forceRefresh) return succeed(null);
70931
71160
  return gen(function* () {
@@ -71762,6 +71991,11 @@ function buildProxyRouteLayer(route) {
71762
71991
  const handler = gen(function* () {
71763
71992
  const req = yield* HttpServerRequest;
71764
71993
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
71994
+ const limited = rateLimitOrRespond({
71995
+ routeClass: "cmmd-proxy",
71996
+ clientKey: session.sessionId
71997
+ });
71998
+ if (limited) return limited;
71765
71999
  const pathParams = yield* params;
71766
72000
  const cmmdPath = substitutePath(route.cmmdPath, pathParams);
71767
72001
  if (!cmmdPath) return jsonUnsafe({ error: "Missing required path parameter." }, { status: 400 });
@@ -71821,6 +72055,11 @@ function buildProxyHandlerEffect(route) {
71821
72055
  return gen(function* () {
71822
72056
  const req = yield* HttpServerRequest;
71823
72057
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
72058
+ const limited = rateLimitOrRespond({
72059
+ routeClass: "cmmd-proxy",
72060
+ clientKey: session.sessionId
72061
+ });
72062
+ if (limited) return limited;
71824
72063
  if (!readCommandAccessTokenForSession(session.sessionId)) return yield* respondCmmdConnectionRequired(session.sessionId);
71825
72064
  const parsed = yield* route.parse(req);
71826
72065
  if (!parsed.ok) return jsonUnsafe(parsed.body, { status: parsed.status });
@@ -75459,7 +75698,7 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
75459
75698
  var fs$6 = __require("fs");
75460
75699
  var path$5 = __require("path");
75461
75700
  var url$1 = __require("url");
75462
- var os$2 = __require("os");
75701
+ var os$3 = __require("os");
75463
75702
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
75464
75703
  var vars = process.config && process.config.variables || {};
75465
75704
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
@@ -75467,8 +75706,8 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
75467
75706
  var abi = versions.modules;
75468
75707
  if (versions.deno || process.isBun) abi = "unsupported";
75469
75708
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
75470
- var arch = process.env.npm_config_arch || os$2.arch();
75471
- var platform = process.env.npm_config_platform || os$2.platform();
75709
+ var arch = process.env.npm_config_arch || os$3.arch();
75710
+ var platform = process.env.npm_config_platform || os$3.platform();
75472
75711
  var libc = process.env.LIBC || (isMusl(platform) ? "musl" : "glibc");
75473
75712
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
75474
75713
  var uv = (versions.uv || "").split(".")[0];
@@ -93650,7 +93889,7 @@ const launchDetached = (launch) => gen(function* () {
93650
93889
  });
93651
93890
  const OpenLive = effect(Open, gen(function* () {
93652
93891
  const open = yield* tryPromise({
93653
- try: () => import("./open-D8mGyWmt.mjs"),
93892
+ try: () => import("./open-FIgTDosf.mjs"),
93654
93893
  catch: (cause) => new OpenError$1({
93655
93894
  message: "failed to load browser opener",
93656
93895
  cause
@@ -97647,6 +97886,26 @@ function serializeNativeToolActivity(activity) {
97647
97886
  }
97648
97887
  };
97649
97888
  }
97889
+ function serializeWorktreeLaunchActivity(activity) {
97890
+ if (decodeWorktreeLaunchActivityKind(activity.kind) === null) return null;
97891
+ const summary = text(activity.summary, 4e3);
97892
+ if (summary === null) return null;
97893
+ return {
97894
+ sourceSequence: null,
97895
+ event: {
97896
+ eventId: `activity:${activity.id}`,
97897
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
97898
+ ...eventTurn(activity.turnId),
97899
+ kind: "tool_call",
97900
+ summary,
97901
+ status: "completed",
97902
+ payload: encodeCmmdRuntimeWorktreeLaunchActivityPayload(activity.kind, activity.payload)
97903
+ }
97904
+ };
97905
+ }
97906
+ function decodeWorktreeLaunchActivityKind(kind) {
97907
+ return isCmmdRuntimeWorktreeLaunchActivityKind(kind) ? kind : null;
97908
+ }
97650
97909
  function isReplicatedFailureActivity(activity) {
97651
97910
  return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
97652
97911
  }
@@ -98027,12 +98286,17 @@ function serializeRuntimeThreadEvents(thread) {
98027
98286
  const event = serializeNativeToolActivity(activity);
98028
98287
  return event === null ? [] : [event];
98029
98288
  });
98289
+ const worktreeLaunchActivities = thread.activities.flatMap((activity) => {
98290
+ const event = serializeWorktreeLaunchActivity(activity);
98291
+ return event === null ? [] : [event];
98292
+ });
98030
98293
  const recordedTurns = serializeRecordedTurns(thread);
98031
98294
  const textGenerations = serializeTextGenerations(thread);
98032
98295
  return [
98033
98296
  ...messages,
98034
98297
  ...replicaActivities,
98035
98298
  ...toolActivities,
98299
+ ...worktreeLaunchActivities,
98036
98300
  ...failureActivities,
98037
98301
  ...recordedTurns,
98038
98302
  ...textGenerations
@@ -106186,6 +106450,39 @@ function loadInitialShellSnapshot(input) {
106186
106450
  return yield* input.projectionSnapshotQuery.getShellSnapshot();
106187
106451
  });
106188
106452
  }
106453
+ /**
106454
+ * The thread snapshot a subscription falls back to, windowed when the
106455
+ * subscriber asked for a window. A missing thread is a failure rather than an
106456
+ * empty snapshot: the subscriber asked about a thread this server does not
106457
+ * have, and saying so is the only honest answer.
106458
+ *
106459
+ * Also the read behind `getThreadDetailPage`, the socket RPC a relay-only
106460
+ * connection uses for "load earlier turns" in place of the HTTP snapshot
106461
+ * route: same window, same pure `windowThreadDetail`, so the two reads cannot
106462
+ * disagree about what a page is.
106463
+ */
106464
+ function loadThreadDetailSnapshot(input) {
106465
+ return gen(function* () {
106466
+ const [threadDetail, snapshotSequence] = yield* all([input.projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
106467
+ message: `Failed to load thread ${input.threadId}`,
106468
+ cause
106469
+ }))), input.orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence))]);
106470
+ if (isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106471
+ message: `Thread ${input.threadId} was not found`,
106472
+ cause: input.threadId
106473
+ });
106474
+ const windowed = windowThreadDetail({
106475
+ thread: threadDetail.value,
106476
+ window: input.window,
106477
+ snapshotSequence
106478
+ });
106479
+ return {
106480
+ snapshotSequence,
106481
+ thread: windowed.thread,
106482
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
106483
+ };
106484
+ });
106485
+ }
106189
106486
  function makeSnapshotThenLiveStream(input) {
106190
106487
  return unwrap$1(gen(function* () {
106191
106488
  const liveEvents = yield* input.subscribeLiveEvents;
@@ -106411,32 +106708,6 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
106411
106708
  }, normalizedCommand) : (normalizedCommand.type === "thread.turn.interrupt" ? interruptBootstrapTurnStart(orchestrationEngine, normalizedCommand) : orchestrationEngine.dispatch(normalizedCommand)).pipe(mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
106412
106709
  return startup.enqueueCommand(dispatchEffect).pipe(mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
106413
106710
  };
106414
- /**
106415
- * The thread snapshot a subscription falls back to, windowed when the
106416
- * subscriber asked for a window. A missing thread is a failure rather
106417
- * than an empty snapshot: the subscriber asked about a thread this server
106418
- * does not have, and saying so is the only honest answer.
106419
- */
106420
- const loadThreadDetailSnapshot = (threadId, turnLimit) => gen(function* () {
106421
- const [threadDetail, snapshotSequence] = yield* all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
106422
- message: `Failed to load thread ${threadId}`,
106423
- cause
106424
- }))), orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence))]);
106425
- if (isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106426
- message: `Thread ${threadId} was not found`,
106427
- cause: threadId
106428
- });
106429
- const windowed = windowThreadDetail({
106430
- thread: threadDetail.value,
106431
- window: turnLimit === void 0 ? {} : { turnLimit },
106432
- snapshotSequence
106433
- });
106434
- return {
106435
- snapshotSequence,
106436
- thread: windowed.thread,
106437
- ...windowed.page === void 0 ? {} : { page: windowed.page }
106438
- };
106439
- });
106440
106711
  const loadServerConfig = gen(function* () {
106441
106712
  const keybindingsConfig = yield* keybindings.loadConfigState;
106442
106713
  const providers = yield* providerRegistry.getProviders;
@@ -106460,6 +106731,7 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
106460
106731
  },
106461
106732
  threadResumeCompletionMarker: true,
106462
106733
  threadSnapshotPagination: true,
106734
+ threadDetailPageRpc: true,
106463
106735
  observability: {
106464
106736
  logsDirectoryPath: config.logsDir,
106465
106737
  localTracingEnabled: true,
@@ -106560,12 +106832,26 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
106560
106832
  [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, succeed(makeThreadSubscriptionStream(input, {
106561
106833
  subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(map$3((events) => events.pipe(filter$2(isThreadDetailEventFor(input.threadId))))),
106562
106834
  currentSequence: orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence)),
106563
- loadSnapshot: loadThreadDetailSnapshot(input.threadId, input.turnLimit),
106835
+ loadSnapshot: loadThreadDetailSnapshot({
106836
+ threadId: input.threadId,
106837
+ window: input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
106838
+ projectionSnapshotQuery,
106839
+ orchestrationEngine
106840
+ }),
106564
106841
  replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(filter$2(isThreadDetailEventFor(input.threadId)), mapError$1((cause) => new OrchestrationGetSnapshotError({
106565
106842
  message: `Failed to replay thread ${input.threadId}`,
106566
106843
  cause
106567
106844
  })))
106568
106845
  })), { "rpc.aggregate": "orchestration" }),
106846
+ [ORCHESTRATION_WS_METHODS.getThreadDetailPage]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.getThreadDetailPage, loadThreadDetailSnapshot({
106847
+ threadId: input.threadId,
106848
+ window: {
106849
+ ...input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
106850
+ ...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor }
106851
+ },
106852
+ projectionSnapshotQuery,
106853
+ orchestrationEngine
106854
+ }), { "rpc.aggregate": "orchestration" }),
106569
106855
  [WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
106570
106856
  [WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(map$3((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
106571
106857
  [WS_METHODS.serverStartProviderAuth]: (input) => observeRpcEffect(WS_METHODS.serverStartProviderAuth, gen(function* () {
@@ -190966,7 +191252,7 @@ var Vce = {
190966
191252
  function Ve() {
190967
191253
  return Vce;
190968
191254
  }
190969
- var Us = $U.homedir(), lk = $U.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
191255
+ var Us = os.homedir(), lk = os.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
190970
191256
  let t = path.join(Us, "Library");
190971
191257
  return {
190972
191258
  data: path.join(t, "Application Support", e),
@@ -201166,12 +201452,12 @@ function qZ(e, t) {
201166
201452
  }
201167
201453
  return n;
201168
201454
  }
201169
- var os$1 = "https://code.claude.com/docs/en", kKe = [
201455
+ var os$2 = "https://code.claude.com/docs/en", kKe = [
201170
201456
  {
201171
201457
  matches: (e) => e.path === "permissions.defaultMode" && e.code === "invalid_value",
201172
201458
  tip: {
201173
201459
  suggestion: "Valid modes: \"acceptEdits\" (ask before file changes), \"plan\" (analysis only), \"bypassPermissions\" (auto-accept all), or \"default\" (standard behavior)",
201174
- docLink: `${os$1}/iam#permission-modes`
201460
+ docLink: `${os$2}/iam#permission-modes`
201175
201461
  }
201176
201462
  },
201177
201463
  {
@@ -201186,7 +201472,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
201186
201472
  matches: (e) => e.path.startsWith("env.") && e.code === "invalid_type",
201187
201473
  tip: {
201188
201474
  suggestion: "Environment variables must be strings. Wrap numbers and booleans in quotes. Example: \"DEBUG\": \"true\", \"PORT\": \"3000\"",
201189
- docLink: `${os$1}/settings#environment-variables`
201475
+ docLink: `${os$2}/settings#environment-variables`
201190
201476
  }
201191
201477
  },
201192
201478
  {
@@ -201197,14 +201483,14 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
201197
201483
  matches: (e) => e.path.startsWith("hooks.") && e.code === "invalid_key",
201198
201484
  tip: {
201199
201485
  suggestion: "Not a recognized hook event. Common events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. Check spelling and capitalization.",
201200
- docLink: `${os$1}/hooks`
201486
+ docLink: `${os$2}/hooks`
201201
201487
  }
201202
201488
  },
201203
201489
  {
201204
201490
  matches: (e) => /\.hooks\.\d+\.command$/.test(e.path) && e.code === "invalid_type" && e.received === "undefined",
201205
201491
  tip: {
201206
201492
  suggestion: "Command hooks require `command`. For exec form (no shell), set `command` to the executable and `args` to its arguments: {\"type\": \"command\", \"command\": \"echo\", \"args\": [\"hi\"]}. For shell form, set `command` to the full shell string: {\"type\": \"command\", \"command\": \"echo hi\"}.",
201207
- docLink: `${os$1}/hooks#exec-form-and-shell-form`
201493
+ docLink: `${os$2}/hooks#exec-form-and-shell-form`
201208
201494
  }
201209
201495
  },
201210
201496
  {
@@ -201219,7 +201505,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
201219
201505
  matches: (e) => e.code === "unrecognized_keys",
201220
201506
  tip: {
201221
201507
  suggestion: "Check for typos or refer to the documentation for valid fields",
201222
- docLink: `${os$1}/settings`
201508
+ docLink: `${os$2}/settings`
201223
201509
  }
201224
201510
  },
201225
201511
  {
@@ -201234,13 +201520,13 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
201234
201520
  matches: (e) => e.path === "permissions.additionalDirectories" && e.code === "invalid_type",
201235
201521
  tip: {
201236
201522
  suggestion: "Must be an array of directory paths. Example: [\"~/projects\", \"/tmp/workspace\"]. You can also use --add-dir flag or /add-dir command",
201237
- docLink: `${os$1}/iam#working-directories`
201523
+ docLink: `${os$2}/iam#working-directories`
201238
201524
  }
201239
201525
  }
201240
201526
  ], AKe = {
201241
- permissions: `${os$1}/iam#configuring-permissions`,
201242
- env: `${os$1}/settings#environment-variables`,
201243
- hooks: `${os$1}/hooks`
201527
+ permissions: `${os$2}/iam#configuring-permissions`,
201528
+ env: `${os$2}/settings#environment-variables`,
201529
+ hooks: `${os$2}/hooks`
201244
201530
  };
201245
201531
  function GZ(e) {
201246
201532
  let t = kKe.find((r) => r.matches(e));
@@ -212612,6 +212898,8 @@ const PUBLIC_PROVIDER_LEAK_PATTERNS = [
212612
212898
  /\bCURSOR_API_KEY\b/,
212613
212899
  /\bOPENCODE_API_KEY\b/,
212614
212900
  /\bforge_proxy_[A-Za-z0-9._-]+\b/,
212901
+ /\bProviderValidationError\b/,
212902
+ /\/forge-server\/node_modules\//,
212615
212903
  /https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
212616
212904
  /https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
212617
212905
  ];
@@ -212621,6 +212909,12 @@ function isProviderRateOrQuotaError(message) {
212621
212909
  const normalized = message.toLowerCase();
212622
212910
  return /\b(rate[\s-]?limit|too\s+many\s+requests|usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/.test(normalized) || /\b429\b/.test(normalized);
212623
212911
  }
212912
+ function providerLifecycleOrAccountCopy(message) {
212913
+ if (/\bbound to provider account\b/.test(message) && /\bcannot use provider account\b/.test(message)) return "Forge could not switch this conversation to the selected AI account. Retry the account switch.";
212914
+ if (/\bforge_runtime_repairing\b/.test(message) || /\bthis turn was interrupted because the environment is being repaired\b/.test(message)) return "This turn was interrupted because the environment is being repaired. Retry after the repair finishes.";
212915
+ if (/\bforge_runtime_stopped\b/.test(message) || /\bthis turn was interrupted because the environment was stopped\b/.test(message)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
212916
+ return null;
212917
+ }
212624
212918
  /**
212625
212919
  * Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
212626
212920
  *
@@ -212633,7 +212927,8 @@ function isProviderRateOrQuotaError(message) {
212633
212927
  function categorizePublicProviderError(message) {
212634
212928
  if (!message) return null;
212635
212929
  const m = message.toLowerCase();
212636
- if (/\bforge_runtime_stopped\b/.test(m) || /\bthis turn was interrupted because the environment was stopped\b/.test(m)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
212930
+ const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
212931
+ if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
212637
212932
  if (/\bforge_provider_model_not_allowed\b/.test(m) || /\bnot\s+scoped\s+to\s+the\s+requested\s+model\b/.test(m)) return "The workspace AI credential does not cover the model this turn asked for. Retry, or pick that model for the thread.";
212638
212933
  if (/\bmodel\b[\s\S]*\b(not\s+(found|available|supported)|does\s+not\s+exist|unsupported|invalid|unknown)\b/.test(m) || /\b(unsupported|unknown|invalid)\b[\s\S]*\bmodel\b/.test(m) || /\b(model_not_found|invalid_model|model_not_supported)\b/.test(m)) return "The selected model isn't available right now. Pick a different model and retry.";
212639
212934
  if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
@@ -212813,13 +213108,20 @@ function claudeConfigDirectory(env) {
212813
213108
  return null;
212814
213109
  }
212815
213110
  }
212816
- /** The fallback is allowed only for the exact credential bytes CMMD signed. */
212817
- function primaryClaudeCredentialMatchesRevision(env, revision) {
213111
+ /**
213112
+ * The fallback is allowed only when the primary on-disk credential is
213113
+ * byte-identical to what CMMD prepared for the selected account. A ticket's
213114
+ * `credentialRevisionHash` cannot express that: it hashes the encrypted row
213115
+ * CMMD stores, not the decrypted bytes this process ever sees. Comparing
213116
+ * files directly is the only check that means what the fallback needs.
213117
+ */
213118
+ function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
212818
213119
  const directory = claudeConfigDirectory(env);
212819
- if (!directory || !/^[a-f0-9]{64}$/.test(revision)) return false;
213120
+ if (!directory) return false;
212820
213121
  try {
212821
- const credential = readFileSync(join(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
212822
- return createHash("sha256").update(credential).digest("hex") === revision && hasAccessToken(JSON.parse(credential));
213122
+ const primary = readFileSync(join(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213123
+ const prepared = readFileSync(join(preparedDirectory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213124
+ return hasAccessToken(JSON.parse(primary)) && createHash("sha256").update(primary).digest("hex") === createHash("sha256").update(prepared).digest("hex");
212823
213125
  } catch {
212824
213126
  return false;
212825
213127
  }
@@ -217344,7 +217646,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
217344
217646
  baseDir: serverConfig.baseDir,
217345
217647
  accountIdentity: selectedAccountIdentity,
217346
217648
  probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
217347
- primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesRevision(process.env, preparedCredential.credentialRevisionHash)
217649
+ primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
217348
217650
  });
217349
217651
  if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
217350
217652
  provider: PROVIDER$11,
@@ -232483,7 +232785,7 @@ async function acquireSelectedClaudeCredential(expected, transport) {
232483
232785
  const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
232484
232786
  if (!token || !claims) throw new Error("invalid_ticket");
232485
232787
  const credentialJson = await transport.readCredential(token);
232486
- if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || createHash("sha256").update(credentialJson).digest("hex") !== claims.credentialRevisionHash || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
232788
+ if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
232487
232789
  return {
232488
232790
  claims,
232489
232791
  credentialJson
@@ -268449,7 +268751,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
268449
268751
  var dgram = __require("dgram");
268450
268752
  var thunky = require_thunky();
268451
268753
  var events = __require("events");
268452
- var os = __require("os");
268754
+ var os$1 = __require("os");
268453
268755
  var noop = function() {};
268454
268756
  module.exports = function(opts) {
268455
268757
  if (!opts) opts = {};
@@ -268580,14 +268882,14 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
268580
268882
  return that;
268581
268883
  };
268582
268884
  function defaultInterface() {
268583
- var networks = os.networkInterfaces();
268885
+ var networks = os$1.networkInterfaces();
268584
268886
  var names = Object.keys(networks);
268585
268887
  for (var i = 0; i < names.length; i++) {
268586
268888
  var net = networks[names[i]];
268587
268889
  for (var j = 0; j < net.length; j++) {
268588
268890
  var iface = net[j];
268589
268891
  if (isIPv4(iface.family) && !iface.internal) {
268590
- if (os.platform() === "darwin" && names[i] === "en0") return iface.address;
268892
+ if (os$1.platform() === "darwin" && names[i] === "en0") return iface.address;
268591
268893
  return "0.0.0.0";
268592
268894
  }
268593
268895
  }
@@ -268595,7 +268897,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
268595
268897
  return "127.0.0.1";
268596
268898
  }
268597
268899
  function allInterfaces() {
268598
- var networks = os.networkInterfaces();
268900
+ var networks = os$1.networkInterfaces();
268599
268901
  var names = Object.keys(networks);
268600
268902
  var res = [];
268601
268903
  for (var i = 0; i < names.length; i++) {
@@ -291649,6 +291951,10 @@ const ProviderCommandReactorLive = effect(ProviderCommandReactor, gen(function*
291649
291951
  shouldRestartForAccountChange,
291650
291952
  hasResumeCursor: resumeCursor !== void 0
291651
291953
  });
291954
+ if (shouldRestartForAccountChange) yield* providerService.stopSession({
291955
+ threadId,
291956
+ preserveResumeState: true
291957
+ });
291652
291958
  const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
291653
291959
  yield* logInfo("provider command reactor restarted provider session", {
291654
291960
  threadId,
@@ -297584,7 +297890,7 @@ function resolveBuildCommitFromEnv(env) {
297584
297890
  * environment descriptor down instead of reporting an honest "unknown".
297585
297891
  */
297586
297892
  function readBakedBuildCommit() {
297587
- return "e51896589531a57a458ca5c2430dfd54cdc16476";
297893
+ return "81ce9360f406bcb5ae9fbdc9f90dd55d33f25f0c";
297588
297894
  }
297589
297895
  async function resolveServerBuildCommit(input) {
297590
297896
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -298997,20 +299303,20 @@ const generatePairingToken = () => {
298997
299303
  const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
298998
299304
  return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
298999
299305
  };
299306
+ const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
299307
+ message,
299308
+ status: 401
299309
+ });
299310
+ const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
299311
+ message,
299312
+ status: 500,
299313
+ cause
299314
+ });
299000
299315
  const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(function* () {
299001
299316
  const config = yield* ServerConfig;
299002
299317
  const pairingLinks = yield* AuthPairingLinkRepository;
299003
299318
  const seededGrantsRef = yield* make$50(/* @__PURE__ */ new Map());
299004
299319
  const changesPubSub = yield* unbounded$1();
299005
- const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
299006
- message,
299007
- status: 401
299008
- });
299009
- const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
299010
- message,
299011
- status: 500,
299012
- cause
299013
- });
299014
299320
  const seedGrant = (credential, grant) => update$4(seededGrantsRef, (current) => {
299015
299321
  const next = new Map(current);
299016
299322
  next.set(credential, grant);
@@ -299025,19 +299331,19 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
299025
299331
  id
299026
299332
  }).pipe(asVoid);
299027
299333
  if (config.desktopBootstrapToken) {
299028
- const now$2 = yield* now;
299334
+ const now$3 = yield* now;
299029
299335
  yield* seedGrant(config.desktopBootstrapToken, {
299030
299336
  method: "desktop-bootstrap",
299031
299337
  role: "owner",
299032
299338
  subject: "desktop-bootstrap",
299033
- expiresAt: add$3(now$2, { milliseconds: toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
299339
+ expiresAt: add$3(now$3, { milliseconds: toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
299034
299340
  remainingUses: "unbounded"
299035
299341
  });
299036
299342
  }
299037
299343
  const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
299038
299344
  const listActive = () => gen(function* () {
299039
- const now$5 = yield* now;
299040
- return (yield* pairingLinks.listActive({ now: now$5 })).map((row) => row.label ? {
299345
+ const now$2 = yield* now;
299346
+ return (yield* pairingLinks.listActive({ now: now$2 })).map((row) => row.label ? {
299041
299347
  id: row.id,
299042
299348
  credential: row.credential,
299043
299349
  role: row.role,
@@ -299067,8 +299373,8 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
299067
299373
  const id = crypto.randomUUID();
299068
299374
  const credential = generatePairingToken();
299069
299375
  const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
299070
- const now$4 = yield* now;
299071
- const expiresAt = add$3(now$4, { milliseconds: toMillis(ttl) });
299376
+ const now$5 = yield* now;
299377
+ const expiresAt = add$3(now$5, { milliseconds: toMillis(ttl) });
299072
299378
  const issued = {
299073
299379
  id,
299074
299380
  credential,
@@ -299082,7 +299388,7 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
299082
299388
  role: input?.role ?? "client",
299083
299389
  subject: input?.subject ?? "one-time-token",
299084
299390
  label: input?.label ?? null,
299085
- createdAt: now$4,
299391
+ createdAt: now$5,
299086
299392
  expiresAt
299087
299393
  });
299088
299394
  yield* emitUpsert({
@@ -299091,13 +299397,13 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
299091
299397
  role: input?.role ?? "client",
299092
299398
  subject: input?.subject ?? "one-time-token",
299093
299399
  ...input?.label ? { label: input.label } : {},
299094
- createdAt: now$4,
299400
+ createdAt: now$5,
299095
299401
  expiresAt
299096
299402
  });
299097
299403
  return issued;
299098
299404
  }).pipe(mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
299099
299405
  const consume = (credential) => gen(function* () {
299100
- const now$3 = yield* now;
299406
+ const now$4 = yield* now;
299101
299407
  const seededResult = yield* modify$3(seededGrantsRef, (current) => {
299102
299408
  const grant = current.get(credential);
299103
299409
  if (!grant) return [{
@@ -299106,7 +299412,7 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
299106
299412
  error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
299107
299413
  }, current];
299108
299414
  const next = new Map(current);
299109
- if (isGreaterThanOrEqualTo$1(now$3, grant.expiresAt)) {
299415
+ if (isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
299110
299416
  next.delete(credential);
299111
299417
  return [{
299112
299418
  _tag: "error",
@@ -299135,8 +299441,8 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
299135
299441
  if (seededResult.reason !== "not-found") return yield* seededResult.error;
299136
299442
  const consumed = yield* pairingLinks.consumeAvailable({
299137
299443
  credential,
299138
- consumedAt: now$3,
299139
- now: now$3
299444
+ consumedAt: now$4,
299445
+ now: now$4
299140
299446
  });
299141
299447
  if (isSome(consumed)) {
299142
299448
  yield* emitRemoved(consumed.value.id);
@@ -299152,9 +299458,9 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
299152
299458
  if (isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
299153
299459
  if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
299154
299460
  if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
299155
- if (isGreaterThanOrEqualTo$1(now$3, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
299461
+ if (isGreaterThanOrEqualTo$1(now$4, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
299156
299462
  return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
299157
- }).pipe(mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)));
299463
+ }).pipe(mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)), tapError((error) => error.status === 401 ? logWarning$1("Rejected bootstrap credential exchange.", { reason: error.message }) : void_$1));
299158
299464
  return {
299159
299465
  issueOneTimeToken,
299160
299466
  listActive,
@@ -299209,6 +299515,10 @@ const SetAuthSessionLastConnectedAtInput = Struct({
299209
299515
  sessionId: AuthSessionId,
299210
299516
  lastConnectedAt: DateTimeUtcFromString
299211
299517
  });
299518
+ const ExtendAuthSessionExpiryInput = Struct({
299519
+ sessionId: AuthSessionId,
299520
+ expiresAt: DateTimeUtcFromString
299521
+ });
299212
299522
  var AuthSessionRepository = class extends Service$1()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
299213
299523
  //#endregion
299214
299524
  //#region src/persistence/Layers/AuthSessions.ts
@@ -299360,6 +299670,17 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
299360
299670
  WHERE session_id = ${sessionId}
299361
299671
  AND revoked_at IS NULL
299362
299672
  RETURNING session_id AS "sessionId"
299673
+ `
299674
+ });
299675
+ const extendSessionExpiryRows = findAll({
299676
+ Request: ExtendAuthSessionExpiryInput,
299677
+ Result: Struct({ sessionId: AuthSessionId }),
299678
+ execute: ({ sessionId, expiresAt }) => sql`
299679
+ UPDATE auth_sessions
299680
+ SET expires_at = ${expiresAt}
299681
+ WHERE session_id = ${sessionId}
299682
+ AND revoked_at IS NULL
299683
+ RETURNING session_id AS "sessionId"
299363
299684
  `
299364
299685
  });
299365
299686
  const revokeOtherSessionRows = findAll({
@@ -299382,13 +299703,15 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
299382
299703
  const revoke = (input) => revokeSessionRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows")), map$3((rows) => rows.length > 0));
299383
299704
  const revokeAllExcept = (input) => revokeOtherSessionRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows")), map$3((rows) => rows.map((row) => row.sessionId)));
299384
299705
  const setLastConnectedAt = (input) => setLastConnectedAtRow(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest")));
299706
+ const extendExpiry = (input) => extendSessionExpiryRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.extendExpiry:query", "AuthSessionRepository.extendExpiry:decodeRows")), map$3((rows) => rows.length > 0));
299385
299707
  return {
299386
299708
  create,
299387
299709
  getById,
299388
299710
  listActive,
299389
299711
  revoke,
299390
299712
  revokeAllExcept,
299391
- setLastConnectedAt
299713
+ setLastConnectedAt,
299714
+ extendExpiry
299392
299715
  };
299393
299716
  }));
299394
299717
  //#endregion
@@ -299396,6 +299719,20 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
299396
299719
  const SIGNING_SECRET_NAME = "server-signing-key";
299397
299720
  const DEFAULT_SESSION_TTL = days(30);
299398
299721
  const DEFAULT_WEBSOCKET_TOKEN_TTL = minutes(5);
299722
+ /**
299723
+ * Whether a session presented at `nowMs` has used up half its own lifetime.
299724
+ *
299725
+ * A phone that opens the app daily should never hit the 30-day wall, but a
299726
+ * lost phone's session should still die on schedule if it is never presented
299727
+ * again. Sliding the expiry forward only when the session is already more
299728
+ * than half spent gives an active phone many chances to renew before expiry
299729
+ * while keeping a dormant session's remaining lifetime bounded by the last
299730
+ * time it was actually used.
299731
+ */
299732
+ function isPastHalfLife(input) {
299733
+ const halfLifeMarkMs = input.issuedAtMs + (input.expiresAtMs - input.issuedAtMs) / 2;
299734
+ return input.nowMs >= halfLifeMarkMs;
299735
+ }
299399
299736
  const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
299400
299737
  message,
299401
299738
  cause
@@ -299556,6 +299893,42 @@ const SessionCredentialServiceLive = effect(SessionCredentialService, gen(functi
299556
299893
  role: claims.role
299557
299894
  };
299558
299895
  }).pipe(mapError(toSessionCredentialError("Failed to issue session credential.")));
299896
+ /**
299897
+ * Slide a session's expiry forward when the token it just verified is past
299898
+ * half its own lifetime, so a phone that keeps using the app never runs
299899
+ * into the 30-day wall.
299900
+ *
299901
+ * The session id, subject, role and method carry over unchanged — only the
299902
+ * signature-bearing `iat`/`exp` differ — which is what lets the DB row be
299903
+ * extended in place (`extendExpiry`) rather than replaced: everything keyed
299904
+ * by session id, most importantly the `command_access_tokens` row holding
299905
+ * this session's CMMD credential, needs no migration.
299906
+ */
299907
+ const renewIfPastHalfLife = (input) => gen(function* () {
299908
+ const nowMs = yield* currentTimeMillis;
299909
+ if (!isPastHalfLife({
299910
+ issuedAtMs: input.row.issuedAt.epochMilliseconds,
299911
+ expiresAtMs: input.row.expiresAt.epochMilliseconds,
299912
+ nowMs
299913
+ })) return void 0;
299914
+ const newIssuedAt = yield* now;
299915
+ const newExpiresAt = add$3(newIssuedAt, { milliseconds: toMillis(DEFAULT_SESSION_TTL) });
299916
+ const newClaims = {
299917
+ ...input.claims,
299918
+ iat: newIssuedAt.epochMilliseconds,
299919
+ exp: newExpiresAt.epochMilliseconds
299920
+ };
299921
+ const newEncodedPayload = base64UrlEncode(JSON.stringify(newClaims));
299922
+ const newSignature = signPayload(newEncodedPayload, signingSecret);
299923
+ if (!(yield* authSessions.extendExpiry({
299924
+ sessionId: input.claims.sid,
299925
+ expiresAt: newExpiresAt
299926
+ }).pipe(orElseSucceed(() => false)))) return void 0;
299927
+ return {
299928
+ token: `${newEncodedPayload}.${newSignature}`,
299929
+ expiresAt: newExpiresAt
299930
+ };
299931
+ });
299559
299932
  const verify = (token) => gen(function* () {
299560
299933
  const [encodedPayload, signature] = token.split(".");
299561
299934
  if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
@@ -299570,14 +299943,19 @@ const SessionCredentialServiceLive = effect(SessionCredentialService, gen(functi
299570
299943
  if (isSome(row)) {
299571
299944
  if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
299572
299945
  } else if (!(statelessCookieSessions && claims.method === "browser-session-cookie")) return yield* new SessionCredentialError({ message: "Unknown session token." });
299946
+ const renewed = isSome(row) ? yield* renewIfPastHalfLife({
299947
+ claims,
299948
+ row: row.value
299949
+ }) : void 0;
299573
299950
  return {
299574
299951
  sessionId: claims.sid,
299575
299952
  token,
299576
299953
  method: claims.method,
299577
299954
  client: isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
299578
- expiresAt: makeUnsafe$5(claims.exp),
299955
+ expiresAt: renewed?.expiresAt ?? makeUnsafe$5(claims.exp),
299579
299956
  subject: claims.sub,
299580
- role: claims.role
299957
+ role: claims.role,
299958
+ ...renewed ? { renewed } : {}
299581
299959
  };
299582
299960
  }).pipe(mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
299583
299961
  message: "Failed to verify session credential.",
@@ -300069,7 +300447,10 @@ const ServerAuthLive = effect(ServerAuth, gen(function* () {
300069
300447
  subject: session.subject,
300070
300448
  method: session.method,
300071
300449
  role: session.role,
300072
- ...session.expiresAt ? { expiresAt: session.expiresAt } : {}
300450
+ ...session.renewed ? {
300451
+ expiresAt: session.renewed.expiresAt,
300452
+ renewedSessionToken: session.renewed.token
300453
+ } : session.expiresAt ? { expiresAt: session.expiresAt } : {}
300073
300454
  })), mapError((cause) => new AuthError({
300074
300455
  message: "Unauthorized request.",
300075
300456
  status: 401,
@@ -300104,6 +300485,7 @@ const ServerAuthLive = effect(ServerAuth, gen(function* () {
300104
300485
  role: session.role,
300105
300486
  sessionMethod: session.method,
300106
300487
  ...session.expiresAt ? { expiresAt: toUtc(session.expiresAt) } : {},
300488
+ ...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
300107
300489
  cmmdIdentity
300108
300490
  })))), tapError((e) => {
300109
300491
  if (e._tag === "AuthError" && (e.status ?? 500) < 500) return void_$1;
@@ -318374,7 +318756,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
318374
318756
  const sql = yield* SqlClient;
318375
318757
  const fetchFn = options.fetchFn ?? fetch;
318376
318758
  const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
318377
- const resolveStoredJwt = (forceRefresh) => gen(function* () {
318759
+ const resolveStoredJwt = (forceRefresh, staleJwt) => gen(function* () {
318378
318760
  const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(provideService(SqlClient, sql), mapError((cause) => new ProjectLeadControllerClientError({
318379
318761
  kind: "unavailable",
318380
318762
  operation: "resolve-owner-session",
@@ -318388,7 +318770,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
318388
318770
  sessionId: owner.sessionId,
318389
318771
  subject: owner.subject,
318390
318772
  logPrefix: "project-lead-scheduler",
318391
- forceRefresh
318773
+ forceRefresh,
318774
+ staleAccessToken: staleJwt
318392
318775
  }).pipe(provideService(SqlClient, sql));
318393
318776
  if (!jwt) return yield* new ProjectLeadControllerClientError({
318394
318777
  kind: "unauthenticated",
@@ -318431,7 +318814,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
318431
318814
  }));
318432
318815
  if (first._tag === "Success") return first.success;
318433
318816
  if (first.failure.kind !== "unauthenticated") return yield* first.failure;
318434
- const refreshed = yield* resolveJwt(true);
318817
+ const refreshed = yield* resolveJwt(true, jwt);
318435
318818
  return yield* requestAttempt({
318436
318819
  ...input,
318437
318820
  jwt: refreshed
@@ -321836,7 +322219,7 @@ const connectLoginCommand = make$34("login", {
321836
322219
  })).json();
321837
322220
  },
321838
322221
  openBrowser: async (url) => {
321839
- const open = (await import("./open-D8mGyWmt.mjs")).default;
322222
+ const open = (await import("./open-FIgTDosf.mjs")).default;
321840
322223
  await open(url);
321841
322224
  return true;
321842
322225
  },