@cmmd-center/forge 0.10.16 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -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
  /**
@@ -65495,7 +65495,7 @@ function normalizeNumberish(value) {
65495
65495
  }
65496
65496
  //#endregion
65497
65497
  //#region package.json
65498
- var version$1 = "0.10.16";
65498
+ var version$1 = "0.11.0";
65499
65499
  //#endregion
65500
65500
  //#region src/sentry.ts
65501
65501
  const SERVER_APP_NAME = "forge-server";
@@ -92650,7 +92650,7 @@ var AnalyticsService = class AnalyticsService extends Service$1()("forge/telemet
92650
92650
  var RuntimeReplication = class extends Service$1()("forge/runtimeReplication/Services/RuntimeReplication") {};
92651
92651
  //#endregion
92652
92652
  //#region src/runtimeDrain/forgeGitCommit.ts
92653
- const execFileAsync$6 = promisify$1(execFile);
92653
+ const execFileAsync$7 = promisify$1(execFile);
92654
92654
  const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
92655
92655
  /**
92656
92656
  * Resolve the commit this forge checkout actually booted from, so the
@@ -92674,7 +92674,7 @@ const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
92674
92674
  * it yet.
92675
92675
  */
92676
92676
  let cached$1 = null;
92677
- async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$6(command, [...args], { timeout: 5e3 })) {
92677
+ async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$7(command, [...args], { timeout: 5e3 })) {
92678
92678
  if (cached$1 !== null) return cached$1;
92679
92679
  try {
92680
92680
  const { stdout } = await execImpl("git", ["rev-parse", "HEAD"]);
@@ -94606,7 +94606,7 @@ function mightCarryUsage(line, provider) {
94606
94606
  * a line at a time, so the usual cause is a tail the scan opened mid-write,
94607
94607
  * and a count that keeps growing is a file the scan can no longer read.
94608
94608
  */
94609
- function parseJsonObject$1(line, diagnostics) {
94609
+ function parseJsonObject$2(line, diagnostics) {
94610
94610
  let parsed;
94611
94611
  try {
94612
94612
  parsed = JSON.parse(line);
@@ -94636,7 +94636,7 @@ function claudeDedupeKey(messageId, requestId) {
94636
94636
  return `${messageId ?? ""}:${requestId ?? ""}`;
94637
94637
  }
94638
94638
  function parseClaudeLine(line, diagnostics) {
94639
- const record = parseJsonObject$1(line, diagnostics);
94639
+ const record = parseJsonObject$2(line, diagnostics);
94640
94640
  if (record === null) return null;
94641
94641
  if (record["type"] !== "assistant") return null;
94642
94642
  const messageRecord = objectAt(record, "message");
@@ -94783,7 +94783,7 @@ function parseCodexTokenCount(record, payloadRecord, state) {
94783
94783
  };
94784
94784
  }
94785
94785
  function parseCodexLine(line, state, diagnostics) {
94786
- const record = parseJsonObject$1(line, diagnostics);
94786
+ const record = parseJsonObject$2(line, diagnostics);
94787
94787
  if (record === null) return null;
94788
94788
  const payloadRecord = objectAt(record, "payload");
94789
94789
  if (payloadRecord === null) return null;
@@ -99160,7 +99160,7 @@ function buildDiscoveryResult(probes) {
99160
99160
  }
99161
99161
  //#endregion
99162
99162
  //#region src/sourceControl/probe.ts
99163
- const execFileAsync$5 = promisify$1(execFile);
99163
+ const execFileAsync$6 = promisify$1(execFile);
99164
99164
  /**
99165
99165
  * How long a machine's toolchain answer stays good for.
99166
99166
  *
@@ -99171,7 +99171,7 @@ const execFileAsync$5 = promisify$1(execFile);
99171
99171
  */
99172
99172
  const DISCOVERY_TTL = "5 minutes";
99173
99173
  /** Probes time out because `gh auth status` reaches the network on a cold keyring. */
99174
- const PROBE_TIMEOUT_MS = 5e3;
99174
+ const PROBE_TIMEOUT_MS$1 = 5e3;
99175
99175
  /**
99176
99176
  * Runs one probe. A tool that is absent, fails, or hangs is reported the same
99177
99177
  * way — `null`, meaning "no answer" — because the discovery result only
@@ -99183,7 +99183,7 @@ const PROBE_TIMEOUT_MS = 5e3;
99183
99183
  */
99184
99184
  const probe = (command, args) => promise(async () => {
99185
99185
  try {
99186
- const { stdout, stderr } = await execFileAsync$5(command, [...args], { timeout: PROBE_TIMEOUT_MS });
99186
+ const { stdout, stderr } = await execFileAsync$6(command, [...args], { timeout: PROBE_TIMEOUT_MS$1 });
99187
99187
  const output = `${stdout}\n${stderr}`.trim();
99188
99188
  return { output: output.length === 0 ? null : output };
99189
99189
  } catch (cause) {
@@ -100720,6 +100720,7 @@ const CLAUDE_IDENTITY_KEYS = new Set([
100720
100720
  "provider",
100721
100721
  "credentialGenerationId"
100722
100722
  ]);
100723
+ const CLAUDE_IDENTITY_OPTIONAL_KEYS = new Set(["providerAccountIdentity"]);
100723
100724
  function isRecord$13(value) {
100724
100725
  return typeof value === "object" && value !== null && !Array.isArray(value);
100725
100726
  }
@@ -100744,6 +100745,10 @@ function daysInMonth$1(year, month) {
100744
100745
  function hasExactKeys$2(value, keys) {
100745
100746
  return Object.keys(value).every((key) => keys.has(key)) && Object.keys(value).length === keys.size;
100746
100747
  }
100748
+ function hasAllowedKeys(value, required, optional) {
100749
+ const keys = Object.keys(value);
100750
+ return keys.every((key) => required.has(key) || optional.has(key)) && [...required].every((key) => keys.includes(key));
100751
+ }
100747
100752
  function parseDeliveryIdentity(value, provider) {
100748
100753
  if (!isRecord$13(value)) return null;
100749
100754
  if (provider === "openai") return parseCodexDeliveryIdentity(value);
@@ -100758,12 +100763,25 @@ function parseCodexDeliveryIdentity(value) {
100758
100763
  providerAccountIdentity: value.providerAccountIdentity
100759
100764
  };
100760
100765
  }
100766
+ /**
100767
+ * Absent means the writer does not name the Provider Account yet, and the
100768
+ * identity keeps the shape it has always had. Present but malformed means the
100769
+ * identity is not the one CMMD signed, so it is refused the same way a
100770
+ * malformed Codex account identity is.
100771
+ */
100772
+ function claudeAccountFields(value) {
100773
+ if (value === void 0) return {};
100774
+ if (typeof value !== "string" || !PROVIDER_ACCOUNT_IDENTITY$2.test(value)) return null;
100775
+ return { providerAccountIdentity: value };
100776
+ }
100761
100777
  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;
100778
+ const account = claudeAccountFields(value.providerAccountIdentity);
100779
+ 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
100780
  return {
100764
100781
  kind: "credential_generation",
100765
100782
  provider: "claude",
100766
- credentialGenerationId: value.credentialGenerationId
100783
+ credentialGenerationId: value.credentialGenerationId,
100784
+ ...account
100767
100785
  };
100768
100786
  }
100769
100787
  function parseProviderRuntimeDeliveryReceipt(value, expected) {
@@ -100802,10 +100820,49 @@ function readProviderDeliveryBinding(value) {
100802
100820
  identity
100803
100821
  };
100804
100822
  }
100823
+ /** The Provider Account a delivery identity names, or null when it names none. */
100824
+ function providerDeliveryAccount(identity) {
100825
+ return identity.kind === "provider_account" ? identity.providerAccountIdentity : identity.providerAccountIdentity ?? null;
100826
+ }
100827
+ function sameDeliveredCredential(left, right) {
100828
+ if (left.kind === "provider_account" && right.kind === "provider_account") return left.providerAccountIdentity === right.providerAccountIdentity;
100829
+ return left.kind === "credential_generation" && right.kind === "credential_generation" && left.credentialGenerationId === right.credentialGenerationId;
100830
+ }
100831
+ /**
100832
+ * Names how one thread's delivery binding moved, which is the question the
100833
+ * delivery layer could not answer while Claude's identity carried no account.
100834
+ *
100835
+ * `account_switch` needs both sides to name an account, the accounts to differ,
100836
+ * the delivered credential to change with them, and the runtime, machine and
100837
+ * epoch to hold. A switch changes the account and the generation together, so
100838
+ * two accounts on one generation would be two accounts sharing a credential,
100839
+ * and a switch that also moved the epoch changed more than the identity. Both
100840
+ * are `unknown`.
100841
+ *
100842
+ * The same delivered credential is `unchanged` whether or not either side names
100843
+ * an account, so a binding recorded before CMMD named the account still resumes
100844
+ * against the credential it ran on. A changed credential where either side
100845
+ * names no account is `unknown` rather than guessed at, which keeps an
100846
+ * account-less binding compared on exactly the fields it was compared on before.
100847
+ *
100848
+ * `providerDeliveryBindingsEqual` asks only whether the answer is `unchanged`.
100849
+ * `permitsProviderAccountTransition` in `provider/providerAccountTransition.ts`
100850
+ * asks whether it is `account_switch`, and is the Codex resume gate's rule
100851
+ * today. #3055 wires the Claude resume gate onto it.
100852
+ */
100853
+ function classifyProviderDeliveryBindingChange(prior, current) {
100854
+ if (prior.runtimeId !== current.runtimeId || prior.machineId !== current.machineId || prior.identity.kind !== current.identity.kind || prior.identity.provider !== current.identity.provider) return "unknown";
100855
+ const sameEpoch = prior.credentialEpoch === current.credentialEpoch;
100856
+ const sameCredential = sameDeliveredCredential(prior.identity, current.identity);
100857
+ const priorAccount = providerDeliveryAccount(prior.identity);
100858
+ const currentAccount = providerDeliveryAccount(current.identity);
100859
+ if (priorAccount !== null && currentAccount !== null && priorAccount !== currentAccount) return sameEpoch && !sameCredential ? "account_switch" : "unknown";
100860
+ if (sameEpoch && sameCredential) return "unchanged";
100861
+ if (priorAccount === null || currentAccount === null) return "unknown";
100862
+ return "credential_refresh";
100863
+ }
100805
100864
  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;
100865
+ return classifyProviderDeliveryBindingChange(left, right) === "unchanged";
100809
100866
  }
100810
100867
  //#endregion
100811
100868
  //#region src/provider/claudeReadinessReason.ts
@@ -101084,8 +101141,128 @@ const runClaudeLogout = (runtime) => new Promise((resolve, reject) => {
101084
101141
  });
101085
101142
  });
101086
101143
  //#endregion
101144
+ //#region src/usage/providerRateLimits.ts
101145
+ /**
101146
+ * `new Date(ms).toISOString()` throws past +-8.64e15 ms, so a `resetsAt` past
101147
+ * the same bound in seconds is refused rather than allowed to throw inside a
101148
+ * decoder whose whole job is to fail closed.
101149
+ */
101150
+ const MAX_RESETS_AT_SECONDS = 864e10;
101151
+ /**
101152
+ * One reset time in unix seconds as an ISO string, or null.
101153
+ *
101154
+ * Both mappers read reset times through this, so a malformed `resetsAt`
101155
+ * yields the null that means "this observation said nothing" instead of a
101156
+ * RangeError thrown out of a decoder the ingestion layer runs on every Claude
101157
+ * rate-limit event.
101158
+ */
101159
+ function resetsAtIso(resetsAt) {
101160
+ if (!Number.isFinite(resetsAt) || resetsAt <= 0 || resetsAt > MAX_RESETS_AT_SECONDS) return null;
101161
+ return (/* @__PURE__ */ new Date(resetsAt * 1e3)).toISOString();
101162
+ }
101163
+ /**
101164
+ * The SDK only ever reports ONE window per event. Only `five_hour` and
101165
+ * `seven_day` map to the two windows the Usage page shows; the
101166
+ * per-model/overage variants (seven_day_opus, overage, ...) are ignored here
101167
+ * on purpose rather than silently overwriting the plain 7-day figure.
101168
+ */
101169
+ function mapClaudeRateLimitInfo(info) {
101170
+ if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
101171
+ const kind = info.rateLimitType === "five_hour" ? "fiveHour" : info.rateLimitType === "seven_day" ? "sevenDay" : null;
101172
+ if (!kind) return null;
101173
+ const resetsAt = resetsAtIso(info.resetsAt);
101174
+ if (resetsAt === null) return null;
101175
+ return {
101176
+ kind,
101177
+ window: {
101178
+ usedPercent: Math.round(info.utilization * 100),
101179
+ resetsAt,
101180
+ status: info.status
101181
+ }
101182
+ };
101183
+ }
101184
+ /**
101185
+ * The one place a used-percent becomes a status. `unifiedWindows` carries no
101186
+ * per-window status of its own, and the `/usage` parser derives one the same
101187
+ * way, so both Claude usage sources agree on where "warning" starts.
101188
+ */
101189
+ function usageStatus(usedPercent) {
101190
+ if (usedPercent >= 100) return "rejected";
101191
+ return usedPercent >= 80 ? "allowed_warning" : "allowed";
101192
+ }
101193
+ /**
101194
+ * One untyped `utilization` as a whole percent, or nothing. The [0, 100] bound
101195
+ * matches `claudeUsageCli.ts`'s own bound on the `/usage` text, which in turn
101196
+ * matches CMMD's parser.
101197
+ */
101198
+ function readUsedPercent(utilization) {
101199
+ if (typeof utilization !== "number" || !Number.isFinite(utilization)) return null;
101200
+ const usedPercent = Math.round(utilization * 100);
101201
+ return usedPercent < 0 || usedPercent > 100 ? null : usedPercent;
101202
+ }
101203
+ /**
101204
+ * Reads one `unifiedWindows` entry, or nothing. Every field is checked because
101205
+ * none of them are typed: a non-object entry, a utilization that is not a
101206
+ * finite number in [0, 1], or a reset time that is not a positive finite
101207
+ * number of seconds all mean "this observation said nothing about this
101208
+ * window".
101209
+ */
101210
+ function readUnifiedWindow(raw) {
101211
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
101212
+ const { utilization, resetsAt } = raw;
101213
+ const usedPercent = readUsedPercent(utilization);
101214
+ if (usedPercent === null) return null;
101215
+ if (typeof resetsAt !== "number") return null;
101216
+ const resetsAtIsoValue = resetsAtIso(resetsAt);
101217
+ if (resetsAtIsoValue === null) return null;
101218
+ return {
101219
+ usedPercent,
101220
+ resetsAt: resetsAtIsoValue,
101221
+ status: usageStatus(usedPercent)
101222
+ };
101223
+ }
101224
+ /**
101225
+ * Reads the `unifiedWindows` sub-object of `rate_limit_info` into the two
101226
+ * rolling windows. The SDK rebuilds that sub-object on every turn from the
101227
+ * `anthropic-ratelimit-unified-{5h,7d}-utilization` and `-reset` response
101228
+ * headers.
101229
+ *
101230
+ * The top-level `rateLimitType` names one window per event and carries a
101231
+ * utilization only for `five_hour` or `seven_day`. This sub-object reports
101232
+ * every window it knows on every observation, which makes it the only
101233
+ * SDK-side source that can feed the Usage card without waking a Sprite and
101234
+ * delivering a credential.
101235
+ *
101236
+ * Its `seven_day_overage_included` window is skipped. `/usage` names each model
101237
+ * row after the display name Anthropic's usage API gives that model, one row
101238
+ * per allowlisted model, and the `7d_oi` header behind this window is one
101239
+ * aggregate that carries no name. Any name given to it here would be a guess
101240
+ * that could disagree with the rows the probe writes, so model rows come from
101241
+ * the probe alone.
101242
+ *
101243
+ * It is also undocumented internal surface stripped from the SDK's exported
101244
+ * types, so this decoder fails closed everywhere. It returns null when the
101245
+ * sub-object is absent or is not a plain object, and null when neither window
101246
+ * in it survives validation. A window that fails validation is reported as
101247
+ * null, and an unrecognized key is skipped, never fatal. A caller that gets
101248
+ * null has learned nothing and must not publish, because a partial write could
101249
+ * overwrite a good `/usage` reading with a guess.
101250
+ */
101251
+ function mapClaudeUnifiedWindows(info) {
101252
+ const raw = info.unifiedWindows;
101253
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
101254
+ const windows = raw;
101255
+ const fiveHour = readUnifiedWindow(windows.five_hour);
101256
+ const sevenDay = readUnifiedWindow(windows.seven_day);
101257
+ if (!fiveHour && !sevenDay) return null;
101258
+ return {
101259
+ fiveHour,
101260
+ sevenDay
101261
+ };
101262
+ }
101263
+ //#endregion
101087
101264
  //#region src/usage/claudeUsageCli.ts
101088
- const execFileAsync$4 = promisify$1(execFile);
101265
+ const execFileAsync$5 = promisify$1(execFile);
101089
101266
  const USAGE_ARGS = [
101090
101267
  "--safe-mode",
101091
101268
  "--no-session-persistence",
@@ -101109,7 +101286,7 @@ const MONTH_BY_NAME = {
101109
101286
  Dec: 11
101110
101287
  };
101111
101288
  async function runClaudeUsageCommand(binaryPath, args, claudeConfigDir) {
101112
- const { stdout } = await execFileAsync$4(binaryPath, [...args], {
101289
+ const { stdout } = await execFileAsync$5(binaryPath, [...args], {
101113
101290
  env: {
101114
101291
  ...process.env,
101115
101292
  TZ: "UTC",
@@ -101184,10 +101361,6 @@ function parseResetAt(value, now) {
101184
101361
  });
101185
101362
  return resetAt === null || Number.isNaN(resetAt.getTime()) ? null : resetAt.toISOString();
101186
101363
  }
101187
- function usageStatus(usedPercent) {
101188
- if (usedPercent >= 100) return "rejected";
101189
- return usedPercent >= 80 ? "allowed_warning" : "allowed";
101190
- }
101191
101364
  function parseUsageLine(output, label, now) {
101192
101365
  const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
101193
101366
  const match = new RegExp(`^${escapedLabel}: (\\d+(?:\\.\\d+)?)% used(?: · resets (.+))?$`, "m").exec(output);
@@ -101256,6 +101429,29 @@ async function fetchClaudeUsageViaOfficialClient(input) {
101256
101429
  };
101257
101430
  }
101258
101431
  //#endregion
101432
+ //#region src/usage/claudeUsageTelemetryRequest.ts
101433
+ /** The owner-session route for a Claude usage reading. */
101434
+ const CLAUDE_USAGE_TELEMETRY_PATH = "/api/forge/context/providers/usage/claude/telemetry";
101435
+ /**
101436
+ * Builds the body for either lane.
101437
+ *
101438
+ * `modelWindows` is sent only when the reading carries it. CMMD keeps the
101439
+ * stored model rows when the key is absent, so a reading that cannot name
101440
+ * model rows the way `/usage` does leaves the key out.
101441
+ */
101442
+ function claudeUsageTelemetryBody(input) {
101443
+ return {
101444
+ providerAccountId: input.attribution.providerAccountId,
101445
+ credentialGenerationId: input.attribution.credentialGenerationId,
101446
+ ...input.account,
101447
+ source: input.source,
101448
+ fiveHour: input.windows.fiveHour,
101449
+ sevenDay: input.windows.sevenDay,
101450
+ ...input.windows.modelWindows !== void 0 ? { modelWindows: input.windows.modelWindows } : {},
101451
+ observedAt: input.observedAt
101452
+ };
101453
+ }
101454
+ //#endregion
101259
101455
  //#region src/cmmd/syncClaudeCredential.ts
101260
101456
  /**
101261
101457
  * Card 1359 (harvest path A, Claude) — publish a personal Claude subscription
@@ -101271,7 +101467,6 @@ async function fetchClaudeUsageViaOfficialClient(input) {
101271
101467
  const USER_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/providers/claude/credential";
101272
101468
  const USER_CLAUDE_LINKED_ACCOUNT_PATH = "/api/forge/context/providers/claude/accounts";
101273
101469
  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
101470
  const AUTH_RUNTIME_CLAUDE_USAGE_PATH = "/api/forge/context/auth-runtime-claude-credential/usage";
101276
101471
  /** Path to the local Claude credentials.json, mirroring Claude Code's resolution. */
101277
101472
  function resolveLocalClaudeCredentialPath(homePath) {
@@ -101300,7 +101495,7 @@ function resolveLocalClaudeCredentialPath(homePath) {
101300
101495
  * it reaches — this is the first gate and the only one near the user, who is
101301
101496
  * standing in front of the sign-in and can simply try again.
101302
101497
  *
101303
- * The floor is ONE token, not both, and that distinction is load-bearing: an
101498
+ * The floor is ONE token, not both, because an
101304
101499
  * expired or blank access token beside a live refresh token is the normal
101305
101500
  * resting state of a rotating login, which Claude Code refreshes silently on
101306
101501
  * use. Demanding both would reject a perfectly recoverable credential and break
@@ -101378,10 +101573,10 @@ function extractClaudeAccountMetadata(credentialJson) {
101378
101573
  return {};
101379
101574
  }
101380
101575
  }
101381
- const execFileAsync$3 = promisify$1(execFile);
101576
+ const execFileAsync$4 = promisify$1(execFile);
101382
101577
  async function loadClaudeAccountMetadataFromCli(homePath) {
101383
101578
  try {
101384
- const { stdout } = await execFileAsync$3(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
101579
+ const { stdout } = await execFileAsync$4(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
101385
101580
  timeout: 2e4,
101386
101581
  maxBuffer: 1024 * 1024,
101387
101582
  env: {
@@ -101477,18 +101672,20 @@ async function publishClaudeUsageAfterSync(input) {
101477
101672
  binaryPath: resolveClaudeBinaryPath("claude"),
101478
101673
  ...input.homePath ? { claudeConfigDir: input.homePath } : {}
101479
101674
  });
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,
101675
+ await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : CLAUDE_USAGE_TELEMETRY_PATH, claudeUsageTelemetryBody({
101676
+ attribution: {
101677
+ providerAccountId: input.providerAccountId,
101678
+ credentialGenerationId: input.credentialGenerationId
101679
+ },
101680
+ account: {
101681
+ accountEmail: input.accountEmail,
101682
+ accountDisplayName: input.accountDisplayName,
101683
+ planType: input.planType
101684
+ },
101486
101685
  source: "active_poll",
101487
- fiveHour: usage.fiveHour,
101488
- sevenDay: usage.sevenDay,
101489
- modelWindows: usage.modelWindows,
101686
+ windows: usage,
101490
101687
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
101491
- });
101688
+ }));
101492
101689
  return null;
101493
101690
  } catch (cause) {
101494
101691
  return { cause };
@@ -149790,7 +149987,7 @@ function issueCmmdLocalUserProviderEgressCredential(input) {
149790
149987
  });
149791
149988
  }
149792
149989
  //#endregion
149793
- //#region src/provider/codexAccountTransition.ts
149990
+ //#region src/provider/providerAccountTransition.ts
149794
149991
  function explicitProviderAccountTransition(fromAccountId, toAccountId) {
149795
149992
  if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) return void 0;
149796
149993
  return {
@@ -149798,13 +149995,20 @@ function explicitProviderAccountTransition(fromAccountId, toAccountId) {
149798
149995
  toAccountId
149799
149996
  };
149800
149997
  }
149801
- function permitsCodexAccountTransition(input) {
149998
+ /**
149999
+ * Whether a thread may move from `prior` to `current` because the person asked
150000
+ * for another Provider Account. The credential broker authorizes the selected
150001
+ * account, so only the identity may change, which is what `account_switch`
150002
+ * means.
150003
+ *
150004
+ * Nothing in the rule is specific to a provider, but the Codex resume gate in
150005
+ * `codexAppServerManager.ts` is its only caller. The Claude half is parked
150006
+ * until #3055 wires the Claude resume gate onto it.
150007
+ */
150008
+ function permitsProviderAccountTransition(input) {
149802
150009
  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);
150010
+ if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId) return false;
150011
+ return classifyProviderDeliveryBindingChange(prior, current) === "account_switch";
149808
150012
  }
149809
150013
  //#endregion
149810
150014
  //#region src/providerAuth/codexLocalAccountIdentity.ts
@@ -150304,7 +150508,7 @@ var CodexAppServerManager = class extends EventEmitter$1 {
150304
150508
  const rawPriorDeliveryBinding = input.resumeCursor.providerDeliveryBinding;
150305
150509
  const priorDeliveryBinding = readProviderDeliveryBinding(rawPriorDeliveryBinding);
150306
150510
  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({
150511
+ if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !permitsProviderAccountTransition({
150308
150512
  transition: input.accountTransition,
150309
150513
  requestedAccountId: input.providerAccountId,
150310
150514
  prior: priorDeliveryBinding,
@@ -210785,6 +210989,374 @@ function withoutInheritedAnthropicCredentials(env, options) {
210785
210989
  return contained;
210786
210990
  }
210787
210991
  //#endregion
210992
+ //#region src/provider/personalClaudeInVm.ts
210993
+ /**
210994
+ * CMMD writes this marker when it activates a personal Claude subscription
210995
+ * inside a running sprite. Activation always happens after boot, so a
210996
+ * boot-time env var alone cannot reflect it (FOR-36249).
210997
+ *
210998
+ * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
210999
+ * Without that, the marker sits directly under FORGE_HOME, which is the same
211000
+ * guest home CMMD derives every sprite path from. A server with neither has no
211001
+ * sprite home to look in, and a desktop never has a marker, so it reads none.
211002
+ */
211003
+ const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
211004
+ const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
211005
+ function markerPath(env) {
211006
+ if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
211007
+ if (env.FORGE_HOME) return join(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
211008
+ return null;
211009
+ }
211010
+ /**
211011
+ * True when this runtime should offer the personal Claude subscription lane.
211012
+ *
211013
+ * Accepts either signal: the boot-time env var (backward compatible) or the
211014
+ * activation marker on disk. Reading the marker at call time lets the provider
211015
+ * catalog and the per-turn readiness guard self-heal once CMMD delivers,
211016
+ * without a sprite reboot. Callers still verify the credential is non-empty
211017
+ * before treating it as usable; this only decides whether the lane is active.
211018
+ */
211019
+ function personalClaudeInVmEnabled(env = process.env) {
211020
+ if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
211021
+ const marker = markerPath(env);
211022
+ if (!marker) return false;
211023
+ try {
211024
+ return existsSync(marker);
211025
+ } catch {
211026
+ return false;
211027
+ }
211028
+ }
211029
+ /** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
211030
+ function bootExportedPersonalClaudeGenerationHash(env = process.env) {
211031
+ return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
211032
+ }
211033
+ /**
211034
+ * The sha256 of the Claude credential generation this runtime holds, or "" when
211035
+ * it holds none. The readiness guard compares it with the generation CMMD
211036
+ * applied. The restart proof must not use it; it uses the boot-exported hash.
211037
+ *
211038
+ * The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
211039
+ * the marker already exists at boot. Activation in place writes the generation
211040
+ * file and touches the marker without setting any env, so an empty variable
211041
+ * falls back to that file, hashed the way the preload hashes it. CMMD puts the
211042
+ * file beside the marker in the guest home and names no path for it. A marker
211043
+ * with no generation file is a legacy runtime, and gets no hash here either.
211044
+ */
211045
+ function personalClaudeGenerationHash(env = process.env) {
211046
+ const exported = bootExportedPersonalClaudeGenerationHash(env);
211047
+ if (exported) return exported;
211048
+ const marker = markerPath(env);
211049
+ if (!marker) return "";
211050
+ try {
211051
+ if (!existsSync(marker)) return "";
211052
+ const generation = readFileSync(join(dirname(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
211053
+ return generation ? createHash("sha256").update(generation).digest("hex") : "";
211054
+ } catch {
211055
+ return "";
211056
+ }
211057
+ }
211058
+ //#endregion
211059
+ //#region src/provider/claudeCredentialDirectory.ts
211060
+ /**
211061
+ * Per-account credential directories for Claude turns.
211062
+ *
211063
+ * Claude Code keeps conversation transcripts under
211064
+ * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
211065
+ * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
211066
+ * credential file, so each Provider Account can hold its own login while every
211067
+ * session file stays in one place and `--resume` keeps finding its target. See
211068
+ * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
211069
+ *
211070
+ * That variable is not documented. Under
211071
+ * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
211072
+ * only allowed because absence degrades to a named behaviour that already
211073
+ * works: one account per Environment, exactly what runs today. This module is
211074
+ * the gate that keeps the degrade honest. It sets the variable only when a
211075
+ * readable `.credentials.json` is already sitting in the resolved directory,
211076
+ * because a missing or empty directory makes the CLI report logged out and a
211077
+ * live turn would fail. Nothing writes these directories yet, so every runtime
211078
+ * today takes the skip path.
211079
+ *
211080
+ * Pure and dependency-free on purpose, for the same reason as
211081
+ * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
211082
+ * services, the browser manager and the server config, so a unit test that
211083
+ * imports it cannot run in a worktree. This rule decides which account pays for
211084
+ * a turn, so it has to stay directly testable.
211085
+ */
211086
+ /** Repoints `.credentials.json` without moving the transcripts. */
211087
+ const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
211088
+ /** The one file Claude Code writes into the credential directory. */
211089
+ const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
211090
+ /**
211091
+ * Runtime env var naming the Provider Account this runtime's Claude turns
211092
+ * belong to.
211093
+ *
211094
+ * NOTHING SETS THIS. It has no writer anywhere in Forge or CMMD, so as shipped
211095
+ * the per-account lane can never turn on and every runtime keeps today's single
211096
+ * account. The writer waits on issue 3073, which decides which identity keys
211097
+ * the directory. That has to be settled first: the credential generation
211098
+ * rotates on every re-delivery, so keying on it would give a new directory
211099
+ * each time and defeat the point.
211100
+ *
211101
+ * It is also not the seam. The seam is the `credentialDirectory` parameter on
211102
+ * `ClaudeAdapter.buildClaudeQueryEnv`, which is what tests and callers drive.
211103
+ * This variable is one bridge into that parameter, and the delivery step is
211104
+ * free to replace it with something better rather than adopt it.
211105
+ */
211106
+ const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
211107
+ /** Operator escape hatch for the directory root, mostly for local debugging. */
211108
+ const CLAUDE_CREDENTIAL_ROOT_ENV = "FORGE_CLAUDE_CREDENTIAL_ROOT";
211109
+ const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
211110
+ const FALLBACK_ROOT_DIRECTORY_NAME = ".forge-claude-credentials";
211111
+ const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
211112
+ const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
211113
+ const DIRECTORY_NAME_HASH_LENGTH = 32;
211114
+ /**
211115
+ * Directory name for an account. A hash, so no email or display name reaches a
211116
+ * filesystem path and no path-illegal character can appear in one either.
211117
+ */
211118
+ function claudeCredentialDirectoryName(accountIdentity) {
211119
+ return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
211120
+ }
211121
+ /** The account this runtime's Claude turns belong to, or null on every runtime today. */
211122
+ function resolveClaudeProviderAccountIdentity(env) {
211123
+ const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
211124
+ if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
211125
+ return identity;
211126
+ }
211127
+ /**
211128
+ * Where per-account credential directories live. Always outside
211129
+ * `CLAUDE_CONFIG_DIR`, so one of them can never be mistaken for the main
211130
+ * credential store, and so a stray read of the config directory cannot walk
211131
+ * into another account's login.
211132
+ */
211133
+ function claudeCredentialDirectoryRoot(env) {
211134
+ const root = credentialRootCandidate(env);
211135
+ if (!root || !isAbsolute(root)) return null;
211136
+ return isInside(root, claudeConfigDirectory(env)) ? null : resolve(root);
211137
+ }
211138
+ /** Absolute credential directory for one account, or null when either half is missing. */
211139
+ function resolveClaudeCredentialDirectory(input) {
211140
+ if (!input.accountIdentity) return null;
211141
+ const root = claudeCredentialDirectoryRoot(input.env);
211142
+ if (!root) return null;
211143
+ return join(root, claudeCredentialDirectoryName(input.accountIdentity));
211144
+ }
211145
+ /**
211146
+ * Decide whether this turn may point at a per-account credential directory.
211147
+ *
211148
+ * The cheap checks run first and the capability probe runs last, so a runtime
211149
+ * with no delivered credential never spawns the CLI. Any failure is a skip, so
211150
+ * the worst outcome is today's behaviour.
211151
+ */
211152
+ async function decideClaudeCredentialDirectory(input) {
211153
+ if (!input.accountIdentity) return skip("no-account");
211154
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
211155
+ const directory = resolveClaudeCredentialDirectory(input);
211156
+ if (!directory) return skip("no-root");
211157
+ if (!(input.credentialFilePresent ?? hasReadableCredentialFile)(directory)) return skip("no-credential-file");
211158
+ return await probeSupportSafely(input.probeSupport) === "supported" ? {
211159
+ outcome: "set",
211160
+ directory
211161
+ } : skip("capability-not-supported");
211162
+ }
211163
+ /** True when `.credentials.json` is in the directory and readable right now. */
211164
+ function hasReadableCredentialFile(directory) {
211165
+ const file = join(directory, CLAUDE_CREDENTIAL_FILE_NAME);
211166
+ try {
211167
+ if (!statSync(file).isFile()) return false;
211168
+ accessSync(file, constants.R_OK);
211169
+ return true;
211170
+ } catch {
211171
+ return false;
211172
+ }
211173
+ }
211174
+ function skip(reason) {
211175
+ return {
211176
+ outcome: "skipped",
211177
+ reason
211178
+ };
211179
+ }
211180
+ /** `inconclusive` is treated as `unsupported`, and so is a probe that throws. */
211181
+ async function probeSupportSafely(probeSupport) {
211182
+ try {
211183
+ return await probeSupport();
211184
+ } catch {
211185
+ return "inconclusive";
211186
+ }
211187
+ }
211188
+ function credentialRootCandidate(env) {
211189
+ const explicit = env[CLAUDE_CREDENTIAL_ROOT_ENV]?.trim();
211190
+ if (explicit) return explicit;
211191
+ const forgeHome = env.FORGE_HOME?.trim();
211192
+ if (forgeHome) return join(forgeHome, CREDENTIAL_ROOT_DIRECTORY_NAME);
211193
+ try {
211194
+ return join(homedir(), FALLBACK_ROOT_DIRECTORY_NAME);
211195
+ } catch {
211196
+ return null;
211197
+ }
211198
+ }
211199
+ function claudeConfigDirectory(env) {
211200
+ const configured = env.CLAUDE_CONFIG_DIR?.trim();
211201
+ if (configured) return configured;
211202
+ try {
211203
+ return join(homedir(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
211204
+ } catch {
211205
+ return null;
211206
+ }
211207
+ }
211208
+ function isInside(candidate, parent) {
211209
+ if (!parent) return false;
211210
+ const step = relative(resolve(parent), resolve(candidate));
211211
+ return step === "" || !step.startsWith("..") && !isAbsolute(step);
211212
+ }
211213
+ //#endregion
211214
+ //#region src/provider/claudeCredentialDirectoryProbe.ts
211215
+ /**
211216
+ * Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
211217
+ *
211218
+ * The variable is unpublished, so a runtime is asked rather than assumed to
211219
+ * honour it (`docs/adr/unpublished-provider-surface-needs-a-fallback.md`). The
211220
+ * question the probe can actually answer: point the variable at an empty
211221
+ * directory and a runtime that honours it flips from logged in to logged out.
211222
+ * A runtime with no credential at all is logged out either way and cannot
211223
+ * discriminate, which is `inconclusive`, and callers read that as unsupported.
211224
+ *
211225
+ * `claude auth status` is a pure read. It sends no prompt and leaves
211226
+ * `.credentials.json` byte-identical, so probing spends no subscription usage.
211227
+ * It prints JSON on stdout and exits 1 when logged out, so the exit code is not
211228
+ * a failure signal and the `loggedIn` field is what gets read.
211229
+ */
211230
+ const execFileAsync$3 = promisify$1(execFile);
211231
+ const AUTH_STATUS_ARGS = ["auth", "status"];
211232
+ const VERSION_ARGS = ["--version"];
211233
+ /**
211234
+ * The probe sits in front of a turn, so a provider process that never answers
211235
+ * must not hold that turn open. `execFile` kills the child at this bound, and
211236
+ * `runBounded` gives up on it at the same bound whatever the deps do, so a
211237
+ * hung or unkillable process costs a few seconds and an `inconclusive`.
211238
+ */
211239
+ const PROBE_TIMEOUT_MS = 5e3;
211240
+ const PROBE_DIRECTORY_PREFIX = "forge-claude-capability-";
211241
+ const VERSION_PATTERN = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
211242
+ function makeClaudeCredentialDirectoryProbe(deps) {
211243
+ const answers = /* @__PURE__ */ new Map();
211244
+ return { support: async () => {
211245
+ const version = await readVersionSafely(deps);
211246
+ if (version === null) return runProbe(deps);
211247
+ const cached = answers.get(version);
211248
+ if (cached) return cached;
211249
+ const pending = runProbe(deps);
211250
+ answers.set(version, pending);
211251
+ return pending;
211252
+ } };
211253
+ }
211254
+ /**
211255
+ * One probe per binary path for the life of the process. The probe spawns the
211256
+ * CLI twice, and every Claude session start would otherwise pay for it.
211257
+ */
211258
+ const probesByBinaryPath = /* @__PURE__ */ new Map();
211259
+ function claudeCredentialDirectoryProbeFor(binaryPath) {
211260
+ const binary = binaryPath?.trim() || "claude";
211261
+ const existing = probesByBinaryPath.get(binary);
211262
+ if (existing) return existing;
211263
+ const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binary));
211264
+ probesByBinaryPath.set(binary, probe);
211265
+ return probe;
211266
+ }
211267
+ function claudeCredentialDirectoryProbeDeps(binaryPath) {
211268
+ return {
211269
+ runAuthStatus: ({ credentialDirectory }) => runClaudeCommand$1(binaryPath, AUTH_STATUS_ARGS, credentialDirectory),
211270
+ readBinaryVersion: async () => {
211271
+ const { stdout } = await runClaudeCommand$1(binaryPath, VERSION_ARGS, null);
211272
+ return VERSION_PATTERN.exec(stdout)?.[0] ?? null;
211273
+ },
211274
+ makeProbeDirectory: () => mkdtemp(join(tmpdir(), PROBE_DIRECTORY_PREFIX)),
211275
+ removeProbeDirectory: (directory) => rm(directory, {
211276
+ recursive: true,
211277
+ force: true
211278
+ })
211279
+ };
211280
+ }
211281
+ async function runProbe(deps) {
211282
+ let probeDirectory = null;
211283
+ try {
211284
+ if (parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: null }))) !== true) return "inconclusive";
211285
+ probeDirectory = await deps.makeProbeDirectory();
211286
+ const directory = probeDirectory;
211287
+ const redirected = parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: directory })));
211288
+ if (redirected === false) return "supported";
211289
+ return redirected === true ? "unsupported" : "inconclusive";
211290
+ } catch {
211291
+ return "inconclusive";
211292
+ } finally {
211293
+ if (probeDirectory !== null) await deps.removeProbeDirectory(probeDirectory).catch(() => void 0);
211294
+ }
211295
+ }
211296
+ async function readVersionSafely(deps) {
211297
+ try {
211298
+ return await runBounded(deps, () => deps.readBinaryVersion());
211299
+ } catch {
211300
+ return null;
211301
+ }
211302
+ }
211303
+ /**
211304
+ * Give up on a call that outlives the bound. The underlying promise is left to
211305
+ * settle on its own; the real deps hand `execFile` the same bound, so its child
211306
+ * is already being killed by the time this rejects.
211307
+ */
211308
+ async function runBounded(deps, operation) {
211309
+ const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
211310
+ let timer;
211311
+ try {
211312
+ return await Promise.race([operation(), new Promise((_resolve, reject) => {
211313
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude auth status timed out")), timeoutMs);
211314
+ })]);
211315
+ } finally {
211316
+ if (timer !== void 0) clearTimeout(timer);
211317
+ }
211318
+ }
211319
+ /** The `loggedIn` field, or null when the output does not carry one. */
211320
+ function parseLoggedIn(run) {
211321
+ const parsed = parseJsonObject$1(run.stdout);
211322
+ return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
211323
+ }
211324
+ function parseJsonObject$1(output) {
211325
+ const trimmed = output.trim();
211326
+ const start = trimmed.indexOf("{");
211327
+ const end = trimmed.lastIndexOf("}");
211328
+ if (start === -1 || end <= start) return null;
211329
+ try {
211330
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
211331
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
211332
+ } catch {
211333
+ return null;
211334
+ }
211335
+ }
211336
+ async function runClaudeCommand$1(binaryPath, args, credentialDirectory) {
211337
+ try {
211338
+ const { stdout } = await execFileAsync$3(binaryPath, [...args], {
211339
+ env: {
211340
+ ...process.env,
211341
+ ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
211342
+ },
211343
+ maxBuffer: 1024 * 1024,
211344
+ timeout: PROBE_TIMEOUT_MS
211345
+ });
211346
+ return {
211347
+ stdout,
211348
+ exitCode: 0
211349
+ };
211350
+ } catch (cause) {
211351
+ const failure = cause;
211352
+ if (typeof failure.stdout !== "string") throw cause;
211353
+ return {
211354
+ stdout: failure.stdout,
211355
+ exitCode: typeof failure.code === "number" ? failure.code : null
211356
+ };
211357
+ }
211358
+ }
211359
+ //#endregion
210788
211360
  //#region src/provider/makeManagedServerProvider.ts
210789
211361
  const makeManagedServerProvider = fn$1("makeManagedServerProvider")(function* (input) {
210790
211362
  const refreshSemaphore = yield* make$43(1);
@@ -210873,43 +211445,6 @@ const makeProviderAuthProvenance = (input) => {
210873
211445
  };
210874
211446
  };
210875
211447
  //#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
211448
  //#region src/provider/Layers/ClaudeProvider.ts
210914
211449
  function endpointProfileDisplayNames(settings) {
210915
211450
  return Object.fromEntries(settings.endpointProfiles.filter((p) => p.displayName).map((p) => [p.slug, p.displayName]));
@@ -212328,10 +212863,27 @@ function claudeForgeAttributionPrompt() {
212328
212863
  const attribution = resolveAgentAttribution();
212329
212864
  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
212865
  }
212331
- function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential) {
212866
+ /**
212867
+ * `credentialDirectory` points this turn at one Provider Account's own
212868
+ * `.credentials.json` without moving the transcripts, which is what lets an
212869
+ * account switch keep `--resume` working
212870
+ * (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It is null on
212871
+ * every runtime today: `decideClaudeCredentialDirectory` only hands back a
212872
+ * directory that already holds a readable credential file, and nothing writes
212873
+ * one yet, so the environment stays byte-for-byte what it was.
212874
+ *
212875
+ * It applies to the personal-subscription branch below and to nothing else. On
212876
+ * the profile / proxy branch the credential file is not what authenticates the
212877
+ * turn, an API key is, so pointing the variable there would add a second source
212878
+ * of truth that a later reader has to rule out.
212879
+ */
212880
+ function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
212332
212881
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
212333
212882
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
212334
- if (!effectiveBaseUrl) return withoutInheritedAnthropicCredentials(baseEnv, { stripEndpointOverride: baseProcessEnv.FORGE_PERSONAL_CLAUDE_IN_VM === "true" });
212883
+ if (!effectiveBaseUrl) return {
212884
+ ...withoutInheritedAnthropicCredentials(baseEnv, { stripEndpointOverride: personalClaudeInVmEnabled(baseProcessEnv) }),
212885
+ ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
212886
+ };
212335
212887
  const effectiveApiKey = resolveClaudeQueryApiKey(endpointProfile, claudeSettings, providerProxyCredential?.token ?? null);
212336
212888
  return {
212337
212889
  ...baseEnv,
@@ -214599,6 +215151,11 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
214599
215151
  })));
214600
215152
  const claudeSettings = serverSettings.providers.claudeAgent;
214601
215153
  const claudeBinaryPath = claudeSettings.binaryPath;
215154
+ const claudeCredentialDirectory = yield* tryPromise(() => decideClaudeCredentialDirectory({
215155
+ env: process.env,
215156
+ accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
215157
+ probeSupport: () => claudeCredentialDirectoryProbeFor(claudeBinaryPath).support()
215158
+ })).pipe(map$3((decision) => decision.outcome === "set" ? decision.directory : null), orElseSucceed(() => null));
214602
215159
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
214603
215160
  const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
214604
215161
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
@@ -214623,7 +215180,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
214623
215180
  existingResumeSessionId,
214624
215181
  newSessionId,
214625
215182
  canUseTool,
214626
- env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential),
215183
+ env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
214627
215184
  browserManager,
214628
215185
  threadId,
214629
215186
  browserToolEnvironmentId,
@@ -229697,7 +230254,7 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
229697
230254
  });
229698
230255
  }
229699
230256
  function requiresNativeClaudeReadiness(config, provider) {
229700
- return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && process.env.FORGE_PERSONAL_CLAUDE_IN_VM === "true";
230257
+ return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
229701
230258
  }
229702
230259
  function resolveGuardAuthority(config, readSessionId, input) {
229703
230260
  const runtimeId = config.forgeCmmdRuntimeId?.trim();
@@ -229709,7 +230266,7 @@ function resolveGuardAuthority(config, readSessionId, input) {
229709
230266
  runtimeId,
229710
230267
  machineId,
229711
230268
  credentialEpoch,
229712
- localGenerationHash: process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "",
230269
+ localGenerationHash: personalClaudeGenerationHash(),
229713
230270
  sessionId
229714
230271
  };
229715
230272
  }
@@ -286869,25 +287426,124 @@ const makeRuntimeDrainReporter = gen(function* () {
286869
287426
  var RuntimeDrainReporter = class extends Service$1()("forge/runtimeDrain/RuntimeDrainReporter") {};
286870
287427
  const RuntimeDrainReporterLive = effect(RuntimeDrainReporter, makeRuntimeDrainReporter);
286871
287428
  //#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
- };
287429
+ //#region src/usage/Services/ClaudeUsageTelemetry.ts
287430
+ /**
287431
+ * Claude usage the SDK reports on every turn, published as passive telemetry.
287432
+ *
287433
+ * Every Claude turn carries a `rate_limit_event` whose `unifiedWindows`
287434
+ * sub-object holds the two rolling windows the Usage card renders. Reading it
287435
+ * costs nothing: no `claude` process, no credential delivery, no woken sprite.
287436
+ *
287437
+ * This module is the whole publish lane behind one call. `observe` takes what
287438
+ * an observation knows and returns void. Whether the floor admits it, which
287439
+ * credential generation it ran on, whether the value is new, and whether CMMD
287440
+ * accepted it are all decided here, so the ingestion layer stays a decoder and
287441
+ * a hand-off. Nothing decided here can fail a turn, and no admitted
287442
+ * observation outlives `CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS`.
287443
+ *
287444
+ * The `/usage` probe in `syncClaudeCredential.ts` is a separate lane that
287445
+ * publishes `source: "active_poll"`. This one publishes
287446
+ * `source: "passive_sdk_telemetry"`, and both lanes build their body with
287447
+ * `claudeUsageTelemetryBody`.
287448
+ *
287449
+ * @module ClaudeUsageTelemetry
287450
+ */
287451
+ var ClaudeUsageTelemetry = class extends Service$1()("forge/usage/Services/ClaudeUsageTelemetry") {};
287452
+ /**
287453
+ * The longest one admitted observation may take, from the credential
287454
+ * generation lookup through the CMMD credential and the POST, before it is
287455
+ * abandoned and counted as failed. Ingestion does not wait on it, so this
287456
+ * bounds how long a slow lookup or a slow CMMD keeps a fiber alive, not how
287457
+ * long a turn waits. Together with the floor it caps how many observations for
287458
+ * one account can be in flight at once.
287459
+ */
287460
+ const CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS = 1e4;
287461
+ /** How many accounts, and how many pairs, the observer remembers before evicting the oldest. */
287462
+ const MAX_TRACKED_KEYS = 32;
287463
+ /**
287464
+ * Why one publish did not land. Every reason is the same to `observe`, which
287465
+ * logs it and moves on, so the type exists to name the lane rather than to be
287466
+ * matched on.
287467
+ */
287468
+ var ClaudeUsageTelemetryPublishError = class extends TaggedError("ClaudeUsageTelemetryPublishError") {};
287469
+ function pairKey(attribution) {
287470
+ return `${attribution.providerAccountId}\u0000${attribution.credentialGenerationId}`;
287471
+ }
287472
+ function windowFingerprint(value) {
287473
+ return value === null ? "-" : `${value.usedPercent}|${value.resetsAt}|${value.status}`;
287474
+ }
287475
+ /**
287476
+ * A value fingerprint built field by field rather than by `JSON.stringify` on
287477
+ * the whole object, so the comparison does not depend on key insertion order
287478
+ * and cannot silently start ignoring a field a future mapper adds without a
287479
+ * matching change here.
287480
+ */
287481
+ function windowsFingerprint(windows) {
287482
+ return `${windowFingerprint(windows.fiveHour)}/${windowFingerprint(windows.sevenDay)}`;
287483
+ }
287484
+ /**
287485
+ * Stores `value` as the newest entry for `key` and evicts the oldest entry past
287486
+ * the cap. Map iteration is insertion-ordered, so the first key is the one
287487
+ * touched longest ago. An idle account or a rotated credential generation
287488
+ * rarely comes back, so evicting it costs at most one early lookup or one
287489
+ * redundant publish.
287490
+ */
287491
+ function remember(map, key, value) {
287492
+ map.delete(key);
287493
+ map.set(key, value);
287494
+ if (map.size > MAX_TRACKED_KEYS) {
287495
+ const oldest = map.keys().next();
287496
+ if (!oldest.done) map.delete(oldest.value);
287497
+ }
287498
+ }
287499
+ /**
287500
+ * Builds the observer over injected dependencies. The HTTP call and the clock
287501
+ * both arrive from outside, so a test drives the whole policy without a network
287502
+ * or a timer.
287503
+ */
287504
+ function makeClaudeUsageTelemetry(dependencies) {
287505
+ const now = dependencies.now ?? (() => Date.now());
287506
+ /** When each account last had an observation admitted, whatever came of it. */
287507
+ const admittedAt = /* @__PURE__ */ new Map();
287508
+ /** The fingerprint of the last value CMMD accepted, per pair. */
287509
+ const publishedFingerprints = /* @__PURE__ */ new Map();
287510
+ const publishIfNew = (windows, providerAccountId, lookupCredentialGenerationId, at) => gen(function* () {
287511
+ const credentialGenerationId = yield* lookupCredentialGenerationId;
287512
+ if (!credentialGenerationId) return;
287513
+ const attribution = {
287514
+ providerAccountId,
287515
+ credentialGenerationId
287516
+ };
287517
+ const key = pairKey(attribution);
287518
+ const fingerprint = windowsFingerprint(windows);
287519
+ if (publishedFingerprints.get(key) === fingerprint) return;
287520
+ yield* dependencies.publish(claudeUsageTelemetryBody({
287521
+ attribution,
287522
+ source: "passive_sdk_telemetry",
287523
+ windows,
287524
+ observedAt: new Date(at).toISOString()
287525
+ }));
287526
+ remember(publishedFingerprints, key, fingerprint);
287527
+ });
287528
+ /**
287529
+ * The floor is read and claimed inside the returned effect, never while
287530
+ * building it, and before anything suspends. Two turns on one account can be
287531
+ * in flight at once, and the floor only holds them apart if the second one
287532
+ * reads what the first one wrote.
287533
+ */
287534
+ const observe = (observation) => suspend$1(() => {
287535
+ const { windows, providerAccountId } = observation;
287536
+ if (!windows || !providerAccountId) return void_$1;
287537
+ const at = now();
287538
+ const lastAdmittedAt = admittedAt.get(providerAccountId);
287539
+ if (lastAdmittedAt !== void 0 && at - lastAdmittedAt < 5e3) return void_$1;
287540
+ remember(admittedAt, providerAccountId, at);
287541
+ 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", {
287542
+ providerAccountId,
287543
+ cause
287544
+ })), asVoid);
287545
+ });
287546
+ return { observe };
286891
287547
  }
286892
287548
  //#endregion
286893
287549
  //#region src/orchestration/Layers/ProviderRuntimeIngestion.ts
@@ -286902,20 +287558,37 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = minutes(120);
286902
287558
  const MAX_BUFFERED_ASSISTANT_CHARS = 24e3;
286903
287559
  const PROVIDER_INGESTION_CONCURRENCY = 8;
286904
287560
  const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.FORGE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
286905
- function decodeClaudeRateLimitWindows(raw) {
287561
+ /**
287562
+ * The SDK's own `rate_limit_info` off one ClaudeAdapter rate-limit event, or
287563
+ * nothing. Both readers below start here: the banner takes the single window
287564
+ * `rateLimitType` names, and passive usage telemetry takes the `unifiedWindows`
287565
+ * sub-object riding along on the same object.
287566
+ */
287567
+ function readClaudeRateLimitInfo(raw) {
286906
287568
  const rateLimitInfo = raw.rate_limit_info;
286907
- if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return [];
287569
+ if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return null;
287570
+ return rateLimitInfo;
287571
+ }
287572
+ function decodeClaudeRateLimitWindows(raw) {
287573
+ const rateLimitInfo = readClaudeRateLimitInfo(raw);
287574
+ if (!rateLimitInfo) return [];
286908
287575
  const mapped = mapClaudeRateLimitInfo(rateLimitInfo);
286909
287576
  return mapped ? [{
286910
287577
  window: mapped.kind === "fiveHour" ? "five_hour" : "seven_day",
286911
287578
  ...mapped.window
286912
287579
  }] : [];
286913
287580
  }
287581
+ /** The raw telemetry object ClaudeAdapter/CodexAdapter wrap as `payload.rateLimits`. */
287582
+ function readRateLimitPayload(event) {
287583
+ if (event.type !== "account.rate-limits.updated") return null;
287584
+ const raw = event.payload.rateLimits;
287585
+ return raw !== null && typeof raw === "object" ? raw : null;
287586
+ }
286914
287587
  /**
286915
287588
  * 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.
287589
+ * codebase has access to, so it's read defensively. An unrecognized shape
287590
+ * is skipped rather than thrown, since this is a best-effort in-chat notice
287591
+ * that the turn itself never depends on.
286919
287592
  */
286920
287593
  /**
286921
287594
  * Decodes ONE window of the codex app-server `account/rateLimits/updated`
@@ -287228,6 +287901,9 @@ function shouldApplyProviderTurnCompleted(activeTurnId, eventTurnId) {
287228
287901
  }
287229
287902
  const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$1("make")(function* () {
287230
287903
  const orchestrationEngine = yield* OrchestrationEngineService;
287904
+ const claudeUsageTelemetry = yield* ClaudeUsageTelemetry;
287905
+ const claudeUsageScope = yield* make$37("parallel");
287906
+ yield* addFinalizer$2(() => close(claudeUsageScope, void_$3));
287231
287907
  const providerService = yield* ProviderService;
287232
287908
  const projectionTurnRepository = yield* ProjectionTurnRepository;
287233
287909
  const serverSettingsService = yield* ServerSettingsService;
@@ -287635,18 +288311,53 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287635
288311
  * SDKRateLimitInfo, reused from apps/server/src/usage/providerRateLimits.ts
287636
288312
  * (same decoder the Usage page cache uses). Codex's app-server protocol
287637
288313
  * 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.
288314
+ * read defensively. An unrecognized shape means no window update, not a
288315
+ * thrown error, since this is a best-effort in-chat notice that the turn
288316
+ * itself never depends on.
287641
288317
  */
287642
288318
  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 [];
288319
+ const raw = readRateLimitPayload(event);
288320
+ if (!raw) return [];
287646
288321
  if (event.provider === "claudeAgent") return decodeClaudeRateLimitWindows(raw);
287647
288322
  if (event.provider === "codex") return decodeCodexRateLimitWindows(raw);
287648
288323
  return [];
287649
288324
  }
288325
+ /**
288326
+ * The credential generation a Claude thread's live session runs on, or null.
288327
+ *
288328
+ * It is the delivery identity the readiness guard verified before the
288329
+ * session started, which ProviderService writes into the provider resume
288330
+ * cursor and `listSessions` hands back on the live session. Claude's
288331
+ * delivery identity is `{kind: "credential_generation"}` and carries NO
288332
+ * account field, so it cannot stand in for the account the thread's model
288333
+ * selection names, and neither id is derivable from the other. The usage
288334
+ * observer drops an observation missing either one, because publishing under
288335
+ * a guessed id would write one account's usage onto another account's card.
288336
+ */
288337
+ const claudeCredentialGenerationId = fn$1("claudeCredentialGenerationId")(function* (threadId) {
288338
+ const cursor = (yield* providerService.listSessions()).find((entry) => entry.threadId === threadId && entry.provider === "claudeAgent")?.resumeCursor;
288339
+ const binding = cursor !== null && typeof cursor === "object" ? readProviderDeliveryBinding(cursor.providerDeliveryBinding) : null;
288340
+ return binding?.identity.kind === "credential_generation" ? binding.identity.credentialGenerationId : null;
288341
+ });
288342
+ /**
288343
+ * Publishes the unified windows the same event already carried for the
288344
+ * banner. Passive: no probe, no credential delivery, no woken sprite. The SDK
288345
+ * reports these figures on every turn whether anything reads them or not.
288346
+ * The observer owns the floor, the credential generation lookup, dedupe, the
288347
+ * timeout and the HTTP call, and never fails.
288348
+ */
288349
+ const observeClaudeUsage = fn$1("observeClaudeUsage")(function* (event, thread) {
288350
+ if (event.provider !== "claudeAgent") return;
288351
+ const raw = readRateLimitPayload(event);
288352
+ const rateLimitInfo = raw ? readClaudeRateLimitInfo(raw) : null;
288353
+ const windows = rateLimitInfo ? mapClaudeUnifiedWindows(rateLimitInfo) : null;
288354
+ if (!windows) return;
288355
+ yield* claudeUsageTelemetry.observe({
288356
+ windows,
288357
+ providerAccountId: readProviderAccountId(thread.modelSelection) ?? null,
288358
+ credentialGenerationId: claudeCredentialGenerationId(thread.id)
288359
+ });
288360
+ });
287650
288361
  const handleRateLimitEvent = fn$1("handleRateLimitEvent")(function* (event, thread) {
287651
288362
  const provider = event.provider === "claudeAgent" ? "claude" : event.provider === "codex" ? "codex" : null;
287652
288363
  if (!provider) return;
@@ -287662,6 +288373,10 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287662
288373
  resetsAt: win.resetsAt,
287663
288374
  createdAt: event.createdAt
287664
288375
  }), { discard: true });
288376
+ yield* observeClaudeUsage(event, thread).pipe(catchCause((cause) => hasInterruptsOnly(cause) ? void_$1 : logWarning$1("claude usage observation failed", {
288377
+ eventId: event.eventId,
288378
+ cause
288379
+ })), forkIn(claudeUsageScope));
287665
288380
  });
287666
288381
  const processRuntimeEvent = fn$1("processRuntimeEvent")(function* (event) {
287667
288382
  const thread = (yield* orchestrationEngine.getReadModel()).threads.find((entry) => entry.id === event.threadId);
@@ -287776,6 +288491,44 @@ const ProviderRuntimeIngestionLive = effect(ProviderRuntimeIngestionService, fn$
287776
288491
  };
287777
288492
  })()).pipe(provide$1(ProjectionTurnRepositoryLive));
