@cmmd-center/forge 0.11.0 → 0.12.0

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 (26) hide show
  1. package/dist/bin.cjs +852 -656
  2. package/dist/bin.mjs +887 -691
  3. package/dist/client/assets/{CanvasPanel-Cogtmutb.js → CanvasPanel-CL4T3ptl.js} +2 -2
  4. package/dist/client/assets/{CompositeItem-D6h2WFmU.js → CompositeItem-NATmsDHX.js} +2 -2
  5. package/dist/client/assets/{DiffPanel-B1yCvMgl.js → DiffPanel-29UmSyE3.js} +3 -3
  6. package/dist/client/assets/{DiffPanel.logic-DS-ZTg-9.js → DiffPanel.logic-D5Bn5tgT.js} +2 -2
  7. package/dist/client/assets/DiffPanelShell-ClKPX_Pu.js +2531 -0
  8. package/dist/client/assets/{DiffWorkerPoolProvider-Q7qqna90.js → DiffWorkerPoolProvider-BMZL3CsO.js} +9 -9
  9. package/dist/client/assets/{PullRequestCodePanel-CO19fx8r.js → PullRequestCodePanel-Cp75delh.js} +3 -3
  10. package/dist/client/assets/{Virtualizer-nnC3ZYoF.js → Virtualizer-uq0d5TwY.js} +2 -2
  11. package/dist/client/assets/{canvasStateStore-DUZyBLSv.js → canvasStateStore-DtidOKbV.js} +2 -2
  12. package/dist/client/assets/{cmmdAuthRedirect-Bkc6Luqb.js → cmmdAuthRedirect-CzACd5XJ.js} +2 -2
  13. package/dist/client/assets/index-BOI5P7rG.js +151 -0
  14. package/dist/client/assets/src-BlFkWEsP.js +3 -0
  15. package/dist/client/assets/{toggle-CWmPePti.js → toggle-pwklbW9N.js} +2 -2
  16. package/dist/client/assets/{useTriggerFocusGuards-O7DUCec0.js → useTriggerFocusGuards-CPLQ8_CW.js} +2 -2
  17. package/dist/client/index.html +10 -10
  18. package/dist/forge-build.json +1 -1
  19. package/dist/runtimeBootstrap.cjs +1 -1
  20. package/dist/runtimeBootstrap.mjs +1 -1
  21. package/dist/{runtimeControlAuthority-BdIovv-r.cjs → runtimeControlAuthority-B3kNmYRT.cjs} +4 -2
  22. package/dist/{runtimeControlAuthority-COHmz_WT.mjs → runtimeControlAuthority-D_zKr_eI.mjs} +4 -2
  23. package/package.json +1 -1
  24. package/dist/client/assets/DiffPanelShell-BFklbpRe.js +0 -2531
  25. package/dist/client/assets/index-ChfmpOIo.js +0 -151
  26. package/dist/client/assets/src-BlF9iHm-.js +0 -3
package/dist/bin.mjs CHANGED
@@ -5,13 +5,13 @@ import { $ as encodeEffect, $a as make$39, $c as fromUndefinedOr, $i as spaced,
5
5
  import { $ as toAsyncIterable, A as make$45, At as make$44, Bt as publish, C as fromEffect, Ct as make$43, E as fromQueue, Et as end, F as merge$4, Ft as shutdown$1, G as runFold, H as run$2, Ht as subscribe$1, It as sliding, Jt as getCurrent, K as runForEach, Kt as makeUnsafe$7, Lt as take$1, M as mapAccum, Mt as offerAll, N as mapEffect$1, Nt as offerUnsafe, O as fromSubscription, Ot as failCause$4, Q as tick, R as onExit$1, Rt as takeAll, S as fromChannel, T as fromPubSub, Tt as bounded, U as runCollect, Ut as unbounded$1, V as provideContext$1, Vt as shutdown$2, W as runDrain, Y as succeed$4, Yt as interrupt$4, Z as takeWhile, Zt as runIn, _t as mapInput, at as make$46, b as flatMap$3, bt as fromTransform, c as callback$1, d as debounce$1, f as decodeText$1, g as ensuring$1, h as encodeText, it as get$4, j as map$7, jt as offer$1, k as isStream, l as catchCause$1, m as empty$5, mt as forEach$1, ot as get$5, pt as drain, q as runForEachArray, qt as await_, r as FileSystem, rt as unwrap$1, st as make$47, t as Path$2, u as concat$2, v as failCause$5, w as fromIterable$1, wt as makeUnsafe$6, x as fromAsyncIterable, y as filter$2, yt as fromSubscriptionArray, zt as unbounded$2 } from "./Path-C6Zb2WHp.mjs";
6
6
  import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-BEGtoMSS.mjs";
7
7
  import { n as layer$7 } from "./NodeServices-CBNXOYLG.mjs";
8
- import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$2, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-DNGCTTdk.mjs";
8
+ import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-DNGCTTdk.mjs";
9
9
  import { _ as withDefault$3, a as all$1, c as logLevel, d as option$1, f as port, g as url$2, i as TrueValues, l as map$8, m as string$7, n as FalseValues, o as boolean$5, p as schema$2, r as Record$1, s as int$2, t as Boolean$2, u as number$2 } from "./Config-AqUR_lhX.mjs";
10
- import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$3, t as get$8 } from "./Cache-p0X77SKw.mjs";
10
+ import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-p0X77SKw.mjs";
11
11
  import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-cyJT2IRY.mjs";
12
12
  import { r as wrapper_default, t as Mime_default } from "./Mime-Dq9IoNiV.mjs";
13
13
  import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-wLIF-u3N.mjs";
14
- import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as setRuntimeGrantToken, f as createReadOnceFileTokenAccessor, i as sendRuntimeControlHeartbeat, l as verifyCmmdRuntimeTicketRefreshProof, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, r as readRuntimeControlAuthority, s as verifyCmmdClaudeUsageTicket, t as bootstrapRuntimeControlAuthority, u as runtimeGrantToken } from "./runtimeControlAuthority-COHmz_WT.mjs";
14
+ import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as setRuntimeGrantToken, f as createReadOnceFileTokenAccessor, i as sendRuntimeControlHeartbeat, l as verifyCmmdRuntimeTicketRefreshProof, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, r as readRuntimeControlAuthority, s as verifyCmmdClaudeUsageTicket, t as bootstrapRuntimeControlAuthority, u as runtimeGrantToken } from "./runtimeControlAuthority-D_zKr_eI.mjs";
15
15
  import { n as PtySpawnError, t as PtyAdapter } from "./PTY-rIPf52QR.mjs";
16
16
  import { createRequire } from "node:module";
17
17
  import * as HE from "node:child_process";
@@ -15750,7 +15750,7 @@ const get$1 = (self) => modify$1(self, (current) => [current, current]);
15750
15750
  * })
