@cmmd-center/forge 0.9.6 → 0.9.8

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
@@ -95950,6 +95950,35 @@ const prepareReplicaSummary = require_Schema$1.fn("prepareReplicaSummary")(funct
95950
95950
  candidate
95951
95951
  });
95952
95952
  });
95953
+ /**
95954
+ * Contains a per-thread hydration failure so it cannot brick the runtime.
95955
+ *
95956
+ * Hydration is a recovery convenience: it replays durable history into a fresh
95957
+ * runtime. One thread that fails identity or generation checks used to fail the
95958
+ * WHOLE hydration, which `serverRuntimeStartup` turned into
95959
+ * `commandGate.failCommandReady()` — the HTTP server kept serving 200 and the
95960
+ * runtime still reported `server_ready`, while every turn was refused with
95961
+ * "Could not connect".
95962
+ *
95963
+ * Observed in production 2026-08-31: a thread replayed from a DESTROYED runtime
95964
+ * failed its generation check, and because the poison row lives in CMMD's
95965
+ * durable replica rather than on the guest, destroying and reprovisioning the
95966
+ * sprite did not clear it. One bad row could brick every future runtime for
95967
+ * that user, permanently.
95968
+ *
95969
+ * Structural failures (index runtime identity changing between pages, a
95970
+ * repeated cursor) stay fatal on purpose: those mean the index itself cannot be
95971
+ * trusted, so continuing would hydrate an unknown mixture.
95972
+ */
95973
+ function skipUnhydratableThread(operation, threadId, state) {
95974
+ return operation.pipe(require_Schema$1.catchTag("RuntimeReplicaHydrationError", (error) => require_Schema$1.gen(function* () {
95975
+ state.unhydratableThreadIds.push(threadId);
95976
+ yield* require_Schema$1.logWarning("skipped a thread that could not be hydrated", {
95977
+ threadId,
95978
+ detail: error.detail
95979
+ });
95980
+ })));
95981
+ }
95953
95982
  const applyPreparedReplica = require_Schema$1.fn("applyPreparedReplica")(function* (expectedRuntimeId, prepared, state, dependencies) {
95954
95983
  const { summary, candidate } = prepared;
95955
95984
  if (!(yield* dependencies.restoreThreadCursor({
@@ -95975,7 +96004,7 @@ function prepareReplicaPages(expectedRuntimeId, cursor, state, dependencies) {
95975
96004
  if (index === null) return;
95976
96005
  if (state.runtimeIdentity === null) state.runtimeIdentity = index.runtime;
95977
96006
  else if (!replicaRuntimeMatches(state.runtimeIdentity, index.runtime)) return yield* replicaHydrationFailure("index runtime identity changed between pages");
95978
- yield* require_Schema$1.forEach(index.threads, (summary) => prepareReplicaSummary(expectedRuntimeId, index, summary, state, dependencies), {
96007
+ yield* require_Schema$1.forEach(index.threads, (summary) => skipUnhydratableThread(prepareReplicaSummary(expectedRuntimeId, index, summary, state, dependencies), summary.threadId, state), {
95979
96008
  concurrency: 1,
95980
96009
  discard: true
95981
96010
  });
@@ -95995,16 +96024,19 @@ const hydrateRuntimeReplica = require_Schema$1.fn("hydrateRuntimeReplica")(funct
95995
96024
  prepared: [],
95996
96025
  runtimeIdentity: null,
95997
96026
  hydrated: 0,
95998
- skippedExisting: 0
96027
+ skippedExisting: 0,
96028
+ unhydratableThreadIds: []
95999
96029
  };
96000
96030
  yield* prepareReplicaPages(input.expectedRuntimeId, void 0, state, dependencies);
96001
- yield* require_Schema$1.forEach(state.prepared, (prepared) => applyPreparedReplica(input.expectedRuntimeId, prepared, state, dependencies), {
96031
+ yield* require_Schema$1.forEach(state.prepared, (prepared) => skipUnhydratableThread(applyPreparedReplica(input.expectedRuntimeId, prepared, state, dependencies), prepared.summary.threadId, state), {
96002
96032
  concurrency: 1,
96003
96033
  discard: true
96004
96034
  });
96005
96035
  return {
96006
96036
  hydrated: state.hydrated,
96007
- skippedExisting: state.skippedExisting
96037
+ skippedExisting: state.skippedExisting,
96038
+ skippedUnhydratable: state.unhydratableThreadIds.length,
96039
+ unhydratableThreadIds: [...state.unhydratableThreadIds]
96008
96040
  };
96009
96041
  });
96010
96042
  //#endregion
@@ -301957,8 +301989,52 @@ function serializeNativeToolActivity(activity) {
301957
301989
  }
301958
301990
  };
301959
301991
  }
301992
+ /** The failure activities `appendProviderFailureActivity` emits, all `provider.*.failed`. */
301993
+ function isProviderFailureActivity(activity) {
301994
+ return activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
301995
+ }
301996
+ function failureDetail(activity) {
301997
+ const payload = activity.payload;
301998
+ if (!payload || typeof payload !== "object" || !("detail" in payload)) return null;
301999
+ const detail = payload.detail;
302000
+ return typeof detail === "string" && detail.trim() ? detail.trim() : null;
302001
+ }
302002
+ /**
302003
+ * Replicates why a turn failed.
302004
+ *
302005
+ * Without this a failure never crossed the replication boundary — only `tool.`
302006
+ * activities were serialized — so durable history could not show one and every
302007
+ * failure reached the user as an endless "Working" spinner. Measured in
302008
+ * production on 2026-08-30: zero failure events had EVER been recorded against
302009
+ * 652 tool_call rows, which is how a workspace-path block, a broker-token
302010
+ * refusal and a dispatch into a deleted directory all looked identical.
302011
+ *
302012
+ * `error_summary` is used rather than a new kind because both the Forge
302013
+ * protocol and CMMD's ingest schema already accept it; nothing here needed a
302014
+ * contract change. The detail carries the actionable sentence, so it is joined
302015
+ * onto the summary — the event has no separate detail field.
302016
+ */
302017
+ function serializeProviderFailureActivity(activity) {
302018
+ if (!isProviderFailureActivity(activity)) return null;
302019
+ const summary = text(activity.summary, 4e3);
302020
+ if (summary === null) return null;
302021
+ const detail = failureDetail(activity);
302022
+ return {
302023
+ sourceSequence: null,
302024
+ event: {
302025
+ eventId: `activity:${activity.id}`,
302026
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
302027
+ kind: "error_summary",
302028
+ summary: text(detail === null ? summary : `${summary}: ${detail}`, 4e3) ?? summary
302029
+ }
302030
+ };
302031
+ }
301960
302032
  function serializeRuntimeThreadEvents(thread) {
301961
302033
  const messages = thread.messages.flatMap(serializeThreadMessage);
302034
+ const failureActivities = thread.activities.flatMap((activity) => {
302035
+ const event = serializeProviderFailureActivity(activity);
302036
+ return event === null ? [] : [event];
302037
+ });
301962
302038
  const replicaActivities = thread.activities.flatMap((activity) => {
301963
302039
  const event = serializeReplicaActivity(activity);
301964
302040
  return event === null ? [] : [event];
@@ -301970,7 +302046,8 @@ function serializeRuntimeThreadEvents(thread) {
301970
302046
  return [
301971
302047
  ...messages,
301972
302048
  ...replicaActivities,
301973
- ...toolActivities
302049
+ ...toolActivities,
302050
+ ...failureActivities
301974
302051
  ].toSorted((left, right) => {
301975
302052
  if (left.sourceSequence !== null && right.sourceSequence !== null) return left.sourceSequence - right.sourceSequence;
301976
302053
  if (left.sourceSequence !== null) return -1;
package/dist/bin.mjs CHANGED
@@ -95649,6 +95649,35 @@ const prepareReplicaSummary = fn$1("prepareReplicaSummary")(function* (expectedR
95649
95649
  candidate
95650
95650
  });
95651
95651
  });
95652
+ /**
95653
+ * Contains a per-thread hydration failure so it cannot brick the runtime.
95654
+ *
95655
+ * Hydration is a recovery convenience: it replays durable history into a fresh
95656
+ * runtime. One thread that fails identity or generation checks used to fail the
95657
+ * WHOLE hydration, which `serverRuntimeStartup` turned into
95658
+ * `commandGate.failCommandReady()` — the HTTP server kept serving 200 and the
95659
+ * runtime still reported `server_ready`, while every turn was refused with
95660
+ * "Could not connect".
95661
+ *
95662
+ * Observed in production 2026-08-31: a thread replayed from a DESTROYED runtime
95663
+ * failed its generation check, and because the poison row lives in CMMD's
95664
+ * durable replica rather than on the guest, destroying and reprovisioning the
95665
+ * sprite did not clear it. One bad row could brick every future runtime for
95666
+ * that user, permanently.
95667
+ *
95668
+ * Structural failures (index runtime identity changing between pages, a
95669
+ * repeated cursor) stay fatal on purpose: those mean the index itself cannot be
95670
+ * trusted, so continuing would hydrate an unknown mixture.
95671
+ */
95672
+ function skipUnhydratableThread(operation, threadId, state) {
95673
+ return operation.pipe(catchTag("RuntimeReplicaHydrationError", (error) => gen(function* () {
95674
+ state.unhydratableThreadIds.push(threadId);
95675
+ yield* logWarning$1("skipped a thread that could not be hydrated", {
95676
+ threadId,
95677
+ detail: error.detail
95678
+ });
95679
+ })));
95680
+ }
95652
95681
  const applyPreparedReplica = fn$1("applyPreparedReplica")(function* (expectedRuntimeId, prepared, state, dependencies) {
95653
95682
  const { summary, candidate } = prepared;
95654
95683
  if (!(yield* dependencies.restoreThreadCursor({
@@ -95674,7 +95703,7 @@ function prepareReplicaPages(expectedRuntimeId, cursor, state, dependencies) {
95674
95703
  if (index === null) return;
95675
95704
  if (state.runtimeIdentity === null) state.runtimeIdentity = index.runtime;
95676
95705
  else if (!replicaRuntimeMatches(state.runtimeIdentity, index.runtime)) return yield* replicaHydrationFailure("index runtime identity changed between pages");
95677
- yield* forEach(index.threads, (summary) => prepareReplicaSummary(expectedRuntimeId, index, summary, state, dependencies), {
95706
+ yield* forEach(index.threads, (summary) => skipUnhydratableThread(prepareReplicaSummary(expectedRuntimeId, index, summary, state, dependencies), summary.threadId, state), {
95678
95707
  concurrency: 1,
95679
95708
  discard: true
95680
95709
  });
@@ -95694,16 +95723,19 @@ const hydrateRuntimeReplica = fn$1("hydrateRuntimeReplica")(function* (input, de
95694
95723
  prepared: [],
95695
95724
  runtimeIdentity: null,
95696
95725
  hydrated: 0,
95697
- skippedExisting: 0
95726
+ skippedExisting: 0,
95727
+ unhydratableThreadIds: []
95698
95728
  };
95699
95729
  yield* prepareReplicaPages(input.expectedRuntimeId, void 0, state, dependencies);
95700
- yield* forEach(state.prepared, (prepared) => applyPreparedReplica(input.expectedRuntimeId, prepared, state, dependencies), {
95730
+ yield* forEach(state.prepared, (prepared) => skipUnhydratableThread(applyPreparedReplica(input.expectedRuntimeId, prepared, state, dependencies), prepared.summary.threadId, state), {
95701
95731
  concurrency: 1,
95702
95732
  discard: true
95703
95733
  });
95704
95734
  return {
95705
95735
  hydrated: state.hydrated,
95706
- skippedExisting: state.skippedExisting
95736
+ skippedExisting: state.skippedExisting,
95737
+ skippedUnhydratable: state.unhydratableThreadIds.length,
95738
+ unhydratableThreadIds: [...state.unhydratableThreadIds]
95707
95739
  };
95708
95740
  });
95709
95741
  //#endregion
@@ -301412,8 +301444,52 @@ function serializeNativeToolActivity(activity) {
301412
301444
  }
301413
301445
  };
301414
301446
  }
301447
+ /** The failure activities `appendProviderFailureActivity` emits, all `provider.*.failed`. */
301448
+ function isProviderFailureActivity(activity) {
301449
+ return activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
301450
+ }
301451
+ function failureDetail(activity) {
301452
+ const payload = activity.payload;
301453
+ if (!payload || typeof payload !== "object" || !("detail" in payload)) return null;
301454
+ const detail = payload.detail;
301455
+ return typeof detail === "string" && detail.trim() ? detail.trim() : null;
301456
+ }
301457
+ /**
301458
+ * Replicates why a turn failed.
301459
+ *
301460
+ * Without this a failure never crossed the replication boundary — only `tool.`
301461
+ * activities were serialized — so durable history could not show one and every
301462
+ * failure reached the user as an endless "Working" spinner. Measured in
301463
+ * production on 2026-08-30: zero failure events had EVER been recorded against
301464
+ * 652 tool_call rows, which is how a workspace-path block, a broker-token
301465
+ * refusal and a dispatch into a deleted directory all looked identical.
301466
+ *
301467
+ * `error_summary` is used rather than a new kind because both the Forge
301468
+ * protocol and CMMD's ingest schema already accept it; nothing here needed a
301469
+ * contract change. The detail carries the actionable sentence, so it is joined
301470
+ * onto the summary — the event has no separate detail field.
301471
+ */
301472
+ function serializeProviderFailureActivity(activity) {
301473
+ if (!isProviderFailureActivity(activity)) return null;
301474
+ const summary = text(activity.summary, 4e3);
301475
+ if (summary === null) return null;
301476
+ const detail = failureDetail(activity);
301477
+ return {
301478
+ sourceSequence: null,
301479
+ event: {
301480
+ eventId: `activity:${activity.id}`,
301481
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
301482
+ kind: "error_summary",
301483
+ summary: text(detail === null ? summary : `${summary}: ${detail}`, 4e3) ?? summary
301484
+ }
301485
+ };
301486
+ }
301415
301487
  function serializeRuntimeThreadEvents(thread) {
301416
301488
  const messages = thread.messages.flatMap(serializeThreadMessage);
301489
+ const failureActivities = thread.activities.flatMap((activity) => {
301490
+ const event = serializeProviderFailureActivity(activity);
301491
+ return event === null ? [] : [event];
301492
+ });
301417
301493
  const replicaActivities = thread.activities.flatMap((activity) => {
301418
301494
  const event = serializeReplicaActivity(activity);
301419
301495
  return event === null ? [] : [event];
@@ -301425,7 +301501,8 @@ function serializeRuntimeThreadEvents(thread) {
301425
301501
  return [
301426
301502
  ...messages,
301427
301503
  ...replicaActivities,
301428
- ...toolActivities
301504
+ ...toolActivities,
301505
+ ...failureActivities
301429
301506
  ].toSorted((left, right) => {
301430
301507
  if (left.sourceSequence !== null && right.sourceSequence !== null) return left.sourceSequence - right.sourceSequence;
301431
301508
  if (left.sourceSequence !== null) return -1;
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.6",
12
+ "version": "0.9.8",
13
13
  "engines": {
14
14
  "node": "^22.16 || ^23.11 || >=24.10"
15
15
  },