@cmmd-center/forge 0.13.12 → 0.13.14

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 (23) hide show
  1. package/dist/bin.cjs +543 -75
  2. package/dist/bin.mjs +543 -75
  3. package/dist/client/assets/{CanvasPanel-BjHxCGlb.js → CanvasPanel-RpD9Eh_S.js} +2 -2
  4. package/dist/client/assets/{CompositeItem-D4LER3__.js → CompositeItem-DEIBUHJR.js} +2 -2
  5. package/dist/client/assets/{DiffPanel-D3btZGjW.js → DiffPanel-P3t5Fq1q.js} +2 -2
  6. package/dist/client/assets/{DiffPanel.logic-DWcwE9Wj.js → DiffPanel.logic-BXQ1C0-e.js} +2 -2
  7. package/dist/client/assets/{DiffPanelShell-oXHh--ae.js → DiffPanelShell-3EpWEr7p.js} +103 -103
  8. package/dist/client/assets/{DiffWorkerPoolProvider-R9YPaaNy.js → DiffWorkerPoolProvider-3_hkcEDq.js} +2 -2
  9. package/dist/client/assets/{PullRequestCodePanel-DTCc2TBl.js → PullRequestCodePanel-B2T6MPcE.js} +2 -2
  10. package/dist/client/assets/{Virtualizer-arKGxq_z.js → Virtualizer-DCt36RGD.js} +2 -2
  11. package/dist/client/assets/{canvasStateStore-B5zd1u9r.js → canvasStateStore-NrAxDqvM.js} +2 -2
  12. package/dist/client/assets/{cmmdAuthRedirect-DeFAIQpj.js → cmmdAuthRedirect-BahlX0gF.js} +2 -2
  13. package/dist/client/assets/index-C3pC5t1K.css +1 -0
  14. package/dist/client/assets/index-DvwXD11B.js +154 -0
  15. package/dist/client/assets/{popover-DM7IA0zJ.js → popover-Z8Tvl-OD.js} +2 -2
  16. package/dist/client/assets/src-BU7yHzO9.js +3 -0
  17. package/dist/client/assets/{toggle-DVtS0BJJ.js → toggle-CWYdWYBn.js} +2 -2
  18. package/dist/client/index.html +11 -11
  19. package/dist/forge-build.json +1 -1
  20. package/package.json +1 -1
  21. package/dist/client/assets/index-Bn-ASQRG.css +0 -1
  22. package/dist/client/assets/index-C7YKDHIe.js +0 -154
  23. package/dist/client/assets/src-LDQtPKQt.js +0 -3
package/dist/bin.cjs CHANGED
@@ -17402,7 +17402,8 @@ const ORCHESTRATION_WS_METHODS = {
17402
17402
  getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
17403
17403
  subscribeShell: "orchestration.subscribeShell",
17404
17404
  subscribeThread: "orchestration.subscribeThread",
17405
- searchThreads: "orchestration.searchThreads"
17405
+ searchThreads: "orchestration.searchThreads",
17406
+ getThreadDetailPage: "orchestration.getThreadDetailPage"
17406
17407
  };