15751
15751
  * ```
15752
15752
  */
15753
- const set$1 = /* @__PURE__ */ dual(2, (self, value) => update$1(self, () => value));
15753
+ const set$2 = /* @__PURE__ */ dual(2, (self, value) => update$1(self, () => value));
15754
15754
  //#endregion
15755
15755
  //#region ../../node_modules/.bun/effect@4.0.0-beta.51/node_modules/effect/dist/TxChunk.js
15756
15756
  /**
@@ -15939,7 +15939,7 @@ const get = (self) => get$1(self.ref);
15939
15939
  * @since 4.0.0
15940
15940
  * @category combinators
15941
15941
  */
15942
- const set = /* @__PURE__ */ dual(2, (self, chunk) => set$1(self.ref, chunk));
15942
+ const set$1 = /* @__PURE__ */ dual(2, (self, chunk) => set$2(self.ref, chunk));
15943
15943
  /**
15944
15944
  * Appends an element to the end of the `TxChunk`.
15945
15945
  *
@@ -16187,7 +16187,7 @@ const take = (self) => gen(function* () {
16187
16187
  const head$3 = head(yield* get(self.items));
16188
16188
  if (isNone(head$3)) return yield* txRetry;
16189
16189
  yield* drop(self.items, 1);
16190
- if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$1(self.stateRef, {
16190
+ if (state._tag === "Closing" && (yield* isEmpty(self))) yield* set$2(self.stateRef, {
16191
16191
  _tag: "Done",
16192
16192
  cause: state.cause
16193
16193
  });
@@ -16285,11 +16285,11 @@ const interrupt = (self) => withFiber$1((fiber) => failCause(self, interrupt$2(f
16285
16285
  */
16286
16286
  const failCause = /* @__PURE__ */ dual(2, (self, cause) => gen(function* () {
16287
16287
  if ((yield* get$1(self.stateRef))._tag !== "Open") return false;
16288
- if (yield* isEmpty(self)) yield* set$1(self.stateRef, {
16288
+ if (yield* isEmpty(self)) yield* set$2(self.stateRef, {
16289
16289
  _tag: "Done",
16290
16290
  cause
16291
16291
  });
16292
- else yield* set$1(self.stateRef, {
16292
+ else yield* set$2(self.stateRef, {
16293
16293
  _tag: "Closing",
16294
16294
  cause
16295
16295
  });
@@ -16331,7 +16331,7 @@ const clear = (self) => gen(function* () {
16331
16331
  return yield* failCause$1(state.cause);
16332
16332
  }
16333
16333
  const chunk = yield* get(self.items);
16334
- yield* set(self.items, empty$2());
16334
+ yield* set$1(self.items, empty$2());
16335
16335
  return toArray(chunk);
16336
16336
  }).pipe(tx$1);
16337
16337
  /**
@@ -16520,7 +16520,7 @@ function makeNetServiceShape(createServer = Net.createServer) {
16520
16520
  }
16521
16521
  //#endregion
16522
16522
  //#region ../../packages/shared/src/releaseInfo.generated.ts
16523
- const FORGE_RELEASE_VERSION = "0.11.0";
16523
+ const FORGE_RELEASE_VERSION = "0.12.0";
16524
16524
  //#endregion
16525
16525
  //#region ../../packages/shared/src/hostProcess.ts
16526
16526
  /**
@@ -19116,6 +19116,7 @@ const ORCHESTRATION_WS_METHODS = {
19116
19116
  streamTurnDiff: "orchestration.streamTurnDiff",
19117
19117
  streamFullThreadDiff: "orchestration.streamFullThreadDiff",
19118
19118
  replayEvents: "orchestration.replayEvents",
19119
+ getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
19119
19120
  subscribeShell: "orchestration.subscribeShell",
19120
19121
  subscribeThread: "orchestration.subscribeThread"
19121
19122
  };
@@ -20551,6 +20552,10 @@ const OrchestrationRpcSchemas = {
20551
20552
  subscribeShell: {
20552
20553
  input: Struct({}),
20553
20554
  output: OrchestrationShellStreamItem
20555
+ },
20556
+ getArchivedShellSnapshot: {
20557
+ input: Struct({}),
20558
+ output: OrchestrationShellSnapshot
20554
20559
  }
20555
20560
  };
20556
20561
  var OrchestrationGetSnapshotError = class extends TaggedErrorClass()("OrchestrationGetSnapshotError", {
@@ -26292,6 +26297,11 @@ const WsOrchestrationReplayEventsRpc = make$30(ORCHESTRATION_WS_METHODS.replayEv
26292
26297
  success: OrchestrationRpcSchemas.replayEvents.output,
26293
26298
  error: OrchestrationReplayEventsError
26294
26299
  });
26300
+ const WsOrchestrationGetArchivedShellSnapshotRpc = make$30(ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, {
26301
+ payload: OrchestrationRpcSchemas.getArchivedShellSnapshot.input,
26302
+ success: OrchestrationRpcSchemas.getArchivedShellSnapshot.output,
26303
+ error: OrchestrationGetSnapshotError
26304
+ });
26295
26305
  const WsOrchestrationSubscribeShellRpc = make$30(ORCHESTRATION_WS_METHODS.subscribeShell, {
26296
26306
  payload: OrchestrationRpcSchemas.subscribeShell.input,
26297
26307
  success: OrchestrationRpcSchemas.subscribeShell.output,
@@ -26372,7 +26382,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
26372
26382
  payload: UsageSummaryInput,
26373
26383
  success: UsageSummary,
26374
26384
  error: UsageReadError
26375
- }), WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsThreadCommentsRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsProjectCheckoutDeletionPreviewRpc, WsProjectCheckoutDeletionExecuteRpc, WsCodeGraphSummaryRpc, WsCodeGraphNeighborhoodRpc, WsCodeGraphSearchRpc, WsWorkspaceReadFileRpc, WsWorkspaceBeginWriteFileRpc, WsWorkspaceWriteFileChunkRpc, WsWorkspaceCommitWriteFileRpc, WsWorkspaceCancelWriteFileRpc, WsShellOpenInEditorRpc, WsSubscribeGitStatusRpc, WsGitPullRpc, WsGitRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsGitListBranchesRpc, WsVcsListRefsRpc, WsVcsRefreshStatusRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsGitCreateWorktreeRpc, WsGitRemoveWorktreeRpc, WsGitCreateBranchRpc, WsGitCheckoutRpc, WsGitInitRpc, WsFilesystemBrowseRpc, WsProjectsReadFileRpc, WsReviewGetDiffPreviewRpc, WsCloudListRuntimesRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalSignalRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsBrowserOpenSessionRpc, WsBrowserListSessionsRpc, WsBrowserCloseSessionRpc, WsBrowserGetSessionRpc, WsBrowserCommandRpc, WsBrowserClearBrowsingDataRpc, WsBrowserAttachDisplayRpc, WsBrowserAddDisplayIceCandidateRpc, WsComputerInputRpc, WsDevTunnelRegisterRpc, WsDevTunnelUnregisterRpc, WsDevTunnelListRpc, WsThreadDevRuntimeGetRpc, WsThreadDevRuntimeAllocatePortRpc, WsThreadDevRuntimeReleaseRpc, WsWorkspaceEnvironmentPreflightRpc, WsWorkspaceEnsureScratchRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsProjectsListEntriesRpc, WsServerDiscoverSourceControlRpc, WsServerReportClientActivityRpc, WsSubscribeBrowserEventsRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsSubscribeWorkspaceReadinessRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationStreamTurnDiffRpc, WsOrchestrationStreamFullThreadDiffRpc, WsOrchestrationReplayEventsRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
26385
+ }), WsPullRequestsListRpc, WsPullRequestsListStatsRpc, WsPullRequestsDetailRpc, WsPullRequestsActivityRpc, WsPullRequestsThreadCommentsRpc, WsPullRequestsDiffFileContentsRpc, WsPullRequestsRunActionRpc, WsPullRequestsUpdateRpc, WsPullRequestsCommentRpc, WsPullRequestsUpdateCommentRpc, WsPullRequestsSubmitReviewRpc, WsPullRequestsReplyToThreadRpc, WsPullRequestsSetThreadResolutionRpc, WsPullRequestsSetReactionRpc, WsPullRequestsInvalidateRpc, WsPullRequestsReviewerCandidatesRpc, WsPullRequestsRequestReviewersRpc, WsProjectsSearchEntriesRpc, WsProjectsWriteFileRpc, WsProjectCheckoutDeletionPreviewRpc, WsProjectCheckoutDeletionExecuteRpc, WsCodeGraphSummaryRpc, WsCodeGraphNeighborhoodRpc, WsCodeGraphSearchRpc, WsWorkspaceReadFileRpc, WsWorkspaceBeginWriteFileRpc, WsWorkspaceWriteFileChunkRpc, WsWorkspaceCommitWriteFileRpc, WsWorkspaceCancelWriteFileRpc, WsShellOpenInEditorRpc, WsSubscribeGitStatusRpc, WsGitPullRpc, WsGitRefreshStatusRpc, WsGitRunStackedActionRpc, WsGitResolvePullRequestRpc, WsGitPreparePullRequestThreadRpc, WsGitListBranchesRpc, WsVcsListRefsRpc, WsVcsRefreshStatusRpc, WsSubscribeVcsStatusRpc, WsVcsPullRpc, WsVcsCreateWorktreeRpc, WsVcsRemoveWorktreeRpc, WsVcsCreateRefRpc, WsVcsSwitchRefRpc, WsVcsInitRpc, WsGitCreateWorktreeRpc, WsGitRemoveWorktreeRpc, WsGitCreateBranchRpc, WsGitCheckoutRpc, WsGitInitRpc, WsFilesystemBrowseRpc, WsProjectsReadFileRpc, WsReviewGetDiffPreviewRpc, WsCloudListRuntimesRpc, WsTerminalOpenRpc, WsTerminalAttachRpc, WsTerminalWriteRpc, WsTerminalResizeRpc, WsTerminalSignalRpc, WsTerminalClearRpc, WsTerminalRestartRpc, WsTerminalCloseRpc, WsBrowserOpenSessionRpc, WsBrowserListSessionsRpc, WsBrowserCloseSessionRpc, WsBrowserGetSessionRpc, WsBrowserCommandRpc, WsBrowserClearBrowsingDataRpc, WsBrowserAttachDisplayRpc, WsBrowserAddDisplayIceCandidateRpc, WsComputerInputRpc, WsDevTunnelRegisterRpc, WsDevTunnelUnregisterRpc, WsDevTunnelListRpc, WsThreadDevRuntimeGetRpc, WsThreadDevRuntimeAllocatePortRpc, WsThreadDevRuntimeReleaseRpc, WsWorkspaceEnvironmentPreflightRpc, WsWorkspaceEnsureScratchRpc, WsSubscribeTerminalEventsRpc, WsSubscribeTerminalMetadataRpc, WsProjectsListEntriesRpc, WsServerDiscoverSourceControlRpc, WsServerReportClientActivityRpc, WsSubscribeBrowserEventsRpc, WsSubscribeServerConfigRpc, WsSubscribeServerLifecycleRpc, WsSubscribeAuthAccessRpc, WsSubscribeWorkspaceReadinessRpc, WsOrchestrationDispatchCommandRpc, WsOrchestrationGetTurnDiffRpc, WsOrchestrationGetFullThreadDiffRpc, WsOrchestrationStreamTurnDiffRpc, WsOrchestrationStreamFullThreadDiffRpc, WsOrchestrationReplayEventsRpc, WsOrchestrationGetArchivedShellSnapshotRpc, WsOrchestrationSubscribeShellRpc, WsOrchestrationSubscribeThreadRpc);
26376
26386
  const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
26377
26387
  /**
26378
26388
  * The direct-WebSocket mount of the same group, with per-method scope
@@ -26406,6 +26416,29 @@ function encodeCmmdRuntimeThreadLifecycleMetadata(lifecycle) {
26406
26416
  } };
26407
26417
  }
26408
26418
  //#endregion
26419
+ //#region ../../packages/contracts/src/cmmdRuntimeThreadWorkspace.ts
26420
+ function encodeCmmdRuntimeThreadWorkspaceMetadata(workspace) {
26421
+ return { forgeWorkspace: {
26422
+ schemaVersion: 1,
26423
+ worktreePath: workspace.worktreePath
26424
+ } };
26425
+ }
26426
+ //#endregion
26427
+ //#region ../../packages/contracts/src/cmmdRuntimeToolActivity.ts
26428
+ function isRecord$22(value) {
26429
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26430
+ }
26431
+ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
26432
+ return {
26433
+ ...isRecord$22(payload) ? payload : { sourcePayload: payload },
26434
+ forgeToolActivity: {
26435
+ schemaVersion: 1,
26436
+ source: "native",
26437
+ kind
26438
+ }
26439
+ };
26440
+ }
26441
+ //#endregion
26409
26442
  //#region ../../packages/contracts/src/cmmdProjectPatch.ts
26410
26443
  const CMMD_PROJECT_PATCH_FIELDS = {
26411
26444
  title: "requiredText",
@@ -31467,6 +31500,11 @@ const RELAY_METHOD_POLICY_BASES = [
31467
31500
  requiredScope: "thread.read",
31468
31501
  deliveryClass: "subscription"
31469
31502
  },
31503
+ {
31504
+ method: ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot,
31505
+ requiredScope: "thread.read",
31506
+ deliveryClass: "read"
31507
+ },
31470
31508
  {
31471
31509
  method: ORCHESTRATION_WS_METHODS.subscribeShell,
31472
31510
  requiredScope: "thread.read",
@@ -65495,7 +65533,7 @@ function normalizeNumberish(value) {
65495
65533
  }
65496
65534
  //#endregion
65497
65535
  //#region package.json
65498
- var version$1 = "0.11.0";
65536
+ var version$1 = "0.12.0";
65499
65537
  //#endregion
65500
65538
  //#region src/sentry.ts
65501
65539
  const SERVER_APP_NAME = "forge-server";
@@ -69266,6 +69304,17 @@ const browserApiCorsLayer = middleware(cors({
69266
69304
  credentials: true,
69267
69305
  maxAge: 600
69268
69306
  }), { global: true });
69307
+ /**
69308
+ * An /api path that reaches the static catch-all is not implemented by this
69309
+ * server: every real API route is registered ahead of it. Serving the web app's
69310
+ * index.html for those made API clients decode an HTML document as JSON, which
69311
+ * is how the phone's shell snapshot fetch failed with a JSON parse error on
69312
+ * every connect (FOR-36611). Callers can act on a 404; they cannot act on a
69313
+ * document that claims to be their payload.
69314
+ */
69315
+ function isApiRequestPath(pathname) {
69316
+ return pathname === "/api" || pathname.startsWith("/api/");
69317
+ }
69269
69318
  function resolveDevRedirectUrl(devUrl, requestUrl) {
69270
69319
  const redirectUrl = new URL(devUrl.toString());
69271
69320
  redirectUrl.pathname = requestUrl.pathname;
@@ -69387,6 +69436,10 @@ const projectFaviconRouteLayer = add$1("GET", "/api/project-favicon", gen(functi
69387
69436
  const staticAndDevRouteLayer = add$1("GET", "*", gen(function* () {
69388
69437
  const url = toURL(yield* HttpServerRequest);
69389
69438
  if (isNone(url)) return text$2("Bad Request", { status: 400 });
69439
+ if (isApiRequestPath(url.value.pathname)) return jsonUnsafe({
69440
+ error: "not_found",
69441
+ message: `This server does not serve ${url.value.pathname}.`
69442
+ }, { status: 404 });
69390
69443
  const config = yield* ServerConfig;
69391
69444
  if (config.devUrl && isLoopbackHostname(url.value.hostname)) return redirect(resolveDevRedirectUrl(config.devUrl, url.value), { status: 302 });
69392
69445
  const staticDir = config.staticDir ?? (config.devUrl ? yield* resolveStaticDir() : void 0);
@@ -82637,7 +82690,7 @@ var ServerSettingsService = class ServerSettingsService extends Service$1()("for
82637
82690
  start: void_$1,
82638
82691
  ready: void_$1,
82639
82692
  getSettings: get$6(currentSettingsRef),
82640
- updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$2(currentSettingsRef, nextSettings))),
82693
+ updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$3(currentSettingsRef, nextSettings))),
82641
82694
  streamChanges: empty$5
82642
82695
  };
82643
82696
  }));
@@ -82777,7 +82830,7 @@ const ServerSettingsLive = effect(ServerSettingsService, gen(function* () {
82777
82830
  cause
82778
82831
  })));
82779
82832
  yield* writeSettingsAtomically(next);
82780
- yield* set$3(settingsCache, cacheKey, next);
82833
+ yield* set$4(settingsCache, cacheKey, next);
82781
82834
  yield* emitChange(next);
82782
82835
  return resolveTextGenerationProvider(next);
82783
82836
  })),
@@ -91707,7 +91760,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91707
91760
  return {
91708
91761
  start: gen(function* () {
91709
91762
  if (yield* get$6(startedRef)) return yield* _await(startedDeferred);
91710
- yield* set$2(startedRef, true);
91763
+ yield* set$3(startedRef, true);
91711
91764
  const startupExit = yield* exit(gen(function* () {
91712
91765
  yield* startWatcher;
91713
91766
  yield* syncDefaultKeybindingsOnStartup;
@@ -91736,7 +91789,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91736
91789
  });
91737
91790
  yield* writeConfigAtomically(cappedConfig);
91738
91791
  const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(cappedConfig));
91739
- yield* set$3(resolvedConfigCache, resolvedConfigCacheKey, {
91792
+ yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
91740
91793
  keybindings: nextResolved,
91741
91794
  issues: []
91742
91795
  });
@@ -91750,7 +91803,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91750
91803
  const nextConfig = (yield* loadWritableCustomKeybindingsConfig()).filter((entry) => entry.command !== command);
91751
91804
  yield* writeConfigAtomically(nextConfig);
91752
91805
  const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(nextConfig));
91753
- yield* set$3(resolvedConfigCache, resolvedConfigCacheKey, {
91806
+ yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
91754
91807
  keybindings: nextResolved,
91755
91808
  issues: []
91756
91809
  });
@@ -92685,6 +92738,15 @@ async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync
92685
92738
  }
92686
92739
  return cached$1;
92687
92740
  }
92741
+ //#endregion
92742
+ //#region src/environment/serverVersion.ts
92743
+ const installedForgeServerVersion = version$1;
92744
+ const SEMVER_NUMERIC_IDENTIFIER = "(?:0|[1-9][0-9]*)";
92745
+ const SEMVER_PRERELEASE_IDENTIFIER = "(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)";
92746
+ const EXACT_FORGE_SERVER_VERSION_PATTERN = new RegExp(`^${SEMVER_NUMERIC_IDENTIFIER}[.]${SEMVER_NUMERIC_IDENTIFIER}[.]${SEMVER_NUMERIC_IDENTIFIER}(?:-${SEMVER_PRERELEASE_IDENTIFIER}(?:[.]${SEMVER_PRERELEASE_IDENTIFIER})*)?(?:[+][0-9A-Za-z-]+(?:[.][0-9A-Za-z-]+)*)?\$`);
92747
+ function isExactForgeServerVersion(value) {
92748
+ return typeof value === "string" && EXACT_FORGE_SERVER_VERSION_PATTERN.test(value);
92749
+ }
92688
92750
  const decodeSessions = decodeUnknownSync(ArraySchema(Struct({
92689
92751
  sessionId: AuthSessionId,
92690
92752
  subject: String$1
@@ -94299,7 +94361,8 @@ const registerCloudRuntimeForReplicaHydration = gen(function* () {
94299
94361
  credentialEpoch,
94300
94362
  activeTurnCount: 0,
94301
94363
  activeSessionCount: 0,
94302
- ...forgeGitCommit !== void 0 ? { forgeGitCommit } : {}
94364
+ ...forgeGitCommit !== void 0 ? { forgeGitCommit } : {},
94365
+ forgeServerVersion: installedForgeServerVersion
94303
94366
  }),
94304
94367
  catch: (cause) => replicaHydrationFailure("runtime registration heartbeat failed", cause)
94305
94368
  })) === null) return yield* replicaHydrationFailure("runtime control authority is unavailable");
@@ -95620,10 +95683,13 @@ function serializeRuntimeInventory(snapshot, providerInventory) {
95620
95683
  workerRole: thread.workerRole,
95621
95684
  anchorKind: thread.anchorKind,
95622
95685
  anchorCmmdId: thread.anchorCmmdId,
95623
- metadata: encodeCmmdRuntimeThreadLifecycleMetadata({
95624
- settledAt: thread.settledAt ?? null,
95625
- settledOverride: thread.settledOverride ?? null
95626
- })
95686
+ metadata: {
95687
+ ...encodeCmmdRuntimeThreadLifecycleMetadata({
95688
+ settledAt: thread.settledAt ?? null,
95689
+ settledOverride: thread.settledOverride ?? null
95690
+ }),
95691
+ ...encodeCmmdRuntimeThreadWorkspaceMetadata({ worktreePath: thread.worktreePath })
95692
+ }
95627
95693
  })),
95628
95694
  metadata: {
95629
95695
  source: "forge-server-projection",
@@ -95690,7 +95756,7 @@ function serializeNativeToolActivity(activity) {
95690
95756
  kind: "tool_call",
95691
95757
  summary,
95692
95758
  status: activityStatus(activity),
95693
- ...activity.payload === void 0 ? {} : { payload: activity.payload }
95759
+ payload: encodeCmmdRuntimeToolActivityPayload(activity.kind, activity.payload)
95694
95760
  }
95695
95761
  };
95696
95762
  }
@@ -97852,8 +97918,8 @@ const runUsageScanWarmup = (options) => gen(function* () {
97852
97918
  return yield* forever$1(gen(function* () {
97853
97919
  if (yield* get$6(scanning)) yield* logInfo("usage scan warm-up skipped; the previous scan is still running");
97854
97920
  else {
97855
- yield* set$2(scanning, true);
97856
- yield* forkChild(scanOnce(options, now).pipe(ensuring(set$2(scanning, false))));
97921
+ yield* set$3(scanning, true);
97922
+ yield* forkChild(scanOnce(options, now).pipe(ensuring(set$3(scanning, false))));
97857
97923
  }
97858
97924
  yield* sleep(intervalMs);
97859
97925
  }));
@@ -97871,11 +97937,11 @@ const makeCommandGate = gen(function* () {
97871
97937
  return {
97872
97938
  awaitCommandReady: _await(commandReady),
97873
97939
  signalCommandReady: gen(function* () {
97874
- yield* set$2(commandReadinessState, "ready");
97940
+ yield* set$3(commandReadinessState, "ready");
97875
97941
  yield* succeed$3(commandReady, void 0).pipe(orDie);
97876
97942
  }),
97877
97943
  failCommandReady: (error) => gen(function* () {
97878
- yield* set$2(commandReadinessState, error);
97944
+ yield* set$3(commandReadinessState, error);
97879
97945
  yield* fail$3(commandReady, error).pipe(orDie);
97880
97946
  }),
97881
97947
  enqueueCommand: (effect) => gen(function* () {
@@ -98256,413 +98322,23 @@ function browseDirectory(input) {
98256
98322
  });
98257
98323
  }
98258
98324
  //#endregion
98259
- //#region src/workspace/Services/WorkspaceEntries.ts
98325
+ //#region src/pathWithinRoot.ts
98260
98326
  /**
98261
- * WorkspaceEntries - Effect service contract for cached workspace entry search.
98327
+ * True when `candidate` is `root` itself or lies below it, judged on the path
98328
+ * strings alone. The path from `root` to `candidate` is taken with
98329
+ * `path.relative` and normalised to forward slashes, and the candidate is
98330
+ * outside when that path climbs out (`..` or `../...`) or is absolute, which is
98331
+ * what `relative` returns across Windows drives. A child whose name only starts
98332
+ * with two dots, such as `..cache`, is inside. Symlinks are not followed, so a
98333
+ * caller that cares where a link points has to compare resolved paths.
98262
98334
  *
98263
- * Owns indexed workspace entry search plus cache invalidation for workspace
98264
- * roots when the underlying filesystem changes.
98265
- *
98266
- * @module WorkspaceEntries
98267
- */
98268
- var WorkspaceEntriesError = class extends TaggedErrorClass()("WorkspaceEntriesError", {
98269
- cwd: String$1,
98270
- operation: String$1,
98271
- detail: String$1,
98272
- cause: optional$4(Defect)
98273
- }) {};
98274
- /**
98275
- * WorkspaceEntries - Service tag for cached workspace entry search.
98276
- */
98277
- var WorkspaceEntries = class extends Service$1()("forge/workspace/Services/WorkspaceEntries") {};
98278
- //#endregion
98279
- //#region src/workspace/Services/WorkspaceBulkTransfer.ts
98280
- var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
98281
- //#endregion
98282
- //#region src/workspace/Layers/WorkspaceBulkTransfer.ts
98283
- const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
98284
- const TRANSFER_LOCK_STRIPE_COUNT = 64;
98285
- function transferError(input) {
98286
- return new BulkTransferError({
98287
- code: input.code,
98288
- message: input.message,
98289
- transferId: input.transferId ?? null,
98290
- retryable: input.retryable ?? false,
98291
- ...input.cause === void 0 ? {} : { cause: input.cause }
98292
- });
98293
- }
98294
- function mapFailure(operation, transferId) {
98295
- return (cause) => is$1(BulkTransferError)(cause) ? cause : transferError({
98296
- code: "io_error",
98297
- message: `Workspace bulk transfer failed during ${operation}.`,
98298
- ...transferId === void 0 ? {} : { transferId },
98299
- retryable: true,
98300
- cause
98301
- });
98302
- }
98303
- /**
98304
- * Exported so the unary file read in `projectFile/readProjectFile.ts` uses this
98305
- * containment check rather than a second copy of it. Two copies of a security
98306
- * rule in one repo is how they drift, and the drift is silent.
98335
+ * Only a type import, so a module that has to stay free of Effect services can
98336
+ * use it without importing a Layer.
98307
98337
  */
98308
98338
  function isPathWithinRoot(path, root, candidate) {
98309
98339
  const relative = path.relative(root, candidate).replaceAll("\\", "/");
98310
98340
  return relative === "" || !relative.startsWith("../") && relative !== ".." && !path.isAbsolute(relative);
98311
98341
  }
98312
- function transferLockStripe(transferId) {
98313
- let hash = 2166136261;
98314
- for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
98315
- return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
98316
- }
98317
- const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
98318
- const fileSystem = yield* FileSystem;
98319
- const path = yield* Path$2;
98320
- const sql = yield* SqlClient;
98321
- const workspacePaths = yield* WorkspacePaths;
98322
- const workspaceEntries = yield* WorkspaceEntries;
98323
- const fault = options.fault ?? (() => void_$1);
98324
- const transferLocks = yield* forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => make$43(1));
98325
- const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
98326
- const findTransfer = (transferId) => sql`
98327
- SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
98328
- relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
98329
- total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
98330
- next_offset AS "nextOffset", state, expires_at AS "expiresAt"
98331
- FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
98332
- `.pipe(map$3((rows) => rows[0] ?? null));
98333
- const requireTransfer = (transferId) => findTransfer(transferId).pipe(flatMap((row) => row === null ? fail(transferError({
98334
- code: "transfer_not_found",
98335
- message: "Bulk transfer does not exist.",
98336
- transferId
98337
- })) : succeed(row)));
98338
- const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(flatMap((row) => {
98339
- if (row.state === "cancelled") return fail(transferError({
98340
- code: "transfer_cancelled",
98341
- message: "Bulk transfer was cancelled.",
98342
- transferId
98343
- }));
98344
- if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return fail(transferError({
98345
- code: "transfer_expired",
98346
- message: "Bulk transfer expired.",
98347
- transferId
98348
- }));
98349
- return succeed(row);
98350
- }));
98351
- const resolveTarget = fn$1("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
98352
- const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
98353
- const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
98354
- workspaceRoot,
98355
- relativePath: input.relativePath
98356
- });
98357
- const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
98358
- const parentSegments = resolved.relativePath.split("/").slice(0, -1);
98359
- let canonicalParent = canonicalRoot;
98360
- for (const segment of parentSegments) {
98361
- const candidate = path.join(canonicalParent, segment);
98362
- const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
98363
- if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
98364
- code: "symlink_escape",
98365
- message: "Workspace file path resolves outside the project root."
98366
- });
98367
- canonicalParent = canonicalCandidate;
98368
- }
98369
- const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
98370
- const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(catch_(() => succeed(null)));
98371
- if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
98372
- code: "symlink_escape",
98373
- message: "Workspace file target resolves outside the project root."
98374
- });
98375
- return {
98376
- workspaceRoot: canonicalRoot,
98377
- relativePath: resolved.relativePath,
98378
- targetPath,
98379
- canonicalParent
98380
- };
98381
- });
98382
- const readFile = (input) => unwrap$1(gen(function* () {
98383
- const target = yield* resolveTarget({
98384
- ...input,
98385
- createParent: false
98386
- });
98387
- const info = yield* fileSystem.stat(target.targetPath);
98388
- const maxBytes = input.maxBytes ?? 16777216;
98389
- if (Number(info.size) > maxBytes) return yield* transferError({
98390
- code: "size_limit_exceeded",
98391
- message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
98392
- });
98393
- const bytes = yield* fileSystem.readFile(target.targetPath);
98394
- const transfer = yield* promise(() => chunkBulkTransfer(bytes));
98395
- const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
98396
- return fromIterable$1([
98397
- {
98398
- kind: "started",
98399
- transferId,
98400
- totalBytes: transfer.totalBytes,
98401
- sha256: transfer.sha256
98402
- },
98403
- ...transfer.chunks.map((chunk) => ({
98404
- ...chunk,
98405
- transferId
98406
- })),
98407
- {
98408
- kind: "completed",
98409
- transferId,
98410
- totalBytes: transfer.totalBytes,
98411
- sha256: transfer.sha256
98412
- }
98413
- ]);
98414
- }).pipe(mapError(mapFailure("read file"))));
98415
- const resumeWrite = fn$1("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
98416
- const target = yield* resolveTarget({
98417
- ...input,
98418
- createParent: true
98419
- });
98420
- if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
98421
- code: "transfer_identity_conflict",
98422
- message: "Bulk transfer ID is already bound to different content or a different path.",
98423
- transferId: input.transferId
98424
- });
98425
- if (existing.state === "cancelled") return yield* transferError({
98426
- code: "transfer_cancelled",
98427
- message: "Bulk transfer was cancelled.",
98428
- transferId: input.transferId
98429
- });
98430
- if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
98431
- code: "transfer_expired",
98432
- message: "Bulk transfer expired.",
98433
- transferId: input.transferId
98434
- });
98435
- return {
98436
- transferId: input.transferId,
98437
- nextOffset: existing.nextOffset,
98438
- state: existing.state === "completed" ? "completed" : "open"
98439
- };
98440
- });
98441
- const createWrite = fn$1("WorkspaceBulkTransfer.createWrite")(function* (input) {
98442
- const target = yield* resolveTarget({
98443
- ...input,
98444
- createParent: true
98445
- }).pipe(mapError((cause) => is$1(WorkspacePathOutsideRootError)(cause) ? transferError({
98446
- code: "path_outside_workspace",
98447
- message: "Workspace file path must stay within the project root.",
98448
- transferId: input.transferId,
98449
- cause
98450
- }) : cause));
98451
- const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
98452
- yield* scoped(fileSystem.open(tempPath, {
98453
- flag: "wx",
98454
- mode: 384
98455
- }).pipe(flatMap((file) => file.sync), catch_(() => fileSystem.stat(tempPath).pipe(flatMap((stat) => Number(stat.size) === 0 ? void_$1 : fail(transferError({
98456
- code: "transfer_identity_conflict",
98457
- message: "Bulk transfer temporary path already contains data.",
98458
- transferId: input.transferId
98459
- })))))));
98460
- const now = /* @__PURE__ */ new Date();
98461
- yield* sql`
98462
- INSERT INTO workspace_bulk_transfers (
98463
- transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
98464
- whole_sha256, next_offset, state, expires_at, created_at, updated_at
98465
- ) VALUES (
98466
- ${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
98467
- ${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
98468
- ${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
98469
- ${now.toISOString()}
98470
- )
98471
- `;
98472
- return {
98473
- transferId: input.transferId,
98474
- nextOffset: 0,
98475
- state: "open"
98476
- };
98477
- });
98478
- const beginWriteEffect = fn$1("WorkspaceBulkTransfer.beginWrite")(function* (input) {
98479
- const existing = yield* findTransfer(input.transferId);
98480
- return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
98481
- });
98482
- const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(mapError(mapFailure("begin write", input.transferId)));
98483
- const acceptRetriedChunk = fn$1("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
98484
- const accepted = (yield* sql`
98485
- SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
98486
- chunk_sha256 AS "chunkSha256"
98487
- FROM workspace_bulk_transfer_chunks
98488
- WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
98489
- LIMIT 1
98490
- `)[0];
98491
- if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
98492
- code: "integrity_mismatch",
98493
- message: "Retried bulk transfer chunk differs from the accepted chunk.",
98494
- transferId: input.transferId
98495
- });
98496
- return {
98497
- transferId: input.transferId,
98498
- nextOffset: row.nextOffset,
98499
- duplicate: true
98500
- };
98501
- });
98502
- const validateChunkRange = fn$1("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
98503
- if (input.offset !== row.nextOffset) return yield* transferError({
98504
- code: "offset_mismatch",
98505
- message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
98506
- transferId: input.transferId,
98507
- retryable: true
98508
- });
98509
- const nextOffset = input.offset + byteLength;
98510
- if (nextOffset > row.totalBytes) return yield* transferError({
98511
- code: "size_limit_exceeded",
98512
- message: "Bulk transfer chunk exceeds the declared total byte count.",
98513
- transferId: input.transferId
98514
- });
98515
- return nextOffset;
98516
- });
98517
- const reconcileChunkBytes = fn$1("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
98518
- const tempInfo = yield* fileSystem.stat(row.tempPath);
98519
- if (Number(tempInfo.size) > row.nextOffset) {
98520
- if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
98521
- code: "integrity_mismatch",
98522
- message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
98523
- transferId: input.transferId
98524
- });
98525
- const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
98526
- if ((yield* promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
98527
- code: "integrity_mismatch",
98528
- message: "Bulk transfer temporary bytes differ from the retried chunk.",
98529
- transferId: input.transferId
98530
- });
98531
- return;
98532
- }
98533
- yield* fault("before-byte-write");
98534
- yield* scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(flatMap((file) => file.seek(input.offset, "start").pipe(flatMap(() => file.writeAll(bytes)), flatMap(() => file.sync)))));
98535
- yield* fault("after-byte-write-before-metadata");
98536
- });
98537
- const persistAcceptedChunk = fn$1("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
98538
- const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
98539
- yield* sql.withTransaction(sql`
98540
- INSERT INTO workspace_bulk_transfer_chunks (
98541
- transfer_id, chunk_offset, chunk_bytes, chunk_sha256
98542
- ) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
98543
- ON CONFLICT DO NOTHING
98544
- `.pipe(flatMap(() => sql`
98545
- UPDATE workspace_bulk_transfers
98546
- SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
98547
- WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
98548
- `)));
98549
- yield* fault("after-metadata-advance");
98550
- });
98551
- const writeChunkEffect = fn$1("WorkspaceBulkTransfer.writeChunk")(function* (input) {
98552
- const bytes = yield* promise(() => verifyBulkTransferChunk({
98553
- ...input,
98554
- kind: "chunk"
98555
- }));
98556
- const row = yield* requireOpenTransfer(input.transferId);
98557
- if (row.state === "completed") return yield* transferError({
98558
- code: "offset_mismatch",
98559
- message: "Bulk transfer is already complete.",
98560
- transferId: input.transferId
98561
- });
98562
- if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
98563
- const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
98564
- yield* reconcileChunkBytes(input, row, bytes, nextOffset);
98565
- yield* persistAcceptedChunk(input, nextOffset);
98566
- return {
98567
- transferId: input.transferId,
98568
- nextOffset,
98569
- duplicate: false
98570
- };
98571
- });
98572
- const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(mapError(mapFailure("write chunk", input.transferId)));
98573
- const commitWriteEffect = fn$1("WorkspaceBulkTransfer.commitWrite")(function* (input) {
98574
- const row = yield* requireOpenTransfer(input.transferId);
98575
- if (row.state === "completed") return {
98576
- transferId: row.transferId,
98577
- relativePath: row.relativePath,
98578
- totalBytes: row.totalBytes,
98579
- sha256: row.wholeSha256
98580
- };
98581
- if (row.nextOffset !== row.totalBytes) return yield* transferError({
98582
- code: "offset_mismatch",
98583
- message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
98584
- transferId: row.transferId,
98585
- retryable: true
98586
- });
98587
- if ((yield* resolveTarget({
98588
- cwd: row.workspaceRoot,
98589
- relativePath: row.relativePath,
98590
- createParent: false
98591
- })).targetPath !== row.targetPath) return yield* transferError({
98592
- code: "symlink_escape",
98593
- message: "Workspace file destination changed during transfer.",
98594
- transferId: row.transferId
98595
- });
98596
- const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(catch_(() => succeed(null)));
98597
- const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
98598
- const candidate = yield* fileSystem.readFile(candidatePath);
98599
- if (candidate.byteLength !== row.totalBytes || (yield* promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
98600
- code: "integrity_mismatch",
98601
- message: "Bulk transfer payload does not match its declared size and digest.",
98602
- transferId: row.transferId
98603
- });
98604
- if (tempInfo !== null) {
98605
- yield* fileSystem.rename(row.tempPath, row.targetPath);
98606
- yield* fault("after-atomic-rename");
98607
- }
98608
- yield* sql`
98609
- UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
98610
- WHERE transfer_id = ${row.transferId}
98611
- `;
98612
- yield* workspaceEntries.invalidate(row.workspaceRoot);
98613
- return {
98614
- transferId: row.transferId,
98615
- relativePath: row.relativePath,
98616
- totalBytes: row.totalBytes,
98617
- sha256: row.wholeSha256
98618
- };
98619
- });
98620
- const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(mapError(mapFailure("commit write", input.transferId)));
98621
- const cancelWriteEffect = fn$1("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
98622
- const row = yield* requireTransfer(input.transferId);
98623
- if (row.state !== "completed") {
98624
- yield* fileSystem.remove(row.tempPath, { force: true });
98625
- yield* sql`
98626
- UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
98627
- WHERE transfer_id = ${input.transferId}
98628
- `;
98629
- }
98630
- return {
98631
- transferId: input.transferId,
98632
- state: row.state === "completed" ? "completed" : "cancelled"
98633
- };
98634
- });
98635
- const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(mapError(mapFailure("cancel write", input.transferId)));
98636
- const cleanupExpiredEffect = fn$1("WorkspaceBulkTransfer.cleanupExpired")(function* () {
98637
- const now = (/* @__PURE__ */ new Date()).toISOString();
98638
- const rows = yield* sql`
98639
- SELECT transfer_id AS "transferId", temp_path AS "tempPath"
98640
- FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
98641
- `;
98642
- let expiredCount = 0;
98643
- for (const row of rows) if (yield* withTransferLock(row.transferId, gen(function* () {
98644
- const current = yield* findTransfer(row.transferId);
98645
- if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
98646
- yield* fileSystem.remove(current.tempPath, { force: true });
98647
- yield* sql`
98648
- UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
98649
- WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
98650
- `;
98651
- return true;
98652
- }))) expiredCount += 1;
98653
- return expiredCount;
98654
- });
98655
- const cleanupExpired = () => cleanupExpiredEffect().pipe(mapError(mapFailure("expiry cleanup")));
98656
- return {
98657
- readFile,
98658
- beginWrite,
98659
- writeChunk,
98660
- commitWrite,
98661
- cancelWrite,
98662
- cleanupExpired
98663
- };
98664
- });
98665
- const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
98666
98342
  //#endregion
98667
98343
  //#region src/projectFile/readProjectFile.ts
98668
98344
  /**
@@ -99464,6 +99140,26 @@ const DevTunnelLive = effect(DevTunnel, gen(function* () {
99464
99140
  };
99465
99141
  }));
99466
99142
  //#endregion
99143
+ //#region src/workspace/Services/WorkspaceEntries.ts
99144
+ /**
99145
+ * WorkspaceEntries - Effect service contract for cached workspace entry search.
99146
+ *
99147
+ * Owns indexed workspace entry search plus cache invalidation for workspace
99148
+ * roots when the underlying filesystem changes.
99149
+ *
99150
+ * @module WorkspaceEntries
99151
+ */
99152
+ var WorkspaceEntriesError = class extends TaggedErrorClass()("WorkspaceEntriesError", {
99153
+ cwd: String$1,
99154
+ operation: String$1,
99155
+ detail: String$1,
99156
+ cause: optional$4(Defect)
99157
+ }) {};
99158
+ /**
99159
+ * WorkspaceEntries - Service tag for cached workspace entry search.
99160
+ */
99161
+ var WorkspaceEntries = class extends Service$1()("forge/workspace/Services/WorkspaceEntries") {};
99162
+ //#endregion
99467
99163
  //#region src/workspace/Services/WorkspaceFileSystem.ts
99468
99164
  /**
99469
99165
  * WorkspaceFileSystem - Effect service contract for workspace file mutations.
@@ -99485,6 +99181,9 @@ var WorkspaceFileSystemError = class extends TaggedErrorClass()("WorkspaceFileSy
99485
99181
  */
99486
99182
  var WorkspaceFileSystem = class extends Service$1()("forge/workspace/Services/WorkspaceFileSystem") {};
99487
99183
  //#endregion
99184
+ //#region src/workspace/Services/WorkspaceBulkTransfer.ts
99185
+ var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
99186
+ //#endregion
99488
99187
  //#region src/workspace/Services/WorkspaceReadinessState.ts
99489
99188
  var WorkspaceReadinessState = class extends Service$1()("forge/workspace/Services/WorkspaceReadinessState") {};
99490
99189
  //#endregion
@@ -104941,6 +104640,13 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
104941
104640
  message: "Failed to replay orchestration events",
104942
104641
  cause
104943
104642
  }))), { "rpc.aggregate": "orchestration" }),
104643
+ [ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot]: (_input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, gen(function* () {
104644
+ yield* startup.awaitCommandReady;
104645
+ return yield* projectionSnapshotQuery.getArchivedShellSnapshot();
104646
+ }).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
104647
+ message: "Failed to load archived orchestration shell snapshot",
104648
+ cause
104649
+ }))), { "rpc.aggregate": "orchestration" }),
104944
104650
  [ORCHESTRATION_WS_METHODS.subscribeShell]: (_input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeShell, succeed(makeSnapshotThenLiveStream({
104945
104651
  loadSnapshot: loadInitialShellSnapshot({
104946
104652
  startup,
@@ -210937,56 +210643,169 @@ function hasConfiguredApexCredential(profiles) {
210937
210643
  }));
210938
210644
  }
210939
210645
  //#endregion
210940
- //#region src/provider/claudeCredentialContainment.ts
210646
+ //#region src/provider/claudeCredentialDirectory.ts
210941
210647
  /**
210942
- * Containment for provider credentials inherited by the Claude Agent SDK child.
210648
+ * Per-account credential directories for Claude turns.
210943
210649
  *
210944
- * Pure and dependency-free on purpose: ClaudeAdapter pulls in Effect platform
210945
- * services, the browser manager, and the server config, so a unit test that
210946
- * imports it cannot run in a worktree. This rule is cost-critical and must stay
210947
- * directly testable.
210650
+ * Claude Code keeps conversation transcripts under
210651
+ * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
210652
+ * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
210653
+ * credential file, so each Provider Account can hold its own login while every
210654
+ * session file stays in one place and `--resume` keeps finding its target. See
210655
+ * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
210948
210656
  *
210949
- * Audit finding S1 (2026-07-25). The Claude Agent SDK reads ANTHROPIC_API_KEY /
210950
- * ANTHROPIC_AUTH_TOKEN from its own environment, and those TAKE PRECEDENCE over
210951
- * the user's claude.ai OAuth login — that precedence is exactly what the
210952
- * adapter relies on to redirect billing at the proxy.
210657
+ * That variable is not documented. Under
210658
+ * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
210659
+ * only allowed because absence degrades to a named behaviour that already
210660
+ * works: a thread with no account runs on the Environment's own login. This
210661
+ * module sets the variable only when the resolved directory holds a credential
210662
+ * the CLI can use, because a missing file, or one whose access token a refusal
210663
+ * has blanked, makes the CLI report logged out and a live turn would fail.
210953
210664
  *
210954
- * So on a plain Claude turn (no endpoint profile, no proxy credential), which
210955
- * is supposed to run on the personal subscription, an ANTHROPIC_* key inherited
210956
- * from the server's environment would send the turn straight to
210957
- * api.anthropic.com on a metered account — never reaching
210958
- * /api/provider-egress/*, leaving the fail-closed billing gate inert.
210665
+ * Nothing writes these directories yet. #3049 adds the writer and keys each
210666
+ * directory on the account identity CMMD writes on the delivery receipt it
210667
+ * signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
210668
+ * every runtime takes the `no-account` skip.
210959
210669
  *
210960
- * Forge Cloud sprites were only incidentally safe (no Anthropic key present on
210961
- * the box). This makes containment an enforced property rather than an
210962
- * assumption about what the deployment environment happens to contain.
210670
+ * Free of Effect services on purpose, for the same reason as
210671
+ * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
210672
+ * services, the browser manager and the server config, so a unit test that
210673
+ * imports it cannot run in a worktree. This rule decides which account pays for
210674
+ * a turn, so it has to stay directly testable.
210963
210675
  */
210964
- /** Credentials the Claude Agent SDK will prefer over the user's OAuth login. */
210965
- const INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
210676
+ /** Repoints `.credentials.json` without moving the transcripts. */
210677
+ const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
210678
+ /** The one file Claude Code writes into the credential directory. */
210679
+ const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
210966
210680
  /**
210967
- * Routing override the SDK would obey instead of the login's default endpoint.
210968
- * A CMMD cloud runtime exports it to point at the workspace provider-egress
210969
- * proxy; on a PERSONAL-Claude-in-VM runtime that must be dropped so the turn
210970
- * spends the on-box ~/.claude subscription (card 1359 Phase 2). Stripped ONLY
210971
- * when the runtime signals in-VM delivery — otherwise existing behavior (the
210972
- * inherited base URL survives) is preserved exactly.
210681
+ * Runtime env var naming the Provider Account this runtime's Claude turns
210682
+ * belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
210683
+ * and each keeps its one login.
210684
+ *
210685
+ * It is a bridge that will not get a writer. #3073 settled the key in
210686
+ * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
210687
+ * comes from the identity CMMD issues and writes on the delivery receipt it
210688
+ * signs, and this variable goes away. #3049 makes that change. The credential
210689
+ * generation was ruled out as a key: it rotates on every reconnect, and CMMD
210690
+ * uses it as the primary account's id, so keying on it would strand every
210691
+ * thread pinned to that account on a directory that stops receiving deliveries.
210692
+ *
210693
+ * Tests reach the `credentialDirectory` parameter on
210694
+ * `ClaudeAdapter.buildClaudeQueryEnv` directly.
210973
210695
  */
210974
- const ANTHROPIC_ENDPOINT_OVERRIDE_KEY = "ANTHROPIC_BASE_URL";
210696
+ const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
210697
+ /** Directory under the server's base directory that holds one directory per account. */
210698
+ const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
210699
+ const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
210700
+ const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
210701
+ const DIRECTORY_NAME_HASH_LENGTH = 32;
210975
210702
  /**
210976
- * Remove inherited Anthropic credentials so the child falls back to the user's
210977
- * personal claude.ai login. Call this ONLY on the path where no base URL
210978
- * resolved; when the adapter has a profile or proxy credential it sets these
210979
- * variables deliberately, and that assignment must win.
210703
+ * Directory name for an account. A hash, so no email or display name reaches a
210704
+ * filesystem path and no path-illegal character can appear in one either.
210705
+ */
210706
+ function claudeCredentialDirectoryName(accountIdentity) {
210707
+ return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
210708
+ }
210709
+ /** The account this runtime's Claude turns belong to, or null while the variable is unset. */
210710
+ function resolveClaudeProviderAccountIdentity(env) {
210711
+ const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
210712
+ if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
210713
+ return identity;
210714
+ }
210715
+ /**
210716
+ * Where per-account credential directories live: `claude-credentials` under the
210717
+ * server's base directory, `ServerConfig.baseDir`, which is `FORGE_HOME` or
210718
+ * `~/.forge`. A delivery writer gets the same `ServerConfig`, so reader and
210719
+ * writer cannot disagree about the root. Always outside `CLAUDE_CONFIG_DIR`, so
210720
+ * one of these can never be mistaken for the main credential store, and a stray
210721
+ * read of the config directory cannot walk into another account's login.
210722
+ */
210723
+ function claudeCredentialDirectoryRoot(env, baseDir) {
210724
+ if (!isAbsolute(baseDir)) return null;
210725
+ const root = resolve(baseDir, CREDENTIAL_ROOT_DIRECTORY_NAME);
210726
+ const configDirectory = claudeConfigDirectory(env);
210727
+ if (configDirectory && isPathWithinRoot({
210728
+ relative,
210729
+ isAbsolute
210730
+ }, configDirectory, root)) return null;
210731
+ return root;
210732
+ }
210733
+ /** Absolute credential directory for one account, or null when either half is missing. */
210734
+ function resolveClaudeCredentialDirectory(input) {
210735
+ if (!input.accountIdentity) return null;
210736
+ const root = claudeCredentialDirectoryRoot(input.env, input.baseDir);
210737
+ if (!root) return null;
210738
+ return join(root, claudeCredentialDirectoryName(input.accountIdentity));
210739
+ }
210740
+ /**
210741
+ * Decide whether this turn may point at a per-account credential directory.
210980
210742
  *
210981
- * `stripEndpointOverride` additionally drops ANTHROPIC_BASE_URL — pass it only on
210982
- * a personal-Claude-in-VM runtime (gated), never by default, so existing turns
210983
- * that rely on an inherited base URL are untouched.
210743
+ * The cheap checks run first and the capability probe runs last, so a runtime
210744
+ * with no usable credential never spawns the CLI. The probe's error channel is
210745
+ * empty, so nothing here catches.
210746
+ *
210747
+ * A skip is not a fallback onto the default account. With no account the turn
210748
+ * runs on the Environment's own login, unchanged. A thread pinned to an account
210749
+ * whose credential is missing is refused by name under
210750
+ * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
210751
+ * `no-usable-credential`, and `capability-not-supported` for a pinned account,
210752
+ * into those refusals. Until #3049 nothing names an account, so `no-account` is
210753
+ * the only skip a runtime sees.
210754
+ */
210755
+ function decideClaudeCredentialDirectory(input) {
210756
+ return gen(function* () {
210757
+ if (!input.accountIdentity) return skip("no-account");
210758
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
210759
+ const directory = resolveClaudeCredentialDirectory(input);
210760
+ if (!directory) return skip("no-root");
210761
+ if (!holdsUsableCredential(directory)) return skip("no-usable-credential");
210762
+ return (yield* input.probeSupport) === "supported" ? set(directory) : skip("capability-not-supported");
210763
+ });
210764
+ }
210765
+ /**
210766
+ * True when `.credentials.json` parses and carries a non-empty access token.
210767
+ *
210768
+ * File presence is not readiness. A refused credential is rewritten in place
210769
+ * with `accessToken: ""`, so a file that exists can still leave the CLI logged
210770
+ * out. Only the token's length is checked. The value is never logged, returned
210771
+ * or kept.
210984
210772
  */
210985
- function withoutInheritedAnthropicCredentials(env, options) {
210986
- const contained = { ...env };
210987
- for (const key of INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS) delete contained[key];
210988
- if (options?.stripEndpointOverride) delete contained[ANTHROPIC_ENDPOINT_OVERRIDE_KEY];
210989
- return contained;
210773
+ function holdsUsableCredential(directory) {
210774
+ const file = join(directory, CLAUDE_CREDENTIAL_FILE_NAME);
210775
+ try {
210776
+ if (!statSync(file).isFile()) return false;
210777
+ return hasAccessToken(JSON.parse(readFileSync(file, "utf8")));
210778
+ } catch {
210779
+ return false;
210780
+ }
210781
+ }
210782
+ function hasAccessToken(credential) {
210783
+ if (typeof credential !== "object" || credential === null) return false;
210784
+ const oauth = credential.claudeAiOauth;
210785
+ if (typeof oauth !== "object" || oauth === null) return false;
210786
+ const token = oauth.accessToken;
210787
+ return typeof token === "string" && token.length > 0;
210788
+ }
210789
+ function set(directory) {
210790
+ return {
210791
+ outcome: "set",
210792
+ directory
210793
+ };
210794
+ }
210795
+ function skip(reason) {
210796
+ return {
210797
+ outcome: "skipped",
210798
+ reason
210799
+ };
210800
+ }
210801
+ function claudeConfigDirectory(env) {
210802
+ const configured = env.CLAUDE_CONFIG_DIR?.trim();
210803
+ if (configured) return configured;
210804
+ try {
210805
+ return join(homedir(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
210806
+ } catch {
210807
+ return null;
210808
+ }
210990
210809
  }
210991
210810
  //#endregion
210992
210811
  //#region src/provider/personalClaudeInVm.ts
@@ -211056,160 +210875,76 @@ function personalClaudeGenerationHash(env = process.env) {
211056
210875
  }
211057
210876
  }
211058
210877
  //#endregion
211059
- //#region src/provider/claudeCredentialDirectory.ts
210878
+ //#region src/provider/claudeCredentialContainment.ts
211060
210879
  /**
211061
- * Per-account credential directories for Claude turns.
211062
- *
211063
- * Claude Code keeps conversation transcripts under
211064
- * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
211065
- * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
211066
- * credential file, so each Provider Account can hold its own login while every
211067
- * session file stays in one place and `--resume` keeps finding its target. See
211068
- * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
210880
+ * Containment for provider credentials inherited by the Claude Agent SDK child.
211069
210881
  *
211070
- * That variable is not documented. Under
211071
- * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
211072
- * only allowed because absence degrades to a named behaviour that already
211073
- * works: one account per Environment, exactly what runs today. This module is
211074
- * the gate that keeps the degrade honest. It sets the variable only when a
211075
- * readable `.credentials.json` is already sitting in the resolved directory,
211076
- * because a missing or empty directory makes the CLI report logged out and a
211077
- * live turn would fail. Nothing writes these directories yet, so every runtime
211078
- * today takes the skip path.
210882
+ * Free of Effect services on purpose: ClaudeAdapter pulls in Effect platform
210883
+ * services, the browser manager, and the server config, so a unit test that
210884
+ * imports it cannot run in a worktree. This rule is cost-critical and must stay
210885
+ * directly testable.
211079
210886
  *
211080
- * Pure and dependency-free on purpose, for the same reason as
211081
- * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
211082
- * services, the browser manager and the server config, so a unit test that
211083
- * imports it cannot run in a worktree. This rule decides which account pays for
211084
- * a turn, so it has to stay directly testable.
211085
- */
211086
- /** Repoints `.credentials.json` without moving the transcripts. */
211087
- const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
211088
- /** The one file Claude Code writes into the credential directory. */
211089
- const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
211090
- /**
211091
- * Runtime env var naming the Provider Account this runtime's Claude turns
211092
- * belong to.
210887
+ * Audit finding S1 (2026-07-25). The Claude Agent SDK reads ANTHROPIC_API_KEY /
210888
+ * ANTHROPIC_AUTH_TOKEN from its own environment, and those TAKE PRECEDENCE over
210889
+ * the user's claude.ai OAuth login — that precedence is exactly what the
210890
+ * adapter relies on to redirect billing at the proxy.
211093
210891
  *
211094
- * NOTHING SETS THIS. It has no writer anywhere in Forge or CMMD, so as shipped
211095
- * the per-account lane can never turn on and every runtime keeps today's single
211096
- * account. The writer waits on issue 3073, which decides which identity keys
211097
- * the directory. That has to be settled first: the credential generation
211098
- * rotates on every re-delivery, so keying on it would give a new directory
211099
- * each time and defeat the point.
210892
+ * So on a plain Claude turn (no endpoint profile, no proxy credential), which
210893
+ * is supposed to run on the personal subscription, an ANTHROPIC_* key inherited
210894
+ * from the server's environment would send the turn straight to
210895
+ * api.anthropic.com on a metered account — never reaching
210896
+ * /api/provider-egress/*, leaving the fail-closed billing gate inert.
211100
210897
  *
211101
- * It is also not the seam. The seam is the `credentialDirectory` parameter on
211102
- * `ClaudeAdapter.buildClaudeQueryEnv`, which is what tests and callers drive.
211103
- * This variable is one bridge into that parameter, and the delivery step is
211104
- * free to replace it with something better rather than adopt it.
210898
+ * Forge Cloud sprites were only incidentally safe (no Anthropic key present on
210899
+ * the box). This makes containment an enforced property rather than an
210900
+ * assumption about what the deployment environment happens to contain.
211105
210901
  */
211106
- const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
211107
- /** Operator escape hatch for the directory root, mostly for local debugging. */
211108
- const CLAUDE_CREDENTIAL_ROOT_ENV = "FORGE_CLAUDE_CREDENTIAL_ROOT";
211109
- const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
211110
- const FALLBACK_ROOT_DIRECTORY_NAME = ".forge-claude-credentials";
211111
- const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
211112
- const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
211113
- const DIRECTORY_NAME_HASH_LENGTH = 32;
210902
+ /** Credentials the Claude Agent SDK will prefer over the user's OAuth login. */
210903
+ const INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
211114
210904
  /**
211115
- * Directory name for an account. A hash, so no email or display name reaches a
211116
- * filesystem path and no path-illegal character can appear in one either.
210905
+ * Routing override the SDK would obey instead of the login's default endpoint.
210906
+ * A CMMD cloud runtime exports it to point at the workspace provider-egress
210907
+ * proxy; on a PERSONAL-Claude-in-VM runtime that must be dropped so the turn
210908
+ * spends the on-box ~/.claude subscription (card 1359 Phase 2). Stripped ONLY
210909
+ * when the runtime signals in-VM delivery — otherwise existing behavior (the
210910
+ * inherited base URL survives) is preserved exactly.
211117
210911
  */
211118
- function claudeCredentialDirectoryName(accountIdentity) {
211119
- return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
211120
- }
211121
- /** The account this runtime's Claude turns belong to, or null on every runtime today. */
211122
- function resolveClaudeProviderAccountIdentity(env) {
211123
- const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
211124
- if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
211125
- return identity;
211126
- }
210912
+ const ANTHROPIC_ENDPOINT_OVERRIDE_KEY = "ANTHROPIC_BASE_URL";
211127
210913
  /**
211128
- * Where per-account credential directories live. Always outside
211129
- * `CLAUDE_CONFIG_DIR`, so one of them can never be mistaken for the main
211130
- * credential store, and so a stray read of the config directory cannot walk
211131
- * into another account's login.
210914
+ * Remove inherited Anthropic credentials so the child falls back to the user's
210915
+ * personal claude.ai login. Call this ONLY on the path where no base URL
210916
+ * resolved; when the adapter has a profile or proxy credential it sets these
210917
+ * variables deliberately, and that assignment must win.
210918
+ *
210919
+ * `stripEndpointOverride` additionally drops ANTHROPIC_BASE_URL — pass it only on
210920
+ * a personal-Claude-in-VM runtime (gated), never by default, so existing turns
210921
+ * that rely on an inherited base URL are untouched.
211132
210922
  */
211133
- function claudeCredentialDirectoryRoot(env) {
211134
- const root = credentialRootCandidate(env);
211135
- if (!root || !isAbsolute(root)) return null;
211136
- return isInside(root, claudeConfigDirectory(env)) ? null : resolve(root);
211137
- }
211138
- /** Absolute credential directory for one account, or null when either half is missing. */
211139
- function resolveClaudeCredentialDirectory(input) {
211140
- if (!input.accountIdentity) return null;
211141
- const root = claudeCredentialDirectoryRoot(input.env);
211142
- if (!root) return null;
211143
- return join(root, claudeCredentialDirectoryName(input.accountIdentity));
210923
+ function withoutInheritedAnthropicCredentials(env, options) {
210924
+ const contained = { ...env };
210925
+ for (const key of INHERITABLE_ANTHROPIC_CREDENTIAL_KEYS) delete contained[key];
210926
+ if (options?.stripEndpointOverride) delete contained[ANTHROPIC_ENDPOINT_OVERRIDE_KEY];
210927
+ return contained;
211144
210928
  }
211145
210929
  /**
211146
- * Decide whether this turn may point at a per-account credential directory.
210930
+ * The environment a Claude child gets when it runs on the personal
210931
+ * subscription. Server-only secrets are scrubbed, inherited Anthropic keys are
210932
+ * removed so they cannot outrank the login, and the credential directory is set
210933
+ * only when one was decided. The turn's personal-subscription branch and the
210934
+ * capability probe both build their environment here, so the probe asks about
210935
+ * the setup the turn will run with.
211147
210936
  *
211148
- * The cheap checks run first and the capability probe runs last, so a runtime
211149
- * with no delivered credential never spawns the CLI. Any failure is a skip, so
211150
- * the worst outcome is today's behaviour.
211151
- */
211152
- async function decideClaudeCredentialDirectory(input) {
211153
- if (!input.accountIdentity) return skip("no-account");
211154
- if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
211155
- const directory = resolveClaudeCredentialDirectory(input);
211156
- if (!directory) return skip("no-root");
211157
- if (!(input.credentialFilePresent ?? hasReadableCredentialFile)(directory)) return skip("no-credential-file");
211158
- return await probeSupportSafely(input.probeSupport) === "supported" ? {
211159
- outcome: "set",
211160
- directory
211161
- } : skip("capability-not-supported");
211162
- }
211163
- /** True when `.credentials.json` is in the directory and readable right now. */
211164
- function hasReadableCredentialFile(directory) {
211165
- const file = join(directory, CLAUDE_CREDENTIAL_FILE_NAME);
211166
- try {
211167
- if (!statSync(file).isFile()) return false;
211168
- accessSync(file, constants.R_OK);
211169
- return true;
211170
- } catch {
211171
- return false;
211172
- }
211173
- }
211174
- function skip(reason) {
210937
+ * The endpoint override goes only when `personalClaudeInVmEnabled` says this
210938
+ * runtime is on the personal Claude lane. That check also reads the activation
210939
+ * marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
210940
+ * has the marker and no env var until it restarts, drops the override too.
210941
+ */
210942
+ function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
211175
210943
  return {
211176
- outcome: "skipped",
211177
- reason
210944
+ ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
210945
+ ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
211178
210946
  };
211179
210947
  }
211180
- /** `inconclusive` is treated as `unsupported`, and so is a probe that throws. */
211181
- async function probeSupportSafely(probeSupport) {
211182
- try {
211183
- return await probeSupport();
211184
- } catch {
211185
- return "inconclusive";
211186
- }
211187
- }
211188
- function credentialRootCandidate(env) {
211189
- const explicit = env[CLAUDE_CREDENTIAL_ROOT_ENV]?.trim();
211190
- if (explicit) return explicit;
211191
- const forgeHome = env.FORGE_HOME?.trim();
211192
- if (forgeHome) return join(forgeHome, CREDENTIAL_ROOT_DIRECTORY_NAME);
211193
- try {
211194
- return join(homedir(), FALLBACK_ROOT_DIRECTORY_NAME);
211195
- } catch {
211196
- return null;
211197
- }
211198
- }
211199
- function claudeConfigDirectory(env) {
211200
- const configured = env.CLAUDE_CONFIG_DIR?.trim();
211201
- if (configured) return configured;
211202
- try {
211203
- return join(homedir(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
211204
- } catch {
211205
- return null;
211206
- }
211207
- }
211208
- function isInside(candidate, parent) {
211209
- if (!parent) return false;
211210
- const step = relative(resolve(parent), resolve(candidate));
211211
- return step === "" || !step.startsWith("..") && !isAbsolute(step);
211212
- }
211213
210948
  //#endregion
211214
210949
  //#region src/provider/claudeCredentialDirectoryProbe.ts
211215
210950
  /**
@@ -211222,6 +210957,11 @@ function isInside(candidate, parent) {
211222
210957
  * A runtime with no credential at all is logged out either way and cannot
211223
210958
  * discriminate, which is `inconclusive`, and callers read that as unsupported.
211224
210959
  *
210960
+ * An answer only counts for the binary and environment the turn will use. The
210961
+ * probe spawns the `binaryPath` the turn hands the SDK, and builds its
210962
+ * environment with `claudeSubscriptionChildEnv`, the same builder as the turn's
210963
+ * personal-subscription branch.
210964
+ *
211225
210965
  * `claude auth status` is a pure read. It sends no prompt and leaves
211226
210966
  * `.credentials.json` byte-identical, so probing spends no subscription usage.
211227
210967
  * It prints JSON on stdout and exits 1 when logged out, so the exit code is not
@@ -211237,38 +210977,62 @@ const VERSION_ARGS = ["--version"];
211237
210977
  * hung or unkillable process costs a few seconds and an `inconclusive`.
211238
210978
  */
211239
210979
  const PROBE_TIMEOUT_MS = 5e3;
210980
+ const PROBE_OUTPUT_LIMIT_BYTES = 1024 * 1024;
211240
210981
  const PROBE_DIRECTORY_PREFIX = "forge-claude-capability-";
211241
210982
  const VERSION_PATTERN = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
211242
210983
  function makeClaudeCredentialDirectoryProbe(deps) {
211243
210984
  const answers = /* @__PURE__ */ new Map();
211244
- return { support: async () => {
210985
+ const answer = async () => {
211245
210986
  const version = await readVersionSafely(deps);
211246
210987
  if (version === null) return runProbe(deps);
211247
210988
  const cached = answers.get(version);
211248
210989
  if (cached) return cached;
211249
210990
  const pending = runProbe(deps);
211250
210991
  answers.set(version, pending);
210992
+ const forget = () => {
210993
+ if (answers.get(version) === pending) answers.delete(version);
210994
+ };
210995
+ pending.then((result) => {
210996
+ if (result === "inconclusive") forget();
210997
+ }, forget);
211251
210998
  return pending;
211252
- } };
210999
+ };
211000
+ return { support: tryPromise(answer).pipe(orElseSucceed(() => "inconclusive")) };
211253
211001
  }
211254
211002
  /**
211255
- * One probe per binary path for the life of the process. The probe spawns the
211256
- * CLI twice, and every Claude session start would otherwise pay for it.
211003
+ * One probe per binary path for the life of the process. A conclusive answer is
211004
+ * cached per binary version, so the two `auth status` spawns run once per path
211005
+ * and version. `claude --version` still spawns on every call, which is how an
211006
+ * in-place CLI upgrade gets a fresh answer.
211257
211007
  */
211258
211008
  const probesByBinaryPath = /* @__PURE__ */ new Map();
211009
+ const INCONCLUSIVE_PROBE = { support: succeed("inconclusive") };
211010
+ /**
211011
+ * The probe for the binary a turn with this `binaryPath` setting runs.
211012
+ *
211013
+ * `buildClaudeQueryOptions` hands the setting to the SDK as
211014
+ * `pathToClaudeCodeExecutable`, and the SDK spawns it as given, so the default
211015
+ * `claude` goes through PATH for the probe exactly as for the turn. The SDK
211016
+ * picks a bundled build of its own only when the path is empty, and the
211017
+ * settings schema turns an empty value into `claude`. Should one arrive empty
211018
+ * anyway, the answer is `inconclusive` rather than an answer about another
211019
+ * binary.
211020
+ */
211259
211021
  function claudeCredentialDirectoryProbeFor(binaryPath) {
211260
- const binary = binaryPath?.trim() || "claude";
211261
- const existing = probesByBinaryPath.get(binary);
211022
+ if (!binaryPath) return INCONCLUSIVE_PROBE;
211023
+ const existing = probesByBinaryPath.get(binaryPath);
211262
211024
  if (existing) return existing;
211263
- const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binary));
211264
- probesByBinaryPath.set(binary, probe);
211025
+ const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binaryPath));
211026
+ probesByBinaryPath.set(binaryPath, probe);
211265
211027
  return probe;
211266
211028
  }
211267
- function claudeCredentialDirectoryProbeDeps(binaryPath) {
211029
+ const spawnClaude = (file, args, options) => execFileAsync$3(file, [...args], { ...options });
211030
+ function claudeCredentialDirectoryProbeDeps(binaryPath, options = {}) {
211031
+ const run = (args, credentialDirectory) => runClaudeCommand$1(options.execFile ?? spawnClaude, binaryPath, args, claudeSubscriptionChildEnv(options.baseEnv ?? process.env, credentialDirectory));
211268
211032
  return {
211269
- runAuthStatus: ({ credentialDirectory }) => runClaudeCommand$1(binaryPath, AUTH_STATUS_ARGS, credentialDirectory),
211033
+ runAuthStatus: ({ credentialDirectory }) => run(AUTH_STATUS_ARGS, credentialDirectory),
211270
211034
  readBinaryVersion: async () => {
211271
- const { stdout } = await runClaudeCommand$1(binaryPath, VERSION_ARGS, null);
211035
+ const { stdout } = await run(VERSION_ARGS, null);
211272
211036
  return VERSION_PATTERN.exec(stdout)?.[0] ?? null;
211273
211037
  },
211274
211038
  makeProbeDirectory: () => mkdtemp(join(tmpdir(), PROBE_DIRECTORY_PREFIX)),
@@ -211278,6 +211042,7 @@ function claudeCredentialDirectoryProbeDeps(binaryPath) {
211278
211042
  })
211279
211043
  };
211280
211044
  }
211045
+ /** Rejects when the probe could not tell. `support` turns that into `inconclusive`. */
211281
211046
  async function runProbe(deps) {
211282
211047
  let probeDirectory = null;
211283
211048
  try {
@@ -211287,8 +211052,6 @@ async function runProbe(deps) {
211287
211052
  const redirected = parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: directory })));
211288
211053
  if (redirected === false) return "supported";
211289
211054
  return redirected === true ? "unsupported" : "inconclusive";
211290
- } catch {
211291
- return "inconclusive";
211292
211055
  } finally {
211293
211056
  if (probeDirectory !== null) await deps.removeProbeDirectory(probeDirectory).catch(() => void 0);
211294
211057
  }
@@ -211310,15 +211073,15 @@ async function runBounded(deps, operation) {
211310
211073
  let timer;
211311
211074
  try {
211312
211075
  return await Promise.race([operation(), new Promise((_resolve, reject) => {
211313
- timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude auth status timed out")), timeoutMs);
211076
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude probe call timed out")), timeoutMs);
211314
211077
  })]);
211315
211078
  } finally {
211316
211079
  if (timer !== void 0) clearTimeout(timer);
211317
211080
  }
211318
211081
  }
211319
211082
  /** The `loggedIn` field, or null when the output does not carry one. */
211320
- function parseLoggedIn(run) {
211321
- const parsed = parseJsonObject$1(run.stdout);
211083
+ function parseLoggedIn(output) {
211084
+ const parsed = parseJsonObject$1(output.stdout);
211322
211085
  return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
211323
211086
  }
211324
211087
  function parseJsonObject$1(output) {
@@ -211333,27 +211096,18 @@ function parseJsonObject$1(output) {
211333
211096
  return null;
211334
211097
  }
211335
211098
  }
211336
- async function runClaudeCommand$1(binaryPath, args, credentialDirectory) {
211099
+ async function runClaudeCommand$1(execFileImpl, binaryPath, args, env) {
211337
211100
  try {
211338
- const { stdout } = await execFileAsync$3(binaryPath, [...args], {
211339
- env: {
211340
- ...process.env,
211341
- ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
211342
- },
211343
- maxBuffer: 1024 * 1024,
211101
+ const { stdout } = await execFileImpl(binaryPath, args, {
211102
+ env,
211103
+ maxBuffer: PROBE_OUTPUT_LIMIT_BYTES,
211344
211104
  timeout: PROBE_TIMEOUT_MS
211345
211105
  });
211346
- return {
211347
- stdout,
211348
- exitCode: 0
211349
- };
211106
+ return { stdout };
211350
211107
  } catch (cause) {
211351
211108
  const failure = cause;
211352
211109
  if (typeof failure.stdout !== "string") throw cause;
211353
- return {
211354
- stdout: failure.stdout,
211355
- exitCode: typeof failure.code === "number" ? failure.code : null
211356
- };
211110
+ return { stdout: failure.stdout };
211357
211111
  }
211358
211112
  }
211359
211113
  //#endregion
@@ -211368,12 +211122,12 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
211368
211122
  const forceRefresh = options?.forceRefresh === true;
211369
211123
  const previousSettings = yield* get$6(settingsRef);
211370
211124
  if (!forceRefresh && !input.haveSettingsChanged(previousSettings, nextSettings)) {
211371
- yield* set$2(settingsRef, nextSettings);
211125
+ yield* set$3(settingsRef, nextSettings);
211372
211126
  return yield* get$6(snapshotRef);
211373
211127
  }
211374
211128
  const nextSnapshot = yield* input.checkProvider;
211375
- yield* set$2(settingsRef, nextSettings);
211376
- yield* set$2(snapshotRef, nextSnapshot);
211129
+ yield* set$3(settingsRef, nextSettings);
211130
+ yield* set$3(snapshotRef, nextSnapshot);
211377
211131
  yield* publish(changesPubSub, nextSnapshot);
211378
211132
  return nextSnapshot;
211379
211133
  });
@@ -211393,7 +211147,7 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
211393
211147
  failureCount: failureCount + 1,
211394
211148
  nextDelayMs: Math.min(baseMs * Math.pow(2, failureCount + 1), 3e5)
211395
211149
  });
211396
- } else yield* set$2(refreshFailureCount, 0);
211150
+ } else yield* set$3(refreshFailureCount, 0);
211397
211151
  })).pipe(forkScoped);
211398
211152
  return {
211399
211153
  getSnapshot: input.getSettings.pipe(flatMap(applySnapshot), tapError(logError), orDie),
@@ -212867,10 +212621,12 @@ function claudeForgeAttributionPrompt() {
212867
212621
  * `credentialDirectory` points this turn at one Provider Account's own
212868
212622
  * `.credentials.json` without moving the transcripts, which is what lets an
212869
212623
  * account switch keep `--resume` working
212870
- * (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It is null on
212871
- * every runtime today: `decideClaudeCredentialDirectory` only hands back a
212872
- * directory that already holds a readable credential file, and nothing writes
212873
- * one yet, so the environment stays byte-for-byte what it was.
212624
+ * (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It stays null
212625
+ * until #3049 gives the directories a writer, keyed on the account identity on
212626
+ * the delivery receipt CMMD signs. `decideClaudeCredentialDirectory` only hands
212627
+ * back a directory that already holds a usable credential, and while none
212628
+ * exists the environment is byte-for-byte what it would be without this
212629
+ * parameter.
212874
212630
  *
212875
212631
  * It applies to the personal-subscription branch below and to nothing else. On
212876
212632
  * the profile / proxy branch the credential file is not what authenticates the
@@ -212880,10 +212636,7 @@ function claudeForgeAttributionPrompt() {
212880
212636
  function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
212881
212637
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
212882
212638
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
212883
- if (!effectiveBaseUrl) return {
212884
- ...withoutInheritedAnthropicCredentials(baseEnv, { stripEndpointOverride: personalClaudeInVmEnabled(baseProcessEnv) }),
212885
- ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
212886
- };
212639
+ if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
212887
212640
  const effectiveApiKey = resolveClaudeQueryApiKey(endpointProfile, claudeSettings, providerProxyCredential?.token ?? null);
212888
212641
  return {
212889
212642
  ...baseEnv,
@@ -215151,11 +214904,17 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
215151
214904
  })));
215152
214905
  const claudeSettings = serverSettings.providers.claudeAgent;
215153
214906
  const claudeBinaryPath = claudeSettings.binaryPath;
215154
- const claudeCredentialDirectory = yield* tryPromise(() => decideClaudeCredentialDirectory({
214907
+ const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
215155
214908
  env: process.env,
214909
+ baseDir: serverConfig.baseDir,
215156
214910
  accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
215157
- probeSupport: () => claudeCredentialDirectoryProbeFor(claudeBinaryPath).support()
215158
- })).pipe(map$3((decision) => decision.outcome === "set" ? decision.directory : null), orElseSucceed(() => null));
214911
+ probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
214912
+ });
214913
+ if (credentialDirectoryDecision.outcome === "skipped") {
214914
+ const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
214915
+ yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
214916
+ }
214917
+ const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
215159
214918
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
215160
214919
  const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
215161
214920
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
@@ -215217,7 +214976,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
215217
214976
  effectiveApiModelId: modelConfig.effectiveApiModelId,
215218
214977
  effectiveEffort: modelConfig.effectiveEffort
215219
214978
  });
215220
- yield* set$2(contextRef, context$4);
214979
+ yield* set$3(contextRef, context$4);
215221
214980
  sessions.set(threadId, context$4);
215222
214981
  yield* emitClaudeSessionStartupEvents({
215223
214982
  threadId,
@@ -220098,7 +219857,7 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
220098
219857
  }
220099
219858
  });
220100
219859
  });
220101
- const updateConfigOptions = (response) => set$2(configOptionsRef, sessionConfigOptionsFromSetup(response));
219860
+ const updateConfigOptions = (response) => set$3(configOptionsRef, sessionConfigOptionsFromSetup(response));
220102
219861
  const updateCurrentModeId = (modeId) => update$4(modeStateRef, (current) => current ? {
220103
219862
  ...current,
220104
219863
  currentModeId: modeId
@@ -220157,8 +219916,8 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
220157
219916
  sessionId = created.sessionId;
220158
219917
  sessionSetupResult = created;
220159
219918
  }
220160
- yield* set$2(modeStateRef, parseSessionModeState(sessionSetupResult));
220161
- yield* set$2(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult));
219919
+ yield* set$3(modeStateRef, parseSessionModeState(sessionSetupResult));
219920
+ yield* set$3(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult));
220162
219921
  return {
220163
219922
  sessionId,
220164
219923
  initializeResult,
@@ -220352,10 +220111,10 @@ const makeStartCoordinator = (startStateRef, startOnce) => gen(function* () {
220352
220111
  switch (state._tag) {
220353
220112
  case "Started": return [succeed(state.result), state];
220354
220113
  case "Starting": return [_await(state.deferred), state];
220355
- case "NotStarted": return [startOnce.pipe(tap((result) => set$2(startStateRef, {
220114
+ case "NotStarted": return [startOnce.pipe(tap((result) => set$3(startStateRef, {
220356
220115
  _tag: "Started",
220357
220116
  result
220358
- }).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$2(startStateRef, { _tag: "NotStarted" }))))), {
220117
+ }).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$3(startStateRef, { _tag: "NotStarted" }))))), {
220359
220118
  _tag: "Starting",
220360
220119
  deferred
220361
220120
  }];
@@ -284032,7 +283791,7 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
284032
283791
  let preResolvedCommitSuggestion = void 0;
284033
283792
  const modelSelection = yield* serverSettingsService.getSettings.pipe(map$3((settings) => settings.textGenerationModelSelection), mapError((cause) => gitManagerError("runStackedAction", "Failed to get server settings.", cause)));
284034
283793
  if (input.featureBranch) {
284035
- yield* set$2(currentPhase, some("branch"));
283794
+ yield* set$3(currentPhase, some("branch"));
284036
283795
  yield* progress.emit({
284037
283796
  kind: "phase_started",
284038
283797
  phase: "branch",
@@ -284045,12 +283804,12 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
284045
283804
  } else branchStep = { status: "skipped_not_requested" };
284046
283805
  const currentBranch = branchStep.name ?? initialStatus.branch;
284047
283806
  const commitAction = isCommitAction(input.action) ? input.action : null;
284048
- const commit = commitAction ? yield* set$2(currentPhase, some("commit")).pipe(flatMap(() => runCommitStep(modelSelection, input.cwd, commitAction, currentBranch, commitMessageForStep, preResolvedCommitSuggestion, input.filePaths, options?.progressReporter, progress.actionId, input.threadId))) : { status: "skipped_not_requested" };
283807
+ const commit = commitAction ? yield* set$3(currentPhase, some("commit")).pipe(flatMap(() => runCommitStep(modelSelection, input.cwd, commitAction, currentBranch, commitMessageForStep, preResolvedCommitSuggestion, input.filePaths, options?.progressReporter, progress.actionId, input.threadId))) : { status: "skipped_not_requested" };
284049
283808
  const push = wantsPush ? yield* progress.emit({
284050
283809
  kind: "phase_started",
284051
283810
  phase: "push",
284052
283811
  label: "Pushing..."
284053
- }).pipe(tap(() => set$2(currentPhase, some("push"))), flatMap(() => {
283812
+ }).pipe(tap(() => set$3(currentPhase, some("push"))), flatMap(() => {
284054
283813
  const emit = (event) => progress.emit(event);
284055
283814
  const hookProgress = createHookProgressEmitter(emit);
284056
283815
  return gitCore.pushCurrentBranch(input.cwd, currentBranch, { progress: hookProgress.progress }).pipe(tap(() => {
@@ -284068,11 +283827,11 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
284068
283827
  kind: "phase_started",
284069
283828
  phase: "pr",
284070
283829
  label: "Preparing PR merge..."
284071
- }).pipe(tap(() => set$2(currentPhase, some("pr"))), flatMap(() => runMergePrStep(input.cwd, currentBranch, progress.emit))) : wantsPr ? yield* progress.emit({
283830
+ }).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runMergePrStep(input.cwd, currentBranch, progress.emit))) : wantsPr ? yield* progress.emit({
284072
283831
  kind: "phase_started",
284073
283832
  phase: "pr",
284074
283833
  label: "Preparing PR..."
284075
- }).pipe(tap(() => set$2(currentPhase, some("pr"))), flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch, progress.emit, input.threadId))) : { status: "skipped_not_requested" };
283834
+ }).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch, progress.emit, input.threadId))) : { status: "skipped_not_requested" };
284076
283835
  const toast = yield* buildCompletionToast(input.cwd, {
284077
283836
  action: input.action,
284078
283837
  branch: branchStep,
@@ -287344,7 +287103,8 @@ const makeRuntimeDrainReporter = gen(function* () {
287344
287103
  ...config,
287345
287104
  activeTurnCount: snapshot.activeTurnCount,
287346
287105
  activeSessionCount: snapshot.activeSessionCount,
287347
- ...forgeGitCommit !== void 0 ? { forgeGitCommit } : {}
287106
+ ...forgeGitCommit !== void 0 ? { forgeGitCommit } : {},
287107
+ forgeServerVersion: installedForgeServerVersion
287348
287108
  });
287349
287109
  });
287350
287110
  const applyControlPlaneState = (runtime) => drainState.applyControlPlaneState({
@@ -287928,7 +287688,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287928
287688
  createdAt: ""
287929
287689
  })
287930
287690
  });
287931
- const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$3(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), match(existingIds, {
287691
+ const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), match(existingIds, {
287932
287692
  onNone: () => new Set([messageId]),
287933
287693
  onSome: (ids) => {
287934
287694
  const nextIds = new Set(ids);
@@ -287942,7 +287702,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287942
287702
  const nextIds = new Set(ids);
287943
287703
  nextIds.delete(messageId);
287944
287704
  if (nextIds.size === 0) return invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId));
287945
- return set$3(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds);
287705
+ return set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds);
287946
287706
  }
287947
287707
  })));
287948
287708
  const getAssistantMessageIdsForTurn = (threadId, turnId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(map$3((existingIds) => getOrElse(existingIds, () => /* @__PURE__ */ new Set())));
@@ -287953,7 +287713,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287953
287713
  onSome: (text) => `${text}${delta}`
287954
287714
  });
287955
287715
  if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) {
287956
- yield* set$3(bufferedAssistantTextByMessageId, messageId, nextText);
287716
+ yield* set$4(bufferedAssistantTextByMessageId, messageId, nextText);
287957
287717
  return "";
287958
287718
  }
287959
287719
  yield* invalidate(bufferedAssistantTextByMessageId, messageId);
@@ -287963,7 +287723,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287963
287723
  const clearBufferedAssistantText = (messageId) => invalidate(bufferedAssistantTextByMessageId, messageId);
287964
287724
  const appendBufferedProposedPlan = (planId, delta, createdAt) => getOption(bufferedProposedPlanById, planId).pipe(flatMap((existingEntry) => {
287965
287725
  const existing = getOrUndefined(existingEntry);
287966
- return set$3(bufferedProposedPlanById, planId, {
287726
+ return set$4(bufferedProposedPlanById, planId, {
287967
287727
  text: `${existing?.text ?? ""}${delta}`,
287968
287728
  createdAt: existing?.createdAt && existing.createdAt.length > 0 ? existing.createdAt : createdAt
287969
287729
  });
@@ -288841,7 +288601,7 @@ const ProviderCommandReactorLive = effect(ProviderCommandReactor, gen(function*
288841
288601
  timeToLive: HANDLED_TURN_START_KEY_TTL,
288842
288602
  lookup: () => succeed(true)
288843
288603
  });
288844
- const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$3(handledTurnStartKeys, key, true).pipe(as$3(isSome(cached)))));
288604
+ const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$4(handledTurnStartKeys, key, true).pipe(as$3(isSome(cached)))));
288845
288605
  const threadModelSelections = /* @__PURE__ */ new Map();
288846
288606
  /**
288847
288607
  * The providerAccountId each LIVE provider session was actually started with.
@@ -292684,6 +292444,382 @@ const WorkspaceFileSystemLive = effect(WorkspaceFileSystem, gen(function* () {
292684
292444
  }) };
292685
292445
  }));
292686
292446
  //#endregion
292447
+ //#region src/workspace/Layers/WorkspaceBulkTransfer.ts
292448
+ const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
292449
+ const TRANSFER_LOCK_STRIPE_COUNT = 64;
292450
+ function transferError(input) {
292451
+ return new BulkTransferError({
292452
+ code: input.code,
292453
+ message: input.message,
292454
+ transferId: input.transferId ?? null,
292455
+ retryable: input.retryable ?? false,
292456
+ ...input.cause === void 0 ? {} : { cause: input.cause }
292457
+ });
292458
+ }
292459
+ function mapFailure(operation, transferId) {
292460
+ return (cause) => is$1(BulkTransferError)(cause) ? cause : transferError({
292461
+ code: "io_error",
292462
+ message: `Workspace bulk transfer failed during ${operation}.`,
292463
+ ...transferId === void 0 ? {} : { transferId },
292464
+ retryable: true,
292465
+ cause
292466
+ });
292467
+ }
292468
+ function transferLockStripe(transferId) {
292469
+ let hash = 2166136261;
292470
+ for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
292471
+ return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
292472
+ }
292473
+ const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
292474
+ const fileSystem = yield* FileSystem;
292475
+ const path = yield* Path$2;
292476
+ const sql = yield* SqlClient;
292477
+ const workspacePaths = yield* WorkspacePaths;
292478
+ const workspaceEntries = yield* WorkspaceEntries;
292479
+ const fault = options.fault ?? (() => void_$1);
292480
+ const transferLocks = yield* forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => make$43(1));
292481
+ const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
292482
+ const findTransfer = (transferId) => sql`
292483
+ SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
292484
+ relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
292485
+ total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
292486
+ next_offset AS "nextOffset", state, expires_at AS "expiresAt"
292487
+ FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
292488
+ `.pipe(map$3((rows) => rows[0] ?? null));
292489
+ const requireTransfer = (transferId) => findTransfer(transferId).pipe(flatMap((row) => row === null ? fail(transferError({
292490
+ code: "transfer_not_found",
292491
+ message: "Bulk transfer does not exist.",
292492
+ transferId
292493
+ })) : succeed(row)));
292494
+ const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(flatMap((row) => {
292495
+ if (row.state === "cancelled") return fail(transferError({
292496
+ code: "transfer_cancelled",
292497
+ message: "Bulk transfer was cancelled.",
292498
+ transferId
292499
+ }));
292500
+ if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return fail(transferError({
292501
+ code: "transfer_expired",
292502
+ message: "Bulk transfer expired.",
292503
+ transferId
292504
+ }));
292505
+ return succeed(row);
292506
+ }));
292507
+ const resolveTarget = fn$1("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
292508
+ const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
292509
+ const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
292510
+ workspaceRoot,
292511
+ relativePath: input.relativePath
292512
+ });
292513
+ const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
292514
+ const parentSegments = resolved.relativePath.split("/").slice(0, -1);
292515
+ let canonicalParent = canonicalRoot;
292516
+ for (const segment of parentSegments) {
292517
+ const candidate = path.join(canonicalParent, segment);
292518
+ const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
292519
+ if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
292520
+ code: "symlink_escape",
292521
+ message: "Workspace file path resolves outside the project root."
292522
+ });
292523
+ canonicalParent = canonicalCandidate;
292524
+ }
292525
+ const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
292526
+ const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(catch_(() => succeed(null)));
292527
+ if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
292528
+ code: "symlink_escape",
292529
+ message: "Workspace file target resolves outside the project root."
292530
+ });
292531
+ return {
292532
+ workspaceRoot: canonicalRoot,
292533
+ relativePath: resolved.relativePath,
292534
+ targetPath,
292535
+ canonicalParent
292536
+ };
292537
+ });
292538
+ const readFile = (input) => unwrap$1(gen(function* () {
292539
+ const target = yield* resolveTarget({
292540
+ ...input,
292541
+ createParent: false
292542
+ });
292543
+ const info = yield* fileSystem.stat(target.targetPath);
292544
+ const maxBytes = input.maxBytes ?? 16777216;
292545
+ if (Number(info.size) > maxBytes) return yield* transferError({
292546
+ code: "size_limit_exceeded",
292547
+ message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
292548
+ });
292549
+ const bytes = yield* fileSystem.readFile(target.targetPath);
292550
+ const transfer = yield* promise(() => chunkBulkTransfer(bytes));
292551
+ const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
292552
+ return fromIterable$1([
292553
+ {
292554
+ kind: "started",
292555
+ transferId,
292556
+ totalBytes: transfer.totalBytes,
292557
+ sha256: transfer.sha256
292558
+ },
292559
+ ...transfer.chunks.map((chunk) => ({
292560
+ ...chunk,
292561
+ transferId
292562
+ })),
292563
+ {
292564
+ kind: "completed",
292565
+ transferId,
292566
+ totalBytes: transfer.totalBytes,
292567
+ sha256: transfer.sha256
292568
+ }
292569
+ ]);
292570
+ }).pipe(mapError(mapFailure("read file"))));
292571
+ const resumeWrite = fn$1("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
292572
+ const target = yield* resolveTarget({
292573
+ ...input,
292574
+ createParent: true
292575
+ });
292576
+ if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
292577
+ code: "transfer_identity_conflict",
292578
+ message: "Bulk transfer ID is already bound to different content or a different path.",
292579
+ transferId: input.transferId
292580
+ });
292581
+ if (existing.state === "cancelled") return yield* transferError({
292582
+ code: "transfer_cancelled",
292583
+ message: "Bulk transfer was cancelled.",
292584
+ transferId: input.transferId
292585
+ });
292586
+ if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
292587
+ code: "transfer_expired",
292588
+ message: "Bulk transfer expired.",
292589
+ transferId: input.transferId
292590
+ });
292591
+ return {
292592
+ transferId: input.transferId,
292593
+ nextOffset: existing.nextOffset,
292594
+ state: existing.state === "completed" ? "completed" : "open"
292595
+ };
292596
+ });
292597
+ const createWrite = fn$1("WorkspaceBulkTransfer.createWrite")(function* (input) {
292598
+ const target = yield* resolveTarget({
292599
+ ...input,
292600
+ createParent: true
292601
+ }).pipe(mapError((cause) => is$1(WorkspacePathOutsideRootError)(cause) ? transferError({
292602
+ code: "path_outside_workspace",
292603
+ message: "Workspace file path must stay within the project root.",
292604
+ transferId: input.transferId,
292605
+ cause
292606
+ }) : cause));
292607
+ const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
292608
+ yield* scoped(fileSystem.open(tempPath, {
292609
+ flag: "wx",
292610
+ mode: 384
292611
+ }).pipe(flatMap((file) => file.sync), catch_(() => fileSystem.stat(tempPath).pipe(flatMap((stat) => Number(stat.size) === 0 ? void_$1 : fail(transferError({
292612
+ code: "transfer_identity_conflict",
292613
+ message: "Bulk transfer temporary path already contains data.",
292614
+ transferId: input.transferId
292615
+ })))))));
292616
+ const now = /* @__PURE__ */ new Date();
292617
+ yield* sql`
292618
+ INSERT INTO workspace_bulk_transfers (
292619
+ transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
292620
+ whole_sha256, next_offset, state, expires_at, created_at, updated_at
292621
+ ) VALUES (
292622
+ ${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
292623
+ ${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
292624
+ ${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
292625
+ ${now.toISOString()}
292626
+ )
292627
+ `;
292628
+ return {
292629
+ transferId: input.transferId,
292630
+ nextOffset: 0,
292631
+ state: "open"
292632
+ };
292633
+ });
292634
+ const beginWriteEffect = fn$1("WorkspaceBulkTransfer.beginWrite")(function* (input) {
292635
+ const existing = yield* findTransfer(input.transferId);
292636
+ return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
292637
+ });
292638
+ const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(mapError(mapFailure("begin write", input.transferId)));
292639
+ const acceptRetriedChunk = fn$1("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
292640
+ const accepted = (yield* sql`
292641
+ SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
292642
+ chunk_sha256 AS "chunkSha256"
292643
+ FROM workspace_bulk_transfer_chunks
292644
+ WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
292645
+ LIMIT 1
292646
+ `)[0];
292647
+ if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
292648
+ code: "integrity_mismatch",
292649
+ message: "Retried bulk transfer chunk differs from the accepted chunk.",
292650
+ transferId: input.transferId
292651
+ });
292652
+ return {
292653
+ transferId: input.transferId,
292654
+ nextOffset: row.nextOffset,
292655
+ duplicate: true
292656
+ };
292657
+ });
292658
+ const validateChunkRange = fn$1("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
292659
+ if (input.offset !== row.nextOffset) return yield* transferError({
292660
+ code: "offset_mismatch",
292661
+ message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
292662
+ transferId: input.transferId,
292663
+ retryable: true
292664
+ });
292665
+ const nextOffset = input.offset + byteLength;
292666
+ if (nextOffset > row.totalBytes) return yield* transferError({
292667
+ code: "size_limit_exceeded",
292668
+ message: "Bulk transfer chunk exceeds the declared total byte count.",
292669
+ transferId: input.transferId
292670
+ });
292671
+ return nextOffset;
292672
+ });
292673
+ const reconcileChunkBytes = fn$1("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
292674
+ const tempInfo = yield* fileSystem.stat(row.tempPath);
292675
+ if (Number(tempInfo.size) > row.nextOffset) {
292676
+ if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
292677
+ code: "integrity_mismatch",
292678
+ message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
292679
+ transferId: input.transferId
292680
+ });
292681
+ const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
292682
+ if ((yield* promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
292683
+ code: "integrity_mismatch",
292684
+ message: "Bulk transfer temporary bytes differ from the retried chunk.",
292685
+ transferId: input.transferId
292686
+ });
292687
+ return;
292688
+ }
292689
+ yield* fault("before-byte-write");
292690
+ yield* scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(flatMap((file) => file.seek(input.offset, "start").pipe(flatMap(() => file.writeAll(bytes)), flatMap(() => file.sync)))));
292691
+ yield* fault("after-byte-write-before-metadata");
292692
+ });
292693
+ const persistAcceptedChunk = fn$1("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
292694
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
292695
+ yield* sql.withTransaction(sql`
292696
+ INSERT INTO workspace_bulk_transfer_chunks (
292697
+ transfer_id, chunk_offset, chunk_bytes, chunk_sha256
292698
+ ) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
292699
+ ON CONFLICT DO NOTHING
292700
+ `.pipe(flatMap(() => sql`
292701
+ UPDATE workspace_bulk_transfers
292702
+ SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
292703
+ WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
292704
+ `)));
292705
+ yield* fault("after-metadata-advance");
292706
+ });
292707
+ const writeChunkEffect = fn$1("WorkspaceBulkTransfer.writeChunk")(function* (input) {
292708
+ const bytes = yield* promise(() => verifyBulkTransferChunk({
292709
+ ...input,
292710
+ kind: "chunk"
292711
+ }));
292712
+ const row = yield* requireOpenTransfer(input.transferId);
292713
+ if (row.state === "completed") return yield* transferError({
292714
+ code: "offset_mismatch",
292715
+ message: "Bulk transfer is already complete.",
292716
+ transferId: input.transferId
292717
+ });
292718
+ if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
292719
+ const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
292720
+ yield* reconcileChunkBytes(input, row, bytes, nextOffset);
292721
+ yield* persistAcceptedChunk(input, nextOffset);
292722
+ return {
292723
+ transferId: input.transferId,
292724
+ nextOffset,
292725
+ duplicate: false
292726
+ };
292727
+ });
292728
+ const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(mapError(mapFailure("write chunk", input.transferId)));
292729
+ const commitWriteEffect = fn$1("WorkspaceBulkTransfer.commitWrite")(function* (input) {
292730
+ const row = yield* requireOpenTransfer(input.transferId);
292731
+ if (row.state === "completed") return {
292732
+ transferId: row.transferId,
292733
+ relativePath: row.relativePath,
292734
+ totalBytes: row.totalBytes,
292735
+ sha256: row.wholeSha256
292736
+ };
292737
+ if (row.nextOffset !== row.totalBytes) return yield* transferError({
292738
+ code: "offset_mismatch",
292739
+ message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
292740
+ transferId: row.transferId,
292741
+ retryable: true
292742
+ });
292743
+ if ((yield* resolveTarget({
292744
+ cwd: row.workspaceRoot,
292745
+ relativePath: row.relativePath,
292746
+ createParent: false
292747
+ })).targetPath !== row.targetPath) return yield* transferError({
292748
+ code: "symlink_escape",
292749
+ message: "Workspace file destination changed during transfer.",
292750
+ transferId: row.transferId
292751
+ });
292752
+ const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(catch_(() => succeed(null)));
292753
+ const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
292754
+ const candidate = yield* fileSystem.readFile(candidatePath);
292755
+ if (candidate.byteLength !== row.totalBytes || (yield* promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
292756
+ code: "integrity_mismatch",
292757
+ message: "Bulk transfer payload does not match its declared size and digest.",
292758
+ transferId: row.transferId
292759
+ });
292760
+ if (tempInfo !== null) {
292761
+ yield* fileSystem.rename(row.tempPath, row.targetPath);
292762
+ yield* fault("after-atomic-rename");
292763
+ }
292764
+ yield* sql`
292765
+ UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
292766
+ WHERE transfer_id = ${row.transferId}
292767
+ `;
292768
+ yield* workspaceEntries.invalidate(row.workspaceRoot);
292769
+ return {
292770
+ transferId: row.transferId,
292771
+ relativePath: row.relativePath,
292772
+ totalBytes: row.totalBytes,
292773
+ sha256: row.wholeSha256
292774
+ };
292775
+ });
292776
+ const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(mapError(mapFailure("commit write", input.transferId)));
292777
+ const cancelWriteEffect = fn$1("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
292778
+ const row = yield* requireTransfer(input.transferId);
292779
+ if (row.state !== "completed") {
292780
+ yield* fileSystem.remove(row.tempPath, { force: true });
292781
+ yield* sql`
292782
+ UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
292783
+ WHERE transfer_id = ${input.transferId}
292784
+ `;
292785
+ }
292786
+ return {
292787
+ transferId: input.transferId,
292788
+ state: row.state === "completed" ? "completed" : "cancelled"
292789
+ };
292790
+ });
292791
+ const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(mapError(mapFailure("cancel write", input.transferId)));
292792
+ const cleanupExpiredEffect = fn$1("WorkspaceBulkTransfer.cleanupExpired")(function* () {
292793
+ const now = (/* @__PURE__ */ new Date()).toISOString();
292794
+ const rows = yield* sql`
292795
+ SELECT transfer_id AS "transferId", temp_path AS "tempPath"
292796
+ FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
292797
+ `;
292798
+ let expiredCount = 0;
292799
+ for (const row of rows) if (yield* withTransferLock(row.transferId, gen(function* () {
292800
+ const current = yield* findTransfer(row.transferId);
292801
+ if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
292802
+ yield* fileSystem.remove(current.tempPath, { force: true });
292803
+ yield* sql`
292804
+ UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
292805
+ WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
292806
+ `;
292807
+ return true;
292808
+ }))) expiredCount += 1;
292809
+ return expiredCount;
292810
+ });
292811
+ const cleanupExpired = () => cleanupExpiredEffect().pipe(mapError(mapFailure("expiry cleanup")));
292812
+ return {
292813
+ readFile,
292814
+ beginWrite,
292815
+ writeChunk,
292816
+ commitWrite,
292817
+ cancelWrite,
292818
+ cleanupExpired
292819
+ };
292820
+ });
292821
+ const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
292822
+ //#endregion
292687
292823
  //#region src/workspace/Layers/WorkspacePaths.ts
292688
292824
  function toPosixRelativePath(input) {
292689
292825
  return input.replaceAll("\\", "/");
@@ -294277,7 +294413,7 @@ function resolveBuildCommitFromEnv(env) {
294277
294413
  * environment descriptor down instead of reporting an honest "unknown".
294278
294414
  */
294279
294415
  function readBakedBuildCommit() {
294280
- return "8cafbb87b6204b98e93c1c69253c1c590af5f8a1";
294416
+ return "7695a8221bc664caedd33aa411ac7bcdbb0a3594";
294281
294417
  }
294282
294418
  async function resolveServerBuildCommit(input) {
294283
294419
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -294469,7 +294605,7 @@ const ServerEnvironmentLive = effect(ServerEnvironment, fn$1("makeServerEnvironm
294469
294605
  os,
294470
294606
  arch: platformArch()
294471
294607
  },
294472
- serverVersion: version$1,
294608
+ serverVersion: installedForgeServerVersion,
294473
294609
  ...buildCommit !== void 0 ? { buildCommit } : {},
294474
294610
  ...deviceRuntimeId !== void 0 ? { deviceRuntimeId } : {},
294475
294611
  capabilities: {
@@ -294485,7 +294621,7 @@ const ServerEnvironmentLive = effect(ServerEnvironment, fn$1("makeServerEnvironm
294485
294621
  updateDisplayName: (displayName) => gen(function* () {
294486
294622
  const normalized = normalizeDisplayName(displayName);
294487
294623
  yield* writeMetadata({ displayName: normalized }).pipe(orDie);
294488
- yield* set$2(displayNameOverrideRef, normalized);
294624
+ yield* set$3(displayNameOverrideRef, normalized);
294489
294625
  const descriptor = yield* buildDescriptor;
294490
294626
  yield* publish(changesPubSub, descriptor);
294491
294627
  return descriptor;
@@ -301218,6 +301354,54 @@ const OrchestrationInfrastructureLayerLive = mergeAll(effect(ProjectionSnapshotQ
301218
301354
  AND deleted_at IS NULL
301219
301355
  AND archived_at IS NULL
301220
301356
  ORDER BY created_at ASC, thread_id ASC
301357
+ `
301358
+ });
301359
+ /**
301360
+ * The archive's own rows.
301361
+ *
301362
+ * `listThreadRows` deliberately carries archived threads too, because the
301363
+ * active list needs `archivedAt` to know what to hide and the web archive
301364
+ * panel filters that same snapshot client side. This query is the opposite
301365
+ * selection, for the clients that ask for the archive directly.
301366
+ */
301367
+ const listArchivedThreadRows = findAll({
301368
+ Request: Void,
301369
+ Result: ProjectionThreadDbRowSchema,
301370
+ execute: () => sql`
301371
+ SELECT
301372
+ thread_id AS "threadId",
301373
+ project_id AS "projectId",
301374
+ title,
301375
+ model_selection_json AS "modelSelection",
301376
+ runtime_mode AS "runtimeMode",
301377
+ interaction_mode AS "interactionMode",
301378
+ branch,
301379
+ worktree_path AS "worktreePath",
301380
+ latest_turn_id AS "latestTurnId",
301381
+ created_at AS "createdAt",
301382
+ updated_at AS "updatedAt",
301383
+ archived_at AS "archivedAt",
301384
+ settled_at AS "settledAt",
301385
+ settled_override AS "settledOverride",
301386
+ latest_user_message_at AS "latestUserMessageAt",
301387
+ pending_approval_count AS "pendingApprovalCount",
301388
+ pending_user_input_count AS "pendingUserInputCount",
301389
+ has_actionable_proposed_plan AS "hasActionableProposedPlan",
301390
+ deleted_at AS "deletedAt",
301391
+ kind,
301392
+ parent_thread_id AS "parentThreadId",
301393
+ anchor_kind AS "anchorKind",
301394
+ anchor_cmmd_id AS "anchorCmmdId",
301395
+ linked_pull_request_json AS "linkedPullRequest",
301396
+ status,
301397
+ worker_role AS "workerRole",
301398
+ goal,
301399
+ pending_auto_create_prompt_json AS "pendingAutoCreatePrompt",
301400
+ COALESCE(rate_limit_status_json, '[]') AS "rateLimitStatus"
301401
+ FROM projection_threads
301402
+ WHERE archived_at IS NOT NULL
301403
+ AND deleted_at IS NULL
301404
+ ORDER BY archived_at DESC, thread_id ASC
301221
301405
  `
301222
301406
  });
301223
301407
  const listThreadMessageRows = findAll({
@@ -301872,13 +302056,22 @@ const OrchestrationInfrastructureLayerLive = mergeAll(effect(ProjectionSnapshotQ
301872
302056
  if (isPersistenceError(error)) return error;
301873
302057
  return toPersistenceSqlError("ProjectionSnapshotQuery.getSnapshot:query")(error);
301874
302058
  }));
301875
- const getShellSnapshot = () => {
302059
+ /**
302060
+ * One snapshot assembly, two selections of threads.
302061
+ *
302062
+ * The active list and the archive differ only in which thread rows they
302063
+ * read, so they share this body. Keeping them on one mapper is the point:
302064
+ * FOR-36394 was a projected column that one path carried and another
302065
+ * dropped, and a second hand-copied assembly here would invite the same
302066
+ * failure for anyone who only remembered to update the active list.
302067
+ */
302068
+ const readShellSnapshot = (operation, listThreads) => {
301876
302069
  return sql.withTransaction(all([
301877
- listProjectRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.getShellSnapshot:listProjects:query", "ProjectionSnapshotQuery.getShellSnapshot:listProjects:decodeRows"))),
301878
- listThreadRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.getShellSnapshot:listThreads:query", "ProjectionSnapshotQuery.getShellSnapshot:listThreads:decodeRows"))),
301879
- listThreadSessionRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.getShellSnapshot:listThreadSessions:query", "ProjectionSnapshotQuery.getShellSnapshot:listThreadSessions:decodeRows"))),
301880
- listLatestTurnRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.getShellSnapshot:listLatestTurns:query", "ProjectionSnapshotQuery.getShellSnapshot:listLatestTurns:decodeRows"))),
301881
- listProjectionStateRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.getShellSnapshot:listProjectionState:query", "ProjectionSnapshotQuery.getShellSnapshot:listProjectionState:decodeRows")))
302070
+ listProjectRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError(`ProjectionSnapshotQuery.${operation}:listProjects:query`, `ProjectionSnapshotQuery.${operation}:listProjects:decodeRows`))),
302071
+ listThreads(void 0).pipe(mapError(toPersistenceSqlOrDecodeError(`ProjectionSnapshotQuery.${operation}:listThreads:query`, `ProjectionSnapshotQuery.${operation}:listThreads:decodeRows`))),
302072
+ listThreadSessionRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError(`ProjectionSnapshotQuery.${operation}:listThreadSessions:query`, `ProjectionSnapshotQuery.${operation}:listThreadSessions:decodeRows`))),
302073
+ listLatestTurnRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError(`ProjectionSnapshotQuery.${operation}:listLatestTurns:query`, `ProjectionSnapshotQuery.${operation}:listLatestTurns:decodeRows`))),
302074
+ listProjectionStateRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError(`ProjectionSnapshotQuery.${operation}:listProjectionState:query`, `ProjectionSnapshotQuery.${operation}:listProjectionState:decodeRows`)))
301882
302075
  ])).pipe(flatMap(([projectRows, threadRows, sessionRows, latestTurnRows, stateRows]) => gen(function* () {
301883
302076
  let updatedAt = null;
301884
302077
  for (const row of projectRows) updatedAt = maxIso(updatedAt, row.updatedAt);
@@ -301899,12 +302092,14 @@ const OrchestrationInfrastructureLayerLive = mergeAll(effect(ProjectionSnapshotQ
301899
302092
  projects: projectRows.filter((row) => row.deletedAt === null).map((row) => mapProjectShellRow(row, repositoryIdentities.get(row.projectId) ?? null, projectAvailabilities.get(row.projectId) ?? "unknown")),
301900
302093
  threads: threadShells.filter((thread) => projectIds.has(thread.projectId)),
301901
302094
  updatedAt: updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString()
301902
- }).pipe(mapError(toPersistenceDecodeError("ProjectionSnapshotQuery.getShellSnapshot:decodeShellSnapshot")));
302095
+ }).pipe(mapError(toPersistenceDecodeError(`ProjectionSnapshotQuery.${operation}:decodeShellSnapshot`)));
301903
302096
  })), mapError((error) => {
301904
302097
  if (isPersistenceError(error)) return error;
301905
- return toPersistenceSqlError("ProjectionSnapshotQuery.getShellSnapshot:query")(error);
302098
+ return toPersistenceSqlError(`ProjectionSnapshotQuery.${operation}:query`)(error);
301906
302099
  }));
