@cmmd-center/forge 0.13.11 → 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 (53) hide show
  1. package/dist/{BunHttpServer-C7-1JJ7b.cjs → BunHttpServer-C0oPP_3A.cjs} +4 -4
  2. package/dist/{BunHttpServer-CVPGkizx.mjs → BunHttpServer-FbbQmsl_.mjs} +4 -4
  3. package/dist/{BunServices-CPrZSIfE.mjs → BunServices-DC7nhA3w.mjs} +2 -2
  4. package/dist/{BunServices-Dl-3897e.cjs → BunServices-DkenwD7S.cjs} +2 -2
  5. package/dist/{Cache-Cf3ARNaS.cjs → Cache-BmGc_2BZ.cjs} +2 -2
  6. package/dist/{Cache-C9cwdCDt.mjs → Cache-D57GWb9-.mjs} +2 -2
  7. package/dist/{HttpServer-CggZzScj.cjs → HttpServer-BBzRBmVt.cjs} +2 -2
  8. package/dist/{HttpServer-COaBeM7-.mjs → HttpServer-Bclwvu0I.mjs} +2 -2
  9. package/dist/{NodeHttpServer-UQnvZnmu.mjs → NodeHttpServer-BJ4l15f-.mjs} +5 -5
  10. package/dist/{NodeHttpServer-Bi339XFk.cjs → NodeHttpServer-Cjd3bFrD.cjs} +5 -5
  11. package/dist/{NodePTY-UOaRyQsj.cjs → NodePTY-BehazUMi.cjs} +2 -2
  12. package/dist/{NodePTY-B1u6RHXk.mjs → NodePTY-BhezYy9H.mjs} +2 -2
  13. package/dist/{NodeServices-R0BZsZ72.cjs → NodeServices-BvdIc9Vh.cjs} +1 -1
  14. package/dist/{NodeServices-CKFIZVu5.cjs → NodeServices-CtC4a-h5.cjs} +2 -2
  15. package/dist/{NodeServices-z1Ac4lEO.mjs → NodeServices-D3IxOzHl.mjs} +2 -2
  16. package/dist/{NodeSqliteClient-CyJvo9y9.cjs → NodeSqliteClient-CfsdC3AB.cjs} +4 -4
  17. package/dist/{NodeSqliteClient-BrzqdOsF.mjs → NodeSqliteClient-md8xd9hY.mjs} +4 -4
  18. package/dist/{NodeTerminal-pYVVRTiD.mjs → NodeTerminal-9iHZIdt1.mjs} +2 -2
  19. package/dist/{NodeTerminal-B89MobLp.cjs → NodeTerminal-CO-i-6Je.cjs} +2 -2
  20. package/dist/{Path-BunGrE8F.cjs → Path-BQOH_fWQ.cjs} +7 -1
  21. package/dist/{Path-evOm-H0Q.mjs → Path-CZJ1gPPo.mjs} +2 -2
  22. package/dist/{SqlError-CezTFGvr.mjs → SqlError-BMBkezUO.mjs} +2 -2
  23. package/dist/{SqlError-1wIMjXAf.cjs → SqlError-g4pLzNWR.cjs} +2 -2
  24. package/dist/{SqliteClient-D3Yd3ZHn.mjs → SqliteClient-BKB695RF.mjs} +3 -3
  25. package/dist/{SqliteClient-bo7iui1t.cjs → SqliteClient-O1ABIngw.cjs} +3 -3
  26. package/dist/bin.cjs +1451 -233
  27. package/dist/bin.mjs +1470 -260
  28. package/dist/client/assets/{CanvasPanel-z0F-jdf1.js → CanvasPanel-RpD9Eh_S.js} +2 -2
  29. package/dist/client/assets/{CompositeItem-P9UVXCKv.js → CompositeItem-DEIBUHJR.js} +2 -2
  30. package/dist/client/assets/{DiffPanel-BrlSm5kH.js → DiffPanel-cwm7LEtZ.js} +2 -2
  31. package/dist/client/assets/{DiffPanel.logic-OZhO8GfQ.js → DiffPanel.logic-B9AWYFxE.js} +2 -2
  32. package/dist/client/assets/{DiffPanelShell-DnugngHM.js → DiffPanelShell-BhokrrDZ.js} +112 -112
  33. package/dist/client/assets/{DiffWorkerPoolProvider-BOD7cQPe.js → DiffWorkerPoolProvider-BZjEmEzL.js} +2 -2
  34. package/dist/client/assets/{PullRequestCodePanel-DACYpcbJ.js → PullRequestCodePanel-BPJINzCp.js} +2 -2
  35. package/dist/client/assets/{Virtualizer-R_LG4CvG.js → Virtualizer-i6HiQ7Qm.js} +2 -2
  36. package/dist/client/assets/{canvasStateStore-CdQxd2aO.js → canvasStateStore-NrAxDqvM.js} +2 -2
  37. package/dist/client/assets/cmmdAuthRedirect-BahlX0gF.js +2 -0
  38. package/dist/client/assets/{index-BXzYKPmD.js → index-DMWfw0Ih.js} +61 -61
  39. package/dist/client/assets/index-dEPiAWVG.css +1 -0
  40. package/dist/client/assets/{popover-CUSlb3eK.js → popover-Z8Tvl-OD.js} +2 -2
  41. package/dist/client/assets/src-BU7yHzO9.js +3 -0
  42. package/dist/client/assets/{toggle-BREV66nT.js → toggle-CWYdWYBn.js} +2 -2
  43. package/dist/client/index.html +11 -11
  44. package/dist/forge-build.json +1 -1
  45. package/dist/{open-D8mGyWmt.mjs → open-FIgTDosf.mjs} +3 -3
  46. package/dist/runtimeBootstrap.cjs +1 -1
  47. package/dist/runtimeBootstrap.mjs +1 -1
  48. package/dist/{runtimeControlAuthority-B3kNmYRT.cjs → runtimeControlAuthority-BkVB0uAA.cjs} +17 -1
  49. package/dist/{runtimeControlAuthority-D_zKr_eI.mjs → runtimeControlAuthority-E_kdeD_P.mjs} +12 -2
  50. package/package.json +1 -1
  51. package/dist/client/assets/cmmdAuthRedirect-Bp1uQaQ9.js +0 -2
  52. package/dist/client/assets/index-Bn-ASQRG.css +0 -1
  53. package/dist/client/assets/src-BL-mOkT1.js +0 -3
package/dist/bin.cjs CHANGED
@@ -3,16 +3,16 @@
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const require_chunk = require("./chunk-BhaIgcZK.cjs");
5
5
  const require_Schema$1 = require("./Schema-CDCJ3xZo.cjs");
6
- const require_Path = require("./Path-BunGrE8F.cjs");
7
- const require_NodeTerminal = require("./NodeTerminal-B89MobLp.cjs");
8
- const require_NodeServices = require("./NodeServices-CKFIZVu5.cjs");
9
- const require_HttpServer = require("./HttpServer-CggZzScj.cjs");
6
+ const require_Path = require("./Path-BQOH_fWQ.cjs");
7
+ const require_NodeTerminal = require("./NodeTerminal-CO-i-6Je.cjs");
8
+ const require_NodeServices = require("./NodeServices-CtC4a-h5.cjs");
9
+ const require_HttpServer = require("./HttpServer-BBzRBmVt.cjs");
10
10
  const require_Config = require("./Config-CwKCCk14.cjs");
11
- const require_Cache = require("./Cache-Cf3ARNaS.cjs");
12
- const require_SqlError = require("./SqlError-1wIMjXAf.cjs");
11
+ const require_Cache = require("./Cache-BmGc_2BZ.cjs");
12
+ const require_SqlError = require("./SqlError-g4pLzNWR.cjs");
13
13
  const require_Mime = require("./Mime-Bilnh8zE.cjs");
14
14
  const require_esm = require("./esm-BZhXwX5p.cjs");
15
- const require_runtimeControlAuthority = require("./runtimeControlAuthority-B3kNmYRT.cjs");
15
+ const require_runtimeControlAuthority = require("./runtimeControlAuthority-BkVB0uAA.cjs");
16
16
  const require_PTY = require("./PTY-B4ZctbwQ.cjs");
17
17
  let node_child_process = require("node:child_process");
18
18
  node_child_process = require_chunk.__toESM(node_child_process);
@@ -17402,7 +17402,8 @@ const ORCHESTRATION_WS_METHODS = {
17402
17402
  getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
17403
17403
  subscribeShell: "orchestration.subscribeShell",
17404
17404
  subscribeThread: "orchestration.subscribeThread",
17405
- searchThreads: "orchestration.searchThreads"
17405
+ searchThreads: "orchestration.searchThreads",
17406
+ getThreadDetailPage: "orchestration.getThreadDetailPage"
17406
17407
  };
17407
17408
  const ProviderKind = require_Schema$1.Literals([
17408
17409
  "codex",
@@ -17920,10 +17921,47 @@ const OrchestrationShellStreamItem = require_Schema$1.Union([require_Schema$1.St
17920
17921
  kind: require_Schema$1.Literal("snapshot"),
17921
17922
  snapshot: OrchestrationShellSnapshot
17922
17923
  }), OrchestrationShellStreamEvent]);
17923
- const OrchestrationSubscribeThreadInput = require_Schema$1.Struct({ threadId: ThreadId });
17924
+ const OrchestrationSubscribeThreadInput = require_Schema$1.Struct({
17925
+ threadId: ThreadId,
17926
+ afterSequence: require_Schema$1.optionalKey(NonNegativeInt),
17927
+ requestCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
17928
+ turnLimit: require_Schema$1.optionalKey(PositiveInt)
17929
+ });
17930
+ require_Schema$1.Struct({
17931
+ turnLimit: require_Schema$1.optionalKey(PositiveInt),
17932
+ beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
17933
+ });
17934
+ /**
17935
+ * Reads one windowed page of a thread's detail directly over the RPC socket.
17936
+ *
17937
+ * The HTTP snapshot route and `subscribeThread`'s fallback snapshot both take
17938
+ * the same `turnLimit`/`beforeCursor` window, but neither can serve "load
17939
+ * earlier" on a connection with no HTTP endpoint: the fallback snapshot only
17940
+ * fires once, on subscribe. This method exists for exactly that case (a
17941
+ * relay-only connection), gated on the server's `threadDetailPageRpc`
17942
+ * capability so older servers are never sent a method they do not know.
17943
+ */
17944
+ const OrchestrationGetThreadDetailPageInput = require_Schema$1.Struct({
17945
+ threadId: ThreadId,
17946
+ turnLimit: require_Schema$1.optionalKey(PositiveInt),
17947
+ beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
17948
+ });
17949
+ /**
17950
+ * Page metadata for a windowed thread detail read. `beforeCursor` is opaque and
17951
+ * exclusive: passing it back returns the adjacent slice of older turns. `null`
17952
+ * means nothing older is left. `snapshotSequence` mirrors the top-level
17953
+ * sequence so a history page can be sequence-checked against live state before
17954
+ * a client merges it.
17955
+ */
17956
+ const OrchestrationThreadDetailPage = require_Schema$1.Struct({
17957
+ beforeCursor: require_Schema$1.NullOr(TrimmedNonEmptyString),
17958
+ hasMore: require_Schema$1.Boolean,
17959
+ snapshotSequence: NonNegativeInt
17960
+ });
17924
17961
  const OrchestrationThreadDetailSnapshot = require_Schema$1.Struct({
17925
17962
  snapshotSequence: NonNegativeInt,
17926
- thread: OrchestrationThread
17963
+ thread: OrchestrationThread,
17964
+ page: require_Schema$1.optional(OrchestrationThreadDetailPage)
17927
17965
  });
17928
17966
  const ProjectCreateCommand = require_Schema$1.Struct({
17929
17967
  type: require_Schema$1.Literal("project.create"),
@@ -18747,13 +18785,17 @@ const OrchestrationEvent = require_Schema$1.Union([
18747
18785
  payload: ThreadRateLimitSetPayload$1
18748
18786
  })
18749
18787
  ]);
18750
- const OrchestrationThreadStreamItem = require_Schema$1.Union([require_Schema$1.Struct({
18751
- kind: require_Schema$1.Literal("snapshot"),
18752
- snapshot: OrchestrationThreadDetailSnapshot
18753
- }), require_Schema$1.Struct({
18754
- kind: require_Schema$1.Literal("event"),
18755
- event: OrchestrationEvent
18756
- })]);
18788
+ const OrchestrationThreadStreamItem = require_Schema$1.Union([
18789
+ require_Schema$1.Struct({ kind: require_Schema$1.Literal("synchronized") }),
18790
+ require_Schema$1.Struct({
18791
+ kind: require_Schema$1.Literal("snapshot"),
18792
+ snapshot: OrchestrationThreadDetailSnapshot
18793
+ }),
18794
+ require_Schema$1.Struct({
18795
+ kind: require_Schema$1.Literal("event"),
18796
+ event: OrchestrationEvent
18797
+ })
18798
+ ]);
18757
18799
  const OrchestrationCommandReceiptStatus = require_Schema$1.Literals(["accepted", "rejected"]);