287778
288493
  //#endregion
288494
+ //#region src/usage/Layers/ClaudeUsageTelemetry.ts
288495
+ /**
288496
+ * The real publish behind `ClaudeUsageTelemetry`: the owner's CMMD credential
288497
+ * and one POST to `CLAUDE_USAGE_TELEMETRY_PATH`.
288498
+ *
288499
+ * The call always goes out on the owner session credential, never on a
288500
+ * runtime grant.
288501
+ *
288502
+ * A machine with nobody signed in, or one whose CMMD credential cannot be
288503
+ * minted, fails the publish rather than reporting success. The observer only
288504
+ * remembers a value it managed to send, so a quiet success here would suppress
288505
+ * the publish for as long as that reading stayed the same, which on a stable
288506
+ * account is hours.
288507
+ *
288508
+ * @module ClaudeUsageTelemetryLive
288509
+ */
288510
+ const LOG_PREFIX = "claude-usage-passive-telemetry";
288511
+ const ClaudeUsageTelemetryLive = effect(ClaudeUsageTelemetry, gen(function* () {
288512
+ const sql = yield* SqlClient;
288513
+ const publish = (body) => gen(function* () {
288514
+ const credential = yield* resolveCmmdOwnerSessionCredential(sql, { logPrefix: LOG_PREFIX });
288515
+ if (credential.kind !== "credential") return yield* new ClaudeUsageTelemetryPublishError({ reason: credential.kind });
288516
+ yield* tryPromise({
288517
+ try: () => cmmdGateway.request({
288518
+ method: "POST",
288519
+ jwt: credential.jwt,
288520
+ path: CLAUDE_USAGE_TELEMETRY_PATH,
288521
+ body
288522
+ }),
288523
+ catch: (cause) => new ClaudeUsageTelemetryPublishError({
288524
+ reason: "request-failed",
288525
+ cause
288526
+ })
288527
+ });
288528
+ });
288529
+ return makeClaudeUsageTelemetry({ publish });
288530
+ }));
288531
+ //#endregion
287779
288532
  //#region ../../packages/shared/src/DrainableWorker.ts
