@cmmd-center/forge 0.10.16 → 0.11.1

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.
package/dist/bin.mjs CHANGED
@@ -5,9 +5,9 @@ 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";
@@ -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.10.6";
16523
+ const FORGE_RELEASE_VERSION = "0.11.0";
16524
16524
  //#endregion
16525
16525
  //#region ../../packages/shared/src/hostProcess.ts
16526
16526
  /**
@@ -26406,6 +26406,29 @@ function encodeCmmdRuntimeThreadLifecycleMetadata(lifecycle) {
26406
26406
  } };
26407
26407
  }
26408
26408
  //#endregion
26409
+ //#region ../../packages/contracts/src/cmmdRuntimeThreadWorkspace.ts
26410
+ function encodeCmmdRuntimeThreadWorkspaceMetadata(workspace) {
26411
+ return { forgeWorkspace: {
26412
+ schemaVersion: 1,
26413
+ worktreePath: workspace.worktreePath
26414
+ } };
26415
+ }
26416
+ //#endregion
26417
+ //#region ../../packages/contracts/src/cmmdRuntimeToolActivity.ts
26418
+ function isRecord$22(value) {
26419
+ return value !== null && typeof value === "object" && !Array.isArray(value);
26420
+ }
26421
+ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
26422
+ return {
26423
+ ...isRecord$22(payload) ? payload : { sourcePayload: payload },
26424
+ forgeToolActivity: {
26425
+ schemaVersion: 1,
26426
+ source: "native",
26427
+ kind
26428
+ }
26429
+ };
26430
+ }
26431
+ //#endregion
26409
26432
  //#region ../../packages/contracts/src/cmmdProjectPatch.ts
26410
26433
  const CMMD_PROJECT_PATCH_FIELDS = {
26411
26434
  title: "requiredText",
@@ -65495,7 +65518,7 @@ function normalizeNumberish(value) {
65495
65518
  }
65496
65519
  //#endregion
65497
65520
  //#region package.json
65498
- var version$1 = "0.10.16";
65521
+ var version$1 = "0.11.1";
65499
65522
  //#endregion
65500
65523
  //#region src/sentry.ts
65501
65524
  const SERVER_APP_NAME = "forge-server";
@@ -69266,6 +69289,17 @@ const browserApiCorsLayer = middleware(cors({
69266
69289
  credentials: true,
69267
69290
  maxAge: 600
69268
69291
  }), { global: true });
69292
+ /**
69293
+ * An /api path that reaches the static catch-all is not implemented by this
69294
+ * server: every real API route is registered ahead of it. Serving the web app's
69295
+ * index.html for those made API clients decode an HTML document as JSON, which
69296
+ * is how the phone's shell snapshot fetch failed with a JSON parse error on
69297
+ * every connect (FOR-36611). Callers can act on a 404; they cannot act on a
69298
+ * document that claims to be their payload.
69299
+ */
69300
+ function isApiRequestPath(pathname) {
69301
+ return pathname === "/api" || pathname.startsWith("/api/");
69302
+ }
69269
69303
  function resolveDevRedirectUrl(devUrl, requestUrl) {
69270
69304
  const redirectUrl = new URL(devUrl.toString());
69271
69305
  redirectUrl.pathname = requestUrl.pathname;
@@ -69387,6 +69421,10 @@ const projectFaviconRouteLayer = add$1("GET", "/api/project-favicon", gen(functi
69387
69421
  const staticAndDevRouteLayer = add$1("GET", "*", gen(function* () {
69388
69422
  const url = toURL(yield* HttpServerRequest);
69389
69423
  if (isNone(url)) return text$2("Bad Request", { status: 400 });
69424
+ if (isApiRequestPath(url.value.pathname)) return jsonUnsafe({
69425
+ error: "not_found",
69426
+ message: `This server does not serve ${url.value.pathname}.`
69427
+ }, { status: 404 });
69390
69428
  const config = yield* ServerConfig;
69391
69429
  if (config.devUrl && isLoopbackHostname(url.value.hostname)) return redirect(resolveDevRedirectUrl(config.devUrl, url.value), { status: 302 });
69392
69430
  const staticDir = config.staticDir ?? (config.devUrl ? yield* resolveStaticDir() : void 0);
@@ -82637,7 +82675,7 @@ var ServerSettingsService = class ServerSettingsService extends Service$1()("for
82637
82675
  start: void_$1,
82638
82676
  ready: void_$1,
82639
82677
  getSettings: get$6(currentSettingsRef),
82640
- updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$2(currentSettingsRef, nextSettings))),
82678
+ updateSettings: (patch) => get$6(currentSettingsRef).pipe(map$3((currentSettings) => deepMerge$1(currentSettings, patch)), tap((nextSettings) => set$3(currentSettingsRef, nextSettings))),
82641
82679
  streamChanges: empty$5
82642
82680
  };
82643
82681
  }));
@@ -82777,7 +82815,7 @@ const ServerSettingsLive = effect(ServerSettingsService, gen(function* () {
82777
82815
  cause
82778
82816
  })));
82779
82817
  yield* writeSettingsAtomically(next);
82780
- yield* set$3(settingsCache, cacheKey, next);
82818
+ yield* set$4(settingsCache, cacheKey, next);
82781
82819
  yield* emitChange(next);
82782
82820
  return resolveTextGenerationProvider(next);
82783
82821
  })),
