@cmmd-center/forge 0.13.11 → 0.13.12

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 (50) 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 +1034 -199
  27. package/dist/bin.mjs +1029 -202
  28. package/dist/client/assets/{CanvasPanel-z0F-jdf1.js → CanvasPanel-BjHxCGlb.js} +2 -2
  29. package/dist/client/assets/{CompositeItem-P9UVXCKv.js → CompositeItem-D4LER3__.js} +2 -2
  30. package/dist/client/assets/{DiffPanel-BrlSm5kH.js → DiffPanel-D3btZGjW.js} +2 -2
  31. package/dist/client/assets/{DiffPanel.logic-OZhO8GfQ.js → DiffPanel.logic-DWcwE9Wj.js} +2 -2
  32. package/dist/client/assets/{DiffPanelShell-DnugngHM.js → DiffPanelShell-oXHh--ae.js} +109 -109
  33. package/dist/client/assets/{DiffWorkerPoolProvider-BOD7cQPe.js → DiffWorkerPoolProvider-R9YPaaNy.js} +2 -2
  34. package/dist/client/assets/{PullRequestCodePanel-DACYpcbJ.js → PullRequestCodePanel-DTCc2TBl.js} +2 -2
  35. package/dist/client/assets/{Virtualizer-R_LG4CvG.js → Virtualizer-arKGxq_z.js} +2 -2
  36. package/dist/client/assets/{canvasStateStore-CdQxd2aO.js → canvasStateStore-B5zd1u9r.js} +2 -2
  37. package/dist/client/assets/cmmdAuthRedirect-DeFAIQpj.js +2 -0
  38. package/dist/client/assets/{index-BXzYKPmD.js → index-C7YKDHIe.js} +58 -58
  39. package/dist/client/assets/{popover-CUSlb3eK.js → popover-DM7IA0zJ.js} +2 -2
  40. package/dist/client/assets/src-LDQtPKQt.js +3 -0
  41. package/dist/client/assets/{toggle-BREV66nT.js → toggle-DVtS0BJJ.js} +2 -2
  42. package/dist/client/index.html +10 -10
  43. package/dist/forge-build.json +1 -1
  44. package/dist/runtimeBootstrap.cjs +1 -1
  45. package/dist/runtimeBootstrap.mjs +1 -1
  46. package/dist/{runtimeControlAuthority-B3kNmYRT.cjs → runtimeControlAuthority-BkVB0uAA.cjs} +17 -1
  47. package/dist/{runtimeControlAuthority-D_zKr_eI.mjs → runtimeControlAuthority-E_kdeD_P.mjs} +12 -2
  48. package/package.json +1 -1
  49. package/dist/client/assets/cmmdAuthRedirect-Bp1uQaQ9.js +0 -2
  50. 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);
@@ -17920,10 +17920,32 @@ const OrchestrationShellStreamItem = require_Schema$1.Union([require_Schema$1.St
17920
17920
  kind: require_Schema$1.Literal("snapshot"),
17921
17921
  snapshot: OrchestrationShellSnapshot
17922
17922
  }), OrchestrationShellStreamEvent]);
17923
- const OrchestrationSubscribeThreadInput = require_Schema$1.Struct({ threadId: ThreadId });
17923
+ const OrchestrationSubscribeThreadInput = require_Schema$1.Struct({
17924
+ threadId: ThreadId,
17925
+ afterSequence: require_Schema$1.optionalKey(NonNegativeInt),
17926
+ requestCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
17927
+ turnLimit: require_Schema$1.optionalKey(PositiveInt)
17928
+ });
17929
+ require_Schema$1.Struct({
17930
+ turnLimit: require_Schema$1.optionalKey(PositiveInt),
17931
+ beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
17932
+ });
17933
+ /**
17934
+ * Page metadata for a windowed thread detail read. `beforeCursor` is opaque and
17935
+ * exclusive: passing it back returns the adjacent slice of older turns. `null`
17936
+ * means nothing older is left. `snapshotSequence` mirrors the top-level
17937
+ * sequence so a history page can be sequence-checked against live state before
17938
+ * a client merges it.
17939
+ */
17940
+ const OrchestrationThreadDetailPage = require_Schema$1.Struct({
17941
+ beforeCursor: require_Schema$1.NullOr(TrimmedNonEmptyString),
17942
+ hasMore: require_Schema$1.Boolean,
17943
+ snapshotSequence: NonNegativeInt
17944
+ });
17924
17945
  const OrchestrationThreadDetailSnapshot = require_Schema$1.Struct({
17925
17946
  snapshotSequence: NonNegativeInt,
17926
- thread: OrchestrationThread
17947
+ thread: OrchestrationThread,
17948
+ page: require_Schema$1.optional(OrchestrationThreadDetailPage)
17927
17949
  });
17928
17950
  const ProjectCreateCommand = require_Schema$1.Struct({
17929
17951
  type: require_Schema$1.Literal("project.create"),
@@ -18747,13 +18769,17 @@ const OrchestrationEvent = require_Schema$1.Union([
18747
18769
  payload: ThreadRateLimitSetPayload$1
18748
18770
  })
18749
18771
  ]);
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
- })]);
18772
+ const OrchestrationThreadStreamItem = require_Schema$1.Union([
18773
+ require_Schema$1.Struct({ kind: require_Schema$1.Literal("synchronized") }),
18774
+ require_Schema$1.Struct({
18775
+ kind: require_Schema$1.Literal("snapshot"),
18776
+ snapshot: OrchestrationThreadDetailSnapshot
18777
+ }),
18778
+ require_Schema$1.Struct({
18779
+ kind: require_Schema$1.Literal("event"),
18780
+ event: OrchestrationEvent
18781
+ })
18782
+ ]);
18757
18783
  const OrchestrationCommandReceiptStatus = require_Schema$1.Literals(["accepted", "rejected"]);