18758
18800
  const TurnCountRange = require_Schema$1.Struct({
18759
18801
  fromTurnCount: NonNegativeInt,
@@ -18902,6 +18944,10 @@ const OrchestrationRpcSchemas = {
18902
18944
  searchThreads: {
18903
18945
  input: OrchestrationSearchThreadsInput,
18904
18946
  output: OrchestrationSearchThreadsResult
18947
+ },
18948
+ getThreadDetailPage: {
18949
+ input: OrchestrationGetThreadDetailPageInput,
18950
+ output: OrchestrationThreadDetailSnapshot
18905
18951
  }
18906
18952
  };
18907
18953
  var OrchestrationGetSnapshotError = class extends require_Schema$1.TaggedErrorClass()("OrchestrationGetSnapshotError", {
@@ -22978,6 +23024,9 @@ const ServerConfig$1 = require_Schema$1.Struct({
22978
23024
  availableEditors: require_Schema$1.ArraySchema(EditorId),
22979
23025
  shellRevealInFileManager: require_Schema$1.optionalKey(require_Schema$1.Boolean),
22980
23026
  shellRevealInFileManagerKind: require_Schema$1.optionalKey(FileManagerRevealKind),
23027
+ threadResumeCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
23028
+ threadSnapshotPagination: require_Schema$1.optionalKey(require_Schema$1.Boolean),
23029
+ threadDetailPageRpc: require_Schema$1.optionalKey(require_Schema$1.Boolean),
22981
23030
  observability: ServerObservability,
22982
23031
  settings: ServerSettings
22983
23032
  });
@@ -26937,6 +26986,17 @@ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchT
26937
26986
  error: OrchestrationSearchThreadsError
26938
26987
  });
26939
26988
  /**
26989
+ * The socket-only path for "load earlier turns": a relay-only connection has
26990
+ * no HTTP endpoint, so this reads the same windowed page the HTTP snapshot
26991
+ * route and `subscribeThread`'s fallback snapshot serve, over the RPC socket
26992
+ * instead.
26993
+ */
26994
+ const WsOrchestrationGetThreadDetailPageRpc = make$30(ORCHESTRATION_WS_METHODS.getThreadDetailPage, {
26995
+ payload: OrchestrationRpcSchemas.getThreadDetailPage.input,
26996
+ success: OrchestrationRpcSchemas.getThreadDetailPage.output,
26997
+ error: OrchestrationGetSnapshotError
26998
+ });
26999
+ /**
26940
27000
  * Resolve `owner/repo` to the repository the caller is allowed to clone.
26941
27001
  *
26942
27002
  * Both of these run against the same CMMD-brokered GitHub credential the web
@@ -27049,7 +27109,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
27049
27109
  payload: ProviderUploadFeedbackInput,
27050
27110
  success: ProviderUploadFeedbackResult,
27051
27111
  error: ProviderUploadFeedbackError
27052
- }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
27112
+ }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsOrchestrationGetThreadDetailPageRpc);
27053
27113
  const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
27054
27114
  /**
27055
27115
  * The direct-WebSocket mount of the same group, with per-method scope
@@ -27122,6 +27182,32 @@ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
27122
27182
  }
27123
27183
  };
27124
27184
  }
27185
+ const WORKTREE_LAUNCH_ACTIVITY_KINDS = new Set([
27186
+ "worktree.requested",
27187
+ "worktree.stage",
27188
+ "worktree.output",
27189
+ "worktree.created",
27190
+ "worktree.failed",
27191
+ "setup-script.requested",
27192
+ "setup-script.started",
27193
+ "setup-script.skipped",
27194
+ "setup-script.failed",
27195
+ "launch.agent-starting",
27196
+ "launch.cancelled"
27197
+ ]);
27198
+ function encodeCmmdRuntimeWorktreeLaunchActivityPayload(kind, payload) {
27199
+ return {
27200
+ ...isRecord$23(payload) ? payload : { sourcePayload: payload },
27201
+ forgeWorktreeLaunchActivity: {
27202
+ schemaVersion: 1,
27203
+ source: "native",
27204
+ kind
27205
+ }
27206
+ };
27207
+ }
27208
+ function isCmmdRuntimeWorktreeLaunchActivityKind(kind) {
27209
+ return WORKTREE_LAUNCH_ACTIVITY_KINDS.has(kind);
27210
+ }
27125
27211
  //#endregion
27126
27212
  //#region ../../packages/contracts/src/cmmdProjectPatch.ts
27127
27213
  const CMMD_PROJECT_PATCH_FIELDS = {
@@ -32232,6 +32318,11 @@ const RELAY_METHOD_POLICY_BASES = [
32232
32318
  requiredScope: "thread.read",
32233
32319
  deliveryClass: "subscription"
32234
32320
  },
32321
+ {
32322
+ method: ORCHESTRATION_WS_METHODS.getThreadDetailPage,
32323
+ requiredScope: "thread.read",
32324
+ deliveryClass: "read"
32325
+ },
32235
32326
  {
32236
32327
  method: WS_METHODS.assetsCreateUrl,
32237
32328
  requiredScope: "thread.read",
@@ -41486,7 +41577,7 @@ function splitPath(filename) {
41486
41577
  const parts = splitPathRe.exec(truncated);
41487
41578
  return parts ? parts.slice(1) : [];
41488
41579
  }
41489
- function dirname$7(path) {
41580
+ function dirname$8(path) {
41490
41581
  const result = splitPath(path);
41491
41582
  const root = result[0] || "";
41492
41583
  let dir = result[1];
@@ -58801,7 +58892,7 @@ function getSpotlightConfig(optionsSpotlight) {
58801
58892
  function normalizeWindowsPath(path) {
58802
58893
  return path.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
58803
58894
  }
58804
- function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$7(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
58895
+ function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$8(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
58805
58896
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
58806
58897
  return (filename) => {
58807
58898
  if (!filename) return;
@@ -66289,7 +66380,7 @@ function normalizeNumberish(value) {
66289
66380
  }
66290
66381
  //#endregion
66291
66382
  //#region package.json
66292
- var version$1 = "0.13.11";
66383
+ var version$1 = "0.13.13";
66293
66384
  //#endregion
66294
66385
  //#region src/sentry.ts
66295
66386
  const SERVER_APP_NAME = "forge-server";
@@ -66691,6 +66782,129 @@ const respondToAuthError = (error) => require_Schema$1.gen(function* () {
66691
66782
  }));
66692
66783
  return response;
66693
66784
  });
66785
+ //#endregion
66786
+ //#region src/http/rateLimit.ts
66787
+ /**
66788
+ * Per-client, per-route-class rate limiting for Forge's own HTTP surface.
66789
+ *
66790
+ * Scoped to the routes a broken or looping client can hammer before it ever
66791
+ * has an authenticated session — the CMMD device-code poll, the bootstrap and
66792
+ * pairing-token exchanges — plus the authenticated /api/cmmd proxy, which
66793
+ * every desktop UI interaction runs through. None of these had any limit
66794
+ * before FOR-36706: a stuck pairing loop or a poll that never saw `slow_down`
66795
+ * could retry as fast as the network allowed, with nothing to stop it and
66796
+ * nothing telling the client to back off.
66797
+ *
66798
+ * Defaults are generous on purpose. This exists to bound a runaway client,
66799
+ * not to throttle a working one — see `defaultRateLimitPolicies` for the
66800
+ * reasoning behind each number and how to override it.
66801
+ *
66802
+ * Deliberately plain module state, not an Effect service: every route this
66803
+ * guards is a synchronous decision (count, compare, maybe reset a window),
66804
+ * and threading a new Context requirement through the whole server's layer
66805
+ * graph for that would touch every test file that stands up any of these
66806
+ * routes. `cmmdJwtResolver.ts`'s in-flight-refresh map is the same call.
66807
+ */
66808
+ function envInt(name, fallback) {
66809
+ const raw = process.env[name];
66810
+ if (!raw) return fallback;
66811
+ const parsed = Number(raw);
66812
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
66813
+ }
66814
+ /**
66815
+ * One reasoning line per class, so a future change to a default has to
66816
+ * confront why the old one was picked:
66817
+ *
66818
+ * - `auth-device-poll` (`/api/auth/cmmd-oauth/device` and `/token`): RFC 8628
66819
+ * polls every `interval` seconds, 5 by default, so a well-behaved client
66820
+ * sits under 12/min. 120/min leaves room for several concurrent sign-in
66821
+ * attempts and CMMD's own `slow_down` handling without ever tripping on
66822
+ * normal use, while still bounding a client that ignores `slow_down`.
66823
+ * - `auth-bootstrap` (`/api/auth/bootstrap*`, the bearer bootstrap routes):
66824
+ * a working client bootstraps once per sign-in. 30/min is generous headroom
66825
+ * for retries after a network blip, not a budget anyone should approach.
66826
+ * - `auth-token-exchange` (`/oauth/token`, the vendored pairing exchange):
66827
+ * same shape as bootstrap — one pairing attempt, occasionally retried.
66828
+ * - `cmmd-proxy` (every `/api/cmmd/*` route): this is the one real desktop
66829
+ * traffic actually leans on — task lists, project browsing, comments, all
66830
+ * day. 1200/min (20/sec) is set high enough that no normal session should
66831
+ * ever see it; it exists for a retry loop or a runaway resync, not for
66832
+ * ordinary use.
66833
+ */
66834
+ function buildPoliciesFromEnv() {
66835
+ return {
66836
+ "auth-device-poll": {
66837
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_MAX", 120),
66838
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_WINDOW_SECONDS", 60) * 1e3
66839
+ },
66840
+ "auth-bootstrap": {
66841
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_MAX", 30),
66842
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_WINDOW_SECONDS", 60) * 1e3
66843
+ },
66844
+ "auth-token-exchange": {
66845
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_MAX", 30),
66846
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_WINDOW_SECONDS", 60) * 1e3
66847
+ },
66848
+ "cmmd-proxy": {
66849
+ maxRequests: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_MAX", 1200),
66850
+ windowMs: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_WINDOW_SECONDS", 60) * 1e3
66851
+ }
66852
+ };
66853
+ }
66854
+ let policies = buildPoliciesFromEnv();
66855
+ const windows = /* @__PURE__ */ new Map();
66856
+ function windowKey(routeClass, clientKey) {
66857
+ return `${routeClass}\0${clientKey}`;
66858
+ }
66859
+ /**
66860
+ * Fixed-window counting: cheap, and precise enough for a loop-guard rather
66861
+ * than a security boundary. The one imprecision this trades away — up to 2x
66862
+ * the configured rate right at a window boundary — matters for an attacker
66863
+ * budgeting requests against the limiter's clock, not for the runaway
66864
+ * clients this exists to catch.
66865
+ */
66866
+ function checkHttpRateLimit(input) {
66867
+ const policy = policies[input.routeClass];
66868
+ const nowMs = Date.now();
66869
+ const key = windowKey(input.routeClass, input.clientKey);
66870
+ const existing = windows.get(key);
66871
+ const windowIsLive = existing !== void 0 && nowMs - existing.windowStartMs < policy.windowMs;
66872
+ const windowStartMs = windowIsLive ? existing.windowStartMs : nowMs;
66873
+ const count = (windowIsLive ? existing.count : 0) + 1;
66874
+ windows.set(key, {
66875
+ windowStartMs,
66876
+ count
66877
+ });
66878
+ return {
66879
+ allowed: count <= policy.maxRequests,
66880
+ retryAfterSeconds: Math.max(0, Math.ceil((windowStartMs + policy.windowMs - nowMs) / 1e3))
66881
+ };
66882
+ }
66883
+ /**
66884
+ * Checks the limiter for one request and, when the caller is over budget,
66885
+ * returns the 429 to send instead of running the route — `null` means
66886
+ * proceed. Every route this guards is expected to call this first and return
66887
+ * early on a non-null result, before doing any other work.
66888
+ */
66889
+ function rateLimitOrRespond(input) {
66890
+ const decision = checkHttpRateLimit(input);
66891
+ if (decision.allowed) return null;
66892
+ return require_HttpServer.jsonUnsafe({ error: "Too many requests. Try again later." }, {
66893
+ status: 429,
66894
+ headers: { "retry-after": String(decision.retryAfterSeconds) }
66895
+ });
66896
+ }
66897
+ /**
66898
+ * The client identity for a route with no session yet — the device-code
66899
+ * poll, bootstrap and pairing-token exchange all run before Forge has issued
66900
+ * one. IP address is the only stable-enough signal available at that point;
66901
+ * the authenticated `/api/cmmd` proxy keys on the session instead (see its
66902
+ * call site), so one desktop and one phone sharing a NAT never share a
66903
+ * budget there.
66904
+ */
66905
+ function clientKeyFromRequest(request) {
66906
+ return normalizeIpAddress(require_Schema$1.getOrNull(request.remoteAddress)) ?? "unknown";
66907
+ }
66694
66908
  const OAUTH_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
66695
66909
  /**
66696
66910
  * The one exchange this endpoint performs, or the reason it will not.
@@ -66716,6 +66930,29 @@ function accessTokenExpiresInSeconds(remainingMillis) {
66716
66930
  return Math.max(0, Math.floor(remainingMillis / 1e3));
66717
66931
  }
66718
66932
  //#endregion
66933
+ //#region src/auth/commandBootstrapClient.ts
66934
+ const BOOTSTRAP_CLIENT_LABELS = {
66935
+ desktop: "Forge Desktop Cloud",
66936
+ mobile: "Forge Mobile Cloud"
66937
+ };
66938
+ /**
66939
+ * Client metadata for a bearer bootstrap session.
66940
+ *
66941
+ * The declared device type wins over the one inferred from the user agent. A
66942
+ * React Native fetch sends whatever the platform's networking stack sends, and
66943
+ * on iOS that is a CFNetwork string no user-agent heuristic reads as a phone.
66944
+ */
66945
+ function resolveCommandBootstrapClient(input) {
66946
+ const deviceType = input.clientDeviceType ?? "desktop";
66947
+ return {
66948
+ ...deriveAuthClientMetadata({
66949
+ request: input.request,
66950
+ label: BOOTSTRAP_CLIENT_LABELS[deviceType]
66951
+ }),
66952
+ deviceType
66953
+ };
66954
+ }
66955
+ //#endregion
66719
66956
  //#region ../../packages/shared/src/cmmdIdentity.ts
66720
66957
  const MAX_CMMD_AVATAR_URL_LENGTH = 256 * 1024;
66721
66958
  const DATA_IMAGE_AVATAR_PATTERN = /^data:image\/(?:avif|gif|jpeg|jpg|png|webp);base64,[a-z0-9+/=]+$/i;
@@ -67169,14 +67406,26 @@ function extractJwtExpiresAt(jwtToken) {
67169
67406
  }
67170
67407
  }
67171
67408
  /**
67172
- * Returns true when a stored JWT is past its expiry or has no decodable expiry.
67173
- * CMMD REST JWTs are short-lived bearer tokens, so unknown expiry is not safe.
67409
+ * How long before `exp` a stored JWT is already treated as spent.
67410
+ *
67411
+ * Comparing against `exp` exactly leaves a window where Forge sends a token it
67412
+ * believes is good and CMMD has already stopped accepting it: the clocks differ by a
67413
+ * little, the request spends time in flight, and the answer comes back 401 on a
67414
+ * session that had a perfectly good refresh token sitting next to it. Two
67415
+ * minutes is far inside the JWT's own lifetime, so this costs one early
67416
+ * rotation and removes the race.
67417
+ */
67418
+ const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
67419
+ /**
67420
+ * Returns true when a stored JWT is at or near its expiry, or has no decodable
67421
+ * expiry. CMMD REST JWTs are short-lived bearer tokens, so unknown expiry is
67422
+ * not safe.
67174
67423
  */
67175
67424
  function isJwtExpiredForSession(sessionId) {
67176
67425
  if (!jwtCache.has(sessionId)) return false;
67177
67426
  const expiresAt = jwtExpiryCache.get(sessionId);
67178
67427
  if (!expiresAt) return true;
67179
- return new Date(expiresAt) <= /* @__PURE__ */ new Date();
67428
+ return new Date(expiresAt).getTime() - JWT_EXPIRY_SKEW_MS <= Date.now();
67180
67429
  }
67181
67430
  /**
67182
67431
  * Hydrate the in-memory cache from the configured persistence layer. Call once at server startup
@@ -67732,10 +67981,27 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId) => require_Schema$1.gen
67732
67981
  * Retire refresh credentials copied into historical desktop bearer sessions.
67733
67982
  * Electron owns rotation for native desktop OAuth; Forge Local keeps only the
67734
67983
  * derived access authority needed by the current bearer session.
67735
- */
67736
- const clearDesktopBearerRefreshTokensForSubject = (subject) => require_Schema$1.gen(function* () {
67737
- const normalizedSubject = subject.trim();
67738
- if (!normalizedSubject) return;
67984
+ *
67985
+ * Scoped to the bootstrapping client, not to the whole subject. The phone and
67986
+ * Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
67987
+ * desktop launch null out a phone's refresh token and end its session a day
67988
+ * later. Two clauses keep that from happening:
67989
+ *
67990
+ * - `client_device_type = 'desktop'`, which now excludes phones outright,
67991
+ * because the mobile app declares itself at bootstrap.
67992
+ * - the same recorded `client_user_agent` as the bootstrap doing the
67993
+ * retirement, which is the only thing that separates a phone row written
67994
+ * before that stamp existed from a real Electron row. Both are stamped
67995
+ * `desktop`; neither carries any other mark. An empty or unmatched user agent
67996
+ * retires nothing, which is the direction that keeps a live credential.
67997
+ *
67998
+ * Electron re-bootstraps under a stable user agent, so its own historical rows
67999
+ * still match and still get swept.
68000
+ */
68001
+ const clearDesktopBearerRefreshTokensForSubject = (input) => require_Schema$1.gen(function* () {
68002
+ const normalizedSubject = input.subject.trim();
68003
+ const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
68004
+ if (!normalizedSubject || !normalizedUserAgent) return;
67739
68005
  const rows = yield* (yield* require_SqlError.SqlClient)`
67740
68006
  UPDATE command_access_tokens
67741
68007
  SET
@@ -67748,6 +68014,7 @@ const clearDesktopBearerRefreshTokensForSubject = (subject) => require_Schema$1.
67748
68014
  AND role = 'owner'
67749
68015
  AND method = 'bearer-session-token'
67750
68016
  AND client_device_type = 'desktop'
68017
+ AND client_user_agent = ${normalizedUserAgent}
67751
68018
  )
67752
68019
  RETURNING session_id AS "sessionId"
67753
68020
  `.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
@@ -67770,6 +68037,17 @@ const OAUTH_STATE_COOKIE = "forge_oauth_state";
67770
68037
  const OAUTH_VERIFIER_COOKIE = "forge_oauth_verifier";
67771
68038
  //#endregion
67772
68039
  //#region src/auth/http/cloudOwnerOAuth.ts
68040
+ const FORGE_CLOUD_SESSION_GRANT_COOKIE = "forge_cloud_session_grant";
68041
+ function resolveCloudOwnerSessionGrantCookie(input) {
68042
+ const token = input.token?.trim();
68043
+ if (!token || !input.expiresAt) return null;
68044
+ const expiresAt = new Date(input.expiresAt);
68045
+ if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= Date.now()) return null;
68046
+ return {
68047
+ token,
68048
+ expiresAt
68049
+ };
68050
+ }
67773
68051
  function validateCloudOwnerCallbackAssertion(input) {
67774
68052
  if (input.serverAuth.isForgeCloudShell?.() !== true) return require_Schema$1.succeed(null);
67775
68053
  if (!input.assertionToken || !input.serverAuth.verifyForgeCloudOwnerAssertion) return require_Schema$1.fail(new AuthError({
@@ -67833,8 +68111,19 @@ function cloudOwnerAssertionCookieOptions(assertion) {
67833
68111
  secure: true
67834
68112
  };
67835
68113
  }
67836
- function cloudOwnerOAuthResponse(homeUrl, assertion) {
67837
- return require_HttpServer.redirect(homeUrl, { status: 302 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)), require_Schema$1.flatMap(require_HttpServer.setCookie(OAUTH_STATE_COOKIE, "", expiredCallbackCookie)), require_Schema$1.flatMap(require_HttpServer.setCookie(OAUTH_VERIFIER_COOKIE, "", expiredCallbackCookie)));
68114
+ function cloudOwnerSessionGrantCookieOptions(grant) {
68115
+ return {
68116
+ expires: grant.expiresAt,
68117
+ httpOnly: true,
68118
+ path: "/api/auth",
68119
+ sameSite: "lax",
68120
+ secure: true
68121
+ };
68122
+ }
68123
+ function cloudOwnerOAuthResponse(homeUrl, assertion, sessionGrant) {
68124
+ let response = require_HttpServer.redirect(homeUrl, { status: 302 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
68125
+ if (sessionGrant) response = response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
68126
+ return response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(OAUTH_STATE_COOKIE, "", expiredCallbackCookie)), require_Schema$1.flatMap(require_HttpServer.setCookie(OAUTH_VERIFIER_COOKIE, "", expiredCallbackCookie)));
67838
68127
  }
67839
68128
  //#endregion
67840
68129
  //#region src/auth/http.ts
@@ -67884,6 +68173,56 @@ const authSessionRouteLayer = add$1("GET", "/api/auth/session", require_Schema$1
67884
68173
  }));
67885
68174
  return response;
67886
68175
  }));
68176
+ const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", require_Schema$1.gen(function* () {
68177
+ const request = yield* require_HttpServer.HttpServerRequest;
68178
+ const serverAuth = yield* ServerAuth;
68179
+ if (serverAuth.isForgeCloudShell?.() !== true) return yield* new AuthError({
68180
+ message: "Session renewal is unavailable.",
68181
+ status: 401
68182
+ });
68183
+ const sessionGrant = request.cookies[FORGE_CLOUD_SESSION_GRANT_COOKIE]?.trim();
68184
+ if (!sessionGrant) return yield* new AuthError({
68185
+ message: "Session renewal is unavailable.",
68186
+ status: 401
68187
+ });
68188
+ const credentials = yield* resolveCmmdTokenBundleCredentials(yield* readCmmdJsonResponse(yield* require_Schema$1.tryPromise({
68189
+ try: () => fetch(resolveCommandAppEndpoint("/api/mcp-server/oauth/token"), {
68190
+ method: "POST",
68191
+ headers: { "content-type": "application/json" },
68192
+ body: JSON.stringify({
68193
+ grant_type: "forge_session_grant",
68194
+ grant_token: sessionGrant,
68195
+ client_id: "cmmd-forge-web"
68196
+ })
68197
+ }),
68198
+ catch: (cause) => new AuthError({
68199
+ message: "Failed to renew the Forge Cloud session with CMMD.",
68200
+ status: 500,
68201
+ cause
68202
+ })
68203
+ }), "Failed to renew the Forge Cloud session with CMMD."));
68204
+ const { cmmdIdentity, userId, patToStore } = credentials;
68205
+ if (!cmmdIdentity || !userId || !patToStore) return yield* new AuthError({
68206
+ message: "CMMD did not return a complete renewed Forge session.",
68207
+ status: 500
68208
+ });
68209
+ const assertion = yield* validateCloudOwnerCallbackAssertion({
68210
+ serverAuth,
68211
+ assertionToken: credentials.forgeOwnerAssertion,
68212
+ authenticatedUserId: userId
68213
+ });
68214
+ if (!assertion) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a renewed Forge Cloud owner assertion."));
68215
+ yield* persistCloudOwnerArtifacts({
68216
+ assertion,
68217
+ accessToken: patToStore,
68218
+ identity: cmmdIdentity,
68219
+ jwtToken: credentials.jwtToStore,
68220
+ refreshToken: credentials.refreshToken,
68221
+ sessionGrant,
68222
+ sessionGrantExpiresAt: void 0
68223
+ });
68224
+ return yield* require_HttpServer.jsonUnsafe({ renewed: true }, { status: 200 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
68225
+ }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
67887
68226
  const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", require_Schema$1.gen(function* () {
67888
68227
  const request = yield* require_HttpServer.HttpServerRequest;
67889
68228
  const serverAuth = yield* ServerAuth;
@@ -67906,14 +68245,24 @@ const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", require_Schema$1.
67906
68245
  sameSite: "lax",
67907
68246
  secure
67908
68247
  }));