17407
17408
  const ProviderKind = require_Schema$1.Literals([
17408
17409
  "codex",
@@ -17931,6 +17932,21 @@ require_Schema$1.Struct({
17931
17932
  beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
17932
17933
  });
17933
17934
  /**
17935
+ * Reads one windowed page of a thread's detail directly over the RPC socket.
17936
+ *
17937
+ * The HTTP snapshot route and `subscribeThread`'s fallback snapshot both take
17938
+ * the same `turnLimit`/`beforeCursor` window, but neither can serve "load
17939
+ * earlier" on a connection with no HTTP endpoint: the fallback snapshot only
17940
+ * fires once, on subscribe. This method exists for exactly that case (a
17941
+ * relay-only connection), gated on the server's `threadDetailPageRpc`
17942
+ * capability so older servers are never sent a method they do not know.
17943
+ */
17944
+ const OrchestrationGetThreadDetailPageInput = require_Schema$1.Struct({
17945
+ threadId: ThreadId,
17946
+ turnLimit: require_Schema$1.optionalKey(PositiveInt),
17947
+ beforeCursor: require_Schema$1.optionalKey(TrimmedNonEmptyString)
17948
+ });
17949
+ /**
17934
17950
  * Page metadata for a windowed thread detail read. `beforeCursor` is opaque and
17935
17951
  * exclusive: passing it back returns the adjacent slice of older turns. `null`
17936
17952
  * means nothing older is left. `snapshotSequence` mirrors the top-level
@@ -18928,6 +18944,10 @@ const OrchestrationRpcSchemas = {
18928
18944
  searchThreads: {
18929
18945
  input: OrchestrationSearchThreadsInput,
18930
18946
  output: OrchestrationSearchThreadsResult
18947
+ },
18948
+ getThreadDetailPage: {
18949
+ input: OrchestrationGetThreadDetailPageInput,
18950
+ output: OrchestrationThreadDetailSnapshot
18931
18951
  }
18932
18952
  };
18933
18953
  var OrchestrationGetSnapshotError = class extends require_Schema$1.TaggedErrorClass()("OrchestrationGetSnapshotError", {
@@ -23006,6 +23026,7 @@ const ServerConfig$1 = require_Schema$1.Struct({
23006
23026
  shellRevealInFileManagerKind: require_Schema$1.optionalKey(FileManagerRevealKind),
23007
23027
  threadResumeCompletionMarker: require_Schema$1.optionalKey(require_Schema$1.Boolean),
23008
23028
  threadSnapshotPagination: require_Schema$1.optionalKey(require_Schema$1.Boolean),
23029
+ threadDetailPageRpc: require_Schema$1.optionalKey(require_Schema$1.Boolean),
23009
23030
  observability: ServerObservability,
23010
23031
  settings: ServerSettings
23011
23032
  });
@@ -26965,6 +26986,17 @@ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchT
26965
26986
  error: OrchestrationSearchThreadsError
26966
26987
  });
26967
26988
  /**
26989
+ * The socket-only path for "load earlier turns": a relay-only connection has
26990
+ * no HTTP endpoint, so this reads the same windowed page the HTTP snapshot
26991
+ * route and `subscribeThread`'s fallback snapshot serve, over the RPC socket
26992
+ * instead.
26993
+ */
26994
+ const WsOrchestrationGetThreadDetailPageRpc = make$30(ORCHESTRATION_WS_METHODS.getThreadDetailPage, {
26995
+ payload: OrchestrationRpcSchemas.getThreadDetailPage.input,
26996
+ success: OrchestrationRpcSchemas.getThreadDetailPage.output,
26997
+ error: OrchestrationGetSnapshotError
26998
+ });
26999
+ /**
26968
27000
  * Resolve `owner/repo` to the repository the caller is allowed to clone.
26969
27001
  *
26970
27002
  * Both of these run against the same CMMD-brokered GitHub credential the web
@@ -27077,7 +27109,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
27077
27109
  payload: ProviderUploadFeedbackInput,
27078
27110
  success: ProviderUploadFeedbackResult,
27079
27111
  error: ProviderUploadFeedbackError
27080
- }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
27112
+ }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsOrchestrationGetThreadDetailPageRpc);
27081
27113
  const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
27082
27114
  /**
27083
27115
  * The direct-WebSocket mount of the same group, with per-method scope
@@ -27150,6 +27182,32 @@ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
27150
27182
  }
27151
27183
  };
27152
27184
  }
27185
+ const WORKTREE_LAUNCH_ACTIVITY_KINDS = new Set([
27186
+ "worktree.requested",
27187
+ "worktree.stage",
27188
+ "worktree.output",
27189
+ "worktree.created",
27190
+ "worktree.failed",
27191
+ "setup-script.requested",
27192
+ "setup-script.started",
27193
+ "setup-script.skipped",
27194
+ "setup-script.failed",
27195
+ "launch.agent-starting",
27196
+ "launch.cancelled"
27197
+ ]);
27198
+ function encodeCmmdRuntimeWorktreeLaunchActivityPayload(kind, payload) {
27199
+ return {
27200
+ ...isRecord$23(payload) ? payload : { sourcePayload: payload },
27201
+ forgeWorktreeLaunchActivity: {
27202
+ schemaVersion: 1,
27203
+ source: "native",
27204
+ kind
27205
+ }
27206
+ };
27207
+ }
27208
+ function isCmmdRuntimeWorktreeLaunchActivityKind(kind) {
27209
+ return WORKTREE_LAUNCH_ACTIVITY_KINDS.has(kind);
27210
+ }
27153
27211
  //#endregion
27154
27212
  //#region ../../packages/contracts/src/cmmdProjectPatch.ts
27155
27213
  const CMMD_PROJECT_PATCH_FIELDS = {
@@ -32260,6 +32318,11 @@ const RELAY_METHOD_POLICY_BASES = [
32260
32318
  requiredScope: "thread.read",
32261
32319
  deliveryClass: "subscription"
32262
32320
  },
32321
+ {
32322
+ method: ORCHESTRATION_WS_METHODS.getThreadDetailPage,
32323
+ requiredScope: "thread.read",
32324
+ deliveryClass: "read"
32325
+ },
32263
32326
  {
32264
32327
  method: WS_METHODS.assetsCreateUrl,
32265
32328
  requiredScope: "thread.read",
@@ -66317,7 +66380,7 @@ function normalizeNumberish(value) {
66317
66380
  }
66318
66381
  //#endregion
66319
66382
  //#region package.json
66320
- var version$1 = "0.13.12";
66383
+ var version$1 = "0.13.14";
66321
66384
  //#endregion
66322
66385
  //#region src/sentry.ts
66323
66386
  const SERVER_APP_NAME = "forge-server";
@@ -66719,6 +66782,129 @@ const respondToAuthError = (error) => require_Schema$1.gen(function* () {
66719
66782
  }));
66720
66783
  return response;
66721
66784
  });
66785
+ //#endregion
66786
+ //#region src/http/rateLimit.ts
66787
+ /**
66788
+ * Per-client, per-route-class rate limiting for Forge's own HTTP surface.
66789
+ *
66790
+ * Scoped to the routes a broken or looping client can hammer before it ever
66791
+ * has an authenticated session — the CMMD device-code poll, the bootstrap and
66792
+ * pairing-token exchanges — plus the authenticated /api/cmmd proxy, which
66793
+ * every desktop UI interaction runs through. None of these had any limit
66794
+ * before FOR-36706: a stuck pairing loop or a poll that never saw `slow_down`
66795
+ * could retry as fast as the network allowed, with nothing to stop it and
66796
+ * nothing telling the client to back off.
66797
+ *
66798
+ * Defaults are generous on purpose. This exists to bound a runaway client,
66799
+ * not to throttle a working one — see `defaultRateLimitPolicies` for the
66800
+ * reasoning behind each number and how to override it.
66801
+ *
66802
+ * Deliberately plain module state, not an Effect service: every route this
66803
+ * guards is a synchronous decision (count, compare, maybe reset a window),
66804
+ * and threading a new Context requirement through the whole server's layer
66805
+ * graph for that would touch every test file that stands up any of these
66806
+ * routes. `cmmdJwtResolver.ts`'s in-flight-refresh map is the same call.
66807
+ */
66808
+ function envInt(name, fallback) {
66809
+ const raw = process.env[name];
66810
+ if (!raw) return fallback;
66811
+ const parsed = Number(raw);
66812
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
66813
+ }
66814
+ /**
66815
+ * One reasoning line per class, so a future change to a default has to
66816
+ * confront why the old one was picked:
66817
+ *
66818
+ * - `auth-device-poll` (`/api/auth/cmmd-oauth/device` and `/token`): RFC 8628
66819
+ * polls every `interval` seconds, 5 by default, so a well-behaved client
66820
+ * sits under 12/min. 120/min leaves room for several concurrent sign-in
66821
+ * attempts and CMMD's own `slow_down` handling without ever tripping on
66822
+ * normal use, while still bounding a client that ignores `slow_down`.
66823
+ * - `auth-bootstrap` (`/api/auth/bootstrap*`, the bearer bootstrap routes):
66824
+ * a working client bootstraps once per sign-in. 30/min is generous headroom
66825
+ * for retries after a network blip, not a budget anyone should approach.
66826
+ * - `auth-token-exchange` (`/oauth/token`, the vendored pairing exchange):
66827
+ * same shape as bootstrap — one pairing attempt, occasionally retried.
66828
+ * - `cmmd-proxy` (every `/api/cmmd/*` route): this is the one real desktop
66829
+ * traffic actually leans on — task lists, project browsing, comments, all
66830
+ * day. 1200/min (20/sec) is set high enough that no normal session should
66831
+ * ever see it; it exists for a retry loop or a runaway resync, not for
66832
+ * ordinary use.
66833
+ */
66834
+ function buildPoliciesFromEnv() {
66835
+ return {
66836
+ "auth-device-poll": {
66837
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_MAX", 120),
66838
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_WINDOW_SECONDS", 60) * 1e3
66839
+ },
66840
+ "auth-bootstrap": {
66841
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_MAX", 30),
66842
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_WINDOW_SECONDS", 60) * 1e3
66843
+ },
66844
+ "auth-token-exchange": {
66845
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_MAX", 30),
66846
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_WINDOW_SECONDS", 60) * 1e3
66847
+ },
66848
+ "cmmd-proxy": {
66849
+ maxRequests: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_MAX", 1200),
66850
+ windowMs: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_WINDOW_SECONDS", 60) * 1e3
66851
+ }
66852
+ };
66853
+ }
66854
+ let policies = buildPoliciesFromEnv();
66855
+ const windows = /* @__PURE__ */ new Map();
66856
+ function windowKey(routeClass, clientKey) {
66857
+ return `${routeClass}\0${clientKey}`;
66858
+ }
66859
+ /**
66860
+ * Fixed-window counting: cheap, and precise enough for a loop-guard rather
66861
+ * than a security boundary. The one imprecision this trades away — up to 2x
66862
+ * the configured rate right at a window boundary — matters for an attacker
66863
+ * budgeting requests against the limiter's clock, not for the runaway
66864
+ * clients this exists to catch.
66865
+ */
66866
+ function checkHttpRateLimit(input) {
66867
+ const policy = policies[input.routeClass];
66868
+ const nowMs = Date.now();
66869
+ const key = windowKey(input.routeClass, input.clientKey);
66870
+ const existing = windows.get(key);
66871
+ const windowIsLive = existing !== void 0 && nowMs - existing.windowStartMs < policy.windowMs;
66872
+ const windowStartMs = windowIsLive ? existing.windowStartMs : nowMs;
66873
+ const count = (windowIsLive ? existing.count : 0) + 1;
66874
+ windows.set(key, {
66875
+ windowStartMs,
66876
+ count
66877
+ });
66878
+ return {
66879
+ allowed: count <= policy.maxRequests,
66880
+ retryAfterSeconds: Math.max(0, Math.ceil((windowStartMs + policy.windowMs - nowMs) / 1e3))
66881
+ };
66882
+ }
66883
+ /**
66884
+ * Checks the limiter for one request and, when the caller is over budget,
66885
+ * returns the 429 to send instead of running the route — `null` means
66886
+ * proceed. Every route this guards is expected to call this first and return
66887
+ * early on a non-null result, before doing any other work.
66888
+ */
66889
+ function rateLimitOrRespond(input) {
66890
+ const decision = checkHttpRateLimit(input);
66891
+ if (decision.allowed) return null;
66892
+ return require_HttpServer.jsonUnsafe({ error: "Too many requests. Try again later." }, {
66893
+ status: 429,
66894
+ headers: { "retry-after": String(decision.retryAfterSeconds) }
66895
+ });
66896
+ }
66897
+ /**
66898
+ * The client identity for a route with no session yet — the device-code
66899
+ * poll, bootstrap and pairing-token exchange all run before Forge has issued
66900
+ * one. IP address is the only stable-enough signal available at that point;
66901
+ * the authenticated `/api/cmmd` proxy keys on the session instead (see its
66902
+ * call site), so one desktop and one phone sharing a NAT never share a
66903
+ * budget there.
66904
+ */
66905
+ function clientKeyFromRequest(request) {
66906
+ return normalizeIpAddress(require_Schema$1.getOrNull(request.remoteAddress)) ?? "unknown";
66907
+ }
66722
66908
  const OAUTH_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
66723
66909
  /**
66724
66910
  * The one exchange this endpoint performs, or the reason it will not.
@@ -68566,6 +68752,11 @@ function commandOAuthProxyStatus(path, status, body) {
68566
68752
  }
68567
68753
  const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Schema$1.gen(function* () {
68568
68754
  const request = yield* require_HttpServer.HttpServerRequest;
68755
+ const limited = rateLimitOrRespond({
68756
+ routeClass: "auth-bootstrap",
68757
+ clientKey: clientKeyFromRequest(request)
68758
+ });
68759
+ if (limited) return limited;
68569
68760
  const serverAuth = yield* ServerAuth;
68570
68761
  const sessions = yield* SessionCredentialService;
68571
68762
  const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
@@ -68583,6 +68774,11 @@ const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", require_Sch
68583
68774
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68584
68775
  const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command", require_Schema$1.gen(function* () {
68585
68776
  const request = yield* require_HttpServer.HttpServerRequest;
68777
+ const limited = rateLimitOrRespond({
68778
+ routeClass: "auth-bootstrap",
68779
+ clientKey: clientKeyFromRequest(request)
68780
+ });
68781
+ if (limited) return limited;
68586
68782
  const serverAuth = yield* ServerAuth;
68587
68783
  const sessions = yield* SessionCredentialService;
68588
68784
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68631,6 +68827,11 @@ const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/comman
68631
68827
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68632
68828
  const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command/bearer", require_Schema$1.gen(function* () {
68633
68829
  const request = yield* require_HttpServer.HttpServerRequest;
68830
+ const limited = rateLimitOrRespond({
68831
+ routeClass: "auth-bootstrap",
68832
+ clientKey: clientKeyFromRequest(request)
68833
+ });
68834
+ if (limited) return limited;
68634
68835
  const serverAuth = yield* ServerAuth;
68635
68836
  const sessions = yield* SessionCredentialService;
68636
68837
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68679,6 +68880,11 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68679
68880
  }, { status: 200 });
68680
68881
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68681
68882
  const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device", require_Schema$1.gen(function* () {
68883
+ const limited = rateLimitOrRespond({
68884
+ routeClass: "auth-device-poll",
68885
+ clientKey: clientKeyFromRequest(yield* require_HttpServer.HttpServerRequest)
68886
+ });
68887
+ if (limited) return limited;
68682
68888
  const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthDeviceInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
68683
68889
  message: "Invalid CMMD OAuth device payload.",
68684
68890
  status: 400,
@@ -68690,7 +68896,17 @@ const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device
68690
68896
  ...payload.scope ? { scope: payload.scope } : {}
68691
68897
  });
68692
68898
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68899
+ /**
68900
+ * The device-code poll. Called every `interval` seconds by design (RFC
68901
+ * 8628), which is exactly why FOR-36706 puts a limiter on it: a client that
68902
+ * ignores `slow_down` — a bug, not a person — has no other backstop here.
68903
+ */
68693
68904
  const authCmmdOAuthTokenRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/token", require_Schema$1.gen(function* () {
68905
+ const limited = rateLimitOrRespond({
68906
+ routeClass: "auth-device-poll",
68907
+ clientKey: clientKeyFromRequest(yield* require_HttpServer.HttpServerRequest)
68908
+ });
68909
+ if (limited) return limited;
68694
68910
  const payload = yield* require_HttpServer.schemaBodyJson(CommandOAuthTokenInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
68695
68911
  message: "Invalid CMMD OAuth token payload.",
68696
68912
  status: 400,
@@ -68985,6 +69201,11 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", require_Sch
68985
69201
  }).pipe(require_Schema$1.catchTag("AuthError", (error) => respondToAuthError(error))));