@@ -91707,7 +91745,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91707
91745
  return {
91708
91746
  start: gen(function* () {
91709
91747
  if (yield* get$6(startedRef)) return yield* _await(startedDeferred);
91710
- yield* set$2(startedRef, true);
91748
+ yield* set$3(startedRef, true);
91711
91749
  const startupExit = yield* exit(gen(function* () {
91712
91750
  yield* startWatcher;
91713
91751
  yield* syncDefaultKeybindingsOnStartup;
@@ -91736,7 +91774,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91736
91774
  });
91737
91775
  yield* writeConfigAtomically(cappedConfig);
91738
91776
  const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(cappedConfig));
91739
- yield* set$3(resolvedConfigCache, resolvedConfigCacheKey, {
91777
+ yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
91740
91778
  keybindings: nextResolved,
91741
91779
  issues: []
91742
91780
  });
@@ -91750,7 +91788,7 @@ const KeybindingsLive = effect(Keybindings, gen(function* () {
91750
91788
  const nextConfig = (yield* loadWritableCustomKeybindingsConfig()).filter((entry) => entry.command !== command);
91751
91789
  yield* writeConfigAtomically(nextConfig);
91752
91790
  const nextResolved = mergeWithDefaultKeybindings(compileResolvedKeybindingsConfig(nextConfig));
91753
- yield* set$3(resolvedConfigCache, resolvedConfigCacheKey, {
91791
+ yield* set$4(resolvedConfigCache, resolvedConfigCacheKey, {
91754
91792
  keybindings: nextResolved,
91755
91793
  issues: []
91756
91794
  });
@@ -92650,7 +92688,7 @@ var AnalyticsService = class AnalyticsService extends Service$1()("forge/telemet
92650
92688
  var RuntimeReplication = class extends Service$1()("forge/runtimeReplication/Services/RuntimeReplication") {};
92651
92689
  //#endregion
92652
92690
  //#region src/runtimeDrain/forgeGitCommit.ts
92653
- const execFileAsync$6 = promisify$1(execFile);
92691
+ const execFileAsync$7 = promisify$1(execFile);
92654
92692
  const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
92655
92693
  /**
92656
92694
  * Resolve the commit this forge checkout actually booted from, so the
@@ -92674,7 +92712,7 @@ const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
92674
92712
  * it yet.
92675
92713
  */
92676
92714
  let cached$1 = null;
92677
- async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$6(command, [...args], { timeout: 5e3 })) {
92715
+ async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$7(command, [...args], { timeout: 5e3 })) {
92678
92716
  if (cached$1 !== null) return cached$1;
92679
92717
  try {
92680
92718
  const { stdout } = await execImpl("git", ["rev-parse", "HEAD"]);
@@ -94606,7 +94644,7 @@ function mightCarryUsage(line, provider) {
94606
94644
  * a line at a time, so the usual cause is a tail the scan opened mid-write,
94607
94645
  * and a count that keeps growing is a file the scan can no longer read.
94608
94646
  */
94609
- function parseJsonObject$1(line, diagnostics) {
94647
+ function parseJsonObject$2(line, diagnostics) {
94610
94648
  let parsed;
94611
94649
  try {
94612
94650
  parsed = JSON.parse(line);
@@ -94636,7 +94674,7 @@ function claudeDedupeKey(messageId, requestId) {
94636
94674
  return `${messageId ?? ""}:${requestId ?? ""}`;
94637
94675
  }
94638
94676
  function parseClaudeLine(line, diagnostics) {
94639
- const record = parseJsonObject$1(line, diagnostics);
94677
+ const record = parseJsonObject$2(line, diagnostics);
94640
94678
  if (record === null) return null;
94641
94679
  if (record["type"] !== "assistant") return null;
94642
94680
  const messageRecord = objectAt(record, "message");
@@ -94783,7 +94821,7 @@ function parseCodexTokenCount(record, payloadRecord, state) {
94783
94821
  };
94784
94822
  }
94785
94823
  function parseCodexLine(line, state, diagnostics) {
94786
- const record = parseJsonObject$1(line, diagnostics);
94824
+ const record = parseJsonObject$2(line, diagnostics);
94787
94825
  if (record === null) return null;
94788
94826
  const payloadRecord = objectAt(record, "payload");
94789
94827
  if (payloadRecord === null) return null;
@@ -95620,10 +95658,13 @@ function serializeRuntimeInventory(snapshot, providerInventory) {
95620
95658
  workerRole: thread.workerRole,
95621
95659
  anchorKind: thread.anchorKind,
95622
95660
  anchorCmmdId: thread.anchorCmmdId,
95623
- metadata: encodeCmmdRuntimeThreadLifecycleMetadata({
95624
- settledAt: thread.settledAt ?? null,
95625
- settledOverride: thread.settledOverride ?? null
95626
- })
95661
+ metadata: {
95662
+ ...encodeCmmdRuntimeThreadLifecycleMetadata({
95663
+ settledAt: thread.settledAt ?? null,
95664
+ settledOverride: thread.settledOverride ?? null
95665
+ }),
95666
+ ...encodeCmmdRuntimeThreadWorkspaceMetadata({ worktreePath: thread.worktreePath })
95667
+ }
95627
95668
  })),
95628
95669
  metadata: {
95629
95670
  source: "forge-server-projection",
@@ -95690,7 +95731,7 @@ function serializeNativeToolActivity(activity) {
95690
95731
  kind: "tool_call",
95691
95732
  summary,
95692
95733
  status: activityStatus(activity),
95693
- ...activity.payload === void 0 ? {} : { payload: activity.payload }
95734
+ payload: encodeCmmdRuntimeToolActivityPayload(activity.kind, activity.payload)
95694
95735
  }
95695
95736
  };
95696
95737
  }
@@ -97852,8 +97893,8 @@ const runUsageScanWarmup = (options) => gen(function* () {
97852
97893
  return yield* forever$1(gen(function* () {
97853
97894
  if (yield* get$6(scanning)) yield* logInfo("usage scan warm-up skipped; the previous scan is still running");
97854
97895
  else {
97855
- yield* set$2(scanning, true);
97856
- yield* forkChild(scanOnce(options, now).pipe(ensuring(set$2(scanning, false))));
97896
+ yield* set$3(scanning, true);
97897
+ yield* forkChild(scanOnce(options, now).pipe(ensuring(set$3(scanning, false))));
97857
97898
  }
97858
97899
  yield* sleep(intervalMs);
97859
97900
  }));
@@ -97871,11 +97912,11 @@ const makeCommandGate = gen(function* () {
97871
97912
  return {
97872
97913
  awaitCommandReady: _await(commandReady),
97873
97914
  signalCommandReady: gen(function* () {
97874
- yield* set$2(commandReadinessState, "ready");
97915
+ yield* set$3(commandReadinessState, "ready");
97875
97916
  yield* succeed$3(commandReady, void 0).pipe(orDie);
97876
97917
  }),
97877
97918
  failCommandReady: (error) => gen(function* () {
97878
- yield* set$2(commandReadinessState, error);
97919
+ yield* set$3(commandReadinessState, error);
97879
97920
  yield* fail$3(commandReady, error).pipe(orDie);
97880
97921
  }),
97881
97922
  enqueueCommand: (effect) => gen(function* () {
@@ -98256,413 +98297,23 @@ function browseDirectory(input) {
98256
98297
  });
98257
98298
  }
98258
98299
  //#endregion
98259
- //#region src/workspace/Services/WorkspaceEntries.ts
98300
+ //#region src/pathWithinRoot.ts
98260
98301
  /**
98261
- * WorkspaceEntries - Effect service contract for cached workspace entry search.
98302
+ * True when `candidate` is `root` itself or lies below it, judged on the path
98303
+ * strings alone. The path from `root` to `candidate` is taken with
98304
+ * `path.relative` and normalised to forward slashes, and the candidate is
98305
+ * outside when that path climbs out (`..` or `../...`) or is absolute, which is
98306
+ * what `relative` returns across Windows drives. A child whose name only starts
98307
+ * with two dots, such as `..cache`, is inside. Symlinks are not followed, so a
98308
+ * caller that cares where a link points has to compare resolved paths.
98262
98309
  *
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.
98310
+ * Only a type import, so a module that has to stay free of Effect services can
98311
+ * use it without importing a Layer.
98307
98312
  */
98308
98313
  function isPathWithinRoot(path, root, candidate) {
98309
98314
  const relative = path.relative(root, candidate).replaceAll("\\", "/");
98310
98315
  return relative === "" || !relative.startsWith("../") && relative !== ".." && !path.isAbsolute(relative);
98311
98316
  }
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
98317
  //#endregion
98667
98318
  //#region src/projectFile/readProjectFile.ts
98668
98319
  /**
@@ -99160,7 +98811,7 @@ function buildDiscoveryResult(probes) {
99160
98811
  }
99161
98812
  //#endregion
99162
98813
  //#region src/sourceControl/probe.ts
99163
- const execFileAsync$5 = promisify$1(execFile);
98814
+ const execFileAsync$6 = promisify$1(execFile);
99164
98815
  /**
99165
98816
  * How long a machine's toolchain answer stays good for.
99166
98817
  *
@@ -99171,7 +98822,7 @@ const execFileAsync$5 = promisify$1(execFile);
99171
98822
  */
99172
98823
  const DISCOVERY_TTL = "5 minutes";
99173
98824
  /** Probes time out because `gh auth status` reaches the network on a cold keyring. */
99174
- const PROBE_TIMEOUT_MS = 5e3;
98825
+ const PROBE_TIMEOUT_MS$1 = 5e3;
99175
98826
  /**
99176
98827
  * Runs one probe. A tool that is absent, fails, or hangs is reported the same
99177
98828
  * way — `null`, meaning "no answer" — because the discovery result only
@@ -99183,7 +98834,7 @@ const PROBE_TIMEOUT_MS = 5e3;
99183
98834
  */
99184
98835
  const probe = (command, args) => promise(async () => {
99185
98836
  try {
99186
- const { stdout, stderr } = await execFileAsync$5(command, [...args], { timeout: PROBE_TIMEOUT_MS });
98837
+ const { stdout, stderr } = await execFileAsync$6(command, [...args], { timeout: PROBE_TIMEOUT_MS$1 });
99187
98838
  const output = `${stdout}\n${stderr}`.trim();
99188
98839
  return { output: output.length === 0 ? null : output };
99189
98840
  } catch (cause) {
@@ -99464,6 +99115,26 @@ const DevTunnelLive = effect(DevTunnel, gen(function* () {
99464
99115
  };
99465
99116
  }));
99466
99117
  //#endregion
99118
+ //#region src/workspace/Services/WorkspaceEntries.ts
99119
+ /**
99120
+ * WorkspaceEntries - Effect service contract for cached workspace entry search.
99121
+ *
99122
+ * Owns indexed workspace entry search plus cache invalidation for workspace
99123
+ * roots when the underlying filesystem changes.
99124
+ *
99125
+ * @module WorkspaceEntries
99126
+ */
99127
+ var WorkspaceEntriesError = class extends TaggedErrorClass()("WorkspaceEntriesError", {
99128
+ cwd: String$1,
99129
+ operation: String$1,
99130
+ detail: String$1,
99131
+ cause: optional$4(Defect)
99132
+ }) {};
99133
+ /**
99134
+ * WorkspaceEntries - Service tag for cached workspace entry search.
99135
+ */
99136
+ var WorkspaceEntries = class extends Service$1()("forge/workspace/Services/WorkspaceEntries") {};
99137
+ //#endregion
99467
99138
  //#region src/workspace/Services/WorkspaceFileSystem.ts
99468
99139
  /**
99469
99140
  * WorkspaceFileSystem - Effect service contract for workspace file mutations.
@@ -99485,6 +99156,9 @@ var WorkspaceFileSystemError = class extends TaggedErrorClass()("WorkspaceFileSy
99485
99156
  */
99486
99157
  var WorkspaceFileSystem = class extends Service$1()("forge/workspace/Services/WorkspaceFileSystem") {};
99487
99158
  //#endregion
99159
+ //#region src/workspace/Services/WorkspaceBulkTransfer.ts
99160
+ var WorkspaceBulkTransfer = class extends Service$1()("forge/workspace/Services/WorkspaceBulkTransfer") {};
99161
+ //#endregion
99488
99162
  //#region src/workspace/Services/WorkspaceReadinessState.ts
99489
99163
  var WorkspaceReadinessState = class extends Service$1()("forge/workspace/Services/WorkspaceReadinessState") {};
99490
99164
  //#endregion
@@ -100720,6 +100394,7 @@ const CLAUDE_IDENTITY_KEYS = new Set([
100720
100394
  "provider",
100721
100395
  "credentialGenerationId"
100722
100396
  ]);
100397
+ const CLAUDE_IDENTITY_OPTIONAL_KEYS = new Set(["providerAccountIdentity"]);
100723
100398
  function isRecord$13(value) {
100724
100399
  return typeof value === "object" && value !== null && !Array.isArray(value);
100725
100400
  }
@@ -100744,6 +100419,10 @@ function daysInMonth$1(year, month) {
100744
100419
  function hasExactKeys$2(value, keys) {
100745
100420
  return Object.keys(value).every((key) => keys.has(key)) && Object.keys(value).length === keys.size;
100746
100421
  }
100422
+ function hasAllowedKeys(value, required, optional) {
100423
+ const keys = Object.keys(value);
100424
+ return keys.every((key) => required.has(key) || optional.has(key)) && [...required].every((key) => keys.includes(key));
100425
+ }
100747
100426
  function parseDeliveryIdentity(value, provider) {
100748
100427
  if (!isRecord$13(value)) return null;
100749
100428
  if (provider === "openai") return parseCodexDeliveryIdentity(value);
@@ -100758,12 +100437,25 @@ function parseCodexDeliveryIdentity(value) {
100758
100437
  providerAccountIdentity: value.providerAccountIdentity
100759
100438
  };
100760
100439
  }
100440
+ /**
100441
+ * Absent means the writer does not name the Provider Account yet, and the
100442
+ * identity keeps the shape it has always had. Present but malformed means the
100443
+ * identity is not the one CMMD signed, so it is refused the same way a
100444
+ * malformed Codex account identity is.
100445
+ */
100446
+ function claudeAccountFields(value) {
100447
+ if (value === void 0) return {};
100448
+ if (typeof value !== "string" || !PROVIDER_ACCOUNT_IDENTITY$2.test(value)) return null;
100449
+ return { providerAccountIdentity: value };
100450
+ }
100761
100451
  function parseClaudeDeliveryIdentity(value) {
100762
- if (!hasExactKeys$2(value, CLAUDE_IDENTITY_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
100452
+ const account = claudeAccountFields(value.providerAccountIdentity);
100453
+ if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
100763
100454
  return {
100764
100455
  kind: "credential_generation",
100765
100456
  provider: "claude",
100766
- credentialGenerationId: value.credentialGenerationId
100457
+ credentialGenerationId: value.credentialGenerationId,
100458
+ ...account
100767
100459
  };
100768
100460
  }
100769
100461
  function parseProviderRuntimeDeliveryReceipt(value, expected) {
@@ -100802,10 +100494,49 @@ function readProviderDeliveryBinding(value) {
100802
100494
  identity
100803
100495
  };
100804
100496
  }
100497
+ /** The Provider Account a delivery identity names, or null when it names none. */
100498
+ function providerDeliveryAccount(identity) {
100499
+ return identity.kind === "provider_account" ? identity.providerAccountIdentity : identity.providerAccountIdentity ?? null;
100500
+ }
100501
+ function sameDeliveredCredential(left, right) {
100502
+ if (left.kind === "provider_account" && right.kind === "provider_account") return left.providerAccountIdentity === right.providerAccountIdentity;
100503
+ return left.kind === "credential_generation" && right.kind === "credential_generation" && left.credentialGenerationId === right.credentialGenerationId;
100504
+ }
100505
+ /**
100506
+ * Names how one thread's delivery binding moved, which is the question the
100507
+ * delivery layer could not answer while Claude's identity carried no account.
100508
+ *
100509
+ * `account_switch` needs both sides to name an account, the accounts to differ,
100510
+ * the delivered credential to change with them, and the runtime, machine and
100511
+ * epoch to hold. A switch changes the account and the generation together, so
100512
+ * two accounts on one generation would be two accounts sharing a credential,
100513
+ * and a switch that also moved the epoch changed more than the identity. Both
100514
+ * are `unknown`.
100515
+ *
100516
+ * The same delivered credential is `unchanged` whether or not either side names
100517
+ * an account, so a binding recorded before CMMD named the account still resumes
100518
+ * against the credential it ran on. A changed credential where either side
100519
+ * names no account is `unknown` rather than guessed at, which keeps an
100520
+ * account-less binding compared on exactly the fields it was compared on before.
100521
+ *
100522
+ * `providerDeliveryBindingsEqual` asks only whether the answer is `unchanged`.
100523
+ * `permitsProviderAccountTransition` in `provider/providerAccountTransition.ts`
100524
+ * asks whether it is `account_switch`, and is the Codex resume gate's rule
100525
+ * today. #3055 wires the Claude resume gate onto it.
100526
+ */
100527
+ function classifyProviderDeliveryBindingChange(prior, current) {
100528
+ if (prior.runtimeId !== current.runtimeId || prior.machineId !== current.machineId || prior.identity.kind !== current.identity.kind || prior.identity.provider !== current.identity.provider) return "unknown";
100529
+ const sameEpoch = prior.credentialEpoch === current.credentialEpoch;
100530
+ const sameCredential = sameDeliveredCredential(prior.identity, current.identity);
100531
+ const priorAccount = providerDeliveryAccount(prior.identity);
100532
+ const currentAccount = providerDeliveryAccount(current.identity);
100533
+ if (priorAccount !== null && currentAccount !== null && priorAccount !== currentAccount) return sameEpoch && !sameCredential ? "account_switch" : "unknown";
100534
+ if (sameEpoch && sameCredential) return "unchanged";
100535
+ if (priorAccount === null || currentAccount === null) return "unknown";
100536
+ return "credential_refresh";
100537
+ }
100805
100538
  function providerDeliveryBindingsEqual(left, right) {
100806
- if (left.runtimeId !== right.runtimeId || left.machineId !== right.machineId || left.credentialEpoch !== right.credentialEpoch || left.identity.kind !== right.identity.kind || left.identity.provider !== right.identity.provider) return false;
100807
- if (left.identity.kind === "provider_account" && right.identity.kind === "provider_account") return left.identity.providerAccountIdentity === right.identity.providerAccountIdentity;
100808
- return left.identity.kind === "credential_generation" && right.identity.kind === "credential_generation" && left.identity.credentialGenerationId === right.identity.credentialGenerationId;
100539
+ return classifyProviderDeliveryBindingChange(left, right) === "unchanged";
100809
100540
  }
100810
100541
  //#endregion
100811
100542
  //#region src/provider/claudeReadinessReason.ts
@@ -101084,8 +100815,128 @@ const runClaudeLogout = (runtime) => new Promise((resolve, reject) => {
101084
100815
  });
101085
100816
  });
101086
100817
  //#endregion
100818
+ //#region src/usage/providerRateLimits.ts
100819
+ /**
100820
+ * `new Date(ms).toISOString()` throws past +-8.64e15 ms, so a `resetsAt` past
100821
+ * the same bound in seconds is refused rather than allowed to throw inside a
100822
+ * decoder whose whole job is to fail closed.
100823
+ */
100824
+ const MAX_RESETS_AT_SECONDS = 864e10;
100825
+ /**
100826
+ * One reset time in unix seconds as an ISO string, or null.
100827
+ *
100828
+ * Both mappers read reset times through this, so a malformed `resetsAt`
100829
+ * yields the null that means "this observation said nothing" instead of a
100830
+ * RangeError thrown out of a decoder the ingestion layer runs on every Claude
100831
+ * rate-limit event.
100832
+ */
100833
+ function resetsAtIso(resetsAt) {
100834
+ if (!Number.isFinite(resetsAt) || resetsAt <= 0 || resetsAt > MAX_RESETS_AT_SECONDS) return null;
100835
+ return (/* @__PURE__ */ new Date(resetsAt * 1e3)).toISOString();
100836
+ }
100837
+ /**
100838
+ * The SDK only ever reports ONE window per event. Only `five_hour` and
100839
+ * `seven_day` map to the two windows the Usage page shows; the
100840
+ * per-model/overage variants (seven_day_opus, overage, ...) are ignored here
100841
+ * on purpose rather than silently overwriting the plain 7-day figure.
100842
+ */
100843
+ function mapClaudeRateLimitInfo(info) {
100844
+ if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
100845
+ const kind = info.rateLimitType === "five_hour" ? "fiveHour" : info.rateLimitType === "seven_day" ? "sevenDay" : null;
100846
+ if (!kind) return null;
100847
+ const resetsAt = resetsAtIso(info.resetsAt);
100848
+ if (resetsAt === null) return null;
100849
+ return {
100850
+ kind,
100851
+ window: {
100852
+ usedPercent: Math.round(info.utilization * 100),
100853
+ resetsAt,
100854
+ status: info.status
100855
+ }
100856
+ };
100857
+ }
100858
+ /**
100859
+ * The one place a used-percent becomes a status. `unifiedWindows` carries no
100860
+ * per-window status of its own, and the `/usage` parser derives one the same
100861
+ * way, so both Claude usage sources agree on where "warning" starts.
100862
+ */
100863
+ function usageStatus(usedPercent) {
100864
+ if (usedPercent >= 100) return "rejected";
100865
+ return usedPercent >= 80 ? "allowed_warning" : "allowed";
100866
+ }
100867
+ /**
100868
+ * One untyped `utilization` as a whole percent, or nothing. The [0, 100] bound
100869
+ * matches `claudeUsageCli.ts`'s own bound on the `/usage` text, which in turn
100870
+ * matches CMMD's parser.
100871
+ */
100872
+ function readUsedPercent(utilization) {
100873
+ if (typeof utilization !== "number" || !Number.isFinite(utilization)) return null;
100874
+ const usedPercent = Math.round(utilization * 100);
100875
+ return usedPercent < 0 || usedPercent > 100 ? null : usedPercent;
100876
+ }
100877
+ /**
100878
+ * Reads one `unifiedWindows` entry, or nothing. Every field is checked because
100879
+ * none of them are typed: a non-object entry, a utilization that is not a
100880
+ * finite number in [0, 1], or a reset time that is not a positive finite
100881
+ * number of seconds all mean "this observation said nothing about this
100882
+ * window".
100883
+ */
100884
+ function readUnifiedWindow(raw) {
100885
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
100886
+ const { utilization, resetsAt } = raw;
100887
+ const usedPercent = readUsedPercent(utilization);
100888
+ if (usedPercent === null) return null;
100889
+ if (typeof resetsAt !== "number") return null;
100890
+ const resetsAtIsoValue = resetsAtIso(resetsAt);
100891
+ if (resetsAtIsoValue === null) return null;
100892
+ return {
100893
+ usedPercent,
100894
+ resetsAt: resetsAtIsoValue,
100895
+ status: usageStatus(usedPercent)
100896
+ };
100897
+ }
100898
+ /**
100899
+ * Reads the `unifiedWindows` sub-object of `rate_limit_info` into the two
100900
+ * rolling windows. The SDK rebuilds that sub-object on every turn from the
100901
+ * `anthropic-ratelimit-unified-{5h,7d}-utilization` and `-reset` response
100902
+ * headers.
100903
+ *
100904
+ * The top-level `rateLimitType` names one window per event and carries a
100905
+ * utilization only for `five_hour` or `seven_day`. This sub-object reports
100906
+ * every window it knows on every observation, which makes it the only
100907
+ * SDK-side source that can feed the Usage card without waking a Sprite and
100908
+ * delivering a credential.
100909
+ *
100910
+ * Its `seven_day_overage_included` window is skipped. `/usage` names each model
100911
+ * row after the display name Anthropic's usage API gives that model, one row
100912
+ * per allowlisted model, and the `7d_oi` header behind this window is one
100913
+ * aggregate that carries no name. Any name given to it here would be a guess
100914
+ * that could disagree with the rows the probe writes, so model rows come from
100915
+ * the probe alone.
100916
+ *
100917
+ * It is also undocumented internal surface stripped from the SDK's exported
100918
+ * types, so this decoder fails closed everywhere. It returns null when the
100919
+ * sub-object is absent or is not a plain object, and null when neither window
100920
+ * in it survives validation. A window that fails validation is reported as
100921
+ * null, and an unrecognized key is skipped, never fatal. A caller that gets
100922
+ * null has learned nothing and must not publish, because a partial write could
100923
+ * overwrite a good `/usage` reading with a guess.
100924
+ */
100925
+ function mapClaudeUnifiedWindows(info) {
100926
+ const raw = info.unifiedWindows;
100927
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
100928
+ const windows = raw;
100929
+ const fiveHour = readUnifiedWindow(windows.five_hour);
100930
+ const sevenDay = readUnifiedWindow(windows.seven_day);
100931
+ if (!fiveHour && !sevenDay) return null;
100932
+ return {
100933
+ fiveHour,
100934
+ sevenDay
100935
+ };
100936
+ }
100937
+ //#endregion
101087
100938
  //#region src/usage/claudeUsageCli.ts
101088
- const execFileAsync$4 = promisify$1(execFile);
100939
+ const execFileAsync$5 = promisify$1(execFile);
101089
100940
  const USAGE_ARGS = [
101090
100941
  "--safe-mode",
101091
100942
  "--no-session-persistence",
@@ -101109,7 +100960,7 @@ const MONTH_BY_NAME = {
101109
100960
  Dec: 11
101110
100961
  };
101111
100962
  async function runClaudeUsageCommand(binaryPath, args, claudeConfigDir) {
101112
- const { stdout } = await execFileAsync$4(binaryPath, [...args], {
100963
+ const { stdout } = await execFileAsync$5(binaryPath, [...args], {
101113
100964
  env: {
101114
100965
  ...process.env,
101115
100966
  TZ: "UTC",
@@ -101184,10 +101035,6 @@ function parseResetAt(value, now) {
101184
101035
  });
101185
101036
  return resetAt === null || Number.isNaN(resetAt.getTime()) ? null : resetAt.toISOString();
101186
101037
  }
101187
- function usageStatus(usedPercent) {
101188
- if (usedPercent >= 100) return "rejected";
101189
- return usedPercent >= 80 ? "allowed_warning" : "allowed";
101190
- }
101191
101038
  function parseUsageLine(output, label, now) {
101192
101039
  const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
101193
101040
  const match = new RegExp(`^${escapedLabel}: (\\d+(?:\\.\\d+)?)% used(?: · resets (.+))?$`, "m").exec(output);
@@ -101256,6 +101103,29 @@ async function fetchClaudeUsageViaOfficialClient(input) {
101256
101103
  };
101257
101104
  }
101258
101105
  //#endregion
101106
+ //#region src/usage/claudeUsageTelemetryRequest.ts
101107
+ /** The owner-session route for a Claude usage reading. */
101108
+ const CLAUDE_USAGE_TELEMETRY_PATH = "/api/forge/context/providers/usage/claude/telemetry";
101109
+ /**
101110
+ * Builds the body for either lane.
101111
+ *
101112
+ * `modelWindows` is sent only when the reading carries it. CMMD keeps the
101113
+ * stored model rows when the key is absent, so a reading that cannot name
101114
+ * model rows the way `/usage` does leaves the key out.
101115
+ */
101116
+ function claudeUsageTelemetryBody(input) {
101117
+ return {
101118
+ providerAccountId: input.attribution.providerAccountId,
101119
+ credentialGenerationId: input.attribution.credentialGenerationId,
101120
+ ...input.account,
101121
+ source: input.source,
101122
+ fiveHour: input.windows.fiveHour,
101123
+ sevenDay: input.windows.sevenDay,
101124
+ ...input.windows.modelWindows !== void 0 ? { modelWindows: input.windows.modelWindows } : {},
101125
+ observedAt: input.observedAt
101126
+ };
101127
+ }
101128
+ //#endregion
101259
101129
  //#region src/cmmd/syncClaudeCredential.ts
101260
101130
  /**
101261
101131
  * Card 1359 (harvest path A, Claude) — publish a personal Claude subscription
@@ -101271,7 +101141,6 @@ async function fetchClaudeUsageViaOfficialClient(input) {
101271
101141
  const USER_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/providers/claude/credential";
101272
101142
  const USER_CLAUDE_LINKED_ACCOUNT_PATH = "/api/forge/context/providers/claude/accounts";
101273
101143
  const AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/auth-runtime-claude-credential";
101274
- const USER_CLAUDE_USAGE_PATH = "/api/forge/context/providers/usage/claude/telemetry";
101275
101144
  const AUTH_RUNTIME_CLAUDE_USAGE_PATH = "/api/forge/context/auth-runtime-claude-credential/usage";
101276
101145
  /** Path to the local Claude credentials.json, mirroring Claude Code's resolution. */
101277
101146
  function resolveLocalClaudeCredentialPath(homePath) {
@@ -101300,7 +101169,7 @@ function resolveLocalClaudeCredentialPath(homePath) {
101300
101169
  * it reaches — this is the first gate and the only one near the user, who is
101301
101170
  * standing in front of the sign-in and can simply try again.
101302
101171
  *
101303
- * The floor is ONE token, not both, and that distinction is load-bearing: an
101172
+ * The floor is ONE token, not both, because an
101304
101173
  * expired or blank access token beside a live refresh token is the normal
101305
101174
  * resting state of a rotating login, which Claude Code refreshes silently on
101306
101175
  * use. Demanding both would reject a perfectly recoverable credential and break
@@ -101378,10 +101247,10 @@ function extractClaudeAccountMetadata(credentialJson) {
101378
101247
  return {};
101379
101248
  }
101380
101249
  }
101381
- const execFileAsync$3 = promisify$1(execFile);
101250
+ const execFileAsync$4 = promisify$1(execFile);
101382
101251
  async function loadClaudeAccountMetadataFromCli(homePath) {
101383
101252
  try {
101384
- const { stdout } = await execFileAsync$3(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
101253
+ const { stdout } = await execFileAsync$4(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
101385
101254
  timeout: 2e4,
101386
101255
  maxBuffer: 1024 * 1024,
101387
101256
  env: {
@@ -101477,18 +101346,20 @@ async function publishClaudeUsageAfterSync(input) {
101477
101346
  binaryPath: resolveClaudeBinaryPath("claude"),
101478
101347
  ...input.homePath ? { claudeConfigDir: input.homePath } : {}
101479
101348
  });
101480
- await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : USER_CLAUDE_USAGE_PATH, {
101481
- providerAccountId: input.providerAccountId,
101482
- credentialGenerationId: input.credentialGenerationId,
101483
- accountEmail: input.accountEmail,
101484
- accountDisplayName: input.accountDisplayName,
101485
- planType: input.planType,
101349
+ await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : CLAUDE_USAGE_TELEMETRY_PATH, claudeUsageTelemetryBody({
101350
+ attribution: {
101351
+ providerAccountId: input.providerAccountId,
101352
+ credentialGenerationId: input.credentialGenerationId
101353
+ },
101354
+ account: {
101355
+ accountEmail: input.accountEmail,
101356
+ accountDisplayName: input.accountDisplayName,
101357
+ planType: input.planType
101358
+ },
101486
101359
  source: "active_poll",
101487
- fiveHour: usage.fiveHour,
101488
- sevenDay: usage.sevenDay,
101489
- modelWindows: usage.modelWindows,
101360
+ windows: usage,
101490
101361
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
101491
- });
101362
+ }));
101492
101363
  return null;
101493
101364
  } catch (cause) {
101494
101365
  return { cause };
@@ -149790,7 +149661,7 @@ function issueCmmdLocalUserProviderEgressCredential(input) {
149790
149661
  });
149791
149662
  }
149792
149663
  //#endregion
149793
- //#region src/provider/codexAccountTransition.ts
149664
+ //#region src/provider/providerAccountTransition.ts
149794
149665
  function explicitProviderAccountTransition(fromAccountId, toAccountId) {
149795
149666
  if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) return void 0;
149796
149667
  return {
@@ -149798,13 +149669,20 @@ function explicitProviderAccountTransition(fromAccountId, toAccountId) {
149798
149669
  toAccountId
149799
149670
  };
149800
149671
  }
149801
- function permitsCodexAccountTransition(input) {
149672
+ /**
149673
+ * Whether a thread may move from `prior` to `current` because the person asked
149674
+ * for another Provider Account. The credential broker authorizes the selected
149675
+ * account, so only the identity may change, which is what `account_switch`
149676
+ * means.
149677
+ *
149678
+ * Nothing in the rule is specific to a provider, but the Codex resume gate in
149679
+ * `codexAppServerManager.ts` is its only caller. The Claude half is parked
149680
+ * until #3055 wires the Claude resume gate onto it.
149681
+ */
149682
+ function permitsProviderAccountTransition(input) {
149802
149683
  const { transition, requestedAccountId, prior, current } = input;
149803
- if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId || prior.identity.kind !== "provider_account" || current.identity.kind !== "provider_account" || prior.identity.provider !== "codex" || current.identity.provider !== "codex") return false;
149804
- return providerDeliveryBindingsEqual({
149805
- ...prior,
149806
- identity: current.identity
149807
- }, current);
149684
+ if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId) return false;
149685
+ return classifyProviderDeliveryBindingChange(prior, current) === "account_switch";
149808
149686
  }
149809
149687
  //#endregion
149810
149688
  //#region src/providerAuth/codexLocalAccountIdentity.ts
@@ -150304,7 +150182,7 @@ var CodexAppServerManager = class extends EventEmitter$1 {
150304
150182
  const rawPriorDeliveryBinding = input.resumeCursor.providerDeliveryBinding;
150305
150183
  const priorDeliveryBinding = readProviderDeliveryBinding(rawPriorDeliveryBinding);
150306
150184
  if (rawPriorDeliveryBinding !== void 0 && rawPriorDeliveryBinding !== null && !priorDeliveryBinding) throw new Error("Existing Codex history cannot resume because its persisted delivered provider account binding is unreadable.");
150307
- if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !permitsCodexAccountTransition({
150185
+ if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !permitsProviderAccountTransition({
150308
150186
  transition: input.accountTransition,
150309
150187
  requestedAccountId: input.providerAccountId,
150310
150188
  prior: priorDeliveryBinding,
@@ -210733,11 +210611,243 @@ function hasConfiguredApexCredential(profiles) {
210733
210611
  }));
210734
210612
  }
210735
210613
  //#endregion
210614
+ //#region src/provider/claudeCredentialDirectory.ts
210615
+ /**
210616
+ * Per-account credential directories for Claude turns.
210617
+ *
210618
+ * Claude Code keeps conversation transcripts under
210619
+ * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
210620
+ * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
210621
+ * credential file, so each Provider Account can hold its own login while every
210622
+ * session file stays in one place and `--resume` keeps finding its target. See
210623
+ * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
210624
+ *
210625
+ * That variable is not documented. Under
210626
+ * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
210627
+ * only allowed because absence degrades to a named behaviour that already
210628
+ * works: a thread with no account runs on the Environment's own login. This
210629
+ * module sets the variable only when the resolved directory holds a credential
210630
+ * the CLI can use, because a missing file, or one whose access token a refusal
210631
+ * has blanked, makes the CLI report logged out and a live turn would fail.
210632
+ *
210633
+ * Nothing writes these directories yet. #3049 adds the writer and keys each
210634
+ * directory on the account identity CMMD writes on the delivery receipt it
210635
+ * signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
210636
+ * every runtime takes the `no-account` skip.
210637
+ *
210638
+ * Free of Effect services on purpose, for the same reason as
210639
+ * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
210640
+ * services, the browser manager and the server config, so a unit test that
210641
+ * imports it cannot run in a worktree. This rule decides which account pays for
210642
+ * a turn, so it has to stay directly testable.
210643
+ */
210644
+ /** Repoints `.credentials.json` without moving the transcripts. */
210645
+ const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
210646
+ /** The one file Claude Code writes into the credential directory. */
210647
+ const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
210648
+ /**
210649
+ * Runtime env var naming the Provider Account this runtime's Claude turns
210650
+ * belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
210651
+ * and each keeps its one login.
210652
+ *
210653
+ * It is a bridge that will not get a writer. #3073 settled the key in
210654
+ * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
210655
+ * comes from the identity CMMD issues and writes on the delivery receipt it
210656
+ * signs, and this variable goes away. #3049 makes that change. The credential
210657
+ * generation was ruled out as a key: it rotates on every reconnect, and CMMD
210658
+ * uses it as the primary account's id, so keying on it would strand every
210659
+ * thread pinned to that account on a directory that stops receiving deliveries.
210660
+ *
210661
+ * Tests reach the `credentialDirectory` parameter on
210662
+ * `ClaudeAdapter.buildClaudeQueryEnv` directly.
210663
+ */
210664
+ const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
210665
+ /** Directory under the server's base directory that holds one directory per account. */
210666
+ const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
210667
+ const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
210668
+ const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
210669
+ const DIRECTORY_NAME_HASH_LENGTH = 32;
210670
+ /**
210671
+ * Directory name for an account. A hash, so no email or display name reaches a
210672
+ * filesystem path and no path-illegal character can appear in one either.
210673
+ */
210674
+ function claudeCredentialDirectoryName(accountIdentity) {
210675
+ return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
210676
+ }
210677
+ /** The account this runtime's Claude turns belong to, or null while the variable is unset. */
210678
+ function resolveClaudeProviderAccountIdentity(env) {
210679
+ const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
210680
+ if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
210681
+ return identity;
210682
+ }
210683
+ /**
210684
+ * Where per-account credential directories live: `claude-credentials` under the
210685
+ * server's base directory, `ServerConfig.baseDir`, which is `FORGE_HOME` or
210686
+ * `~/.forge`. A delivery writer gets the same `ServerConfig`, so reader and
210687
+ * writer cannot disagree about the root. Always outside `CLAUDE_CONFIG_DIR`, so
210688
+ * one of these can never be mistaken for the main credential store, and a stray
210689
+ * read of the config directory cannot walk into another account's login.
210690
+ */
210691
+ function claudeCredentialDirectoryRoot(env, baseDir) {
210692
+ if (!isAbsolute(baseDir)) return null;
210693
+ const root = resolve(baseDir, CREDENTIAL_ROOT_DIRECTORY_NAME);
210694
+ const configDirectory = claudeConfigDirectory(env);
210695
+ if (configDirectory && isPathWithinRoot({
210696
+ relative,
210697
+ isAbsolute
210698
+ }, configDirectory, root)) return null;
210699
+ return root;
210700
+ }
210701
+ /** Absolute credential directory for one account, or null when either half is missing. */
210702
+ function resolveClaudeCredentialDirectory(input) {
210703
+ if (!input.accountIdentity) return null;
210704
+ const root = claudeCredentialDirectoryRoot(input.env, input.baseDir);
210705
+ if (!root) return null;
210706
+ return join(root, claudeCredentialDirectoryName(input.accountIdentity));
210707
+ }
210708
+ /**
210709
+ * Decide whether this turn may point at a per-account credential directory.
210710
+ *
210711
+ * The cheap checks run first and the capability probe runs last, so a runtime
210712
+ * with no usable credential never spawns the CLI. The probe's error channel is
210713
+ * empty, so nothing here catches.
210714
+ *
210715
+ * A skip is not a fallback onto the default account. With no account the turn
210716
+ * runs on the Environment's own login, unchanged. A thread pinned to an account
210717
+ * whose credential is missing is refused by name under
210718
+ * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
210719
+ * `no-usable-credential`, and `capability-not-supported` for a pinned account,
210720
+ * into those refusals. Until #3049 nothing names an account, so `no-account` is
210721
+ * the only skip a runtime sees.
210722
+ */
210723
+ function decideClaudeCredentialDirectory(input) {
210724
+ return gen(function* () {
210725
+ if (!input.accountIdentity) return skip("no-account");
210726
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
210727
+ const directory = resolveClaudeCredentialDirectory(input);
210728
+ if (!directory) return skip("no-root");
210729
+ if (!holdsUsableCredential(directory)) return skip("no-usable-credential");
210730
+ return (yield* input.probeSupport) === "supported" ? set(directory) : skip("capability-not-supported");
210731
+ });
210732
+ }
210733
+ /**
210734
+ * True when `.credentials.json` parses and carries a non-empty access token.
210735
+ *
210736
+ * File presence is not readiness. A refused credential is rewritten in place
210737
+ * with `accessToken: ""`, so a file that exists can still leave the CLI logged
210738
+ * out. Only the token's length is checked. The value is never logged, returned
210739
+ * or kept.
210740
+ */
210741
+ function holdsUsableCredential(directory) {
210742
+ const file = join(directory, CLAUDE_CREDENTIAL_FILE_NAME);
210743
+ try {
210744
+ if (!statSync(file).isFile()) return false;
210745
+ return hasAccessToken(JSON.parse(readFileSync(file, "utf8")));
210746
+ } catch {
210747
+ return false;
210748
+ }
210749
+ }
210750
+ function hasAccessToken(credential) {
210751
+ if (typeof credential !== "object" || credential === null) return false;
210752
+ const oauth = credential.claudeAiOauth;
210753
+ if (typeof oauth !== "object" || oauth === null) return false;
210754
+ const token = oauth.accessToken;
210755
+ return typeof token === "string" && token.length > 0;
210756
+ }
210757
+ function set(directory) {
210758
+ return {
210759
+ outcome: "set",
210760
+ directory
210761
+ };
210762
+ }
210763
+ function skip(reason) {
210764
+ return {
210765
+ outcome: "skipped",
210766
+ reason
210767
+ };
210768
+ }
210769
+ function claudeConfigDirectory(env) {
210770
+ const configured = env.CLAUDE_CONFIG_DIR?.trim();
210771
+ if (configured) return configured;
210772
+ try {
210773
+ return join(homedir(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
210774
+ } catch {
210775
+ return null;
210776
+ }
210777
+ }
210778
+ //#endregion
210779
+ //#region src/provider/personalClaudeInVm.ts
210780
+ /**
210781
+ * CMMD writes this marker when it activates a personal Claude subscription
210782
+ * inside a running sprite. Activation always happens after boot, so a
210783
+ * boot-time env var alone cannot reflect it (FOR-36249).
210784
+ *
210785
+ * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
210786
+ * Without that, the marker sits directly under FORGE_HOME, which is the same
210787
+ * guest home CMMD derives every sprite path from. A server with neither has no
210788
+ * sprite home to look in, and a desktop never has a marker, so it reads none.
210789
+ */
210790
+ const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
210791
+ const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
210792
+ function markerPath(env) {
210793
+ if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
210794
+ if (env.FORGE_HOME) return join(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
210795
+ return null;
210796
+ }
210797
+ /**
210798
+ * True when this runtime should offer the personal Claude subscription lane.
210799
+ *
210800
+ * Accepts either signal: the boot-time env var (backward compatible) or the
210801
+ * activation marker on disk. Reading the marker at call time lets the provider
210802
+ * catalog and the per-turn readiness guard self-heal once CMMD delivers,
210803
+ * without a sprite reboot. Callers still verify the credential is non-empty
210804
+ * before treating it as usable; this only decides whether the lane is active.
210805
+ */
210806
+ function personalClaudeInVmEnabled(env = process.env) {
210807
+ if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
210808
+ const marker = markerPath(env);
210809
+ if (!marker) return false;
210810
+ try {
210811
+ return existsSync(marker);
210812
+ } catch {
210813
+ return false;
210814
+ }
210815
+ }
210816
+ /** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
210817
+ function bootExportedPersonalClaudeGenerationHash(env = process.env) {
210818
+ return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
210819
+ }
210820
+ /**
210821
+ * The sha256 of the Claude credential generation this runtime holds, or "" when
210822
+ * it holds none. The readiness guard compares it with the generation CMMD
210823
+ * applied. The restart proof must not use it; it uses the boot-exported hash.
210824
+ *
210825
+ * The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
210826
+ * the marker already exists at boot. Activation in place writes the generation
210827
+ * file and touches the marker without setting any env, so an empty variable
210828
+ * falls back to that file, hashed the way the preload hashes it. CMMD puts the
210829
+ * file beside the marker in the guest home and names no path for it. A marker
210830
+ * with no generation file is a legacy runtime, and gets no hash here either.
210831
+ */
210832
+ function personalClaudeGenerationHash(env = process.env) {
210833
+ const exported = bootExportedPersonalClaudeGenerationHash(env);
210834
+ if (exported) return exported;
210835
+ const marker = markerPath(env);
210836
+ if (!marker) return "";
210837
+ try {
210838
+ if (!existsSync(marker)) return "";
210839
+ const generation = readFileSync(join(dirname(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
210840
+ return generation ? createHash("sha256").update(generation).digest("hex") : "";
210841
+ } catch {
210842
+ return "";
210843
+ }
210844
+ }
210845
+ //#endregion
210736
210846
  //#region src/provider/claudeCredentialContainment.ts
210737
210847
  /**
210738
210848
  * Containment for provider credentials inherited by the Claude Agent SDK child.
210739
210849
  *
210740
- * Pure and dependency-free on purpose: ClaudeAdapter pulls in Effect platform
210850
+ * Free of Effect services on purpose: ClaudeAdapter pulls in Effect platform
210741
210851
  * services, the browser manager, and the server config, so a unit test that
210742
210852
  * imports it cannot run in a worktree. This rule is cost-critical and must stay
210743
210853
  * directly testable.
@@ -210784,6 +210894,190 @@ function withoutInheritedAnthropicCredentials(env, options) {
210784
210894
  if (options?.stripEndpointOverride) delete contained[ANTHROPIC_ENDPOINT_OVERRIDE_KEY];
210785
210895
  return contained;
210786
210896
  }
210897
+ /**
210898
+ * The environment a Claude child gets when it runs on the personal
210899
+ * subscription. Server-only secrets are scrubbed, inherited Anthropic keys are
210900
+ * removed so they cannot outrank the login, and the credential directory is set
210901
+ * only when one was decided. The turn's personal-subscription branch and the
210902
+ * capability probe both build their environment here, so the probe asks about
210903
+ * the setup the turn will run with.
210904
+ *
210905
+ * The endpoint override goes only when `personalClaudeInVmEnabled` says this
210906
+ * runtime is on the personal Claude lane. That check also reads the activation
210907
+ * marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
210908
+ * has the marker and no env var until it restarts, drops the override too.
210909
+ */
210910
+ function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
210911
+ return {
210912
+ ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
210913
+ ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
210914
+ };
210915
+ }
210916
+ //#endregion
210917
+ //#region src/provider/claudeCredentialDirectoryProbe.ts
210918
+ /**
210919
+ * Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
210920
+ *
210921
+ * The variable is unpublished, so a runtime is asked rather than assumed to
210922
+ * honour it (`docs/adr/unpublished-provider-surface-needs-a-fallback.md`). The
210923
+ * question the probe can actually answer: point the variable at an empty
210924
+ * directory and a runtime that honours it flips from logged in to logged out.
210925
+ * A runtime with no credential at all is logged out either way and cannot
210926
+ * discriminate, which is `inconclusive`, and callers read that as unsupported.
210927
+ *
210928
+ * An answer only counts for the binary and environment the turn will use. The
210929
+ * probe spawns the `binaryPath` the turn hands the SDK, and builds its
210930
+ * environment with `claudeSubscriptionChildEnv`, the same builder as the turn's
210931
+ * personal-subscription branch.
210932
+ *
210933
+ * `claude auth status` is a pure read. It sends no prompt and leaves
210934
+ * `.credentials.json` byte-identical, so probing spends no subscription usage.
210935
+ * It prints JSON on stdout and exits 1 when logged out, so the exit code is not
210936
+ * a failure signal and the `loggedIn` field is what gets read.
210937
+ */
210938
+ const execFileAsync$3 = promisify$1(execFile);
210939
+ const AUTH_STATUS_ARGS = ["auth", "status"];
210940
+ const VERSION_ARGS = ["--version"];
210941
+ /**
210942
+ * The probe sits in front of a turn, so a provider process that never answers
210943
+ * must not hold that turn open. `execFile` kills the child at this bound, and
210944
+ * `runBounded` gives up on it at the same bound whatever the deps do, so a
210945
+ * hung or unkillable process costs a few seconds and an `inconclusive`.
210946
+ */
210947
+ const PROBE_TIMEOUT_MS = 5e3;
210948
+ const PROBE_OUTPUT_LIMIT_BYTES = 1024 * 1024;
210949
+ const PROBE_DIRECTORY_PREFIX = "forge-claude-capability-";
210950
+ const VERSION_PATTERN = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
210951
+ function makeClaudeCredentialDirectoryProbe(deps) {
210952
+ const answers = /* @__PURE__ */ new Map();
210953
+ const answer = async () => {
210954
+ const version = await readVersionSafely(deps);
210955
+ if (version === null) return runProbe(deps);
210956
+ const cached = answers.get(version);
210957
+ if (cached) return cached;
210958
+ const pending = runProbe(deps);
210959
+ answers.set(version, pending);
210960
+ const forget = () => {
210961
+ if (answers.get(version) === pending) answers.delete(version);
210962
+ };
210963
+ pending.then((result) => {
210964
+ if (result === "inconclusive") forget();
210965
+ }, forget);
210966
+ return pending;
210967
+ };
210968
+ return { support: tryPromise(answer).pipe(orElseSucceed(() => "inconclusive")) };
210969
+ }
210970
+ /**
210971
+ * One probe per binary path for the life of the process. A conclusive answer is
210972
+ * cached per binary version, so the two `auth status` spawns run once per path
210973
+ * and version. `claude --version` still spawns on every call, which is how an
210974
+ * in-place CLI upgrade gets a fresh answer.
210975
+ */
210976
+ const probesByBinaryPath = /* @__PURE__ */ new Map();
210977
+ const INCONCLUSIVE_PROBE = { support: succeed("inconclusive") };
210978
+ /**
210979
+ * The probe for the binary a turn with this `binaryPath` setting runs.
210980
+ *
210981
+ * `buildClaudeQueryOptions` hands the setting to the SDK as
210982
+ * `pathToClaudeCodeExecutable`, and the SDK spawns it as given, so the default
210983
+ * `claude` goes through PATH for the probe exactly as for the turn. The SDK
210984
+ * picks a bundled build of its own only when the path is empty, and the
210985
+ * settings schema turns an empty value into `claude`. Should one arrive empty
210986
+ * anyway, the answer is `inconclusive` rather than an answer about another
210987
+ * binary.
210988
+ */
210989
+ function claudeCredentialDirectoryProbeFor(binaryPath) {
210990
+ if (!binaryPath) return INCONCLUSIVE_PROBE;
210991
+ const existing = probesByBinaryPath.get(binaryPath);
210992
+ if (existing) return existing;
210993
+ const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binaryPath));
210994
+ probesByBinaryPath.set(binaryPath, probe);
210995
+ return probe;
210996
+ }
210997
+ const spawnClaude = (file, args, options) => execFileAsync$3(file, [...args], { ...options });
210998
+ function claudeCredentialDirectoryProbeDeps(binaryPath, options = {}) {
210999
+ const run = (args, credentialDirectory) => runClaudeCommand$1(options.execFile ?? spawnClaude, binaryPath, args, claudeSubscriptionChildEnv(options.baseEnv ?? process.env, credentialDirectory));
211000
+ return {
211001
+ runAuthStatus: ({ credentialDirectory }) => run(AUTH_STATUS_ARGS, credentialDirectory),
211002
+ readBinaryVersion: async () => {
211003
+ const { stdout } = await run(VERSION_ARGS, null);
211004
+ return VERSION_PATTERN.exec(stdout)?.[0] ?? null;
211005
+ },
211006
+ makeProbeDirectory: () => mkdtemp(join(tmpdir(), PROBE_DIRECTORY_PREFIX)),
211007
+ removeProbeDirectory: (directory) => rm(directory, {
211008
+ recursive: true,
211009
+ force: true
211010
+ })
211011
+ };
211012
+ }
211013
+ /** Rejects when the probe could not tell. `support` turns that into `inconclusive`. */
211014
+ async function runProbe(deps) {
211015
+ let probeDirectory = null;
211016
+ try {
211017
+ if (parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: null }))) !== true) return "inconclusive";
211018
+ probeDirectory = await deps.makeProbeDirectory();
211019
+ const directory = probeDirectory;
211020
+ const redirected = parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: directory })));
211021
+ if (redirected === false) return "supported";
211022
+ return redirected === true ? "unsupported" : "inconclusive";
211023
+ } finally {
211024
+ if (probeDirectory !== null) await deps.removeProbeDirectory(probeDirectory).catch(() => void 0);
211025
+ }
211026
+ }
211027
+ async function readVersionSafely(deps) {
211028
+ try {
211029
+ return await runBounded(deps, () => deps.readBinaryVersion());
211030
+ } catch {
211031
+ return null;
211032
+ }
211033
+ }
211034
+ /**
211035
+ * Give up on a call that outlives the bound. The underlying promise is left to
211036
+ * settle on its own; the real deps hand `execFile` the same bound, so its child
211037
+ * is already being killed by the time this rejects.
211038
+ */
211039
+ async function runBounded(deps, operation) {
211040
+ const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
211041
+ let timer;
211042
+ try {
211043
+ return await Promise.race([operation(), new Promise((_resolve, reject) => {
211044
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude probe call timed out")), timeoutMs);
211045
+ })]);
211046
+ } finally {
211047
+ if (timer !== void 0) clearTimeout(timer);
211048
+ }
211049
+ }
211050
+ /** The `loggedIn` field, or null when the output does not carry one. */
211051
+ function parseLoggedIn(output) {
211052
+ const parsed = parseJsonObject$1(output.stdout);
211053
+ return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
211054
+ }
211055
+ function parseJsonObject$1(output) {
211056
+ const trimmed = output.trim();
211057
+ const start = trimmed.indexOf("{");
211058
+ const end = trimmed.lastIndexOf("}");
211059
+ if (start === -1 || end <= start) return null;
211060
+ try {
211061
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
211062
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
211063
+ } catch {
211064
+ return null;
211065
+ }
211066
+ }
211067
+ async function runClaudeCommand$1(execFileImpl, binaryPath, args, env) {
211068
+ try {
211069
+ const { stdout } = await execFileImpl(binaryPath, args, {
211070
+ env,
211071
+ maxBuffer: PROBE_OUTPUT_LIMIT_BYTES,
211072
+ timeout: PROBE_TIMEOUT_MS
211073
+ });
211074
+ return { stdout };
211075
+ } catch (cause) {
211076
+ const failure = cause;
211077
+ if (typeof failure.stdout !== "string") throw cause;
211078
+ return { stdout: failure.stdout };
211079
+ }
211080
+ }
210787
211081
  //#endregion
210788
211082
  //#region src/provider/makeManagedServerProvider.ts
210789
211083
  const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (input) {
@@ -210796,12 +211090,12 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
210796
211090
  const forceRefresh = options?.forceRefresh === true;
210797
211091
  const previousSettings = yield* get$6(settingsRef);
210798
211092
  if (!forceRefresh && !input.haveSettingsChanged(previousSettings, nextSettings)) {
210799
- yield* set$2(settingsRef, nextSettings);
211093
+ yield* set$3(settingsRef, nextSettings);
210800
211094
  return yield* get$6(snapshotRef);
210801
211095
  }
210802
211096
  const nextSnapshot = yield* input.checkProvider;
210803
- yield* set$2(settingsRef, nextSettings);
210804
- yield* set$2(snapshotRef, nextSnapshot);
211097
+ yield* set$3(settingsRef, nextSettings);
211098
+ yield* set$3(snapshotRef, nextSnapshot);
210805
211099
  yield* publish(changesPubSub, nextSnapshot);
210806
211100
  return nextSnapshot;
210807
211101
  });
@@ -210821,7 +211115,7 @@ const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (i
210821
211115
  failureCount: failureCount + 1,
210822
211116
  nextDelayMs: Math.min(baseMs * Math.pow(2, failureCount + 1), 3e5)
210823
211117
  });
210824
- } else yield* set$2(refreshFailureCount, 0);
211118
+ } else yield* set$3(refreshFailureCount, 0);
210825
211119
  })).pipe(forkScoped);
210826
211120
  return {
210827
211121
  getSnapshot: input.getSettings.pipe(flatMap(applySnapshot), tapError(logError), orDie),
@@ -210873,43 +211167,6 @@ const makeProviderAuthProvenance = (input) => {
210873
211167
  };
210874
211168
  };
210875
211169
  //#endregion
210876
- //#region src/provider/personalClaudeInVm.ts
210877
- /**
210878
- * CMMD writes this marker when it activates a personal Claude subscription
210879
- * inside a running sprite. Activation always happens after boot, so a
210880
- * boot-time env var alone cannot reflect it (FOR-36249).
210881
- *
210882
- * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
210883
- * Without that, the marker sits directly under FORGE_HOME, which is the same
210884
- * guest home CMMD derives every sprite path from. A server with neither has no
210885
- * sprite home to look in, and a desktop never has a marker, so it reads none.
210886
- */
210887
- const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
210888
- function markerPath(env) {
210889
- if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
210890
- if (env.FORGE_HOME) return join(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
210891
- return null;
210892
- }
210893
- /**
210894
- * True when this runtime should offer the personal Claude subscription lane.
210895
- *
210896
- * Accepts either signal: the boot-time env var (backward compatible) or the
210897
- * activation marker on disk. Reading the marker at call time lets the provider
210898
- * catalog and the per-turn readiness guard self-heal once CMMD delivers,
210899
- * without a sprite reboot. Callers still verify the credential is non-empty
210900
- * before treating it as usable; this only decides whether the lane is active.
210901
- */
210902
- function personalClaudeInVmEnabled(env = process.env) {
210903
- if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
210904
- const marker = markerPath(env);
210905
- if (!marker) return false;
210906
- try {
210907
- return existsSync(marker);
210908
- } catch {
210909
- return false;
210910
- }
210911
- }
210912
- //#endregion
210913
211170
  //#region src/provider/Layers/ClaudeProvider.ts
210914
211171
  function endpointProfileDisplayNames(settings) {
210915
211172
  return Object.fromEntries(settings.endpointProfiles.filter((p) => p.displayName).map((p) => [p.slug, p.displayName]));
@@ -212328,10 +212585,26 @@ function claudeForgeAttributionPrompt() {
212328
212585
  const attribution = resolveAgentAttribution();
212329
212586
  return `When creating git commits, do NOT add any "Claude" or "Anthropic" co-author trailers. Use exactly:\n ${attribution.coauthorTrailer}\n\nWhen creating GitHub pull request descriptions, do NOT add any "🤖 Generated with [Claude Code]" footer. Use exactly:\n ${attribution.prFooter}`;
212330
212587
  }
212331
- function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential) {
212588
+ /**
212589
+ * `credentialDirectory` points this turn at one Provider Account's own
212590
+ * `.credentials.json` without moving the transcripts, which is what lets an
212591
+ * account switch keep `--resume` working
212592
+ * (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It stays null
212593
+ * until #3049 gives the directories a writer, keyed on the account identity on
212594
+ * the delivery receipt CMMD signs. `decideClaudeCredentialDirectory` only hands
212595
+ * back a directory that already holds a usable credential, and while none
212596
+ * exists the environment is byte-for-byte what it would be without this
212597
+ * parameter.
212598
+ *
212599
+ * It applies to the personal-subscription branch below and to nothing else. On
212600
+ * the profile / proxy branch the credential file is not what authenticates the
212601
+ * turn, an API key is, so pointing the variable there would add a second source
212602
+ * of truth that a later reader has to rule out.
212603
+ */
212604
+ function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
212332
212605
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
212333
212606
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
212334
- if (!effectiveBaseUrl) return withoutInheritedAnthropicCredentials(baseEnv, { stripEndpointOverride: baseProcessEnv.FORGE_PERSONAL_CLAUDE_IN_VM === "true" });
212607
+ if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
212335
212608
  const effectiveApiKey = resolveClaudeQueryApiKey(endpointProfile, claudeSettings, providerProxyCredential?.token ?? null);
212336
212609
  return {
212337
212610
  ...baseEnv,
@@ -214599,6 +214872,17 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
214599
214872
  })));
214600
214873
  const claudeSettings = serverSettings.providers.claudeAgent;
214601
214874
  const claudeBinaryPath = claudeSettings.binaryPath;
214875
+ const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
214876
+ env: process.env,
214877
+ baseDir: serverConfig.baseDir,
214878
+ accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
214879
+ probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
214880
+ });
214881
+ if (credentialDirectoryDecision.outcome === "skipped") {
214882
+ const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
214883
+ yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
214884
+ }
214885
+ const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
214602
214886
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
214603
214887
  const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
214604
214888
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
@@ -214623,7 +214907,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
214623
214907
  existingResumeSessionId,
214624
214908
  newSessionId,
214625
214909
  canUseTool,
214626
- env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential),
214910
+ env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
214627
214911
  browserManager,
214628
214912
  threadId,
214629
214913
  browserToolEnvironmentId,
@@ -214660,7 +214944,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
214660
214944
  effectiveApiModelId: modelConfig.effectiveApiModelId,
214661
214945
  effectiveEffort: modelConfig.effectiveEffort
214662
214946
  });
214663
- yield* set$2(contextRef, context$4);
214947
+ yield* set$3(contextRef, context$4);
214664
214948
  sessions.set(threadId, context$4);
214665
214949
  yield* emitClaudeSessionStartupEvents({
214666
214950
  threadId,
@@ -219541,7 +219825,7 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
219541
219825
  }
219542
219826
  });
219543
219827
  });
219544
- const updateConfigOptions = (response) => set$2(configOptionsRef, sessionConfigOptionsFromSetup(response));
219828
+ const updateConfigOptions = (response) => set$3(configOptionsRef, sessionConfigOptionsFromSetup(response));
219545
219829
  const updateCurrentModeId = (modeId) => update$4(modeStateRef, (current) => current ? {
219546
219830
  ...current,
219547
219831
  currentModeId: modeId
@@ -219600,8 +219884,8 @@ const makeAcpSessionRuntime = (options) => gen(function* () {
219600
219884
  sessionId = created.sessionId;
219601
219885
  sessionSetupResult = created;
219602
219886
  }
219603
- yield* set$2(modeStateRef, parseSessionModeState(sessionSetupResult));
219604
- yield* set$2(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult));
219887
+ yield* set$3(modeStateRef, parseSessionModeState(sessionSetupResult));
219888
+ yield* set$3(configOptionsRef, sessionConfigOptionsFromSetup(sessionSetupResult));
219605
219889
  return {
219606
219890
  sessionId,
219607
219891
  initializeResult,
@@ -219795,10 +220079,10 @@ const makeStartCoordinator = (startStateRef, startOnce) => gen(function* () {
219795
220079
  switch (state._tag) {
219796
220080
  case "Started": return [succeed(state.result), state];
219797
220081
  case "Starting": return [_await(state.deferred), state];
219798
- case "NotStarted": return [startOnce.pipe(tap((result) => set$2(startStateRef, {
220082
+ case "NotStarted": return [startOnce.pipe(tap((result) => set$3(startStateRef, {
219799
220083
  _tag: "Started",
219800
220084
  result
219801
- }).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$2(startStateRef, { _tag: "NotStarted" }))))), {
220085
+ }).pipe(andThen(succeed$3(deferred, result)))), onError$1((cause) => failCause$3(deferred, cause).pipe(andThen(set$3(startStateRef, { _tag: "NotStarted" }))))), {
219802
220086
  _tag: "Starting",
219803
220087
  deferred
219804
220088
  }];
@@ -229697,7 +229981,7 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
229697
229981
  });
229698
229982
  }
229699
229983
  function requiresNativeClaudeReadiness(config, provider) {
229700
- return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && process.env.FORGE_PERSONAL_CLAUDE_IN_VM === "true";
229984
+ return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
229701
229985
  }
229702
229986
  function resolveGuardAuthority(config, readSessionId, input) {
229703
229987
  const runtimeId = config.forgeCmmdRuntimeId?.trim();
@@ -229709,7 +229993,7 @@ function resolveGuardAuthority(config, readSessionId, input) {
229709
229993
  runtimeId,
229710
229994
  machineId,
229711
229995
  credentialEpoch,
229712
- localGenerationHash: process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "",
229996
+ localGenerationHash: personalClaudeGenerationHash(),
229713
229997
  sessionId
229714
229998
  };
229715
229999
  }
@@ -283475,7 +283759,7 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
283475
283759
  let preResolvedCommitSuggestion = void 0;
283476
283760
  const modelSelection = yield* serverSettingsService.getSettings.pipe(map$3((settings) => settings.textGenerationModelSelection), mapError((cause) => gitManagerError("runStackedAction", "Failed to get server settings.", cause)));
283477
283761
  if (input.featureBranch) {
283478
- yield* set$2(currentPhase, some("branch"));
283762
+ yield* set$3(currentPhase, some("branch"));
283479
283763
  yield* progress.emit({
283480
283764
  kind: "phase_started",
283481
283765
  phase: "branch",
@@ -283488,12 +283772,12 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
283488
283772
  } else branchStep = { status: "skipped_not_requested" };
283489
283773
  const currentBranch = branchStep.name ?? initialStatus.branch;
283490
283774
  const commitAction = isCommitAction(input.action) ? input.action : null;
283491
- 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" };
283775
+ 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" };
283492
283776
  const push = wantsPush ? yield* progress.emit({
283493
283777
  kind: "phase_started",
283494
283778
  phase: "push",
283495
283779
  label: "Pushing..."
283496
- }).pipe(tap(() => set$2(currentPhase, some("push"))), flatMap(() => {
283780
+ }).pipe(tap(() => set$3(currentPhase, some("push"))), flatMap(() => {
283497
283781
  const emit = (event) => progress.emit(event);
283498
283782
  const hookProgress = createHookProgressEmitter(emit);
283499
283783
  return gitCore.pushCurrentBranch(input.cwd, currentBranch, { progress: hookProgress.progress }).pipe(tap(() => {
@@ -283511,11 +283795,11 @@ const GitManagerLive = effect(GitManager, fn$1("makeGitManager")(function* () {
283511
283795
  kind: "phase_started",
283512
283796
  phase: "pr",
283513
283797
  label: "Preparing PR merge..."
283514
- }).pipe(tap(() => set$2(currentPhase, some("pr"))), flatMap(() => runMergePrStep(input.cwd, currentBranch, progress.emit))) : wantsPr ? yield* progress.emit({
283798
+ }).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runMergePrStep(input.cwd, currentBranch, progress.emit))) : wantsPr ? yield* progress.emit({
283515
283799
  kind: "phase_started",
283516
283800
  phase: "pr",
283517
283801
  label: "Preparing PR..."
283518
- }).pipe(tap(() => set$2(currentPhase, some("pr"))), flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch, progress.emit, input.threadId))) : { status: "skipped_not_requested" };
283802
+ }).pipe(tap(() => set$3(currentPhase, some("pr"))), flatMap(() => runPrStep(modelSelection, input.cwd, currentBranch, progress.emit, input.threadId))) : { status: "skipped_not_requested" };
283519
283803
  const toast = yield* buildCompletionToast(input.cwd, {
283520
283804
  action: input.action,
283521
283805
  branch: branchStep,
@@ -286869,25 +287153,124 @@ const makeRuntimeDrainReporter = gen(function* () {
286869
287153
  var RuntimeDrainReporter = class extends Service$1()("forge/runtimeDrain/RuntimeDrainReporter") {};
286870
287154
  const RuntimeDrainReporterLive = effect(RuntimeDrainReporter, makeRuntimeDrainReporter);
286871
287155
  //#endregion
286872
- //#region src/usage/providerRateLimits.ts
286873
- /**
286874
- * The SDK only ever reports ONE window per event. Only `five_hour` and
286875
- * `seven_day` map to the two windows the Usage page shows; the
286876
- * per-model/overage variants (seven_day_opus, overage, ...) are ignored here
286877
- * on purpose rather than silently overwriting the plain 7-day figure.
286878
- */
286879
- function mapClaudeRateLimitInfo(info) {
286880
- if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
286881
- const kind = info.rateLimitType === "five_hour" ? "fiveHour" : info.rateLimitType === "seven_day" ? "sevenDay" : null;
286882
- if (!kind) return null;
286883
- return {
286884
- kind,
286885
- window: {
286886
- usedPercent: Math.round(info.utilization * 100),
286887
- resetsAt: (/* @__PURE__ */ new Date(info.resetsAt * 1e3)).toISOString(),
286888
- status: info.status
286889
- }
286890
- };
287156
+ //#region src/usage/Services/ClaudeUsageTelemetry.ts
287157
+ /**
287158
+ * Claude usage the SDK reports on every turn, published as passive telemetry.
287159
+ *
287160
+ * Every Claude turn carries a `rate_limit_event` whose `unifiedWindows`
287161
+ * sub-object holds the two rolling windows the Usage card renders. Reading it
287162
+ * costs nothing: no `claude` process, no credential delivery, no woken sprite.
287163
+ *
287164
+ * This module is the whole publish lane behind one call. `observe` takes what
287165
+ * an observation knows and returns void. Whether the floor admits it, which
287166
+ * credential generation it ran on, whether the value is new, and whether CMMD
287167
+ * accepted it are all decided here, so the ingestion layer stays a decoder and
287168
+ * a hand-off. Nothing decided here can fail a turn, and no admitted
287169
+ * observation outlives `CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS`.
287170
+ *
287171
+ * The `/usage` probe in `syncClaudeCredential.ts` is a separate lane that
287172
+ * publishes `source: "active_poll"`. This one publishes
287173
+ * `source: "passive_sdk_telemetry"`, and both lanes build their body with
287174
+ * `claudeUsageTelemetryBody`.
287175
+ *
287176
+ * @module ClaudeUsageTelemetry
287177
+ */
287178
+ var ClaudeUsageTelemetry = class extends Service$1()("forge/usage/Services/ClaudeUsageTelemetry") {};
287179
+ /**
287180
+ * The longest one admitted observation may take, from the credential
287181
+ * generation lookup through the CMMD credential and the POST, before it is
287182
+ * abandoned and counted as failed. Ingestion does not wait on it, so this
287183
+ * bounds how long a slow lookup or a slow CMMD keeps a fiber alive, not how
287184
+ * long a turn waits. Together with the floor it caps how many observations for
287185
+ * one account can be in flight at once.
287186
+ */
287187
+ const CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS = 1e4;
287188
+ /** How many accounts, and how many pairs, the observer remembers before evicting the oldest. */
287189
+ const MAX_TRACKED_KEYS = 32;
287190
+ /**
287191
+ * Why one publish did not land. Every reason is the same to `observe`, which
287192
+ * logs it and moves on, so the type exists to name the lane rather than to be
287193
+ * matched on.
287194
+ */
287195
+ var ClaudeUsageTelemetryPublishError = class extends TaggedError("ClaudeUsageTelemetryPublishError") {};
287196
+ function pairKey(attribution) {
287197
+ return `${attribution.providerAccountId}\u0000${attribution.credentialGenerationId}`;
287198
+ }
287199
+ function windowFingerprint(value) {
287200
+ return value === null ? "-" : `${value.usedPercent}|${value.resetsAt}|${value.status}`;
287201
+ }
287202
+ /**
287203
+ * A value fingerprint built field by field rather than by `JSON.stringify` on
287204
+ * the whole object, so the comparison does not depend on key insertion order
287205
+ * and cannot silently start ignoring a field a future mapper adds without a
287206
+ * matching change here.
287207
+ */
287208
+ function windowsFingerprint(windows) {
287209
+ return `${windowFingerprint(windows.fiveHour)}/${windowFingerprint(windows.sevenDay)}`;
287210
+ }
287211
+ /**
287212
+ * Stores `value` as the newest entry for `key` and evicts the oldest entry past
287213
+ * the cap. Map iteration is insertion-ordered, so the first key is the one
287214
+ * touched longest ago. An idle account or a rotated credential generation
287215
+ * rarely comes back, so evicting it costs at most one early lookup or one
287216
+ * redundant publish.
287217
+ */
287218
+ function remember(map, key, value) {
287219
+ map.delete(key);
287220
+ map.set(key, value);
287221
+ if (map.size > MAX_TRACKED_KEYS) {
287222
+ const oldest = map.keys().next();
287223
+ if (!oldest.done) map.delete(oldest.value);
287224
+ }
287225
+ }
287226
+ /**
287227
+ * Builds the observer over injected dependencies. The HTTP call and the clock
287228
+ * both arrive from outside, so a test drives the whole policy without a network
287229
+ * or a timer.
287230
+ */
287231
+ function makeClaudeUsageTelemetry(dependencies) {
287232
+ const now = dependencies.now ?? (() => Date.now());
287233
+ /** When each account last had an observation admitted, whatever came of it. */
287234
+ const admittedAt = /* @__PURE__ */ new Map();
287235
+ /** The fingerprint of the last value CMMD accepted, per pair. */
287236
+ const publishedFingerprints = /* @__PURE__ */ new Map();
287237
+ const publishIfNew = (windows, providerAccountId, lookupCredentialGenerationId, at) => gen(function* () {
287238
+ const credentialGenerationId = yield* lookupCredentialGenerationId;
287239
+ if (!credentialGenerationId) return;
287240
+ const attribution = {
287241
+ providerAccountId,
287242
+ credentialGenerationId
287243
+ };
287244
+ const key = pairKey(attribution);
287245
+ const fingerprint = windowsFingerprint(windows);
287246
+ if (publishedFingerprints.get(key) === fingerprint) return;
287247
+ yield* dependencies.publish(claudeUsageTelemetryBody({
287248
+ attribution,
287249
+ source: "passive_sdk_telemetry",
287250
+ windows,
287251
+ observedAt: new Date(at).toISOString()
287252
+ }));
287253
+ remember(publishedFingerprints, key, fingerprint);
287254
+ });
287255
+ /**
287256
+ * The floor is read and claimed inside the returned effect, never while
287257
+ * building it, and before anything suspends. Two turns on one account can be
287258
+ * in flight at once, and the floor only holds them apart if the second one
287259
+ * reads what the first one wrote.
287260
+ */
287261
+ const observe = (observation) => suspend$1(() => {
287262
+ const { windows, providerAccountId } = observation;
287263
+ if (!windows || !providerAccountId) return void_$1;
287264
+ const at = now();
287265
+ const lastAdmittedAt = admittedAt.get(providerAccountId);
287266
+ if (lastAdmittedAt !== void 0 && at - lastAdmittedAt < 5e3) return void_$1;
287267
+ remember(admittedAt, providerAccountId, at);
287268
+ return publishIfNew(windows, providerAccountId, observation.credentialGenerationId, at).pipe(timeout$1(CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS), catchCause((cause) => logWarning$1("claude usage passive telemetry publish failed", {
287269
+ providerAccountId,
287270
+ cause
287271
+ })), asVoid);
287272
+ });
287273
+ return { observe };
286891
287274
  }
286892
287275
  //#endregion
286893
287276
  //#region src/orchestration/Layers/ProviderRuntimeIngestion.ts
@@ -286902,20 +287285,37 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = minutes(120);
286902
287285
  const MAX_BUFFERED_ASSISTANT_CHARS = 24e3;
286903
287286
  const PROVIDER_INGESTION_CONCURRENCY = 8;
286904
287287
  const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.FORGE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
286905
- function decodeClaudeRateLimitWindows(raw) {
287288
+ /**
287289
+ * The SDK's own `rate_limit_info` off one ClaudeAdapter rate-limit event, or
287290
+ * nothing. Both readers below start here: the banner takes the single window
287291
+ * `rateLimitType` names, and passive usage telemetry takes the `unifiedWindows`
287292
+ * sub-object riding along on the same object.
287293
+ */
287294
+ function readClaudeRateLimitInfo(raw) {
286906
287295
  const rateLimitInfo = raw.rate_limit_info;
286907
- if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return [];
287296
+ if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return null;
287297
+ return rateLimitInfo;
287298
+ }
287299
+ function decodeClaudeRateLimitWindows(raw) {
287300
+ const rateLimitInfo = readClaudeRateLimitInfo(raw);
287301
+ if (!rateLimitInfo) return [];
286908
287302
  const mapped = mapClaudeRateLimitInfo(rateLimitInfo);
286909
287303
  return mapped ? [{
286910
287304
  window: mapped.kind === "fiveHour" ? "five_hour" : "seven_day",
286911
287305
  ...mapped.window
286912
287306
  }] : [];
286913
287307
  }
287308
+ /** The raw telemetry object ClaudeAdapter/CodexAdapter wrap as `payload.rateLimits`. */
287309
+ function readRateLimitPayload(event) {
287310
+ if (event.type !== "account.rate-limits.updated") return null;
287311
+ const raw = event.payload.rateLimits;
287312
+ return raw !== null && typeof raw === "object" ? raw : null;
287313
+ }
286914
287314
  /**
286915
287315
  * Codex's app-server protocol shape isn't type-published anywhere this
286916
- * codebase has access to, so it's read defensively - an unrecognized shape
286917
- * is skipped rather than thrown, since this is a best-effort in-chat notice,
286918
- * never load-bearing for the turn itself.
287316
+ * codebase has access to, so it's read defensively. An unrecognized shape
287317
+ * is skipped rather than thrown, since this is a best-effort in-chat notice
287318
+ * that the turn itself never depends on.
286919
287319
  */
286920
287320
  /**
286921
287321
  * Decodes ONE window of the codex app-server `account/rateLimits/updated`
@@ -287228,6 +287628,9 @@ function shouldApplyProviderTurnCompleted(activeTurnId, eventTurnId) {
287228
287628
  }
287229
287629
  const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$1("make")(function* () {
287230
287630
  const orchestrationEngine = yield* OrchestrationEngineService;
287631
+ const claudeUsageTelemetry = yield* ClaudeUsageTelemetry;
287632
+ const claudeUsageScope = yield* make$37("parallel");
287633
+ yield* addFinalizer$2(() => close(claudeUsageScope, void_$3));
287231
287634
  const providerService = yield* ProviderService;
287232
287635
  const projectionTurnRepository = yield* ProjectionTurnRepository;
287233
287636
  const serverSettingsService = yield* ServerSettingsService;
@@ -287252,7 +287655,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287252
287655
  createdAt: ""
287253
287656
  })
287254
287657
  });
287255
- const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$3(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), match(existingIds, {
287658
+ const rememberAssistantMessageId = (threadId, turnId, messageId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(flatMap((existingIds) => set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), match(existingIds, {
287256
287659
  onNone: () => new Set([messageId]),
287257
287660
  onSome: (ids) => {
287258
287661
  const nextIds = new Set(ids);
@@ -287266,7 +287669,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287266
287669
  const nextIds = new Set(ids);
287267
287670
  nextIds.delete(messageId);
287268
287671
  if (nextIds.size === 0) return invalidate(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId));
287269
- return set$3(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds);
287672
+ return set$4(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId), nextIds);
287270
287673
  }
287271
287674
  })));
287272
287675
  const getAssistantMessageIdsForTurn = (threadId, turnId) => getOption(turnMessageIdsByTurnKey, providerTurnKey(threadId, turnId)).pipe(map$3((existingIds) => getOrElse(existingIds, () => /* @__PURE__ */ new Set())));
@@ -287277,7 +287680,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287277
287680
  onSome: (text) => `${text}${delta}`
287278
287681
  });
287279
287682
  if (nextText.length <= MAX_BUFFERED_ASSISTANT_CHARS) {
287280
- yield* set$3(bufferedAssistantTextByMessageId, messageId, nextText);
287683
+ yield* set$4(bufferedAssistantTextByMessageId, messageId, nextText);
287281
287684
  return "";
287282
287685
  }
287283
287686
  yield* invalidate(bufferedAssistantTextByMessageId, messageId);
@@ -287287,7 +287690,7 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287287
287690
  const clearBufferedAssistantText = (messageId) => invalidate(bufferedAssistantTextByMessageId, messageId);
287288
287691
  const appendBufferedProposedPlan = (planId, delta, createdAt) => getOption(bufferedProposedPlanById, planId).pipe(flatMap((existingEntry) => {
287289
287692
  const existing = getOrUndefined(existingEntry);
287290
- return set$3(bufferedProposedPlanById, planId, {
287693
+ return set$4(bufferedProposedPlanById, planId, {
287291
287694
  text: `${existing?.text ?? ""}${delta}`,
287292
287695
  createdAt: existing?.createdAt && existing.createdAt.length > 0 ? existing.createdAt : createdAt
287293
287696
  });
@@ -287635,18 +288038,53 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287635
288038
  * SDKRateLimitInfo, reused from apps/server/src/usage/providerRateLimits.ts
287636
288039
  * (same decoder the Usage page cache uses). Codex's app-server protocol
287637
288040
  * shape isn't type-published anywhere this codebase has access to, so it's
287638
- * read defensively - an unrecognized shape means no window update, not a
287639
- * thrown error, since this is a best-effort in-chat notice, never load-
287640
- * bearing for the turn itself.
288041
+ * read defensively. An unrecognized shape means no window update, not a
288042
+ * thrown error, since this is a best-effort in-chat notice that the turn
288043
+ * itself never depends on.
287641
288044
  */
287642
288045
  function decodeRateLimitWindows(event) {
287643
- if (event.type !== "account.rate-limits.updated") return [];
287644
- const raw = event.payload.rateLimits;
287645
- if (raw === null || typeof raw !== "object") return [];
288046
+ const raw = readRateLimitPayload(event);
288047
+ if (!raw) return [];
287646
288048
  if (event.provider === "claudeAgent") return decodeClaudeRateLimitWindows(raw);
287647
288049
  if (event.provider === "codex") return decodeCodexRateLimitWindows(raw);
287648
288050
  return [];
287649
288051
  }
288052
+ /**
288053
+ * The credential generation a Claude thread's live session runs on, or null.
288054
+ *
288055
+ * It is the delivery identity the readiness guard verified before the
288056
+ * session started, which ProviderService writes into the provider resume
288057
+ * cursor and `listSessions` hands back on the live session. Claude's
288058
+ * delivery identity is `{kind: "credential_generation"}` and carries NO
288059
+ * account field, so it cannot stand in for the account the thread's model
288060
+ * selection names, and neither id is derivable from the other. The usage
288061
+ * observer drops an observation missing either one, because publishing under
288062
+ * a guessed id would write one account's usage onto another account's card.
288063
+ */
288064
+ const claudeCredentialGenerationId = fn$1("claudeCredentialGenerationId")(function* (threadId) {
288065
+ const cursor = (yield* providerService.listSessions()).find((entry) => entry.threadId === threadId && entry.provider === "claudeAgent")?.resumeCursor;
288066
+ const binding = cursor !== null && typeof cursor === "object" ? readProviderDeliveryBinding(cursor.providerDeliveryBinding) : null;
288067
+ return binding?.identity.kind === "credential_generation" ? binding.identity.credentialGenerationId : null;
288068
+ });
288069
+ /**
288070
+ * Publishes the unified windows the same event already carried for the
288071
+ * banner. Passive: no probe, no credential delivery, no woken sprite. The SDK
288072
+ * reports these figures on every turn whether anything reads them or not.
288073
+ * The observer owns the floor, the credential generation lookup, dedupe, the
288074
+ * timeout and the HTTP call, and never fails.
288075
+ */
288076
+ const observeClaudeUsage = fn$1("observeClaudeUsage")(function* (event, thread) {
288077
+ if (event.provider !== "claudeAgent") return;
288078
+ const raw = readRateLimitPayload(event);
288079
+ const rateLimitInfo = raw ? readClaudeRateLimitInfo(raw) : null;
288080
+ const windows = rateLimitInfo ? mapClaudeUnifiedWindows(rateLimitInfo) : null;
288081
+ if (!windows) return;
288082
+ yield* claudeUsageTelemetry.observe({
288083
+ windows,
288084
+ providerAccountId: readProviderAccountId(thread.modelSelection) ?? null,
288085
+ credentialGenerationId: claudeCredentialGenerationId(thread.id)
288086
+ });
288087
+ });
287650
288088
  const handleRateLimitEvent = fn$1("handleRateLimitEvent")(function* (event, thread) {
287651
288089
  const provider = event.provider === "claudeAgent" ? "claude" : event.provider === "codex" ? "codex" : null;
287652
288090
  if (!provider) return;
@@ -287662,6 +288100,10 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287662
288100
  resetsAt: win.resetsAt,
287663
288101
  createdAt: event.createdAt
287664
288102
  }), { discard: true });
288103
+ yield* observeClaudeUsage(event, thread).pipe(catchCause((cause) => hasInterruptsOnly(cause) ? void_$1 : logWarning$1("claude usage observation failed", {
288104
+ eventId: event.eventId,
288105
+ cause
288106
+ })), forkIn(claudeUsageScope));
287665
288107
  });
287666
288108
  const processRuntimeEvent = fn$1("processRuntimeEvent")(function* (event) {
287667
288109
  const thread = (yield* orchestrationEngine.getReadModel()).threads.find((entry) => entry.id === event.threadId);
@@ -287776,6 +288218,44 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287776
288218
  };
287777
288219
  })()).pipe(provide$1(ProjectionTurnRepositoryLive));
287778
288220
  //#endregion
288221
+ //#region src/usage/Layers/ClaudeUsageTelemetry.ts
288222
+ /**
288223
+ * The real publish behind `ClaudeUsageTelemetry`: the owner's CMMD credential
288224
+ * and one POST to `CLAUDE_USAGE_TELEMETRY_PATH`.
288225
+ *
288226
+ * The call always goes out on the owner session credential, never on a
288227
+ * runtime grant.
288228
+ *
288229
+ * A machine with nobody signed in, or one whose CMMD credential cannot be
288230
+ * minted, fails the publish rather than reporting success. The observer only
288231
+ * remembers a value it managed to send, so a quiet success here would suppress
288232
+ * the publish for as long as that reading stayed the same, which on a stable
288233
+ * account is hours.
288234
+ *
288235
+ * @module ClaudeUsageTelemetryLive
288236
+ */
288237
+ const LOG_PREFIX = "claude-usage-passive-telemetry";
288238
+ const ClaudeUsageTelemetryLive = effect(ClaudeUsageTelemetry, gen(function* () {
288239
+ const sql = yield* SqlClient;
288240
+ const publish = (body) => gen(function* () {
288241
+ const credential = yield* resolveCmmdOwnerSessionCredential(sql, { logPrefix: LOG_PREFIX });
288242
+ if (credential.kind !== "credential") return yield* new ClaudeUsageTelemetryPublishError({ reason: credential.kind });
288243
+ yield* tryPromise({
288244
+ try: () => cmmdGateway.request({
288245
+ method: "POST",
288246
+ jwt: credential.jwt,
288247
+ path: CLAUDE_USAGE_TELEMETRY_PATH,
288248
+ body
288249
+ }),
288250
+ catch: (cause) => new ClaudeUsageTelemetryPublishError({
288251
+ reason: "request-failed",
288252
+ cause
288253
+ })
288254
+ });
288255
+ });
288256
+ return makeClaudeUsageTelemetry({ publish });
288257
+ }));
288258
+ //#endregion
287779
288259
  //#region ../../packages/shared/src/DrainableWorker.ts
287780
288260
  /**
287781
288261
  * Create a drainable worker that processes items from an unbounded queue.
@@ -288088,7 +288568,7 @@ const ProviderCommandReactorLive = effect(ProviderCommandReactor, gen(function*
288088
288568
  timeToLive: HANDLED_TURN_START_KEY_TTL,
288089
288569
  lookup: () => succeed(true)
288090
288570
  });
288091
- const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$3(handledTurnStartKeys, key, true).pipe(as$3(isSome(cached)))));
288571
+ const hasHandledTurnStartRecently = (key) => getOption(handledTurnStartKeys, key).pipe(flatMap((cached) => set$4(handledTurnStartKeys, key, true).pipe(as$3(isSome(cached)))));
288092
288572
  const threadModelSelections = /* @__PURE__ */ new Map();
288093
288573
  /**
288094
288574
  * The providerAccountId each LIVE provider session was actually started with.
@@ -291931,6 +292411,382 @@ const WorkspaceFileSystemLive = effect(WorkspaceFileSystem, gen(function* () {
291931
292411
  }) };
291932
292412
  }));
291933
292413
  //#endregion
292414
+ //#region src/workspace/Layers/WorkspaceBulkTransfer.ts
292415
+ const TRANSFER_TTL_MS = 1440 * 60 * 1e3;
292416
+ const TRANSFER_LOCK_STRIPE_COUNT = 64;
292417
+ function transferError(input) {
292418
+ return new BulkTransferError({
292419
+ code: input.code,
292420
+ message: input.message,
292421
+ transferId: input.transferId ?? null,
292422
+ retryable: input.retryable ?? false,
292423
+ ...input.cause === void 0 ? {} : { cause: input.cause }
292424
+ });
292425
+ }
292426
+ function mapFailure(operation, transferId) {
292427
+ return (cause) => is$1(BulkTransferError)(cause) ? cause : transferError({
292428
+ code: "io_error",
292429
+ message: `Workspace bulk transfer failed during ${operation}.`,
292430
+ ...transferId === void 0 ? {} : { transferId },
292431
+ retryable: true,
292432
+ cause
292433
+ });
292434
+ }
292435
+ function transferLockStripe(transferId) {
292436
+ let hash = 2166136261;
292437
+ for (let index = 0; index < transferId.length; index += 1) hash = Math.imul(hash ^ transferId.charCodeAt(index), 16777619);
292438
+ return (hash >>> 0) % TRANSFER_LOCK_STRIPE_COUNT;
292439
+ }
292440
+ const makeWorkspaceBulkTransfer = (options = {}) => gen(function* () {
292441
+ const fileSystem = yield* FileSystem;
292442
+ const path = yield* Path$2;
292443
+ const sql = yield* SqlClient;
292444
+ const workspacePaths = yield* WorkspacePaths;
292445
+ const workspaceEntries = yield* WorkspaceEntries;
292446
+ const fault = options.fault ?? (() => void_$1);
292447
+ const transferLocks = yield* forEach(Array.from({ length: TRANSFER_LOCK_STRIPE_COUNT }), () => make$43(1));
292448
+ const withTransferLock = (transferId, effect) => transferLocks[transferLockStripe(transferId)].withPermit(effect);
292449
+ const findTransfer = (transferId) => sql`
292450
+ SELECT transfer_id AS "transferId", workspace_root AS "workspaceRoot",
292451
+ relative_path AS "relativePath", target_path AS "targetPath", temp_path AS "tempPath",
292452
+ total_bytes AS "totalBytes", whole_sha256 AS "wholeSha256",
292453
+ next_offset AS "nextOffset", state, expires_at AS "expiresAt"
292454
+ FROM workspace_bulk_transfers WHERE transfer_id = ${transferId} LIMIT 1
292455
+ `.pipe(map$3((rows) => rows[0] ?? null));
292456
+ const requireTransfer = (transferId) => findTransfer(transferId).pipe(flatMap((row) => row === null ? fail(transferError({
292457
+ code: "transfer_not_found",
292458
+ message: "Bulk transfer does not exist.",
292459
+ transferId
292460
+ })) : succeed(row)));
292461
+ const requireOpenTransfer = (transferId) => requireTransfer(transferId).pipe(flatMap((row) => {
292462
+ if (row.state === "cancelled") return fail(transferError({
292463
+ code: "transfer_cancelled",
292464
+ message: "Bulk transfer was cancelled.",
292465
+ transferId
292466
+ }));
292467
+ if (row.state === "expired" || Date.parse(row.expiresAt) <= Date.now()) return fail(transferError({
292468
+ code: "transfer_expired",
292469
+ message: "Bulk transfer expired.",
292470
+ transferId
292471
+ }));
292472
+ return succeed(row);
292473
+ }));
292474
+ const resolveTarget = fn$1("WorkspaceBulkTransfer.resolveTarget")(function* (input) {
292475
+ const workspaceRoot = yield* workspacePaths.normalizeWorkspaceRoot(input.cwd);
292476
+ const resolved = yield* workspacePaths.resolveRelativePathWithinRoot({
292477
+ workspaceRoot,
292478
+ relativePath: input.relativePath
292479
+ });
292480
+ const canonicalRoot = yield* fileSystem.realPath(workspaceRoot);
292481
+ const parentSegments = resolved.relativePath.split("/").slice(0, -1);
292482
+ let canonicalParent = canonicalRoot;
292483
+ for (const segment of parentSegments) {
292484
+ const candidate = path.join(canonicalParent, segment);
292485
+ const canonicalCandidate = yield* fileSystem.realPath(candidate).pipe(catch_(() => input.createParent ? fileSystem.makeDirectory(candidate).pipe(flatMap(() => fileSystem.realPath(candidate))) : fileSystem.realPath(candidate)));
292486
+ if (!isPathWithinRoot(path, canonicalRoot, canonicalCandidate)) return yield* transferError({
292487
+ code: "symlink_escape",
292488
+ message: "Workspace file path resolves outside the project root."
292489
+ });
292490
+ canonicalParent = canonicalCandidate;
292491
+ }
292492
+ const targetPath = path.join(canonicalParent, path.basename(resolved.relativePath));
292493
+ const targetRealPath = yield* fileSystem.realPath(targetPath).pipe(catch_(() => succeed(null)));
292494
+ if (targetRealPath !== null && !isPathWithinRoot(path, canonicalRoot, targetRealPath)) return yield* transferError({
292495
+ code: "symlink_escape",
292496
+ message: "Workspace file target resolves outside the project root."
292497
+ });
292498
+ return {
292499
+ workspaceRoot: canonicalRoot,
292500
+ relativePath: resolved.relativePath,
292501
+ targetPath,
292502
+ canonicalParent
292503
+ };
292504
+ });
292505
+ const readFile = (input) => unwrap$1(gen(function* () {
292506
+ const target = yield* resolveTarget({
292507
+ ...input,
292508
+ createParent: false
292509
+ });
292510
+ const info = yield* fileSystem.stat(target.targetPath);
292511
+ const maxBytes = input.maxBytes ?? 16777216;
292512
+ if (Number(info.size) > maxBytes) return yield* transferError({
292513
+ code: "size_limit_exceeded",
292514
+ message: `Workspace file exceeds the ${maxBytes}-byte read limit.`
292515
+ });
292516
+ const bytes = yield* fileSystem.readFile(target.targetPath);
292517
+ const transfer = yield* promise(() => chunkBulkTransfer(bytes));
292518
+ const transferId = transfer.chunks[0]?.transferId ?? crypto.randomUUID().replaceAll("-", "_");
292519
+ return fromIterable$1([
292520
+ {
292521
+ kind: "started",
292522
+ transferId,
292523
+ totalBytes: transfer.totalBytes,
292524
+ sha256: transfer.sha256
292525
+ },
292526
+ ...transfer.chunks.map((chunk) => ({
292527
+ ...chunk,
292528
+ transferId
292529
+ })),
292530
+ {
292531
+ kind: "completed",
292532
+ transferId,
292533
+ totalBytes: transfer.totalBytes,
292534
+ sha256: transfer.sha256
292535
+ }
292536
+ ]);
292537
+ }).pipe(mapError(mapFailure("read file"))));
292538
+ const resumeWrite = fn$1("WorkspaceBulkTransfer.resumeWrite")(function* (input, existing) {
292539
+ const target = yield* resolveTarget({
292540
+ ...input,
292541
+ createParent: true
292542
+ });
292543
+ if (!(existing.workspaceRoot === target.workspaceRoot && existing.relativePath === target.relativePath && existing.totalBytes === input.totalBytes && existing.wholeSha256 === input.sha256)) return yield* transferError({
292544
+ code: "transfer_identity_conflict",
292545
+ message: "Bulk transfer ID is already bound to different content or a different path.",
292546
+ transferId: input.transferId
292547
+ });
292548
+ if (existing.state === "cancelled") return yield* transferError({
292549
+ code: "transfer_cancelled",
292550
+ message: "Bulk transfer was cancelled.",
292551
+ transferId: input.transferId
292552
+ });
292553
+ if (existing.state === "expired" || Date.parse(existing.expiresAt) <= Date.now()) return yield* transferError({
292554
+ code: "transfer_expired",
292555
+ message: "Bulk transfer expired.",
292556
+ transferId: input.transferId
292557
+ });
292558
+ return {
292559
+ transferId: input.transferId,
292560
+ nextOffset: existing.nextOffset,
292561
+ state: existing.state === "completed" ? "completed" : "open"
292562
+ };
292563
+ });
292564
+ const createWrite = fn$1("WorkspaceBulkTransfer.createWrite")(function* (input) {
292565
+ const target = yield* resolveTarget({
292566
+ ...input,
292567
+ createParent: true
292568
+ }).pipe(mapError((cause) => is$1(WorkspacePathOutsideRootError)(cause) ? transferError({
292569
+ code: "path_outside_workspace",
292570
+ message: "Workspace file path must stay within the project root.",
292571
+ transferId: input.transferId,
292572
+ cause
292573
+ }) : cause));
292574
+ const tempPath = path.join(target.canonicalParent, `.forge-transfer-${input.transferId}.part`);
292575
+ yield* scoped(fileSystem.open(tempPath, {
292576
+ flag: "wx",
292577
+ mode: 384
292578
+ }).pipe(flatMap((file) => file.sync), catch_(() => fileSystem.stat(tempPath).pipe(flatMap((stat) => Number(stat.size) === 0 ? void_$1 : fail(transferError({
292579
+ code: "transfer_identity_conflict",
292580
+ message: "Bulk transfer temporary path already contains data.",
292581
+ transferId: input.transferId
292582
+ })))))));
292583
+ const now = /* @__PURE__ */ new Date();
292584
+ yield* sql`
292585
+ INSERT INTO workspace_bulk_transfers (
292586
+ transfer_id, workspace_root, relative_path, target_path, temp_path, total_bytes,
292587
+ whole_sha256, next_offset, state, expires_at, created_at, updated_at
292588
+ ) VALUES (
292589
+ ${input.transferId}, ${target.workspaceRoot}, ${target.relativePath}, ${target.targetPath},
292590
+ ${tempPath}, ${input.totalBytes}, ${input.sha256}, 0, 'open',
292591
+ ${new Date(now.getTime() + TRANSFER_TTL_MS).toISOString()}, ${now.toISOString()},
292592
+ ${now.toISOString()}
292593
+ )
292594
+ `;
292595
+ return {
292596
+ transferId: input.transferId,
292597
+ nextOffset: 0,
292598
+ state: "open"
292599
+ };
292600
+ });
292601
+ const beginWriteEffect = fn$1("WorkspaceBulkTransfer.beginWrite")(function* (input) {
292602
+ const existing = yield* findTransfer(input.transferId);
292603
+ return yield* existing === null ? createWrite(input) : resumeWrite(input, existing);
292604
+ });
292605
+ const beginWrite = (input) => withTransferLock(input.transferId, beginWriteEffect(input)).pipe(mapError(mapFailure("begin write", input.transferId)));
292606
+ const acceptRetriedChunk = fn$1("WorkspaceBulkTransfer.acceptRetriedChunk")(function* (input, row) {
292607
+ const accepted = (yield* sql`
292608
+ SELECT chunk_offset AS "chunkOffset", chunk_bytes AS "chunkBytes",
292609
+ chunk_sha256 AS "chunkSha256"
292610
+ FROM workspace_bulk_transfer_chunks
292611
+ WHERE transfer_id = ${input.transferId} AND chunk_offset = ${input.offset}
292612
+ LIMIT 1
292613
+ `)[0];
292614
+ if (!(accepted !== void 0 && accepted.chunkBytes === input.bytes && accepted.chunkSha256 === input.sha256)) return yield* transferError({
292615
+ code: "integrity_mismatch",
292616
+ message: "Retried bulk transfer chunk differs from the accepted chunk.",
292617
+ transferId: input.transferId
292618
+ });
292619
+ return {
292620
+ transferId: input.transferId,
292621
+ nextOffset: row.nextOffset,
292622
+ duplicate: true
292623
+ };
292624
+ });
292625
+ const validateChunkRange = fn$1("WorkspaceBulkTransfer.validateChunkRange")(function* (input, row, byteLength) {
292626
+ if (input.offset !== row.nextOffset) return yield* transferError({
292627
+ code: "offset_mismatch",
292628
+ message: `Expected chunk offset ${row.nextOffset}, received ${input.offset}.`,
292629
+ transferId: input.transferId,
292630
+ retryable: true
292631
+ });
292632
+ const nextOffset = input.offset + byteLength;
292633
+ if (nextOffset > row.totalBytes) return yield* transferError({
292634
+ code: "size_limit_exceeded",
292635
+ message: "Bulk transfer chunk exceeds the declared total byte count.",
292636
+ transferId: input.transferId
292637
+ });
292638
+ return nextOffset;
292639
+ });
292640
+ const reconcileChunkBytes = fn$1("WorkspaceBulkTransfer.reconcileChunkBytes")(function* (input, row, bytes, nextOffset) {
292641
+ const tempInfo = yield* fileSystem.stat(row.tempPath);
292642
+ if (Number(tempInfo.size) > row.nextOffset) {
292643
+ if (Number(tempInfo.size) !== nextOffset) return yield* transferError({
292644
+ code: "integrity_mismatch",
292645
+ message: "Bulk transfer temporary file cannot be reconciled with the next chunk.",
292646
+ transferId: input.transferId
292647
+ });
292648
+ const recovered = (yield* fileSystem.readFile(row.tempPath)).slice(input.offset, nextOffset);
292649
+ if ((yield* promise(() => sha256Hex(recovered))) !== input.sha256) return yield* transferError({
292650
+ code: "integrity_mismatch",
292651
+ message: "Bulk transfer temporary bytes differ from the retried chunk.",
292652
+ transferId: input.transferId
292653
+ });
292654
+ return;
292655
+ }
292656
+ yield* fault("before-byte-write");
292657
+ yield* scoped(fileSystem.open(row.tempPath, { flag: "r+" }).pipe(flatMap((file) => file.seek(input.offset, "start").pipe(flatMap(() => file.writeAll(bytes)), flatMap(() => file.sync)))));
292658
+ yield* fault("after-byte-write-before-metadata");
292659
+ });
292660
+ const persistAcceptedChunk = fn$1("WorkspaceBulkTransfer.persistAcceptedChunk")(function* (input, nextOffset) {
292661
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
292662
+ yield* sql.withTransaction(sql`
292663
+ INSERT INTO workspace_bulk_transfer_chunks (
292664
+ transfer_id, chunk_offset, chunk_bytes, chunk_sha256
292665
+ ) VALUES (${input.transferId}, ${input.offset}, ${input.bytes}, ${input.sha256})
292666
+ ON CONFLICT DO NOTHING
292667
+ `.pipe(flatMap(() => sql`
292668
+ UPDATE workspace_bulk_transfers
292669
+ SET next_offset = ${nextOffset}, updated_at = ${updatedAt}
292670
+ WHERE transfer_id = ${input.transferId} AND next_offset = ${input.offset} AND state = 'open'
292671
+ `)));
292672
+ yield* fault("after-metadata-advance");
292673
+ });
292674
+ const writeChunkEffect = fn$1("WorkspaceBulkTransfer.writeChunk")(function* (input) {
292675
+ const bytes = yield* promise(() => verifyBulkTransferChunk({
292676
+ ...input,
292677
+ kind: "chunk"
292678
+ }));
292679
+ const row = yield* requireOpenTransfer(input.transferId);
292680
+ if (row.state === "completed") return yield* transferError({
292681
+ code: "offset_mismatch",
292682
+ message: "Bulk transfer is already complete.",
292683
+ transferId: input.transferId
292684
+ });
292685
+ if (input.offset < row.nextOffset) return yield* acceptRetriedChunk(input, row);
292686
+ const nextOffset = yield* validateChunkRange(input, row, bytes.byteLength);
292687
+ yield* reconcileChunkBytes(input, row, bytes, nextOffset);
292688
+ yield* persistAcceptedChunk(input, nextOffset);
292689
+ return {
292690
+ transferId: input.transferId,
292691
+ nextOffset,
292692
+ duplicate: false
292693
+ };
292694
+ });
292695
+ const writeChunk = (input) => withTransferLock(input.transferId, writeChunkEffect(input)).pipe(mapError(mapFailure("write chunk", input.transferId)));
292696
+ const commitWriteEffect = fn$1("WorkspaceBulkTransfer.commitWrite")(function* (input) {
292697
+ const row = yield* requireOpenTransfer(input.transferId);
292698
+ if (row.state === "completed") return {
292699
+ transferId: row.transferId,
292700
+ relativePath: row.relativePath,
292701
+ totalBytes: row.totalBytes,
292702
+ sha256: row.wholeSha256
292703
+ };
292704
+ if (row.nextOffset !== row.totalBytes) return yield* transferError({
292705
+ code: "offset_mismatch",
292706
+ message: `Bulk transfer is incomplete at byte ${row.nextOffset} of ${row.totalBytes}.`,
292707
+ transferId: row.transferId,
292708
+ retryable: true
292709
+ });
292710
+ if ((yield* resolveTarget({
292711
+ cwd: row.workspaceRoot,
292712
+ relativePath: row.relativePath,
292713
+ createParent: false
292714
+ })).targetPath !== row.targetPath) return yield* transferError({
292715
+ code: "symlink_escape",
292716
+ message: "Workspace file destination changed during transfer.",
292717
+ transferId: row.transferId
292718
+ });
292719
+ const tempInfo = yield* fileSystem.stat(row.tempPath).pipe(catch_(() => succeed(null)));
292720
+ const candidatePath = tempInfo === null ? row.targetPath : row.tempPath;
292721
+ const candidate = yield* fileSystem.readFile(candidatePath);
292722
+ if (candidate.byteLength !== row.totalBytes || (yield* promise(() => sha256Hex(candidate))) !== row.wholeSha256) return yield* transferError({
292723
+ code: "integrity_mismatch",
292724
+ message: "Bulk transfer payload does not match its declared size and digest.",
292725
+ transferId: row.transferId
292726
+ });
292727
+ if (tempInfo !== null) {
292728
+ yield* fileSystem.rename(row.tempPath, row.targetPath);
292729
+ yield* fault("after-atomic-rename");
292730
+ }
292731
+ yield* sql`
292732
+ UPDATE workspace_bulk_transfers SET state = 'completed', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
292733
+ WHERE transfer_id = ${row.transferId}
292734
+ `;
292735
+ yield* workspaceEntries.invalidate(row.workspaceRoot);
292736
+ return {
292737
+ transferId: row.transferId,
292738
+ relativePath: row.relativePath,
292739
+ totalBytes: row.totalBytes,
292740
+ sha256: row.wholeSha256
292741
+ };
292742
+ });
292743
+ const commitWrite = (input) => withTransferLock(input.transferId, commitWriteEffect(input)).pipe(mapError(mapFailure("commit write", input.transferId)));
292744
+ const cancelWriteEffect = fn$1("WorkspaceBulkTransfer.cancelWrite")(function* (input) {
292745
+ const row = yield* requireTransfer(input.transferId);
292746
+ if (row.state !== "completed") {
292747
+ yield* fileSystem.remove(row.tempPath, { force: true });
292748
+ yield* sql`
292749
+ UPDATE workspace_bulk_transfers SET state = 'cancelled', updated_at = ${(/* @__PURE__ */ new Date()).toISOString()}
292750
+ WHERE transfer_id = ${input.transferId}
292751
+ `;
292752
+ }
292753
+ return {
292754
+ transferId: input.transferId,
292755
+ state: row.state === "completed" ? "completed" : "cancelled"
292756
+ };
292757
+ });
292758
+ const cancelWrite = (input) => withTransferLock(input.transferId, cancelWriteEffect(input)).pipe(mapError(mapFailure("cancel write", input.transferId)));
292759
+ const cleanupExpiredEffect = fn$1("WorkspaceBulkTransfer.cleanupExpired")(function* () {
292760
+ const now = (/* @__PURE__ */ new Date()).toISOString();
292761
+ const rows = yield* sql`
292762
+ SELECT transfer_id AS "transferId", temp_path AS "tempPath"
292763
+ FROM workspace_bulk_transfers WHERE state = 'open' AND expires_at <= ${now}
292764
+ `;
292765
+ let expiredCount = 0;
292766
+ for (const row of rows) if (yield* withTransferLock(row.transferId, gen(function* () {
292767
+ const current = yield* findTransfer(row.transferId);
292768
+ if (current === null || current.state !== "open" || Date.parse(current.expiresAt) > Date.parse(now)) return false;
292769
+ yield* fileSystem.remove(current.tempPath, { force: true });
292770
+ yield* sql`
292771
+ UPDATE workspace_bulk_transfers SET state = 'expired', updated_at = ${now}
292772
+ WHERE transfer_id = ${row.transferId} AND state = 'open' AND expires_at <= ${now}
292773
+ `;
292774
+ return true;
292775
+ }))) expiredCount += 1;
292776
+ return expiredCount;
292777
+ });
292778
+ const cleanupExpired = () => cleanupExpiredEffect().pipe(mapError(mapFailure("expiry cleanup")));
292779
+ return {
292780
+ readFile,
292781
+ beginWrite,
292782
+ writeChunk,
292783
+ commitWrite,
292784
+ cancelWrite,
292785
+ cleanupExpired
292786
+ };
292787
+ });
292788
+ const WorkspaceBulkTransferLive = effect(WorkspaceBulkTransfer, makeWorkspaceBulkTransfer());
292789
+ //#endregion
291934
292790
  //#region src/workspace/Layers/WorkspacePaths.ts
291935
292791
  function toPosixRelativePath(input) {
291936
292792
  return input.replaceAll("\\", "/");
@@ -293524,7 +294380,7 @@ function resolveBuildCommitFromEnv(env) {
293524
294380
  * environment descriptor down instead of reporting an honest "unknown".
293525
294381
  */
293526
294382
  function readBakedBuildCommit() {
293527
- return "7f0d765bc59cb837f8aa8025b9b2f855df3fd36d";
294383
+ return "b79da36c868d5629c76dcec88a3326634a02c2c7";
293528
294384
  }
293529
294385
  async function resolveServerBuildCommit(input) {
293530
294386
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -293732,7 +294588,7 @@ const ServerEnvironmentLive = effect(ServerEnvironment, fn$1("makeServerEnvironm
293732
294588
  updateDisplayName: (displayName) => gen(function* () {
293733
294589
  const normalized = normalizeDisplayName(displayName);
293734
294590
  yield* writeMetadata({ displayName: normalized }).pipe(orDie);
293735
- yield* set$2(displayNameOverrideRef, normalized);
294591
+ yield* set$3(displayNameOverrideRef, normalized);
293736
294592
  const descriptor = yield* buildDescriptor;
293737
294593
  yield* publish(changesPubSub, descriptor);
293738
294594
  return descriptor;
@@ -310647,7 +311503,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
310647
311503
  contextHash: preparedHandoff.authority.contextHash,
310648
311504
  contextSnapshot: snapshot.run.contextSnapshot
310649
311505
  }).pipe(mapError((cause) => coordinatorError("registerHandoff", "PIPELINE_CONTEXT_ARTIFACT_FAILED", "The immutable CMMD run context could not be materialized for the provider.", cause)));
310650
- const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$2(acceptingHeartbeatRef, false).pipe(andThen(get$6(preparedAuthorityRef))));
311506
+ const latestPreparedAuthority = yield* heartbeatPublicationSemaphore.withPermits(1)(set$3(acceptingHeartbeatRef, false).pipe(andThen(get$6(preparedAuthorityRef))));
310651
311507
  const persistedAuthority = yield* persistAuthoritySecret({
310652
311508
  runId: input.runId,
310653
311509
  organizationId: latestPreparedAuthority.authority.organizationId,
@@ -310734,7 +311590,7 @@ const PipelineRunCoordinatorLive = effect(PipelineRunCoordinator, gen(function*
310734
311590
  });
310735
311591
  yield* heartbeatPublicationSemaphore.withPermits(1)(gen(function* () {
310736
311592
  if (!(yield* get$6(acceptingHeartbeatRef))) return;
310737
- yield* set$2(preparedAuthorityRef, {
311593
+ yield* set$3(preparedAuthorityRef, {
310738
311594
  authority: heartbeat.refreshedAuthority,
310739
311595
  preparation
310740
311596
  });
@@ -315508,7 +316364,7 @@ const WorkspaceReadinessStateLive = effect(WorkspaceReadinessState, gen(function
315508
316364
  const slot = yield* getOrCreateSlot(userId);
315509
316365
  const previous = yield* get$6(slot.snapshotRef);
315510
316366
  if (previous !== null && event.ts < previous.ts) return;
315511
- yield* set$2(slot.snapshotRef, event);
316367
+ yield* set$3(slot.snapshotRef, event);
315512
316368
  yield* publish(slot.pubsub, event);
315513
316369
  if (event.phase === "app-ready") yield* schedulePrune(userId, slot);
315514
316370
  });
@@ -316008,7 +316864,7 @@ function claimClaudeReadinessJti(input) {
316008
316864
  return "claimed";
316009
316865
  }
316010
316866
  const readUsableClaudeGenerationHash = (config) => {
316011
- const configuredGenerationHash = process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
316867
+ const configuredGenerationHash = bootExportedPersonalClaudeGenerationHash();
316012
316868
  if (!/^[a-f0-9]{64}$/.test(configuredGenerationHash)) return succeed(null);
316013
316869
  return tryPromise(() => readFile$1(resolveLocalClaudeCredentialPath(config.forgeClaudeConfigPath), "utf8")).pipe(map$3((credentialJson) => isClaudeSubscriptionCredential(credentialJson) ? configuredGenerationHash : null), orElseSucceed(() => null));
316014
316870
  };
@@ -316523,7 +317379,7 @@ const PlatformServicesLive = unwrap(gen(function* () {
316523
317379
  }));
316524
317380
  const RuntimeReplicationLayerLive = RuntimeReplicationLive.pipe(provideMerge(RuntimeReplicationClientLive), provideMerge(RuntimeReplicationOutboxLive), provideMerge(ActiveHourMeterLive.pipe(provide$1(RuntimeReplicationOutboxLive))));
316525
317381
  const RuntimeDrainLayerLive = RuntimeDrainReporterLive.pipe(provideMerge(RuntimeDrainStateLive));
316526
- const RuntimeAwareProviderReactorsLive = mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(provide$1(RuntimeDrainLayerLive));
317382
+ const RuntimeAwareProviderReactorsLive = mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(provide$1(RuntimeDrainLayerLive), provide$1(ClaudeUsageTelemetryLive));
316527
317383
  const PipelineWorkerCoordinatorLayerLive = PipelineWorkerCoordinatorLive.pipe(provideMerge(PipelineRunClientLive));
316528
317384
  const AgentProtocolReactorLayerLive = AgentProtocolReactorLive.pipe(provideMerge(PipelineRunStoreLive));
316529
317385
  const PipelineRunCoordinatorLayerLive = PipelineRunCoordinatorLive.pipe(provideMerge(PipelineProviderAuthorityInstallerLive), provideMerge(PipelineChangeInspectorLive), provideMerge(PipelineContextArtifactStoreLive), provideMerge(PipelineModelPolicyLive), provideMerge(PipelineRunClientLive), provideMerge(PipelineRunStoreLive), provideMerge(PipelineRunWorkspaceAllocatorLive), provideMerge(PipelineStagePolicyLive), provideMerge(PipelineWorkerCoordinatorLayerLive));