287780
288533
  /**
287781
288534
  * Create a drainable worker that processes items from an unbounded queue.
@@ -293524,7 +294277,7 @@ function resolveBuildCommitFromEnv(env) {
293524
294277
  * environment descriptor down instead of reporting an honest "unknown".
293525
294278
  */
293526
294279
  function readBakedBuildCommit() {
293527
- return "7f0d765bc59cb837f8aa8025b9b2f855df3fd36d";
294280
+ return "8cafbb87b6204b98e93c1c69253c1c590af5f8a1";
293528
294281
  }
293529
294282
  async function resolveServerBuildCommit(input) {
293530
294283
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -316008,7 +316761,7 @@ function claimClaudeReadinessJti(input) {
316008
316761
  return "claimed";
316009
316762
  }
316010
316763
  const readUsableClaudeGenerationHash = (config) => {
316011
- const configuredGenerationHash = process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
316764
+ const configuredGenerationHash = bootExportedPersonalClaudeGenerationHash();
316012
316765
  if (!/^[a-f0-9]{64}$/.test(configuredGenerationHash)) return succeed(null);
316013
316766
  return tryPromise(() => readFile$1(resolveLocalClaudeCredentialPath(config.forgeClaudeConfigPath), "utf8")).pipe(map$3((credentialJson) => isClaudeSubscriptionCredential(credentialJson) ? configuredGenerationHash : null), orElseSucceed(() => null));
316014
316767
  };
@@ -316523,7 +317276,7 @@ const PlatformServicesLive = unwrap(gen(function* () {
316523
317276
  }));
316524
317277
  const RuntimeReplicationLayerLive = RuntimeReplicationLive.pipe(provideMerge(RuntimeReplicationClientLive), provideMerge(RuntimeReplicationOutboxLive), provideMerge(ActiveHourMeterLive.pipe(provide$1(RuntimeReplicationOutboxLive))));
316525
317278
  const RuntimeDrainLayerLive = RuntimeDrainReporterLive.pipe(provideMerge(RuntimeDrainStateLive));
316526
- const RuntimeAwareProviderReactorsLive = mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(provide$1(RuntimeDrainLayerLive));
317279
+ const RuntimeAwareProviderReactorsLive = mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(provide$1(RuntimeDrainLayerLive), provide$1(ClaudeUsageTelemetryLive));
316527
317280
  const PipelineWorkerCoordinatorLayerLive = PipelineWorkerCoordinatorLive.pipe(provideMerge(PipelineRunClientLive));
316528
317281
  const AgentProtocolReactorLayerLive = AgentProtocolReactorLive.pipe(provideMerge(PipelineRunStoreLive));
316529
317282
  const PipelineRunCoordinatorLayerLive = PipelineRunCoordinatorLive.pipe(provideMerge(PipelineProviderAuthorityInstallerLive), provideMerge(PipelineChangeInspectorLive), provideMerge(PipelineContextArtifactStoreLive), provideMerge(PipelineModelPolicyLive), provideMerge(PipelineRunClientLive), provideMerge(PipelineRunStoreLive), provideMerge(PipelineRunWorkspaceAllocatorLive), provideMerge(PipelineStagePolicyLive), provideMerge(PipelineWorkerCoordinatorLayerLive));