18758
18784
  const TurnCountRange = require_Schema$1.Struct({
18759
18785
  fromTurnCount: NonNegativeInt,
@@ -22978,6 +23004,8 @@ const ServerConfig$1 = require_Schema$1.Struct({
22978
23004
  availableEditors: require_Schema$1.ArraySchema(EditorId),
22979
23005
  shellRevealInFileManager: require_Schema$1.optionalKey(require_Schema$1.Boolean),
22980
23006
  shellRevealInFileManagerKind: require_Schema$1.optionalKey(FileManagerRevealKind),
23007
+ threadResumeCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
23008
+ threadSnapshotPagination: require_Schema$1.optionalKey(require_Schema$1.Boolean),
22981
23009
  observability: ServerObservability,
22982
23010
  settings: ServerSettings
22983
23011
  });
@@ -41486,7 +41514,7 @@ function splitPath(filename) {
41486
41514
  const parts = splitPathRe.exec(truncated);
41487
41515
  return parts ? parts.slice(1) : [];
41488
41516
  }
41489
- function dirname$7(path) {
41517
+ function dirname$8(path) {
41490
41518
  const result = splitPath(path);
41491
41519
  const root = result[0] || "";
41492
41520
  let dir = result[1];
@@ -58801,7 +58829,7 @@ function getSpotlightConfig(optionsSpotlight) {
58801
58829
  function normalizeWindowsPath(path) {
58802
58830
  return path.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
58803
58831
  }
58804
- function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$7(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
58832
+ function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$8(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
58805
58833
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
58806
58834
  return (filename) => {
58807
58835
  if (!filename) return;
@@ -66289,7 +66317,7 @@ function normalizeNumberish(value) {
66289
66317
  }
66290
66318
  //#endregion
66291
66319
  //#region package.json
66292
- var version$1 = "0.13.11";
66320
+ var version$1 = "0.13.12";
66293
66321
  //#endregion
66294
66322
  //#region src/sentry.ts
66295
66323
  const SERVER_APP_NAME = "forge-server";
@@ -66716,6 +66744,29 @@ function accessTokenExpiresInSeconds(remainingMillis) {
66716
66744
  return Math.max(0, Math.floor(remainingMillis / 1e3));
66717
66745
  }
66718
66746
  //#endregion
66747
+ //#region src/auth/commandBootstrapClient.ts
66748
+ const BOOTSTRAP_CLIENT_LABELS = {
66749
+ desktop: "Forge Desktop Cloud",
66750
+ mobile: "Forge Mobile Cloud"
66751
+ };
66752
+ /**
66753
+ * Client metadata for a bearer bootstrap session.
66754
+ *
66755
+ * The declared device type wins over the one inferred from the user agent. A
66756
+ * React Native fetch sends whatever the platform's networking stack sends, and
66757
+ * on iOS that is a CFNetwork string no user-agent heuristic reads as a phone.
66758
+ */
66759
+ function resolveCommandBootstrapClient(input) {
66760
+ const deviceType = input.clientDeviceType ?? "desktop";
66761
+ return {
66762
+ ...deriveAuthClientMetadata({
66763
+ request: input.request,
66764
+ label: BOOTSTRAP_CLIENT_LABELS[deviceType]
66765
+ }),
66766
+ deviceType
66767
+ };
66768
+ }
66769
+ //#endregion
66719
66770
  //#region ../../packages/shared/src/cmmdIdentity.ts
66720
66771
  const MAX_CMMD_AVATAR_URL_LENGTH = 256 * 1024;
66721
66772
  const DATA_IMAGE_AVATAR_PATTERN = /^data:image\/(?:avif|gif|jpeg|jpg|png|webp);base64,[a-z0-9+/=]+$/i;
@@ -67169,14 +67220,26 @@ function extractJwtExpiresAt(jwtToken) {
67169
67220
  }
67170
67221
  }
67171
67222
  /**
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.
67223
+ * How long before `exp` a stored JWT is already treated as spent.
67224
+ *
67225
+ * Comparing against `exp` exactly leaves a window where Forge sends a token it
67226
+ * believes is good and CMMD has already stopped accepting it: the clocks differ by a
67227
+ * little, the request spends time in flight, and the answer comes back 401 on a
67228
+ * session that had a perfectly good refresh token sitting next to it. Two
67229
+ * minutes is far inside the JWT's own lifetime, so this costs one early
67230
+ * rotation and removes the race.
67231
+ */
67232
+ const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
67233
+ /**
67234
+ * Returns true when a stored JWT is at or near its expiry, or has no decodable
67235
+ * expiry. CMMD REST JWTs are short-lived bearer tokens, so unknown expiry is
67236
+ * not safe.
67174
67237
  */
67175
67238
  function isJwtExpiredForSession(sessionId) {
67176
67239
  if (!jwtCache.has(sessionId)) return false;
67177
67240
  const expiresAt = jwtExpiryCache.get(sessionId);
67178
67241
  if (!expiresAt) return true;
67179
- return new Date(expiresAt) <= /* @__PURE__ */ new Date();
67242
+ return new Date(expiresAt).getTime() - JWT_EXPIRY_SKEW_MS <= Date.now();
67180
67243
  }
67181
67244
  /**
67182
67245
  * Hydrate the in-memory cache from the configured persistence layer. Call once at server startup
@@ -67732,10 +67795,27 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId) => require_Schema$1.gen
67732
67795
  * Retire refresh credentials copied into historical desktop bearer sessions.
67733
67796
  * Electron owns rotation for native desktop OAuth; Forge Local keeps only the
67734
67797
  * 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;
67798
+ *
67799
+ * Scoped to the bootstrapping client, not to the whole subject. The phone and
67800
+ * Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
67801
+ * desktop launch null out a phone's refresh token and end its session a day
67802
+ * later. Two clauses keep that from happening:
67803
+ *
67804
+ * - `client_device_type = 'desktop'`, which now excludes phones outright,
67805
+ * because the mobile app declares itself at bootstrap.
67806
+ * - the same recorded `client_user_agent` as the bootstrap doing the
67807
+ * retirement, which is the only thing that separates a phone row written
67808
+ * before that stamp existed from a real Electron row. Both are stamped
67809
+ * `desktop`; neither carries any other mark. An empty or unmatched user agent
67810
+ * retires nothing, which is the direction that keeps a live credential.
67811
+ *
67812
+ * Electron re-bootstraps under a stable user agent, so its own historical rows
67813
+ * still match and still get swept.
67814
+ */
67815
+ const clearDesktopBearerRefreshTokensForSubject = (input) => require_Schema$1.gen(function* () {
67816
+ const normalizedSubject = input.subject.trim();
67817
+ const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
67818
+ if (!normalizedSubject || !normalizedUserAgent) return;
67739
67819
  const rows = yield* (yield* require_SqlError.SqlClient)`
67740
67820
  UPDATE command_access_tokens
67741
67821
  SET
@@ -67748,6 +67828,7 @@ const clearDesktopBearerRefreshTokensForSubject = (subject) => require_Schema$1.
67748
67828
  AND role = 'owner'
67749
67829
  AND method = 'bearer-session-token'
67750
67830
  AND client_device_type = 'desktop'
67831
+ AND client_user_agent = ${normalizedUserAgent}
67751
67832
  )
67752
67833
  RETURNING session_id AS "sessionId"
67753
67834
  `.pipe(require_Schema$1.mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
@@ -67770,6 +67851,17 @@ const OAUTH_STATE_COOKIE = "forge_oauth_state";
67770
67851
  const OAUTH_VERIFIER_COOKIE = "forge_oauth_verifier";
67771
67852
  //#endregion
67772
67853
  //#region src/auth/http/cloudOwnerOAuth.ts
67854
+ const FORGE_CLOUD_SESSION_GRANT_COOKIE = "forge_cloud_session_grant";
67855
+ function resolveCloudOwnerSessionGrantCookie(input) {
67856
+ const token = input.token?.trim();
67857
+ if (!token || !input.expiresAt) return null;
67858
+ const expiresAt = new Date(input.expiresAt);
67859
+ if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= Date.now()) return null;
67860
+ return {
67861
+ token,
67862
+ expiresAt
67863
+ };
67864
+ }
67773
67865
  function validateCloudOwnerCallbackAssertion(input) {
67774
67866
  if (input.serverAuth.isForgeCloudShell?.() !== true) return require_Schema$1.succeed(null);
67775
67867
  if (!input.assertionToken || !input.serverAuth.verifyForgeCloudOwnerAssertion) return require_Schema$1.fail(new AuthError({
@@ -67833,8 +67925,19 @@ function cloudOwnerAssertionCookieOptions(assertion) {
67833
67925
  secure: true
67834
67926
  };
67835
67927
  }
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)));
67928
+ function cloudOwnerSessionGrantCookieOptions(grant) {
67929
+ return {
67930
+ expires: grant.expiresAt,
67931
+ httpOnly: true,
67932
+ path: "/api/auth",
67933
+ sameSite: "lax",
67934
+ secure: true
67935
+ };
67936
+ }
67937
+ function cloudOwnerOAuthResponse(homeUrl, assertion, sessionGrant) {
67938
+ let response = require_HttpServer.redirect(homeUrl, { status: 302 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
67939
+ if (sessionGrant) response = response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
67940
+ 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
67941
  }
67839
67942
  //#endregion
67840
67943
  //#region src/auth/http.ts
@@ -67884,6 +67987,56 @@ const authSessionRouteLayer = add$1("GET", "/api/auth/session", require_Schema$1
67884
67987
  }));
67885
67988
  return response;
67886
67989
  }));
67990
+ const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", require_Schema$1.gen(function* () {
67991
+ const request = yield* require_HttpServer.HttpServerRequest;
67992
+ const serverAuth = yield* ServerAuth;
67993
+ if (serverAuth.isForgeCloudShell?.() !== true) return yield* new AuthError({
67994
+ message: "Session renewal is unavailable.",
67995
+ status: 401
67996
+ });
67997
+ const sessionGrant = request.cookies[FORGE_CLOUD_SESSION_GRANT_COOKIE]?.trim();
67998
+ if (!sessionGrant) return yield* new AuthError({
67999
+ message: "Session renewal is unavailable.",
68000
+ status: 401
68001
+ });
68002
+ const credentials = yield* resolveCmmdTokenBundleCredentials(yield* readCmmdJsonResponse(yield* require_Schema$1.tryPromise({
68003
+ try: () => fetch(resolveCommandAppEndpoint("/api/mcp-server/oauth/token"), {
68004
+ method: "POST",
68005
+ headers: { "content-type": "application/json" },
68006
+ body: JSON.stringify({
68007
+ grant_type: "forge_session_grant",
68008
+ grant_token: sessionGrant,
68009
+ client_id: "cmmd-forge-web"
68010
+ })
68011
+ }),
68012
+ catch: (cause) => new AuthError({
68013
+ message: "Failed to renew the Forge Cloud session with CMMD.",
68014
+ status: 500,
68015
+ cause
68016
+ })
68017
+ }), "Failed to renew the Forge Cloud session with CMMD."));
68018
+ const { cmmdIdentity, userId, patToStore } = credentials;
68019
+ if (!cmmdIdentity || !userId || !patToStore) return yield* new AuthError({
68020
+ message: "CMMD did not return a complete renewed Forge session.",
68021
+ status: 500
68022
+ });
68023
+ const assertion = yield* validateCloudOwnerCallbackAssertion({
68024
+ serverAuth,
68025
+ assertionToken: credentials.forgeOwnerAssertion,
68026
+ authenticatedUserId: userId
68027
+ });
68028
+ if (!assertion) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a renewed Forge Cloud owner assertion."));
68029
+ yield* persistCloudOwnerArtifacts({
68030
+ assertion,
68031
+ accessToken: patToStore,
68032
+ identity: cmmdIdentity,
68033
+ jwtToken: credentials.jwtToStore,
68034
+ refreshToken: credentials.refreshToken,
68035
+ sessionGrant,
68036
+ sessionGrantExpiresAt: void 0
68037
+ });
68038
+ return yield* require_HttpServer.jsonUnsafe({ renewed: true }, { status: 200 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
68039
+ }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
67887
68040
  const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", require_Schema$1.gen(function* () {
67888
68041
  const request = yield* require_HttpServer.HttpServerRequest;
67889
68042
  const serverAuth = yield* ServerAuth;
@@ -67906,14 +68059,24 @@ const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", require_Schema$1.
67906
68059
  sameSite: "lax",
67907
68060
  secure
67908
68061
  }));
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
- }));
68062
+ if (cloudShell) {
68063
+ response = yield* response.pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, "", {
68064
+ expires: /* @__PURE__ */ new Date(0),
68065
+ httpOnly: true,
68066
+ maxAge: 0,
68067
+ path: "/",
68068
+ sameSite: "lax",
68069
+ secure: true
68070
+ }));
68071
+ response = yield* response.pipe(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, "", {
68072
+ expires: /* @__PURE__ */ new Date(0),
68073
+ httpOnly: true,
68074
+ maxAge: 0,
68075
+ path: "/api/auth",
68076
+ sameSite: "lax",
68077
+ secure: true
68078
+ }));
68079
+ }
67917
68080
  response = yield* pipeExpireLegacySessionCookie(sessions.cookieName, require_Schema$1.succeed(response), { secure });