301907
302100
  };
302101
+ const getShellSnapshot = () => readShellSnapshot("getShellSnapshot", listThreadRows);
302102
+ const getArchivedShellSnapshot = () => readShellSnapshot("getArchivedShellSnapshot", listArchivedThreadRows);
301908
302103
  const listActiveWorkerShells = () => sql.withTransaction(all([
301909
302104
  listActiveWorkerThreadRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.listActiveWorkerShells:listThreads:query", "ProjectionSnapshotQuery.listActiveWorkerShells:listThreads:decodeRows"))),
301910
302105
  listActiveWorkerSessionRows(void 0).pipe(mapError(toPersistenceSqlOrDecodeError("ProjectionSnapshotQuery.listActiveWorkerShells:listThreadSessions:query", "ProjectionSnapshotQuery.listActiveWorkerShells:listThreadSessions:decodeRows"))),
@@ -302071,6 +302266,7 @@ const OrchestrationInfrastructureLayerLive = mergeAll(effect(ProjectionSnapshotQ
302071
302266
  return {
302072
302267
  getSnapshot,
302073
302268
  getShellSnapshot,
302269
+ getArchivedShellSnapshot,
302074
302270
  listActiveWorkerShells,
302075
302271
  listStaleRunningSessionShells,
302076
302272
  getCounts,
@@ -306060,7 +306256,6 @@ const cmmdUpdateProjectAgentMemberAvatarRouteLayer = registerProxyRoute({
306060
306256
  });
306061
306257
  //#endregion
306062
306258
  //#region src/command/runtimeProviderAuthorityUpgradeModel.ts
306063
- const GIT_COMMIT = /^[0-9a-f]{40}$/u;
306064
306259
  const AUTHORITY_UPGRADE_OPERATION_KEYS = new Set([
306065
306260
  "operationId",
306066
306261
  "runtimeId",
@@ -306070,7 +306265,7 @@ const AUTHORITY_UPGRADE_OPERATION_KEYS = new Set([
306070
306265
  "attemptCount",
306071
306266
  "expectedCredentialEpoch",
306072
306267
  "resultingCredentialEpoch",
306073
- "targetForgeGitCommit",
306268
+ "targetForgeServerVersion",
306074
306269
  "retryAt",
306075
306270
  "completedAt",
306076
306271
  "failureCode"
@@ -306138,7 +306333,7 @@ function parseRuntimeAuthorityUpgradeOperation(value, expected) {
306138
306333
  Number.isSafeInteger(operation.attemptCount) && operation.attemptCount >= 0,
306139
306334
  operation.expectedCredentialEpoch === 0,
306140
306335
  operation.resultingCredentialEpoch === 1,
306141
- typeof operation.targetForgeGitCommit === "string" && GIT_COMMIT.test(operation.targetForgeGitCommit),
306336
+ isExactForgeServerVersion(operation.targetForgeServerVersion),
306142
306337
  isNullableDateTime(operation.retryAt),
306143
306338
  isNullableDateTime(operation.completedAt),
306144
306339
  operation.failureCode === null || typeof operation.failureCode === "string",
@@ -306155,6 +306350,7 @@ function isLegacyRuntimeConnection(value, expectedRuntimeId) {
306155
306350
  runtime.brokerAuthorityAttestedImageRef === null,
306156
306351
  runtime.brokerAuthorityAttestedImageVersion === null,
306157
306352
  runtime.brokerAuthorityAttestedForgeGitCommit === null,
306353
+ runtime.brokerAuthorityAttestedForgeServerVersion === null,
306158
306354
  runtime.brokerAuthorityAttestedMachineId === null,
306159
306355
  runtime.brokerAuthorityAttestedCredentialEpoch === null,
306160
306356
  runtime.brokerAuthorityAttestedAt === null
@@ -306318,13 +306514,13 @@ function hasTrustedRuntimeRoute(url, trustedBase) {
306318
306514
  const relativePath = url.pathname.slice(trustedPrefix.length);
306319
306515
  return url.origin === trustedBase.origin && url.username === "" && url.password === "" && url.port === "" && url.search === "" && url.hash === "" && url.pathname.startsWith(`${trustedPrefix}/r/`) && RUNTIME_ROUTE_PATH.test(relativePath);
306320
306516
  }
306321
- function hasExactRuntimeConnectionFields(data, runtime, expectedRuntimeId, expectedAttestedForgeGitCommit) {
306322
- return runtime.runtimeId === expectedRuntimeId && runtime.runtimeCapabilityMode === "exact_machine_v2" && isNonEmptyString(runtime.brokerAuthorityAttestedMachineId) && isSafeCredentialEpoch(runtime.brokerAuthorityAttestedCredentialEpoch) && (expectedAttestedForgeGitCommit === void 0 || runtime.brokerAuthorityAttestedForgeGitCommit === expectedAttestedForgeGitCommit) && isNonEmptyString(data.httpBaseUrl) && isNonEmptyString(data.bearerToken);
306517
+ function hasExactRuntimeConnectionFields(data, runtime, expectedRuntimeId, expectedAttestedForgeServerVersion) {
306518
+ return runtime.runtimeId === expectedRuntimeId && runtime.runtimeCapabilityMode === "exact_machine_v2" && isNonEmptyString(runtime.brokerAuthorityAttestedMachineId) && isSafeCredentialEpoch(runtime.brokerAuthorityAttestedCredentialEpoch) && (expectedAttestedForgeServerVersion === void 0 || runtime.brokerAuthorityAttestedForgeServerVersion === expectedAttestedForgeServerVersion) && isNonEmptyString(data.httpBaseUrl) && isNonEmptyString(data.bearerToken);
306323
306519
  }
306324
- function parseRuntimeConnection(value, expectedRuntimeId, trustedRuntimeRouterBaseUrl, expectedAttestedForgeGitCommit) {
306520
+ function parseRuntimeConnection(value, expectedRuntimeId, trustedRuntimeRouterBaseUrl, expectedAttestedForgeServerVersion) {
306325
306521
  const data = asRecord$1(unwrapData(value));
306326
306522
  const runtime = asRecord$1(data?.runtime);
306327
- if (!data || !runtime || !hasExactRuntimeConnectionFields(data, runtime, expectedRuntimeId, expectedAttestedForgeGitCommit)) return null;
306523
+ if (!data || !runtime || !hasExactRuntimeConnectionFields(data, runtime, expectedRuntimeId, expectedAttestedForgeServerVersion)) return null;
306328
306524
  const trustedBase = parseTrustedRuntimeRouterBaseUrl(trustedRuntimeRouterBaseUrl);
306329
306525
  if (!trustedBase) return null;
306330
306526
  let httpBaseUrl;
@@ -306504,7 +306700,7 @@ function classifyInitialConnection(outcome, runtimeId, trustedRuntimeRouterBaseU
306504
306700
  }
306505
306701
  };
306506
306702
  }
306507
- function reconnectExactRuntime(input, targetForgeGitCommit) {
306703
+ function reconnectExactRuntime(input, targetForgeServerVersion) {
306508
306704
  return gen(function* () {
306509
306705
  const connectPath = `/api/forge/context/runtimes/${encodeURIComponent(input.runtimeId)}/connect`;
306510
306706
  const reconnected = yield* postRuntimeProviderCmmd({
@@ -306521,7 +306717,7 @@ function reconnectExactRuntime(input, targetForgeGitCommit) {
306521
306717
  error: reconnected.error,
306522
306718
  stage: "reconnect"
306523
306719
  };
306524
- const exact = parseRuntimeConnection(reconnected.value, input.runtimeId, input.trustedRuntimeRouterBaseUrl, targetForgeGitCommit);
306720
+ const exact = parseRuntimeConnection(reconnected.value, input.runtimeId, input.trustedRuntimeRouterBaseUrl, targetForgeServerVersion);
306525
306721
  return exact ? {
306526
306722
  ok: true,
306527
306723
  value: exact
@@ -306532,7 +306728,7 @@ function reconnectExactRuntime(input, targetForgeGitCommit) {
306532
306728
  });
306533
306729
  }
306534
306730
  function resolveOperation(input, operation) {
306535
- return operation.status === "completed" ? reconnectExactRuntime(input, operation.targetForgeGitCommit) : succeed(pendingOperation(operation));
306731
+ return operation.status === "completed" ? reconnectExactRuntime(input, operation.targetForgeServerVersion) : succeed(pendingOperation(operation));
306536
306732
  }
306537
306733
  function beginExactRuntimeConnection(input) {
306538
306734
  return gen(function* () {
@@ -311400,7 +311596,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
311400
311596
  contextHash: preparedHandoff.authority.contextHash,
311401
311597
  contextSnapshot: snapshot.run.contextSnapshot
311402
311598
  }).pipe(mapError((cause) => coordinatorError("registerHandoff", "PIPELINE_CONTEXT_ARTIFACT_FAILED", "The immutable CMMD run context could not be materialized for the provider.", cause)));
311403
- const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$2(acceptingHeartbeatRef, false).pipe(andThen(get$6(preparedAuthorityRef))));
311599
+ const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$3(acceptingHeartbeatRef, false).pipe(andThen(get$6(preparedAuthorityRef))));
311404
311600
  const persistedAuthority = yield* persistAuthoritySecret({
311405
311601
  runId: input.runId,
311406
311602
  organizationId: latestPreparedAuthority.authority.organizationId,
@@ -311487,7 +311683,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
311487
311683
  });
311488
311684
  yield* heartbeatPublicationSemaphore.withPermits(1)(gen(function* () {
311489
311685
  if (!(yield* get$6(acceptingHeartbeatRef))) return;
311490
- yield* set$2(preparedAuthorityRef, {
311686
+ yield* set$3(preparedAuthorityRef, {
311491
311687
  authority: heartbeat.refreshedAuthority,
311492
311688
  preparation
311493
311689
  });
@@ -316261,7 +316457,7 @@ const WorkspaceReadinessStateLive = effect(WorkspaceReadinessState, gen(function
316261
316457
  const slot = yield* getOrCreateSlot(userId);
316262
316458
  const previous = yield* get$6(slot.snapshotRef);
316263
316459
  if (previous !== null && event.ts < previous.ts) return;
316264
- yield* set$2(slot.snapshotRef, event);
316460
+ yield* set$3(slot.snapshotRef, event);
316265
316461
  yield* publish(slot.pubsub, event);
316266
316462
  if (event.phase === "app-ready") yield* schedulePrune(userId, slot);
316267
316463
  });