67909
- if (cloudShell) response = yield* response.pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, "", {
67910
- expires: /* @__PURE__ */ new Date(0),
67911
- httpOnly: true,
67912
- maxAge: 0,
67913
- path: "/",
67914
- sameSite: "lax",
67915
- secure: true
67916
- }));
68248
+ if (cloudShell) {
68249
+ response = yield* response.pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, "", {
68250
+ expires: /* @__PURE__ */ new Date(0),
68251
+ httpOnly: true,
68252
+ maxAge: 0,
68253
+ path: "/",
68254
+ sameSite: "lax",
68255
+ secure: true
68256
+ }));
68257
+ response = yield* response.pipe(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, "", {
68258
+ expires: /* @__PURE__ */ new Date(0),
68259
+ httpOnly: true,
68260
+ maxAge: 0,
68261
+ path: "/api/auth",
68262
+ sameSite: "lax",
68263
+ secure: true
68264
+ }));
68265
+ }
67917
68266
  response = yield* pipeExpireLegacySessionCookie(sessions.cookieName, require_Schema$1.succeed(response), { secure });
67918
68267
  if (sessions.cookieName !== "forge_session") response = yield* response.pipe(require_HttpServer.setCookie(BASE_SESSION_COOKIE_NAME, "", {
67919
68268
  expires: /* @__PURE__ */ new Date(0),
@@ -67936,7 +68285,8 @@ const CommandBootstrapInput = require_Schema$1.Struct({
67936
68285
  refreshToken: require_Schema$1.optionalKey(require_Schema$1.String),
67937
68286
  userId: require_Schema$1.optionalKey(require_Schema$1.Number),
67938
68287
  displayName: require_Schema$1.optionalKey(require_Schema$1.String),
67939
- user: require_Schema$1.optionalKey(require_Schema$1.Unknown)
68288
+ user: require_Schema$1.optionalKey(require_Schema$1.Unknown),
68289
+ clientDeviceType: require_Schema$1.optionalKey(require_Schema$1.Literals(["desktop", "mobile"]))
67940
68290
  });
67941
68291
  const CommandOAuthDeviceInput = require_Schema$1.Struct({
67942
68292
  client_id: require_Schema$1.String,
@@ -68402,6 +68752,11 @@ function commandOAuthProxyStatus(path, status, body) {
68402
68752
  }
68403
68753
  const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Schema$1.gen(function* () {
68404
68754
  const request = yield* require_HttpServer.HttpServerRequest;
68755
+ const limited = rateLimitOrRespond({
68756
+ routeClass: "auth-bootstrap",
68757
+ clientKey: clientKeyFromRequest(request)
68758
+ });
68759
+ if (limited) return limited;
68405
68760
  const serverAuth = yield* ServerAuth;
68406
68761
  const sessions = yield* SessionCredentialService;
68407
68762
  const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
@@ -68419,6 +68774,11 @@ const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Sch
68419
68774
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68420
68775
  const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command", require_Schema$1.gen(function* () {
68421
68776
  const request = yield* require_HttpServer.HttpServerRequest;
68777
+ const limited = rateLimitOrRespond({
68778
+ routeClass: "auth-bootstrap",
68779
+ clientKey: clientKeyFromRequest(request)
68780
+ });
68781
+ if (limited) return limited;
68422
68782
  const serverAuth = yield* ServerAuth;
68423
68783
  const sessions = yield* SessionCredentialService;
68424
68784
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68467,6 +68827,11 @@ const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/comman
68467
68827
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68468
68828
  const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command/bearer", require_Schema$1.gen(function* () {
68469
68829
  const request = yield* require_HttpServer.HttpServerRequest;
68830
+ const limited = rateLimitOrRespond({
68831
+ routeClass: "auth-bootstrap",
68832
+ clientKey: clientKeyFromRequest(request)
68833
+ });
68834
+ if (limited) return limited;
68470
68835
  const serverAuth = yield* ServerAuth;
68471
68836
  const sessions = yield* SessionCredentialService;
68472
68837
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68484,13 +68849,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68484
68849
  method: "bearer-session-token",
68485
68850
  role: "owner",
68486
68851
  subject: sessionSubject,
68487
- client: {
68488
- ...deriveAuthClientMetadata({
68489
- request,
68490
- label: "Forge Desktop Cloud"
68491
- }),
68492
- deviceType: "desktop"
68493
- }
68852
+ client: resolveCommandBootstrapClient({
68853
+ request,
68854
+ clientDeviceType: payload.clientDeviceType
68855
+ })
68494
68856
  }).pipe(require_Schema$1.mapError((cause) => new AuthError({
68495
68857
  message: "Failed to issue authenticated bearer session.",
68496
68858
  cause
@@ -68500,7 +68862,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68500
68862
  payload,
68501
68863
  cmmdIdentity: validated.cmmdIdentity
68502
68864
  });
68503
- if (!payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject(sessionSubject).pipe(require_Schema$1.mapError((cause) => new AuthError({
68865
+ if ((payload.clientDeviceType ?? "desktop") === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
68866
+ subject: sessionSubject,
68867
+ clientUserAgent: request.headers["user-agent"]
68868
+ }).pipe(require_Schema$1.mapError((cause) => new AuthError({
68504
68869
  message: "Failed to retire legacy desktop refresh credentials.",
68505
68870
  status: 500,
68506
68871
  cause
@@ -68515,6 +68880,11 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68515
68880
  }, { status: 200 });
68516
68881
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68517
68882
  const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device", require_Schema$1.gen(function* () {
68883
+ const limited = rateLimitOrRespond({
68884
+ routeClass: "auth-device-poll",
68885
+ clientKey: clientKeyFromRequest(yield* require_HttpServer.HttpServerRequest)
68886
+ });
68887
+ if (limited) return limited;
68518
68888
  const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthDeviceInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
68519
68889
  message: "Invalid CMMD OAuth device payload.",
68520
68890
  status: 400,
@@ -68526,7 +68896,17 @@ const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device
68526
68896
  ...payload.scope ? { scope: payload.scope } : {}
68527
68897
  });
68528
68898
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68899
+ /**
68900
+ * The device-code poll. Called every `interval` seconds by design (RFC
68901
+ * 8628), which is exactly why FOR-36706 puts a limiter on it: a client that
68902
+ * ignores `slow_down` — a bug, not a person — has no other backstop here.
68903
+ */
68529
68904
  const authCmmdOAuthTokenRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/token", require_Schema$1.gen(function* () {
68905
+ const limited = rateLimitOrRespond({
68906
+ routeClass: "auth-device-poll",
68907
+ clientKey: clientKeyFromRequest(yield* require_HttpServer.HttpServerRequest)
68908
+ });
68909
+ if (limited) return limited;
68530
68910
  const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthTokenInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
68531
68911
  message: "Invalid CMMD OAuth token payload.",
68532
68912
  status: 400,
@@ -68568,6 +68948,14 @@ function completeCloudOwnerPasswordLogin(input) {
68568
68948
  authenticatedUserId: userId
68569
68949
  });
68570
68950
  if (!assertion) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a Forge Cloud owner assertion in cloud shell mode."));
68951
+ const sessionGrant = resolveCloudOwnerSessionGrantCookie({
68952
+ token: credentials.sessionGrant,
68953
+ expiresAt: credentials.sessionGrantExpiresAt
68954
+ });
68955
+ if (!sessionGrant) return yield* new AuthError({
68956
+ message: "CMMD did not return a durable Forge Cloud session grant.",
68957
+ status: 500
68958
+ });
68571
68959
  yield* persistCloudOwnerArtifacts({
68572
68960
  assertion,
68573
68961
  accessToken: patToStore,
@@ -68577,10 +68965,12 @@ function completeCloudOwnerPasswordLogin(input) {
68577
68965
  sessionGrant: credentials.sessionGrant,
68578
68966
  sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
68579
68967
  });
68580
- return yield* require_HttpServer.jsonUnsafe({
68968
+ let response = require_HttpServer.jsonUnsafe({
68581
68969
  authenticated: true,
68582
68970
  cmmdIdentity
68583
68971
  }, { status: 200 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
68972
+ if (sessionGrant) response = response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
68973
+ return yield* response;
68584
68974
  });
68585
68975
  }
68586
68976
  const authCmmdPasswordTokenRouteLayer = add$1("POST", "/api/auth/cmmd-password/token", require_Schema$1.gen(function* () {
@@ -68754,6 +69144,14 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
68754
69144
  assertionToken: forgeOwnerAssertion,
68755
69145
  authenticatedUserId: userId
68756
69146
  });
69147
+ const cloudSessionGrant = cloudOwnerAssertion ? resolveCloudOwnerSessionGrantCookie({
69148
+ token: sessionGrant,
69149
+ expiresAt: sessionGrantExpiresAt
69150
+ }) : null;
69151
+ if (cloudOwnerAssertion && !cloudSessionGrant) return yield* new AuthError({
69152
+ message: "CMMD did not return a durable Forge Cloud session grant.",
69153
+ status: 500
69154
+ });
68757
69155
  const sessionSubject = resolveCommandOAuthSubject(userId);
68758
69156
  const issued = cloudOwnerAssertion ? null : yield* sessions.issue({
68759
69157
  method: "browser-session-cookie",
@@ -68785,7 +69183,7 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
68785
69183
  else yield* persistLocalOAuthArtifacts(persistenceInput);
68786
69184
  const homeUrl = `${publicUrl}/`;
68787
69185
  const secureCookie = shouldUseSecureCookie(publicUrl);
68788
- if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion);
69186
+ if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion, cloudSessionGrant);
68789
69187
  if (!issued) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a local session outside Forge Cloud."));
68790
69188
  return yield* pipeExpireLegacySessionCookie(sessions.cookieName, require_HttpServer.redirect(homeUrl, { status: 302 }).pipe(require_HttpServer.setCookie(sessions.cookieName, issued.token, {
68791
69189
  expires: require_Schema$1.toDate(issued.expiresAt),
@@ -68803,6 +69201,11 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
68803
69201
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68804
69202
  const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer", require_Schema$1.gen(function* () {
68805
69203
  const request = yield* require_HttpServer.HttpServerRequest;
69204
+ const limited = rateLimitOrRespond({
69205
+ routeClass: "auth-bootstrap",
69206
+ clientKey: clientKeyFromRequest(request)
69207
+ });
69208
+ if (limited) return limited;
68806
69209
  const serverAuth = yield* ServerAuth;
68807
69210
  const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
68808
69211
  message: "Invalid bootstrap payload.",
@@ -68835,6 +69238,11 @@ const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer"
68835
69238
  */
68836
69239
  const authOAuthTokenExchangeRouteLayer = add$1("POST", "/oauth/token", require_Schema$1.gen(function* () {
68837
69240
  const request = yield* require_HttpServer.HttpServerRequest;
69241
+ const limited = rateLimitOrRespond({
69242
+ routeClass: "auth-token-exchange",
69243
+ clientKey: clientKeyFromRequest(request)
69244
+ });
69245
+ if (limited) return limited;
68838
69246
  const serverAuth = yield* ServerAuth;
68839
69247
  const payload = yield* require_HttpServer.schemaBodyUrlParams(OAuthTokenExchangeInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
68840
69248
  message: "Invalid token exchange payload.",
@@ -70829,6 +71237,11 @@ function redeemCmmdSessionGrant(grantToken) {
70829
71237
  };
70830
71238
  }).catch(() => null);
70831
71239
  }
71240
+ const REJECTED_OAUTH_ERROR_CODES = new Set([
71241
+ "invalid_grant",
71242
+ "invalid_client",
71243
+ "invalid_request"
71244
+ ]);
70832
71245
  /**
70833
71246
  * Rotate a CMMD refresh token into a fresh access JWT.
70834
71247
  *
@@ -70841,7 +71254,7 @@ function redeemCmmdSessionGrant(grantToken) {
70841
71254
  * code are what identify the failure, and `invalid_grant` in particular means
70842
71255
  * the token was already spent, which is a different fix from a 500.
70843
71256
  */
70844
- function refreshCmmdJwt(refreshToken) {
71257
+ function refreshCmmdJwt(refreshToken, observer) {
70845
71258
  return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
70846
71259
  method: "POST",
70847
71260
  headers: { "Content-Type": "application/json" },
@@ -70852,7 +71265,9 @@ function refreshCmmdJwt(refreshToken) {
70852
71265
  })
70853
71266
  }).then(async (response) => {
70854
71267
  if (!response.ok) {
70855
- console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${await readOAuthError(response)}`);
71268
+ const errorCode = await readOAuthErrorCode(response);
71269
+ if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
71270
+ console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
70856
71271
  return null;
70857
71272
  }
70858
71273
  const parsed = parseCmmdOAuthRefreshResponse(await response.json());
@@ -70864,20 +71279,73 @@ function refreshCmmdJwt(refreshToken) {
70864
71279
  });
70865
71280
  }
70866
71281
  /**
70867
- * The OAuth `error` code from a refusal, as a loggable suffix.
71282
+ * The OAuth `error` code from a refusal, lowercased, or null when there is none.
70868
71283
  *
70869
71284
  * Best effort by design: a body that is not JSON, or is already consumed, must
70870
- * not turn a logged failure into an unlogged one.
71285
+ * not turn a logged failure into an unlogged one. Null also reads correctly as
71286
+ * "no evidence the credential is dead", which is the safe answer for a caller
71287
+ * deciding whether to keep a refresh token.
70871
71288
  */
70872
- async function readOAuthError(response) {
71289
+ async function readOAuthErrorCode(response) {
70873
71290
  try {
70874
71291
  const code = (await response.json())["error"];
70875
- return typeof code === "string" && code.trim().length > 0 ? ` (${code.trim()})` : "";
71292
+ return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
70876
71293
  } catch {
70877
- return "";
71294
+ return null;
70878
71295
  }
70879
71296
  }
70880
71297
  //#endregion
71298
+ //#region src/command/cmmdRotationFence.ts
71299
+ /**
71300
+ * True when Forge must not rotate this session's CMMD credentials.
71301
+ *
71302
+ * Fails closed: if the session row cannot be read, assume the fence applies.
71303
+ * Spending a token Forge had no right to spend is worse than one extra
71304
+ * reconnect.
71305
+ */
71306
+ function isRotationFencedSession(sessionId) {
71307
+ if (readCommandRefreshTokenForSession(sessionId)) return require_Schema$1.succeed(false);
71308
+ return isDesktopBearerAuthSession(sessionId).pipe(require_Schema$1.orElseSucceed(() => true));
71309
+ }
71310
+ //#endregion
71311
+ //#region src/cmmdConnection/cmmdCredentialRejection.ts
71312
+ /**
71313
+ * How long a recorded rejection stays usable.
71314
+ *
71315
+ * Long enough to span the resolver ladder and the response that follows it,
71316
+ * short enough that it cannot outlive the credential it describes.
71317
+ */
71318
+ const REJECTION_TTL_MS = 6e4;
71319
+ const rejectedAt = /* @__PURE__ */ new Map();
71320
+ /** Record that CMMD refused this session's credential outright. */
71321
+ function noteCmmdCredentialRejected(sessionId) {
71322
+ rejectedAt.set(sessionId, Date.now());
71323
+ }
71324
+ /** Whether CMMD refused this session's credential recently enough to act on. */
71325
+ function hasCmmdCredentialRejection(sessionId) {
71326
+ const observedAt = rejectedAt.get(sessionId);
71327
+ if (observedAt === void 0) return false;
71328
+ if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
71329
+ rejectedAt.delete(sessionId);
71330
+ return false;
71331
+ }
71332
+ /** Drop a recorded rejection, once it has been acted on or superseded. */
71333
+ function forgetCmmdCredentialRejection(sessionId) {
71334
+ rejectedAt.delete(sessionId);
71335
+ }
71336
+ /**
71337
+ * Clear a session's CMMD credentials, but only if CMMD rejected them.
71338
+ *
71339
+ * This is the only door the 401 path may use. A resolver that returned null
71340
+ * because it was not allowed to try is a policy refusal and leaves the stored
71341
+ * credential exactly where it is, so the next request can rotate it.
71342
+ */
71343
+ const clearCmmdAuthAfterCmmdRejection = (sessionId) => require_Schema$1.gen(function* () {
71344
+ if (!hasCmmdCredentialRejection(sessionId)) return;
71345
+ yield* clearUnrecoverableCmmdAuthForSession(sessionId);
71346
+ forgetCmmdCredentialRejection(sessionId);
71347
+ });
71348
+ //#endregion
70881
71349
  //#region src/command/cmmdJwtResolver.ts
70882
71350
  function usableCmmdUserJwt(token) {
70883
71351
  return isCmmdUserAccessJwt(token) ? token : null;
@@ -70890,7 +71358,7 @@ function refreshOnceForSession(sessionId, refreshToken) {
70890
71358
  return require_Schema$1.promise(() => {
70891
71359
  const existing = inFlightRefreshes.get(sessionId);
70892
71360
  if (existing) return existing;
70893
- const pending = refreshCmmdJwt(refreshToken).catch(() => null).finally(() => {
71361
+ const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
70894
71362
  inFlightRefreshes.delete(sessionId);
70895
71363
  });
70896
71364
  inFlightRefreshes.set(sessionId, pending);
@@ -71000,13 +71468,16 @@ function trySubjectFallback(sessionId, subject, logPrefix) {
71000
71468
  * Fail-closed: never returns a non-user / malformed Bearer. Callers must map
71001
71469
  * null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
71002
71470
  */
71003
- function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false }) {
71471
+ function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
71004
71472
  const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
71005
- if (!forceRefresh && cached && !isJwtExpiredForSession(sessionId)) return require_Schema$1.succeed(cached);
71473
+ if (cached && !isJwtExpiredForSession(sessionId)) {
71474
+ if (!forceRefresh) return require_Schema$1.succeed(cached);
71475
+ if (staleAccessToken && cached !== staleAccessToken) return require_Schema$1.succeed(cached);
71476
+ }
71006
71477
  const normalizedSubject = subject?.trim() || null;
71007
71478
  if (!normalizedSubject && !forceRefresh) return require_Schema$1.succeed(null);
71008
71479
  return require_Schema$1.gen(function* () {
71009
- if (yield* isDesktopBearerAuthSession(sessionId).pipe(require_Schema$1.orElseSucceed(() => true))) return null;
71480
+ if (yield* isRotationFencedSession(sessionId)) return null;
71010
71481
  if (forceRefresh) {
71011
71482
  const forced = yield* tryOwnCredentialRecovery(sessionId, logPrefix);
71012
71483
  if (forced) return forced;
@@ -71765,15 +72236,25 @@ function clearCmmdAuthAfterFailure(sessionId) {
71765
72236
  })));
71766
72237
  }
71767
72238
  /**
71768
- * The single "no usable CMMD credential" response: clears whatever
71769
- * unrecoverable auth state this session is holding, then returns the
71770
- * canonical `CMMD_AUTH_REQUIRED` body. Every prologue path that decides the
71771
- * session has no way to reach CMMD — a missing PAT, a JWT that fails to
71772
- * resolve, a bespoke gate elsewhere in http.ts — emits this same response so
71773
- * the Shell only ever has one `CMMD_AUTH_REQUIRED` body to branch on.
72239
+ * The single "no usable CMMD credential" response: returns the canonical
72240
+ * `CMMD_AUTH_REQUIRED` body. Every prologue path that decides the session has
72241
+ * no way to reach CMMD — a missing PAT, a JWT that fails to resolve, a bespoke
72242
+ * gate elsewhere in http.ts — emits this same response so the Shell only ever
72243
+ * has one `CMMD_AUTH_REQUIRED` body to branch on.
72244
+ *
72245
+ * It used to delete the session's stored credentials on the way out, and that
72246
+ * was the wrong reading of its own argument. Most of the paths that reach here
72247
+ * are refusals, not rejections: the resolver was fenced, the subject was
72248
+ * missing, a gate said no. A phone would arrive holding a refresh token good
72249
+ * for the next three months, be refused by a rule, and have that token deleted
72250
+ * as "unrecoverable". The clear now happens only where CMMD has actually
72251
+ * refused the credential, which `clearCmmdAuthAfterCmmdRejection` checks.
71774
72252
  */
71775
72253
  function respondCmmdConnectionRequired(sessionId) {
71776
- return clearCmmdAuthAfterFailure(sessionId).pipe(require_Schema$1.as(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
72254
+ return clearCmmdAuthAfterCmmdRejection(sessionId).pipe(require_Schema$1.catch_((error) => require_Schema$1.logWarning("Failed to clear CMMD auth that CMMD itself rejected.", {
72255
+ cause: error,
72256
+ sessionId
72257
+ })), require_Schema$1.as(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
71777
72258
  }
71778
72259
  async function dispatchToProxy(method, jwt, cmmdPath, rawBody, orgId, searchParams) {
71779
72260
  return cmmdGateway.request({
@@ -71829,6 +72310,11 @@ function buildProxyRouteLayer(route) {
71829
72310
  const handler = require_Schema$1.gen(function* () {
71830
72311
  const req = yield* require_HttpServer.HttpServerRequest;
71831
72312
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
72313
+ const limited = rateLimitOrRespond({
72314
+ routeClass: "cmmd-proxy",
72315
+ clientKey: session.sessionId
72316
+ });
72317
+ if (limited) return limited;
71832
72318
  const pathParams = yield* params;
71833
72319
  const cmmdPath = substitutePath(route.cmmdPath, pathParams);
71834
72320
  if (!cmmdPath) return require_HttpServer.jsonUnsafe({ error: "Missing required path parameter." }, { status: 400 });
@@ -71889,6 +72375,11 @@ function buildProxyHandlerEffect(route) {
71889
72375
  return require_Schema$1.gen(function* () {
71890
72376
  const req = yield* require_HttpServer.HttpServerRequest;
71891
72377
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
72378
+ const limited = rateLimitOrRespond({
72379
+ routeClass: "cmmd-proxy",
72380
+ clientKey: session.sessionId
72381
+ });
72382
+ if (limited) return limited;
71892
72383
  if (!readCommandAccessTokenForSession(session.sessionId)) return yield* respondCmmdConnectionRequired(session.sessionId);
71893
72384
  const parsed = yield* route.parse(req);
71894
72385
  if (!parsed.ok) return require_HttpServer.jsonUnsafe(parsed.body, { status: parsed.status });
@@ -83674,12 +84165,12 @@ const makePullRequestService = require_Schema$1.gen(function* () {
83674
84165
  }));
83675
84166
  const submitReview = (input) => requireProject(input).pipe(require_Schema$1.flatMap((project) => {
83676
84167
  const review = project.api.capabilities.review;
83677
- if (!review.verdicts.includes(input.verdict)) return refuse(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
83678
- if (input.comments.length > 0 && !review.inlineComment) return refuse("This host cannot comment on a line of a change request.");
83679
- if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse("A review needs a summary or at least one comment.");
84168
+ if (!review.verdicts.includes(input.verdict)) return refuse$1(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
84169
+ if (input.comments.length > 0 && !review.inlineComment) return refuse$1("This host cannot comment on a line of a change request.");
84170
+ if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse$1("A review needs a summary or at least one comment.");
83680
84171
  return viewerPermissionsOf(project, input, "submitReview").pipe(require_Schema$1.flatMap((viewer) => {
83681
- if (!viewer.verdicts.includes(input.verdict)) return refuse(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
83682
- if (input.comments.length > 0 && !viewer.comment) return refuse("You need write access on this repository to comment on a line of a change request.");
84172
+ if (!viewer.verdicts.includes(input.verdict)) return refuse$1(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
84173
+ if (input.comments.length > 0 && !viewer.comment) return refuse$1("You need write access on this repository to comment on a line of a change request.");
83683
84174
  return project.api.submitReview({
83684
84175
  projectId: project.project.id,
83685
84176
  cwd: project.project.workspaceRoot,
@@ -84077,7 +84568,7 @@ const toEntry = (input) => {
84077
84568
  ...input.item.reviewDecision === void 0 || input.item.reviewDecision === null ? {} : { reviewDecision: input.item.reviewDecision }
84078
84569
  };
84079
84570
  };
84080
- const refuse = (detail) => require_Schema$1.fail(new PullRequestOperationError({
84571
+ const refuse$1 = (detail) => require_Schema$1.fail(new PullRequestOperationError({
84081
84572
  operation: "submitReview",
84082
84573
  detail
84083
84574
  }));
@@ -93944,6 +94435,166 @@ const admitNormalizedDispatchCommand = (command, admit) => require_Schema$1.unin
93944
94435
  };
93945
94436
  }));
93946
94437
  //#endregion
94438
+ //#region src/orchestration/threadDetailWindow.ts
94439
+ function earliest(current, candidate) {
94440
+ return current === void 0 || candidate < current ? candidate : current;
94441
+ }
94442
+ /**
94443
+ * Every turn in the thread, oldest first.
94444
+ *
94445
+ * Ordering is by the earliest timestamp across all four windowed collections
94446
+ * rather than by message order alone: a turn can open with an activity (a tool
94447
+ * call before any text) and a checkpoint-only turn has no message at all.
94448
+ * Ties break on turn id so the order is total and a cursor means the same thing
94449
+ * on every read.
94450
+ */
94451
+ function orderThreadTurns(thread) {
94452
+ const anchors = /* @__PURE__ */ new Map();
94453
+ const touch = (turnId, at, isUserMessage) => {
94454
+ if (turnId === null) return;
94455
+ const existing = anchors.get(turnId);
94456
+ anchors.set(turnId, {
94457
+ at: earliest(existing?.at, at),
94458
+ hasUserMessage: (existing?.hasUserMessage ?? false) || isUserMessage
94459
+ });
94460
+ };
94461
+ for (const message of thread.messages) touch(message.turnId, message.createdAt, message.role === "user");
94462
+ for (const activity of thread.activities) touch(activity.turnId, activity.createdAt, false);
94463
+ for (const plan of thread.proposedPlans) touch(plan.turnId, plan.createdAt, false);
94464
+ for (const checkpoint of thread.checkpoints) touch(checkpoint.turnId, checkpoint.completedAt, false);
94465
+ return Array.from(anchors, ([turnId, anchor]) => ({
94466
+ turnId,
94467
+ ...anchor
94468
+ })).toSorted((left, right) => left.at === right.at ? left.turnId.localeCompare(right.turnId) : left.at < right.at ? -1 : 1);
94469
+ }
94470
+ /**
94471
+ * Index of the first turn to include, counting back `turnLimit` user-anchored
94472
+ * turns from the end of `ordered`. Returns 0 when the thread holds fewer user
94473
+ * turns than the limit, which is how a short thread comes back whole.
94474
+ */
94475
+ function windowStartIndex(ordered, turnLimit) {
94476
+ let userTurns = 0;
94477
+ for (let index = ordered.length - 1; index >= 0; index -= 1) {
94478
+ const anchor = ordered[index];
94479
+ if (anchor !== void 0 && anchor.hasUserMessage) {
94480
+ userTurns += 1;
94481
+ if (userTurns === turnLimit) return index;
94482
+ }
94483
+ }
94484
+ return 0;
94485
+ }
94486
+ /**
94487
+ * The turns a windowed read returns.
94488
+ *
94489
+ * `beforeCursor` bounds the read to turns strictly older than a cursor an
94490
+ * earlier read handed out, which is what "load earlier" sends back. A cursor
94491
+ * naming a turn this thread no longer holds, after a revert, say, yields an
94492
+ * empty page rather than the whole thread: returning everything would be a
94493
+ * silent full download at the exact moment the client asked for less.
94494
+ */
94495
+ function selectWindowTurnIds(ordered, window) {
94496
+ const cursorIndex = window.beforeCursor === void 0 ? ordered.length : ordered.findIndex((anchor) => anchor.turnId === window.beforeCursor);
94497
+ if (cursorIndex < 0) return {
94498
+ turnIds: /* @__PURE__ */ new Set(),
94499
+ beforeCursor: null,
94500
+ hasMore: false,
94501
+ includeUnanchored: false
94502
+ };
94503
+ const candidates = ordered.slice(0, cursorIndex);
94504
+ const startIndex = window.turnLimit === void 0 ? 0 : windowStartIndex(candidates, window.turnLimit);
94505
+ const included = candidates.slice(startIndex);
94506
+ const oldest = included[0];
94507
+ return {
94508
+ turnIds: new Set(included.map((anchor) => anchor.turnId)),
94509
+ beforeCursor: oldest?.turnId ?? null,
94510
+ hasMore: startIndex > 0,
94511
+ includeUnanchored: startIndex === 0
94512
+ };
94513
+ }
94514
+ function keepsTurn(selection, turnId) {
94515
+ return turnId === null ? selection.includeUnanchored : selection.turnIds.has(turnId);
94516
+ }
94517
+ /**
94518
+ * Applies a turn window to a full thread detail read.
94519
+ *
94520
+ * A request with neither field is a full read and comes back untouched with no
94521
+ * page, which is exactly what a pre-pagination client already receives. Only
94522
+ * the four turn-scoped collections are filtered; thread metadata, the session,
94523
+ * and `latestTurn` describe the thread as a whole and stay as they are, so a
94524
+ * windowed snapshot still renders a correct header and a correct running state.
94525
+ */
94526
+ function windowThreadDetail(input) {
94527
+ const { thread, window, snapshotSequence } = input;
94528
+ if (window.turnLimit === void 0 && window.beforeCursor === void 0) return {
94529
+ thread,
94530
+ page: void 0
94531
+ };
94532
+ const selection = selectWindowTurnIds(orderThreadTurns(thread), window);
94533
+ return {
94534
+ thread: {
94535
+ ...thread,
94536
+ messages: thread.messages.filter((row) => keepsTurn(selection, row.turnId)),
94537
+ activities: thread.activities.filter((row) => keepsTurn(selection, row.turnId)),
94538
+ proposedPlans: thread.proposedPlans.filter((row) => keepsTurn(selection, row.turnId)),
94539
+ checkpoints: thread.checkpoints.filter((row) => keepsTurn(selection, row.turnId)),
94540
+ ...thread.turnModelSelections === void 0 ? {} : { turnModelSelections: thread.turnModelSelections.filter((row) => keepsTurn(selection, row.turnId)) }
94541
+ },
94542
+ page: {
94543
+ beforeCursor: selection.hasMore ? selection.beforeCursor : null,
94544
+ hasMore: selection.hasMore,
94545
+ snapshotSequence
94546
+ }
94547
+ };
94548
+ }
94549
+ /** Whether a subscriber's cursor can be served by replay rather than a snapshot. */
94550
+ function canResumeFromSequence(input) {
94551
+ if (input.afterSequence > input.currentSequence) return false;
94552
+ return input.currentSequence - input.afterSequence <= (input.maxGap ?? 2e3);
94553
+ }
94554
+ function snapshotCatchUp(snapshot) {
94555
+ return {
94556
+ items: [{
94557
+ kind: "snapshot",
94558
+ snapshot
94559
+ }],
94560
+ watermark: snapshot.snapshotSequence
94561
+ };
94562
+ }
94563
+ function replayCatchUp(events, afterSequence) {
94564
+ return {
94565
+ items: events.map((event) => ({
94566
+ kind: "event",
94567
+ event
94568
+ })),
94569
+ watermark: events.at(-1)?.sequence ?? afterSequence
94570
+ };
94571
+ }
94572
+ /**
94573
+ * Builds the frames that precede live events: either the replayed catch-up or
94574
+ * the snapshot, plus the completion marker when one was asked for.
94575
+ */
94576
+ const buildCatchUp = require_Schema$1.fn("threadSubscription.buildCatchUp")(function* (input, sources) {
94577
+ const afterSequence = input.afterSequence;
94578
+ if (afterSequence === void 0) return snapshotCatchUp(yield* sources.loadSnapshot);
94579
+ if (!canResumeFromSequence({
94580
+ afterSequence,
94581
+ currentSequence: yield* sources.currentSequence
94582
+ })) return snapshotCatchUp(yield* sources.loadSnapshot);
94583
+ const replayed = yield* require_Path.runCollect(sources.replayEvents(afterSequence));
94584
+ return replayCatchUp(Array.from(replayed), afterSequence);
94585
+ });
94586
+ function makeThreadSubscriptionStream(input, sources) {
94587
+ return require_Path.unwrap(require_Schema$1.gen(function* () {
94588
+ const liveEvents = yield* sources.subscribeLiveEvents;
94589
+ const catchUp = yield* buildCatchUp(input, sources);
94590
+ const marker = input.requestCompletionMarker === true ? [{ kind: "synchronized" }] : [];
94591
+ return require_Path.concat(require_Path.fromArray([...catchUp.items, ...marker]), liveEvents.pipe(require_Path.filter((event) => event.sequence > catchUp.watermark), require_Path.map((event) => ({
94592
+ kind: "event",
94593
+ event
94594
+ }))));
94595
+ }));
94596
+ }
94597
+ //#endregion
93947
94598
  //#region src/orchestration/bootstrapCommandCoordinator.ts
93948
94599
  const MAX_COMPLETED_COMMANDS_PER_ENGINE = 256;
93949
94600
  const commandsByEngine = /* @__PURE__ */ new WeakMap();
@@ -97628,6 +98279,26 @@ function serializeNativeToolActivity(activity) {
97628
98279
  }
97629
98280
  };
97630
98281
  }
98282
+ function serializeWorktreeLaunchActivity(activity) {
98283
+ if (decodeWorktreeLaunchActivityKind(activity.kind) === null) return null;
98284
+ const summary = text(activity.summary, 4e3);
98285
+ if (summary === null) return null;
98286
+ return {
98287
+ sourceSequence: null,
98288
+ event: {
98289
+ eventId: `activity:${activity.id}`,
98290
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
98291
+ ...eventTurn(activity.turnId),
98292
+ kind: "tool_call",
98293
+ summary,
98294
+ status: "completed",
98295
+ payload: encodeCmmdRuntimeWorktreeLaunchActivityPayload(activity.kind, activity.payload)
98296
+ }
98297
+ };
98298
+ }
98299
+ function decodeWorktreeLaunchActivityKind(kind) {
98300
+ return isCmmdRuntimeWorktreeLaunchActivityKind(kind) ? kind : null;
98301
+ }
97631
98302
  function isReplicatedFailureActivity(activity) {
97632
98303
  return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
97633
98304
  }
@@ -98008,12 +98679,17 @@ function serializeRuntimeThreadEvents(thread) {
98008
98679
  const event = serializeNativeToolActivity(activity);
98009
98680
  return event === null ? [] : [event];
98010
98681
  });
98682
+ const worktreeLaunchActivities = thread.activities.flatMap((activity) => {
98683
+ const event = serializeWorktreeLaunchActivity(activity);
98684
+ return event === null ? [] : [event];
98685
+ });
98011
98686
  const recordedTurns = serializeRecordedTurns(thread);
98012
98687
  const textGenerations = serializeTextGenerations(thread);
98013
98688
  return [
98014
98689
  ...messages,
98015
98690
  ...replicaActivities,
98016
98691
  ...toolActivities,
98692
+ ...worktreeLaunchActivities,
98017
98693
  ...failureActivities,
98018
98694
  ...recordedTurns,
98019
98695
  ...textGenerations
@@ -102295,7 +102971,7 @@ function claudeReadinessReasonCode(reason) {
102295
102971
  //#endregion
102296
102972
  //#region src/egress/providerDeliveryReceipt.ts
102297
102973
  const PROVIDER_ACCOUNT_IDENTITY$2 = /^pai_v1_[A-Za-z0-9_-]{43}$/;
102298
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
102974
+ const UUID$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
102299
102975
  const RFC3339$1 = /^(\d{4})-(\d{2})-(\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
102300
102976
  const RECEIPT_KEYS = new Set([
102301
102977
  "receiptId",
@@ -102378,7 +103054,7 @@ function claudeAccountFields(value) {
102378
103054
  }
102379
103055
  function parseClaudeDeliveryIdentity(value) {
102380
103056
  const account = claudeAccountFields(value.providerAccountIdentity);
102381
- if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
103057
+ if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID$1.test(value.credentialGenerationId)) return null;
102382
103058
  return {
102383
103059
  kind: "credential_generation",
102384
103060
  provider: "claude",
@@ -103145,7 +103821,10 @@ function extractClaudeProviderAccountId(response) {
103145
103821
  if (typeof response !== "object" || response === null || !("data" in response)) return null;
103146
103822
  const data = response.data;
103147
103823
  if (typeof data !== "object" || data === null) return null;
103148
- if ("connected" in data && data.connected === true) return extractClaudeCredentialGenerationId(response);
103824
+ if ("connected" in data && data.connected === true) {
103825
+ if ("providerAccountId" in data) return asNonEmptyString$1(data.providerAccountId) ?? null;
103826
+ return extractClaudeCredentialGenerationId(response);
103827
+ }
103149
103828
  if (!("account" in data)) return null;
103150
103829
  const account = data.account;
103151
103830
  if (typeof account !== "object" || account === null || !("providerAccountId" in account) || typeof account.providerAccountId !== "string") return null;
@@ -106121,6 +106800,14 @@ function makeRelayCommandInboxRepository(sql) {
106121
106800
  function isThreadDetailEvent(event) {
106122
106801
  return event.type === "thread.message-sent" || event.type === "thread.proposed-plan-upserted" || event.type === "thread.activity-appended" || event.type === "thread.turn-diff-completed" || event.type === "thread.reverted" || event.type === "thread.session-set" || event.type === "thread.rate-limit-set";
106123
106802
  }
106803
+ /**
106804
+ * Narrows the domain-event feed to one thread's stream-visible events. The same
106805
+ * predicate guards the live feed and the resume replay, so a subscriber cannot
106806
+ * be handed a different set of events depending on which path served it.
106807
+ */
106808
+ function isThreadDetailEventFor(threadId) {
106809
+ return (event) => event.aggregateKind === "thread" && event.aggregateId === threadId && isThreadDetailEvent(event);
106810
+ }
106124
106811
  const PROVIDER_STATUS_DEBOUNCE_MS = 200;
106125
106812
  function streamBoundedText(contents) {
106126
106813
  if (Buffer.byteLength(contents, "utf8") > 16777216) return require_Schema$1.fail(new BulkTransferError({
@@ -106166,6 +106853,39 @@ function loadInitialShellSnapshot(input) {
106166
106853
  return yield* input.projectionSnapshotQuery.getShellSnapshot();
106167
106854
  });
106168
106855
  }
106856
+ /**
106857
+ * The thread snapshot a subscription falls back to, windowed when the
106858
+ * subscriber asked for a window. A missing thread is a failure rather than an
106859
+ * empty snapshot: the subscriber asked about a thread this server does not
106860
+ * have, and saying so is the only honest answer.
106861
+ *
106862
+ * Also the read behind `getThreadDetailPage`, the socket RPC a relay-only
106863
+ * connection uses for "load earlier turns" in place of the HTTP snapshot
106864
+ * route: same window, same pure `windowThreadDetail`, so the two reads cannot
106865
+ * disagree about what a page is.
106866
+ */
106867
+ function loadThreadDetailSnapshot(input) {
106868
+ return require_Schema$1.gen(function* () {
106869
+ const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([input.projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
106870
+ message: `Failed to load thread ${input.threadId}`,
106871
+ cause
106872
+ }))), input.orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
106873
+ if (require_Schema$1.isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106874
+ message: `Thread ${input.threadId} was not found`,
106875
+ cause: input.threadId
106876
+ });
106877
+ const windowed = windowThreadDetail({
106878
+ thread: threadDetail.value,
106879
+ window: input.window,
106880
+ snapshotSequence
106881
+ });
106882
+ return {
106883
+ snapshotSequence,
106884
+ thread: windowed.thread,
106885
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
106886
+ };
106887
+ });
106888
+ }
106169
106889
  function makeSnapshotThenLiveStream(input) {
106170
106890
  return require_Path.unwrap(require_Schema$1.gen(function* () {
106171
106891
  const liveEvents = yield* input.subscribeLiveEvents;
@@ -106413,6 +107133,9 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106413
107133
  shellRevealInFileManager: true,
106414
107134
  shellRevealInFileManagerKind: fileManagerRevealKind
106415
107135
  },
107136
+ threadResumeCompletionMarker: true,
107137
+ threadSnapshotPagination: true,
107138
+ threadDetailPageRpc: true,
106416
107139
  observability: {
106417
107140
  logsDirectoryPath: config.logsDir,
106418
107141
  localTracingEnabled: true,
@@ -106510,26 +107233,28 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106510
107233
  projectionSnapshotQuery
106511
107234
  })), require_Path.flatMap((event) => require_Schema$1.isSome(event) ? require_Path.succeed(event.value) : require_Path.empty))))
106512
107235
  })), { "rpc.aggregate": "orchestration" }),
106513
- [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, require_Schema$1.gen(function* () {
106514
- const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
106515
- message: `Failed to load thread ${input.threadId}`,
107236
+ [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, require_Schema$1.succeed(makeThreadSubscriptionStream(input, {
107237
+ subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(require_Schema$1.map((events) => events.pipe(require_Path.filter(isThreadDetailEventFor(input.threadId))))),
107238
+ currentSequence: orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence)),
107239
+ loadSnapshot: loadThreadDetailSnapshot({
107240
+ threadId: input.threadId,
107241
+ window: input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
107242
+ projectionSnapshotQuery,
107243
+ orchestrationEngine
107244
+ }),
107245
+ replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(require_Path.filter(isThreadDetailEventFor(input.threadId)), require_Path.mapError((cause) => new OrchestrationGetSnapshotError({
107246
+ message: `Failed to replay thread ${input.threadId}`,
106516
107247
  cause
106517
- }))), orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
106518
- if (require_Schema$1.isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106519
- message: `Thread ${input.threadId} was not found`,
106520
- cause: input.threadId
106521
- });
106522
- const liveStream = orchestrationEngine.streamDomainEvents.pipe(require_Path.filter((event) => event.aggregateKind === "thread" && event.aggregateId === input.threadId && isThreadDetailEvent(event)), require_Path.map((event) => ({
106523
- kind: "event",
106524
- event
106525
- })));
106526
- return require_Path.concat(require_Path.make$1({
106527
- kind: "snapshot",
106528
- snapshot: {
106529
- snapshotSequence,
106530
- thread: threadDetail.value
106531
- }
106532
- }), liveStream);
107248
+ })))
107249
+ })), { "rpc.aggregate": "orchestration" }),
107250
+ [ORCHESTRATION_WS_METHODS.getThreadDetailPage]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.getThreadDetailPage, loadThreadDetailSnapshot({
107251
+ threadId: input.threadId,
107252
+ window: {
107253
+ ...input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
107254
+ ...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor }
107255
+ },
107256
+ projectionSnapshotQuery,
107257
+ orchestrationEngine
106533
107258
  }), { "rpc.aggregate": "orchestration" }),
106534
107259
  [WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
106535
107260
  [WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(require_Schema$1.map((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
@@ -118006,8 +118731,8 @@ require_Schema$1.effectDiscard(runMigrations());
118006
118731
  //#endregion
118007
118732
  //#region src/persistence/Layers/Sqlite.ts
118008
118733
  const defaultSqliteClientLoaders = {
118009
- bun: () => Promise.resolve().then(() => require("./SqliteClient-bo7iui1t.cjs")),
118010
- node: () => Promise.resolve().then(() => require("./NodeSqliteClient-CyJvo9y9.cjs"))
118734
+ bun: () => Promise.resolve().then(() => require("./SqliteClient-O1ABIngw.cjs")),
118735
+ node: () => Promise.resolve().then(() => require("./NodeSqliteClient-CfsdC3AB.cjs"))
118011
118736
  };
118012
118737
  const makeRuntimeSqliteLayer = require_Schema$1.fn("makeRuntimeSqliteLayer")(function* (config) {
118013
118738
  const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
@@ -178324,7 +179049,7 @@ function em(e) {
178324
179049
  BO ??= new Int32Array(new SharedArrayBuffer(4)), Atomics.wait(BO, 0, 0, SY);
178325
179050
  }
178326
179051
  }
178327
- function rm$6(e, t) {
179052
+ function rm$7(e, t) {
178328
179053
  let n = (0, path.resolve)(t).toLowerCase(), r = (0, path.dirname)((0, path.resolve)(e)).toLowerCase();
178329
179054
  if (r === n || KO(r, n) || qO(r, n, "lexical")) return !0;
178330
179055
  let o = em(t)?.toLowerCase();
@@ -191174,7 +191899,7 @@ function Wo() {
191174
191899
  }
191175
191900
  function sle(e) {
191176
191901
  let t = process.cwd();
191177
- return e.filter((n) => !rm$6(n, t));
191902
+ return e.filter((n) => !rm$7(n, t));
191178
191903
  }
191179
191904
  function ZU(e) {
191180
191905
  let t = Bun.which(e);
@@ -212600,6 +213325,8 @@ const PUBLIC_PROVIDER_LEAK_PATTERNS = [
212600
213325
  /\bCURSOR_API_KEY\b/,
212601
213326
  /\bOPENCODE_API_KEY\b/,
212602
213327
  /\bforge_proxy_[A-Za-z0-9._-]+\b/,
213328
+ /\bProviderValidationError\b/,
213329
+ /\/forge-server\/node_modules\//,
212603
213330
  /https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
212604
213331
  /https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
212605
213332
  ];
@@ -212609,6 +213336,12 @@ function isProviderRateOrQuotaError(message) {
212609
213336
  const normalized = message.toLowerCase();
212610
213337
  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);
212611
213338
  }
213339
+ function providerLifecycleOrAccountCopy(message) {
213340
+ 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.";
213341
+ 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.";
213342
+ 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.";
213343
+ return null;
213344
+ }
212612
213345
  /**
212613
213346
  * Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
212614
213347
  *
@@ -212621,7 +213354,8 @@ function isProviderRateOrQuotaError(message) {
212621
213354
  function categorizePublicProviderError(message) {
212622
213355
  if (!message) return null;
212623
213356
  const m = message.toLowerCase();
212624
- 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.";
213357
+ const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
213358
+ if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
212625
213359
  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.";
212626
213360
  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.";
212627
213361
  if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
@@ -212687,60 +213421,14 @@ function hasConfiguredApexCredential(profiles) {
212687
213421
  }
212688
213422
  //#endregion
212689
213423
  //#region src/provider/claudeCredentialDirectory.ts
212690
- /**
212691
- * Per-account credential directories for Claude turns.
212692
- *
212693
- * Claude Code keeps conversation transcripts under
212694
- * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
212695
- * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
212696
- * credential file, so each Provider Account can hold its own login while every
212697
- * session file stays in one place and `--resume` keeps finding its target. See
212698
- * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
212699
- *
212700
- * That variable is not documented. Under
212701
- * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
212702
- * only allowed because absence degrades to a named behaviour that already
212703
- * works: a thread with no account runs on the Environment's own login. This
212704
- * module sets the variable only when the resolved directory holds a credential
212705
- * the CLI can use, because a missing file, or one whose access token a refusal
212706
- * has blanked, makes the CLI report logged out and a live turn would fail.
212707
- *
212708
- * Nothing writes these directories yet. #3049 adds the writer and keys each
212709
- * directory on the account identity CMMD writes on the delivery receipt it
212710
- * signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
212711
- * every runtime takes the `no-account` skip.
212712
- *
212713
- * Free of Effect services on purpose, for the same reason as
212714
- * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
212715
- * services, the browser manager and the server config, so a unit test that
212716
- * imports it cannot run in a worktree. This rule decides which account pays for
212717
- * a turn, so it has to stay directly testable.
212718
- */
213424
+ /** Per-account credentials share the existing Claude transcript directory. */
212719
213425
  /** Repoints `.credentials.json` without moving the transcripts. */
212720
213426
  const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
212721
213427
  /** The one file Claude Code writes into the credential directory. */
212722
213428
  const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
212723
- /**
212724
- * Runtime env var naming the Provider Account this runtime's Claude turns
212725
- * belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
212726
- * and each keeps its one login.
212727
- *
212728
- * It is a bridge that will not get a writer. #3073 settled the key in
212729
- * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
212730
- * comes from the identity CMMD issues and writes on the delivery receipt it
212731
- * signs, and this variable goes away. #3049 makes that change. The credential
212732
- * generation was ruled out as a key: it rotates on every reconnect, and CMMD
212733
- * uses it as the primary account's id, so keying on it would strand every
212734
- * thread pinned to that account on a directory that stops receiving deliveries.
212735
- *
212736
- * Tests reach the `credentialDirectory` parameter on
212737
- * `ClaudeAdapter.buildClaudeQueryEnv` directly.
212738
- */
212739
- const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
212740
213429
  /** Directory under the server's base directory that holds one directory per account. */
212741
213430
  const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
212742
213431
  const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
212743
- const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
212744
213432
  const DIRECTORY_NAME_HASH_LENGTH = 32;
212745
213433
  /**
212746
213434
  * Directory name for an account. A hash, so no email or display name reaches a
@@ -212749,11 +213437,12 @@ const DIRECTORY_NAME_HASH_LENGTH = 32;
212749
213437
  function claudeCredentialDirectoryName(accountIdentity) {
212750
213438
  return `account-${(0, node_crypto.createHash)("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
212751
213439
  }
212752
- /** The account this runtime's Claude turns belong to, or null while the variable is unset. */
212753
- function resolveClaudeProviderAccountIdentity(env) {
212754
- const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
212755
- if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
212756
- return identity;
213440
+ /** Only a matching CMMD delivery can select a credential directory. */
213441
+ function resolveSelectedClaudeAccountIdentity(selectedAccountId, binding) {
213442
+ if (selectedAccountId === void 0) return null;
213443
+ const identity = binding?.identity;
213444
+ if (identity?.kind !== "credential_generation" || identity.provider !== "claude" || identity.providerAccountIdentity !== selectedAccountId) throw new Error("The selected Claude account has no matching credential delivery. Reconnect that account and try again.");
213445
+ return identity.providerAccountIdentity;
212757
213446
  }
212758
213447
  /**
212759
213448
  * Where per-account credential directories live: `claude-credentials` under the
@@ -212780,29 +213469,19 @@ function resolveClaudeCredentialDirectory(input) {
212780
213469
  if (!root) return null;
212781
213470
  return (0, node_path.join)(root, claudeCredentialDirectoryName(input.accountIdentity));
212782
213471
  }
212783
- /**
212784
- * Decide whether this turn may point at a per-account credential directory.
212785
- *
212786
- * The cheap checks run first and the capability probe runs last, so a runtime
212787
- * with no usable credential never spawns the CLI. The probe's error channel is
212788
- * empty, so nothing here catches.
212789
- *
212790
- * A skip is not a fallback onto the default account. With no account the turn
212791
- * runs on the Environment's own login, unchanged. A thread pinned to an account
212792
- * whose credential is missing is refused by name under
212793
- * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
212794
- * `no-usable-credential`, and `capability-not-supported` for a pinned account,
212795
- * into those refusals. Until #3049 nothing names an account, so `no-account` is
212796
- * the only skip a runtime sees.
212797
- */
213472
+ /** Refuse selected accounts that cannot use their own credential directory. */
212798
213473
  function decideClaudeCredentialDirectory(input) {
212799
213474
  return require_Schema$1.gen(function* () {
212800
213475
  if (!input.accountIdentity) return skip("no-account");
212801
- if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
213476
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return refuse("operator-override");
212802
213477
  const directory = resolveClaudeCredentialDirectory(input);
212803
- if (!directory) return skip("no-root");
212804
- if (!holdsUsableCredential(directory)) return skip("no-usable-credential");
212805
- return (yield* input.probeSupport) === "supported" ? set(directory) : skip("capability-not-supported");
213478
+ if (!directory) return refuse("no-root");
213479
+ if (!holdsUsableCredential(directory)) return refuse("no-usable-credential");
213480
+ if ((yield* input.probeSupport) === "supported") return set(directory);
213481
+ return input.primaryCredentialMatchesSelected === true ? {
213482
+ outcome: "skipped",
213483
+ reason: "same-primary-account"
213484
+ } : refuse("capability-not-supported");
212806
213485
  });
212807
213486
  }
212808
213487
  /**
@@ -212841,6 +213520,12 @@ function skip(reason) {
212841
213520
  reason
212842
213521
  };
212843
213522
  }
213523
+ function refuse(reason) {
213524
+ return {
213525
+ outcome: "refused",
213526
+ reason
213527
+ };
213528
+ }
212844
213529
  function claudeConfigDirectory(env) {
212845
213530
  const configured = env.CLAUDE_CONFIG_DIR?.trim();
212846
213531
  if (configured) return configured;
@@ -212850,6 +213535,24 @@ function claudeConfigDirectory(env) {
212850
213535
  return null;
212851
213536
  }
212852
213537
  }
213538
+ /**
213539
+ * The fallback is allowed only when the primary on-disk credential is
213540
+ * byte-identical to what CMMD prepared for the selected account. A ticket's
213541
+ * `credentialRevisionHash` cannot express that: it hashes the encrypted row
213542
+ * CMMD stores, not the decrypted bytes this process ever sees. Comparing
213543
+ * files directly is the only check that means what the fallback needs.
213544
+ */
213545
+ function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
213546
+ const directory = claudeConfigDirectory(env);
213547
+ if (!directory) return false;
213548
+ try {
213549
+ const primary = (0, node_fs.readFileSync)((0, node_path.join)(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213550
+ const prepared = (0, node_fs.readFileSync)((0, node_path.join)(preparedDirectory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213551
+ return hasAccessToken(JSON.parse(primary)) && (0, node_crypto.createHash)("sha256").update(primary).digest("hex") === (0, node_crypto.createHash)("sha256").update(prepared).digest("hex");
213552
+ } catch {
213553
+ return false;
213554
+ }
213555
+ }
212853
213556
  //#endregion
212854
213557
  //#region src/provider/personalClaudeInVm.ts
212855
213558
  /**
@@ -212982,13 +213685,106 @@ function withoutInheritedAnthropicCredentials(env, options) {
212982
213685
  * marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
212983
213686
  * has the marker and no env var until it restarts, drops the override too.
212984
213687
  */
212985
- function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
213688
+ function claudeSubscriptionChildEnv(processEnv, credentialDirectory, selectedSubscription = false) {
212986
213689
  return {
212987
- ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
213690
+ ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: selectedSubscription || credentialDirectory !== null || personalClaudeInVmEnabled(processEnv) }),
212988
213691
  ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
212989
213692
  };
212990
213693
  }
212991
213694
  //#endregion
213695
+ //#region src/provider/claudeSelectedCredentialStore.ts
213696
+ async function ensurePrivateDirectory(path) {
213697
+ await (0, node_fs_promises.mkdir)(path, {
213698
+ recursive: true,
213699
+ mode: 448
213700
+ });
213701
+ const info = await (0, node_fs_promises.lstat)(path);
213702
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe credential directory.");
213703
+ await (0, node_fs_promises.chmod)(path, 448);
213704
+ }
213705
+ async function makeCapacity(root, target, active, maximum) {
213706
+ const entries = (await (0, node_fs_promises.readdir)(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && /^account-[a-f0-9]{32}$/.test(entry.name));
213707
+ if (entries.some((entry) => (0, node_path.join)(root, entry.name) === target)) return;
213708
+ const candidates = await Promise.all(entries.filter((entry) => !active.has((0, node_path.join)(root, entry.name))).map(async (entry) => ({
213709
+ path: (0, node_path.join)(root, entry.name),
213710
+ modified: (await (0, node_fs_promises.lstat)((0, node_path.join)(root, entry.name))).mtimeMs
213711
+ })));
213712
+ candidates.sort((a, b) => a.modified - b.modified);
213713
+ const required = entries.length - maximum + 1;
213714
+ if (required > candidates.length) throw new Error("All Claude credential slots are active. Stop a session before choosing another account.");
213715
+ for (const candidate of candidates.slice(0, Math.max(0, required))) await (0, node_fs_promises.rm)(candidate.path, { recursive: true });
213716
+ }
213717
+ function createClaudeSelectedCredentialStore(baseDir, options = {}) {
213718
+ const sessions = /* @__PURE__ */ new Map();
213719
+ const maximum = options.maxAccounts ?? 8;
213720
+ if (!Number.isSafeInteger(maximum) || maximum < 1) throw new Error("Invalid credential capacity.");
213721
+ let queue = Promise.resolve();
213722
+ async function install(threadId, acquired) {
213723
+ if (acquired.claims.threadId !== threadId) throw new Error("Credential thread mismatch.");
213724
+ const directory = resolveClaudeCredentialDirectory({
213725
+ env: process.env,
213726
+ baseDir,
213727
+ accountIdentity: acquired.claims.providerAccountId
213728
+ });
213729
+ if (!directory) throw new Error("Unsafe credential directory.");
213730
+ const binding = {
213731
+ runtimeId: acquired.claims.runtimeId,
213732
+ machineId: acquired.claims.machineId,
213733
+ credentialEpoch: acquired.claims.credentialEpoch,
213734
+ identity: {
213735
+ kind: "credential_generation",
213736
+ provider: "claude",
213737
+ credentialGenerationId: acquired.claims.generationId,
213738
+ providerAccountIdentity: acquired.claims.providerAccountId
213739
+ }
213740
+ };
213741
+ for (const prepared of sessions.values()) if (prepared.directory === directory && (!providerDeliveryBindingsEqual(prepared.binding, binding) || prepared.credentialRevisionHash !== acquired.claims.credentialRevisionHash)) throw new Error("The Claude account has an active session. Stop it before replacing its credentials.");
213742
+ const root = (0, node_path.dirname)(directory);
213743
+ await ensurePrivateDirectory(root);
213744
+ await makeCapacity(root, directory, new Set([...sessions.values()].map((entry) => entry.directory)), maximum);
213745
+ await ensurePrivateDirectory(directory);
213746
+ const temporary = (0, node_path.join)(directory, `.credential-${(0, node_crypto.randomUUID)()}.tmp`);
213747
+ try {
213748
+ await (0, node_fs_promises.writeFile)(temporary, acquired.credentialJson, {
213749
+ mode: 384,
213750
+ flag: "wx"
213751
+ });
213752
+ await (0, node_fs_promises.rename)(temporary, (0, node_path.join)(directory, ".credentials.json"));
213753
+ } finally {
213754
+ await (0, node_fs_promises.rm)(temporary, { force: true });
213755
+ }
213756
+ sessions.set(threadId, {
213757
+ directory,
213758
+ binding,
213759
+ accountKind: acquired.claims.accountKind,
213760
+ credentialRevisionHash: acquired.claims.credentialRevisionHash
213761
+ });
213762
+ return binding;
213763
+ }
213764
+ return {
213765
+ prepare(threadId, acquired) {
213766
+ const operation = queue.then(() => install(threadId, acquired));
213767
+ queue = operation.then(() => void 0, () => void 0);
213768
+ return operation;
213769
+ },
213770
+ read(threadId, binding) {
213771
+ const prepared = sessions.get(threadId);
213772
+ return prepared && providerDeliveryBindingsEqual(prepared.binding, binding) ? prepared : null;
213773
+ },
213774
+ release(threadId) {
213775
+ sessions.delete(threadId);
213776
+ }
213777
+ };
213778
+ }
213779
+ const stores = /* @__PURE__ */ new Map();
213780
+ function selectedClaudeCredentialStore(baseDir) {
213781
+ const existing = stores.get(baseDir);
213782
+ if (existing) return existing;
213783
+ const store = createClaudeSelectedCredentialStore(baseDir);
213784
+ stores.set(baseDir, store);
213785
+ return store;
213786
+ }
213787
+ //#endregion
212992
213788
  //#region src/provider/claudeCredentialDirectoryProbe.ts
212993
213789
  /**
212994
213790
  * Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
@@ -215033,7 +215829,8 @@ function claudeForgeAttributionPrompt() {
215033
215829
  * turn, an API key is, so pointing the variable there would add a second source
215034
215830
  * of truth that a later reader has to rule out.
215035
215831
  */
215036
- function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
215832
+ function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory, selectedSubscription = false) {
215833
+ if (selectedSubscription) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory, true);
215037
215834
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
215038
215835
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
215039
215836
  if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
@@ -216947,6 +217744,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
216947
217744
  if (streamFiber && streamFiber.pollUnsafe() === void 0) yield* require_Path.interrupt(streamFiber);
216948
217745
  try {
216949
217746
  context.query.close();
217747
+ selectedClaudeCredentialStore(serverConfig.baseDir).release(context.session.threadId);
216950
217748
  } catch (cause) {
216951
217749
  yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
216952
217750
  }
@@ -217258,6 +218056,43 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217258
218056
  sessionId: existingResumeSessionId ?? newSessionId
217259
218057
  };
217260
218058
  });
218059
+ const resolveSelectedCredentialDirectory = require_Schema$1.fn("resolveSelectedCredentialDirectory")(function* (input, resumeState, claudeBinaryPath) {
218060
+ const requestedAccountId = input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.providerAccountId : void 0;
218061
+ const selectedAccountIdentity = yield* require_Schema$1.try_({
218062
+ try: () => resolveSelectedClaudeAccountIdentity(requestedAccountId, resumeState?.providerDeliveryBinding),
218063
+ catch: () => new ProviderAdapterValidationError({
218064
+ provider: PROVIDER$11,
218065
+ operation: "startSession",
218066
+ issue: `The selected Claude account ${requestedAccountId} has no matching credential delivery. Reconnect that account and try again.`
218067
+ })
218068
+ });
218069
+ const preparedCredential = resumeState?.providerDeliveryBinding ? selectedClaudeCredentialStore(serverConfig.baseDir).read(input.threadId, resumeState.providerDeliveryBinding) : null;
218070
+ if (selectedAccountIdentity !== null && preparedCredential === null) return yield* new ProviderAdapterValidationError({
218071
+ provider: PROVIDER$11,
218072
+ operation: "startSession",
218073
+ issue: `The selected Claude account ${selectedAccountIdentity} has no prepared credential. Try again to reconnect it.`
218074
+ });
218075
+ const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
218076
+ env: process.env,
218077
+ baseDir: serverConfig.baseDir,
218078
+ accountIdentity: selectedAccountIdentity,
218079
+ probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
218080
+ primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
218081
+ });
218082
+ if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
218083
+ provider: PROVIDER$11,
218084
+ operation: "startSession",
218085
+ issue: `The selected Claude account ${selectedAccountIdentity} cannot run on this environment: ${credentialDirectoryDecision.reason}. Reconnect the account or choose another environment.`
218086
+ });
218087
+ if (credentialDirectoryDecision.outcome === "skipped") {
218088
+ const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
218089
+ yield* credentialDirectoryDecision.reason === "no-account" ? require_Schema$1.logDebug(skipLine) : require_Schema$1.logWarning(skipLine);
218090
+ }
218091
+ return {
218092
+ selectedAccountIdentity,
218093
+ claudeCredentialDirectory: credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null
218094
+ };
218095
+ });
217261
218096
  const startSession = require_Schema$1.fn("startSession")(function* (input) {
217262
218097
  const sessionProvider = yield* validateClaudeStartSessionProvider(input.provider);
217263
218098
  const startedAt = yield* nowIso;
@@ -217318,19 +218153,14 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217318
218153
  })));
217319
218154
  const claudeSettings = serverSettings.providers.claudeAgent;
217320
218155
  const claudeBinaryPath = claudeSettings.binaryPath;
217321
- const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
217322
- env: process.env,
217323
- baseDir: serverConfig.baseDir,
217324
- accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
217325
- probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
217326
- });
217327
- if (credentialDirectoryDecision.outcome === "skipped") {
217328
- const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
217329
- yield* credentialDirectoryDecision.reason === "no-account" ? require_Schema$1.logDebug(skipLine) : require_Schema$1.logWarning(skipLine);
217330
- }
217331
- const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
218156
+ const { selectedAccountIdentity, claudeCredentialDirectory } = yield* resolveSelectedCredentialDirectory(input, resumeState, claudeBinaryPath);
217332
218157
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
217333
- const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
218158
+ if (selectedAccountIdentity !== null && modelConfig.endpointProfile !== void 0) return yield* new ProviderAdapterValidationError({
218159
+ provider: PROVIDER$11,
218160
+ operation: "startSession",
218161
+ issue: "The selected Claude account cannot be used with a custom API endpoint. Choose a subscription model."
218162
+ });
218163
+ const providerProxyCredential = selectedAccountIdentity !== null ? null : yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
217334
218164
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
217335
218165
  const settings = claudeQuerySettings(modelConfig.thinking, modelConfig.fastMode);
217336
218166
  const workspacePreflight = input.cwd ? formatWorkspaceEnvironmentPreflightForAgent(runWorkspaceEnvironmentPreflight({ cwd: input.cwd })) : void 0;
@@ -217353,7 +218183,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217353
218183
  existingResumeSessionId,
217354
218184
  newSessionId,
217355
218185
  canUseTool,
217356
- env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
218186
+ env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory, selectedAccountIdentity !== null),
217357
218187
  browserManager,
217358
218188
  threadId,
217359
218189
  browserToolEnvironmentId,
@@ -217405,7 +218235,9 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217405
218235
  });
217406
218236
  attachClaudeStreamFiber(context$4, runFork);
217407
218237
  return { ...session };
217408
- });
218238
+ }, (effect, input) => effect.pipe(require_Schema$1.onExit((exit) => require_Schema$1.sync(() => {
218239
+ if (exit._tag === "Failure") selectedClaudeCredentialStore(serverConfig.baseDir).release(input.threadId);
218240
+ }))));
217409
218241
  const sendTurn = require_Schema$1.fn("sendTurn")(function* (input) {
217410
218242
  const context = yield* requireSession(input.threadId);
217411
218243
  const modelSelection = input.modelSelection?.provider === "claudeAgent" || input.modelSelection?.provider === "apex" || input.modelSelection?.provider === "cmmd" ? input.modelSelection : void 0;
@@ -231714,6 +232546,9 @@ const ProviderRollbackConversationInput = require_Schema$1.Struct({
231714
232546
  threadId: ThreadId,
231715
232547
  numTurns: NonNegativeInt
231716
232548
  });
232549
+ function selectedAccountGuardFields(providerAccountId) {
232550
+ return providerAccountId === void 0 ? {} : { providerAccountId };
232551
+ }
231717
232552
  function cursorRecord(value) {
231718
232553
  return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
231719
232554
  }
@@ -231806,9 +232641,13 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
231806
232641
  const runtimeEventPubSub = yield* require_Path.unbounded$1();
231807
232642
  const assertProviderExecutable = options?.assertProviderExecutable ?? (() => require_Schema$1.succeed(null));
231808
232643
  const assertCurrentProviderDelivery = require_Schema$1.fn("assertCurrentProviderDelivery")(function* (input) {
231809
- const currentBinding = yield* assertProviderExecutable(input);
231810
- if (!currentBinding) return;
231811
232644
  const persistedBinding = require_Schema$1.getOrUndefined$1(yield* directory.getBinding(input.threadId));
232645
+ const providerAccountId = readPersistedProviderAccountId(persistedBinding?.runtimePayload);
232646
+ const currentBinding = yield* assertProviderExecutable({
232647
+ ...input,
232648
+ ...selectedAccountGuardFields(providerAccountId)
232649
+ });
232650
+ if (!currentBinding) return;
231812
232651
  if (!persistedBinding || persistedBinding.provider !== input.provider) return yield* toValidationError(input.operation, `Cannot verify the delivered provider identity for thread '${input.threadId}'.`);
231813
232652
  yield* bindProviderDeliveryToResumeCursor({
231814
232653
  operation: input.operation,
@@ -231864,10 +232703,12 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
231864
232703
  if (!hasResumeCursor) return yield* toValidationError(input.operation, `Cannot recover thread '${input.binding.threadId}' because no provider resume state is persisted.`);
231865
232704
  const persistedCwd = readPersistedCwd(input.binding.runtimePayload);
231866
232705
  const persistedModelSelection = readPersistedModelSelection(input.binding.runtimePayload);
232706
+ const persistedProviderAccountId = readProviderAccountIdFromModelSelection(persistedModelSelection);
231867
232707
  const currentDeliveryBinding = yield* assertProviderExecutable({
231868
232708
  threadId: input.binding.threadId,
231869
232709
  provider: input.binding.provider,
231870
- operation: input.operation
232710
+ operation: input.operation,
232711
+ ...selectedAccountGuardFields(persistedProviderAccountId)
231871
232712
  });
231872
232713
  const boundResumeCursor = yield* bindProviderDeliveryToResumeCursor({
231873
232714
  operation: input.operation,
@@ -231993,7 +232834,8 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
231993
232834
  currentBinding: yield* assertProviderExecutable({
231994
232835
  threadId,
231995
232836
  provider: effectiveProvider,
231996
- operation: "ProviderService.startSession"
232837
+ operation: "ProviderService.startSession",
232838
+ ...selectedAccountGuardFields(requestedAccountId)
231997
232839
  })
231998
232840
  });
231999
232841
  const session = yield* adapter.startSession({
@@ -232059,6 +232901,8 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
232059
232901
  allowRecovery: true
232060
232902
  });
232061
232903
  const persistedBinding = require_Schema$1.getOrUndefined$1(yield* directory.getBinding(input.threadId));
232904
+ const persistedModelSelection = readPersistedModelSelection(persistedBinding?.runtimePayload);
232905
+ const nextModelSelection = input.modelSelection ?? persistedModelSelection;
232062
232906
  yield* assertProviderAccountBoundary({
232063
232907
  operation: "ProviderService.sendTurn",
232064
232908
  threadId: input.threadId,
@@ -232083,7 +232927,7 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
232083
232927
  status: "running",
232084
232928
  ...turn.resumeCursor !== void 0 ? { resumeCursor: turn.resumeCursor } : {},
232085
232929
  runtimePayload: {
232086
- ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {},
232930
+ ...nextModelSelection !== void 0 ? { modelSelection: nextModelSelection } : {},
232087
232931
  activeTurnId: turn.turnId,
232088
232932
  lastRuntimeEvent: "provider.sendTurn",
232089
232933
  lastRuntimeEventAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -232340,6 +233184,201 @@ function makeProviderServiceLive(options) {
232340
233184
  return require_Schema$1.effect(ProviderService, makeProviderService(options));
232341
233185
  }
232342
233186
  //#endregion
233187
+ //#region src/runtimeAuth/cmmdClaudeSelectedTicket.ts
233188
+ const ACCOUNT = /^pai_v1_[A-Za-z0-9_-]{43}$/;
233189
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
233190
+ const REVISION_HASH = /^[a-f0-9]{64}$/;
233191
+ function hasValidTicketLifetime(claims, payload) {
233192
+ const now = Math.floor(Date.now() / 1e3);
233193
+ const issuedAt = payload.iat;
233194
+ const notBefore = payload.nbf;
233195
+ return typeof issuedAt === "number" && Number.isSafeInteger(issuedAt) && issuedAt <= now + 5 && typeof notBefore === "number" && Number.isSafeInteger(notBefore) && notBefore <= now && Number.isSafeInteger(claims.expiresAtSeconds) && claims.expiresAtSeconds - issuedAt <= 900;
233196
+ }
233197
+ function hasSelectedCredentialScope(claims) {
233198
+ return claims.scope.length === 1 && claims.scope[0] === "claude-credential-read";
233199
+ }
233200
+ function hasExpectedSelectedBinding(payload, expected) {
233201
+ return payload.provider_account_id === expected.providerAccountId && ACCOUNT.test(expected.providerAccountId) && payload.thread_id === expected.threadId && payload.operation_id === expected.operationId && UUID.test(expected.operationId);
233202
+ }
233203
+ function selectedCredentialClaims(payload) {
233204
+ const jti = payload.jti;
233205
+ const generationId = payload.credential_generation_id;
233206
+ const credentialRevisionHash = payload.credential_revision_hash;
233207
+ const accountKind = payload.account_kind;
233208
+ if (typeof jti !== "string" || !jti.trim() || typeof generationId !== "string" || !UUID.test(generationId) || typeof credentialRevisionHash !== "string" || !REVISION_HASH.test(credentialRevisionHash) || accountKind !== "primary" && accountKind !== "linked") return null;
233209
+ return {
233210
+ jti,
233211
+ generationId,
233212
+ credentialRevisionHash,
233213
+ accountKind
233214
+ };
233215
+ }
233216
+ function verifyCmmdClaudeSelectedTicket(token, expected) {
233217
+ const verified = require_runtimeControlAuthority.verifyCmmdScopedRuntimePayload(token, expected, "forge-claude-credential-read");
233218
+ if (!verified) return null;
233219
+ const { claims, payload } = verified;
233220
+ if (!hasValidTicketLifetime(claims, payload)) return null;
233221
+ if (!hasSelectedCredentialScope(claims)) return null;
233222
+ if (!hasExpectedSelectedBinding(payload, expected)) return null;
233223
+ const selected = selectedCredentialClaims(payload);
233224
+ if (!selected) return null;
233225
+ return {
233226
+ ...claims,
233227
+ jti: selected.jti,
233228
+ providerAccountId: expected.providerAccountId,
233229
+ generationId: selected.generationId,
233230
+ credentialRevisionHash: selected.credentialRevisionHash,
233231
+ accountKind: selected.accountKind,
233232
+ threadId: expected.threadId,
233233
+ operationId: expected.operationId
233234
+ };
233235
+ }
233236
+ //#endregion
233237
+ //#region src/provider/claudeSelectedCredentialAcquisition.ts
233238
+ function readToken(response) {
233239
+ if (typeof response !== "object" || response === null || !("data" in response)) return null;
233240
+ const data = response.data;
233241
+ return typeof data === "object" && data !== null && "token" in data && typeof data.token === "string" ? data.token : null;
233242
+ }
233243
+ async function acquireSelectedClaudeCredential(expected, transport) {
233244
+ try {
233245
+ const token = readToken(await transport.requestTicket());
233246
+ const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
233247
+ if (!token || !claims) throw new Error("invalid_ticket");
233248
+ const credentialJson = await transport.readCredential(token);
233249
+ if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
233250
+ return {
233251
+ claims,
233252
+ credentialJson
233253
+ };
233254
+ } catch {
233255
+ throw new Error("The selected Claude account could not be prepared. Reconnect the account and try again.");
233256
+ }
233257
+ }
233258
+ //#endregion
233259
+ //#region src/provider/claudeSelectedCredentialPreparer.ts
233260
+ const REQUEST_TIMEOUT_MS = 1e4;
233261
+ const OWNER_TICKET_PATH = "/api/forge/context/providers/claude/runtime-read-ticket";
233262
+ const CREDENTIAL_READ_PATH = "/api/forge/context/auth-runtime-claude-credential";
233263
+ function isRuntimeTicketSession$1(sessionId) {
233264
+ return String(sessionId).startsWith("cmmd-runtime:");
233265
+ }
233266
+ function nonEmptyConfigString(value) {
233267
+ return value?.trim() || null;
233268
+ }
233269
+ function selectedRuntimeStrings(config) {
233270
+ const values = [
233271
+ nonEmptyConfigString(config.forgeCmmdRuntimeId),
233272
+ nonEmptyConfigString(config.forgeCmmdRuntimeMachineId),
233273
+ nonEmptyConfigString(config.forgeCmmdRuntimeOrgId),
233274
+ nonEmptyConfigString(config.forgeCmmdRuntimeUserId),
233275
+ nonEmptyConfigString(config.forgeCmmdRuntimePublicKey)
233276
+ ];
233277
+ if (values.includes(null)) return null;
233278
+ return {
233279
+ runtimeId: values[0],
233280
+ machineId: values[1],
233281
+ orgId: values[2],
233282
+ userId: values[3],
233283
+ publicKeyPem: values[4]
233284
+ };
233285
+ }
233286
+ function selectedCredentialEpoch(config) {
233287
+ const credentialEpoch = config.forgeCmmdRuntimeCredentialEpoch;
233288
+ return typeof credentialEpoch === "number" && Number.isSafeInteger(credentialEpoch) && credentialEpoch >= 0 ? credentialEpoch : null;
233289
+ }
233290
+ function selectedConfig(config) {
233291
+ if (config.forgeCmmdRuntimeMode !== true) return null;
233292
+ const strings = selectedRuntimeStrings(config);
233293
+ const credentialEpoch = selectedCredentialEpoch(config);
233294
+ if (strings === null || credentialEpoch === null) return null;
233295
+ return {
233296
+ ...strings,
233297
+ credentialEpoch
233298
+ };
233299
+ }
233300
+ async function boundedRuntimeTicketRequest(grant, runtimeId, request) {
233301
+ const response = await fetch(resolveCommandAppEndpoint(`/api/forge/runtimes/${encodeURIComponent(runtimeId)}/provider/claude/read-ticket`), {
233302
+ method: "POST",
233303
+ headers: {
233304
+ authorization: `Bearer ${grant}`,
233305
+ "content-type": "application/json"
233306
+ },
233307
+ body: JSON.stringify(request),
233308
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
233309
+ });
233310
+ if (!response.ok) throw new Error("selected_ticket_unavailable");
233311
+ return await response.json();
233312
+ }
233313
+ async function boundedCredentialRead(token) {
233314
+ const response = await fetch(resolveCommandAppEndpoint(CREDENTIAL_READ_PATH), {
233315
+ headers: {
233316
+ authorization: `Bearer ${token}`,
233317
+ accept: "application/json"
233318
+ },
233319
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
233320
+ });
233321
+ if (!response.ok) throw new Error("selected_credential_unavailable");
233322
+ return await response.text();
233323
+ }
233324
+ function missingAuthority() {
233325
+ throw new Error("selected_authority_unavailable");
233326
+ }
233327
+ /**
233328
+ * Acquires one signed, account-scoped Claude credential for the thread that
233329
+ * selected it. Owner and runtime-ticket sessions use different CMMD lanes.
233330
+ */
233331
+ function makeClaudeSelectedCredentialPreparer(config, dependencies) {
233332
+ const readRuntimeAuthority = dependencies.readRuntimeAuthority ?? readRuntimeBrokerAuthority;
233333
+ const requestOwnerTicket = dependencies.requestOwnerTicket ?? ((jwt, request) => proxyPost(jwt, OWNER_TICKET_PATH, request, void 0, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }));
233334
+ const requestRuntimeTicket = dependencies.requestRuntimeTicket ?? boundedRuntimeTicketRequest;
233335
+ const readCredential = dependencies.readCredential ?? boundedCredentialRead;
233336
+ const prepare = dependencies.prepare ?? ((threadId, acquired) => {
233337
+ if (!config.baseDir) return Promise.reject(/* @__PURE__ */ new Error("selected_store_unavailable"));
233338
+ return selectedClaudeCredentialStore(config.baseDir).prepare(threadId, acquired);
233339
+ });
233340
+ const createOperationId = dependencies.createOperationId ?? node_crypto.randomUUID;
233341
+ return async (input) => {
233342
+ const sealed = selectedConfig(config);
233343
+ const sessionId = input.executionSessionId ?? dependencies.readSessionId(input.threadId);
233344
+ if (sealed === null || sessionId === null) return missingAuthority();
233345
+ const operationId = createOperationId();
233346
+ const request = {
233347
+ providerAccountId: input.providerAccountId,
233348
+ threadId: String(input.threadId),
233349
+ operationId
233350
+ };
233351
+ const acquired = await acquireSelectedClaudeCredential({
233352
+ ...sealed,
233353
+ providerAccountId: input.providerAccountId,
233354
+ threadId: String(input.threadId),
233355
+ operationId
233356
+ }, {
233357
+ requestTicket: async () => {
233358
+ if (isRuntimeTicketSession$1(sessionId)) {
233359
+ const grant = readRuntimeAuthority({
233360
+ sessionId,
233361
+ runtimeId: sealed.runtimeId,
233362
+ machineId: sealed.machineId,
233363
+ credentialEpoch: sealed.credentialEpoch,
233364
+ requiredScope: "provider"
233365
+ });
233366
+ if (grant === null) return missingAuthority();
233367
+ return { data: await requestRuntimeTicket(grant, sealed.runtimeId, request) };
233368
+ }
233369
+ const jwt = await dependencies.resolveJwt(sessionId, { forceRefresh: false });
233370
+ if (jwt === null) return missingAuthority();
233371
+ return await requestOwnerTicket(jwt, {
233372
+ ...request,
233373
+ runtimeId: sealed.runtimeId
233374
+ });
233375
+ },
233376
+ readCredential
233377
+ });
233378
+ return await prepare(String(input.threadId), acquired);
233379
+ };
233380
+ }
233381
+ //#endregion
232343
233382
  //#region src/provider/claudeRuntimeReadinessGuard.ts
232344
233383
  const GENERATION_HASH_PATTERN = /^[a-f0-9]{64}$/;
232345
233384
  const RUNTIME_READINESS_TIMEOUT_MS$1 = 1e4;
@@ -232608,7 +233647,22 @@ function makeClaudeRuntimeReadinessGuard(config, dependencies) {
232608
233647
  return (input) => claudeReadinessGuardEffect(config, dependencies, resolved, input);
232609
233648
  }
232610
233649
  function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
232611
- if (!requiresNativeClaudeReadiness(config, input.provider)) return require_Schema$1.succeed(null);
233650
+ if (!requiresNativeClaudeReadiness(config, input.provider, input.providerAccountId)) return require_Schema$1.succeed(null);
233651
+ if (input.providerAccountId !== void 0) {
233652
+ const prepareSelectedCredential = dependencies.prepareSelectedCredential ?? makeClaudeSelectedCredentialPreparer(config, {
233653
+ resolveJwt: dependencies.resolveJwt,
233654
+ readSessionId: resolved.readSessionId,
233655
+ readRuntimeAuthority: resolved.readRuntimeAuthority
233656
+ });
233657
+ const selectedInput = {
233658
+ ...input,
233659
+ providerAccountId: input.providerAccountId
233660
+ };
233661
+ return require_Schema$1.tryPromise({
233662
+ try: () => prepareSelectedCredential(selectedInput),
233663
+ catch: () => selectedCredentialValidationError(input.operation, selectedInput.providerAccountId)
233664
+ });
233665
+ }
232612
233666
  const authority = resolveGuardAuthority(config, resolved.readSessionId, input);
232613
233667
  if (authority === null) return invalidOwnerSessionEffect(input.operation);
232614
233668
  return require_Schema$1.tryPromise({
@@ -232616,8 +233670,9 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
232616
233670
  catch: (cause) => readinessValidationError(input.operation, cause)
232617
233671
  });
232618
233672
  }
232619
- function requiresNativeClaudeReadiness(config, provider) {
232620
- return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
233673
+ function requiresNativeClaudeReadiness(config, provider, providerAccountId) {
233674
+ if (provider !== "claudeAgent" || config.forgeCmmdRuntimeMode !== true) return false;
233675
+ return providerAccountId !== void 0 || personalClaudeInVmEnabled();
232621
233676
  }
232622
233677
  function resolveGuardAuthority(config, readSessionId, input) {
232623
233678
  const runtimeId = config.forgeCmmdRuntimeId?.trim();
@@ -232654,6 +233709,12 @@ async function verifyGuardAuthority(config, dependencies, resolved, authority) {
232654
233709
  fetchReadiness: resolved.fetchReadiness
232655
233710
  });
232656
233711
  }
233712
+ function selectedCredentialValidationError(operation, accountId) {
233713
+ return new ProviderValidationError({
233714
+ operation,
233715
+ issue: `The selected Claude account ${accountId} could not be prepared. Reconnect the account and try again.`
233716
+ });
233717
+ }
232657
233718
  function readinessValidationError(operation, cause) {
232658
233719
  return new ProviderValidationError({
232659
233720
  operation,
@@ -291409,6 +292470,10 @@ const make$8 = require_Schema$1.gen(function* () {
291409
292470
  shouldRestartForAccountChange,
291410
292471
  hasResumeCursor: resumeCursor !== void 0
291411
292472
  });
292473
+ if (shouldRestartForAccountChange) yield* providerService.stopSession({
292474
+ threadId,
292475
+ preserveResumeState: true
292476
+ });
291412
292477
  const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
291413
292478
  yield* require_Schema$1.logInfo("provider command reactor restarted provider session", {
291414
292479
  threadId,
@@ -297393,7 +298458,7 @@ function resolveBuildCommitFromEnv(env) {
297393
298458
  * environment descriptor down instead of reporting an honest "unknown".
297394
298459
  */
297395
298460
  function readBakedBuildCommit() {
297396
- return "05d8568497fdceb5b5d827b142ea4c681bef25ce";
298461
+ return "81ce9360f406bcb5ae9fbdc9f90dd55d33f25f0c";
297397
298462
  }
297398
298463
  async function resolveServerBuildCommit(input) {
297399
298464
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -298818,20 +299883,20 @@ const generatePairingToken = () => {
298818
299883
  const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
298819
299884
  return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
298820
299885
  };
299886
+ const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
299887
+ message,
299888
+ status: 401
299889
+ });
299890
+ const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
299891
+ message,
299892
+ status: 500,
299893
+ cause
299894
+ });
298821
299895
  const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298822
299896
  const config = yield* ServerConfig;
298823
299897
  const pairingLinks = yield* AuthPairingLinkRepository;
298824
299898
  const seededGrantsRef = yield* require_HttpServer.make$3(/* @__PURE__ */ new Map());
298825
299899
  const changesPubSub = yield* require_Path.unbounded$1();
298826
- const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
298827
- message,
298828
- status: 401
298829
- });
298830
- const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
298831
- message,
298832
- status: 500,
298833
- cause
298834
- });
298835
299900
  const seedGrant = (credential, grant) => require_HttpServer.update(seededGrantsRef, (current) => {
298836
299901
  const next = new Map(current);
298837
299902
  next.set(credential, grant);
@@ -298846,19 +299911,19 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298846
299911
  id
298847
299912
  }).pipe(require_Schema$1.asVoid);
298848
299913
  if (config.desktopBootstrapToken) {
298849
- const now$2 = yield* require_Schema$1.now;
299914
+ const now$3 = yield* require_Schema$1.now;
298850
299915
  yield* seedGrant(config.desktopBootstrapToken, {
298851
299916
  method: "desktop-bootstrap",
298852
299917
  role: "owner",
298853
299918
  subject: "desktop-bootstrap",
298854
- expiresAt: require_Schema$1.add(now$2, { milliseconds: require_Schema$1.toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
299919
+ expiresAt: require_Schema$1.add(now$3, { milliseconds: require_Schema$1.toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
298855
299920
  remainingUses: "unbounded"
298856
299921
  });
298857
299922
  }
298858
299923
  const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
298859
299924
  const listActive = () => require_Schema$1.gen(function* () {
298860
- const now$5 = yield* require_Schema$1.now;
298861
- return (yield* pairingLinks.listActive({ now: now$5 })).map((row) => row.label ? {
299925
+ const now$2 = yield* require_Schema$1.now;
299926
+ return (yield* pairingLinks.listActive({ now: now$2 })).map((row) => row.label ? {
298862
299927
  id: row.id,
298863
299928
  credential: row.credential,
298864
299929
  role: row.role,
@@ -298888,8 +299953,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298888
299953
  const id = crypto.randomUUID();
298889
299954
  const credential = generatePairingToken();
298890
299955
  const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
298891
- const now$4 = yield* require_Schema$1.now;
298892
- const expiresAt = require_Schema$1.add(now$4, { milliseconds: require_Schema$1.toMillis(ttl) });
299956
+ const now$5 = yield* require_Schema$1.now;
299957
+ const expiresAt = require_Schema$1.add(now$5, { milliseconds: require_Schema$1.toMillis(ttl) });
298893
299958
  const issued = {
298894
299959
  id,
298895
299960
  credential,
@@ -298903,7 +299968,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298903
299968
  role: input?.role ?? "client",
298904
299969
  subject: input?.subject ?? "one-time-token",
298905
299970
  label: input?.label ?? null,
298906
- createdAt: now$4,
299971
+ createdAt: now$5,
298907
299972
  expiresAt
298908
299973
  });
298909
299974
  yield* emitUpsert({
@@ -298912,13 +299977,13 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298912
299977
  role: input?.role ?? "client",
298913
299978
  subject: input?.subject ?? "one-time-token",
298914
299979
  ...input?.label ? { label: input.label } : {},
298915
- createdAt: now$4,
299980
+ createdAt: now$5,
298916
299981
  expiresAt
298917
299982
  });
298918
299983
  return issued;
298919
299984
  }).pipe(require_Schema$1.mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
298920
299985
  const consume = (credential) => require_Schema$1.gen(function* () {
298921
- const now$3 = yield* require_Schema$1.now;
299986
+ const now$4 = yield* require_Schema$1.now;
298922
299987
  const seededResult = yield* require_HttpServer.modify(seededGrantsRef, (current) => {
298923
299988
  const grant = current.get(credential);
298924
299989
  if (!grant) return [{
@@ -298927,7 +299992,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298927
299992
  error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
298928
299993
  }, current];
298929
299994
  const next = new Map(current);
298930
- if (require_Schema$1.isGreaterThanOrEqualTo$1(now$3, grant.expiresAt)) {
299995
+ if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
298931
299996
  next.delete(credential);
298932
299997
  return [{
298933
299998
  _tag: "error",
@@ -298956,8 +300021,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298956
300021
  if (seededResult.reason !== "not-found") return yield* seededResult.error;
298957
300022
  const consumed = yield* pairingLinks.consumeAvailable({
298958
300023
  credential,
298959
- consumedAt: now$3,
298960
- now: now$3
300024
+ consumedAt: now$4,
300025
+ now: now$4
298961
300026
  });
298962
300027
  if (require_Schema$1.isSome(consumed)) {
298963
300028
  yield* emitRemoved(consumed.value.id);
@@ -298973,9 +300038,9 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
298973
300038
  if (require_Schema$1.isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
298974
300039
  if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
298975
300040
  if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
298976
- if (require_Schema$1.isGreaterThanOrEqualTo$1(now$3, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
300041
+ if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
298977
300042
  return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
298978
- }).pipe(require_Schema$1.mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)));
300043
+ }).pipe(require_Schema$1.mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)), require_Schema$1.tapError((error) => error.status === 401 ? require_Schema$1.logWarning("Rejected bootstrap credential exchange.", { reason: error.message }) : require_Schema$1.void_));
298979
300044
  return {
298980
300045
  issueOneTimeToken,
298981
300046
  listActive,
@@ -299031,6 +300096,10 @@ const SetAuthSessionLastConnectedAtInput = require_Schema$1.Struct({
299031
300096
  sessionId: AuthSessionId,
299032
300097
  lastConnectedAt: require_Schema$1.DateTimeUtcFromString
299033
300098
  });
300099
+ const ExtendAuthSessionExpiryInput = require_Schema$1.Struct({
300100
+ sessionId: AuthSessionId,
300101
+ expiresAt: require_Schema$1.DateTimeUtcFromString
300102
+ });
299034
300103
  var AuthSessionRepository = class extends require_Schema$1.Service()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
299035
300104
  //#endregion
299036
300105
  //#region src/persistence/Layers/AuthSessions.ts
@@ -299182,6 +300251,17 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
299182
300251
  WHERE session_id = ${sessionId}
299183
300252
  AND revoked_at IS NULL
299184
300253
  RETURNING session_id AS "sessionId"
300254
+ `
300255
+ });
300256
+ const extendSessionExpiryRows = findAll({
300257
+ Request: ExtendAuthSessionExpiryInput,
300258
+ Result: require_Schema$1.Struct({ sessionId: AuthSessionId }),
300259
+ execute: ({ sessionId, expiresAt }) => sql`
300260
+ UPDATE auth_sessions
300261
+ SET expires_at = ${expiresAt}
300262
+ WHERE session_id = ${sessionId}
300263
+ AND revoked_at IS NULL
300264
+ RETURNING session_id AS "sessionId"
299185
300265
  `
299186
300266
  });
299187
300267
  const revokeOtherSessionRows = findAll({
@@ -299204,13 +300284,15 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
299204
300284
  const revoke = (input) => revokeSessionRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows")), require_Schema$1.map((rows) => rows.length > 0));
299205
300285
  const revokeAllExcept = (input) => revokeOtherSessionRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows")), require_Schema$1.map((rows) => rows.map((row) => row.sessionId)));
299206
300286
  const setLastConnectedAt = (input) => setLastConnectedAtRow(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest")));
300287
+ const extendExpiry = (input) => extendSessionExpiryRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.extendExpiry:query", "AuthSessionRepository.extendExpiry:decodeRows")), require_Schema$1.map((rows) => rows.length > 0));
299207
300288
  return {
299208
300289
  create,
299209
300290
  getById,
299210
300291
  listActive,
299211
300292
  revoke,
299212
300293
  revokeAllExcept,
299213
- setLastConnectedAt
300294
+ setLastConnectedAt,
300295
+ extendExpiry
299214
300296
  };
299215
300297
  });
299216
300298
  const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository, makeAuthSessionRepository);
@@ -299219,6 +300301,20 @@ const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository,
299219
300301
  const SIGNING_SECRET_NAME = "server-signing-key";
299220
300302
  const DEFAULT_SESSION_TTL = require_Schema$1.days(30);
299221
300303
  const DEFAULT_WEBSOCKET_TOKEN_TTL = require_Schema$1.minutes(5);
300304
+ /**
300305
+ * Whether a session presented at `nowMs` has used up half its own lifetime.
300306
+ *
300307
+ * A phone that opens the app daily should never hit the 30-day wall, but a
300308
+ * lost phone's session should still die on schedule if it is never presented
300309
+ * again. Sliding the expiry forward only when the session is already more
300310
+ * than half spent gives an active phone many chances to renew before expiry
300311
+ * while keeping a dormant session's remaining lifetime bounded by the last
300312
+ * time it was actually used.
300313
+ */
300314
+ function isPastHalfLife(input) {
300315
+ const halfLifeMarkMs = input.issuedAtMs + (input.expiresAtMs - input.issuedAtMs) / 2;
300316
+ return input.nowMs >= halfLifeMarkMs;
300317
+ }
299222
300318
  const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
299223
300319
  message,
299224
300320
  cause
@@ -299379,6 +300475,42 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
299379
300475
  role: claims.role
299380
300476
  };
299381
300477
  }).pipe(require_Schema$1.mapError(toSessionCredentialError("Failed to issue session credential.")));
300478
+ /**
300479
+ * Slide a session's expiry forward when the token it just verified is past
300480
+ * half its own lifetime, so a phone that keeps using the app never runs
300481
+ * into the 30-day wall.
300482
+ *
300483
+ * The session id, subject, role and method carry over unchanged — only the
300484
+ * signature-bearing `iat`/`exp` differ — which is what lets the DB row be
300485
+ * extended in place (`extendExpiry`) rather than replaced: everything keyed
300486
+ * by session id, most importantly the `command_access_tokens` row holding
300487
+ * this session's CMMD credential, needs no migration.
300488
+ */
300489
+ const renewIfPastHalfLife = (input) => require_Schema$1.gen(function* () {
300490
+ const nowMs = yield* require_Schema$1.currentTimeMillis;
300491
+ if (!isPastHalfLife({
300492
+ issuedAtMs: input.row.issuedAt.epochMilliseconds,
300493
+ expiresAtMs: input.row.expiresAt.epochMilliseconds,
300494
+ nowMs
300495
+ })) return void 0;
300496
+ const newIssuedAt = yield* require_Schema$1.now;
300497
+ const newExpiresAt = require_Schema$1.add(newIssuedAt, { milliseconds: require_Schema$1.toMillis(DEFAULT_SESSION_TTL) });
300498
+ const newClaims = {
300499
+ ...input.claims,
300500
+ iat: newIssuedAt.epochMilliseconds,
300501
+ exp: newExpiresAt.epochMilliseconds
300502
+ };
300503
+ const newEncodedPayload = base64UrlEncode(JSON.stringify(newClaims));
300504
+ const newSignature = signPayload(newEncodedPayload, signingSecret);
300505
+ if (!(yield* authSessions.extendExpiry({
300506
+ sessionId: input.claims.sid,
300507
+ expiresAt: newExpiresAt
300508
+ }).pipe(require_Schema$1.orElseSucceed(() => false)))) return void 0;
300509
+ return {
300510
+ token: `${newEncodedPayload}.${newSignature}`,
300511
+ expiresAt: newExpiresAt
300512
+ };
300513
+ });
299382
300514
  const verify = (token) => require_Schema$1.gen(function* () {
299383
300515
  const [encodedPayload, signature] = token.split(".");
299384
300516
  if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
@@ -299393,14 +300525,19 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
299393
300525
  if (require_Schema$1.isSome(row)) {
299394
300526
  if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
299395
300527
  } else if (!(statelessCookieSessions && claims.method === "browser-session-cookie")) return yield* new SessionCredentialError({ message: "Unknown session token." });
300528
+ const renewed = require_Schema$1.isSome(row) ? yield* renewIfPastHalfLife({
300529
+ claims,
300530
+ row: row.value
300531
+ }) : void 0;
299396
300532
  return {
299397
300533
  sessionId: claims.sid,
299398
300534
  token,
299399
300535
  method: claims.method,
299400
300536
  client: require_Schema$1.isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
299401
- expiresAt: require_Schema$1.makeUnsafe(claims.exp),
300537
+ expiresAt: renewed?.expiresAt ?? require_Schema$1.makeUnsafe(claims.exp),
299402
300538
  subject: claims.sub,
299403
- role: claims.role
300539
+ role: claims.role,
300540
+ ...renewed ? { renewed } : {}
299404
300541
  };
299405
300542
  }).pipe(require_Schema$1.mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
299406
300543
  message: "Failed to verify session credential.",
@@ -299893,7 +301030,10 @@ const makeServerAuth = require_Schema$1.gen(function* () {
299893
301030
  subject: session.subject,
299894
301031
  method: session.method,
299895
301032
  role: session.role,
299896
- ...session.expiresAt ? { expiresAt: session.expiresAt } : {}
301033
+ ...session.renewed ? {
301034
+ expiresAt: session.renewed.expiresAt,
301035
+ renewedSessionToken: session.renewed.token
301036
+ } : session.expiresAt ? { expiresAt: session.expiresAt } : {}
299897
301037
  })), require_Schema$1.mapError((cause) => new AuthError({
299898
301038
  message: "Unauthorized request.",
299899
301039
  status: 401,
@@ -299928,6 +301068,7 @@ const makeServerAuth = require_Schema$1.gen(function* () {
299928
301068
  role: session.role,
299929
301069
  sessionMethod: session.method,
299930
301070
  ...session.expiresAt ? { expiresAt: require_Schema$1.toUtc(session.expiresAt) } : {},
301071
+ ...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
299931
301072
  cmmdIdentity
299932
301073
  })))), require_Schema$1.tapError((e) => {
299933
301074
  if (e._tag === "AuthError" && (e.status ?? 500) < 500) return require_Schema$1.void_;
@@ -306307,6 +307448,82 @@ const orchestrationAutomationTerminalReconcileRouteLayer = add$1("POST", "/api/o
306307
307448
  }, { status: 200 });
306308
307449
  }).pipe(require_Schema$1.catchTag("OrchestrationDispatchCommandError", respondToWorkRunEventError)));
306309
307450
  //#endregion
307451
+ //#region src/orchestration/threadSnapshotRoute.ts
307452
+ /**
307453
+ * `GET /api/orchestration/threads/:threadId`, one thread's detail snapshot,
307454
+ * optionally windowed to a page of recent turns.
307455
+ *
307456
+ * Forge never served this route. The mobile client has asked for it on every
307457
+ * thread open since it shipped, fell through to the socket-embedded snapshot on
307458
+ * the 404, and its "load earlier" control had nowhere to fetch from, so a long
307459
+ * thread was only ever loadable in full. Serving it here gives the initial read
307460
+ * a body the transport can gzip, off the RPC socket, and gives older turns a
307461
+ * cursor to arrive on.
307462
+ *
307463
+ * The window is applied by the same pure function the WebSocket fallback
307464
+ * snapshot uses, so the two reads cannot disagree about what a page is.
307465
+ */
307466
+ /**
307467
+ * Reads the window out of the query string.
307468
+ *
307469
+ * Returns null for a malformed value rather than ignoring it. A client that
307470
+ * asked for 10 turns and silently received 4,000 has no way to tell, and the
307471
+ * failure would show up as a slow app rather than as an error.
307472
+ */
307473
+ function parseThreadSnapshotWindow(searchParams) {
307474
+ const rawTurnLimit = searchParams.get("turnLimit");
307475
+ const rawBeforeCursor = searchParams.get("beforeCursor");
307476
+ let turnLimit;
307477
+ if (rawTurnLimit !== null) {
307478
+ const parsed = Number(rawTurnLimit);
307479
+ if (!Number.isSafeInteger(parsed) || parsed < 1) return null;
307480
+ turnLimit = parsed;
307481
+ }
307482
+ let beforeCursor;
307483
+ if (rawBeforeCursor !== null) {
307484
+ const trimmed = rawBeforeCursor.trim();
307485
+ if (trimmed.length === 0) return null;
307486
+ beforeCursor = trimmed;
307487
+ }
307488
+ return {
307489
+ ...turnLimit === void 0 ? {} : { turnLimit },
307490
+ ...beforeCursor === void 0 ? {} : { beforeCursor }
307491
+ };
307492
+ }
307493
+ const orchestrationThreadSnapshotRouteLayer = add$1("GET", "/api/orchestration/threads/:threadId", require_Schema$1.gen(function* () {
307494
+ const request = yield* require_HttpServer.HttpServerRequest;
307495
+ yield* (yield* ServerAuth).authenticateHttpRequest(request);
307496
+ const rawThreadId = (yield* params)["threadId"];
307497
+ if (rawThreadId === void 0 || rawThreadId.length === 0) return require_HttpServer.jsonUnsafe({ error: "Missing thread id." }, { status: 400 });
307498
+ const url = require_HttpServer.toURL(request);
307499
+ const window = require_Schema$1.isNone(url) ? {} : parseThreadSnapshotWindow(url.value.searchParams);
307500
+ if (window === null) return require_HttpServer.jsonUnsafe({ error: "Invalid turnLimit or beforeCursor." }, { status: 400 });
307501
+ const threadId = ThreadId.make(decodeURIComponent(rawThreadId));
307502
+ const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
307503
+ const orchestrationEngine = yield* OrchestrationEngineService;
307504
+ const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
307505
+ message: `Failed to load thread ${threadId}`,
307506
+ cause
307507
+ }))), orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
307508
+ if (require_Schema$1.isNone(threadDetail)) return require_HttpServer.jsonUnsafe({ error: "Thread not found." }, { status: 404 });
307509
+ const windowed = windowThreadDetail({
307510
+ thread: threadDetail.value,
307511
+ window,
307512
+ snapshotSequence
307513
+ });
307514
+ return require_HttpServer.jsonUnsafe({
307515
+ snapshotSequence,
307516
+ thread: windowed.thread,
307517
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
307518
+ }, { status: 200 });
307519
+ }).pipe(require_Schema$1.catchTag("AuthError", respondToAuthError), require_Schema$1.catchTag("OrchestrationGetSnapshotError", (error) => require_Schema$1.gen(function* () {
307520
+ yield* require_Schema$1.logError("thread snapshot route failed", {
307521
+ message: error.message,
307522
+ cause: error.cause
307523
+ });
307524
+ return require_HttpServer.jsonUnsafe({ error: error.message }, { status: 500 });
307525
+ }))));
307526
+ //#endregion
306310
307527
  //#region src/localAutomations/scheduling.ts
306311
307528
  const MINUTE_MS = 6e4;
306312
307529
  const HOUR_MS = 60 * MINUTE_MS;
@@ -318217,7 +319434,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
318217
319434
  const sql = yield* require_SqlError.SqlClient;
318218
319435
  const fetchFn = options.fetchFn ?? fetch;
318219
319436
  const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
318220
- const resolveStoredJwt = (forceRefresh) => require_Schema$1.gen(function* () {
319437
+ const resolveStoredJwt = (forceRefresh, staleJwt) => require_Schema$1.gen(function* () {
318221
319438
  const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql), require_Schema$1.mapError((cause) => new ProjectLeadControllerClientError({
318222
319439
  kind: "unavailable",
318223
319440
  operation: "resolve-owner-session",
@@ -318231,7 +319448,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
318231
319448
  sessionId: owner.sessionId,
318232
319449
  subject: owner.subject,
318233
319450
  logPrefix: "project-lead-scheduler",
318234
- forceRefresh
319451
+ forceRefresh,
319452
+ staleAccessToken: staleJwt
318235
319453
  }).pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql));
318236
319454
  if (!jwt) return yield* new ProjectLeadControllerClientError({
318237
319455
  kind: "unauthenticated",
@@ -318274,7 +319492,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
318274
319492
  }));
318275
319493
  if (first._tag === "Success") return first.success;
318276
319494
  if (first.failure.kind !== "unauthenticated") return yield* first.failure;
318277
- const refreshed = yield* resolveJwt(true);
319495
+ const refreshed = yield* resolveJwt(true, jwt);
318278
319496
  return yield* requestAttempt({
318279
319497
  ...input,
318280
319498
  jwt: refreshed
@@ -320681,12 +321899,12 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", require_
320681
321899
  //#region src/server.ts
320682
321900
  const PtyAdapterLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320683
321901
  if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunPTY-uyreNRGp.cjs")))).layer;
320684
- else return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodePTY-UOaRyQsj.cjs")))).layer;
321902
+ else return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodePTY-BehazUMi.cjs")))).layer;
320685
321903
  }));
320686
321904
  const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320687
321905
  const config = yield* ServerConfig;
320688
- if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-C7-1JJ7b.cjs")))).layer(bunHttpServerOptions(config));
320689
- const [NodeHttpServer, NodeHttp] = yield* require_Schema$1.all([require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeHttpServer-Bi339XFk.cjs"))), require_Schema$1.promise(() => import("node:http"))]);
321906
+ if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-C0oPP_3A.cjs")))).layer(bunHttpServerOptions(config));
321907
+ const [NodeHttpServer, NodeHttp] = yield* require_Schema$1.all([require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeHttpServer-Cjd3bFrD.cjs"))), require_Schema$1.promise(() => import("node:http"))]);
320690
321908
  return NodeHttpServer.layer(NodeHttp.createServer, {
320691
321909
  host: config.host,
320692
321910
  port: config.port
@@ -320694,10 +321912,10 @@ const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* ()
320694
321912
  }));
320695
321913
  const PlatformServicesLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320696
321914
  if (typeof Bun !== "undefined") {
320697
- const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-Dl-3897e.cjs")));
321915
+ const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-DkenwD7S.cjs")));
320698
321916
  return layer;
320699
321917
  } else {
320700
- const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-R0BZsZ72.cjs")));
321918
+ const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-BvdIc9Vh.cjs")));
320701
321919
  return layer;
320702
321920
  }
320703
321921
  }));
@@ -320764,7 +321982,7 @@ const MiscRuntimeLayerLive = require_Schema$1.mergeAll(AnalyticsServiceLayerLive
320764
321982
  const RuntimeDependenciesWithoutPullRequestsLive = ReactorLayerLive.pipe(require_Schema$1.provideMerge(CheckpointingLayerLive), require_Schema$1.provideMerge(GitLayerLive), require_Schema$1.provideMerge(OrchestrationLayerLive), require_Schema$1.provideMerge(ProviderLayerLive), require_Schema$1.provideMerge(TerminalLayerLive), require_Schema$1.provideMerge(AgentBrowserManagerLive), require_Schema$1.provideMerge(require_Schema$1.mergeAll(ThreadDevRuntimeLive, DevTunnelLive.pipe(require_Schema$1.provide$1(ThreadDevRuntimeLive)))), require_Schema$1.provideMerge(PersistenceLayerLive), require_Schema$1.provideMerge(KeybindingsLive), require_Schema$1.provideMerge(ProviderRegistryLive), require_Schema$1.provideMerge(require_Schema$1.mergeAll(ServerSettingsLive, UsageServiceLive.pipe(require_Schema$1.provide$1(ServerSettingsLive), require_Schema$1.provide$1(UsageSessionLookupLive), require_Schema$1.provide$1(UsageRecordedSourceLive.pipe(require_Schema$1.provide$1(OrchestrationLayerLive), require_Schema$1.provide$1(RuntimeReplicationOutboxLive), require_Schema$1.provide$1(ProviderSessionRuntimeRepositoryLive), require_Schema$1.provide$1(TextGenerationSessionRepositoryLive)))))), require_Schema$1.provideMerge(WorkspaceLayerLive), require_Schema$1.provideMerge(WorkspaceReadinessStateLive), require_Schema$1.provideMerge(ProjectFaviconResolverLive), require_Schema$1.provideMerge(RepositoryIdentityResolverLive), require_Schema$1.provideMerge(ServerEnvironmentLive), require_Schema$1.provideMerge(AuthLayerLive), require_Schema$1.provideMerge(ServerSecretStoreLayerLive), require_Schema$1.provideMerge(require_Schema$1.mergeAll(CloudRedisCoordinationLive, CloudStateSyncLive.pipe(require_Schema$1.provide$1(PersistenceLayerLive)))), require_Schema$1.provideMerge(MiscRuntimeLayerLive));
320765
321983
  const RuntimeDependenciesLive = pullRequestServiceFactoryLayer.pipe(require_Schema$1.provideMerge(RuntimeDependenciesWithoutPullRequestsLive), require_Schema$1.provideMerge(cmmdProjectIdentityResolverLive.pipe(require_Schema$1.provide$1(RuntimeDependenciesWithoutPullRequestsLive))), require_Schema$1.provideMerge(pullRequestProviderRuntimeLive));
320766
321984
  const RuntimeServicesLive = ServerRuntimeStartupLive.pipe(require_Schema$1.provideMerge(RuntimeDependenciesLive));
320767
- const makeRoutesLayer = require_Schema$1.mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(require_Schema$1.provide$1(browserApiCorsLayer));
321985
+ const makeRoutesLayer = require_Schema$1.mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRenewRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, orchestrationThreadSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(require_Schema$1.provide$1(browserApiCorsLayer));
320768
321986
  const makeServerLayer = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320769
321987
  const config = yield* ServerConfig;
320770
321988
  fixPath({ preferProcessPath: config.forgeCmmdRuntimeMode === true });