68986
69202
  const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer", require_Schema$1.gen(function* () {
68987
69203
  const request = yield* require_HttpServer.HttpServerRequest;
69204
+ const limited = rateLimitOrRespond({
69205
+ routeClass: "auth-bootstrap",
69206
+ clientKey: clientKeyFromRequest(request)
69207
+ });
69208
+ if (limited) return limited;
68988
69209
  const serverAuth = yield* ServerAuth;
68989
69210
  const payload = yield* require_HttpServer.schemaBodyJson(AuthBootstrapInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
68990
69211
  message: "Invalid bootstrap payload.",
@@ -69017,6 +69238,11 @@ const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer"
69017
69238
  */
69018
69239
  const authOAuthTokenExchangeRouteLayer = add$1("POST", "/oauth/token", require_Schema$1.gen(function* () {
69019
69240
  const request = yield* require_HttpServer.HttpServerRequest;
69241
+ const limited = rateLimitOrRespond({
69242
+ routeClass: "auth-token-exchange",
69243
+ clientKey: clientKeyFromRequest(request)
69244
+ });
69245
+ if (limited) return limited;
69020
69246
  const serverAuth = yield* ServerAuth;
69021
69247
  const payload = yield* require_HttpServer.schemaBodyUrlParams(OAuthTokenExchangeInput).pipe(require_Schema$1.mapError((cause) => new AuthError({
69022
69248
  message: "Invalid token exchange payload.",
@@ -71242,9 +71468,12 @@ function trySubjectFallback(sessionId, subject, logPrefix) {
71242
71468
  * Fail-closed: never returns a non-user / malformed Bearer. Callers must map
71243
71469
  * null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
71244
71470
  */
71245
- function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false }) {
71471
+ function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
71246
71472
  const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
71247
- if (!forceRefresh && cached && !isJwtExpiredForSession(sessionId)) return require_Schema$1.succeed(cached);
71473
+ if (cached && !isJwtExpiredForSession(sessionId)) {
71474
+ if (!forceRefresh) return require_Schema$1.succeed(cached);
71475
+ if (staleAccessToken && cached !== staleAccessToken) return require_Schema$1.succeed(cached);
71476
+ }
71248
71477
  const normalizedSubject = subject?.trim() || null;
71249
71478
  if (!normalizedSubject && !forceRefresh) return require_Schema$1.succeed(null);
71250
71479
  return require_Schema$1.gen(function* () {
@@ -72081,6 +72310,11 @@ function buildProxyRouteLayer(route) {
72081
72310
  const handler = require_Schema$1.gen(function* () {
72082
72311
  const req = yield* require_HttpServer.HttpServerRequest;
72083
72312
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
72313
+ const limited = rateLimitOrRespond({
72314
+ routeClass: "cmmd-proxy",
72315
+ clientKey: session.sessionId
72316
+ });
72317
+ if (limited) return limited;
72084
72318
  const pathParams = yield* params;
72085
72319
  const cmmdPath = substitutePath(route.cmmdPath, pathParams);
72086
72320
  if (!cmmdPath) return require_HttpServer.jsonUnsafe({ error: "Missing required path parameter." }, { status: 400 });
@@ -72141,6 +72375,11 @@ function buildProxyHandlerEffect(route) {
72141
72375
  return require_Schema$1.gen(function* () {
72142
72376
  const req = yield* require_HttpServer.HttpServerRequest;
72143
72377
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
72378
+ const limited = rateLimitOrRespond({
72379
+ routeClass: "cmmd-proxy",
72380
+ clientKey: session.sessionId
72381
+ });
72382
+ if (limited) return limited;
72144
72383
  if (!readCommandAccessTokenForSession(session.sessionId)) return yield* respondCmmdConnectionRequired(session.sessionId);
72145
72384
  const parsed = yield* route.parse(req);
72146
72385
  if (!parsed.ok) return require_HttpServer.jsonUnsafe(parsed.body, { status: parsed.status });
@@ -98040,6 +98279,26 @@ function serializeNativeToolActivity(activity) {
98040
98279
  }
98041
98280
  };
98042
98281
  }
98282
+ function serializeWorktreeLaunchActivity(activity) {
98283
+ if (decodeWorktreeLaunchActivityKind(activity.kind) === null) return null;
98284
+ const summary = text(activity.summary, 4e3);
98285
+ if (summary === null) return null;
98286
+ return {
98287
+ sourceSequence: null,
98288
+ event: {
98289
+ eventId: `activity:${activity.id}`,
98290
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
98291
+ ...eventTurn(activity.turnId),
98292
+ kind: "tool_call",
98293
+ summary,
98294
+ status: "completed",
98295
+ payload: encodeCmmdRuntimeWorktreeLaunchActivityPayload(activity.kind, activity.payload)
98296
+ }
98297
+ };
98298
+ }
98299
+ function decodeWorktreeLaunchActivityKind(kind) {
98300
+ return isCmmdRuntimeWorktreeLaunchActivityKind(kind) ? kind : null;
98301
+ }
98043
98302
  function isReplicatedFailureActivity(activity) {
98044
98303
  return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
98045
98304
  }
@@ -98420,12 +98679,17 @@ function serializeRuntimeThreadEvents(thread) {
98420
98679
  const event = serializeNativeToolActivity(activity);
98421
98680
  return event === null ? [] : [event];
98422
98681
  });
98682
+ const worktreeLaunchActivities = thread.activities.flatMap((activity) => {
98683
+ const event = serializeWorktreeLaunchActivity(activity);
98684
+ return event === null ? [] : [event];
98685
+ });
98423
98686
  const recordedTurns = serializeRecordedTurns(thread);
98424
98687
  const textGenerations = serializeTextGenerations(thread);
98425
98688
  return [
98426
98689
  ...messages,
98427
98690
  ...replicaActivities,
98428
98691
  ...toolActivities,
98692
+ ...worktreeLaunchActivities,
98429
98693
  ...failureActivities,
98430
98694
  ...recordedTurns,
98431
98695
  ...textGenerations
@@ -106589,6 +106853,39 @@ function loadInitialShellSnapshot(input) {
106589
106853
  return yield* input.projectionSnapshotQuery.getShellSnapshot();
106590
106854
  });
106591
106855
  }
106856
+ /**
106857
+ * The thread snapshot a subscription falls back to, windowed when the
106858
+ * subscriber asked for a window. A missing thread is a failure rather than an
106859
+ * empty snapshot: the subscriber asked about a thread this server does not
106860
+ * have, and saying so is the only honest answer.
106861
+ *
106862
+ * Also the read behind `getThreadDetailPage`, the socket RPC a relay-only
106863
+ * connection uses for "load earlier turns" in place of the HTTP snapshot
106864
+ * route: same window, same pure `windowThreadDetail`, so the two reads cannot
106865
+ * disagree about what a page is.
106866
+ */
106867
+ function loadThreadDetailSnapshot(input) {
106868
+ return require_Schema$1.gen(function* () {
106869
+ const [threadDetail, snapshotSequence] = yield* require_Schema$1.all([input.projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(require_Schema$1.mapError((cause) => new OrchestrationGetSnapshotError({
106870
+ message: `Failed to load thread ${input.threadId}`,
106871
+ cause
106872
+ }))), input.orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence))]);
106873
+ if (require_Schema$1.isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106874
+ message: `Thread ${input.threadId} was not found`,
106875
+ cause: input.threadId
106876
+ });
106877
+ const windowed = windowThreadDetail({
106878
+ thread: threadDetail.value,
106879
+ window: input.window,
106880
+ snapshotSequence
106881
+ });
106882
+ return {
106883
+ snapshotSequence,
106884
+ thread: windowed.thread,
106885
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
106886
+ };
106887
+ });
106888
+ }
106592
106889
  function makeSnapshotThenLiveStream(input) {
106593
106890
  return require_Path.unwrap(require_Schema$1.gen(function* () {
106594
106891
  const liveEvents = yield* input.subscribeLiveEvents;
@@ -106815,32 +107112,6 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106815
107112
  }, 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")));
106816
107113
  return startup.enqueueCommand(dispatchEffect).pipe(require_Schema$1.mapError((cause) => toDispatchCommandError(cause, "Failed to dispatch orchestration command")));
106817
107114
  };
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
- });
106844
107115
  const loadServerConfig = require_Schema$1.gen(function* () {
106845
107116
  const keybindingsConfig = yield* keybindings.loadConfigState;
106846
107117
  const providers = yield* providerRegistry.getProviders;
@@ -106864,6 +107135,7 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106864
107135
  },
106865
107136
  threadResumeCompletionMarker: true,
106866
107137
  threadSnapshotPagination: true,
107138
+ threadDetailPageRpc: true,
106867
107139
  observability: {
106868
107140
  logsDirectoryPath: config.logsDir,
106869
107141
  localTracingEnabled: true,
@@ -106964,12 +107236,26 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(r
106964
107236
  [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, require_Schema$1.succeed(makeThreadSubscriptionStream(input, {
106965
107237
  subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(require_Schema$1.map((events) => events.pipe(require_Path.filter(isThreadDetailEventFor(input.threadId))))),
106966
107238
  currentSequence: orchestrationEngine.getReadModel().pipe(require_Schema$1.map((readModel) => readModel.snapshotSequence)),
106967
- loadSnapshot: loadThreadDetailSnapshot(input.threadId, input.turnLimit),
107239
+ loadSnapshot: loadThreadDetailSnapshot({
107240
+ threadId: input.threadId,
107241
+ window: input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
107242
+ projectionSnapshotQuery,
107243
+ orchestrationEngine
107244
+ }),
106968
107245
  replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(require_Path.filter(isThreadDetailEventFor(input.threadId)), require_Path.mapError((cause) => new OrchestrationGetSnapshotError({
106969
107246
  message: `Failed to replay thread ${input.threadId}`,
106970
107247
  cause
106971
107248
  })))
106972
107249
  })), { "rpc.aggregate": "orchestration" }),
107250
+ [ORCHESTRATION_WS_METHODS.getThreadDetailPage]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.getThreadDetailPage, loadThreadDetailSnapshot({
107251
+ threadId: input.threadId,
107252
+ window: {
107253
+ ...input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
107254
+ ...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor }
107255
+ },
107256
+ projectionSnapshotQuery,
107257
+ orchestrationEngine
107258
+ }), { "rpc.aggregate": "orchestration" }),
106973
107259
  [WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
106974
107260
  [WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(require_Schema$1.map((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
106975
107261
  [WS_METHODS.serverStartProviderAuth]: (input) => observeRpcEffect(WS_METHODS.serverStartProviderAuth, require_Schema$1.gen(function* () {
@@ -213039,6 +213325,8 @@ const PUBLIC_PROVIDER_LEAK_PATTERNS = [
213039
213325
  /\bCURSOR_API_KEY\b/,
213040
213326
  /\bOPENCODE_API_KEY\b/,
213041
213327
  /\bforge_proxy_[A-Za-z0-9._-]+\b/,
213328
+ /\bProviderValidationError\b/,
213329
+ /\/forge-server\/node_modules\//,
213042
213330
  /https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
213043
213331
  /https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
213044
213332
  ];
@@ -213048,6 +213336,12 @@ function isProviderRateOrQuotaError(message) {
213048
213336
  const normalized = message.toLowerCase();
213049
213337
  return /\b(rate[\s-]?limit|too\s+many\s+requests|usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/.test(normalized) || /\b429\b/.test(normalized);
213050
213338
  }
213339
+ function providerLifecycleOrAccountCopy(message) {
213340
+ if (/\bbound to provider account\b/.test(message) && /\bcannot use provider account\b/.test(message)) return "Forge could not switch this conversation to the selected AI account. Retry the account switch.";
213341
+ if (/\bforge_runtime_repairing\b/.test(message) || /\bthis turn was interrupted because the environment is being repaired\b/.test(message)) return "This turn was interrupted because the environment is being repaired. Retry after the repair finishes.";
213342
+ if (/\bforge_runtime_stopped\b/.test(message) || /\bthis turn was interrupted because the environment was stopped\b/.test(message)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
213343
+ return null;
213344
+ }
213051
213345
  /**
213052
213346
  * Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
213053
213347
  *
@@ -213060,7 +213354,8 @@ function isProviderRateOrQuotaError(message) {
213060
213354
  function categorizePublicProviderError(message) {
213061
213355
  if (!message) return null;
213062
213356
  const m = message.toLowerCase();
213063
- if (/\bforge_runtime_stopped\b/.test(m) || /\bthis turn was interrupted because the environment was stopped\b/.test(m)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
213357
+ const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
213358
+ if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
213064
213359
  if (/\bforge_provider_model_not_allowed\b/.test(m) || /\bnot\s+scoped\s+to\s+the\s+requested\s+model\b/.test(m)) return "The workspace AI credential does not cover the model this turn asked for. Retry, or pick that model for the thread.";
213065
213360
  if (/\bmodel\b[\s\S]*\b(not\s+(found|available|supported)|does\s+not\s+exist|unsupported|invalid|unknown)\b/.test(m) || /\b(unsupported|unknown|invalid)\b[\s\S]*\bmodel\b/.test(m) || /\b(model_not_found|invalid_model|model_not_supported)\b/.test(m)) return "The selected model isn't available right now. Pick a different model and retry.";
213066
213361
  if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
@@ -213240,13 +213535,20 @@ function claudeConfigDirectory(env) {
213240
213535
  return null;
213241
213536
  }
213242
213537
  }
213243
- /** The fallback is allowed only for the exact credential bytes CMMD signed. */
213244
- function primaryClaudeCredentialMatchesRevision(env, revision) {
213538
+ /**
213539
+ * The fallback is allowed only when the primary on-disk credential is
213540
+ * byte-identical to what CMMD prepared for the selected account. A ticket's
213541
+ * `credentialRevisionHash` cannot express that: it hashes the encrypted row
213542
+ * CMMD stores, not the decrypted bytes this process ever sees. Comparing
213543
+ * files directly is the only check that means what the fallback needs.
213544
+ */
213545
+ function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
213245
213546
  const directory = claudeConfigDirectory(env);
213246
- if (!directory || !/^[a-f0-9]{64}$/.test(revision)) return false;
213547
+ if (!directory) return false;
213247
213548
  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));
213549
+ const primary = (0, node_fs.readFileSync)((0, node_path.join)(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213550
+ const prepared = (0, node_fs.readFileSync)((0, node_path.join)(preparedDirectory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213551
+ return hasAccessToken(JSON.parse(primary)) && (0, node_crypto.createHash)("sha256").update(primary).digest("hex") === (0, node_crypto.createHash)("sha256").update(prepared).digest("hex");
213250
213552
  } catch {
213251
213553
  return false;
213252
213554
  }
@@ -215636,6 +215938,7 @@ function buildClaudeSessionContext(input) {
215636
215938
  pendingUserInputs: input.pendingUserInputs,
215637
215939
  turns: [],
215638
215940
  inFlightTools: input.inFlightTools,
215941
+ taskPlan: makeClaudeTaskPlanState(),
215639
215942
  turnState: void 0,
215640
215943
  lastKnownContextWindow: void 0,
215641
215944
  lastKnownTokenUsage: void 0,
@@ -216145,6 +216448,7 @@ function classifyToolItemType(toolName) {
216145
216448
  if (normalized.includes("agent")) return "collab_agent_tool_call";
216146
216449
  if (normalized === "task" || normalized === "agent" || normalized.includes("subagent") || normalized.includes("sub-agent")) return "collab_agent_tool_call";
216147
216450
  if (normalized.includes("bash") || normalized.includes("command") || normalized.includes("shell") || normalized.includes("terminal")) return "command_execution";
216451
+ if (isTaskBookkeepingTool(normalized)) return "dynamic_tool_call";
216148
216452
  if (normalized.includes("edit") || normalized.includes("write") || normalized.includes("file") || normalized.includes("patch") || normalized.includes("replace") || normalized.includes("create") || normalized.includes("delete")) return "file_change";
216149
216453
  if (normalized.includes("browser")) return "browser_automation";
216150
216454
  if (normalized.includes("mcp")) return "mcp_tool_call";
@@ -216164,6 +216468,88 @@ function classifyRequestType(toolName) {
216164
216468
  function isTodoTool(toolName) {
216165
216469
  return toolName.toLowerCase().includes("todowrite");
216166
216470
  }
216471
+ function makeClaudeTaskPlanState() {
216472
+ return {
216473
+ entries: [],
216474
+ createsByToolUseId: /* @__PURE__ */ new Map()
216475
+ };
216476
+ }
216477
+ function taskPlanToolFor(toolName) {
216478
+ const normalized = toolName.toLowerCase();
216479
+ if (normalized === "taskcreate") return "TaskCreate";
216480
+ if (normalized === "taskupdate") return "TaskUpdate";
216481
+ }
216482
+ /** Task bookkeeping tools (create, update, list, get) are not file changes. */
216483
+ function isTaskBookkeepingTool(normalizedToolName) {
216484
+ return /^task(create|update|list|get)$/.test(normalizedToolName);
216485
+ }
216486
+ function taskPlanStatus(value) {
216487
+ return value === "completed" ? "completed" : value === "in_progress" ? "inProgress" : "pending";
216488
+ }
216489
+ function taskPlanSnapshot(state) {
216490
+ return state.entries.map((entry) => ({
216491
+ step: entry.step,
216492
+ status: entry.status
216493
+ }));
216494
+ }
216495
+ /**
216496
+ * Applies one TaskCreate or TaskUpdate input to the session's task list and
216497
+ * returns the plan to publish, or null when the input carries nothing yet.
216498
+ * Input JSON streams in pieces, so the same tool use can arrive more than once;
216499
+ * creates are keyed by tool use id so a repeat updates the entry in place.
216500
+ */
216501
+ function applyTaskPlanToolInput(state, tool, toolUseId, input) {
216502
+ if (tool === "TaskCreate") {
216503
+ const subject = typeof input.subject === "string" ? input.subject.trim() : "";
216504
+ const existing = state.createsByToolUseId.get(toolUseId);
216505
+ if (existing) {
216506
+ if (subject.length > 0) existing.step = subject;
216507
+ return taskPlanSnapshot(state);
216508
+ }
216509
+ const entry = {
216510
+ id: void 0,
216511
+ step: subject.length > 0 ? subject : "Task",
216512
+ status: taskPlanStatus(input.status)
216513
+ };
216514
+ state.entries.push(entry);
216515
+ state.createsByToolUseId.set(toolUseId, entry);
216516
+ return taskPlanSnapshot(state);
216517
+ }
216518
+ const taskId = typeof input.taskId === "string" ? input.taskId.trim() : typeof input.taskId === "number" ? String(input.taskId) : "";
216519
+ if (taskId.length === 0) return null;
216520
+ let entry = state.entries.find((candidate) => candidate.id === taskId);
216521
+ if (!entry) {
216522
+ const position = Number.parseInt(taskId, 10);
216523
+ const candidate = Number.isInteger(position) ? state.entries[position - 1] : void 0;
216524
+ if (candidate && candidate.id === void 0) {
216525
+ candidate.id = taskId;
216526
+ entry = candidate;
216527
+ }
216528
+ }
216529
+ if (input.status === "deleted") {
216530
+ if (entry) state.entries.splice(state.entries.indexOf(entry), 1);
216531
+ return taskPlanSnapshot(state);
216532
+ }
216533
+ if (!entry) {
216534
+ entry = {
216535
+ id: taskId,
216536
+ step: `Task #${taskId}`,
216537
+ status: "pending"
216538
+ };
216539
+ state.entries.push(entry);
216540
+ }
216541
+ if (typeof input.subject === "string" && input.subject.trim().length > 0) entry.step = input.subject.trim();
216542
+ if (input.status !== void 0) entry.status = taskPlanStatus(input.status);
216543
+ return taskPlanSnapshot(state);
216544
+ }
216545
+ const TASK_CREATE_RESULT_ID = /Task #(\S+) created/;
216546
+ /** Binds the id Claude Code assigned in the TaskCreate result to the entry. */
216547
+ function bindTaskPlanCreateResult(state, toolUseId, resultText) {
216548
+ const entry = state.createsByToolUseId.get(toolUseId);
216549
+ if (!entry || entry.id !== void 0) return;
216550
+ const match = TASK_CREATE_RESULT_ID.exec(resultText);
216551
+ if (match?.[1]) entry.id = match[1];
216552
+ }
216167
216553
  function extractPlanStepsFromTodoInput(input) {
216168
216554
  const todos = input.todos;
216169
216555
  if (!Array.isArray(todos) || todos.length === 0) return null;
@@ -216881,9 +217267,9 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
216881
217267
  }
216882
217268
  });
216883
217269
  });
216884
- const emitPlanUpdateFromToolInput = require_Schema$1.fn("emitPlanUpdateFromToolInput")(function* (context, toolName, parsedInput) {
216885
- if (!isTodoTool(toolName)) return;
216886
- const planSteps = extractPlanStepsFromTodoInput(parsedInput);
217270
+ const emitPlanUpdateFromToolInput = require_Schema$1.fn("emitPlanUpdateFromToolInput")(function* (context, toolName, toolUseId, parsedInput) {
217271
+ const taskPlanTool = taskPlanToolFor(toolName);
217272
+ const planSteps = taskPlanTool ? applyTaskPlanToolInput(context.taskPlan, taskPlanTool, toolUseId, parsedInput) : isTodoTool(toolName) ? extractPlanStepsFromTodoInput(parsedInput) : null;
216887
217273
  if (!planSteps || planSteps.length === 0) return;
216888
217274
  const planStamp = yield* makeEventStamp();
216889
217275
  yield* offerRuntimeEvent({
@@ -216941,7 +217327,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
216941
217327
  };
216942
217328
  context.inFlightTools.set(event.index, nextTool);
216943
217329
  yield* emitToolInputUpdated(context, message, nextTool);
216944
- if (state.parsedInput) yield* emitPlanUpdateFromToolInput(context, nextTool.toolName, state.parsedInput);
217330
+ if (state.parsedInput) yield* emitPlanUpdateFromToolInput(context, nextTool.toolName, nextTool.itemId, state.parsedInput);
216945
217331
  });
216946
217332
  const handleContentBlockDeltaEvent = require_Schema$1.fn("handleContentBlockDeltaEvent")(function* (context, message, event) {
216947
217333
  yield* handleTextOrThinkingDelta(context, message, event);
@@ -216994,6 +217380,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
216994
217380
  if (!toolEntry) continue;
216995
217381
  const [index, tool] = toolEntry;
216996
217382
  const itemStatus = toolResult.isError ? "failed" : "completed";
217383
+ if (taskPlanToolFor(tool.toolName) === "TaskCreate" && !toolResult.isError) bindTaskPlanCreateResult(context.taskPlan, tool.itemId, toolResult.text);
216997
217384
  const toolData = {
216998
217385
  toolName: tool.toolName,
216999
217386
  input: tool.input,
@@ -217775,7 +218162,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
217775
218162
  baseDir: serverConfig.baseDir,
217776
218163
  accountIdentity: selectedAccountIdentity,
217777
218164
  probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
217778
- primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesRevision(process.env, preparedCredential.credentialRevisionHash)
218165
+ primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
217779
218166
  });
217780
218167
  if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
217781
218168
  provider: PROVIDER$11,
@@ -232944,7 +233331,7 @@ async function acquireSelectedClaudeCredential(expected, transport) {
232944
233331
  const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
232945
233332
  if (!token || !claims) throw new Error("invalid_ticket");
232946
233333
  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");
233334
+ if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
232948
233335
  return {
232949
233336
  claims,
232950
233337
  credentialJson
@@ -292168,6 +292555,10 @@ const make$8 = require_Schema$1.gen(function* () {
292168
292555
  shouldRestartForAccountChange,
292169
292556
  hasResumeCursor: resumeCursor !== void 0
292170
292557
  });
292558
+ if (shouldRestartForAccountChange) yield* providerService.stopSession({
292559
+ threadId,
292560
+ preserveResumeState: true
292561
+ });
292171
292562
  const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
292172
292563
  yield* require_Schema$1.logInfo("provider command reactor restarted provider session", {
292173
292564
  threadId,
@@ -298152,7 +298543,7 @@ function resolveBuildCommitFromEnv(env) {
298152
298543
  * environment descriptor down instead of reporting an honest "unknown".
298153
298544
  */
298154
298545
  function readBakedBuildCommit() {
298155
- return "e51896589531a57a458ca5c2430dfd54cdc16476";
298546
+ return "dea91e954d3d2f23154802ae304412fabe90dbbd";
298156
298547
  }
298157
298548
  async function resolveServerBuildCommit(input) {
298158
298549
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -299577,20 +299968,20 @@ const generatePairingToken = () => {
299577
299968
  const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
299578
299969
  return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
299579
299970
  };
299971
+ const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
299972
+ message,
299973
+ status: 401
299974
+ });
299975
+ const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
299976
+ message,
299977
+ status: 500,
299978
+ cause
299979
+ });
299580
299980
  const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299581
299981
  const config = yield* ServerConfig;
299582
299982
  const pairingLinks = yield* AuthPairingLinkRepository;
299583
299983
  const seededGrantsRef = yield* require_HttpServer.make$3(/* @__PURE__ */ new Map());
299584
299984
  const changesPubSub = yield* require_Path.unbounded$1();
299585
- const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
299586
- message,
299587
- status: 401
299588
- });
299589
- const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
299590
- message,
299591
- status: 500,
299592
- cause
299593
- });
299594
299985
  const seedGrant = (credential, grant) => require_HttpServer.update(seededGrantsRef, (current) => {
299595
299986
  const next = new Map(current);
299596
299987
  next.set(credential, grant);
@@ -299605,19 +299996,19 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299605
299996
  id
299606
299997
  }).pipe(require_Schema$1.asVoid);
299607
299998
  if (config.desktopBootstrapToken) {
299608
- const now$2 = yield* require_Schema$1.now;
299999
+ const now$3 = yield* require_Schema$1.now;
299609
300000
  yield* seedGrant(config.desktopBootstrapToken, {
299610
300001
  method: "desktop-bootstrap",
299611
300002
  role: "owner",
299612
300003
  subject: "desktop-bootstrap",
299613
- expiresAt: require_Schema$1.add(now$2, { milliseconds: require_Schema$1.toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
300004
+ expiresAt: require_Schema$1.add(now$3, { milliseconds: require_Schema$1.toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
299614
300005
  remainingUses: "unbounded"
299615
300006
  });
299616
300007
  }
299617
300008
  const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
299618
300009
  const listActive = () => require_Schema$1.gen(function* () {
299619
- const now$5 = yield* require_Schema$1.now;
299620
- return (yield* pairingLinks.listActive({ now: now$5 })).map((row) => row.label ? {
300010
+ const now$2 = yield* require_Schema$1.now;
300011
+ return (yield* pairingLinks.listActive({ now: now$2 })).map((row) => row.label ? {
299621
300012
  id: row.id,
299622
300013
  credential: row.credential,
299623
300014
  role: row.role,
@@ -299647,8 +300038,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299647
300038
  const id = crypto.randomUUID();
299648
300039
  const credential = generatePairingToken();
299649
300040
  const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
299650
- const now$4 = yield* require_Schema$1.now;
299651
- const expiresAt = require_Schema$1.add(now$4, { milliseconds: require_Schema$1.toMillis(ttl) });
300041
+ const now$5 = yield* require_Schema$1.now;
300042
+ const expiresAt = require_Schema$1.add(now$5, { milliseconds: require_Schema$1.toMillis(ttl) });
299652
300043
  const issued = {
299653
300044
  id,
299654
300045
  credential,
@@ -299662,7 +300053,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299662
300053
  role: input?.role ?? "client",
299663
300054
  subject: input?.subject ?? "one-time-token",
299664
300055
  label: input?.label ?? null,
299665
- createdAt: now$4,
300056
+ createdAt: now$5,
299666
300057
  expiresAt
299667
300058
  });
299668
300059
  yield* emitUpsert({
@@ -299671,13 +300062,13 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299671
300062
  role: input?.role ?? "client",
299672
300063
  subject: input?.subject ?? "one-time-token",
299673
300064
  ...input?.label ? { label: input.label } : {},
299674
- createdAt: now$4,
300065
+ createdAt: now$5,
299675
300066
  expiresAt
299676
300067
  });
299677
300068
  return issued;
299678
300069
  }).pipe(require_Schema$1.mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
299679
300070
  const consume = (credential) => require_Schema$1.gen(function* () {
299680
- const now$3 = yield* require_Schema$1.now;
300071
+ const now$4 = yield* require_Schema$1.now;
299681
300072
  const seededResult = yield* require_HttpServer.modify(seededGrantsRef, (current) => {
299682
300073
  const grant = current.get(credential);
299683
300074
  if (!grant) return [{
@@ -299686,7 +300077,7 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299686
300077
  error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
299687
300078
  }, current];
299688
300079
  const next = new Map(current);
299689
- if (require_Schema$1.isGreaterThanOrEqualTo$1(now$3, grant.expiresAt)) {
300080
+ if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
299690
300081
  next.delete(credential);
299691
300082
  return [{
299692
300083
  _tag: "error",
@@ -299715,8 +300106,8 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299715
300106
  if (seededResult.reason !== "not-found") return yield* seededResult.error;
299716
300107
  const consumed = yield* pairingLinks.consumeAvailable({
299717
300108
  credential,
299718
- consumedAt: now$3,
299719
- now: now$3
300109
+ consumedAt: now$4,
300110
+ now: now$4
299720
300111
  });
299721
300112
  if (require_Schema$1.isSome(consumed)) {
299722
300113
  yield* emitRemoved(consumed.value.id);
@@ -299732,9 +300123,9 @@ const makeBootstrapCredentialService = require_Schema$1.gen(function* () {
299732
300123
  if (require_Schema$1.isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
299733
300124
  if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
299734
300125
  if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
299735
- if (require_Schema$1.isGreaterThanOrEqualTo$1(now$3, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
300126
+ if (require_Schema$1.isGreaterThanOrEqualTo$1(now$4, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
299736
300127
  return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
299737
- }).pipe(require_Schema$1.mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)));
300128
+ }).pipe(require_Schema$1.mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)), require_Schema$1.tapError((error) => error.status === 401 ? require_Schema$1.logWarning("Rejected bootstrap credential exchange.", { reason: error.message }) : require_Schema$1.void_));
299738
300129
  return {
299739
300130
  issueOneTimeToken,
299740
300131
  listActive,
@@ -299790,6 +300181,10 @@ const SetAuthSessionLastConnectedAtInput = require_Schema$1.Struct({
299790
300181
  sessionId: AuthSessionId,
299791
300182
  lastConnectedAt: require_Schema$1.DateTimeUtcFromString
299792
300183
  });
300184
+ const ExtendAuthSessionExpiryInput = require_Schema$1.Struct({
300185
+ sessionId: AuthSessionId,
300186
+ expiresAt: require_Schema$1.DateTimeUtcFromString
300187
+ });
299793
300188
  var AuthSessionRepository = class extends require_Schema$1.Service()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
299794
300189
  //#endregion
299795
300190
  //#region src/persistence/Layers/AuthSessions.ts
@@ -299941,6 +300336,17 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
299941
300336
  WHERE session_id = ${sessionId}
299942
300337
  AND revoked_at IS NULL
299943
300338
  RETURNING session_id AS "sessionId"
300339
+ `
300340
+ });
300341
+ const extendSessionExpiryRows = findAll({
300342
+ Request: ExtendAuthSessionExpiryInput,
300343
+ Result: require_Schema$1.Struct({ sessionId: AuthSessionId }),
300344
+ execute: ({ sessionId, expiresAt }) => sql`
300345
+ UPDATE auth_sessions
300346
+ SET expires_at = ${expiresAt}
300347
+ WHERE session_id = ${sessionId}
300348
+ AND revoked_at IS NULL
300349
+ RETURNING session_id AS "sessionId"
299944
300350
  `
299945
300351
  });
299946
300352
  const revokeOtherSessionRows = findAll({
@@ -299963,13 +300369,15 @@ const makeAuthSessionRepository = require_Schema$1.gen(function* () {
299963
300369
  const revoke = (input) => revokeSessionRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows")), require_Schema$1.map((rows) => rows.length > 0));
299964
300370
  const revokeAllExcept = (input) => revokeOtherSessionRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows")), require_Schema$1.map((rows) => rows.map((row) => row.sessionId)));
299965
300371
  const setLastConnectedAt = (input) => setLastConnectedAtRow(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest")));
300372
+ const extendExpiry = (input) => extendSessionExpiryRows(input).pipe(require_Schema$1.mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.extendExpiry:query", "AuthSessionRepository.extendExpiry:decodeRows")), require_Schema$1.map((rows) => rows.length > 0));
299966
300373
  return {
299967
300374
  create,
299968
300375
  getById,
299969
300376
  listActive,
299970
300377
  revoke,
299971
300378
  revokeAllExcept,
299972
- setLastConnectedAt
300379
+ setLastConnectedAt,
300380
+ extendExpiry
299973
300381
  };
299974
300382
  });
299975
300383
  const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository, makeAuthSessionRepository);
@@ -299978,6 +300386,20 @@ const AuthSessionRepositoryLive = require_Schema$1.effect(AuthSessionRepository,
299978
300386
  const SIGNING_SECRET_NAME = "server-signing-key";
299979
300387
  const DEFAULT_SESSION_TTL = require_Schema$1.days(30);
299980
300388
  const DEFAULT_WEBSOCKET_TOKEN_TTL = require_Schema$1.minutes(5);
300389
+ /**
300390
+ * Whether a session presented at `nowMs` has used up half its own lifetime.
300391
+ *
300392
+ * A phone that opens the app daily should never hit the 30-day wall, but a
300393
+ * lost phone's session should still die on schedule if it is never presented
300394
+ * again. Sliding the expiry forward only when the session is already more
300395
+ * than half spent gives an active phone many chances to renew before expiry
300396
+ * while keeping a dormant session's remaining lifetime bounded by the last
300397
+ * time it was actually used.
300398
+ */
300399
+ function isPastHalfLife(input) {
300400
+ const halfLifeMarkMs = input.issuedAtMs + (input.expiresAtMs - input.issuedAtMs) / 2;
300401
+ return input.nowMs >= halfLifeMarkMs;
300402
+ }
299981
300403
  const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
299982
300404
  message,
299983
300405
  cause
@@ -300138,6 +300560,42 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
300138
300560
  role: claims.role
300139
300561
  };
300140
300562
  }).pipe(require_Schema$1.mapError(toSessionCredentialError("Failed to issue session credential.")));
300563
+ /**
300564
+ * Slide a session's expiry forward when the token it just verified is past
300565
+ * half its own lifetime, so a phone that keeps using the app never runs
300566
+ * into the 30-day wall.
300567
+ *
300568
+ * The session id, subject, role and method carry over unchanged — only the
300569
+ * signature-bearing `iat`/`exp` differ — which is what lets the DB row be
300570
+ * extended in place (`extendExpiry`) rather than replaced: everything keyed
300571
+ * by session id, most importantly the `command_access_tokens` row holding
300572
+ * this session's CMMD credential, needs no migration.
300573
+ */
300574
+ const renewIfPastHalfLife = (input) => require_Schema$1.gen(function* () {
300575
+ const nowMs = yield* require_Schema$1.currentTimeMillis;
300576
+ if (!isPastHalfLife({
300577
+ issuedAtMs: input.row.issuedAt.epochMilliseconds,
300578
+ expiresAtMs: input.row.expiresAt.epochMilliseconds,
300579
+ nowMs
300580
+ })) return void 0;
300581
+ const newIssuedAt = yield* require_Schema$1.now;
300582
+ const newExpiresAt = require_Schema$1.add(newIssuedAt, { milliseconds: require_Schema$1.toMillis(DEFAULT_SESSION_TTL) });
300583
+ const newClaims = {
300584
+ ...input.claims,
300585
+ iat: newIssuedAt.epochMilliseconds,
300586
+ exp: newExpiresAt.epochMilliseconds
300587
+ };
300588
+ const newEncodedPayload = base64UrlEncode(JSON.stringify(newClaims));
300589
+ const newSignature = signPayload(newEncodedPayload, signingSecret);
300590
+ if (!(yield* authSessions.extendExpiry({
300591
+ sessionId: input.claims.sid,
300592
+ expiresAt: newExpiresAt
300593
+ }).pipe(require_Schema$1.orElseSucceed(() => false)))) return void 0;
300594
+ return {
300595
+ token: `${newEncodedPayload}.${newSignature}`,
300596
+ expiresAt: newExpiresAt
300597
+ };
300598
+ });
300141
300599
  const verify = (token) => require_Schema$1.gen(function* () {
300142
300600
  const [encodedPayload, signature] = token.split(".");
300143
300601
  if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
@@ -300152,14 +300610,19 @@ const makeSessionCredentialService = require_Schema$1.gen(function* () {
300152
300610
  if (require_Schema$1.isSome(row)) {
300153
300611
  if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
300154
300612
  } else if (!(statelessCookieSessions && claims.method === "browser-session-cookie")) return yield* new SessionCredentialError({ message: "Unknown session token." });
300613
+ const renewed = require_Schema$1.isSome(row) ? yield* renewIfPastHalfLife({
300614
+ claims,
300615
+ row: row.value
300616
+ }) : void 0;
300155
300617
  return {
300156
300618
  sessionId: claims.sid,
300157
300619
  token,
300158
300620
  method: claims.method,
300159
300621
  client: require_Schema$1.isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
300160
- expiresAt: require_Schema$1.makeUnsafe(claims.exp),
300622
+ expiresAt: renewed?.expiresAt ?? require_Schema$1.makeUnsafe(claims.exp),
300161
300623
  subject: claims.sub,
300162
- role: claims.role
300624
+ role: claims.role,
300625
+ ...renewed ? { renewed } : {}
300163
300626
  };
300164
300627
  }).pipe(require_Schema$1.mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
300165
300628
  message: "Failed to verify session credential.",
@@ -300652,7 +301115,10 @@ const makeServerAuth = require_Schema$1.gen(function* () {
300652
301115
  subject: session.subject,
300653
301116
  method: session.method,
300654
301117
  role: session.role,
300655
- ...session.expiresAt ? { expiresAt: session.expiresAt } : {}
301118
+ ...session.renewed ? {
301119
+ expiresAt: session.renewed.expiresAt,
301120
+ renewedSessionToken: session.renewed.token
301121
+ } : session.expiresAt ? { expiresAt: session.expiresAt } : {}
300656
301122
  })), require_Schema$1.mapError((cause) => new AuthError({
300657
301123
  message: "Unauthorized request.",
300658
301124
  status: 401,
@@ -300687,6 +301153,7 @@ const makeServerAuth = require_Schema$1.gen(function* () {
300687
301153
  role: session.role,
300688
301154
  sessionMethod: session.method,
300689
301155
  ...session.expiresAt ? { expiresAt: require_Schema$1.toUtc(session.expiresAt) } : {},
301156
+ ...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
300690
301157
  cmmdIdentity
300691
301158
  })))), require_Schema$1.tapError((e) => {
300692
301159
  if (e._tag === "AuthError" && (e.status ?? 500) < 500) return require_Schema$1.void_;
@@ -319052,7 +319519,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
319052
319519
  const sql = yield* require_SqlError.SqlClient;
319053
319520
  const fetchFn = options.fetchFn ?? fetch;
319054
319521
  const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
319055
- const resolveStoredJwt = (forceRefresh) => require_Schema$1.gen(function* () {
319522
+ const resolveStoredJwt = (forceRefresh, staleJwt) => require_Schema$1.gen(function* () {
319056
319523
  const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql), require_Schema$1.mapError((cause) => new ProjectLeadControllerClientError({
319057
319524
  kind: "unavailable",
319058
319525
  operation: "resolve-owner-session",
@@ -319066,7 +319533,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
319066
319533
  sessionId: owner.sessionId,
319067
319534
  subject: owner.subject,
319068
319535
  logPrefix: "project-lead-scheduler",
319069
- forceRefresh
319536
+ forceRefresh,
319537
+ staleAccessToken: staleJwt
319070
319538
  }).pipe(require_Schema$1.provideService(require_SqlError.SqlClient, sql));
319071
319539
  if (!jwt) return yield* new ProjectLeadControllerClientError({
319072
319540
  kind: "unauthenticated",
@@ -319109,7 +319577,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
319109
319577
  }));
319110
319578
  if (first._tag === "Success") return first.success;
319111
319579
  if (first.failure.kind !== "unauthenticated") return yield* first.failure;
319112
- const refreshed = yield* resolveJwt(true);
319580
+ const refreshed = yield* resolveJwt(true, jwt);
319113
319581
  return yield* requestAttempt({
319114
319582
  ...input,
319115
319583
  jwt: refreshed