@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.cjs CHANGED
@@ -16544,7 +16544,7 @@ function makeNetServiceShape(createServer = node_net.createServer) {
16544
16544
  }
16545
16545
  //#endregion
16546
16546
  //#region ../../packages/shared/src/releaseInfo.generated.ts
16547
- const FORGE_RELEASE_VERSION = "0.10.6";
16547
+ const FORGE_RELEASE_VERSION = "0.11.0";
16548
16548
  //#endregion
16549
16549
  //#region ../../packages/shared/src/hostProcess.ts
16550
16550
  /**
@@ -40972,7 +40972,7 @@ function splitPath(filename) {
40972
40972
  const parts = splitPathRe.exec(truncated);
40973
40973
  return parts ? parts.slice(1) : [];
40974
40974
  }
40975
- function dirname$6(path) {
40975
+ function dirname$7(path) {
40976
40976
  const result = splitPath(path);
40977
40977
  const root = result[0] || "";
40978
40978
  let dir = result[1];
@@ -58287,7 +58287,7 @@ function getSpotlightConfig(optionsSpotlight) {
58287
58287
  function normalizeWindowsPath(path) {
58288
58288
  return path.replace(/^[A-Z]:/, "").replace(/\\/g, "/");
58289
58289
  }
58290
- function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$6(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
58290
+ function createGetModuleFromFilename(basePath = process.argv[1] ? dirname$7(process.argv[1]) : process.cwd(), isWindows = node_path.sep === "\\") {
58291
58291
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
58292
58292
  return (filename) => {
58293
58293
  if (!filename) return;
@@ -65775,7 +65775,7 @@ function normalizeNumberish(value) {
65775
65775
  }
65776
65776
  //#endregion
65777
65777
  //#region package.json
65778
- var version$1 = "0.10.16";
65778
+ var version$1 = "0.11.0";
65779
65779
  //#endregion
65780
65780
  //#region src/sentry.ts
65781
65781
  const SERVER_APP_NAME = "forge-server";
@@ -92992,7 +92992,7 @@ var AnalyticsService = class AnalyticsService extends require_Schema$1.Service()
92992
92992
  var RuntimeReplication = class extends require_Schema$1.Service()("forge/runtimeReplication/Services/RuntimeReplication") {};
92993
92993
  //#endregion
92994
92994
  //#region src/runtimeDrain/forgeGitCommit.ts
92995
- const execFileAsync$6 = (0, node_util.promisify)(node_child_process.execFile);
92995
+ const execFileAsync$7 = (0, node_util.promisify)(node_child_process.execFile);
92996
92996
  const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
92997
92997
  /**
92998
92998
  * Resolve the commit this forge checkout actually booted from, so the
@@ -93016,7 +93016,7 @@ const FORGE_GIT_COMMIT_PATTERN = /^[0-9a-f]{7,40}$/i;
93016
93016
  * it yet.
93017
93017
  */
93018
93018
  let cached$1 = null;