67918
68081
  if (sessions.cookieName !== "forge_session") response = yield* response.pipe(require_HttpServer.setCookie(BASE_SESSION_COOKIE_NAME, "", {
67919
68082
  expires: /* @__PURE__ */ new Date(0),
@@ -67936,7 +68099,8 @@ const CommandBootstrapInput = require_Schema$1.Struct({
67936
68099
  refreshToken: require_Schema$1.optionalKey(require_Schema$1.String),
67937
68100
  userId: require_Schema$1.optionalKey(require_Schema$1.Number),
67938
68101
  displayName: require_Schema$1.optionalKey(require_Schema$1.String),
67939
- user: require_Schema$1.optionalKey(require_Schema$1.Unknown)
68102
+ user: require_Schema$1.optionalKey(require_Schema$1.Unknown),
68103
+ clientDeviceType: require_Schema$1.optionalKey(require_Schema$1.Literals(["desktop", "mobile"]))
67940
68104
  });
67941
68105
  const CommandOAuthDeviceInput = require_Schema$1.Struct({
67942
68106
  client_id: require_Schema$1.String,
@@ -68484,13 +68648,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68484
68648
  method: "bearer-session-token",
68485
68649
  role: "owner",
68486
68650
  subject: sessionSubject,
68487
- client: {
68488
- ...deriveAuthClientMetadata({
68489
- request,
68490
- label: "Forge Desktop Cloud"
68491
- }),
68492
- deviceType: "desktop"
68493
- }
68651
+ client: resolveCommandBootstrapClient({
68652
+ request,
68653
+ clientDeviceType: payload.clientDeviceType
68654
+ })
68494
68655
  }).pipe(require_Schema$1.mapError((cause) => new AuthError({
68495
68656
  message: "Failed to issue authenticated bearer session.",
68496
68657
  cause
@@ -68500,7 +68661,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68500
68661
  payload,
68501
68662
  cmmdIdentity: validated.cmmdIdentity
68502
68663
  });
68503
- if (!payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject(sessionSubject).pipe(require_Schema$1.mapError((cause) => new AuthError({
68664
+ if ((payload.clientDeviceType ?? "desktop") === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
68665
+ subject: sessionSubject,
68666
+ clientUserAgent: request.headers["user-agent"]
68667
+ }).pipe(require_Schema$1.mapError((cause) => new AuthError({
68504
68668
  message: "Failed to retire legacy desktop refresh credentials.",
68505
68669
  status: 500,
68506
68670
  cause
@@ -68568,6 +68732,14 @@ function completeCloudOwnerPasswordLogin(input) {
68568
68732
  authenticatedUserId: userId
68569
68733
  });
68570
68734
  if (!assertion) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a Forge Cloud owner assertion in cloud shell mode."));
68735
+ const sessionGrant = resolveCloudOwnerSessionGrantCookie({
68736
+ token: credentials.sessionGrant,
68737
+ expiresAt: credentials.sessionGrantExpiresAt
68738
+ });
68739
+ if (!sessionGrant) return yield* new AuthError({
68740
+ message: "CMMD did not return a durable Forge Cloud session grant.",
68741
+ status: 500
68742
+ });
68571
68743
  yield* persistCloudOwnerArtifacts({
68572
68744
  assertion,
68573
68745
  accessToken: patToStore,
@@ -68577,10 +68749,12 @@ function completeCloudOwnerPasswordLogin(input) {
68577
68749
  sessionGrant: credentials.sessionGrant,
68578
68750
  sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
68579
68751
  });
68580
- return yield* require_HttpServer.jsonUnsafe({
68752
+ let response = require_HttpServer.jsonUnsafe({
68581
68753
  authenticated: true,
68582
68754
  cmmdIdentity
68583
68755
  }, { status: 200 }).pipe(require_HttpServer.setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
68756
+ if (sessionGrant) response = response.pipe(require_Schema$1.flatMap(require_HttpServer.setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
68757
+ return yield* response;
68584
68758
  });
68585
68759
  }
68586
68760
  const authCmmdPasswordTokenRouteLayer = add$1("POST", "/api/auth/cmmd-password/token", require_Schema$1.gen(function* () {
@@ -68754,6 +68928,14 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
68754
68928
  assertionToken: forgeOwnerAssertion,
68755
68929
  authenticatedUserId: userId
68756
68930
  });
68931
+ const cloudSessionGrant = cloudOwnerAssertion ? resolveCloudOwnerSessionGrantCookie({
68932
+ token: sessionGrant,
68933
+ expiresAt: sessionGrantExpiresAt
68934
+ }) : null;
68935
+ if (cloudOwnerAssertion && !cloudSessionGrant) return yield* new AuthError({
68936
+ message: "CMMD did not return a durable Forge Cloud session grant.",
68937
+ status: 500
68938
+ });
68757
68939
  const sessionSubject = resolveCommandOAuthSubject(userId);
68758
68940
  const issued = cloudOwnerAssertion ? null : yield* sessions.issue({
68759
68941
  method: "browser-session-cookie",
@@ -68785,7 +68967,7 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
68785
68967
  else yield* persistLocalOAuthArtifacts(persistenceInput);
68786
68968
  const homeUrl = `${publicUrl}/`;
68787
68969
  const secureCookie = shouldUseSecureCookie(publicUrl);
68788
- if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion);
68970
+ if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion, cloudSessionGrant);
68789
68971
  if (!issued) return yield* require_Schema$1.die(/* @__PURE__ */ new Error("Expected a local session outside Forge Cloud."));
68790
68972
  return yield* pipeExpireLegacySessionCookie(sessions.cookieName, require_HttpServer.redirect(homeUrl, { status: 302 }).pipe(require_HttpServer.setCookie(sessions.cookieName, issued.token, {
68791
68973
  expires: require_Schema$1.toDate(issued.expiresAt),
@@ -70829,6 +71011,11 @@ function redeemCmmdSessionGrant(grantToken) {
70829
71011
  };
70830
71012
  }).catch(() => null);
70831
71013
  }
71014
+ const REJECTED_OAUTH_ERROR_CODES = new Set([
71015
+ "invalid_grant",
71016
+ "invalid_client",
71017
+ "invalid_request"
71018
+ ]);
70832
71019
  /**
70833
71020
  * Rotate a CMMD refresh token into a fresh access JWT.
70834
71021
  *
@@ -70841,7 +71028,7 @@ function redeemCmmdSessionGrant(grantToken) {
70841
71028
  * code are what identify the failure, and `invalid_grant` in particular means
70842
71029
  * the token was already spent, which is a different fix from a 500.
70843
71030
  */
70844
- function refreshCmmdJwt(refreshToken) {
71031
+ function refreshCmmdJwt(refreshToken, observer) {
70845
71032
  return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
70846
71033
  method: "POST",
70847
71034
  headers: { "Content-Type": "application/json" },
@@ -70852,7 +71039,9 @@ function refreshCmmdJwt(refreshToken) {
70852
71039
  })
70853
71040
  }).then(async (response) => {
70854
71041
  if (!response.ok) {
70855
- console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${await readOAuthError(response)}`);
71042
+ const errorCode = await readOAuthErrorCode(response);
71043
+ if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
71044
+ console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
70856
71045
  return null;
70857
71046
  }
70858
71047
  const parsed = parseCmmdOAuthRefreshResponse(await response.json());
@@ -70864,20 +71053,73 @@ function refreshCmmdJwt(refreshToken) {
70864
71053
  });
70865
71054
  }
70866
71055
  /**
70867
- * The OAuth `error` code from a refusal, as a loggable suffix.
71056
+ * The OAuth `error` code from a refusal, lowercased, or null when there is none.
70868
71057
  *
70869
71058
  * 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.
71059
+ * not turn a logged failure into an unlogged one. Null also reads correctly as
71060
+ * "no evidence the credential is dead", which is the safe answer for a caller
71061
+ * deciding whether to keep a refresh token.
70871
71062
  */
70872
- async function readOAuthError(response) {
71063
+ async function readOAuthErrorCode(response) {
70873
71064
  try {
70874
71065
  const code = (await response.json())["error"];
70875
- return typeof code === "string" && code.trim().length > 0 ? ` (${code.trim()})` : "";
71066
+ return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
70876
71067
  } catch {
70877
- return "";
71068
+ return null;
70878
71069
  }
70879
71070
  }
70880
71071
  //#endregion
71072
+ //#region src/command/cmmdRotationFence.ts
71073
+ /**
71074
+ * True when Forge must not rotate this session's CMMD credentials.
71075
+ *
71076
+ * Fails closed: if the session row cannot be read, assume the fence applies.
71077
+ * Spending a token Forge had no right to spend is worse than one extra
71078
+ * reconnect.
71079
+ */
71080
+ function isRotationFencedSession(sessionId) {
71081
+ if (readCommandRefreshTokenForSession(sessionId)) return require_Schema$1.succeed(false);
71082
+ return isDesktopBearerAuthSession(sessionId).pipe(require_Schema$1.orElseSucceed(() => true));
71083
+ }
71084
+ //#endregion
71085
+ //#region src/cmmdConnection/cmmdCredentialRejection.ts
71086
+ /**
71087
+ * How long a recorded rejection stays usable.
71088
+ *
71089
+ * Long enough to span the resolver ladder and the response that follows it,
71090
+ * short enough that it cannot outlive the credential it describes.
71091
+ */
71092
+ const REJECTION_TTL_MS = 6e4;
71093
+ const rejectedAt = /* @__PURE__ */ new Map();
71094
+ /** Record that CMMD refused this session's credential outright. */
71095
+ function noteCmmdCredentialRejected(sessionId) {
71096
+ rejectedAt.set(sessionId, Date.now());
71097
+ }
71098
+ /** Whether CMMD refused this session's credential recently enough to act on. */
71099
+ function hasCmmdCredentialRejection(sessionId) {
71100
+ const observedAt = rejectedAt.get(sessionId);
71101
+ if (observedAt === void 0) return false;
71102
+ if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
71103
+ rejectedAt.delete(sessionId);
71104
+ return false;
71105
+ }
71106
+ /** Drop a recorded rejection, once it has been acted on or superseded. */
71107
+ function forgetCmmdCredentialRejection(sessionId) {
71108
+ rejectedAt.delete(sessionId);
71109
+ }
71110
+ /**
71111
+ * Clear a session's CMMD credentials, but only if CMMD rejected them.
71112
+ *
71113
+ * This is the only door the 401 path may use. A resolver that returned null
71114
+ * because it was not allowed to try is a policy refusal and leaves the stored
71115
+ * credential exactly where it is, so the next request can rotate it.
71116
+ */
71117
+ const clearCmmdAuthAfterCmmdRejection = (sessionId) => require_Schema$1.gen(function* () {
71118
+ if (!hasCmmdCredentialRejection(sessionId)) return;
71119
+ yield* clearUnrecoverableCmmdAuthForSession(sessionId);
71120
+ forgetCmmdCredentialRejection(sessionId);
71121
+ });
71122
+ //#endregion
70881
71123
  //#region src/command/cmmdJwtResolver.ts
70882
71124
  function usableCmmdUserJwt(token) {
70883
71125
  return isCmmdUserAccessJwt(token) ? token : null;
@@ -70890,7 +71132,7 @@ function refreshOnceForSession(sessionId, refreshToken) {
70890
71132
  return require_Schema$1.promise(() => {
70891
71133
  const existing = inFlightRefreshes.get(sessionId);
70892
71134
  if (existing) return existing;
70893
- const pending = refreshCmmdJwt(refreshToken).catch(() => null).finally(() => {
71135
+ const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
70894
71136
  inFlightRefreshes.delete(sessionId);
70895
71137
  });
70896
71138
  inFlightRefreshes.set(sessionId, pending);
@@ -71006,7 +71248,7 @@ function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth",
71006
71248
  const normalizedSubject = subject?.trim() || null;
71007
71249
  if (!normalizedSubject && !forceRefresh) return require_Schema$1.succeed(null);
71008
71250
  return require_Schema$1.gen(function* () {
71009
- if (yield* isDesktopBearerAuthSession(sessionId).pipe(require_Schema$1.orElseSucceed(() => true))) return null;
71251
+ if (yield* isRotationFencedSession(sessionId)) return null;
71010
71252
  if (forceRefresh) {
71011
71253
  const forced = yield* tryOwnCredentialRecovery(sessionId, logPrefix);
71012
71254
  if (forced) return forced;
@@ -71765,15 +72007,25 @@ function clearCmmdAuthAfterFailure(sessionId) {
71765
72007
  })));
71766
72008
  }
71767
72009
  /**
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.
72010
+ * The single "no usable CMMD credential" response: returns the canonical
72011
+ * `CMMD_AUTH_REQUIRED` body. Every prologue path that decides the session has
72012
+ * no way to reach CMMD — a missing PAT, a JWT that fails to resolve, a bespoke
72013
+ * gate elsewhere in http.ts — emits this same response so the Shell only ever
72014
+ * has one `CMMD_AUTH_REQUIRED` body to branch on.
72015
+ *
72016
+ * It used to delete the session's stored credentials on the way out, and that
72017
+ * was the wrong reading of its own argument. Most of the paths that reach here
72018
+ * are refusals, not rejections: the resolver was fenced, the subject was
72019
+ * missing, a gate said no. A phone would arrive holding a refresh token good
72020
+ * for the next three months, be refused by a rule, and have that token deleted
72021
+ * as "unrecoverable". The clear now happens only where CMMD has actually
72022
+ * refused the credential, which `clearCmmdAuthAfterCmmdRejection` checks.
71774
72023
  */
71775
72024
  function respondCmmdConnectionRequired(sessionId) {
71776
- return clearCmmdAuthAfterFailure(sessionId).pipe(require_Schema$1.as(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
72025
+ return clearCmmdAuthAfterCmmdRejection(sessionId).pipe(require_Schema$1.catch_((error) => require_Schema$1.logWarning("Failed to clear CMMD auth that CMMD itself rejected.", {
72026
+ cause: error,
72027
+ sessionId
72028
+ })), require_Schema$1.as(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
71777
72029
  }
71778
72030
  async function dispatchToProxy(method, jwt, cmmdPath, rawBody, orgId, searchParams) {
71779
72031
  return cmmdGateway.request({
@@ -83674,12 +83926,12 @@ const makePullRequestService = require_Schema$1.gen(function* () {
83674
83926
  }));
83675
83927
  const submitReview = (input) => requireProject(input).pipe(require_Schema$1.flatMap((project) => {
83676
83928
  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.");
83929
+ if (!review.verdicts.includes(input.verdict)) return refuse$1(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
83930
+ if (input.comments.length > 0 && !review.inlineComment) return refuse$1("This host cannot comment on a line of a change request.");
83931
+ 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
83932
  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.");
83933
+ if (!viewer.verdicts.includes(input.verdict)) return refuse$1(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
83934
+ 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
83935
  return project.api.submitReview({
83684
83936
  projectId: project.project.id,
83685
83937
  cwd: project.project.workspaceRoot,
@@ -84077,7 +84329,7 @@ const toEntry = (input) => {
84077
84329
  ...input.item.reviewDecision === void 0 || input.item.reviewDecision === null ? {} : { reviewDecision: input.item.reviewDecision }
84078
84330
  };
84079
84331
  };
84080
- const refuse = (detail) => require_Schema$1.fail(new PullRequestOperationError({
84332
+ const refuse$1 = (detail) => require_Schema$1.fail(new PullRequestOperationError({
84081
84333
  operation: "submitReview",
84082
84334
  detail
84083
84335
  }));
@@ -93944,6 +94196,166 @@ const admitNormalizedDispatchCommand = (command, admit) => require_Schema$1.unin
93944
94196
  };
93945
94197
  }));
93946
94198
  //#endregion
94199
+ //#region src/orchestration/threadDetailWindow.ts
94200
+ function earliest(current, candidate) {
94201
+ return current === void 0 || candidate < current ? candidate : current;
94202
+ }
94203
+ /**
94204
+ * Every turn in the thread, oldest first.
94205
+ *
94206
+ * Ordering is by the earliest timestamp across all four windowed collections
94207
+ * rather than by message order alone: a turn can open with an activity (a tool
94208
+ * call before any text) and a checkpoint-only turn has no message at all.
94209
+ * Ties break on turn id so the order is total and a cursor means the same thing
94210
+ * on every read.
94211
+ */
94212
+ function orderThreadTurns(thread) {
94213
+ const anchors = /* @__PURE__ */ new Map();
94214
+ const touch = (turnId, at, isUserMessage) => {
94215
+ if (turnId === null) return;
94216
+ const existing = anchors.get(turnId);
94217
+ anchors.set(turnId, {
94218
+ at: earliest(existing?.at, at),
94219
+ hasUserMessage: (existing?.hasUserMessage ?? false) || isUserMessage
94220
+ });
94221
+ };
94222
+ for (const message of thread.messages) touch(message.turnId, message.createdAt, message.role === "user");
94223
+ for (const activity of thread.activities) touch(activity.turnId, activity.createdAt, false);
94224
+ for (const plan of thread.proposedPlans) touch(plan.turnId, plan.createdAt, false);
94225
+ for (const checkpoint of thread.checkpoints) touch(checkpoint.turnId, checkpoint.completedAt, false);
94226
+ return Array.from(anchors, ([turnId, anchor]) => ({
94227
+ turnId,
94228
+ ...anchor
94229
+ })).toSorted((left, right) => left.at === right.at ? left.turnId.localeCompare(right.turnId) : left.at < right.at ? -1 : 1);
94230
+ }
94231
+ /**
94232
+ * Index of the first turn to include, counting back `turnLimit` user-anchored
94233
+ * turns from the end of `ordered`. Returns 0 when the thread holds fewer user
94234
+ * turns than the limit, which is how a short thread comes back whole.
94235
+ */
94236
+ function windowStartIndex(ordered, turnLimit) {
94237
+ let userTurns = 0;
94238
+ for (let index = ordered.length - 1; index >= 0; index -= 1) {
94239
+ const anchor = ordered[index];
94240
+ if (anchor !== void 0 && anchor.hasUserMessage) {
94241
+ userTurns += 1;
94242
+ if (userTurns === turnLimit) return index;
94243
+ }
94244
+ }
94245
+ return 0;
94246
+ }
94247
+ /**
94248
+ * The turns a windowed read returns.
94249
+ *
94250
+ * `beforeCursor` bounds the read to turns strictly older than a cursor an
94251
+ * earlier read handed out, which is what "load earlier" sends back. A cursor
94252
+ * naming a turn this thread no longer holds, after a revert, say, yields an
94253
+ * empty page rather than the whole thread: returning everything would be a
94254
+ * silent full download at the exact moment the client asked for less.
94255
+ */
94256
+ function selectWindowTurnIds(ordered, window) {
94257
+ const cursorIndex = window.beforeCursor === void 0 ? ordered.length : ordered.findIndex((anchor) => anchor.turnId === window.beforeCursor);
94258
+ if (cursorIndex < 0) return {
94259
+ turnIds: /* @__PURE__ */ new Set(),
94260
+ beforeCursor: null,
94261
+ hasMore: false,
94262
+ includeUnanchored: false
94263
+ };
94264
+ const candidates = ordered.slice(0, cursorIndex);
94265
+ const startIndex = window.turnLimit === void 0 ? 0 : windowStartIndex(candidates, window.turnLimit);
94266
+ const included = candidates.slice(startIndex);
94267
+ const oldest = included[0];
94268
+ return {
94269
+ turnIds: new Set(included.map((anchor) => anchor.turnId)),
94270
+ beforeCursor: oldest?.turnId ?? null,
94271
+ hasMore: startIndex > 0,
94272
+ includeUnanchored: startIndex === 0
94273
+ };
94274
+ }
94275
+ function keepsTurn(selection, turnId) {
94276
+ return turnId === null ? selection.includeUnanchored : selection.turnIds.has(turnId);
94277
+ }
94278
+ /**
94279
+ * Applies a turn window to a full thread detail read.
94280
+ *
94281
+ * A request with neither field is a full read and comes back untouched with no
94282
+ * page, which is exactly what a pre-pagination client already receives. Only
94283
+ * the four turn-scoped collections are filtered; thread metadata, the session,
94284
+ * and `latestTurn` describe the thread as a whole and stay as they are, so a
94285
+ * windowed snapshot still renders a correct header and a correct running state.
94286
+ */
94287
+ function windowThreadDetail(input) {
94288
+ const { thread, window, snapshotSequence } = input;
94289
+ if (window.turnLimit === void 0 && window.beforeCursor === void 0) return {
94290
+ thread,
94291
+ page: void 0
94292
+ };
94293
+ const selection = selectWindowTurnIds(orderThreadTurns(thread), window);
94294
+ return {
94295
+ thread: {
94296
+ ...thread,
94297
+ messages: thread.messages.filter((row) => keepsTurn(selection, row.turnId)),
94298
+ activities: thread.activities.filter((row) => keepsTurn(selection, row.turnId)),
94299
+ proposedPlans: thread.proposedPlans.filter((row) => keepsTurn(selection, row.turnId)),
94300
+ checkpoints: thread.checkpoints.filter((row) => keepsTurn(selection, row.turnId)),
94301
+ ...thread.turnModelSelections === void 0 ? {} : { turnModelSelections: thread.turnModelSelections.filter((row) => keepsTurn(selection, row.turnId)) }
94302
+ },
94303
+ page: {
94304
+ beforeCursor: selection.hasMore ? selection.beforeCursor : null,
94305
+ hasMore: selection.hasMore,
94306
+ snapshotSequence
94307
+ }
94308
+ };
94309
+ }
94310
+ /** Whether a subscriber's cursor can be served by replay rather than a snapshot. */
94311
+ function canResumeFromSequence(input) {
94312
+ if (input.afterSequence > input.currentSequence) return false;
94313
+ return input.currentSequence - input.afterSequence <= (input.maxGap ?? 2e3);
94314
+ }
94315
+ function snapshotCatchUp(snapshot) {
94316
+ return {
94317
+ items: [{
94318
+ kind: "snapshot",
94319
+ snapshot
94320
+ }],
94321
+ watermark: snapshot.snapshotSequence
94322
+ };
94323
+ }
94324
+ function replayCatchUp(events, afterSequence) {
94325
+ return {
94326
+ items: events.map((event) => ({
94327
+ kind: "event",
94328
+ event
94329
+ })),
94330
+ watermark: events.at(-1)?.sequence ?? afterSequence
94331
+ };
94332
+ }
94333
+ /**
94334
+ * Builds the frames that precede live events: either the replayed catch-up or
94335
+ * the snapshot, plus the completion marker when one was asked for.
94336
+ */
94337
+ const buildCatchUp = require_Schema$1.fn("threadSubscription.buildCatchUp")(function* (input, sources) {
94338
+ const afterSequence = input.afterSequence;
94339
+ if (afterSequence === void 0) return snapshotCatchUp(yield* sources.loadSnapshot);
94340
+ if (!canResumeFromSequence({
94341
+ afterSequence,
94342
+ currentSequence: yield* sources.currentSequence
94343
+ })) return snapshotCatchUp(yield* sources.loadSnapshot);
94344
+ const replayed = yield* require_Path.runCollect(sources.replayEvents(afterSequence));
94345
+ return replayCatchUp(Array.from(replayed), afterSequence);
94346
+ });
94347
+ function makeThreadSubscriptionStream(input, sources) {
94348
+ return require_Path.unwrap(require_Schema$1.gen(function* () {
94349
+ const liveEvents = yield* sources.subscribeLiveEvents;
94350
+ const catchUp = yield* buildCatchUp(input, sources);
94351
+ const marker = input.requestCompletionMarker === true ? [{ kind: "synchronized" }] : [];
94352
+ return require_Path.concat(require_Path.fromArray([...catchUp.items, ...marker]), liveEvents.pipe(require_Path.filter((event) => event.sequence > catchUp.watermark), require_Path.map((event) => ({
94353
+ kind: "event",
94354
+ event
94355
+ }))));
94356
+ }));
94357
+ }
94358
+ //#endregion
93947
94359
  //#region src/orchestration/bootstrapCommandCoordinator.ts
93948
94360
  const MAX_COMPLETED_COMMANDS_PER_ENGINE = 256;
93949
94361
  const commandsByEngine = /* @__PURE__ */ new WeakMap();
@@ -102295,7 +102707,7 @@ function claudeReadinessReasonCode(reason) {
102295
102707
  //#endregion
102296
102708
  //#region src/egress/providerDeliveryReceipt.ts
102297
102709
  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;
102710
+ 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
102711
  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
102712
  const RECEIPT_KEYS = new Set([
102301
102713
  "receiptId",
@@ -102378,7 +102790,7 @@ function claudeAccountFields(value) {
102378
102790
  }
102379
102791
  function parseClaudeDeliveryIdentity(value) {
102380
102792
  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;
102793
+ 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
102794
  return {
102383
102795
  kind: "credential_generation",
102384
102796
  provider: "claude",
@@ -103145,7 +103557,10 @@ function extractClaudeProviderAccountId(response) {
103145
103557
  if (typeof response !== "object" || response === null || !("data" in response)) return null;
103146
103558
  const data = response.data;
103147
103559
  if (typeof data !== "object" || data === null) return null;
103148
- if ("connected" in data && data.connected === true) return extractClaudeCredentialGenerationId(response);
103560
+ if ("connected" in data && data.connected === true) {
103561
+ if ("providerAccountId" in data) return asNonEmptyString$1(data.providerAccountId) ?? null;
103562
+ return extractClaudeCredentialGenerationId(response);
103563
+ }
103149
103564
  if (!("account" in data)) return null;
103150
103565
  const account = data.account;
103151
103566
  if (typeof account !== "object" || account === null || !("providerAccountId" in account) || typeof account.providerAccountId !== "string") return null;
@@ -106121,6 +106536,14 @@ function makeRelayCommandInboxRepository(sql) {
106121
106536
  function isThreadDetailEvent(event) {
106122
106537
  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
106538
  }
106539
+ /**
106540
+ * Narrows the domain-event feed to one thread's stream-visible events. The same
106541
+ * predicate guards the live feed and the resume replay, so a subscriber cannot
106542
+ * be handed a different set of events depending on which path served it.
106543
+ */
106544
+ function isThreadDetailEventFor(threadId) {
106545
+ return (event) => event.aggregateKind === "thread" && event.aggregateId === threadId && isThreadDetailEvent(event);
106546
+ }
106124
106547
  const PROVIDER_STATUS_DEBOUNCE_MS = 200;
106125
106548
  function streamBoundedText(contents) {
106126
106549
  if (Buffer.byteLength(contents, "utf8") > 16777216) return require_Schema$1.fail(new BulkTransferError({
@@ -106392,6 +106815,32 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106392
106815
  }, normalizedCommand) : (normalizedCommand.type === "thread.turn.interrupt" ? interruptBootstrapTurnStart(orchestrationEngine, normalizedCommand) : orchestrationEngine.dispatch(normalizedCommand)).pipe(require_Schema$1.mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
106393
106816
  return startup.enqueueCommand(dispatchEffect).pipe(require_Schema$1.mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
106394
106817
  };
106818
+ /**
106819
+ * The thread snapshot a subscription falls back to, windowed when the
106820
+ * subscriber asked for a window. A missing thread is a failure rather
106821
+ * than an empty snapshot: the subscriber asked about a thread this server
106822
+ * does not have, and saying so is the only honest answer.
106823
+ */
106824
+ const loadThreadDetailSnapshot = (threadId, turnLimit) => require_Schema$1.gen(function* () {
106825
+ const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
106826
+ message: `Failed to load thread ${threadId}`,
106827
+ cause
106828
+ }))), orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
106829
+ if (require_Schema$1.isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106830
+ message: `Thread ${threadId} was not found`,
106831
+ cause: threadId
106832
+ });
106833
+ const windowed = windowThreadDetail({
106834
+ thread: threadDetail.value,
106835
+ window: turnLimit === void 0 ? {} : { turnLimit },
106836
+ snapshotSequence
106837
+ });
106838
+ return {
106839
+ snapshotSequence,
106840
+ thread: windowed.thread,
106841
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
106842
+ };
106843
+ });
106395
106844
  const loadServerConfig = require_Schema$1.gen(function* () {
106396
106845
  const keybindingsConfig = yield* keybindings.loadConfigState;
106397
106846
  const providers = yield* providerRegistry.getProviders;
@@ -106413,6 +106862,8 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106413
106862
  shellRevealInFileManager: true,
106414
106863
  shellRevealInFileManagerKind: fileManagerRevealKind
106415
106864
  },
106865
+ threadResumeCompletionMarker: true,
106866
+ threadSnapshotPagination: true,
106416
106867
  observability: {
106417
106868
  logsDirectoryPath: config.logsDir,
106418
106869
  localTracingEnabled: true,
@@ -106510,27 +106961,15 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106510
106961
  projectionSnapshotQuery
106511
106962
  })), require_Path.flatMap((event) => require_Schema$1.isSome(event) ? require_Path.succeed(event.value) : require_Path.empty))))
106512
106963
  })), { "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}`,
106964
+ [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, require_Schema$1.succeed(makeThreadSubscriptionStream(input, {
106965
+ subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(require_Schema$1.map((events) => events.pipe(require_Path.filter(isThreadDetailEventFor(input.threadId))))),
106966
+ currentSequence: orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence)),
106967
+ loadSnapshot: loadThreadDetailSnapshot(input.threadId, input.turnLimit),
106968
+ replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(require_Path.filter(isThreadDetailEventFor(input.threadId)), require_Path.mapError((cause) => new OrchestrationGetSnapshotError({
106969
+ message: `Failed to replay thread ${input.threadId}`,
106516
106970
  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);
106533
- }), { "rpc.aggregate": "orchestration" }),
106971
+ })))
106972
+ })), { "rpc.aggregate": "orchestration" }),
106534
106973
  [WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
106535
106974
  [WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(require_Schema$1.map((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
106536
106975
  [WS_METHODS.serverStartProviderAuth]: (input) => observeRpcEffect(WS_METHODS.serverStartProviderAuth, require_Schema$1.gen(function* () {
@@ -118006,8 +118445,8 @@ require_Schema$1.effectDiscard(runMigrations());
118006
118445
  //#endregion
118007
118446
  //#region src/persistence/Layers/Sqlite.ts
118008
118447
  const defaultSqliteClientLoaders = {
118009
- bun: () => Promise.resolve().then(() => require("./SqliteClient-bo7iui1t.cjs")),
118010
- node: () => Promise.resolve().then(() => require("./NodeSqliteClient-CyJvo9y9.cjs"))
118448
+ bun: () => Promise.resolve().then(() => require("./SqliteClient-O1ABIngw.cjs")),
118449
+ node: () => Promise.resolve().then(() => require("./NodeSqliteClient-CfsdC3AB.cjs"))
118011
118450
  };
118012
118451
  const makeRuntimeSqliteLayer = require_Schema$1.fn("makeRuntimeSqliteLayer")(function* (config) {
118013
118452
  const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
@@ -178324,7 +178763,7 @@ function em(e) {
178324
178763
  BO ??= new Int32Array(new SharedArrayBuffer(4)), Atomics.wait(BO, 0, 0, SY);
178325
178764
  }
178326
178765
  }
178327
- function rm$6(e, t) {
178766
+ function rm$7(e, t) {
178328
178767
  let n = (0, path.resolve)(t).toLowerCase(), r = (0, path.dirname)((0, path.resolve)(e)).toLowerCase();
178329
178768
  if (r === n || KO(r, n) || qO(r, n, "lexical")) return !0;
178330
178769
  let o = em(t)?.toLowerCase();
@@ -191174,7 +191613,7 @@ function Wo() {
191174
191613
  }
191175
191614
  function sle(e) {
191176
191615
  let t = process.cwd();
191177
- return e.filter((n) => !rm$6(n, t));
191616
+ return e.filter((n) => !rm$7(n, t));
191178
191617
  }
191179
191618
  function ZU(e) {
191180
191619
  let t = Bun.which(e);
@@ -212687,60 +213126,14 @@ function hasConfiguredApexCredential(profiles) {
212687
213126
  }
212688
213127
  //#endregion
212689
213128
  //#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
- */
213129
+ /** Per-account credentials share the existing Claude transcript directory. */
212719
213130
  /** Repoints `.credentials.json` without moving the transcripts. */
212720
213131
  const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
212721
213132
  /** The one file Claude Code writes into the credential directory. */
212722
213133
  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
213134
  /** Directory under the server's base directory that holds one directory per account. */
212741
213135
  const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
212742
213136
  const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
212743
- const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
212744
213137
  const DIRECTORY_NAME_HASH_LENGTH = 32;
212745
213138
  /**
212746
213139
  * Directory name for an account. A hash, so no email or display name reaches a
@@ -212749,11 +213142,12 @@ const DIRECTORY_NAME_HASH_LENGTH = 32;
212749
213142
  function claudeCredentialDirectoryName(accountIdentity) {
212750
213143
  return `account-${(0, node_crypto.createHash)("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
212751
213144
  }
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;
213145
+ /** Only a matching CMMD delivery can select a credential directory. */
213146
+ function resolveSelectedClaudeAccountIdentity(selectedAccountId, binding) {
213147
+ if (selectedAccountId === void 0) return null;
213148
+ const identity = binding?.identity;
213149
+ 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.");
213150
+ return identity.providerAccountIdentity;
212757
213151
  }
212758
213152
  /**
212759
213153
  * Where per-account credential directories live: `claude-credentials` under the
@@ -212780,29 +213174,19 @@ function resolveClaudeCredentialDirectory(input) {
212780
213174
  if (!root) return null;
212781
213175
  return (0, node_path.join)(root, claudeCredentialDirectoryName(input.accountIdentity));
212782
213176
  }
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
- */
213177
+ /** Refuse selected accounts that cannot use their own credential directory. */
212798
213178
  function decideClaudeCredentialDirectory(input) {
212799
213179
  return require_Schema$1.gen(function* () {
212800
213180
  if (!input.accountIdentity) return skip("no-account");
212801
- if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
213181
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return refuse("operator-override");
212802
213182
  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");
213183
+ if (!directory) return refuse("no-root");
213184
+ if (!holdsUsableCredential(directory)) return refuse("no-usable-credential");
213185
+ if ((yield* input.probeSupport) === "supported") return set(directory);
213186
+ return input.primaryCredentialMatchesSelected === true ? {
213187
+ outcome: "skipped",
213188
+ reason: "same-primary-account"
213189
+ } : refuse("capability-not-supported");
212806
213190
  });
212807
213191
  }
212808
213192
  /**
@@ -212841,6 +213225,12 @@ function skip(reason) {
212841
213225
  reason
212842
213226
  };
212843
213227
  }
213228
+ function refuse(reason) {
213229
+ return {
213230
+ outcome: "refused",
213231
+ reason
213232
+ };
213233
+ }
212844
213234
  function claudeConfigDirectory(env) {
212845
213235
  const configured = env.CLAUDE_CONFIG_DIR?.trim();
212846
213236
  if (configured) return configured;
@@ -212850,6 +213240,17 @@ function claudeConfigDirectory(env) {
212850
213240
  return null;
212851
213241
  }
212852
213242
  }
213243
+ /** The fallback is allowed only for the exact credential bytes CMMD signed. */
213244
+ function primaryClaudeCredentialMatchesRevision(env, revision) {
213245
+ const directory = claudeConfigDirectory(env);
213246
+ if (!directory || !/^[a-f0-9]{64}$/.test(revision)) return false;
213247
+ try {
213248
+ const credential = (0, node_fs.readFileSync)((0, node_path.join)(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213249
+ return (0, node_crypto.createHash)("sha256").update(credential).digest("hex") === revision && hasAccessToken(JSON.parse(credential));
213250
+ } catch {
213251
+ return false;
213252
+ }
213253
+ }
212853
213254
  //#endregion
212854
213255
  //#region src/provider/personalClaudeInVm.ts
212855
213256
  /**
@@ -212982,13 +213383,106 @@ function withoutInheritedAnthropicCredentials(env, options) {
212982
213383
  * marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
212983
213384
  * has the marker and no env var until it restarts, drops the override too.
212984
213385
  */
212985
- function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
213386
+ function claudeSubscriptionChildEnv(processEnv, credentialDirectory, selectedSubscription = false) {
212986
213387
  return {
212987
- ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
213388
+ ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: selectedSubscription || credentialDirectory !== null || personalClaudeInVmEnabled(processEnv) }),
212988
213389
  ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
212989
213390
  };
212990
213391
  }
212991
213392
  //#endregion
213393
+ //#region src/provider/claudeSelectedCredentialStore.ts
213394
+ async function ensurePrivateDirectory(path) {
213395
+ await (0, node_fs_promises.mkdir)(path, {
213396
+ recursive: true,
213397
+ mode: 448
213398
+ });
213399
+ const info = await (0, node_fs_promises.lstat)(path);
213400
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe credential directory.");
213401
+ await (0, node_fs_promises.chmod)(path, 448);
213402
+ }
213403
+ async function makeCapacity(root, target, active, maximum) {
213404
+ const entries = (await (0, node_fs_promises.readdir)(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && /^account-[a-f0-9]{32}$/.test(entry.name));
213405
+ if (entries.some((entry) => (0, node_path.join)(root, entry.name) === target)) return;
213406
+ const candidates = await Promise.all(entries.filter((entry) => !active.has((0, node_path.join)(root, entry.name))).map(async (entry) => ({
213407
+ path: (0, node_path.join)(root, entry.name),
213408
+ modified: (await (0, node_fs_promises.lstat)((0, node_path.join)(root, entry.name))).mtimeMs
213409
+ })));
213410
+ candidates.sort((a, b) => a.modified - b.modified);
213411
+ const required = entries.length - maximum + 1;
213412
+ if (required > candidates.length) throw new Error("All Claude credential slots are active. Stop a session before choosing another account.");
213413
+ for (const candidate of candidates.slice(0, Math.max(0, required))) await (0, node_fs_promises.rm)(candidate.path, { recursive: true });
213414
+ }
213415
+ function createClaudeSelectedCredentialStore(baseDir, options = {}) {
213416
+ const sessions = /* @__PURE__ */ new Map();
213417
+ const maximum = options.maxAccounts ?? 8;
213418
+ if (!Number.isSafeInteger(maximum) || maximum < 1) throw new Error("Invalid credential capacity.");
213419
+ let queue = Promise.resolve();
213420
+ async function install(threadId, acquired) {
213421
+ if (acquired.claims.threadId !== threadId) throw new Error("Credential thread mismatch.");
213422
+ const directory = resolveClaudeCredentialDirectory({
213423
+ env: process.env,
213424
+ baseDir,
213425
+ accountIdentity: acquired.claims.providerAccountId
213426
+ });
213427
+ if (!directory) throw new Error("Unsafe credential directory.");
213428
+ const binding = {
213429
+ runtimeId: acquired.claims.runtimeId,
213430
+ machineId: acquired.claims.machineId,
213431
+ credentialEpoch: acquired.claims.credentialEpoch,
213432
+ identity: {
213433
+ kind: "credential_generation",
213434
+ provider: "claude",
213435
+ credentialGenerationId: acquired.claims.generationId,
213436
+ providerAccountIdentity: acquired.claims.providerAccountId
213437
+ }
213438
+ };
213439
+ 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.");
213440
+ const root = (0, node_path.dirname)(directory);
213441
+ await ensurePrivateDirectory(root);
213442
+ await makeCapacity(root, directory, new Set([...sessions.values()].map((entry) => entry.directory)), maximum);
213443
+ await ensurePrivateDirectory(directory);
213444
+ const temporary = (0, node_path.join)(directory, `.credential-${(0, node_crypto.randomUUID)()}.tmp`);
213445
+ try {
213446
+ await (0, node_fs_promises.writeFile)(temporary, acquired.credentialJson, {
213447
+ mode: 384,
213448
+ flag: "wx"
213449
+ });
213450
+ await (0, node_fs_promises.rename)(temporary, (0, node_path.join)(directory, ".credentials.json"));
213451
+ } finally {
213452
+ await (0, node_fs_promises.rm)(temporary, { force: true });
213453
+ }
213454
+ sessions.set(threadId, {
213455
+ directory,
213456
+ binding,
213457
+ accountKind: acquired.claims.accountKind,
213458
+ credentialRevisionHash: acquired.claims.credentialRevisionHash
213459
+ });
213460
+ return binding;
213461
+ }
213462
+ return {
213463
+ prepare(threadId, acquired) {
213464
+ const operation = queue.then(() => install(threadId, acquired));
213465
+ queue = operation.then(() => void 0, () => void 0);
213466
+ return operation;
213467
+ },
213468
+ read(threadId, binding) {
213469
+ const prepared = sessions.get(threadId);
213470
+ return prepared && providerDeliveryBindingsEqual(prepared.binding, binding) ? prepared : null;
213471
+ },
213472
+ release(threadId) {
213473
+ sessions.delete(threadId);
213474
+ }
213475
+ };
213476
+ }
213477
+ const stores = /* @__PURE__ */ new Map();
213478
+ function selectedClaudeCredentialStore(baseDir) {
213479
+ const existing = stores.get(baseDir);
213480
+ if (existing) return existing;
213481
+ const store = createClaudeSelectedCredentialStore(baseDir);
213482
+ stores.set(baseDir, store);
213483
+ return store;
213484
+ }
213485
+ //#endregion
212992
213486
  //#region src/provider/claudeCredentialDirectoryProbe.ts
212993
213487
  /**
212994
213488
  * Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
@@ -215033,7 +215527,8 @@ function claudeForgeAttributionPrompt() {
215033
215527
  * turn, an API key is, so pointing the variable there would add a second source
215034
215528
  * of truth that a later reader has to rule out.
215035
215529
  */
215036
- function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
215530
+ function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory, selectedSubscription = false) {
215531
+ if (selectedSubscription) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory, true);
215037
215532
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
215038
215533
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
215039
215534
  if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
@@ -216947,6 +217442,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
216947
217442
  if (streamFiber && streamFiber.pollUnsafe() === void 0) yield* require_Path.interrupt(streamFiber);
216948
217443
  try {
216949
217444
  context.query.close();
217445
+ selectedClaudeCredentialStore(serverConfig.baseDir).release(context.session.threadId);
216950
217446
  } catch (cause) {
216951
217447
  yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
216952
217448
  }
@@ -217258,6 +217754,43 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217258
217754
  sessionId: existingResumeSessionId ?? newSessionId
217259
217755
  };
217260
217756
  });
217757
+ const resolveSelectedCredentialDirectory = require_Schema$1.fn("resolveSelectedCredentialDirectory")(function* (input, resumeState, claudeBinaryPath) {
217758
+ const requestedAccountId = input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.providerAccountId : void 0;
217759
+ const selectedAccountIdentity = yield* require_Schema$1.try_({
217760
+ try: () => resolveSelectedClaudeAccountIdentity(requestedAccountId, resumeState?.providerDeliveryBinding),
217761
+ catch: () => new ProviderAdapterValidationError({
217762
+ provider: PROVIDER$11,
217763
+ operation: "startSession",
217764
+ issue: `The selected Claude account ${requestedAccountId} has no matching credential delivery. Reconnect that account and try again.`
217765
+ })
217766
+ });
217767
+ const preparedCredential = resumeState?.providerDeliveryBinding ? selectedClaudeCredentialStore(serverConfig.baseDir).read(input.threadId, resumeState.providerDeliveryBinding) : null;
217768
+ if (selectedAccountIdentity !== null && preparedCredential === null) return yield* new ProviderAdapterValidationError({
217769
+ provider: PROVIDER$11,
217770
+ operation: "startSession",
217771
+ issue: `The selected Claude account ${selectedAccountIdentity} has no prepared credential. Try again to reconnect it.`
217772
+ });
217773
+ const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
217774
+ env: process.env,
217775
+ baseDir: serverConfig.baseDir,
217776
+ accountIdentity: selectedAccountIdentity,
217777
+ probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
217778
+ primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesRevision(process.env, preparedCredential.credentialRevisionHash)
217779
+ });
217780
+ if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
217781
+ provider: PROVIDER$11,
217782
+ operation: "startSession",
217783
+ issue: `The selected Claude account ${selectedAccountIdentity} cannot run on this environment: ${credentialDirectoryDecision.reason}. Reconnect the account or choose another environment.`
217784
+ });
217785
+ if (credentialDirectoryDecision.outcome === "skipped") {
217786
+ const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
217787
+ yield* credentialDirectoryDecision.reason === "no-account" ? require_Schema$1.logDebug(skipLine) : require_Schema$1.logWarning(skipLine);
217788
+ }
217789
+ return {
217790
+ selectedAccountIdentity,
217791
+ claudeCredentialDirectory: credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null
217792
+ };
217793
+ });
217261
217794
  const startSession = require_Schema$1.fn("startSession")(function* (input) {
217262
217795
  const sessionProvider = yield* validateClaudeStartSessionProvider(input.provider);
217263
217796
  const startedAt = yield* nowIso;
@@ -217318,19 +217851,14 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217318
217851
  })));
217319
217852
  const claudeSettings = serverSettings.providers.claudeAgent;
217320
217853
  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;
217854
+ const { selectedAccountIdentity, claudeCredentialDirectory } = yield* resolveSelectedCredentialDirectory(input, resumeState, claudeBinaryPath);
217332
217855
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
217333
- const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
217856
+ if (selectedAccountIdentity !== null && modelConfig.endpointProfile !== void 0) return yield* new ProviderAdapterValidationError({
217857
+ provider: PROVIDER$11,
217858
+ operation: "startSession",
217859
+ issue: "The selected Claude account cannot be used with a custom API endpoint. Choose a subscription model."
217860
+ });
217861
+ const providerProxyCredential = selectedAccountIdentity !== null ? null : yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
217334
217862
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
217335
217863
  const settings = claudeQuerySettings(modelConfig.thinking, modelConfig.fastMode);
217336
217864
  const workspacePreflight = input.cwd ? formatWorkspaceEnvironmentPreflightForAgent(runWorkspaceEnvironmentPreflight({ cwd: input.cwd })) : void 0;
@@ -217353,7 +217881,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217353
217881
  existingResumeSessionId,
217354
217882
  newSessionId,
217355
217883
  canUseTool,
217356
- env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
217884
+ env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory, selectedAccountIdentity !== null),
217357
217885
  browserManager,
217358
217886
  threadId,
217359
217887
  browserToolEnvironmentId,
@@ -217405,7 +217933,9 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217405
217933
  });
217406
217934
  attachClaudeStreamFiber(context$4, runFork);
217407
217935
  return { ...session };
217408
- });
217936
+ }, (effect, input) => effect.pipe(require_Schema$1.onExit((exit) => require_Schema$1.sync(() => {
217937
+ if (exit._tag === "Failure") selectedClaudeCredentialStore(serverConfig.baseDir).release(input.threadId);
217938
+ }))));
217409
217939
  const sendTurn = require_Schema$1.fn("sendTurn")(function* (input) {
217410
217940
  const context = yield* requireSession(input.threadId);
217411
217941
  const modelSelection = input.modelSelection?.provider === "claudeAgent" || input.modelSelection?.provider === "apex" || input.modelSelection?.provider === "cmmd" ? input.modelSelection : void 0;
@@ -231714,6 +232244,9 @@ const ProviderRollbackConversationInput = require_Schema$1.Struct({
231714
232244
  threadId: ThreadId,
231715
232245
  numTurns: NonNegativeInt
231716
232246
  });
232247
+ function selectedAccountGuardFields(providerAccountId) {
232248
+ return providerAccountId === void 0 ? {} : { providerAccountId };
232249
+ }
231717
232250
  function cursorRecord(value) {
231718
232251
  return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
231719
232252
  }
@@ -231806,9 +232339,13 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
231806
232339
  const runtimeEventPubSub = yield* require_Path.unbounded$1();
231807
232340
  const assertProviderExecutable = options?.assertProviderExecutable ?? (() => require_Schema$1.succeed(null));
231808
232341
  const assertCurrentProviderDelivery = require_Schema$1.fn("assertCurrentProviderDelivery")(function* (input) {
231809
- const currentBinding = yield* assertProviderExecutable(input);
231810
- if (!currentBinding) return;
231811
232342
  const persistedBinding = require_Schema$1.getOrUndefined$1(yield* directory.getBinding(input.threadId));
232343
+ const providerAccountId = readPersistedProviderAccountId(persistedBinding?.runtimePayload);
232344
+ const currentBinding = yield* assertProviderExecutable({
232345
+ ...input,
232346
+ ...selectedAccountGuardFields(providerAccountId)
232347
+ });
232348
+ if (!currentBinding) return;
231812
232349
  if (!persistedBinding || persistedBinding.provider !== input.provider) return yield* toValidationError(input.operation, `Cannot verify the delivered provider identity for thread '${input.threadId}'.`);
231813
232350
  yield* bindProviderDeliveryToResumeCursor({
231814
232351
  operation: input.operation,
@@ -231864,10 +232401,12 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
231864
232401
  if (!hasResumeCursor) return yield* toValidationError(input.operation, `Cannot recover thread '${input.binding.threadId}' because no provider resume state is persisted.`);
231865
232402
  const persistedCwd = readPersistedCwd(input.binding.runtimePayload);
231866
232403
  const persistedModelSelection = readPersistedModelSelection(input.binding.runtimePayload);
232404
+ const persistedProviderAccountId = readProviderAccountIdFromModelSelection(persistedModelSelection);
231867
232405
  const currentDeliveryBinding = yield* assertProviderExecutable({
231868
232406
  threadId: input.binding.threadId,
231869
232407
  provider: input.binding.provider,
231870
- operation: input.operation
232408
+ operation: input.operation,
232409
+ ...selectedAccountGuardFields(persistedProviderAccountId)
231871
232410
  });
231872
232411
  const boundResumeCursor = yield* bindProviderDeliveryToResumeCursor({
231873
232412
  operation: input.operation,
@@ -231993,7 +232532,8 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
231993
232532
  currentBinding: yield* assertProviderExecutable({
231994
232533
  threadId,
231995
232534
  provider: effectiveProvider,
231996
- operation: "ProviderService.startSession"
232535
+ operation: "ProviderService.startSession",
232536
+ ...selectedAccountGuardFields(requestedAccountId)
231997
232537
  })
231998
232538
  });
231999
232539
  const session = yield* adapter.startSession({
@@ -232059,6 +232599,8 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
232059
232599
  allowRecovery: true
232060
232600
  });
232061
232601
  const persistedBinding = require_Schema$1.getOrUndefined$1(yield* directory.getBinding(input.threadId));
232602
+ const persistedModelSelection = readPersistedModelSelection(persistedBinding?.runtimePayload);
232603
+ const nextModelSelection = input.modelSelection ?? persistedModelSelection;
232062
232604
  yield* assertProviderAccountBoundary({
232063
232605
  operation: "ProviderService.sendTurn",
232064
232606
  threadId: input.threadId,
@@ -232083,7 +232625,7 @@ const makeProviderService = require_Schema$1.fn("makeProviderService")(function*
232083
232625
  status: "running",
232084
232626
  ...turn.resumeCursor !== void 0 ? { resumeCursor: turn.resumeCursor } : {},
232085
232627
  runtimePayload: {
232086
- ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {},
232628
+ ...nextModelSelection !== void 0 ? { modelSelection: nextModelSelection } : {},
232087
232629
  activeTurnId: turn.turnId,
232088
232630
  lastRuntimeEvent: "provider.sendTurn",
232089
232631
  lastRuntimeEventAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -232340,6 +232882,201 @@ function makeProviderServiceLive(options) {
232340
232882
  return require_Schema$1.effect(ProviderService, makeProviderService(options));
232341
232883
  }
232342
232884
  //#endregion
232885
+ //#region src/runtimeAuth/cmmdClaudeSelectedTicket.ts
232886
+ const ACCOUNT = /^pai_v1_[A-Za-z0-9_-]{43}$/;
232887
+ 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;
232888
+ const REVISION_HASH = /^[a-f0-9]{64}$/;
232889
+ function hasValidTicketLifetime(claims, payload) {
232890
+ const now = Math.floor(Date.now() / 1e3);
232891
+ const issuedAt = payload.iat;
232892
+ const notBefore = payload.nbf;
232893
+ 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;
232894
+ }
232895
+ function hasSelectedCredentialScope(claims) {
232896
+ return claims.scope.length === 1 && claims.scope[0] === "claude-credential-read";
232897
+ }
232898
+ function hasExpectedSelectedBinding(payload, expected) {
232899
+ 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);
232900
+ }
232901
+ function selectedCredentialClaims(payload) {
232902
+ const jti = payload.jti;
232903
+ const generationId = payload.credential_generation_id;
232904
+ const credentialRevisionHash = payload.credential_revision_hash;
232905
+ const accountKind = payload.account_kind;
232906
+ 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;
232907
+ return {
232908
+ jti,
232909
+ generationId,
232910
+ credentialRevisionHash,
232911
+ accountKind
232912
+ };
232913
+ }
232914
+ function verifyCmmdClaudeSelectedTicket(token, expected) {
232915
+ const verified = require_runtimeControlAuthority.verifyCmmdScopedRuntimePayload(token, expected, "forge-claude-credential-read");
232916
+ if (!verified) return null;
232917
+ const { claims, payload } = verified;
232918
+ if (!hasValidTicketLifetime(claims, payload)) return null;
232919
+ if (!hasSelectedCredentialScope(claims)) return null;
232920
+ if (!hasExpectedSelectedBinding(payload, expected)) return null;
232921
+ const selected = selectedCredentialClaims(payload);
232922
+ if (!selected) return null;
232923
+ return {
232924
+ ...claims,
232925
+ jti: selected.jti,
232926
+ providerAccountId: expected.providerAccountId,
232927
+ generationId: selected.generationId,
232928
+ credentialRevisionHash: selected.credentialRevisionHash,
232929
+ accountKind: selected.accountKind,
232930
+ threadId: expected.threadId,
232931
+ operationId: expected.operationId
232932
+ };
232933
+ }
232934
+ //#endregion
232935
+ //#region src/provider/claudeSelectedCredentialAcquisition.ts
232936
+ function readToken(response) {
232937
+ if (typeof response !== "object" || response === null || !("data" in response)) return null;
232938
+ const data = response.data;
232939
+ return typeof data === "object" && data !== null && "token" in data && typeof data.token === "string" ? data.token : null;
232940
+ }
232941
+ async function acquireSelectedClaudeCredential(expected, transport) {
232942
+ try {
232943
+ const token = readToken(await transport.requestTicket());
232944
+ const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
232945
+ if (!token || !claims) throw new Error("invalid_ticket");
232946
+ const credentialJson = await transport.readCredential(token);
232947
+ if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || (0, node_crypto.createHash)("sha256").update(credentialJson).digest("hex") !== claims.credentialRevisionHash || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
232948
+ return {
232949
+ claims,
232950
+ credentialJson
232951
+ };
232952
+ } catch {
232953
+ throw new Error("The selected Claude account could not be prepared. Reconnect the account and try again.");
232954
+ }
232955
+ }
232956
+ //#endregion
232957
+ //#region src/provider/claudeSelectedCredentialPreparer.ts
232958
+ const REQUEST_TIMEOUT_MS = 1e4;
232959
+ const OWNER_TICKET_PATH = "/api/forge/context/providers/claude/runtime-read-ticket";
232960
+ const CREDENTIAL_READ_PATH = "/api/forge/context/auth-runtime-claude-credential";
232961
+ function isRuntimeTicketSession$1(sessionId) {
232962
+ return String(sessionId).startsWith("cmmd-runtime:");
232963
+ }
232964
+ function nonEmptyConfigString(value) {
232965
+ return value?.trim() || null;
232966
+ }
232967
+ function selectedRuntimeStrings(config) {
232968
+ const values = [
232969
+ nonEmptyConfigString(config.forgeCmmdRuntimeId),
232970
+ nonEmptyConfigString(config.forgeCmmdRuntimeMachineId),
232971
+ nonEmptyConfigString(config.forgeCmmdRuntimeOrgId),
232972
+ nonEmptyConfigString(config.forgeCmmdRuntimeUserId),
232973
+ nonEmptyConfigString(config.forgeCmmdRuntimePublicKey)
232974
+ ];
232975
+ if (values.includes(null)) return null;
232976
+ return {
232977
+ runtimeId: values[0],
232978
+ machineId: values[1],
232979
+ orgId: values[2],
232980
+ userId: values[3],
232981
+ publicKeyPem: values[4]
232982
+ };
232983
+ }
232984
+ function selectedCredentialEpoch(config) {
232985
+ const credentialEpoch = config.forgeCmmdRuntimeCredentialEpoch;
232986
+ return typeof credentialEpoch === "number" && Number.isSafeInteger(credentialEpoch) && credentialEpoch >= 0 ? credentialEpoch : null;
232987
+ }
232988
+ function selectedConfig(config) {
232989
+ if (config.forgeCmmdRuntimeMode !== true) return null;
232990
+ const strings = selectedRuntimeStrings(config);
232991
+ const credentialEpoch = selectedCredentialEpoch(config);
232992
+ if (strings === null || credentialEpoch === null) return null;
232993
+ return {
232994
+ ...strings,
232995
+ credentialEpoch
232996
+ };
232997
+ }
232998
+ async function boundedRuntimeTicketRequest(grant, runtimeId, request) {
232999
+ const response = await fetch(resolveCommandAppEndpoint(`/api/forge/runtimes/${encodeURIComponent(runtimeId)}/provider/claude/read-ticket`), {
233000
+ method: "POST",
233001
+ headers: {
233002
+ authorization: `Bearer ${grant}`,
233003
+ "content-type": "application/json"
233004
+ },
233005
+ body: JSON.stringify(request),
233006
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
233007
+ });
233008
+ if (!response.ok) throw new Error("selected_ticket_unavailable");
233009
+ return await response.json();
233010
+ }
233011
+ async function boundedCredentialRead(token) {
233012
+ const response = await fetch(resolveCommandAppEndpoint(CREDENTIAL_READ_PATH), {
233013
+ headers: {
233014
+ authorization: `Bearer ${token}`,
233015
+ accept: "application/json"
233016
+ },
233017
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
233018
+ });
233019
+ if (!response.ok) throw new Error("selected_credential_unavailable");
233020
+ return await response.text();
233021
+ }
233022
+ function missingAuthority() {
233023
+ throw new Error("selected_authority_unavailable");
233024
+ }
233025
+ /**
233026
+ * Acquires one signed, account-scoped Claude credential for the thread that
233027
+ * selected it. Owner and runtime-ticket sessions use different CMMD lanes.
233028
+ */
233029
+ function makeClaudeSelectedCredentialPreparer(config, dependencies) {
233030
+ const readRuntimeAuthority = dependencies.readRuntimeAuthority ?? readRuntimeBrokerAuthority;
233031
+ const requestOwnerTicket = dependencies.requestOwnerTicket ?? ((jwt, request) => proxyPost(jwt, OWNER_TICKET_PATH, request, void 0, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }));
233032
+ const requestRuntimeTicket = dependencies.requestRuntimeTicket ?? boundedRuntimeTicketRequest;
233033
+ const readCredential = dependencies.readCredential ?? boundedCredentialRead;
233034
+ const prepare = dependencies.prepare ?? ((threadId, acquired) => {
233035
+ if (!config.baseDir) return Promise.reject(/* @__PURE__ */ new Error("selected_store_unavailable"));
233036
+ return selectedClaudeCredentialStore(config.baseDir).prepare(threadId, acquired);
233037
+ });
233038
+ const createOperationId = dependencies.createOperationId ?? node_crypto.randomUUID;
233039
+ return async (input) => {
233040
+ const sealed = selectedConfig(config);
233041
+ const sessionId = input.executionSessionId ?? dependencies.readSessionId(input.threadId);
233042
+ if (sealed === null || sessionId === null) return missingAuthority();
233043
+ const operationId = createOperationId();
233044
+ const request = {
233045
+ providerAccountId: input.providerAccountId,
233046
+ threadId: String(input.threadId),
233047
+ operationId
233048
+ };
233049
+ const acquired = await acquireSelectedClaudeCredential({
233050
+ ...sealed,
233051
+ providerAccountId: input.providerAccountId,
233052
+ threadId: String(input.threadId),
233053
+ operationId
233054
+ }, {
233055
+ requestTicket: async () => {
233056
+ if (isRuntimeTicketSession$1(sessionId)) {
233057
+ const grant = readRuntimeAuthority({
233058
+ sessionId,
233059
+ runtimeId: sealed.runtimeId,
233060
+ machineId: sealed.machineId,
233061
+ credentialEpoch: sealed.credentialEpoch,
233062
+ requiredScope: "provider"
233063
+ });
233064
+ if (grant === null) return missingAuthority();
233065
+ return { data: await requestRuntimeTicket(grant, sealed.runtimeId, request) };
233066
+ }
233067
+ const jwt = await dependencies.resolveJwt(sessionId, { forceRefresh: false });
233068
+ if (jwt === null) return missingAuthority();
233069
+ return await requestOwnerTicket(jwt, {
233070
+ ...request,
233071
+ runtimeId: sealed.runtimeId
233072
+ });
233073
+ },
233074
+ readCredential
233075
+ });
233076
+ return await prepare(String(input.threadId), acquired);
233077
+ };
233078
+ }
233079
+ //#endregion
232343
233080
  //#region src/provider/claudeRuntimeReadinessGuard.ts
232344
233081
  const GENERATION_HASH_PATTERN = /^[a-f0-9]{64}$/;
232345
233082
  const RUNTIME_READINESS_TIMEOUT_MS$1 = 1e4;
@@ -232608,7 +233345,22 @@ function makeClaudeRuntimeReadinessGuard(config, dependencies) {
232608
233345
  return (input) => claudeReadinessGuardEffect(config, dependencies, resolved, input);
232609
233346
  }
232610
233347
  function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
232611
- if (!requiresNativeClaudeReadiness(config, input.provider)) return require_Schema$1.succeed(null);
233348
+ if (!requiresNativeClaudeReadiness(config, input.provider, input.providerAccountId)) return require_Schema$1.succeed(null);
233349
+ if (input.providerAccountId !== void 0) {
233350
+ const prepareSelectedCredential = dependencies.prepareSelectedCredential ?? makeClaudeSelectedCredentialPreparer(config, {
233351
+ resolveJwt: dependencies.resolveJwt,
233352
+ readSessionId: resolved.readSessionId,
233353
+ readRuntimeAuthority: resolved.readRuntimeAuthority
233354
+ });
233355
+ const selectedInput = {
233356
+ ...input,
233357
+ providerAccountId: input.providerAccountId
233358
+ };
233359
+ return require_Schema$1.tryPromise({
233360
+ try: () => prepareSelectedCredential(selectedInput),
233361
+ catch: () => selectedCredentialValidationError(input.operation, selectedInput.providerAccountId)
233362
+ });
233363
+ }
232612
233364
  const authority = resolveGuardAuthority(config, resolved.readSessionId, input);
232613
233365
  if (authority === null) return invalidOwnerSessionEffect(input.operation);
232614
233366
  return require_Schema$1.tryPromise({
@@ -232616,8 +233368,9 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
232616
233368
  catch: (cause) => readinessValidationError(input.operation, cause)
232617
233369
  });
232618
233370
  }
232619
- function requiresNativeClaudeReadiness(config, provider) {
232620
- return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
233371
+ function requiresNativeClaudeReadiness(config, provider, providerAccountId) {
233372
+ if (provider !== "claudeAgent" || config.forgeCmmdRuntimeMode !== true) return false;
233373
+ return providerAccountId !== void 0 || personalClaudeInVmEnabled();
232621
233374
  }
232622
233375
  function resolveGuardAuthority(config, readSessionId, input) {
232623
233376
  const runtimeId = config.forgeCmmdRuntimeId?.trim();
@@ -232654,6 +233407,12 @@ async function verifyGuardAuthority(config, dependencies, resolved, authority) {
232654
233407
  fetchReadiness: resolved.fetchReadiness
232655
233408
  });
232656
233409
  }
233410
+ function selectedCredentialValidationError(operation, accountId) {
233411
+ return new ProviderValidationError({
233412
+ operation,
233413
+ issue: `The selected Claude account ${accountId} could not be prepared. Reconnect the account and try again.`
233414
+ });
233415
+ }
232657
233416
  function readinessValidationError(operation, cause) {
232658
233417
  return new ProviderValidationError({
232659
233418
  operation,
@@ -297393,7 +298152,7 @@ function resolveBuildCommitFromEnv(env) {
297393
298152
  * environment descriptor down instead of reporting an honest "unknown".
297394
298153
  */
297395
298154
  function readBakedBuildCommit() {
297396
- return "05d8568497fdceb5b5d827b142ea4c681bef25ce";
298155
+ return "e51896589531a57a458ca5c2430dfd54cdc16476";
297397
298156
  }
297398
298157
  async function resolveServerBuildCommit(input) {
297399
298158
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -306307,6 +307066,82 @@ const orchestrationAutomationTerminalReconcileRouteLayer = add$1("POST", "/api/o
306307
307066
  }, { status: 200 });
306308
307067
  }).pipe(require_Schema$1.catchTag("OrchestrationDispatchCommandError", respondToWorkRunEventError)));
306309
307068
  //#endregion
307069
+ //#region src/orchestration/threadSnapshotRoute.ts
307070
+ /**
307071
+ * `GET /api/orchestration/threads/:threadId`, one thread's detail snapshot,
307072
+ * optionally windowed to a page of recent turns.
307073
+ *
307074
+ * Forge never served this route. The mobile client has asked for it on every
307075
+ * thread open since it shipped, fell through to the socket-embedded snapshot on
307076
+ * the 404, and its "load earlier" control had nowhere to fetch from, so a long
307077
+ * thread was only ever loadable in full. Serving it here gives the initial read
307078
+ * a body the transport can gzip, off the RPC socket, and gives older turns a
307079
+ * cursor to arrive on.
307080
+ *
307081
+ * The window is applied by the same pure function the WebSocket fallback
307082
+ * snapshot uses, so the two reads cannot disagree about what a page is.
307083
+ */
307084
+ /**
307085
+ * Reads the window out of the query string.
307086
+ *
307087
+ * Returns null for a malformed value rather than ignoring it. A client that
307088
+ * asked for 10 turns and silently received 4,000 has no way to tell, and the
307089
+ * failure would show up as a slow app rather than as an error.
307090
+ */
307091
+ function parseThreadSnapshotWindow(searchParams) {
307092
+ const rawTurnLimit = searchParams.get("turnLimit");
307093
+ const rawBeforeCursor = searchParams.get("beforeCursor");
307094
+ let turnLimit;
307095
+ if (rawTurnLimit !== null) {
307096
+ const parsed = Number(rawTurnLimit);
307097
+ if (!Number.isSafeInteger(parsed) || parsed < 1) return null;
307098
+ turnLimit = parsed;
307099
+ }
307100
+ let beforeCursor;
307101
+ if (rawBeforeCursor !== null) {
307102
+ const trimmed = rawBeforeCursor.trim();
307103
+ if (trimmed.length === 0) return null;
307104
+ beforeCursor = trimmed;
307105
+ }
307106
+ return {
307107
+ ...turnLimit === void 0 ? {} : { turnLimit },
307108
+ ...beforeCursor === void 0 ? {} : { beforeCursor }
307109
+ };
307110
+ }
307111
+ const orchestrationThreadSnapshotRouteLayer = add$1("GET", "/api/orchestration/threads/:threadId", require_Schema$1.gen(function* () {
307112
+ const request = yield* require_HttpServer.HttpServerRequest;
307113
+ yield* (yield* ServerAuth).authenticateHttpRequest(request);
307114
+ const rawThreadId = (yield* params)["threadId"];
307115
+ if (rawThreadId === void 0 || rawThreadId.length === 0) return require_HttpServer.jsonUnsafe({ error: "Missing thread id." }, { status: 400 });
307116
+ const url = require_HttpServer.toURL(request);
307117
+ const window = require_Schema$1.isNone(url) ? {} : parseThreadSnapshotWindow(url.value.searchParams);
307118
+ if (window === null) return require_HttpServer.jsonUnsafe({ error: "Invalid turnLimit or beforeCursor." }, { status: 400 });
307119
+ const threadId = ThreadId.make(decodeURIComponent(rawThreadId));
307120
+ const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
307121
+ const orchestrationEngine = yield* OrchestrationEngineService;
307122
+ const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
307123
+ message: `Failed to load thread ${threadId}`,
307124
+ cause
307125
+ }))), orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
307126
+ if (require_Schema$1.isNone(threadDetail)) return require_HttpServer.jsonUnsafe({ error: "Thread not found." }, { status: 404 });
307127
+ const windowed = windowThreadDetail({
307128
+ thread: threadDetail.value,
307129
+ window,
307130
+ snapshotSequence
307131
+ });
307132
+ return require_HttpServer.jsonUnsafe({
307133
+ snapshotSequence,
307134
+ thread: windowed.thread,
307135
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
307136
+ }, { status: 200 });
307137
+ }).pipe(require_Schema$1.catchTag("AuthError", respondToAuthError), require_Schema$1.catchTag("OrchestrationGetSnapshotError", (error) => require_Schema$1.gen(function* () {
307138
+ yield* require_Schema$1.logError("thread snapshot route failed", {
307139
+ message: error.message,
307140
+ cause: error.cause
307141
+ });
307142
+ return require_HttpServer.jsonUnsafe({ error: error.message }, { status: 500 });
307143
+ }))));
307144
+ //#endregion
306310
307145
  //#region src/localAutomations/scheduling.ts
306311
307146
  const MINUTE_MS = 6e4;
306312
307147
  const HOUR_MS = 60 * MINUTE_MS;
@@ -320681,12 +321516,12 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", require_
320681
321516
  //#region src/server.ts
320682
321517
  const PtyAdapterLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320683
321518
  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;
321519
+ else return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodePTY-BehazUMi.cjs")))).layer;
320685
321520
  }));
320686
321521
  const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320687
321522
  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"))]);
321523
+ if (typeof Bun !== "undefined") return (yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunHttpServer-C0oPP_3A.cjs")))).layer(bunHttpServerOptions(config));
321524
+ 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
321525
  return NodeHttpServer.layer(NodeHttp.createServer, {
320691
321526
  host: config.host,
320692
321527
  port: config.port
@@ -320694,10 +321529,10 @@ const HttpServerLive = require_Schema$1.unwrap(require_Schema$1.gen(function* ()
320694
321529
  }));
320695
321530
  const PlatformServicesLive = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320696
321531
  if (typeof Bun !== "undefined") {
320697
- const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-Dl-3897e.cjs")));
321532
+ const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./BunServices-DkenwD7S.cjs")));
320698
321533
  return layer;
320699
321534
  } else {
320700
- const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-R0BZsZ72.cjs")));
321535
+ const { layer } = yield* require_Schema$1.promise(() => Promise.resolve().then(() => require("./NodeServices-BvdIc9Vh.cjs")));
320701
321536
  return layer;
320702
321537
  }
320703
321538
  }));
@@ -320764,7 +321599,7 @@ const MiscRuntimeLayerLive = require_Schema$1.mergeAll(AnalyticsServiceLayerLive
320764
321599
  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
321600
  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
321601
  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));
321602
+ 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
321603
  const makeServerLayer = require_Schema$1.unwrap(require_Schema$1.gen(function* () {
320769
321604
  const config = yield* ServerConfig;
320770
321605
  fixPath({ preferProcessPath: config.forgeCmmdRuntimeMode === true });