@cmmd-center/forge 0.9.4 → 0.9.5

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
@@ -12,7 +12,7 @@ const require_Cache = require("./Cache-XSj5g7uG.cjs");
12
12
  const require_SqlError = require("./SqlError-C2mi8_tp.cjs");
13
13
  const require_esm = require("./esm-Do7HVhLA.cjs");
14
14
  const require_Mime = require("./Mime-CyHbecis.cjs");
15
- const require_runtimeControlAuthority = require("./runtimeControlAuthority-Dx6Y5LPI.cjs");
15
+ const require_runtimeControlAuthority = require("./runtimeControlAuthority-SNBF5Mjy.cjs");
16
16
  const require_PTY = require("./PTY-U3JQnLXS.cjs");
17
17
  let node_child_process = require("node:child_process");
18
18
  node_child_process = require_chunk.__toESM(node_child_process);
@@ -67803,7 +67803,7 @@ function normalizeNumberish(value) {
67803
67803
  }
67804
67804
  //#endregion
67805
67805
  //#region package.json
67806
- var version$1 = "0.9.4";
67806
+ var version$1 = "0.9.5";
67807
67807
  //#endregion
67808
67808
  //#region src/sentry.ts
67809
67809
  const SERVER_APP_NAME = "forge-server";
@@ -99120,14 +99120,9 @@ function mapClaudeRateLimitInfo(info) {
99120
99120
  }
99121
99121
  const cachedSnapshots = /* @__PURE__ */ new Map();
99122
99122
  let localClaudeGenerationHash = null;
99123
- let localClaudeGenerationId = null;
99124
99123
  function setLocalClaudeUsageGeneration(generationId) {
99125
- localClaudeGenerationId = generationId;
99126
99124
  localClaudeGenerationHash = (0, node_crypto.createHash)("sha256").update(generationId).digest("hex");
99127
99125
  }
99128
- function currentClaudeUsageGenerationId() {
99129
- return localClaudeGenerationId;
99130
- }
99131
99126
  function currentClaudeUsageGenerationHash() {
99132
99127
  const value = process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
99133
99128
  return /^[a-f0-9]{64}$/.test(value) ? value : localClaudeGenerationHash;
@@ -99547,6 +99542,16 @@ function extractClaudeProviderAccountId(response) {
99547
99542
  const providerAccountId = account.providerAccountId.trim();
99548
99543
  return providerAccountId.length > 0 ? providerAccountId : null;
99549
99544
  }
99545
+ /**
99546
+ * Logs a usage-publish failure, or does nothing when there was none. The null
99547
+ * check lives here rather than in syncClaudeCredentialForSession so that
99548
+ * reporting the failure costs that generator no branches.
99549
+ */
99550
+ const logUsagePublishFailure = (failure, path) => failure === null ? require_Schema$1.void_ : require_Schema$1.logWarning("cmmd-claude-cred-sync: usage publish failed after deposit", {
99551
+ error: failure.cause instanceof Error ? failure.cause.message : String(failure.cause),
99552
+ stack: failure.cause instanceof Error ? failure.cause.stack : void 0,
99553
+ path
99554
+ });
99550
99555
  async function publishClaudeUsageAfterSync(input) {
99551
99556
  try {
99552
99557
  const usage = await input.loadUsage({
@@ -99569,13 +99574,16 @@ async function publishClaudeUsageAfterSync(input) {
99569
99574
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
99570
99575
  }
99571
99576
  });
99572
- } catch {}
99577
+ return null;
99578
+ } catch (cause) {
99579
+ return { cause };
99580
+ }
99573
99581
  }
99574
99582
  async function finalizeClaudeCredentialSync(input) {
99575
99583
  const providerAccountId = extractClaudeProviderAccountId(input.response);
99576
- if (providerAccountId === null) return;
99584
+ if (providerAccountId === null) return null;
99577
99585
  if (input.auth.path === USER_CLAUDE_CREDENTIAL_PATH) setLocalClaudeUsageGeneration(providerAccountId);
99578
- await publishClaudeUsageAfterSync({
99586
+ return publishClaudeUsageAfterSync({
99579
99587
  auth: input.auth,
99580
99588
  providerAccountId,
99581
99589
  accountEmail: input.accountEmail,
@@ -99630,7 +99638,7 @@ function syncClaudeCredentialForSession(input) {
99630
99638
  }
99631
99639
  })).pipe(require_Schema$1.orElseSucceed(() => null));
99632
99640
  if (response === null) return { status: "failed" };
99633
- yield* require_Schema$1.promise(() => finalizeClaudeCredentialSync({
99641
+ yield* logUsagePublishFailure(yield* require_Schema$1.promise(() => finalizeClaudeCredentialSync({
99634
99642
  response,
99635
99643
  auth,
99636
99644
  accountEmail,
@@ -99638,7 +99646,7 @@ function syncClaudeCredentialForSession(input) {
99638
99646
  planType: metadata.planType ?? probedMetadata.planType ?? null,
99639
99647
  ...input.homePath ? { homePath: input.homePath } : {},
99640
99648
  loadUsage: input.loadUsage ?? fetchClaudeUsageViaOfficialClient
99641
- }));
99649
+ })), auth.path);
99642
99650
  return { status: "synced" };
99643
99651
  });
99644
99652
  }
@@ -289919,6 +289927,95 @@ const makeRuntimeServerSecretStore = require_Schema$1.gen(function* () {
289919
289927
  return (yield* ServerConfig).forgeCloudShellMode === true ? yield* makePostgresServerSecretStore : yield* makeLocalServerSecretStore;
289920
289928
  });
289921
289929
  const ServerSecretStoreRuntimeLive = require_Schema$1.effect(ServerSecretStore, makeRuntimeServerSecretStore);