93019
- async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$6(command, [...args], { timeout: 5e3 })) {
93019
+ async function resolveForgeGitCommit(execImpl = (command, args) => execFileAsync$7(command, [...args], { timeout: 5e3 })) {
93020
93020
  if (cached$1 !== null) return cached$1;
93021
93021
  try {
93022
93022
  const { stdout } = await execImpl("git", ["rev-parse", "HEAD"]);
@@ -94979,7 +94979,7 @@ function mightCarryUsage(line, provider) {
94979
94979
  * a line at a time, so the usual cause is a tail the scan opened mid-write,
94980
94980
  * and a count that keeps growing is a file the scan can no longer read.
94981
94981
  */
94982
- function parseJsonObject$1(line, diagnostics) {
94982
+ function parseJsonObject$2(line, diagnostics) {
94983
94983
  let parsed;
94984
94984
  try {
94985
94985
  parsed = JSON.parse(line);
@@ -95009,7 +95009,7 @@ function claudeDedupeKey(messageId, requestId) {
95009
95009
  return `${messageId ?? ""}:${requestId ?? ""}`;
95010
95010
  }
95011
95011
  function parseClaudeLine(line, diagnostics) {
95012
- const record = parseJsonObject$1(line, diagnostics);
95012
+ const record = parseJsonObject$2(line, diagnostics);
95013
95013
  if (record === null) return null;
95014
95014
  if (record["type"] !== "assistant") return null;
95015
95015
  const messageRecord = objectAt(record, "message");
@@ -95156,7 +95156,7 @@ function parseCodexTokenCount(record, payloadRecord, state) {
95156
95156
  };
95157
95157
  }
95158
95158
  function parseCodexLine(line, state, diagnostics) {
95159
- const record = parseJsonObject$1(line, diagnostics);
95159
+ const record = parseJsonObject$2(line, diagnostics);
95160
95160
  if (record === null) return null;
95161
95161
  const payloadRecord = objectAt(record, "payload");
95162
95162
  if (payloadRecord === null) return null;
@@ -99537,7 +99537,7 @@ function buildDiscoveryResult(probes) {
99537
99537
  }
99538
99538
  //#endregion
99539
99539
  //#region src/sourceControl/probe.ts
99540
- const execFileAsync$5 = (0, node_util.promisify)(node_child_process.execFile);
99540
+ const execFileAsync$6 = (0, node_util.promisify)(node_child_process.execFile);
99541
99541
  /**
99542
99542
  * How long a machine's toolchain answer stays good for.
99543
99543
  *
@@ -99548,7 +99548,7 @@ const execFileAsync$5 = (0, node_util.promisify)(node_child_process.execFile);
99548
99548
  */
99549
99549
  const DISCOVERY_TTL = "5 minutes";
99550
99550
  /** Probes time out because `gh auth status` reaches the network on a cold keyring. */
99551
- const PROBE_TIMEOUT_MS = 5e3;
99551
+ const PROBE_TIMEOUT_MS$1 = 5e3;
99552
99552
  /**
99553
99553
  * Runs one probe. A tool that is absent, fails, or hangs is reported the same
99554
99554
  * way — `null`, meaning "no answer" — because the discovery result only
@@ -99560,7 +99560,7 @@ const PROBE_TIMEOUT_MS = 5e3;
99560
99560
  */
99561
99561
  const probe = (command, args) => require_Schema$1.promise(async () => {
99562
99562
  try {
99563
- const { stdout, stderr } = await execFileAsync$5(command, [...args], { timeout: PROBE_TIMEOUT_MS });
99563
+ const { stdout, stderr } = await execFileAsync$6(command, [...args], { timeout: PROBE_TIMEOUT_MS$1 });
99564
99564
  const output = `${stdout}\n${stderr}`.trim();
99565
99565
  return { output: output.length === 0 ? null : output };
99566
99566
  } catch (cause) {
@@ -101099,6 +101099,7 @@ const CLAUDE_IDENTITY_KEYS = new Set([
101099
101099
  "provider",
101100
101100
  "credentialGenerationId"
101101
101101
  ]);
101102
+ const CLAUDE_IDENTITY_OPTIONAL_KEYS = new Set(["providerAccountIdentity"]);
101102
101103
  function isRecord$13(value) {
101103
101104
  return typeof value === "object" && value !== null && !Array.isArray(value);
101104
101105
  }
@@ -101123,6 +101124,10 @@ function daysInMonth$1(year, month) {
101123
101124
  function hasExactKeys$2(value, keys) {
101124
101125
  return Object.keys(value).every((key) => keys.has(key)) && Object.keys(value).length === keys.size;
101125
101126
  }
101127
+ function hasAllowedKeys(value, required, optional) {
101128
+ const keys = Object.keys(value);
101129
+ return keys.every((key) => required.has(key) || optional.has(key)) && [...required].every((key) => keys.includes(key));
101130
+ }
101126
101131
  function parseDeliveryIdentity(value, provider) {
101127
101132
  if (!isRecord$13(value)) return null;
101128
101133
  if (provider === "openai") return parseCodexDeliveryIdentity(value);
@@ -101137,12 +101142,25 @@ function parseCodexDeliveryIdentity(value) {
101137
101142
  providerAccountIdentity: value.providerAccountIdentity
101138
101143
  };
101139
101144
  }
101145
+ /**
101146
+ * Absent means the writer does not name the Provider Account yet, and the
101147
+ * identity keeps the shape it has always had. Present but malformed means the
101148
+ * identity is not the one CMMD signed, so it is refused the same way a
101149
+ * malformed Codex account identity is.
101150
+ */
101151
+ function claudeAccountFields(value) {
101152
+ if (value === void 0) return {};
101153
+ if (typeof value !== "string" || !PROVIDER_ACCOUNT_IDENTITY$2.test(value)) return null;
101154
+ return { providerAccountIdentity: value };
101155
+ }
101140
101156
  function parseClaudeDeliveryIdentity(value) {
101141
- if (!hasExactKeys$2(value, CLAUDE_IDENTITY_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
101157
+ const account = claudeAccountFields(value.providerAccountIdentity);
101158
+ 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;
101142
101159
  return {
101143
101160
  kind: "credential_generation",
101144
101161
  provider: "claude",
101145
- credentialGenerationId: value.credentialGenerationId
101162
+ credentialGenerationId: value.credentialGenerationId,
101163
+ ...account
101146
101164
  };
101147
101165
  }
101148
101166
  function parseProviderRuntimeDeliveryReceipt(value, expected) {
@@ -101181,10 +101199,49 @@ function readProviderDeliveryBinding(value) {
101181
101199
  identity
101182
101200
  };
101183
101201
  }
101202
+ /** The Provider Account a delivery identity names, or null when it names none. */
101203
+ function providerDeliveryAccount(identity) {
101204
+ return identity.kind === "provider_account" ? identity.providerAccountIdentity : identity.providerAccountIdentity ?? null;
101205
+ }
101206
+ function sameDeliveredCredential(left, right) {
101207
+ if (left.kind === "provider_account" && right.kind === "provider_account") return left.providerAccountIdentity === right.providerAccountIdentity;
101208
+ return left.kind === "credential_generation" && right.kind === "credential_generation" && left.credentialGenerationId === right.credentialGenerationId;
101209
+ }
101210
+ /**
101211
+ * Names how one thread's delivery binding moved, which is the question the
101212
+ * delivery layer could not answer while Claude's identity carried no account.
101213
+ *
101214
+ * `account_switch` needs both sides to name an account, the accounts to differ,
101215
+ * the delivered credential to change with them, and the runtime, machine and
101216
+ * epoch to hold. A switch changes the account and the generation together, so
101217
+ * two accounts on one generation would be two accounts sharing a credential,
101218
+ * and a switch that also moved the epoch changed more than the identity. Both
101219
+ * are `unknown`.
101220
+ *
101221
+ * The same delivered credential is `unchanged` whether or not either side names
101222
+ * an account, so a binding recorded before CMMD named the account still resumes
101223
+ * against the credential it ran on. A changed credential where either side
101224
+ * names no account is `unknown` rather than guessed at, which keeps an
101225
+ * account-less binding compared on exactly the fields it was compared on before.
101226
+ *
101227
+ * `providerDeliveryBindingsEqual` asks only whether the answer is `unchanged`.
101228
+ * `permitsProviderAccountTransition` in `provider/providerAccountTransition.ts`
101229
+ * asks whether it is `account_switch`, and is the Codex resume gate's rule
101230
+ * today. #3055 wires the Claude resume gate onto it.
101231
+ */
101232
+ function classifyProviderDeliveryBindingChange(prior, current) {
101233
+ if (prior.runtimeId !== current.runtimeId || prior.machineId !== current.machineId || prior.identity.kind !== current.identity.kind || prior.identity.provider !== current.identity.provider) return "unknown";
101234
+ const sameEpoch = prior.credentialEpoch === current.credentialEpoch;
101235
+ const sameCredential = sameDeliveredCredential(prior.identity, current.identity);
101236
+ const priorAccount = providerDeliveryAccount(prior.identity);
101237
+ const currentAccount = providerDeliveryAccount(current.identity);
101238
+ if (priorAccount !== null && currentAccount !== null && priorAccount !== currentAccount) return sameEpoch && !sameCredential ? "account_switch" : "unknown";
101239
+ if (sameEpoch && sameCredential) return "unchanged";
101240
+ if (priorAccount === null || currentAccount === null) return "unknown";
101241
+ return "credential_refresh";
101242
+ }
101184
101243
  function providerDeliveryBindingsEqual(left, right) {
101185
- 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;
101186
- if (left.identity.kind === "provider_account" && right.identity.kind === "provider_account") return left.identity.providerAccountIdentity === right.identity.providerAccountIdentity;
101187
- return left.identity.kind === "credential_generation" && right.identity.kind === "credential_generation" && left.identity.credentialGenerationId === right.identity.credentialGenerationId;
101244
+ return classifyProviderDeliveryBindingChange(left, right) === "unchanged";
101188
101245
  }
101189
101246
  //#endregion
101190
101247
  //#region src/provider/claudeReadinessReason.ts
@@ -101463,8 +101520,128 @@ const runClaudeLogout = (runtime) => new Promise((resolve, reject) => {
101463
101520
  });
101464
101521
  });
101465
101522
  //#endregion
101523
+ //#region src/usage/providerRateLimits.ts
101524
+ /**
101525
+ * `new Date(ms).toISOString()` throws past +-8.64e15 ms, so a `resetsAt` past
101526
+ * the same bound in seconds is refused rather than allowed to throw inside a
101527
+ * decoder whose whole job is to fail closed.
101528
+ */
101529
+ const MAX_RESETS_AT_SECONDS = 864e10;
101530
+ /**
101531
+ * One reset time in unix seconds as an ISO string, or null.
101532
+ *
101533
+ * Both mappers read reset times through this, so a malformed `resetsAt`
101534
+ * yields the null that means "this observation said nothing" instead of a
101535
+ * RangeError thrown out of a decoder the ingestion layer runs on every Claude
101536
+ * rate-limit event.
101537
+ */
101538
+ function resetsAtIso(resetsAt) {
101539
+ if (!Number.isFinite(resetsAt) || resetsAt <= 0 || resetsAt > MAX_RESETS_AT_SECONDS) return null;
101540
+ return (/* @__PURE__ */ new Date(resetsAt * 1e3)).toISOString();
101541
+ }
101542
+ /**
101543
+ * The SDK only ever reports ONE window per event. Only `five_hour` and
101544
+ * `seven_day` map to the two windows the Usage page shows; the
101545
+ * per-model/overage variants (seven_day_opus, overage, ...) are ignored here
101546
+ * on purpose rather than silently overwriting the plain 7-day figure.
101547
+ */
101548
+ function mapClaudeRateLimitInfo(info) {
101549
+ if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
101550
+ const kind = info.rateLimitType === "five_hour" ? "fiveHour" : info.rateLimitType === "seven_day" ? "sevenDay" : null;
101551
+ if (!kind) return null;
101552
+ const resetsAt = resetsAtIso(info.resetsAt);
101553
+ if (resetsAt === null) return null;
101554
+ return {
101555
+ kind,
101556
+ window: {
101557
+ usedPercent: Math.round(info.utilization * 100),
101558
+ resetsAt,
101559
+ status: info.status
101560
+ }
101561
+ };
101562
+ }
101563
+ /**
101564
+ * The one place a used-percent becomes a status. `unifiedWindows` carries no
101565
+ * per-window status of its own, and the `/usage` parser derives one the same
101566
+ * way, so both Claude usage sources agree on where "warning" starts.
101567
+ */
101568
+ function usageStatus(usedPercent) {
101569
+ if (usedPercent >= 100) return "rejected";
101570
+ return usedPercent >= 80 ? "allowed_warning" : "allowed";
101571
+ }
101572
+ /**
101573
+ * One untyped `utilization` as a whole percent, or nothing. The [0, 100] bound
101574
+ * matches `claudeUsageCli.ts`'s own bound on the `/usage` text, which in turn
101575
+ * matches CMMD's parser.
101576
+ */
101577
+ function readUsedPercent(utilization) {
101578
+ if (typeof utilization !== "number" || !Number.isFinite(utilization)) return null;
101579
+ const usedPercent = Math.round(utilization * 100);
101580
+ return usedPercent < 0 || usedPercent > 100 ? null : usedPercent;
101581
+ }
101582
+ /**
101583
+ * Reads one `unifiedWindows` entry, or nothing. Every field is checked because
101584
+ * none of them are typed: a non-object entry, a utilization that is not a
101585
+ * finite number in [0, 1], or a reset time that is not a positive finite
101586
+ * number of seconds all mean "this observation said nothing about this
101587
+ * window".
101588
+ */
101589
+ function readUnifiedWindow(raw) {
101590
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
101591
+ const { utilization, resetsAt } = raw;
101592
+ const usedPercent = readUsedPercent(utilization);
101593
+ if (usedPercent === null) return null;
101594
+ if (typeof resetsAt !== "number") return null;
101595
+ const resetsAtIsoValue = resetsAtIso(resetsAt);
101596
+ if (resetsAtIsoValue === null) return null;
101597
+ return {
101598
+ usedPercent,
101599
+ resetsAt: resetsAtIsoValue,
101600
+ status: usageStatus(usedPercent)
101601
+ };
101602
+ }
101603
+ /**
101604
+ * Reads the `unifiedWindows` sub-object of `rate_limit_info` into the two
101605
+ * rolling windows. The SDK rebuilds that sub-object on every turn from the
101606
+ * `anthropic-ratelimit-unified-{5h,7d}-utilization` and `-reset` response
101607
+ * headers.
101608
+ *
101609
+ * The top-level `rateLimitType` names one window per event and carries a
101610
+ * utilization only for `five_hour` or `seven_day`. This sub-object reports
101611
+ * every window it knows on every observation, which makes it the only
101612
+ * SDK-side source that can feed the Usage card without waking a Sprite and
101613
+ * delivering a credential.
101614
+ *
101615
+ * Its `seven_day_overage_included` window is skipped. `/usage` names each model
101616
+ * row after the display name Anthropic's usage API gives that model, one row
101617
+ * per allowlisted model, and the `7d_oi` header behind this window is one
101618
+ * aggregate that carries no name. Any name given to it here would be a guess
101619
+ * that could disagree with the rows the probe writes, so model rows come from
101620
+ * the probe alone.
101621
+ *
101622
+ * It is also undocumented internal surface stripped from the SDK's exported
101623
+ * types, so this decoder fails closed everywhere. It returns null when the
101624
+ * sub-object is absent or is not a plain object, and null when neither window
101625
+ * in it survives validation. A window that fails validation is reported as
101626
+ * null, and an unrecognized key is skipped, never fatal. A caller that gets
101627
+ * null has learned nothing and must not publish, because a partial write could
101628
+ * overwrite a good `/usage` reading with a guess.
101629
+ */
101630
+ function mapClaudeUnifiedWindows(info) {
101631
+ const raw = info.unifiedWindows;
101632
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return null;
101633
+ const windows = raw;
101634
+ const fiveHour = readUnifiedWindow(windows.five_hour);
101635
+ const sevenDay = readUnifiedWindow(windows.seven_day);
101636
+ if (!fiveHour && !sevenDay) return null;
101637
+ return {
101638
+ fiveHour,
101639
+ sevenDay
101640
+ };
101641
+ }
101642
+ //#endregion
101466
101643
  //#region src/usage/claudeUsageCli.ts
101467
- const execFileAsync$4 = (0, node_util.promisify)(node_child_process.execFile);
101644
+ const execFileAsync$5 = (0, node_util.promisify)(node_child_process.execFile);
101468
101645
  const USAGE_ARGS = [
101469
101646
  "--safe-mode",
101470
101647
  "--no-session-persistence",
@@ -101488,7 +101665,7 @@ const MONTH_BY_NAME = {
101488
101665
  Dec: 11
101489
101666
  };
101490
101667
  async function runClaudeUsageCommand(binaryPath, args, claudeConfigDir) {
101491
- const { stdout } = await execFileAsync$4(binaryPath, [...args], {
101668
+ const { stdout } = await execFileAsync$5(binaryPath, [...args], {
101492
101669
  env: {
101493
101670
  ...process.env,
101494
101671
  TZ: "UTC",
@@ -101563,10 +101740,6 @@ function parseResetAt(value, now) {
101563
101740
  });
101564
101741
  return resetAt === null || Number.isNaN(resetAt.getTime()) ? null : resetAt.toISOString();
101565
101742
  }
101566
- function usageStatus(usedPercent) {
101567
- if (usedPercent >= 100) return "rejected";
101568
- return usedPercent >= 80 ? "allowed_warning" : "allowed";
101569
- }
101570
101743
  function parseUsageLine(output, label, now) {
101571
101744
  const escapedLabel = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
101572
101745
  const match = new RegExp(`^${escapedLabel}: (\\d+(?:\\.\\d+)?)% used(?: · resets (.+))?$`, "m").exec(output);
@@ -101635,6 +101808,29 @@ async function fetchClaudeUsageViaOfficialClient(input) {
101635
101808
  };
101636
101809
  }
101637
101810
  //#endregion
101811
+ //#region src/usage/claudeUsageTelemetryRequest.ts
101812
+ /** The owner-session route for a Claude usage reading. */
101813
+ const CLAUDE_USAGE_TELEMETRY_PATH = "/api/forge/context/providers/usage/claude/telemetry";
101814
+ /**
101815
+ * Builds the body for either lane.
101816
+ *
101817
+ * `modelWindows` is sent only when the reading carries it. CMMD keeps the
101818
+ * stored model rows when the key is absent, so a reading that cannot name
101819
+ * model rows the way `/usage` does leaves the key out.
101820
+ */
101821
+ function claudeUsageTelemetryBody(input) {
101822
+ return {
101823
+ providerAccountId: input.attribution.providerAccountId,
101824
+ credentialGenerationId: input.attribution.credentialGenerationId,
101825
+ ...input.account,
101826
+ source: input.source,
101827
+ fiveHour: input.windows.fiveHour,
101828
+ sevenDay: input.windows.sevenDay,
101829
+ ...input.windows.modelWindows !== void 0 ? { modelWindows: input.windows.modelWindows } : {},
101830
+ observedAt: input.observedAt
101831
+ };
101832
+ }
101833
+ //#endregion
101638
101834
  //#region src/cmmd/syncClaudeCredential.ts
101639
101835
  /**
101640
101836
  * Card 1359 (harvest path A, Claude) — publish a personal Claude subscription
@@ -101650,7 +101846,6 @@ async function fetchClaudeUsageViaOfficialClient(input) {
101650
101846
  const USER_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/providers/claude/credential";
101651
101847
  const USER_CLAUDE_LINKED_ACCOUNT_PATH = "/api/forge/context/providers/claude/accounts";
101652
101848
  const AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH = "/api/forge/context/auth-runtime-claude-credential";
101653
- const USER_CLAUDE_USAGE_PATH = "/api/forge/context/providers/usage/claude/telemetry";
101654
101849
  const AUTH_RUNTIME_CLAUDE_USAGE_PATH = "/api/forge/context/auth-runtime-claude-credential/usage";
101655
101850
  /** Path to the local Claude credentials.json, mirroring Claude Code's resolution. */
101656
101851
  function resolveLocalClaudeCredentialPath(homePath) {
@@ -101679,7 +101874,7 @@ function resolveLocalClaudeCredentialPath(homePath) {
101679
101874
  * it reaches — this is the first gate and the only one near the user, who is
101680
101875
  * standing in front of the sign-in and can simply try again.
101681
101876
  *
101682
- * The floor is ONE token, not both, and that distinction is load-bearing: an
101877
+ * The floor is ONE token, not both, because an
101683
101878
  * expired or blank access token beside a live refresh token is the normal
101684
101879
  * resting state of a rotating login, which Claude Code refreshes silently on
101685
101880
  * use. Demanding both would reject a perfectly recoverable credential and break
@@ -101757,10 +101952,10 @@ function extractClaudeAccountMetadata(credentialJson) {
101757
101952
  return {};
101758
101953
  }
101759
101954
  }
101760
- const execFileAsync$3 = (0, node_util.promisify)(node_child_process.execFile);
101955
+ const execFileAsync$4 = (0, node_util.promisify)(node_child_process.execFile);
101761
101956
  async function loadClaudeAccountMetadataFromCli(homePath) {
101762
101957
  try {
101763
- const { stdout } = await execFileAsync$3(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
101958
+ const { stdout } = await execFileAsync$4(resolveClaudeBinaryPath("claude"), ["auth", "status"], {
101764
101959
  timeout: 2e4,
101765
101960
  maxBuffer: 1024 * 1024,
101766
101961
  env: {
@@ -101856,18 +102051,20 @@ async function publishClaudeUsageAfterSync(input) {
101856
102051
  binaryPath: resolveClaudeBinaryPath("claude"),
101857
102052
  ...input.homePath ? { claudeConfigDir: input.homePath } : {}
101858
102053
  });
101859
- await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : USER_CLAUDE_USAGE_PATH, {
101860
- providerAccountId: input.providerAccountId,
101861
- credentialGenerationId: input.credentialGenerationId,
101862
- accountEmail: input.accountEmail,
101863
- accountDisplayName: input.accountDisplayName,
101864
- planType: input.planType,
102054
+ await publishUsageBody(input.auth.jwt, input.auth.path === AUTH_RUNTIME_CLAUDE_CREDENTIAL_PATH ? AUTH_RUNTIME_CLAUDE_USAGE_PATH : CLAUDE_USAGE_TELEMETRY_PATH, claudeUsageTelemetryBody({
102055
+ attribution: {
102056
+ providerAccountId: input.providerAccountId,
102057
+ credentialGenerationId: input.credentialGenerationId
102058
+ },
102059
+ account: {
102060
+ accountEmail: input.accountEmail,
102061
+ accountDisplayName: input.accountDisplayName,
102062
+ planType: input.planType
102063
+ },
101865
102064
  source: "active_poll",
101866
- fiveHour: usage.fiveHour,
101867
- sevenDay: usage.sevenDay,
101868
- modelWindows: usage.modelWindows,
102065
+ windows: usage,
101869
102066
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
101870
- });
102067
+ }));
101871
102068
  return null;
101872
102069
  } catch (cause) {
101873
102070
  return { cause };
@@ -150195,7 +150392,7 @@ function issueCmmdLocalUserProviderEgressCredential(input) {
150195
150392
  });
150196
150393
  }
150197
150394
  //#endregion
150198
- //#region src/provider/codexAccountTransition.ts
150395
+ //#region src/provider/providerAccountTransition.ts
150199
150396
  function explicitProviderAccountTransition(fromAccountId, toAccountId) {
150200
150397
  if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) return void 0;
150201
150398
  return {
@@ -150203,13 +150400,20 @@ function explicitProviderAccountTransition(fromAccountId, toAccountId) {
150203
150400
  toAccountId
150204
150401
  };
150205
150402
  }
150206
- function permitsCodexAccountTransition(input) {
150403
+ /**
150404
+ * Whether a thread may move from `prior` to `current` because the person asked
150405
+ * for another Provider Account. The credential broker authorizes the selected
150406
+ * account, so only the identity may change, which is what `account_switch`
150407
+ * means.
150408
+ *
150409
+ * Nothing in the rule is specific to a provider, but the Codex resume gate in
150410
+ * `codexAppServerManager.ts` is its only caller. The Claude half is parked
150411
+ * until #3055 wires the Claude resume gate onto it.
150412
+ */
150413
+ function permitsProviderAccountTransition(input) {
150207
150414
  const { transition, requestedAccountId, prior, current } = input;
150208
- 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;
150209
- return providerDeliveryBindingsEqual({
150210
- ...prior,
150211
- identity: current.identity
150212
- }, current);
150415
+ if (!transition || transition.fromAccountId === transition.toAccountId || transition.toAccountId !== requestedAccountId) return false;
150416
+ return classifyProviderDeliveryBindingChange(prior, current) === "account_switch";
150213
150417
  }
150214
150418
  //#endregion
150215
150419
  //#region src/providerAuth/codexLocalAccountIdentity.ts
@@ -150709,7 +150913,7 @@ var CodexAppServerManager = class extends node_events.EventEmitter {
150709
150913
  const rawPriorDeliveryBinding = input.resumeCursor.providerDeliveryBinding;
150710
150914
  const priorDeliveryBinding = readProviderDeliveryBinding(rawPriorDeliveryBinding);
150711
150915
  if (rawPriorDeliveryBinding !== void 0 && rawPriorDeliveryBinding !== null && !priorDeliveryBinding) throw new Error("Existing Codex history cannot resume because its persisted delivered provider account binding is unreadable.");
150712
- if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !permitsCodexAccountTransition({
150916
+ if (priorDeliveryBinding && !providerDeliveryBindingsEqual(priorDeliveryBinding, currentDeliveryBinding) && !permitsProviderAccountTransition({
150713
150917
  transition: input.accountTransition,
150714
150918
  requestedAccountId: input.providerAccountId,
150715
150919
  prior: priorDeliveryBinding,
@@ -176783,7 +176987,7 @@ function em(e) {
176783
176987
  BO ??= new Int32Array(new SharedArrayBuffer(4)), Atomics.wait(BO, 0, 0, SY);
176784
176988
  }
176785
176989
  }
176786
- function rm$5(e, t) {
176990
+ function rm$6(e, t) {
176787
176991
  let n = (0, path.resolve)(t).toLowerCase(), r = (0, path.dirname)((0, path.resolve)(e)).toLowerCase();
176788
176992
  if (r === n || KO(r, n) || qO(r, n, "lexical")) return !0;
176789
176993
  let o = em(t)?.toLowerCase();
@@ -189633,7 +189837,7 @@ function Wo() {
189633
189837
  }
189634
189838
  function sle(e) {
189635
189839
  let t = process.cwd();
189636
- return e.filter((n) => !rm$5(n, t));
189840
+ return e.filter((n) => !rm$6(n, t));
189637
189841
  }
189638
189842
  function ZU(e) {
189639
189843
  let t = Bun.which(e);
@@ -211192,6 +211396,374 @@ function withoutInheritedAnthropicCredentials(env, options) {
211192
211396
  return contained;
211193
211397
  }
211194
211398
  //#endregion
211399
+ //#region src/provider/personalClaudeInVm.ts
211400
+ /**
211401
+ * CMMD writes this marker when it activates a personal Claude subscription
211402
+ * inside a running sprite. Activation always happens after boot, so a
211403
+ * boot-time env var alone cannot reflect it (FOR-36249).
211404
+ *
211405
+ * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
211406
+ * Without that, the marker sits directly under FORGE_HOME, which is the same
211407
+ * guest home CMMD derives every sprite path from. A server with neither has no
211408
+ * sprite home to look in, and a desktop never has a marker, so it reads none.
211409
+ */
211410
+ const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
211411
+ const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
211412
+ function markerPath(env) {
211413
+ if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
211414
+ if (env.FORGE_HOME) return (0, node_path.join)(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
211415
+ return null;
211416
+ }
211417
+ /**
211418
+ * True when this runtime should offer the personal Claude subscription lane.
211419
+ *
211420
+ * Accepts either signal: the boot-time env var (backward compatible) or the
211421
+ * activation marker on disk. Reading the marker at call time lets the provider
211422
+ * catalog and the per-turn readiness guard self-heal once CMMD delivers,
211423
+ * without a sprite reboot. Callers still verify the credential is non-empty
211424
+ * before treating it as usable; this only decides whether the lane is active.
211425
+ */
211426
+ function personalClaudeInVmEnabled(env = process.env) {
211427
+ if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
211428
+ const marker = markerPath(env);
211429
+ if (!marker) return false;
211430
+ try {
211431
+ return (0, node_fs.existsSync)(marker);
211432
+ } catch {
211433
+ return false;
211434
+ }
211435
+ }
211436
+ /** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
211437
+ function bootExportedPersonalClaudeGenerationHash(env = process.env) {
211438
+ return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
211439
+ }
211440
+ /**
211441
+ * The sha256 of the Claude credential generation this runtime holds, or "" when
211442
+ * it holds none. The readiness guard compares it with the generation CMMD
211443
+ * applied. The restart proof must not use it; it uses the boot-exported hash.
211444
+ *
211445
+ * The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
211446
+ * the marker already exists at boot. Activation in place writes the generation
211447
+ * file and touches the marker without setting any env, so an empty variable
211448
+ * falls back to that file, hashed the way the preload hashes it. CMMD puts the
211449
+ * file beside the marker in the guest home and names no path for it. A marker
211450
+ * with no generation file is a legacy runtime, and gets no hash here either.
211451
+ */
211452
+ function personalClaudeGenerationHash(env = process.env) {
211453
+ const exported = bootExportedPersonalClaudeGenerationHash(env);
211454
+ if (exported) return exported;
211455
+ const marker = markerPath(env);
211456
+ if (!marker) return "";
211457
+ try {
211458
+ if (!(0, node_fs.existsSync)(marker)) return "";
211459
+ const generation = (0, node_fs.readFileSync)((0, node_path.join)((0, node_path.dirname)(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
211460
+ return generation ? (0, node_crypto.createHash)("sha256").update(generation).digest("hex") : "";
211461
+ } catch {
211462
+ return "";
211463
+ }
211464
+ }
211465
+ //#endregion
211466
+ //#region src/provider/claudeCredentialDirectory.ts
211467
+ /**
211468
+ * Per-account credential directories for Claude turns.
211469
+ *
211470
+ * Claude Code keeps conversation transcripts under
211471
+ * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
211472
+ * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
211473
+ * credential file, so each Provider Account can hold its own login while every
211474
+ * session file stays in one place and `--resume` keeps finding its target. See
211475
+ * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
211476
+ *
211477
+ * That variable is not documented. Under
211478
+ * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
211479
+ * only allowed because absence degrades to a named behaviour that already
211480
+ * works: one account per Environment, exactly what runs today. This module is
211481
+ * the gate that keeps the degrade honest. It sets the variable only when a
211482
+ * readable `.credentials.json` is already sitting in the resolved directory,
211483
+ * because a missing or empty directory makes the CLI report logged out and a
211484
+ * live turn would fail. Nothing writes these directories yet, so every runtime
211485
+ * today takes the skip path.
211486
+ *
211487
+ * Pure and dependency-free on purpose, for the same reason as
211488
+ * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
211489
+ * services, the browser manager and the server config, so a unit test that
211490
+ * imports it cannot run in a worktree. This rule decides which account pays for
211491
+ * a turn, so it has to stay directly testable.
211492
+ */
211493
+ /** Repoints `.credentials.json` without moving the transcripts. */
211494
+ const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
211495
+ /** The one file Claude Code writes into the credential directory. */
211496
+ const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
211497
+ /**
211498
+ * Runtime env var naming the Provider Account this runtime's Claude turns
211499
+ * belong to.
211500
+ *
211501
+ * NOTHING SETS THIS. It has no writer anywhere in Forge or CMMD, so as shipped
211502
+ * the per-account lane can never turn on and every runtime keeps today's single
211503
+ * account. The writer waits on issue 3073, which decides which identity keys
211504
+ * the directory. That has to be settled first: the credential generation
211505
+ * rotates on every re-delivery, so keying on it would give a new directory
211506
+ * each time and defeat the point.
211507
+ *
211508
+ * It is also not the seam. The seam is the `credentialDirectory` parameter on
211509
+ * `ClaudeAdapter.buildClaudeQueryEnv`, which is what tests and callers drive.
211510
+ * This variable is one bridge into that parameter, and the delivery step is
211511
+ * free to replace it with something better rather than adopt it.
211512
+ */
211513
+ const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
211514
+ /** Operator escape hatch for the directory root, mostly for local debugging. */
211515
+ const CLAUDE_CREDENTIAL_ROOT_ENV = "FORGE_CLAUDE_CREDENTIAL_ROOT";
211516
+ const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
211517
+ const FALLBACK_ROOT_DIRECTORY_NAME = ".forge-claude-credentials";
211518
+ const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
211519
+ const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
211520
+ const DIRECTORY_NAME_HASH_LENGTH = 32;
211521
+ /**
211522
+ * Directory name for an account. A hash, so no email or display name reaches a
211523
+ * filesystem path and no path-illegal character can appear in one either.
211524
+ */
211525
+ function claudeCredentialDirectoryName(accountIdentity) {
211526
+ return `account-${(0, node_crypto.createHash)("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
211527
+ }
211528
+ /** The account this runtime's Claude turns belong to, or null on every runtime today. */
211529
+ function resolveClaudeProviderAccountIdentity(env) {
211530
+ const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
211531
+ if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
211532
+ return identity;
211533
+ }
211534
+ /**
211535
+ * Where per-account credential directories live. Always outside
211536
+ * `CLAUDE_CONFIG_DIR`, so one of them can never be mistaken for the main
211537
+ * credential store, and so a stray read of the config directory cannot walk
211538
+ * into another account's login.
211539
+ */
211540
+ function claudeCredentialDirectoryRoot(env) {
211541
+ const root = credentialRootCandidate(env);
211542
+ if (!root || !(0, node_path.isAbsolute)(root)) return null;
211543
+ return isInside(root, claudeConfigDirectory(env)) ? null : (0, node_path.resolve)(root);
211544
+ }
211545
+ /** Absolute credential directory for one account, or null when either half is missing. */
211546
+ function resolveClaudeCredentialDirectory(input) {
211547
+ if (!input.accountIdentity) return null;
211548
+ const root = claudeCredentialDirectoryRoot(input.env);
211549
+ if (!root) return null;
211550
+ return (0, node_path.join)(root, claudeCredentialDirectoryName(input.accountIdentity));
211551
+ }
211552
+ /**
211553
+ * Decide whether this turn may point at a per-account credential directory.
211554
+ *
211555
+ * The cheap checks run first and the capability probe runs last, so a runtime
211556
+ * with no delivered credential never spawns the CLI. Any failure is a skip, so
211557
+ * the worst outcome is today's behaviour.
211558
+ */
211559
+ async function decideClaudeCredentialDirectory(input) {
211560
+ if (!input.accountIdentity) return skip("no-account");
211561
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
211562
+ const directory = resolveClaudeCredentialDirectory(input);
211563
+ if (!directory) return skip("no-root");
211564
+ if (!(input.credentialFilePresent ?? hasReadableCredentialFile)(directory)) return skip("no-credential-file");
211565
+ return await probeSupportSafely(input.probeSupport) === "supported" ? {
211566
+ outcome: "set",
211567
+ directory
211568
+ } : skip("capability-not-supported");
211569
+ }
211570
+ /** True when `.credentials.json` is in the directory and readable right now. */
211571
+ function hasReadableCredentialFile(directory) {
211572
+ const file = (0, node_path.join)(directory, CLAUDE_CREDENTIAL_FILE_NAME);
211573
+ try {
211574
+ if (!(0, node_fs.statSync)(file).isFile()) return false;
211575
+ (0, node_fs.accessSync)(file, node_fs.constants.R_OK);
211576
+ return true;
211577
+ } catch {
211578
+ return false;
211579
+ }
211580
+ }
211581
+ function skip(reason) {
211582
+ return {
211583
+ outcome: "skipped",
211584
+ reason
211585
+ };
211586
+ }
211587
+ /** `inconclusive` is treated as `unsupported`, and so is a probe that throws. */
211588
+ async function probeSupportSafely(probeSupport) {
211589
+ try {
211590
+ return await probeSupport();
211591
+ } catch {
211592
+ return "inconclusive";
211593
+ }
211594
+ }
211595
+ function credentialRootCandidate(env) {
211596
+ const explicit = env[CLAUDE_CREDENTIAL_ROOT_ENV]?.trim();
211597
+ if (explicit) return explicit;
211598
+ const forgeHome = env.FORGE_HOME?.trim();
211599
+ if (forgeHome) return (0, node_path.join)(forgeHome, CREDENTIAL_ROOT_DIRECTORY_NAME);
211600
+ try {
211601
+ return (0, node_path.join)((0, node_os.homedir)(), FALLBACK_ROOT_DIRECTORY_NAME);
211602
+ } catch {
211603
+ return null;
211604
+ }
211605
+ }
211606
+ function claudeConfigDirectory(env) {
211607
+ const configured = env.CLAUDE_CONFIG_DIR?.trim();
211608
+ if (configured) return configured;
211609
+ try {
211610
+ return (0, node_path.join)((0, node_os.homedir)(), DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME);
211611
+ } catch {
211612
+ return null;
211613
+ }
211614
+ }
211615
+ function isInside(candidate, parent) {
211616
+ if (!parent) return false;
211617
+ const step = (0, node_path.relative)((0, node_path.resolve)(parent), (0, node_path.resolve)(candidate));
211618
+ return step === "" || !step.startsWith("..") && !(0, node_path.isAbsolute)(step);
211619
+ }
211620
+ //#endregion
211621
+ //#region src/provider/claudeCredentialDirectoryProbe.ts
211622
+ /**
211623
+ * Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
211624
+ *
211625
+ * The variable is unpublished, so a runtime is asked rather than assumed to
211626
+ * honour it (`docs/adr/unpublished-provider-surface-needs-a-fallback.md`). The
211627
+ * question the probe can actually answer: point the variable at an empty
211628
+ * directory and a runtime that honours it flips from logged in to logged out.
211629
+ * A runtime with no credential at all is logged out either way and cannot
211630
+ * discriminate, which is `inconclusive`, and callers read that as unsupported.
211631
+ *
211632
+ * `claude auth status` is a pure read. It sends no prompt and leaves
211633
+ * `.credentials.json` byte-identical, so probing spends no subscription usage.
211634
+ * It prints JSON on stdout and exits 1 when logged out, so the exit code is not
211635
+ * a failure signal and the `loggedIn` field is what gets read.
211636
+ */
211637
+ const execFileAsync$3 = (0, node_util.promisify)(node_child_process.execFile);
211638
+ const AUTH_STATUS_ARGS = ["auth", "status"];
211639
+ const VERSION_ARGS = ["--version"];
211640
+ /**
211641
+ * The probe sits in front of a turn, so a provider process that never answers
211642
+ * must not hold that turn open. `execFile` kills the child at this bound, and
211643
+ * `runBounded` gives up on it at the same bound whatever the deps do, so a
211644
+ * hung or unkillable process costs a few seconds and an `inconclusive`.
211645
+ */
211646
+ const PROBE_TIMEOUT_MS = 5e3;
211647
+ const PROBE_DIRECTORY_PREFIX = "forge-claude-capability-";
211648
+ const VERSION_PATTERN = /\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/;
211649
+ function makeClaudeCredentialDirectoryProbe(deps) {
211650
+ const answers = /* @__PURE__ */ new Map();
211651
+ return { support: async () => {
211652
+ const version = await readVersionSafely(deps);
211653
+ if (version === null) return runProbe(deps);
211654
+ const cached = answers.get(version);
211655
+ if (cached) return cached;
211656
+ const pending = runProbe(deps);
211657
+ answers.set(version, pending);
211658
+ return pending;
211659
+ } };
211660
+ }
211661
+ /**
211662
+ * One probe per binary path for the life of the process. The probe spawns the
211663
+ * CLI twice, and every Claude session start would otherwise pay for it.
211664
+ */
211665
+ const probesByBinaryPath = /* @__PURE__ */ new Map();
211666
+ function claudeCredentialDirectoryProbeFor(binaryPath) {
211667
+ const binary = binaryPath?.trim() || "claude";
211668
+ const existing = probesByBinaryPath.get(binary);
211669
+ if (existing) return existing;
211670
+ const probe = makeClaudeCredentialDirectoryProbe(claudeCredentialDirectoryProbeDeps(binary));
211671
+ probesByBinaryPath.set(binary, probe);
211672
+ return probe;
211673
+ }
211674
+ function claudeCredentialDirectoryProbeDeps(binaryPath) {
211675
+ return {
211676
+ runAuthStatus: ({ credentialDirectory }) => runClaudeCommand$1(binaryPath, AUTH_STATUS_ARGS, credentialDirectory),
211677
+ readBinaryVersion: async () => {
211678
+ const { stdout } = await runClaudeCommand$1(binaryPath, VERSION_ARGS, null);
211679
+ return VERSION_PATTERN.exec(stdout)?.[0] ?? null;
211680
+ },
211681
+ makeProbeDirectory: () => (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), PROBE_DIRECTORY_PREFIX)),
211682
+ removeProbeDirectory: (directory) => (0, node_fs_promises.rm)(directory, {
211683
+ recursive: true,
211684
+ force: true
211685
+ })
211686
+ };
211687
+ }
211688
+ async function runProbe(deps) {
211689
+ let probeDirectory = null;
211690
+ try {
211691
+ if (parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: null }))) !== true) return "inconclusive";
211692
+ probeDirectory = await deps.makeProbeDirectory();
211693
+ const directory = probeDirectory;
211694
+ const redirected = parseLoggedIn(await runBounded(deps, () => deps.runAuthStatus({ credentialDirectory: directory })));
211695
+ if (redirected === false) return "supported";
211696
+ return redirected === true ? "unsupported" : "inconclusive";
211697
+ } catch {
211698
+ return "inconclusive";
211699
+ } finally {
211700
+ if (probeDirectory !== null) await deps.removeProbeDirectory(probeDirectory).catch(() => void 0);
211701
+ }
211702
+ }
211703
+ async function readVersionSafely(deps) {
211704
+ try {
211705
+ return await runBounded(deps, () => deps.readBinaryVersion());
211706
+ } catch {
211707
+ return null;
211708
+ }
211709
+ }
211710
+ /**
211711
+ * Give up on a call that outlives the bound. The underlying promise is left to
211712
+ * settle on its own; the real deps hand `execFile` the same bound, so its child
211713
+ * is already being killed by the time this rejects.
211714
+ */
211715
+ async function runBounded(deps, operation) {
211716
+ const timeoutMs = deps.timeoutMs ?? PROBE_TIMEOUT_MS;
211717
+ let timer;
211718
+ try {
211719
+ return await Promise.race([operation(), new Promise((_resolve, reject) => {
211720
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("claude auth status timed out")), timeoutMs);
211721
+ })]);
211722
+ } finally {
211723
+ if (timer !== void 0) clearTimeout(timer);
211724
+ }
211725
+ }
211726
+ /** The `loggedIn` field, or null when the output does not carry one. */
211727
+ function parseLoggedIn(run) {
211728
+ const parsed = parseJsonObject$1(run.stdout);
211729
+ return typeof parsed?.loggedIn === "boolean" ? parsed.loggedIn : null;
211730
+ }
211731
+ function parseJsonObject$1(output) {
211732
+ const trimmed = output.trim();
211733
+ const start = trimmed.indexOf("{");
211734
+ const end = trimmed.lastIndexOf("}");
211735
+ if (start === -1 || end <= start) return null;
211736
+ try {
211737
+ const parsed = JSON.parse(trimmed.slice(start, end + 1));
211738
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
211739
+ } catch {
211740
+ return null;
211741
+ }
211742
+ }
211743
+ async function runClaudeCommand$1(binaryPath, args, credentialDirectory) {
211744
+ try {
211745
+ const { stdout } = await execFileAsync$3(binaryPath, [...args], {
211746
+ env: {
211747
+ ...process.env,
211748
+ ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
211749
+ },
211750
+ maxBuffer: 1024 * 1024,
211751
+ timeout: PROBE_TIMEOUT_MS
211752
+ });
211753
+ return {
211754
+ stdout,
211755
+ exitCode: 0
211756
+ };
211757
+ } catch (cause) {
211758
+ const failure = cause;
211759
+ if (typeof failure.stdout !== "string") throw cause;
211760
+ return {
211761
+ stdout: failure.stdout,
211762
+ exitCode: typeof failure.code === "number" ? failure.code : null
211763
+ };
211764
+ }
211765
+ }
211766
+ //#endregion
211195
211767
  //#region src/provider/makeManagedServerProvider.ts
211196
211768
  const makeManagedServerProvider = require_Schema$1.fn("makeManagedServerProvider")(function* (input) {
211197
211769
  const refreshSemaphore = yield* require_Path.make$5(1);
@@ -211283,43 +211855,6 @@ const makeProviderAuthProvenance = (input) => {
211283
211855
  };
211284
211856
  };
211285
211857
  //#endregion
211286
- //#region src/provider/personalClaudeInVm.ts
211287
- /**
211288
- * CMMD writes this marker when it activates a personal Claude subscription
211289
- * inside a running sprite. Activation always happens after boot, so a
211290
- * boot-time env var alone cannot reflect it (FOR-36249).
211291
- *
211292
- * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
211293
- * Without that, the marker sits directly under FORGE_HOME, which is the same
211294
- * guest home CMMD derives every sprite path from. A server with neither has no
211295
- * sprite home to look in, and a desktop never has a marker, so it reads none.
211296
- */
211297
- const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
211298
- function markerPath(env) {
211299
- if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
211300
- if (env.FORGE_HOME) return (0, node_path.join)(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
211301
- return null;
211302
- }
211303
- /**
211304
- * True when this runtime should offer the personal Claude subscription lane.
211305
- *
211306
- * Accepts either signal: the boot-time env var (backward compatible) or the
211307
- * activation marker on disk. Reading the marker at call time lets the provider
211308
- * catalog and the per-turn readiness guard self-heal once CMMD delivers,
211309
- * without a sprite reboot. Callers still verify the credential is non-empty
211310
- * before treating it as usable; this only decides whether the lane is active.
211311
- */
211312
- function personalClaudeInVmEnabled(env = process.env) {
211313
- if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
211314
- const marker = markerPath(env);
211315
- if (!marker) return false;
211316
- try {
211317
- return (0, node_fs.existsSync)(marker);
211318
- } catch {
211319
- return false;
211320
- }
211321
- }
211322
- //#endregion
211323
211858
  //#region src/provider/Layers/ClaudeProvider.ts
211324
211859
  function endpointProfileDisplayNames(settings) {
211325
211860
  return Object.fromEntries(settings.endpointProfiles.filter((p) => p.displayName).map((p) => [p.slug, p.displayName]));
@@ -212738,10 +213273,27 @@ function claudeForgeAttributionPrompt() {
212738
213273
  const attribution = resolveAgentAttribution();
212739
213274
  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}`;
212740
213275
  }
212741
- function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential) {
213276
+ /**
213277
+ * `credentialDirectory` points this turn at one Provider Account's own
213278
+ * `.credentials.json` without moving the transcripts, which is what lets an
213279
+ * account switch keep `--resume` working
213280
+ * (`docs/adr/claude-accounts-split-credentials-not-sessions.md`). It is null on
213281
+ * every runtime today: `decideClaudeCredentialDirectory` only hands back a
213282
+ * directory that already holds a readable credential file, and nothing writes
213283
+ * one yet, so the environment stays byte-for-byte what it was.
213284
+ *
213285
+ * It applies to the personal-subscription branch below and to nothing else. On
213286
+ * the profile / proxy branch the credential file is not what authenticates the
213287
+ * turn, an API key is, so pointing the variable there would add a second source
213288
+ * of truth that a later reader has to rule out.
213289
+ */
213290
+ function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
212742
213291
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
212743
213292
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
212744
- if (!effectiveBaseUrl) return withoutInheritedAnthropicCredentials(baseEnv, { stripEndpointOverride: baseProcessEnv.FORGE_PERSONAL_CLAUDE_IN_VM === "true" });
213293
+ if (!effectiveBaseUrl) return {
213294
+ ...withoutInheritedAnthropicCredentials(baseEnv, { stripEndpointOverride: personalClaudeInVmEnabled(baseProcessEnv) }),
213295
+ ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
213296
+ };
212745
213297
  const effectiveApiKey = resolveClaudeQueryApiKey(endpointProfile, claudeSettings, providerProxyCredential?.token ?? null);
212746
213298
  return {
212747
213299
  ...baseEnv,
@@ -215010,6 +215562,11 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
215010
215562
  })));
215011
215563
  const claudeSettings = serverSettings.providers.claudeAgent;
215012
215564
  const claudeBinaryPath = claudeSettings.binaryPath;
215565
+ const claudeCredentialDirectory = yield* require_Schema$1.tryPromise(() => decideClaudeCredentialDirectory({
215566
+ env: process.env,
215567
+ accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
215568
+ probeSupport: () => claudeCredentialDirectoryProbeFor(claudeBinaryPath).support()
215569
+ })).pipe(require_Schema$1.map((decision) => decision.outcome === "set" ? decision.directory : null), require_Schema$1.orElseSucceed(() => null));
215013
215570
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
215014
215571
  const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
215015
215572
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
@@ -215034,7 +215591,7 @@ const makeClaudeAdapter = require_Schema$1.fn("makeClaudeAdapter")(function* (op
215034
215591
  existingResumeSessionId,
215035
215592
  newSessionId,
215036
215593
  canUseTool,
215037
- env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential),
215594
+ env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
215038
215595
  browserManager,
215039
215596
  threadId,
215040
215597
  browserToolEnvironmentId,
@@ -230138,7 +230695,7 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
230138
230695
  });
230139
230696
  }
230140
230697
  function requiresNativeClaudeReadiness(config, provider) {
230141
- return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && process.env.FORGE_PERSONAL_CLAUDE_IN_VM === "true";
230698
+ return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
230142
230699
  }
230143
230700
  function resolveGuardAuthority(config, readSessionId, input) {
230144
230701
  const runtimeId = config.forgeCmmdRuntimeId?.trim();
@@ -230150,7 +230707,7 @@ function resolveGuardAuthority(config, readSessionId, input) {
230150
230707
  runtimeId,
230151
230708
  machineId,
230152
230709
  credentialEpoch,
230153
- localGenerationHash: process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "",
230710
+ localGenerationHash: personalClaudeGenerationHash(),
230154
230711
  sessionId
230155
230712
  };
230156
230713
  }
@@ -287365,25 +287922,124 @@ const makeRuntimeDrainReporter = require_Schema$1.gen(function* () {
287365
287922
  var RuntimeDrainReporter = class extends require_Schema$1.Service()("forge/runtimeDrain/RuntimeDrainReporter") {};
287366
287923
  const RuntimeDrainReporterLive = require_Schema$1.effect(RuntimeDrainReporter, makeRuntimeDrainReporter);
287367
287924
  //#endregion
287368
- //#region src/usage/providerRateLimits.ts
287369
- /**
287370
- * The SDK only ever reports ONE window per event. Only `five_hour` and
287371
- * `seven_day` map to the two windows the Usage page shows; the
287372
- * per-model/overage variants (seven_day_opus, overage, ...) are ignored here
287373
- * on purpose rather than silently overwriting the plain 7-day figure.
287374
- */
287375
- function mapClaudeRateLimitInfo(info) {
287376
- if (typeof info.utilization !== "number" || typeof info.resetsAt !== "number" || !info.status) return null;
287377
- const kind = info.rateLimitType === "five_hour" ? "fiveHour" : info.rateLimitType === "seven_day" ? "sevenDay" : null;
287378
- if (!kind) return null;
287379
- return {
287380
- kind,
287381
- window: {
287382
- usedPercent: Math.round(info.utilization * 100),
287383
- resetsAt: (/* @__PURE__ */ new Date(info.resetsAt * 1e3)).toISOString(),
287384
- status: info.status
287385
- }
287386
- };
287925
+ //#region src/usage/Services/ClaudeUsageTelemetry.ts
287926
+ /**
287927
+ * Claude usage the SDK reports on every turn, published as passive telemetry.
287928
+ *
287929
+ * Every Claude turn carries a `rate_limit_event` whose `unifiedWindows`
287930
+ * sub-object holds the two rolling windows the Usage card renders. Reading it
287931
+ * costs nothing: no `claude` process, no credential delivery, no woken sprite.
287932
+ *
287933
+ * This module is the whole publish lane behind one call. `observe` takes what
287934
+ * an observation knows and returns void. Whether the floor admits it, which
287935
+ * credential generation it ran on, whether the value is new, and whether CMMD
287936
+ * accepted it are all decided here, so the ingestion layer stays a decoder and
287937
+ * a hand-off. Nothing decided here can fail a turn, and no admitted
287938
+ * observation outlives `CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS`.
287939
+ *
287940
+ * The `/usage` probe in `syncClaudeCredential.ts` is a separate lane that
287941
+ * publishes `source: "active_poll"`. This one publishes
287942
+ * `source: "passive_sdk_telemetry"`, and both lanes build their body with
287943
+ * `claudeUsageTelemetryBody`.
287944
+ *
287945
+ * @module ClaudeUsageTelemetry
287946
+ */
287947
+ var ClaudeUsageTelemetry = class extends require_Schema$1.Service()("forge/usage/Services/ClaudeUsageTelemetry") {};
287948
+ /**
287949
+ * The longest one admitted observation may take, from the credential
287950
+ * generation lookup through the CMMD credential and the POST, before it is
287951
+ * abandoned and counted as failed. Ingestion does not wait on it, so this
287952
+ * bounds how long a slow lookup or a slow CMMD keeps a fiber alive, not how
287953
+ * long a turn waits. Together with the floor it caps how many observations for
287954
+ * one account can be in flight at once.
287955
+ */
287956
+ const CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS = 1e4;
287957
+ /** How many accounts, and how many pairs, the observer remembers before evicting the oldest. */
287958
+ const MAX_TRACKED_KEYS = 32;
287959
+ /**
287960
+ * Why one publish did not land. Every reason is the same to `observe`, which
287961
+ * logs it and moves on, so the type exists to name the lane rather than to be
287962
+ * matched on.
287963
+ */
287964
+ var ClaudeUsageTelemetryPublishError = class extends require_Schema$1.TaggedError("ClaudeUsageTelemetryPublishError") {};
287965
+ function pairKey(attribution) {
287966
+ return `${attribution.providerAccountId}\u0000${attribution.credentialGenerationId}`;
287967
+ }
287968
+ function windowFingerprint(value) {
287969
+ return value === null ? "-" : `${value.usedPercent}|${value.resetsAt}|${value.status}`;
287970
+ }
287971
+ /**
287972
+ * A value fingerprint built field by field rather than by `JSON.stringify` on
287973
+ * the whole object, so the comparison does not depend on key insertion order
287974
+ * and cannot silently start ignoring a field a future mapper adds without a
287975
+ * matching change here.
287976
+ */
287977
+ function windowsFingerprint(windows) {
287978
+ return `${windowFingerprint(windows.fiveHour)}/${windowFingerprint(windows.sevenDay)}`;
287979
+ }
287980
+ /**
287981
+ * Stores `value` as the newest entry for `key` and evicts the oldest entry past
287982
+ * the cap. Map iteration is insertion-ordered, so the first key is the one
287983
+ * touched longest ago. An idle account or a rotated credential generation
287984
+ * rarely comes back, so evicting it costs at most one early lookup or one
287985
+ * redundant publish.
287986
+ */
287987
+ function remember(map, key, value) {
287988
+ map.delete(key);
287989
+ map.set(key, value);
287990
+ if (map.size > MAX_TRACKED_KEYS) {
287991
+ const oldest = map.keys().next();
287992
+ if (!oldest.done) map.delete(oldest.value);
287993
+ }
287994
+ }
287995
+ /**
287996
+ * Builds the observer over injected dependencies. The HTTP call and the clock
287997
+ * both arrive from outside, so a test drives the whole policy without a network
287998
+ * or a timer.
287999
+ */
288000
+ function makeClaudeUsageTelemetry(dependencies) {
288001
+ const now = dependencies.now ?? (() => Date.now());
288002
+ /** When each account last had an observation admitted, whatever came of it. */
288003
+ const admittedAt = /* @__PURE__ */ new Map();
288004
+ /** The fingerprint of the last value CMMD accepted, per pair. */
288005
+ const publishedFingerprints = /* @__PURE__ */ new Map();
288006
+ const publishIfNew = (windows, providerAccountId, lookupCredentialGenerationId, at) => require_Schema$1.gen(function* () {
288007
+ const credentialGenerationId = yield* lookupCredentialGenerationId;
288008
+ if (!credentialGenerationId) return;
288009
+ const attribution = {
288010
+ providerAccountId,
288011
+ credentialGenerationId
288012
+ };
288013
+ const key = pairKey(attribution);
288014
+ const fingerprint = windowsFingerprint(windows);
288015
+ if (publishedFingerprints.get(key) === fingerprint) return;
288016
+ yield* dependencies.publish(claudeUsageTelemetryBody({
288017
+ attribution,
288018
+ source: "passive_sdk_telemetry",
288019
+ windows,
288020
+ observedAt: new Date(at).toISOString()
288021
+ }));
288022
+ remember(publishedFingerprints, key, fingerprint);
288023
+ });
288024
+ /**
288025
+ * The floor is read and claimed inside the returned effect, never while
288026
+ * building it, and before anything suspends. Two turns on one account can be
288027
+ * in flight at once, and the floor only holds them apart if the second one
288028
+ * reads what the first one wrote.
288029
+ */
288030
+ const observe = (observation) => require_Schema$1.suspend$1(() => {
288031
+ const { windows, providerAccountId } = observation;
288032
+ if (!windows || !providerAccountId) return require_Schema$1.void_;
288033
+ const at = now();
288034
+ const lastAdmittedAt = admittedAt.get(providerAccountId);
288035
+ if (lastAdmittedAt !== void 0 && at - lastAdmittedAt < 5e3) return require_Schema$1.void_;
288036
+ remember(admittedAt, providerAccountId, at);
288037
+ return publishIfNew(windows, providerAccountId, observation.credentialGenerationId, at).pipe(require_Schema$1.timeout(CLAUDE_USAGE_TELEMETRY_PUBLISH_TIMEOUT_MS), require_Schema$1.catchCause((cause) => require_Schema$1.logWarning("claude usage passive telemetry publish failed", {
288038
+ providerAccountId,
288039
+ cause
288040
+ })), require_Schema$1.asVoid);
288041
+ });
288042
+ return { observe };
287387
288043
  }
287388
288044
  //#endregion
287389
288045
  //#region src/orchestration/Layers/ProviderRuntimeIngestion.ts
@@ -287398,20 +288054,37 @@ const BUFFERED_PROPOSED_PLAN_BY_ID_TTL = require_Schema$1.minutes(120);
287398
288054
  const MAX_BUFFERED_ASSISTANT_CHARS = 24e3;
287399
288055
  const PROVIDER_INGESTION_CONCURRENCY = 8;
287400
288056
  const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.FORGE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
287401
- function decodeClaudeRateLimitWindows(raw) {
288057
+ /**
288058
+ * The SDK's own `rate_limit_info` off one ClaudeAdapter rate-limit event, or
288059
+ * nothing. Both readers below start here: the banner takes the single window
288060
+ * `rateLimitType` names, and passive usage telemetry takes the `unifiedWindows`
288061
+ * sub-object riding along on the same object.
288062
+ */
288063
+ function readClaudeRateLimitInfo(raw) {
287402
288064
  const rateLimitInfo = raw.rate_limit_info;
287403
- if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return [];
288065
+ if (rateLimitInfo === null || typeof rateLimitInfo !== "object") return null;
288066
+ return rateLimitInfo;
288067
+ }
288068
+ function decodeClaudeRateLimitWindows(raw) {
288069
+ const rateLimitInfo = readClaudeRateLimitInfo(raw);
288070
+ if (!rateLimitInfo) return [];
287404
288071
  const mapped = mapClaudeRateLimitInfo(rateLimitInfo);
287405
288072
  return mapped ? [{
287406
288073
  window: mapped.kind === "fiveHour" ? "five_hour" : "seven_day",
287407
288074
  ...mapped.window
287408
288075
  }] : [];
287409
288076
  }
288077
+ /** The raw telemetry object ClaudeAdapter/CodexAdapter wrap as `payload.rateLimits`. */
288078
+ function readRateLimitPayload(event) {
288079
+ if (event.type !== "account.rate-limits.updated") return null;
288080
+ const raw = event.payload.rateLimits;
288081
+ return raw !== null && typeof raw === "object" ? raw : null;
288082
+ }
287410
288083
  /**
287411
288084
  * Codex's app-server protocol shape isn't type-published anywhere this
287412
- * codebase has access to, so it's read defensively - an unrecognized shape
287413
- * is skipped rather than thrown, since this is a best-effort in-chat notice,
287414
- * never load-bearing for the turn itself.
288085
+ * codebase has access to, so it's read defensively. An unrecognized shape
288086
+ * is skipped rather than thrown, since this is a best-effort in-chat notice
288087
+ * that the turn itself never depends on.
287415
288088
  */
287416
288089
  /**
287417
288090
  * Decodes ONE window of the codex app-server `account/rateLimits/updated`
@@ -287724,6 +288397,9 @@ function shouldApplyProviderTurnCompleted(activeTurnId, eventTurnId) {
287724
288397
  }
287725
288398
  const make$9 = require_Schema$1.fn("make")(function* () {
287726
288399
  const orchestrationEngine = yield* OrchestrationEngineService;
288400
+ const claudeUsageTelemetry = yield* ClaudeUsageTelemetry;
288401
+ const claudeUsageScope = yield* require_Schema$1.make$3("parallel");
288402
+ yield* require_Schema$1.addFinalizer(() => require_Schema$1.close(claudeUsageScope, require_Schema$1.void_$1));
287727
288403
  const providerService = yield* ProviderService;
287728
288404
  const projectionTurnRepository = yield* ProjectionTurnRepository;
287729
288405
  const serverSettingsService = yield* ServerSettingsService;
@@ -288132,18 +288808,53 @@ const make$9 = require_Schema$1.fn("make")(function* () {
288132
288808
  * SDKRateLimitInfo, reused from apps/server/src/usage/providerRateLimits.ts
288133
288809
  * (same decoder the Usage page cache uses). Codex's app-server protocol
288134
288810
  * shape isn't type-published anywhere this codebase has access to, so it's
288135
- * read defensively - an unrecognized shape means no window update, not a
288136
- * thrown error, since this is a best-effort in-chat notice, never load-
288137
- * bearing for the turn itself.
288811
+ * read defensively. An unrecognized shape means no window update, not a
288812
+ * thrown error, since this is a best-effort in-chat notice that the turn
288813
+ * itself never depends on.
288138
288814
  */
288139
288815
  function decodeRateLimitWindows(event) {
288140
- if (event.type !== "account.rate-limits.updated") return [];
288141
- const raw = event.payload.rateLimits;
288142
- if (raw === null || typeof raw !== "object") return [];
288816
+ const raw = readRateLimitPayload(event);
288817
+ if (!raw) return [];
288143
288818
  if (event.provider === "claudeAgent") return decodeClaudeRateLimitWindows(raw);
288144
288819
  if (event.provider === "codex") return decodeCodexRateLimitWindows(raw);
288145
288820
  return [];
288146
288821
  }
288822
+ /**
288823
+ * The credential generation a Claude thread's live session runs on, or null.
288824
+ *
288825
+ * It is the delivery identity the readiness guard verified before the
288826
+ * session started, which ProviderService writes into the provider resume
288827
+ * cursor and `listSessions` hands back on the live session. Claude's
288828
+ * delivery identity is `{kind: "credential_generation"}` and carries NO
288829
+ * account field, so it cannot stand in for the account the thread's model
288830
+ * selection names, and neither id is derivable from the other. The usage
288831
+ * observer drops an observation missing either one, because publishing under
288832
+ * a guessed id would write one account's usage onto another account's card.
288833
+ */
288834
+ const claudeCredentialGenerationId = require_Schema$1.fn("claudeCredentialGenerationId")(function* (threadId) {
288835
+ const cursor = (yield* providerService.listSessions()).find((entry) => entry.threadId === threadId && entry.provider === "claudeAgent")?.resumeCursor;
288836
+ const binding = cursor !== null && typeof cursor === "object" ? readProviderDeliveryBinding(cursor.providerDeliveryBinding) : null;
288837
+ return binding?.identity.kind === "credential_generation" ? binding.identity.credentialGenerationId : null;
288838
+ });
288839
+ /**
288840
+ * Publishes the unified windows the same event already carried for the
288841
+ * banner. Passive: no probe, no credential delivery, no woken sprite. The SDK
288842
+ * reports these figures on every turn whether anything reads them or not.
288843
+ * The observer owns the floor, the credential generation lookup, dedupe, the
288844
+ * timeout and the HTTP call, and never fails.
288845
+ */
288846
+ const observeClaudeUsage = require_Schema$1.fn("observeClaudeUsage")(function* (event, thread) {
288847
+ if (event.provider !== "claudeAgent") return;
288848
+ const raw = readRateLimitPayload(event);
288849
+ const rateLimitInfo = raw ? readClaudeRateLimitInfo(raw) : null;
288850
+ const windows = rateLimitInfo ? mapClaudeUnifiedWindows(rateLimitInfo) : null;
288851
+ if (!windows) return;
288852
+ yield* claudeUsageTelemetry.observe({
288853
+ windows,
288854
+ providerAccountId: readProviderAccountId(thread.modelSelection) ?? null,
288855
+ credentialGenerationId: claudeCredentialGenerationId(thread.id)
288856
+ });
288857
+ });
288147
288858
  const handleRateLimitEvent = require_Schema$1.fn("handleRateLimitEvent")(function* (event, thread) {
288148
288859
  const provider = event.provider === "claudeAgent" ? "claude" : event.provider === "codex" ? "codex" : null;
288149
288860
  if (!provider) return;
@@ -288160,6 +288871,10 @@ const make$9 = require_Schema$1.fn("make")(function* () {
288160
288871
  resetsAt: win.resetsAt,
288161
288872
  createdAt: event.createdAt
288162
288873
  }), { discard: true });
288874
+ yield* observeClaudeUsage(event, thread).pipe(require_Schema$1.catchCause((cause) => require_Schema$1.hasInterruptsOnly(cause) ? require_Schema$1.void_ : require_Schema$1.logWarning("claude usage observation failed", {
288875
+ eventId: event.eventId,
288876
+ cause
288877
+ })), require_Schema$1.forkIn(claudeUsageScope));
288163
288878
  });
288164
288879
  const processRuntimeEvent = require_Schema$1.fn("processRuntimeEvent")(function* (event) {
288165
288880
  const thread = (yield* orchestrationEngine.getReadModel()).threads.find((entry) => entry.id === event.threadId);
@@ -288275,6 +288990,44 @@ const make$9 = require_Schema$1.fn("make")(function* () {
288275
288990
  });
288276
288991
  const ProviderRuntimeIngestionLive = require_Schema$1.effect(ProviderRuntimeIngestionService, make$9()).pipe(require_Schema$1.provide$1(ProjectionTurnRepositoryLive));
288277
288992
  //#endregion
288993
+ //#region src/usage/Layers/ClaudeUsageTelemetry.ts
288994
+ /**
288995
+ * The real publish behind `ClaudeUsageTelemetry`: the owner's CMMD credential
288996
+ * and one POST to `CLAUDE_USAGE_TELEMETRY_PATH`.
288997
+ *
288998
+ * The call always goes out on the owner session credential, never on a
288999
+ * runtime grant.
289000
+ *
289001
+ * A machine with nobody signed in, or one whose CMMD credential cannot be
289002
+ * minted, fails the publish rather than reporting success. The observer only
289003
+ * remembers a value it managed to send, so a quiet success here would suppress
289004
+ * the publish for as long as that reading stayed the same, which on a stable
289005
+ * account is hours.
289006
+ *
289007
+ * @module ClaudeUsageTelemetryLive
289008
+ */
289009
+ const LOG_PREFIX = "claude-usage-passive-telemetry";
289010
+ const ClaudeUsageTelemetryLive = require_Schema$1.effect(ClaudeUsageTelemetry, require_Schema$1.gen(function* () {
289011
+ const sql = yield* require_SqlError.SqlClient;
289012
+ const publish = (body) => require_Schema$1.gen(function* () {
289013
+ const credential = yield* resolveCmmdOwnerSessionCredential(sql, { logPrefix: LOG_PREFIX });
289014
+ if (credential.kind !== "credential") return yield* new ClaudeUsageTelemetryPublishError({ reason: credential.kind });
289015
+ yield* require_Schema$1.tryPromise({
289016
+ try: () => cmmdGateway.request({
289017
+ method: "POST",
289018
+ jwt: credential.jwt,
289019
+ path: CLAUDE_USAGE_TELEMETRY_PATH,
289020
+ body
289021
+ }),
289022
+ catch: (cause) => new ClaudeUsageTelemetryPublishError({
289023
+ reason: "request-failed",
289024
+ cause
289025
+ })
289026
+ });
289027
+ });
289028
+ return makeClaudeUsageTelemetry({ publish });
289029
+ }));
289030
+ //#endregion
288278
289031
  //#region ../../packages/shared/src/DrainableWorker.ts
288279
289032
  /**
288280
289033
  * Create a drainable worker that processes items from an unbounded queue.
@@ -294072,7 +294825,7 @@ function resolveBuildCommitFromEnv(env) {
294072
294825
  * environment descriptor down instead of reporting an honest "unknown".
294073
294826
  */
294074
294827
  function readBakedBuildCommit() {
294075
- return "7f0d765bc59cb837f8aa8025b9b2f855df3fd36d";
294828
+ return "8cafbb87b6204b98e93c1c69253c1c590af5f8a1";
294076
294829
  }
294077
294830
  async function resolveServerBuildCommit(input) {
294078
294831
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -316677,7 +317430,7 @@ function claimClaudeReadinessJti(input) {
316677
317430
  return "claimed";
316678
317431
  }
316679
317432
  const readUsableClaudeGenerationHash = (config) => {
316680
- const configuredGenerationHash = process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
317433
+ const configuredGenerationHash = bootExportedPersonalClaudeGenerationHash();
316681
317434
  if (!/^[a-f0-9]{64}$/.test(configuredGenerationHash)) return require_Schema$1.succeed(null);
316682
317435
  return require_Schema$1.tryPromise(() => (0, node_fs_promises.readFile)(resolveLocalClaudeCredentialPath(config.forgeClaudeConfigPath), "utf8")).pipe(require_Schema$1.map((credentialJson) => isClaudeSubscriptionCredential(credentialJson) ? configuredGenerationHash : null), require_Schema$1.orElseSucceed(() => null));
316683
317436
  };
@@ -317195,7 +317948,7 @@ const PlatformServicesLive = require_Schema$1.unwrap(require_Schema$1.gen(functi
317195
317948
  }));
317196
317949
  const RuntimeReplicationLayerLive = RuntimeReplicationLive.pipe(require_Schema$1.provideMerge(RuntimeReplicationClientLive), require_Schema$1.provideMerge(RuntimeReplicationOutboxLive), require_Schema$1.provideMerge(ActiveHourMeterLive.pipe(require_Schema$1.provide$1(RuntimeReplicationOutboxLive))));
317197
317950
  const RuntimeDrainLayerLive = RuntimeDrainReporterLive.pipe(require_Schema$1.provideMerge(RuntimeDrainStateLive));
317198
- const RuntimeAwareProviderReactorsLive = require_Schema$1.mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(require_Schema$1.provide$1(RuntimeDrainLayerLive));
317951
+ const RuntimeAwareProviderReactorsLive = require_Schema$1.mergeAll(ProviderRuntimeIngestionLive, ProviderCommandReactorLive).pipe(require_Schema$1.provide$1(RuntimeDrainLayerLive), require_Schema$1.provide$1(ClaudeUsageTelemetryLive));
317199
317952
  const PipelineWorkerCoordinatorLayerLive = PipelineWorkerCoordinatorLive.pipe(require_Schema$1.provideMerge(PipelineRunClientLive));
317200
317953
  const AgentProtocolReactorLayerLive = AgentProtocolReactorLive.pipe(require_Schema$1.provideMerge(PipelineRunStoreLive));
317201
317954
  const PipelineRunCoordinatorLayerLive = PipelineRunCoordinatorLive.pipe(require_Schema$1.provideMerge(PipelineProviderAuthorityInstallerLive), require_Schema$1.provideMerge(PipelineChangeInspectorLive), require_Schema$1.provideMerge(PipelineContextArtifactStoreLive), require_Schema$1.provideMerge(PipelineModelPolicyLive), require_Schema$1.provideMerge(PipelineRunClientLive), require_Schema$1.provideMerge(PipelineRunStoreLive), require_Schema$1.provideMerge(PipelineRunWorkspaceAllocatorLive), require_Schema$1.provideMerge(PipelineStagePolicyLive), require_Schema$1.provideMerge(PipelineWorkerCoordinatorLayerLive));