289930
+ //#endregion
289931
+ //#region src/runtimeAuth/controlAuthorityRetry.ts
289932
+ /**
289933
+ * When to try for control authority again.
289934
+ *
289935
+ * CMMD ships continuously, so a Forge Cloud runtime will outlive restarts of its
289936
+ * own control plane. Obtaining authority is therefore not a startup-time
289937
+ * question: a runtime that asks once, gets an infrastructure page mid-deploy and
289938
+ * gives up stays unauthenticated until something happens to restart it while
289939
+ * CMMD is healthy. Measured: hours, on a Sprite whose guest was serving fine the
289940
+ * whole time.
289941
+ *
289942
+ * Two shapes of failure arrive here and they need opposite answers, which is why
289943
+ * `retryable` is decided at the boundary that saw the response rather than
289944
+ * guessed at here.
289945
+ */
289946
+ /** First backoff. Short on purpose: a deploy cutover is seconds, not minutes. */
289947
+ const BASE_DELAY_MS = 1e3;
289948
+ /**
289949
+ * Ceiling. Unbounded doubling turns a long outage into a long TAIL: an hour down
289950
+ * would be followed by up to an hour of avoidable downtime after recovery.
289951
+ */
289952
+ const MAX_DELAY_MS = 6e4;
289953
+ /**
289954
+ * Spread, as a fraction of the delay. A fleet loses authority at the same
289955
+ * instant when the control plane restarts, so without this every runtime
289956
+ * retries in lockstep and the recovering CMMD takes the whole fleet at once.
289957
+ *
289958
+ * Kept below 1 so jitter can only ADD: a later attempt must never be scheduled
289959
+ * sooner than an earlier one, or backoff stops meaning anything.
289960
+ */
289961
+ const JITTER_FRACTION = .25;
289962
+ function nextControlAuthorityAttempt(input) {
289963
+ if (!input.retryable) return {
289964
+ retry: false,
289965
+ delayMs: 0
289966
+ };
289967
+ const exponent = Math.max(0, input.consecutiveFailures - 1);
289968
+ const cap = MAX_DELAY_MS / (1 + JITTER_FRACTION);
289969
+ const base = Math.min(BASE_DELAY_MS * 2 ** exponent, cap);
289970
+ const random = input.random ?? Math.random;
289971
+ return {
289972
+ retry: true,
289973
+ delayMs: Math.round(base * (1 + JITTER_FRACTION * random()))
289974
+ };
289975
+ }
289976
+ /** Bounded retry budget. The next connect starts a fresh one. */
289977
+ const DEFAULT_MAX_ATTEMPTS = 8;
289978
+ /**
289979
+ * Keep asking for control authority while asking again could plausibly work.
289980
+ *
289981
+ * Bounded rather than infinite: a loop that gives up and says so lets the caller
289982
+ * log something actionable, and a connect or a heartbeat will start a fresh
289983
+ * budget anyway. An unbounded loop would hide the condition in exactly the way
289984
+ * the original `.catch(() => false)` did.
289985
+ */
289986
+ async function bootstrapWithRetry(input) {
289987
+ const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
289988
+ let lastError;
289989
+ for (let attempts = 1; attempts <= maxAttempts; attempts += 1) {
289990
+ let retryable = true;
289991
+ try {
289992
+ if (await input.attempt()) return {
289993
+ ok: true,
289994
+ attempts
289995
+ };
289996
+ } catch (error) {
289997
+ lastError = error;
289998
+ retryable = typeof error === "object" && error !== null && "retryable" in error && error.retryable === true;
289999
+ }
290000
+ if (!retryable) return {
290001
+ ok: false,
290002
+ attempts,
290003
+ lastError
290004
+ };
290005
+ if (attempts === maxAttempts) break;
290006
+ const next = nextControlAuthorityAttempt({
290007
+ consecutiveFailures: attempts,
290008
+ retryable: true,
290009
+ ...input.random ? { random: input.random } : {}
290010
+ });
290011
+ await input.sleep(next.delayMs);
290012
+ }
290013
+ return {
290014
+ ok: false,
290015
+ attempts: maxAttempts,
290016
+ lastError
290017
+ };
290018
+ }
289922
290019
  var AuthControlPlaneError = class extends require_Schema$1.TaggedError("AuthControlPlaneError") {};
289923
290020
  var AuthControlPlane = class extends require_Schema$1.Service()("forge/AuthControlPlane") {};
289924
290021
  //#endregion
@@ -291118,19 +291215,24 @@ async function bootstrapControlAuthorityFromConnect(input) {
291118
291215
  const credentialEpoch = input.verified.credentialEpoch;
291119
291216
  const hasConnectScope = input.verified.scope.includes("http") || input.verified.scope.includes("ws");
291120
291217
  if (!controlPlaneUrl || !provisioningGrant || machineId === null || credentialEpoch === null || !hasConnectScope) return;
291121
- await require_runtimeControlAuthority.bootstrapRuntimeControlAuthority({
291122
- identity: {
291123
- controlPlaneUrl,
291124
- publicKeyPem: input.runtimeAuthConfig.publicKeyPem,
291125
- orgId: input.verified.orgId,
291126
- userId: input.verified.userId,
291127
- projectId: input.verified.projectId,
291128
- runtimeId: input.verified.runtimeId,
291129
- machineId,
291130
- credentialEpoch
291131
- },
291132
- connectToken: input.token,
291133
- provisioningGrant
291218
+ await bootstrapWithRetry({
291219
+ attempt: () => require_runtimeControlAuthority.bootstrapRuntimeControlAuthority({
291220
+ identity: {
291221
+ controlPlaneUrl,
291222
+ publicKeyPem: input.runtimeAuthConfig.publicKeyPem,
291223
+ orgId: input.verified.orgId,
291224
+ userId: input.verified.userId,
291225
+ projectId: input.verified.projectId,
291226
+ runtimeId: input.verified.runtimeId,
291227
+ machineId,
291228
+ credentialEpoch
291229
+ },
291230
+ connectToken: input.token,
291231
+ provisioningGrant
291232
+ }),
291233
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
291234
+ }).then((result) => {
291235
+ if (!result.ok && result.lastError !== void 0) throw result.lastError;
291134
291236
  }).catch((error) => {
291135
291237
  console.error("[bootstrapControlAuthorityFromConnect] failed to establish runtime control authority; every heartbeat will report null until the next successful connect", {
291136
291238
  runtimeId: input.verified.runtimeId,
@@ -296504,6 +296606,32 @@ const readPersistedServerRuntimeState = (path) => require_Schema$1.gen(function*
296504
296606
  return yield* decodePersistedServerRuntimeState(trimmed).pipe(require_Schema$1.option);
296505
296607
  });
296506
296608
  //#endregion
296609
+ //#region src/orchestration/turnAdmission.ts
296610
+ /**
296611
+ * Whether a turn can actually run, as opposed to merely being accepted.
296612
+ *
296613
+ * A Forge Cloud runtime reaches its provider through CMMD, so without control
296614
+ * authority it cannot start a turn at all. It could still ACCEPT one: the
296615
+ * command validates, the thread and worktree are created, the user message is
296616
+ * recorded, and the turn then sits at `pending` forever with nothing surfaced.
296617
+ *
296618
+ * Measured on forge-cmmd-r03ad897c5e90 while CMMD was redeploying: HTTP 200 on
296619
+ * dispatch, five user messages, zero assistant messages, every turn stuck with
296620
+ * `started_at` empty. The client had no way to know.
296621
+ *
296622
+ * Kept as a pure function because the interesting part is the RULE, not the
296623
+ * plumbing: it is only cloud runtimes, only turns, and only when authority is
296624
+ * genuinely absent.
296625
+ */
296626
+ /** Commands that need a provider, and therefore need control authority. */
296627
+ const TURN_COMMANDS = new Set(["thread.turn.start"]);
296628
+ function refuseTurnWithoutControlAuthority(input) {
296629
+ if (!input.cmmdRuntimeMode) return null;
296630
+ if (!TURN_COMMANDS.has(input.commandType)) return null;
296631
+ if (input.hasControlAuthority) return null;
296632
+ return "This runtime has no CMMD control authority, so a turn cannot reach a provider and would queue without ever running. The runtime re-establishes authority on its own; retry shortly.";
296633
+ }
296634
+ //#endregion
296507
296635
  //#region ../../packages/shared/src/forgeCommands.ts
296508
296636
  /**
296509
296637
  * Forge loop commands (`/forge-*`) — the one trigger surface shared by humans
@@ -297072,6 +297200,16 @@ const orchestrationDispatchRouteLayer = add$1("POST", "/api/orchestration/dispat
297072
297200
  message: "Invalid orchestration command payload.",
297073
297201
  cause
297074
297202
  }))));
297203
+ const admissionRefusal = refuseTurnWithoutControlAuthority({
297204
+ commandType: normalizedCommand.type,
297205
+ cmmdRuntimeMode: process.env.FORGE_CMMD_RUNTIME_MODE === "true",
297206
+ hasControlAuthority: require_runtimeControlAuthority.readRuntimeControlAuthority({
297207
+ runtimeId: process.env.FORGE_CMMD_RUNTIME_ID ?? "",
297208
+ machineId: process.env.FORGE_CMMD_RUNTIME_MACHINE_ID ?? "",
297209
+ credentialEpoch: Number(process.env.FORGE_CMMD_RUNTIME_CREDENTIAL_EPOCH ?? "0")
297210
+ }) !== null
297211
+ });
297212
+ if (admissionRefusal !== null) return yield* new OrchestrationDispatchCommandError({ message: admissionRefusal });
297075
297213
  const dispatchEffect = normalizedCommand.type === "thread.turn.start" && normalizedCommand.bootstrap ? require_Schema$1.gen(function* () {
297076
297214
  const git = yield* require_Schema$1.serviceOption(GitCore);
297077
297215
  const gitStatusBroadcaster = yield* require_Schema$1.serviceOption(GitStatusBroadcaster);
@@ -299394,7 +299532,7 @@ const cmmdSetProviderAccountDefaultRouteLayer = registerProxyRoute({
299394
299532
  orgIdSource: "none",
299395
299533
  responseShape: "unwrapData"
299396
299534
  });
299397
- const resolveConnectedClaudeGenerationHash = (session) => require_Schema$1.gen(function* () {
299535
+ const resolveConnectedClaudeGeneration = (session) => require_Schema$1.gen(function* () {
299398
299536
  const outcome = yield* runCmmdRestProxy(session, (jwt) => cmmdGateway.request({
299399
299537
  method: "GET",
299400
299538
  jwt,
@@ -299408,18 +299546,22 @@ const resolveConnectedClaudeGenerationHash = (session) => require_Schema$1.gen(f
299408
299546
  };
299409
299547
  return {
299410
299548
  ok: true,
299411
- value: (0, node_crypto.createHash)("sha256").update(state.generationId).digest("hex")
299549
+ value: {
299550
+ generationId: state.generationId,
299551
+ generationHash: (0, node_crypto.createHash)("sha256").update(state.generationId).digest("hex")
299552
+ }
299412
299553
  };
299413
299554
  });
299414
299555
  /**
299415
299556
  * The GET path is read-only. The explicit per-account POST refresh invokes
299416
299557
  * this helper and propagates any CMMD forward failure to the caller.
299417
299558
  */
299418
- const forwardClaudeUsageTelemetry = (session, providerAccountId = currentClaudeUsageGenerationId() ?? "", activelyPoll = false, fetchClaudeUsage = fetchClaudeUsageViaOfficialClient) => require_Schema$1.gen(function* () {
299419
- const credentialOutcome = yield* resolveConnectedClaudeGenerationHash(session);
299559
+ const forwardClaudeUsageTelemetry = (session, requestedProviderAccountId, activelyPoll = false, fetchClaudeUsage = fetchClaudeUsageViaOfficialClient) => require_Schema$1.gen(function* () {
299560
+ const credentialOutcome = yield* resolveConnectedClaudeGeneration(session);
299420
299561
  if (!credentialOutcome.ok) return credentialOutcome;
299421
- const generationId = currentClaudeUsageGenerationId();
299422
- if (credentialOutcome.value === null || generationId === null) return null;
299562
+ if (credentialOutcome.value === null) return null;
299563
+ const { generationId, generationHash } = credentialOutcome.value;
299564
+ const providerAccountId = requestedProviderAccountId ?? generationId;
299423
299565
  const activeSnapshot = activelyPoll && providerAccountId === generationId ? yield* require_Schema$1.tryPromise({
299424
299566
  try: () => fetchClaudeUsage({ binaryPath: resolveClaudeBinaryPath("claude") }),
299425
299567
  catch: (cause) => new ClaudeUsageReadError({ cause })
@@ -299427,7 +299569,7 @@ const forwardClaudeUsageTelemetry = (session, providerAccountId = currentClaudeU
299427
299569
  const snapshot = activeSnapshot ? {
299428
299570
  ...activeSnapshot,
299429
299571
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
299430
- } : getLatestClaudeRateLimitTelemetry(credentialOutcome.value, providerAccountId);
299572
+ } : getLatestClaudeRateLimitTelemetry(generationHash, providerAccountId);
299431
299573
  if (!snapshot) return null;
299432
299574
  return yield* runCmmdRestProxy(session, (jwt) => cmmdGateway.request({
299433
299575
  method: "POST",
package/dist/bin.mjs CHANGED
@@ -11,7 +11,7 @@ import { a as make$48, i as keys, n as getOption, o as makeWith$1, r as invalida
11
11
  import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$49, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-1c-3v7GY.mjs";
12
12
  import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-Ce1GqbVk.mjs";
13
13
  import { t as Mime_default } from "./Mime-BITO-E1U.mjs";
14
- import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicketRefreshProof, d as createReadOnceFileTokenAccessor, i as sendRuntimeControlHeartbeat, l as runtimeGrantToken, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, r as readRuntimeControlAuthority, s as verifyCmmdRuntimeTicket, t as bootstrapRuntimeControlAuthority, u as setRuntimeGrantToken } from "./runtimeControlAuthority-Dm9z5ziU.mjs";
14
+ import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicketRefreshProof, d as createReadOnceFileTokenAccessor, i as sendRuntimeControlHeartbeat, l as runtimeGrantToken, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, r as readRuntimeControlAuthority, s as verifyCmmdRuntimeTicket, t as bootstrapRuntimeControlAuthority, u as setRuntimeGrantToken } from "./runtimeControlAuthority-L85w5RTn.mjs";
15
15
  import { n as PtySpawnError, t as PtyAdapter } from "./PTY-CBkE5akF.mjs";
16
16
  import { createRequire } from "node:module";
17
17
  import * as HE from "node:child_process";
@@ -67570,7 +67570,7 @@ function normalizeNumberish(value) {
67570
67570
  }
67571
67571
  //#endregion
67572
67572
  //#region package.json
67573
- var version$1 = "0.9.4";
67573
+ var version$1 = "0.9.5";
67574
67574
  //#endregion
67575
67575
  //#region src/sentry.ts
67576
67576
  const SERVER_APP_NAME = "forge-server";
@@ -98815,14 +98815,9 @@ function mapClaudeRateLimitInfo(info) {
98815
98815
  }
98816
98816
  const cachedSnapshots = /* @__PURE__ */ new Map();
98817
98817
  let localClaudeGenerationHash = null;
98818
- let localClaudeGenerationId = null;
98819
98818
  function setLocalClaudeUsageGeneration(generationId) {
98820
- localClaudeGenerationId = generationId;
98821
98819
  localClaudeGenerationHash = createHash("sha256").update(generationId).digest("hex");
98822
98820
  }
98823
- function currentClaudeUsageGenerationId() {
98824
- return localClaudeGenerationId;
98825
- }
98826
98821
  function currentClaudeUsageGenerationHash() {
98827
98822
  const value = process.env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
98828
98823
  return /^[a-f0-9]{64}$/.test(value) ? value : localClaudeGenerationHash;
@@ -99242,6 +99237,16 @@ function extractClaudeProviderAccountId(response) {
99242
99237
  const providerAccountId = account.providerAccountId.trim();
99243
99238
  return providerAccountId.length > 0 ? providerAccountId : null;
99244
99239
  }
99240
+ /**
99241
+ * Logs a usage-publish failure, or does nothing when there was none. The null
99242
+ * check lives here rather than in syncClaudeCredentialForSession so that
99243
+ * reporting the failure costs that generator no branches.
99244
+ */
99245
+ const logUsagePublishFailure = (failure, path) => failure === null ? void_$1 : logWarning$1("cmmd-claude-cred-sync: usage publish failed after deposit", {
99246
+ error: failure.cause instanceof Error ? failure.cause.message : String(failure.cause),
99247
+ stack: failure.cause instanceof Error ? failure.cause.stack : void 0,
99248
+ path
99249
+ });
99245
99250
  async function publishClaudeUsageAfterSync(input) {
99246
99251
  try {
99247
99252
  const usage = await input.loadUsage({
@@ -99264,13 +99269,16 @@ async function publishClaudeUsageAfterSync(input) {
99264
99269
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
99265
99270
  }
99266
99271
  });
99267
- } catch {}
99272
+ return null;
99273
+ } catch (cause) {
99274
+ return { cause };
99275
+ }
99268
99276
  }
99269
99277
  async function finalizeClaudeCredentialSync(input) {
99270
99278
  const providerAccountId = extractClaudeProviderAccountId(input.response);
99271
- if (providerAccountId === null) return;
99279
+ if (providerAccountId === null) return null;
99272
99280
  if (input.auth.path === USER_CLAUDE_CREDENTIAL_PATH) setLocalClaudeUsageGeneration(providerAccountId);
99273
- await publishClaudeUsageAfterSync({
99281
+ return publishClaudeUsageAfterSync({
99274
99282
  auth: input.auth,
99275
99283
  providerAccountId,
99276
99284
  accountEmail: input.accountEmail,
@@ -99325,7 +99333,7 @@ function syncClaudeCredentialForSession(input) {
99325
99333
  }
99326
99334
  })).pipe(orElseSucceed(() => null));
99327
99335
  if (response === null) return { status: "failed" };
99328
- yield* promise(() => finalizeClaudeCredentialSync({
99336
+ yield* logUsagePublishFailure(yield* promise(() => finalizeClaudeCredentialSync({
99329
99337
  response,
99330
99338
  auth,
99331
99339
  accountEmail,
@@ -99333,7 +99341,7 @@ function syncClaudeCredentialForSession(input) {
99333
99341
  planType: metadata.planType ?? probedMetadata.planType ?? null,
99334
99342
  ...input.homePath ? { homePath: input.homePath } : {},
99335
99343
  loadUsage: input.loadUsage ?? fetchClaudeUsageViaOfficialClient
99336
- }));
99344
+ })), auth.path);
99337
99345
  return { status: "synced" };
99338
99346
  });
99339
99347
  }
@@ -289444,6 +289452,95 @@ effect(ServerSecretStore, makeLocalServerSecretStore);
289444
289452
  const ServerSecretStoreRuntimeLive = effect(ServerSecretStore, gen(function* () {
289445
289453
  return (yield* ServerConfig).forgeCloudShellMode === true ? yield* makePostgresServerSecretStore : yield* makeLocalServerSecretStore;
289446
289454
  }));
289455
+ //#endregion
289456
+ //#region src/runtimeAuth/controlAuthorityRetry.ts
289457
+ /**
289458
+ * When to try for control authority again.
289459
+ *
289460
+ * CMMD ships continuously, so a Forge Cloud runtime will outlive restarts of its
289461
+ * own control plane. Obtaining authority is therefore not a startup-time
289462
+ * question: a runtime that asks once, gets an infrastructure page mid-deploy and
289463
+ * gives up stays unauthenticated until something happens to restart it while
289464
+ * CMMD is healthy. Measured: hours, on a Sprite whose guest was serving fine the
289465
+ * whole time.
289466
+ *
289467
+ * Two shapes of failure arrive here and they need opposite answers, which is why
289468
+ * `retryable` is decided at the boundary that saw the response rather than
289469
+ * guessed at here.
289470
+ */
289471
+ /** First backoff. Short on purpose: a deploy cutover is seconds, not minutes. */
289472
+ const BASE_DELAY_MS = 1e3;
289473
+ /**
289474
+ * Ceiling. Unbounded doubling turns a long outage into a long TAIL: an hour down
289475
+ * would be followed by up to an hour of avoidable downtime after recovery.
289476
+ */
289477
+ const MAX_DELAY_MS = 6e4;
289478
+ /**
289479
+ * Spread, as a fraction of the delay. A fleet loses authority at the same
289480
+ * instant when the control plane restarts, so without this every runtime
289481
+ * retries in lockstep and the recovering CMMD takes the whole fleet at once.
289482
+ *
289483
+ * Kept below 1 so jitter can only ADD: a later attempt must never be scheduled
289484
+ * sooner than an earlier one, or backoff stops meaning anything.
289485
+ */
289486
+ const JITTER_FRACTION = .25;
289487
+ function nextControlAuthorityAttempt(input) {
289488
+ if (!input.retryable) return {
289489
+ retry: false,
289490
+ delayMs: 0
289491
+ };
289492
+ const exponent = Math.max(0, input.consecutiveFailures - 1);
289493
+ const cap = MAX_DELAY_MS / (1 + JITTER_FRACTION);
289494
+ const base = Math.min(BASE_DELAY_MS * 2 ** exponent, cap);
289495
+ const random = input.random ?? Math.random;
289496
+ return {
289497
+ retry: true,
289498
+ delayMs: Math.round(base * (1 + JITTER_FRACTION * random()))
289499
+ };
289500
+ }
289501
+ /** Bounded retry budget. The next connect starts a fresh one. */
289502
+ const DEFAULT_MAX_ATTEMPTS = 8;
289503
+ /**
289504
+ * Keep asking for control authority while asking again could plausibly work.
289505
+ *
289506
+ * Bounded rather than infinite: a loop that gives up and says so lets the caller
289507
+ * log something actionable, and a connect or a heartbeat will start a fresh
289508
+ * budget anyway. An unbounded loop would hide the condition in exactly the way
289509
+ * the original `.catch(() => false)` did.
289510
+ */
289511
+ async function bootstrapWithRetry(input) {
289512
+ const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
289513
+ let lastError;
289514
+ for (let attempts = 1; attempts <= maxAttempts; attempts += 1) {
289515
+ let retryable = true;
289516
+ try {
289517
+ if (await input.attempt()) return {
289518
+ ok: true,
289519
+ attempts
289520
+ };
289521
+ } catch (error) {
289522
+ lastError = error;
289523
+ retryable = typeof error === "object" && error !== null && "retryable" in error && error.retryable === true;
289524
+ }
289525
+ if (!retryable) return {
289526
+ ok: false,
289527
+ attempts,
289528
+ lastError
289529
+ };
289530
+ if (attempts === maxAttempts) break;
289531
+ const next = nextControlAuthorityAttempt({
289532
+ consecutiveFailures: attempts,
289533
+ retryable: true,
289534
+ ...input.random ? { random: input.random } : {}
289535
+ });
289536
+ await input.sleep(next.delayMs);
289537
+ }
289538
+ return {
289539
+ ok: false,
289540
+ attempts: maxAttempts,
289541
+ lastError
289542
+ };
289543
+ }
289447
289544
  var AuthControlPlaneError = class extends TaggedError("AuthControlPlaneError") {};
289448
289545
  var AuthControlPlane = class extends Service$1()("forge/AuthControlPlane") {};
289449
289546
  //#endregion
@@ -290636,19 +290733,24 @@ async function bootstrapControlAuthorityFromConnect(input) {
290636
290733
  const credentialEpoch = input.verified.credentialEpoch;
290637
290734
  const hasConnectScope = input.verified.scope.includes("http") || input.verified.scope.includes("ws");
290638
290735
  if (!controlPlaneUrl || !provisioningGrant || machineId === null || credentialEpoch === null || !hasConnectScope) return;
290639
- await bootstrapRuntimeControlAuthority({
290640
- identity: {
290641
- controlPlaneUrl,
290642
- publicKeyPem: input.runtimeAuthConfig.publicKeyPem,
290643
- orgId: input.verified.orgId,
290644
- userId: input.verified.userId,
290645
- projectId: input.verified.projectId,
290646
- runtimeId: input.verified.runtimeId,
290647
- machineId,
290648
- credentialEpoch
290649
- },
290650
- connectToken: input.token,
290651
- provisioningGrant
290736
+ await bootstrapWithRetry({
290737
+ attempt: () => bootstrapRuntimeControlAuthority({
290738
+ identity: {
290739
+ controlPlaneUrl,
290740
+ publicKeyPem: input.runtimeAuthConfig.publicKeyPem,
290741
+ orgId: input.verified.orgId,
290742
+ userId: input.verified.userId,
290743
+ projectId: input.verified.projectId,
290744
+ runtimeId: input.verified.runtimeId,
290745
+ machineId,
290746
+ credentialEpoch
290747
+ },
290748
+ connectToken: input.token,
290749
+ provisioningGrant
290750
+ }),
290751
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms))
290752
+ }).then((result) => {
290753
+ if (!result.ok && result.lastError !== void 0) throw result.lastError;
290652
290754
  }).catch((error) => {
290653
290755
  console.error("[bootstrapControlAuthorityFromConnect] failed to establish runtime control authority; every heartbeat will report null until the next successful connect", {
290654
290756
  runtimeId: input.verified.runtimeId,
@@ -295985,6 +296087,32 @@ const readPersistedServerRuntimeState = (path) => gen(function* () {
295985
296087
  return yield* decodePersistedServerRuntimeState(trimmed).pipe(option);
295986
296088
  });
295987
296089
  //#endregion
296090
+ //#region src/orchestration/turnAdmission.ts
296091
+ /**
296092
+ * Whether a turn can actually run, as opposed to merely being accepted.
296093
+ *
296094
+ * A Forge Cloud runtime reaches its provider through CMMD, so without control
296095
+ * authority it cannot start a turn at all. It could still ACCEPT one: the
296096
+ * command validates, the thread and worktree are created, the user message is
296097
+ * recorded, and the turn then sits at `pending` forever with nothing surfaced.
296098
+ *
296099
+ * Measured on forge-cmmd-r03ad897c5e90 while CMMD was redeploying: HTTP 200 on
296100
+ * dispatch, five user messages, zero assistant messages, every turn stuck with
296101
+ * `started_at` empty. The client had no way to know.
296102
+ *
296103
+ * Kept as a pure function because the interesting part is the RULE, not the
296104
+ * plumbing: it is only cloud runtimes, only turns, and only when authority is
296105
+ * genuinely absent.
296106
+ */
296107
+ /** Commands that need a provider, and therefore need control authority. */
296108
+ const TURN_COMMANDS = new Set(["thread.turn.start"]);
296109
+ function refuseTurnWithoutControlAuthority(input) {
296110
+ if (!input.cmmdRuntimeMode) return null;
296111
+ if (!TURN_COMMANDS.has(input.commandType)) return null;
296112
+ if (input.hasControlAuthority) return null;
296113
+ return "This runtime has no CMMD control authority, so a turn cannot reach a provider and would queue without ever running. The runtime re-establishes authority on its own; retry shortly.";
296114
+ }
296115
+ //#endregion
295988
296116
  //#region ../../packages/shared/src/forgeCommands.ts
295989
296117
  /** Root of the repo-local skill contracts. */
295990
296118
  const FORGE_SKILLS_DIR = ".agents/skills";
@@ -296539,6 +296667,16 @@ const orchestrationDispatchRouteLayer = add$1("POST", "/api/orchestration/dispat
296539
296667
  message: "Invalid orchestration command payload.",
296540
296668
  cause
296541
296669
  }))));
296670
+ const admissionRefusal = refuseTurnWithoutControlAuthority({
296671
+ commandType: normalizedCommand.type,
296672
+ cmmdRuntimeMode: process.env.FORGE_CMMD_RUNTIME_MODE === "true",
296673
+ hasControlAuthority: readRuntimeControlAuthority({
296674
+ runtimeId: process.env.FORGE_CMMD_RUNTIME_ID ?? "",
296675
+ machineId: process.env.FORGE_CMMD_RUNTIME_MACHINE_ID ?? "",
296676
+ credentialEpoch: Number(process.env.FORGE_CMMD_RUNTIME_CREDENTIAL_EPOCH ?? "0")
296677
+ }) !== null
296678
+ });
296679
+ if (admissionRefusal !== null) return yield* new OrchestrationDispatchCommandError({ message: admissionRefusal });
296542
296680
  const dispatchEffect = normalizedCommand.type === "thread.turn.start" && normalizedCommand.bootstrap ? gen(function* () {
296543
296681
  const git = yield* serviceOption(GitCore);
296544
296682
  const gitStatusBroadcaster = yield* serviceOption(GitStatusBroadcaster);
@@ -298851,7 +298989,7 @@ const cmmdSetProviderAccountDefaultRouteLayer = registerProxyRoute({
298851
298989
  orgIdSource: "none",
298852
298990
  responseShape: "unwrapData"
298853
298991
  });
298854
- const resolveConnectedClaudeGenerationHash = (session) => gen(function* () {
298992
+ const resolveConnectedClaudeGeneration = (session) => gen(function* () {
298855
298993
  const outcome = yield* runCmmdRestProxy(session, (jwt) => cmmdGateway.request({
298856
298994
  method: "GET",
298857
298995
  jwt,
@@ -298865,18 +299003,22 @@ const resolveConnectedClaudeGenerationHash = (session) => gen(function* () {
298865
299003
  };
298866
299004
  return {
298867
299005
  ok: true,
298868
- value: createHash("sha256").update(state.generationId).digest("hex")
299006
+ value: {
299007
+ generationId: state.generationId,
299008
+ generationHash: createHash("sha256").update(state.generationId).digest("hex")
299009
+ }
298869
299010
  };
298870
299011
  });
298871
299012
  /**
298872
299013
  * The GET path is read-only. The explicit per-account POST refresh invokes
298873
299014
  * this helper and propagates any CMMD forward failure to the caller.
298874
299015
  */
298875
- const forwardClaudeUsageTelemetry = (session, providerAccountId = currentClaudeUsageGenerationId() ?? "", activelyPoll = false, fetchClaudeUsage = fetchClaudeUsageViaOfficialClient) => gen(function* () {
298876
- const credentialOutcome = yield* resolveConnectedClaudeGenerationHash(session);
299016
+ const forwardClaudeUsageTelemetry = (session, requestedProviderAccountId, activelyPoll = false, fetchClaudeUsage = fetchClaudeUsageViaOfficialClient) => gen(function* () {
299017
+ const credentialOutcome = yield* resolveConnectedClaudeGeneration(session);
298877
299018
  if (!credentialOutcome.ok) return credentialOutcome;
298878
- const generationId = currentClaudeUsageGenerationId();
298879
- if (credentialOutcome.value === null || generationId === null) return null;
299019
+ if (credentialOutcome.value === null) return null;
299020
+ const { generationId, generationHash } = credentialOutcome.value;
299021
+ const providerAccountId = requestedProviderAccountId ?? generationId;
298880
299022
  const activeSnapshot = activelyPoll && providerAccountId === generationId ? yield* tryPromise({
298881
299023
  try: () => fetchClaudeUsage({ binaryPath: resolveClaudeBinaryPath("claude") }),
298882
299024
  catch: (cause) => new ClaudeUsageReadError({ cause })
@@ -298884,7 +299026,7 @@ const forwardClaudeUsageTelemetry = (session, providerAccountId = currentClaudeU
298884
299026
  const snapshot = activeSnapshot ? {
298885
299027
  ...activeSnapshot,
298886
299028
  observedAt: (/* @__PURE__ */ new Date()).toISOString()
298887
- } : getLatestClaudeRateLimitTelemetry(credentialOutcome.value, providerAccountId);
299029
+ } : getLatestClaudeRateLimitTelemetry(generationHash, providerAccountId);
298888
299030
  if (!snapshot) return null;
298889
299031
  return yield* runCmmdRestProxy(session, (jwt) => cmmdGateway.request({
298890
299032
  method: "POST",
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  require("./chunk-BhaIgcZK.cjs");
5
- const require_runtimeControlAuthority = require("./runtimeControlAuthority-Dx6Y5LPI.cjs");
5
+ const require_runtimeControlAuthority = require("./runtimeControlAuthority-SNBF5Mjy.cjs");
6
6
  exports.bootstrapRuntimeControlAuthorityFromNarrowGrant = require_runtimeControlAuthority.bootstrapRuntimeControlAuthorityFromNarrowGrant;
7
7
  exports.setRuntimeGrantToken = require_runtimeControlAuthority.setRuntimeGrantToken;
8
8
  exports.verifyCmmdRuntimeTicket = require_runtimeControlAuthority.verifyCmmdRuntimeTicket;
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { n as bootstrapRuntimeControlAuthorityFromNarrowGrant, s as verifyCmmdRuntimeTicket, u as setRuntimeGrantToken } from "./runtimeControlAuthority-Dm9z5ziU.mjs";
3
+ import { n as bootstrapRuntimeControlAuthorityFromNarrowGrant, s as verifyCmmdRuntimeTicket, u as setRuntimeGrantToken } from "./runtimeControlAuthority-L85w5RTn.mjs";
4
4
  export { bootstrapRuntimeControlAuthorityFromNarrowGrant, setRuntimeGrantToken, verifyCmmdRuntimeTicket };
@@ -249,13 +249,40 @@ const CONTROL_AUTHORITY_SCOPES = new Set(["heartbeat", "replication"]);
249
249
  var RuntimeControlAuthorityError = class extends Error {
250
250
  status;
251
251
  code;
252
+ /**
253
+ * Whether trying again could plausibly succeed.
254
+ *
255
+ * A restarting control plane and a wrong credential both fail here and need
256
+ * opposite responses: the first must be retried until the deploy finishes,
257
+ * the second must not be retried at all. Without this the caller cannot tell
258
+ * them apart and picks wrong in one direction or the other.
259
+ */
260
+ retryable;
252
261
  constructor(input) {
253
262
  super(input.message);
254
263
  this.name = "RuntimeControlAuthorityError";
255
264
  this.status = input.status ?? null;
256
265
  this.code = input.code;
266
+ this.retryable = input.retryable ?? false;
257
267
  }
258
268
  };
269
+ /** Longest body prefix quoted into an error message. */
270
+ const CONTROL_AUTHORITY_BODY_PREVIEW = 180;
271
+ /**
272
+ * A short, credential-free description of a body that would not parse.
273
+ *
274
+ * The point is to identify the RESPONDER. A proxy error page says who it is in
275
+ * its first line, and that one line is the whole difference between "CMMD is
276
+ * redeploying" and a multi-hour dig through a guest.
277
+ *
278
+ * Redacted because this lands in logs and the responder is not always the one
279
+ * we think it is: anything resembling a JWT or a long opaque token is stripped
280
+ * rather than trusted to be harmless.
281
+ */
282
+ function describeUnparsableBody(body) {
283
+ const collapsed = body.replace(/[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{6,}/g, "<redacted-jwt>").replace(/[A-Za-z0-9_-]{40,}/g, "<redacted>").replace(/\s+/g, " ").trim();
284
+ return collapsed.length > CONTROL_AUTHORITY_BODY_PREVIEW ? `${collapsed.slice(0, CONTROL_AUTHORITY_BODY_PREVIEW)}…` : collapsed;
285
+ }
259
286
  let authority = null;
260
287
  let bootstrapInFlight = null;
261
288
  function normalizedControlPlaneUrl(value) {
@@ -352,15 +379,18 @@ async function readControlAuthorityResponse(response) {
352
379
  if (!response.ok) throw new RuntimeControlAuthorityError({
353
380
  status: response.status,
354
381
  code: "CMMD_RUNTIME_CONTROL_AUTHORITY_REJECTED",
382
+ retryable: response.status >= 500,
355
383
  message: `CMMD runtime control authority request failed (${response.status}).`
356
384
  });
385
+ const raw = await response.text().catch(() => "");
357
386
  try {
358
- return await response.json();
387
+ return JSON.parse(raw);
359
388
  } catch {
360
389
  throw new RuntimeControlAuthorityError({
361
390
  status: response.status,
362
391
  code: "CMMD_RUNTIME_CONTROL_AUTHORITY_INVALID_RESPONSE",
363
- message: "CMMD runtime control authority returned invalid JSON."
392
+ retryable: true,
393
+ message: `CMMD runtime control authority returned ${response.status} with a non-JSON body (content-type: ${response.headers.get("content-type") ?? "none"}): ${describeUnparsableBody(raw) || "<empty>"}`
364
394
  });
365
395
  }
366
396
  }
@@ -502,4 +532,4 @@ async function sendRuntimeControlHeartbeat(input) {
502
532
  //#endregion
503
533
  export { verifyCmmdAuthRuntimeTicket as a, verifyCmmdRuntimeTicketRefreshProof as c, createReadOnceFileTokenAccessor as d, sendRuntimeControlHeartbeat as i, runtimeGrantToken as l, bootstrapRuntimeControlAuthorityFromNarrowGrant as n, verifyCmmdClaudeReadinessTicket as o, readRuntimeControlAuthority as r, verifyCmmdRuntimeTicket as s, bootstrapRuntimeControlAuthority as t, setRuntimeGrantToken as u };
504
534
 
505
- //# sourceMappingURL=runtimeControlAuthority-Dm9z5ziU.mjs.map
535
+ //# sourceMappingURL=runtimeControlAuthority-L85w5RTn.mjs.map
@@ -250,13 +250,40 @@ const CONTROL_AUTHORITY_SCOPES = new Set(["heartbeat", "replication"]);
250
250
  var RuntimeControlAuthorityError = class extends Error {
251
251
  status;
252
252
  code;
253
+ /**
254
+ * Whether trying again could plausibly succeed.
255
+ *
256
+ * A restarting control plane and a wrong credential both fail here and need
257
+ * opposite responses: the first must be retried until the deploy finishes,
258
+ * the second must not be retried at all. Without this the caller cannot tell
259
+ * them apart and picks wrong in one direction or the other.
260
+ */
261
+ retryable;
253
262
  constructor(input) {
254
263
  super(input.message);
255
264
  this.name = "RuntimeControlAuthorityError";
256
265
  this.status = input.status ?? null;
257
266
  this.code = input.code;
267
+ this.retryable = input.retryable ?? false;
258
268
  }
259
269
  };
270
+ /** Longest body prefix quoted into an error message. */
271
+ const CONTROL_AUTHORITY_BODY_PREVIEW = 180;
272
+ /**
273
+ * A short, credential-free description of a body that would not parse.
274
+ *
275
+ * The point is to identify the RESPONDER. A proxy error page says who it is in
276
+ * its first line, and that one line is the whole difference between "CMMD is
277
+ * redeploying" and a multi-hour dig through a guest.
278
+ *
279
+ * Redacted because this lands in logs and the responder is not always the one
280
+ * we think it is: anything resembling a JWT or a long opaque token is stripped
281
+ * rather than trusted to be harmless.
282
+ */
283
+ function describeUnparsableBody(body) {
284
+ const collapsed = body.replace(/[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{12,}\.[A-Za-z0-9_-]{6,}/g, "<redacted-jwt>").replace(/[A-Za-z0-9_-]{40,}/g, "<redacted>").replace(/\s+/g, " ").trim();
285
+ return collapsed.length > CONTROL_AUTHORITY_BODY_PREVIEW ? `${collapsed.slice(0, CONTROL_AUTHORITY_BODY_PREVIEW)}…` : collapsed;
286
+ }
260
287
  let authority = null;
261
288
  let bootstrapInFlight = null;
262
289
  function normalizedControlPlaneUrl(value) {
@@ -353,15 +380,18 @@ async function readControlAuthorityResponse(response) {
353
380
  if (!response.ok) throw new RuntimeControlAuthorityError({
354
381
  status: response.status,
355
382
  code: "CMMD_RUNTIME_CONTROL_AUTHORITY_REJECTED",
383
+ retryable: response.status >= 500,
356
384
  message: `CMMD runtime control authority request failed (${response.status}).`
357
385
  });
386
+ const raw = await response.text().catch(() => "");
358
387
  try {
359
- return await response.json();
388
+ return JSON.parse(raw);
360
389
  } catch {
361
390
  throw new RuntimeControlAuthorityError({
362
391
  status: response.status,
363
392
  code: "CMMD_RUNTIME_CONTROL_AUTHORITY_INVALID_RESPONSE",
364
- message: "CMMD runtime control authority returned invalid JSON."
393
+ retryable: true,
394
+ message: `CMMD runtime control authority returned ${response.status} with a non-JSON body (content-type: ${response.headers.get("content-type") ?? "none"}): ${describeUnparsableBody(raw) || "<empty>"}`
365
395
  });
366
396
  }
367
397
  }
@@ -568,4 +598,4 @@ Object.defineProperty(exports, "verifyCmmdRuntimeTicketRefreshProof", {
568
598
  }
569
599
  });
570
600
 
571
- //# sourceMappingURL=runtimeControlAuthority-Dx6Y5LPI.cjs.map
601
+ //# sourceMappingURL=runtimeControlAuthority-SNBF5Mjy.cjs.map
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "cmmd-forge": "dist/bin.mjs"
10
10
  },
11
11
  "type": "module",
12
- "version": "0.9.4",
12
+ "version": "0.9.5",
13
13
  "engines": {
14
14
  "node": "^22.16 || ^23.11 || >=24.10"
15
15
  },