@openclawbrain/cli 0.4.14 → 0.4.16

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/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
3
4
  import path from "node:path";
4
5
  import process from "node:process";
5
6
  import { compileRuntimeFromActivation } from "@openclawbrain/compiler";
@@ -11,8 +12,8 @@ import { inspectOpenClawBrainHookStatus, summarizeOpenClawBrainHookLoad } from "
11
12
  import { appendLearningUpdateLogs, appendServeTimeRouteDecisionLog } from "./learning-spine.js";
12
13
  import { buildFeedbackSemanticMetadata, buildInteractionSemanticMetadata } from "./semantic-metadata.js";
13
14
  export { clearOpenClawProfileRuntimeLoadProof, listOpenClawProfileRuntimeLoadProofs, recordOpenClawProfileRuntimeLoadProof, resolveAttachmentRuntimeLoadProofsPath } from "./attachment-truth.js";
14
- import { createTeacherLabeler } from "./teacher-labeler.js";
15
- export { createHttpOllamaTeacherLabelerClient, createOllamaTeacherLabeler, createTeacherLabeler } from "./teacher-labeler.js";
15
+ import { createTeacherLabeler, summarizeTeacherLabelerOpportunity } from "./teacher-labeler.js";
16
+ export { createHttpOllamaTeacherLabelerClient, createOllamaTeacherLabeler, createTeacherLabeler, summarizeTeacherLabelerOpportunity } from "./teacher-labeler.js";
16
17
  const DEFAULT_AGENT_ID = "openclaw-runtime";
17
18
  const FEEDBACK_KINDS = new Set(["correction", "teaching", "approval", "suppression"]);
18
19
  export const DEFAULT_ASYNC_TEACHER_QUEUE_CAPACITY = 8;
@@ -27,6 +28,27 @@ export const RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT = {
27
28
  manifest: "manifest.json",
28
29
  payload: "normalized-event-export.json"
29
30
  };
31
+ const RECORDED_SESSION_REPLAY_PROOF_MANIFEST_CONTRACT = "recorded_session_replay_proof_manifest.v1";
32
+ const RECORDED_SESSION_REPLAY_PROOF_ENVIRONMENT_CONTRACT = "recorded_session_replay_environment.v1";
33
+ const RECORDED_SESSION_REPLAY_PROOF_SUMMARY_TABLES_CONTRACT = "recorded_session_replay_summary_tables.v1";
34
+ const RECORDED_SESSION_REPLAY_PROOF_COVERAGE_SNAPSHOT_CONTRACT = "recorded_session_replay_coverage_snapshot.v1";
35
+ const RECORDED_SESSION_REPLAY_PROOF_HARDENING_SNAPSHOT_CONTRACT = "recorded_session_replay_hardening_snapshot.v1";
36
+ const RECORDED_SESSION_REPLAY_PROOF_HASHES_CONTRACT = "recorded_session_replay_hashes.v1";
37
+ const RECORDED_SESSION_REPLAY_PROOF_VALIDATION_CONTRACT = "recorded_session_replay_proof_validation.v1";
38
+ const RECORDED_SESSION_REPLAY_MODE_ORDER = ["no_brain", "vector_only", "graph_prior_only", "learned_route"];
39
+ export const RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT = {
40
+ manifest: "manifest.json",
41
+ trace: "trace.json",
42
+ fixture: "fixture.json",
43
+ bundle: "bundle.json",
44
+ environment: "environment.json",
45
+ summary: "summary.md",
46
+ summaryTables: "summary-tables.json",
47
+ coverageSnapshot: "coverage-snapshot.json",
48
+ hardeningSnapshot: "hardening-snapshot.json",
49
+ hashes: "hashes.json",
50
+ modeDir: "modes"
51
+ };
30
52
  function normalizeRuntimeProfileSelector(value, fieldName, fallback = "current_profile") {
31
53
  if (value === undefined || value === null) {
32
54
  return fallback;
@@ -228,9 +250,45 @@ function buildAsyncTeacherLoopNotes(input) {
228
250
  `teacher_noop=${input.noOpReason}`,
229
251
  `teacher_labeler=${input.teacherLabeler?.status ?? "disabled"}`,
230
252
  `teacher_labeler_detail=${input.teacherLabeler?.detail ?? "disabled"}`,
253
+ `teacher_last_cycle_deterministic_artifacts=${input.lastCycle?.deterministicArtifactCount ?? "unknown"}`,
254
+ `teacher_last_cycle_new_deterministic_artifacts=${input.lastCycle?.newDeterministicArtifactCount ?? "unknown"}`,
255
+ `teacher_last_cycle_labeler_candidates=${input.lastCycle?.labelerCandidateCount ?? "unknown"}`,
256
+ `teacher_last_cycle_labeler_budgeted_candidates=${input.lastCycle?.labelerBudgetedCandidateCount ?? "unknown"}`,
257
+ `teacher_last_cycle_labeler_status=${input.lastCycle?.labelerStatus ?? "unknown"}`,
258
+ `teacher_last_cycle_labeler_detail=${input.lastCycle?.labelerDetail ?? "unknown"}`,
231
259
  input.materialization === null ? "teacher_materialization=noop" : `teacher_materialized_pack=${input.materialization.candidate.summary.packId}`
232
260
  ];
233
261
  }
262
+ function parseAsyncTeacherLastCycleNotes(notes) {
263
+ const values = new Map();
264
+ for (const note of notes) {
265
+ const separator = note.indexOf("=");
266
+ if (separator <= 0) {
267
+ continue;
268
+ }
269
+ values.set(note.slice(0, separator), note.slice(separator + 1));
270
+ }
271
+ const readNullableNumber = (key) => {
272
+ const raw = values.get(key);
273
+ if (raw === undefined || raw === "unknown") {
274
+ return null;
275
+ }
276
+ const parsed = Number.parseInt(raw, 10);
277
+ return Number.isFinite(parsed) ? parsed : null;
278
+ };
279
+ const readNullableString = (key) => {
280
+ const raw = values.get(key);
281
+ return raw === undefined || raw === "unknown" ? null : raw;
282
+ };
283
+ return {
284
+ deterministicArtifactCount: readNullableNumber("teacher_last_cycle_deterministic_artifacts"),
285
+ newDeterministicArtifactCount: readNullableNumber("teacher_last_cycle_new_deterministic_artifacts"),
286
+ labelerCandidateCount: readNullableNumber("teacher_last_cycle_labeler_candidates"),
287
+ labelerBudgetedCandidateCount: readNullableNumber("teacher_last_cycle_labeler_budgeted_candidates"),
288
+ labelerStatus: readNullableString("teacher_last_cycle_labeler_status"),
289
+ labelerDetail: readNullableString("teacher_last_cycle_labeler_detail")
290
+ };
291
+ }
234
292
  function cloneAlwaysOnLearningMaterializationJobOrNull(value) {
235
293
  return value === null ? null : structuredClone(value);
236
294
  }
@@ -543,6 +601,14 @@ export class AsyncTeacherLiveLoop {
543
601
  learnerState = createAlwaysOnLearningRuntimeState();
544
602
  lastMaterialization = null;
545
603
  lastTeacherLabelerResult = null;
604
+ lastCycle = {
605
+ deterministicArtifactCount: null,
606
+ newDeterministicArtifactCount: null,
607
+ labelerCandidateCount: null,
608
+ labelerBudgetedCandidateCount: null,
609
+ labelerStatus: null,
610
+ labelerDetail: null
611
+ };
546
612
  diagnostics = {
547
613
  acceptedExportCount: 0,
548
614
  processedExportCount: 0,
@@ -562,7 +628,8 @@ export class AsyncTeacherLiveLoop {
562
628
  sparseFeedback: this.learnerState.sparseFeedback,
563
629
  noOpReason: "none",
564
630
  materialization: null,
565
- teacherLabeler: null
631
+ teacherLabeler: null,
632
+ lastCycle: this.lastCycle
566
633
  })
567
634
  };
568
635
  constructor(input) {
@@ -590,6 +657,7 @@ export class AsyncTeacherLiveLoop {
590
657
  ...structuredClone(resumedSnapshot.diagnostics),
591
658
  notes: [...resumedSnapshot.diagnostics.notes]
592
659
  };
660
+ this.lastCycle = parseAsyncTeacherLastCycleNotes(this.diagnostics.notes);
593
661
  for (const exportDigest of resumedSnapshot.state?.seenExportDigests ?? []) {
594
662
  this.seenExportDigests.add(exportDigest);
595
663
  }
@@ -836,6 +904,19 @@ export class AsyncTeacherLiveLoop {
836
904
  feedbackEvents: this.feedbackEvents
837
905
  });
838
906
  const learnedRoutingState = this.input.resolveLearnedRoutingState?.() ?? {};
907
+ const currentDedupIds = new Set(this.teacherArtifacts.map((artifact) => artifact.dedupId));
908
+ const currentCycleBuiltArtifacts = buildTeacherSupervisionArtifactsFromNormalizedEventExport({
909
+ normalizedEventExport: job.normalizedEventExport,
910
+ observedAt: job.observedAt,
911
+ staleAfterMs: this.staleAfterMs,
912
+ ...(this.input.sparseFeedback !== undefined ? { sparseFeedback: this.input.sparseFeedback } : {})
913
+ });
914
+ const currentCycleOpportunity = summarizeTeacherLabelerOpportunity({
915
+ normalizedEventExport: job.normalizedEventExport,
916
+ ...(learnedRoutingState.serveTimeDecisions !== undefined
917
+ ? { serveTimeDecisions: learnedRoutingState.serveTimeDecisions }
918
+ : {})
919
+ }, this.input.teacherLabeler ?? null);
839
920
  const builtArtifacts = buildTeacherSupervisionArtifactsFromNormalizedEventExport({
840
921
  normalizedEventExport: mergedNormalizedEventExport,
841
922
  observedAt: job.observedAt,
@@ -865,10 +946,21 @@ export class AsyncTeacherLiveLoop {
865
946
  }
866
947
  }
867
948
  const nextBuiltArtifacts = mergeTeacherArtifacts([], [...builtArtifacts, ...generatedTeacherArtifacts]);
868
- const currentDedupIds = new Set(this.teacherArtifacts.map((artifact) => artifact.dedupId));
869
949
  const nextTeacherArtifacts = mergeTeacherArtifacts(this.teacherArtifacts, nextBuiltArtifacts);
870
950
  const emittedArtifactCount = nextBuiltArtifacts.filter((artifact) => !currentDedupIds.has(artifact.dedupId)).length;
871
951
  const dedupedArtifactCount = nextBuiltArtifacts.length - emittedArtifactCount;
952
+ this.lastCycle = {
953
+ deterministicArtifactCount: currentCycleBuiltArtifacts.length,
954
+ newDeterministicArtifactCount: currentCycleBuiltArtifacts.filter((artifact) => !currentDedupIds.has(artifact.dedupId)).length,
955
+ labelerCandidateCount: currentCycleOpportunity.candidateCount,
956
+ labelerBudgetedCandidateCount: currentCycleOpportunity.budgetedCandidateCount,
957
+ labelerStatus: currentCycleOpportunity.candidateCount === 0
958
+ ? currentCycleOpportunity.status
959
+ : this.lastTeacherLabelerResult?.status ?? (currentCycleOpportunity.enabled ? "unknown" : "disabled"),
960
+ labelerDetail: currentCycleOpportunity.candidateCount === 0
961
+ ? currentCycleOpportunity.detail
962
+ : this.lastTeacherLabelerResult?.detail ?? currentCycleOpportunity.detail
963
+ };
872
964
  this.teacherArtifacts = nextTeacherArtifacts;
873
965
  const learnerResult = advanceAlwaysOnLearningRuntime({
874
966
  packLabel: this.input.packLabel,
@@ -930,7 +1022,8 @@ export class AsyncTeacherLiveLoop {
930
1022
  sparseFeedback: this.learnerState.sparseFeedback,
931
1023
  noOpReason: this.diagnostics.lastNoOpReason,
932
1024
  materialization: this.lastMaterialization,
933
- teacherLabeler: this.lastTeacherLabelerResult
1025
+ teacherLabeler: this.lastTeacherLabelerResult,
1026
+ lastCycle: this.lastCycle
934
1027
  });
935
1028
  }
936
1029
  }
@@ -1058,6 +1151,24 @@ export function scanLiveEventExport(input) {
1058
1151
  function readJsonFile(filePath) {
1059
1152
  return JSON.parse(readFileSync(filePath, "utf8"));
1060
1153
  }
1154
+ function checksumTextPayload(value) {
1155
+ return `sha256-${createHash("sha256").update(value).digest("hex")}`;
1156
+ }
1157
+ function orderedRecordedSessionReplayModes(modes) {
1158
+ const byMode = new Map(modes.map((mode) => [mode.mode, mode]));
1159
+ return RECORDED_SESSION_REPLAY_MODE_ORDER
1160
+ .map((mode) => byMode.get(mode))
1161
+ .filter(isPresent);
1162
+ }
1163
+ function recordedSessionReplayModeOutputPath(mode) {
1164
+ return path.posix.join(RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.modeDir, `${mode}.json`);
1165
+ }
1166
+ function resolveRecordedSessionReplayProofPath(rootDir, relativePath, fieldName) {
1167
+ return resolveBundlePayloadPath(rootDir, normalizeNonEmptyString(relativePath, fieldName));
1168
+ }
1169
+ function readRecordedSessionReplayProofText(rootDir, relativePath, fieldName) {
1170
+ return readFileSync(resolveRecordedSessionReplayProofPath(rootDir, relativePath, fieldName), "utf8");
1171
+ }
1061
1172
  export function resolveAsyncTeacherLiveLoopSnapshotPath(activationRoot) {
1062
1173
  return path.join(path.resolve(normalizeNonEmptyString(activationRoot, "activationRoot")), "async-teacher-live-loop.snapshot.json");
1063
1174
  }
@@ -2340,8 +2451,61 @@ function normalizeIsoTimestamp(value, fieldName, fallbackValue) {
2340
2451
  }
2341
2452
  return new Date(candidate).toISOString();
2342
2453
  }
2343
- function normalizeMode(value) {
2344
- return value ?? "heuristic";
2454
+ const RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG = {
2455
+ vector_only: {
2456
+ routeMode: "heuristic",
2457
+ selectionMode: "flat_rank_v1"
2458
+ },
2459
+ graph_prior_only: {
2460
+ routeMode: "heuristic",
2461
+ selectionMode: "graph_walk_v1"
2462
+ },
2463
+ learned_route: {
2464
+ routeMode: "learned",
2465
+ selectionMode: "graph_walk_v1"
2466
+ }
2467
+ };
2468
+ function resolveRuntimeComparativeReplayMode(value) {
2469
+ return Object.prototype.hasOwnProperty.call(RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG, value)
2470
+ ? value
2471
+ : null;
2472
+ }
2473
+ function resolveCompileModePlan(modeValue, selectionModeValue) {
2474
+ const requestedSelectionMode = normalizeCompileSelectionMode(selectionModeValue);
2475
+ const comparativeMode = resolveRuntimeComparativeReplayMode(modeValue);
2476
+ if (comparativeMode === null) {
2477
+ if (modeValue === undefined) {
2478
+ return {
2479
+ comparativeMode: null,
2480
+ routeMode: "heuristic",
2481
+ selectionMode: requestedSelectionMode
2482
+ };
2483
+ }
2484
+ if (modeValue === "heuristic" || modeValue === "learned") {
2485
+ return {
2486
+ comparativeMode: null,
2487
+ routeMode: modeValue,
2488
+ selectionMode: requestedSelectionMode
2489
+ };
2490
+ }
2491
+ throw new Error("mode must be heuristic, learned, vector_only, graph_prior_only, or learned_route");
2492
+ }
2493
+ const plan = RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG[comparativeMode];
2494
+ if (requestedSelectionMode !== undefined && requestedSelectionMode !== plan.selectionMode) {
2495
+ throw new Error(`selectionMode ${requestedSelectionMode} conflicts with comparative mode ${comparativeMode}, expected ${plan.selectionMode}`);
2496
+ }
2497
+ return {
2498
+ comparativeMode,
2499
+ routeMode: plan.routeMode,
2500
+ selectionMode: plan.selectionMode
2501
+ };
2502
+ }
2503
+ function resolveSyntheticTurnMode(value) {
2504
+ const comparativeMode = resolveRuntimeComparativeReplayMode(value);
2505
+ if (comparativeMode !== null) {
2506
+ return RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG[comparativeMode].routeMode;
2507
+ }
2508
+ return value === "heuristic" || value === "learned" ? value : undefined;
2345
2509
  }
2346
2510
  function normalizeCompileSelectionMode(value) {
2347
2511
  if (value === undefined) {
@@ -2681,8 +2845,9 @@ function appendCompileServeRouteDecisionLog(input) {
2681
2845
  if (input.compileInput.budgetStrategy === "fixed_v1" || input.compileInput.budgetStrategy === "empirical_v1") {
2682
2846
  syntheticTurn.budgetStrategy = input.compileInput.budgetStrategy;
2683
2847
  }
2684
- if (input.compileInput.mode === "heuristic" || input.compileInput.mode === "learned") {
2685
- syntheticTurn.mode = input.compileInput.mode;
2848
+ const syntheticTurnMode = resolveSyntheticTurnMode(input.compileInput.mode);
2849
+ if (syntheticTurnMode !== undefined) {
2850
+ syntheticTurn.mode = syntheticTurnMode;
2686
2851
  }
2687
2852
  if (input.compileInput.runtimeHints !== undefined) {
2688
2853
  syntheticTurn.runtimeHints = input.compileInput.runtimeHints;
@@ -2936,16 +3101,42 @@ export function resolveActivePackForCompile(activationRoot) {
2936
3101
  inspection: inspection.active
2937
3102
  };
2938
3103
  }
3104
+ function normalizeFrozenReplayEvalIdentity(value) {
3105
+ if (value === undefined || value === null) {
3106
+ return null;
3107
+ }
3108
+ if (typeof value !== "object") {
3109
+ throw new Error("_frozenReplayEvalIdentity must be an object");
3110
+ }
3111
+ const frozenIdentity = value;
3112
+ return {
3113
+ packId: normalizeNonEmptyString(frozenIdentity.packId, "_frozenReplayEvalIdentity.packId"),
3114
+ routerIdentity: normalizeOptionalString(frozenIdentity.routerIdentity) ?? null
3115
+ };
3116
+ }
3117
+ function validateFrozenReplayEvalIdentity(target, frozenReplayEvalIdentity) {
3118
+ if (frozenReplayEvalIdentity === null) {
3119
+ return;
3120
+ }
3121
+ const actualRouterIdentity = target.inspection.routerIdentity ?? null;
3122
+ if (target.activePointer.packId === frozenReplayEvalIdentity.packId &&
3123
+ actualRouterIdentity === frozenReplayEvalIdentity.routerIdentity) {
3124
+ return;
3125
+ }
3126
+ throw new Error(`Frozen replay eval identity mismatch: expected pack ${frozenReplayEvalIdentity.packId} (routerIdentity=${frozenReplayEvalIdentity.routerIdentity ?? "null"}), received pack ${target.activePointer.packId} (routerIdentity=${actualRouterIdentity ?? "null"})`);
3127
+ }
2939
3128
  export function compileRuntimeContext(input) {
2940
3129
  const totalStartedAtNs = monotonicClockNs();
2941
3130
  const fallbackActivationRoot = resolveActivationRootForFailure(input.activationRoot);
2942
3131
  let activationRoot = fallbackActivationRoot;
2943
3132
  let agentId = process.env.OPENCLAWBRAIN_AGENT_ID ?? DEFAULT_AGENT_ID;
2944
3133
  let runtimeHints = [];
3134
+ let comparativeMode = null;
2945
3135
  let selectionMode;
2946
3136
  let userMessage = "";
2947
3137
  let maxContextChars;
2948
3138
  let mode = "heuristic";
3139
+ let frozenReplayEvalIdentity = null;
2949
3140
  let routeSelectionStartedAtNs = null;
2950
3141
  let routeSelectionMs = null;
2951
3142
  let promptAssemblyStartedAtNs = null;
@@ -2955,13 +3146,16 @@ export function compileRuntimeContext(input) {
2955
3146
  activationRoot = path.resolve(normalizeNonEmptyString(input.activationRoot, "activationRoot"));
2956
3147
  agentId = normalizeOptionalString(input.agentId) ?? process.env.OPENCLAWBRAIN_AGENT_ID ?? DEFAULT_AGENT_ID;
2957
3148
  runtimeHints = normalizeRuntimeHints(input.runtimeHints);
2958
- selectionMode = normalizeCompileSelectionMode(input.selectionMode);
2959
3149
  userMessage = normalizeNonEmptyString(input.message, "message");
2960
3150
  maxContextChars =
2961
3151
  input.maxContextChars !== undefined
2962
3152
  ? normalizeNonNegativeInteger(input.maxContextChars, "maxContextChars", input.maxContextChars)
2963
3153
  : undefined;
2964
- mode = normalizeMode(input.mode);
3154
+ const compileModePlan = resolveCompileModePlan(input.mode, input.selectionMode);
3155
+ comparativeMode = compileModePlan.comparativeMode;
3156
+ mode = compileModePlan.routeMode;
3157
+ selectionMode = compileModePlan.selectionMode;
3158
+ frozenReplayEvalIdentity = normalizeFrozenReplayEvalIdentity(input._frozenReplayEvalIdentity);
2965
3159
  }
2966
3160
  catch (error) {
2967
3161
  result = failOpenCompileResult(error, fallbackActivationRoot, buildBrainServeHotPathTiming({
@@ -2979,6 +3173,7 @@ export function compileRuntimeContext(input) {
2979
3173
  }
2980
3174
  try {
2981
3175
  const target = resolveActivePackForCompile(activationRoot);
3176
+ validateFrozenReplayEvalIdentity(target, frozenReplayEvalIdentity);
2982
3177
  const resolvedBudget = resolveCompileBudget(target, input);
2983
3178
  routeSelectionStartedAtNs = monotonicClockNs();
2984
3179
  const compile = compileRuntimeFromActivation(activationRoot, {
@@ -2995,11 +3190,29 @@ export function compileRuntimeContext(input) {
2995
3190
  ...(selectionMode !== undefined ? { selectionMode } : {})
2996
3191
  });
2997
3192
  routeSelectionMs = elapsedMsFrom(routeSelectionStartedAtNs);
3193
+ const selectionEngine = selectionMode ?? "flat_rank_v1";
2998
3194
  const compileResponse = {
2999
3195
  ...compile.response,
3000
3196
  diagnostics: {
3001
3197
  ...compile.response.diagnostics,
3002
- notes: uniqueNotes([...compile.response.diagnostics.notes, ...resolvedBudget.notes, "OpenClaw remains the runtime owner"])
3198
+ notes: uniqueNotes([
3199
+ ...compile.response.diagnostics.notes,
3200
+ ...resolvedBudget.notes,
3201
+ `selection_engine=${selectionEngine}`,
3202
+ ...(comparativeMode === null
3203
+ ? []
3204
+ : [
3205
+ `comparative_mode=${comparativeMode}`,
3206
+ `comparative_mode_plan=${mode}+${selectionEngine}`
3207
+ ]),
3208
+ ...(frozenReplayEvalIdentity === null
3209
+ ? []
3210
+ : [
3211
+ `replay_eval_pack_id=${frozenReplayEvalIdentity.packId}`,
3212
+ `replay_eval_router_identity=${frozenReplayEvalIdentity.routerIdentity ?? "null"}`
3213
+ ]),
3214
+ "OpenClaw remains the runtime owner"
3215
+ ])
3003
3216
  }
3004
3217
  };
3005
3218
  promptAssemblyStartedAtNs = monotonicClockNs();
@@ -3882,6 +4095,7 @@ export function runRuntimeTurn(turn, options = {}) {
3882
4095
  ...(turn.mode !== undefined ? { mode: turn.mode } : {}),
3883
4096
  ...(turn.selectionMode !== undefined ? { selectionMode: turn.selectionMode } : {}),
3884
4097
  ...(turn.runtimeHints !== undefined ? { runtimeHints: turn.runtimeHints } : {}),
4098
+ ...(options._frozenReplayEvalIdentity !== undefined ? { _frozenReplayEvalIdentity: options._frozenReplayEvalIdentity } : {}),
3885
4099
  _suppressServeLog: true
3886
4100
  };
3887
4101
  const compileResult = compileRuntimeContext(compileInput);
@@ -4130,6 +4344,7 @@ function ensureRecordedSessionTrace(trace) {
4130
4344
  if (trace.turns.length === 0) {
4131
4345
  throw new Error("recorded session trace requires at least one turn");
4132
4346
  }
4347
+ resolveRecordedSessionReplayEvalTurnCount(trace.turns.length, trace.evalTurnCount, "evalTurnCount");
4133
4348
  for (const [index, cue] of trace.seedCues.entries()) {
4134
4349
  normalizeNonEmptyString(cue.cueId, `seedCues[${index}].cueId`);
4135
4350
  normalizeIsoTimestamp(cue.createdAt, `seedCues[${index}].createdAt`);
@@ -4181,6 +4396,52 @@ function uniqueStringsInOrder(values) {
4181
4396
  }
4182
4397
  return unique;
4183
4398
  }
4399
+ function resolveRecordedSessionReplayEvalTurnCount(turnCount, value, fieldName) {
4400
+ if (turnCount === 0) {
4401
+ return 0;
4402
+ }
4403
+ if (value === undefined) {
4404
+ return 1;
4405
+ }
4406
+ const normalized = normalizeNonNegativeInteger(value, fieldName, value);
4407
+ if (normalized < 1) {
4408
+ throw new Error(`${fieldName} must be at least 1 when turns are present`);
4409
+ }
4410
+ if (normalized > turnCount) {
4411
+ throw new Error(`${fieldName} cannot exceed turns.length (${turnCount})`);
4412
+ }
4413
+ return normalized;
4414
+ }
4415
+ function buildRecordedSessionReplayTurnPlan(fixture) {
4416
+ const evalTurnCount = resolveRecordedSessionReplayEvalTurnCount(fixture.turns.length, fixture.evalTurnCount, "evalTurnCount");
4417
+ const trainTurnCount = Math.max(0, fixture.turns.length - evalTurnCount);
4418
+ return {
4419
+ trainTurns: fixture.turns.slice(0, trainTurnCount),
4420
+ evalTurns: fixture.turns.slice(trainTurnCount),
4421
+ trainTurnCount,
4422
+ evalTurnCount
4423
+ };
4424
+ }
4425
+ function cloneRecordedSessionReplayFrozenEvalIdentity(value) {
4426
+ if (value === null) {
4427
+ return null;
4428
+ }
4429
+ return {
4430
+ packId: value.packId,
4431
+ routerIdentity: value.routerIdentity
4432
+ };
4433
+ }
4434
+ function readRecordedSessionReplayFrozenEvalIdentity(activationRoot) {
4435
+ const inspection = inspectActivationState(activationRoot);
4436
+ const activePointer = inspection.pointers.active;
4437
+ if (inspection.active === null || activePointer === null) {
4438
+ return null;
4439
+ }
4440
+ return {
4441
+ packId: activePointer.packId,
4442
+ routerIdentity: inspection.active.routerIdentity ?? null
4443
+ };
4444
+ }
4184
4445
  function buildRecordedSessionSeedExport(trace) {
4185
4446
  const agentId = normalizeOptionalString(trace.agentId) ?? DEFAULT_AGENT_ID;
4186
4447
  const seedSessionId = `${trace.sessionId}-seed`;
@@ -4292,6 +4553,9 @@ function recordedSessionFixtureBase(trace) {
4292
4553
  revision: trace.workspace.revision,
4293
4554
  ...(trace.workspace.labels !== undefined ? { labels: [...trace.workspace.labels] } : {})
4294
4555
  },
4556
+ ...(trace.evalTurnCount !== undefined
4557
+ ? { evalTurnCount: resolveRecordedSessionReplayEvalTurnCount(trace.turns.length, trace.evalTurnCount, "evalTurnCount") }
4558
+ : {}),
4295
4559
  seedBuiltAt: trace.seedBuiltAt,
4296
4560
  seedActivatedAt: trace.seedActivatedAt,
4297
4561
  seedExport: buildRecordedSessionSeedExport(trace),
@@ -4327,6 +4591,9 @@ function recordedSessionReplayFixtureBase(fixture) {
4327
4591
  revision: fixture.workspace.revision,
4328
4592
  ...(fixture.workspace.labels !== undefined ? { labels: [...fixture.workspace.labels] } : {})
4329
4593
  },
4594
+ ...(fixture.evalTurnCount !== undefined
4595
+ ? { evalTurnCount: resolveRecordedSessionReplayEvalTurnCount(fixture.turns.length, fixture.evalTurnCount, "evalTurnCount") }
4596
+ : {}),
4330
4597
  seedBuiltAt: fixture.seedBuiltAt,
4331
4598
  seedActivatedAt: fixture.seedActivatedAt,
4332
4599
  seedExport: fixture.seedExport,
@@ -4377,7 +4644,49 @@ function buildRecordedSessionTurnObservability(result) {
4377
4644
  freshestCreatedAt: observability.teacherFreshness.freshestCreatedAt ?? freshestSource?.freshestCreatedAt ?? null
4378
4645
  };
4379
4646
  }
4380
- function buildRecordedSessionTurnReport(turnFixture, result, options) {
4647
+ const RECORDED_SESSION_REPLAY_MODE_PLAN = {
4648
+ no_brain: {
4649
+ activationStrategy: "no_brain",
4650
+ runtimeMode: null,
4651
+ routeModeRequested: null,
4652
+ selectionEngine: null,
4653
+ learnedRouting: false
4654
+ },
4655
+ vector_only: {
4656
+ activationStrategy: "seed_pack",
4657
+ runtimeMode: "vector_only",
4658
+ routeModeRequested: "heuristic",
4659
+ selectionEngine: "flat_rank_v1",
4660
+ learnedRouting: false
4661
+ },
4662
+ graph_prior_only: {
4663
+ activationStrategy: "seed_pack",
4664
+ runtimeMode: "graph_prior_only",
4665
+ routeModeRequested: "heuristic",
4666
+ selectionEngine: "graph_walk_v1",
4667
+ learnedRouting: false
4668
+ },
4669
+ learned_route: {
4670
+ activationStrategy: "continuous_learned_loop",
4671
+ runtimeMode: "learned_route",
4672
+ routeModeRequested: "learned",
4673
+ selectionEngine: "graph_walk_v1",
4674
+ learnedRouting: true
4675
+ }
4676
+ };
4677
+ function recordedSessionReplayModePlan(mode) {
4678
+ return RECORDED_SESSION_REPLAY_MODE_PLAN[mode];
4679
+ }
4680
+ function buildRecordedSessionReplayTurnInput(turnFixture, modeRoot, replayMode) {
4681
+ const plan = recordedSessionReplayModePlan(replayMode);
4682
+ return {
4683
+ ...turnFixture.turn,
4684
+ ...(plan.runtimeMode === null ? {} : { mode: plan.runtimeMode }),
4685
+ export: buildRecordedSessionTurnExportRoot(modeRoot, turnFixture.turnId)
4686
+ };
4687
+ }
4688
+ function buildRecordedSessionTurnReport(replayMode, turnFixture, result, options) {
4689
+ const plan = recordedSessionReplayModePlan(replayMode);
4381
4690
  const compileOk = result.ok;
4382
4691
  const selectedContextTexts = compileOk ? result.compileResponse.selectedContext.map((block) => block.text) : [];
4383
4692
  const selectedContextIds = compileOk ? result.compileResponse.selectedContext.map((block) => block.id) : [];
@@ -4390,10 +4699,15 @@ function buildRecordedSessionTurnReport(turnFixture, result, options) {
4390
4699
  const eventExportDigest = result.eventExport.ok === true ? result.eventExport.normalizedEventExport.provenance.exportDigest : null;
4391
4700
  return {
4392
4701
  turnId: turnFixture.turnId,
4702
+ replayMode,
4703
+ phase: options.phase,
4393
4704
  compileOk,
4394
4705
  fallbackToStaticContext: result.fallbackToStaticContext,
4395
4706
  hardRequirementViolated: result.hardRequirementViolated,
4396
4707
  activePackId: result.ok ? result.activePackId : null,
4708
+ modeRequested: plan.routeModeRequested,
4709
+ modeEffective: result.ok ? result.compileResponse.diagnostics.modeEffective : null,
4710
+ selectionEngine: plan.selectionEngine,
4397
4711
  usedLearnedRouteFn: result.ok ? result.compileResponse.diagnostics.usedLearnedRouteFn : false,
4398
4712
  routerIdentity: result.ok ? result.compileResponse.diagnostics.routerIdentity : null,
4399
4713
  selectionDigest: result.ok ? result.compileResponse.diagnostics.selectionDigest : null,
@@ -4447,7 +4761,7 @@ function buildRecordedSessionReplayScannerWarnings(mode, turns, activePackChange
4447
4761
  if (turns.some((turn) => turn.compileOk) && selectionDigestTurnCount === 0) {
4448
4762
  warnings.push("selection_digest_missing");
4449
4763
  }
4450
- if (mode === "learned_replay" && activePackChangeCount === 0) {
4764
+ if (mode === "learned_route" && activePackChangeCount === 0) {
4451
4765
  warnings.push("active_pack_never_moved");
4452
4766
  }
4453
4767
  return warnings;
@@ -4479,17 +4793,33 @@ function buildRecordedSessionReplayScannerEvidence(mode, turns) {
4479
4793
  warnings: buildRecordedSessionReplayScannerWarnings(mode, turns, activePackChangeCount)
4480
4794
  };
4481
4795
  }
4482
- function buildRecordedSessionReplayModeSummary(mode, turns) {
4796
+ function buildRecordedSessionReplayModeQualityScore(turns, compileOkCount, phraseHitCount, phraseCount) {
4797
+ if (turns.length === 0) {
4798
+ return 0;
4799
+ }
4800
+ // Score mode quality from aggregate replay coverage so multi-phrase turns
4801
+ // keep their full weight instead of being flattened into a per-turn average.
4802
+ const compileScore = (compileOkCount / turns.length) * 40;
4803
+ const phraseScore = phraseCount === 0 ? 60 : (phraseHitCount / phraseCount) * 60;
4804
+ return Math.min(100, Math.round(compileScore + phraseScore));
4805
+ }
4806
+ function buildRecordedSessionReplayModeSummary(mode, turns, options = {}) {
4807
+ const plan = recordedSessionReplayModePlan(mode);
4483
4808
  const compileOkCount = turns.filter((turn) => turn.compileOk).length;
4484
4809
  const phraseHitCount = turns.reduce((sum, turn) => sum + turn.phraseHits.length, 0);
4485
4810
  const phraseCount = turns.reduce((sum, turn) => sum + turn.expectedContextPhrases.length, 0);
4486
4811
  const usedLearnedRouteTurnCount = turns.filter((turn) => turn.usedLearnedRouteFn).length;
4487
4812
  const promotionCount = turns.filter((turn) => turn.promoted).length;
4488
- const qualityScore = turns.length === 0 ? 0 : Math.round(turns.reduce((sum, turn) => sum + turn.qualityScore, 0) / turns.length);
4813
+ const qualityScore = buildRecordedSessionReplayModeQualityScore(turns, compileOkCount, phraseHitCount, phraseCount);
4489
4814
  const packIds = uniqueStringsInOrder(turns.map((turn) => turn.activePackId).filter(isPresent));
4815
+ const trainTurnCount = turns.filter((turn) => turn.phase === "train").length;
4816
+ const evalTurnCount = turns.filter((turn) => turn.phase === "eval").length;
4490
4817
  const scannerEvidence = buildRecordedSessionReplayScannerEvidence(mode, turns);
4491
4818
  const base = {
4492
4819
  mode,
4820
+ activationStrategy: plan.activationStrategy,
4821
+ modeRequested: plan.routeModeRequested,
4822
+ selectionEngine: plan.selectionEngine,
4493
4823
  qualityScore,
4494
4824
  compileOkCount,
4495
4825
  phraseHitCount,
@@ -4497,6 +4827,10 @@ function buildRecordedSessionReplayModeSummary(mode, turns) {
4497
4827
  usedLearnedRouteTurnCount,
4498
4828
  promotionCount,
4499
4829
  packIds,
4830
+ trainTurnCount,
4831
+ evalTurnCount,
4832
+ frozenEvalPackId: options.frozenEvalIdentity?.packId ?? null,
4833
+ frozenEvalRouterIdentity: options.frozenEvalIdentity?.routerIdentity ?? null,
4500
4834
  scannerEvidence
4501
4835
  };
4502
4836
  return {
@@ -4505,10 +4839,15 @@ function buildRecordedSessionReplayModeSummary(mode, turns) {
4505
4839
  summary: base,
4506
4840
  turns: turns.map((turn) => ({
4507
4841
  turnId: turn.turnId,
4842
+ phase: turn.phase,
4508
4843
  qualityScore: turn.qualityScore,
4509
4844
  phraseHits: turn.phraseHits,
4510
4845
  missedPhrases: turn.missedPhrases,
4511
4846
  compileOk: turn.compileOk,
4847
+ replayMode: turn.replayMode,
4848
+ modeRequested: turn.modeRequested,
4849
+ modeEffective: turn.modeEffective,
4850
+ selectionEngine: turn.selectionEngine,
4512
4851
  usedLearnedRouteFn: turn.usedLearnedRouteFn,
4513
4852
  activePackId: turn.activePackId,
4514
4853
  selectionDigest: turn.selectionDigest,
@@ -4518,16 +4857,19 @@ function buildRecordedSessionReplayModeSummary(mode, turns) {
4518
4857
  })
4519
4858
  };
4520
4859
  }
4521
- function buildRecordedSessionReplayModeReport(mode, turns) {
4860
+ function buildRecordedSessionReplayModeReport(mode, turns, options = {}) {
4522
4861
  return {
4523
4862
  mode,
4524
- summary: buildRecordedSessionReplayModeSummary(mode, turns),
4863
+ summary: buildRecordedSessionReplayModeSummary(mode, turns, options),
4525
4864
  turns: [...turns]
4526
4865
  };
4527
4866
  }
4528
4867
  function buildRecordedSessionReplayScoreHash(modes) {
4529
4868
  return checksumJsonPayload(modes.map((mode) => ({
4530
4869
  mode: mode.mode,
4870
+ activationStrategy: mode.summary.activationStrategy,
4871
+ modeRequested: mode.summary.modeRequested,
4872
+ selectionEngine: mode.summary.selectionEngine,
4531
4873
  qualityScore: mode.summary.qualityScore,
4532
4874
  compileOkCount: mode.summary.compileOkCount,
4533
4875
  phraseHitCount: mode.summary.phraseHitCount,
@@ -4535,6 +4877,10 @@ function buildRecordedSessionReplayScoreHash(modes) {
4535
4877
  usedLearnedRouteTurnCount: mode.summary.usedLearnedRouteTurnCount,
4536
4878
  promotionCount: mode.summary.promotionCount,
4537
4879
  packIds: mode.summary.packIds,
4880
+ trainTurnCount: mode.summary.trainTurnCount,
4881
+ evalTurnCount: mode.summary.evalTurnCount,
4882
+ frozenEvalPackId: mode.summary.frozenEvalPackId,
4883
+ frozenEvalRouterIdentity: mode.summary.frozenEvalRouterIdentity,
4538
4884
  scannerEvidence: mode.summary.scannerEvidence,
4539
4885
  scoreHash: mode.summary.scoreHash
4540
4886
  })));
@@ -4625,45 +4971,42 @@ function runRecordedSessionNoBrainMode(rootDir, fixture) {
4625
4971
  const modeRoot = prepareReplayModeRoot(rootDir, "no_brain");
4626
4972
  const activationRoot = path.join(modeRoot, "activation");
4627
4973
  const turns = fixture.turns.map((turnFixture) => {
4628
- const result = runRuntimeTurn({
4629
- ...turnFixture.turn,
4630
- export: buildRecordedSessionTurnExportRoot(modeRoot, turnFixture.turnId)
4631
- }, {
4974
+ const result = runRuntimeTurn(buildRecordedSessionReplayTurnInput(turnFixture, modeRoot, "no_brain"), {
4632
4975
  activationRoot,
4633
4976
  failOpen: true
4634
4977
  });
4635
- return buildRecordedSessionTurnReport(turnFixture, result, {
4978
+ return buildRecordedSessionTurnReport("no_brain", turnFixture, result, {
4979
+ phase: "eval",
4636
4980
  compileActiveVersion: null,
4637
4981
  promoted: false
4638
4982
  });
4639
4983
  });
4640
4984
  return buildRecordedSessionReplayModeReport("no_brain", turns);
4641
4985
  }
4642
- function runRecordedSessionSeedPackMode(rootDir, fixture) {
4643
- const modeRoot = prepareReplayModeRoot(rootDir, "seed_pack");
4986
+ function runRecordedSessionSeededComparativeMode(rootDir, fixture, replayMode) {
4987
+ const modeRoot = prepareReplayModeRoot(rootDir, replayMode);
4644
4988
  const { activationRoot } = prepareSeedActivation(modeRoot, fixture);
4645
4989
  const turns = fixture.turns.map((turnFixture) => {
4646
- const result = runRuntimeTurn({
4647
- ...turnFixture.turn,
4648
- export: buildRecordedSessionTurnExportRoot(modeRoot, turnFixture.turnId)
4649
- }, {
4990
+ const result = runRuntimeTurn(buildRecordedSessionReplayTurnInput(turnFixture, modeRoot, replayMode), {
4650
4991
  activationRoot,
4651
4992
  failOpen: false
4652
4993
  });
4653
- return buildRecordedSessionTurnReport(turnFixture, result, {
4994
+ return buildRecordedSessionTurnReport(replayMode, turnFixture, result, {
4995
+ phase: "eval",
4654
4996
  compileActiveVersion: 1,
4655
4997
  promoted: false
4656
4998
  });
4657
4999
  });
4658
- return buildRecordedSessionReplayModeReport("seed_pack", turns);
5000
+ return buildRecordedSessionReplayModeReport(replayMode, turns);
4659
5001
  }
4660
- function runRecordedSessionLearnedReplayMode(rootDir, fixture) {
4661
- const modeRoot = prepareReplayModeRoot(rootDir, "learned_replay");
5002
+ function runRecordedSessionLearnedRouteMode(rootDir, fixture) {
5003
+ const modeRoot = prepareReplayModeRoot(rootDir, "learned_route");
4662
5004
  const { activationRoot } = prepareSeedActivation(modeRoot, fixture);
4663
5005
  const loopRoot = path.join(modeRoot, "loop");
5006
+ const turnPlan = buildRecordedSessionReplayTurnPlan(fixture);
4664
5007
  let state;
4665
5008
  const turns = [];
4666
- for (const turnFixture of fixture.turns) {
5009
+ for (const turnFixture of turnPlan.trainTurns) {
4667
5010
  const compileCreatedAt = normalizeIsoTimestamp(turnFixture.turn.compile?.createdAt, "turn.compile.createdAt", turnFixture.turn.createdAt);
4668
5011
  const result = runContinuousProductLoopTurn({
4669
5012
  activationRoot,
@@ -4682,12 +5025,40 @@ function runRecordedSessionLearnedReplayMode(rootDir, fixture) {
4682
5025
  promoteUpdatedAt: addMinutes(compileCreatedAt, 4)
4683
5026
  });
4684
5027
  state = result.state;
4685
- turns.push(buildRecordedSessionTurnReport(turnFixture, result.turn, {
5028
+ turns.push(buildRecordedSessionTurnReport("learned_route", turnFixture, result.turn, {
5029
+ phase: "train",
4686
5030
  compileActiveVersion: result.compileActiveVersion,
4687
5031
  promoted: result.learning.promoted
4688
5032
  }));
4689
5033
  }
4690
- return buildRecordedSessionReplayModeReport("learned_replay", turns);
5034
+ const frozenEvalIdentity = readRecordedSessionReplayFrozenEvalIdentity(activationRoot);
5035
+ for (const turnFixture of turnPlan.evalTurns) {
5036
+ const currentState = cloneContinuousProductLoopState(state ??
5037
+ createContinuousProductLoopState({
5038
+ activationRoot,
5039
+ loopRoot
5040
+ }));
5041
+ currentState.activationRoot = activationRoot;
5042
+ currentState.loopRoot = loopRoot;
5043
+ const activeBeforeTurn = syncContinuousActivePack(currentState);
5044
+ state = cloneContinuousProductLoopState(currentState);
5045
+ const result = runRuntimeTurn({
5046
+ ...turnFixture.turn,
5047
+ export: buildRecordedSessionTurnExportRoot(modeRoot, turnFixture.turnId)
5048
+ }, {
5049
+ activationRoot,
5050
+ failOpen: false,
5051
+ ...(frozenEvalIdentity === null ? {} : { _frozenReplayEvalIdentity: frozenEvalIdentity })
5052
+ });
5053
+ turns.push(buildRecordedSessionTurnReport("learned_route", turnFixture, result, {
5054
+ phase: "eval",
5055
+ compileActiveVersion: activeBeforeTurn?.version ?? 0,
5056
+ promoted: false
5057
+ }));
5058
+ }
5059
+ return buildRecordedSessionReplayModeReport("learned_route", turns, {
5060
+ frozenEvalIdentity: cloneRecordedSessionReplayFrozenEvalIdentity(frozenEvalIdentity)
5061
+ });
4691
5062
  }
4692
5063
  export function runRecordedSessionReplay(rootDir, fixture) {
4693
5064
  const resolvedRoot = path.resolve(normalizeNonEmptyString(rootDir, "rootDir"));
@@ -4701,8 +5072,9 @@ export function runRecordedSessionReplay(rootDir, fixture) {
4701
5072
  }
4702
5073
  const modes = [
4703
5074
  runRecordedSessionNoBrainMode(resolvedRoot, fixture),
4704
- runRecordedSessionSeedPackMode(resolvedRoot, fixture),
4705
- runRecordedSessionLearnedReplayMode(resolvedRoot, fixture)
5075
+ runRecordedSessionSeededComparativeMode(resolvedRoot, fixture, "vector_only"),
5076
+ runRecordedSessionSeededComparativeMode(resolvedRoot, fixture, "graph_prior_only"),
5077
+ runRecordedSessionLearnedRouteMode(resolvedRoot, fixture)
4706
5078
  ];
4707
5079
  const ranking = modes
4708
5080
  .map((mode) => ({
@@ -4754,7 +5126,14 @@ function rescoreRecordedSessionReplayTurn(turn) {
4754
5126
  }
4755
5127
  function rescoreRecordedSessionReplayMode(mode) {
4756
5128
  const turns = mode.turns.map((turn) => rescoreRecordedSessionReplayTurn(turn));
4757
- return buildRecordedSessionReplayModeReport(mode.mode, turns);
5129
+ return buildRecordedSessionReplayModeReport(mode.mode, turns, {
5130
+ frozenEvalIdentity: mode.summary.frozenEvalPackId === null
5131
+ ? null
5132
+ : {
5133
+ packId: mode.summary.frozenEvalPackId,
5134
+ routerIdentity: mode.summary.frozenEvalRouterIdentity ?? null
5135
+ }
5136
+ });
4758
5137
  }
4759
5138
  export function rescoreRecordedSessionReplayBundle(bundle) {
4760
5139
  const modes = bundle.modes.map((mode) => rescoreRecordedSessionReplayMode(mode));
@@ -4775,6 +5154,558 @@ export function verifyRecordedSessionReplayBundleHashes(bundle) {
4775
5154
  scoreHashMatches: rescored.scoreHash === bundle.scoreHash
4776
5155
  };
4777
5156
  }
5157
+ function buildRecordedSessionReplayProofEnvironment() {
5158
+ return {
5159
+ contract: RECORDED_SESSION_REPLAY_PROOF_ENVIRONMENT_CONTRACT,
5160
+ runtimeOwner: "openclaw",
5161
+ generator: {
5162
+ packageName: "@openclawbrain/cli",
5163
+ entrypoint: "writeRecordedSessionReplayProofBundle",
5164
+ nodeVersion: process.version,
5165
+ platform: process.platform,
5166
+ arch: process.arch
5167
+ },
5168
+ determinism: {
5169
+ hashAlgorithm: "sha256",
5170
+ canonicalJson: true,
5171
+ modeOrder: [...RECORDED_SESSION_REPLAY_MODE_ORDER],
5172
+ scratchReplayRoot: "temporary_directory"
5173
+ }
5174
+ };
5175
+ }
5176
+ function toRecordedSessionReplayProofRate(numerator, denominator) {
5177
+ return denominator > 0 ? Number((numerator / denominator).toFixed(6)) : null;
5178
+ }
5179
+ function buildRecordedSessionReplayProofSummaryTables(bundle) {
5180
+ const orderedModes = orderedRecordedSessionReplayModes(bundle.modes);
5181
+ return {
5182
+ contract: RECORDED_SESSION_REPLAY_PROOF_SUMMARY_TABLES_CONTRACT,
5183
+ traceId: bundle.traceId,
5184
+ winnerMode: bundle.summary.winnerMode,
5185
+ ranking: bundle.summary.ranking.map((entry) => ({ ...entry })),
5186
+ modes: orderedModes.map((mode) => ({
5187
+ mode: mode.mode,
5188
+ turnCount: mode.turns.length,
5189
+ qualityScore: mode.summary.qualityScore,
5190
+ compileOkCount: mode.summary.compileOkCount,
5191
+ phraseHitCount: mode.summary.phraseHitCount,
5192
+ phraseCount: mode.summary.phraseCount,
5193
+ usedLearnedRouteTurnCount: mode.summary.usedLearnedRouteTurnCount,
5194
+ promotionCount: mode.summary.promotionCount,
5195
+ exportTurnCount: mode.summary.scannerEvidence.exportTurnCount,
5196
+ humanLabelCount: mode.summary.scannerEvidence.humanLabelCount,
5197
+ attributedTurnCount: mode.summary.scannerEvidence.attributedTurnCount,
5198
+ activePackChangeCount: mode.summary.scannerEvidence.activePackChangeCount,
5199
+ warningCount: mode.summary.scannerEvidence.warnings.length,
5200
+ scoreHash: mode.summary.scoreHash
5201
+ })),
5202
+ turns: orderedModes.flatMap((mode) => mode.turns.map((turn) => ({
5203
+ mode: mode.mode,
5204
+ turnId: turn.turnId,
5205
+ qualityScore: turn.qualityScore,
5206
+ compileOk: turn.compileOk,
5207
+ phraseHitCount: turn.phraseHits.length,
5208
+ phraseCount: turn.expectedContextPhrases.length,
5209
+ usedLearnedRouteFn: turn.usedLearnedRouteFn,
5210
+ promoted: turn.promoted,
5211
+ activePackId: turn.activePackId,
5212
+ selectionDigest: turn.selectionDigest,
5213
+ eventExportDigest: turn.eventExportDigest,
5214
+ warningCount: turn.warnings.length
5215
+ })))
5216
+ };
5217
+ }
5218
+ function buildRecordedSessionReplayProofCoverageSnapshot(bundle) {
5219
+ const orderedModes = orderedRecordedSessionReplayModes(bundle.modes);
5220
+ const totalTurns = orderedModes.reduce((sum, mode) => sum + mode.turns.length, 0);
5221
+ const compileOkTurnCount = orderedModes.reduce((sum, mode) => sum + mode.summary.compileOkCount, 0);
5222
+ const phraseHitCount = orderedModes.reduce((sum, mode) => sum + mode.summary.phraseHitCount, 0);
5223
+ const phraseCount = orderedModes.reduce((sum, mode) => sum + mode.summary.phraseCount, 0);
5224
+ return {
5225
+ contract: RECORDED_SESSION_REPLAY_PROOF_COVERAGE_SNAPSHOT_CONTRACT,
5226
+ traceId: bundle.traceId,
5227
+ winnerMode: bundle.summary.winnerMode,
5228
+ totalTurns,
5229
+ compileOkTurnCount,
5230
+ compileOkRate: toRecordedSessionReplayProofRate(compileOkTurnCount, totalTurns),
5231
+ phraseHitCount,
5232
+ phraseCount,
5233
+ phraseHitRate: toRecordedSessionReplayProofRate(phraseHitCount, phraseCount),
5234
+ modes: orderedModes.map((mode) => ({
5235
+ mode: mode.mode,
5236
+ turnCount: mode.turns.length,
5237
+ compileOkRate: toRecordedSessionReplayProofRate(mode.summary.compileOkCount, mode.turns.length),
5238
+ phraseHitRate: toRecordedSessionReplayProofRate(mode.summary.phraseHitCount, mode.summary.phraseCount),
5239
+ learnedRouteTurnRate: toRecordedSessionReplayProofRate(mode.summary.usedLearnedRouteTurnCount, mode.turns.length),
5240
+ attributedTurnRate: toRecordedSessionReplayProofRate(mode.summary.scannerEvidence.attributedTurnCount, mode.turns.length)
5241
+ }))
5242
+ };
5243
+ }
5244
+ function buildRecordedSessionReplayProofHardeningSnapshot(bundle) {
5245
+ const orderedModes = orderedRecordedSessionReplayModes(bundle.modes);
5246
+ const totalTurns = orderedModes.reduce((sum, mode) => sum + mode.turns.length, 0);
5247
+ const compileFailureCount = orderedModes.reduce((sum, mode) => sum + (mode.turns.length - mode.summary.compileOkCount), 0);
5248
+ const warningCount = orderedModes.reduce((sum, mode) => sum + mode.summary.scannerEvidence.warnings.length, 0);
5249
+ const promotionCount = orderedModes.reduce((sum, mode) => sum + mode.summary.promotionCount, 0);
5250
+ const exportTurnCount = orderedModes.reduce((sum, mode) => sum + mode.summary.scannerEvidence.exportTurnCount, 0);
5251
+ const attributedTurnCount = orderedModes.reduce((sum, mode) => sum + mode.summary.scannerEvidence.attributedTurnCount, 0);
5252
+ return {
5253
+ contract: RECORDED_SESSION_REPLAY_PROOF_HARDENING_SNAPSHOT_CONTRACT,
5254
+ traceId: bundle.traceId,
5255
+ totalTurns,
5256
+ compileFailureCount,
5257
+ compileFailureRate: toRecordedSessionReplayProofRate(compileFailureCount, totalTurns),
5258
+ warningCount,
5259
+ promotionCount,
5260
+ exportTurnCount,
5261
+ attributedTurnCount,
5262
+ modes: orderedModes.map((mode) => ({
5263
+ mode: mode.mode,
5264
+ warningCount: mode.summary.scannerEvidence.warnings.length,
5265
+ compileFailureCount: mode.turns.length - mode.summary.compileOkCount,
5266
+ promotionCount: mode.summary.promotionCount,
5267
+ exportTurnCount: mode.summary.scannerEvidence.exportTurnCount,
5268
+ attributedTurnCount: mode.summary.scannerEvidence.attributedTurnCount
5269
+ }))
5270
+ };
5271
+ }
5272
+ function buildRecordedSessionReplayProofSummary(input) {
5273
+ const rankingRows = input.summaryTables.ranking.map((entry, index) => `| ${index + 1} | ${entry.mode} | ${entry.qualityScore} |`);
5274
+ const modeRows = input.summaryTables.modes.map((mode) => `| ${mode.mode} | ${mode.turnCount} | ${mode.compileOkCount} | ${mode.phraseHitCount}/${mode.phraseCount} | ${mode.usedLearnedRouteTurnCount} | ${mode.promotionCount} | ${mode.exportTurnCount} | ${mode.humanLabelCount} | ${mode.warningCount} | ${mode.scoreHash} |`);
5275
+ const turnRows = input.summaryTables.turns.map((turn) => `| ${turn.mode} | ${turn.turnId} | ${turn.qualityScore} | ${turn.compileOk ? "yes" : "no"} | ${turn.phraseHitCount}/${turn.phraseCount} | ${turn.usedLearnedRouteFn ? "yes" : "no"} | ${turn.promoted ? "yes" : "no"} | ${turn.activePackId ?? "none"} | ${turn.selectionDigest ?? "none"} |`);
5276
+ const coverageRows = input.coverageSnapshot.modes.map((mode) => `| ${mode.mode} | ${mode.turnCount} | ${mode.compileOkRate ?? "none"} | ${mode.phraseHitRate ?? "none"} | ${mode.learnedRouteTurnRate ?? "none"} | ${mode.attributedTurnRate ?? "none"} |`);
5277
+ const hardeningRows = input.hardeningSnapshot.modes.map((mode) => `| ${mode.mode} | ${mode.warningCount} | ${mode.compileFailureCount} | ${mode.promotionCount} | ${mode.exportTurnCount} | ${mode.attributedTurnCount} |`);
5278
+ return [
5279
+ "# Recorded Session Replay Proof Bundle",
5280
+ "",
5281
+ `- trace id: \`${input.bundle.traceId}\``,
5282
+ `- winner mode: \`${input.bundle.summary.winnerMode ?? "none"}\``,
5283
+ `- trace hash: \`${input.semanticHashes.traceHash}\``,
5284
+ `- fixture hash: \`${input.semanticHashes.fixtureHash}\``,
5285
+ `- score hash: \`${input.semanticHashes.scoreHash}\``,
5286
+ `- bundle hash: \`${input.semanticHashes.bundleHash}\``,
5287
+ "",
5288
+ "## Ranking",
5289
+ "| rank | mode | quality score |",
5290
+ "| --- | --- | ---: |",
5291
+ ...(rankingRows.length === 0 ? ["| - | none | 0 |"] : rankingRows),
5292
+ "",
5293
+ "## Coverage Snapshot",
5294
+ `- compile ok turns: ${input.coverageSnapshot.compileOkTurnCount}/${input.coverageSnapshot.totalTurns}`,
5295
+ `- compile ok rate: ${input.coverageSnapshot.compileOkRate ?? "none"}`,
5296
+ `- phrase hits: ${input.coverageSnapshot.phraseHitCount}/${input.coverageSnapshot.phraseCount}`,
5297
+ `- phrase hit rate: ${input.coverageSnapshot.phraseHitRate ?? "none"}`,
5298
+ "",
5299
+ "| mode | turns | compile ok rate | phrase hit rate | learned route turn rate | attributed turn rate |",
5300
+ "| --- | ---: | ---: | ---: | ---: | ---: |",
5301
+ ...(coverageRows.length === 0 ? ["| - | 0 | none | none | none | none |"] : coverageRows),
5302
+ "",
5303
+ "## Hardening Snapshot",
5304
+ `- compile failures: ${input.hardeningSnapshot.compileFailureCount}/${input.hardeningSnapshot.totalTurns}`,
5305
+ `- compile failure rate: ${input.hardeningSnapshot.compileFailureRate ?? "none"}`,
5306
+ `- warnings: ${input.hardeningSnapshot.warningCount}`,
5307
+ `- promotions: ${input.hardeningSnapshot.promotionCount}`,
5308
+ "",
5309
+ "| mode | warnings | compile failures | promotions | export turns | attributed turns |",
5310
+ "| --- | ---: | ---: | ---: | ---: | ---: |",
5311
+ ...(hardeningRows.length === 0 ? ["| - | 0 | 0 | 0 | 0 | 0 |"] : hardeningRows),
5312
+ "",
5313
+ "## Mode Table",
5314
+ "| mode | turns | compile ok | phrase hits | learned route turns | promotions | export turns | human labels | warnings | score hash |",
5315
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |",
5316
+ ...(modeRows.length === 0 ? ["| - | 0 | 0 | 0/0 | 0 | 0 | 0 | 0 | 0 | none |"] : modeRows),
5317
+ "",
5318
+ "## Turn Table",
5319
+ "| mode | turn | quality | compile ok | phrase hits | learned route | promoted | active pack | selection digest |",
5320
+ "| --- | --- | ---: | --- | ---: | --- | --- | --- | --- |",
5321
+ ...(turnRows.length === 0 ? ["| - | none | 0 | no | 0/0 | no | no | none | none |"] : turnRows),
5322
+ ""
5323
+ ].join("\n");
5324
+ }
5325
+ function buildRecordedSessionReplayProofManifest(bundle) {
5326
+ return {
5327
+ contract: RECORDED_SESSION_REPLAY_PROOF_MANIFEST_CONTRACT,
5328
+ traceId: bundle.traceId,
5329
+ source: bundle.source,
5330
+ recordedAt: bundle.recordedAt,
5331
+ generatedAt: bundle.generatedAt,
5332
+ hashAlgorithm: "sha256",
5333
+ modeOrder: [...RECORDED_SESSION_REPLAY_MODE_ORDER],
5334
+ contracts: {
5335
+ trace: RECORDED_SESSION_TRACE_CONTRACT,
5336
+ fixture: RECORDED_SESSION_FIXTURE_CONTRACT,
5337
+ bundle: RECORDED_SESSION_BUNDLE_CONTRACT,
5338
+ environment: RECORDED_SESSION_REPLAY_PROOF_ENVIRONMENT_CONTRACT,
5339
+ summaryTables: RECORDED_SESSION_REPLAY_PROOF_SUMMARY_TABLES_CONTRACT,
5340
+ coverageSnapshot: RECORDED_SESSION_REPLAY_PROOF_COVERAGE_SNAPSHOT_CONTRACT,
5341
+ hardeningSnapshot: RECORDED_SESSION_REPLAY_PROOF_HARDENING_SNAPSHOT_CONTRACT,
5342
+ hashes: RECORDED_SESSION_REPLAY_PROOF_HASHES_CONTRACT
5343
+ },
5344
+ hashes: {
5345
+ traceHash: bundle.traceHash,
5346
+ fixtureHash: bundle.fixtureHash,
5347
+ scoreHash: bundle.scoreHash,
5348
+ bundleHash: bundle.bundleHash
5349
+ },
5350
+ files: {
5351
+ trace: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.trace,
5352
+ fixture: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.fixture,
5353
+ bundle: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.bundle,
5354
+ environment: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.environment,
5355
+ summary: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.summary,
5356
+ summaryTables: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.summaryTables,
5357
+ coverageSnapshot: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.coverageSnapshot,
5358
+ hardeningSnapshot: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.hardeningSnapshot,
5359
+ hashes: RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.hashes,
5360
+ modes: RECORDED_SESSION_REPLAY_MODE_ORDER.map((mode) => ({
5361
+ mode,
5362
+ path: recordedSessionReplayModeOutputPath(mode)
5363
+ }))
5364
+ }
5365
+ };
5366
+ }
5367
+ function validateRecordedSessionReplayProofManifest(manifest) {
5368
+ const errors = [];
5369
+ if (manifest.contract !== RECORDED_SESSION_REPLAY_PROOF_MANIFEST_CONTRACT) {
5370
+ errors.push("manifest contract is invalid");
5371
+ }
5372
+ if (canonicalJson(manifest.modeOrder ?? []) !== canonicalJson(RECORDED_SESSION_REPLAY_MODE_ORDER)) {
5373
+ errors.push("manifest modeOrder must stay fixed at no_brain, vector_only, graph_prior_only, learned_route");
5374
+ }
5375
+ if (manifest.hashAlgorithm !== "sha256") {
5376
+ errors.push("manifest hashAlgorithm must be sha256");
5377
+ }
5378
+ if (manifest.contracts?.trace !== RECORDED_SESSION_TRACE_CONTRACT ||
5379
+ manifest.contracts?.fixture !== RECORDED_SESSION_FIXTURE_CONTRACT ||
5380
+ manifest.contracts?.bundle !== RECORDED_SESSION_BUNDLE_CONTRACT ||
5381
+ manifest.contracts?.environment !== RECORDED_SESSION_REPLAY_PROOF_ENVIRONMENT_CONTRACT ||
5382
+ manifest.contracts?.summaryTables !== RECORDED_SESSION_REPLAY_PROOF_SUMMARY_TABLES_CONTRACT ||
5383
+ manifest.contracts?.coverageSnapshot !== RECORDED_SESSION_REPLAY_PROOF_COVERAGE_SNAPSHOT_CONTRACT ||
5384
+ manifest.contracts?.hardeningSnapshot !== RECORDED_SESSION_REPLAY_PROOF_HARDENING_SNAPSHOT_CONTRACT ||
5385
+ manifest.contracts?.hashes !== RECORDED_SESSION_REPLAY_PROOF_HASHES_CONTRACT) {
5386
+ errors.push("manifest contracts block is invalid");
5387
+ }
5388
+ if (manifest.files?.trace !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.trace ||
5389
+ manifest.files?.fixture !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.fixture ||
5390
+ manifest.files?.bundle !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.bundle ||
5391
+ manifest.files?.environment !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.environment ||
5392
+ manifest.files?.summary !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.summary ||
5393
+ manifest.files?.summaryTables !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.summaryTables ||
5394
+ manifest.files?.coverageSnapshot !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.coverageSnapshot ||
5395
+ manifest.files?.hardeningSnapshot !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.hardeningSnapshot ||
5396
+ manifest.files?.hashes !== RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.hashes) {
5397
+ errors.push("manifest top-level file layout is invalid");
5398
+ }
5399
+ const expectedModeFiles = RECORDED_SESSION_REPLAY_MODE_ORDER.map((mode) => ({
5400
+ mode,
5401
+ path: recordedSessionReplayModeOutputPath(mode)
5402
+ }));
5403
+ if (canonicalJson(manifest.files?.modes ?? []) !== canonicalJson(expectedModeFiles)) {
5404
+ errors.push("manifest mode outputs must stay under modes/<mode>.json in canonical order");
5405
+ }
5406
+ return errors;
5407
+ }
5408
+ function validateRecordedSessionReplayProofEnvironment(environment) {
5409
+ const errors = [];
5410
+ if (environment.contract !== RECORDED_SESSION_REPLAY_PROOF_ENVIRONMENT_CONTRACT) {
5411
+ errors.push("environment contract is invalid");
5412
+ }
5413
+ if (environment.runtimeOwner !== "openclaw") {
5414
+ errors.push("environment runtimeOwner must be openclaw");
5415
+ }
5416
+ if (environment.generator?.packageName !== "@openclawbrain/cli" ||
5417
+ environment.generator?.entrypoint !== "writeRecordedSessionReplayProofBundle") {
5418
+ errors.push("environment generator metadata is invalid");
5419
+ }
5420
+ if (normalizeOptionalString(environment.generator?.nodeVersion) === undefined ||
5421
+ normalizeOptionalString(environment.generator?.platform) === undefined ||
5422
+ normalizeOptionalString(environment.generator?.arch) === undefined) {
5423
+ errors.push("environment generator runtime fields are required");
5424
+ }
5425
+ if (environment.determinism?.hashAlgorithm !== "sha256" ||
5426
+ environment.determinism?.canonicalJson !== true ||
5427
+ canonicalJson(environment.determinism?.modeOrder ?? []) !== canonicalJson(RECORDED_SESSION_REPLAY_MODE_ORDER) ||
5428
+ environment.determinism?.scratchReplayRoot !== "temporary_directory") {
5429
+ errors.push("environment determinism block is invalid");
5430
+ }
5431
+ return errors;
5432
+ }
5433
+ function buildRecordedSessionReplayProofFileDigestEntries(rootDir, manifest) {
5434
+ const relativePaths = [
5435
+ manifest.files.trace,
5436
+ manifest.files.fixture,
5437
+ manifest.files.bundle,
5438
+ manifest.files.environment,
5439
+ manifest.files.summary,
5440
+ manifest.files.summaryTables,
5441
+ manifest.files.coverageSnapshot,
5442
+ manifest.files.hardeningSnapshot,
5443
+ manifest.files.modes.map((entry) => entry.path),
5444
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.manifest
5445
+ ].flat();
5446
+ return relativePaths.map((relativePath, index) => ({
5447
+ path: relativePath,
5448
+ digest: checksumTextPayload(readRecordedSessionReplayProofText(rootDir, relativePath, `files[${index}]`))
5449
+ }));
5450
+ }
5451
+ function buildRecordedSessionReplayProofHashes(rootDir, manifest) {
5452
+ return {
5453
+ contract: RECORDED_SESSION_REPLAY_PROOF_HASHES_CONTRACT,
5454
+ algorithm: "sha256",
5455
+ semantic: {
5456
+ traceHash: manifest.hashes.traceHash,
5457
+ fixtureHash: manifest.hashes.fixtureHash,
5458
+ scoreHash: manifest.hashes.scoreHash,
5459
+ bundleHash: manifest.hashes.bundleHash
5460
+ },
5461
+ files: buildRecordedSessionReplayProofFileDigestEntries(rootDir, manifest)
5462
+ };
5463
+ }
5464
+ export function loadRecordedSessionReplayProofBundle(rootDir) {
5465
+ const resolvedRoot = path.resolve(rootDir);
5466
+ const manifestPath = path.join(resolvedRoot, RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.manifest);
5467
+ const manifest = readJsonFile(manifestPath);
5468
+ const tracePath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.trace, "manifest.files.trace");
5469
+ const fixturePath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.fixture, "manifest.files.fixture");
5470
+ const bundlePath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.bundle, "manifest.files.bundle");
5471
+ const environmentPath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.environment, "manifest.files.environment");
5472
+ const summaryPath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.summary, "manifest.files.summary");
5473
+ const summaryTablesPath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.summaryTables, "manifest.files.summaryTables");
5474
+ const coverageSnapshotPath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.coverageSnapshot, "manifest.files.coverageSnapshot");
5475
+ const hardeningSnapshotPath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.hardeningSnapshot, "manifest.files.hardeningSnapshot");
5476
+ const hashesPath = resolveRecordedSessionReplayProofPath(resolvedRoot, manifest.files.hashes, "manifest.files.hashes");
5477
+ const modeOutputs = (manifest.files.modes ?? []).map((entry, index) => {
5478
+ const outputPath = resolveRecordedSessionReplayProofPath(resolvedRoot, entry.path, `manifest.files.modes[${index}].path`);
5479
+ return {
5480
+ mode: entry.mode,
5481
+ path: outputPath,
5482
+ report: readJsonFile(outputPath)
5483
+ };
5484
+ });
5485
+ return {
5486
+ rootDir: resolvedRoot,
5487
+ manifestPath,
5488
+ tracePath,
5489
+ fixturePath,
5490
+ bundlePath,
5491
+ environmentPath,
5492
+ summaryPath,
5493
+ summaryTablesPath,
5494
+ coverageSnapshotPath,
5495
+ hardeningSnapshotPath,
5496
+ hashesPath,
5497
+ manifest,
5498
+ trace: readJsonFile(tracePath),
5499
+ fixture: readJsonFile(fixturePath),
5500
+ bundle: readJsonFile(bundlePath),
5501
+ environment: readJsonFile(environmentPath),
5502
+ summaryText: readFileSync(summaryPath, "utf8"),
5503
+ summaryTables: readJsonFile(summaryTablesPath),
5504
+ coverageSnapshot: readJsonFile(coverageSnapshotPath),
5505
+ hardeningSnapshot: readJsonFile(hardeningSnapshotPath),
5506
+ hashes: readJsonFile(hashesPath),
5507
+ modeOutputs
5508
+ };
5509
+ }
5510
+ export function validateRecordedSessionReplayProofBundle(rootDir) {
5511
+ const resolvedRoot = path.resolve(rootDir);
5512
+ try {
5513
+ const descriptor = loadRecordedSessionReplayProofBundle(resolvedRoot);
5514
+ const errors = [];
5515
+ errors.push(...validateRecordedSessionReplayProofManifest(descriptor.manifest));
5516
+ errors.push(...validateRecordedSessionReplayProofEnvironment(descriptor.environment));
5517
+ if (descriptor.trace.contract !== RECORDED_SESSION_TRACE_CONTRACT) {
5518
+ errors.push("trace.json contract is invalid");
5519
+ }
5520
+ if (descriptor.fixture.contract !== RECORDED_SESSION_FIXTURE_CONTRACT) {
5521
+ errors.push("fixture.json contract is invalid");
5522
+ }
5523
+ if (descriptor.bundle.contract !== RECORDED_SESSION_BUNDLE_CONTRACT) {
5524
+ errors.push("bundle.json contract is invalid");
5525
+ }
5526
+ if (descriptor.summaryTables.contract !== RECORDED_SESSION_REPLAY_PROOF_SUMMARY_TABLES_CONTRACT) {
5527
+ errors.push("summary-tables.json contract is invalid");
5528
+ }
5529
+ if (descriptor.coverageSnapshot.contract !== RECORDED_SESSION_REPLAY_PROOF_COVERAGE_SNAPSHOT_CONTRACT) {
5530
+ errors.push("coverage-snapshot.json contract is invalid");
5531
+ }
5532
+ if (descriptor.hardeningSnapshot.contract !== RECORDED_SESSION_REPLAY_PROOF_HARDENING_SNAPSHOT_CONTRACT) {
5533
+ errors.push("hardening-snapshot.json contract is invalid");
5534
+ }
5535
+ if (descriptor.hashes.contract !== RECORDED_SESSION_REPLAY_PROOF_HASHES_CONTRACT) {
5536
+ errors.push("hashes.json contract is invalid");
5537
+ }
5538
+ if (descriptor.hashes.algorithm !== "sha256") {
5539
+ errors.push("hashes.json algorithm must be sha256");
5540
+ }
5541
+ const rebuiltFixture = buildRecordedSessionReplayFixture(structuredClone(descriptor.trace));
5542
+ if (canonicalJson(rebuiltFixture) !== canonicalJson(descriptor.fixture)) {
5543
+ errors.push("fixture.json does not match the canonical fixture rebuilt from trace.json");
5544
+ }
5545
+ if (descriptor.bundle.traceHash !== descriptor.fixture.traceHash) {
5546
+ errors.push("bundle traceHash does not match fixture traceHash");
5547
+ }
5548
+ if (descriptor.bundle.fixtureHash !== descriptor.fixture.fixtureHash) {
5549
+ errors.push("bundle fixtureHash does not match fixture fixtureHash");
5550
+ }
5551
+ if (descriptor.manifest.hashes.traceHash !== descriptor.fixture.traceHash ||
5552
+ descriptor.manifest.hashes.fixtureHash !== descriptor.fixture.fixtureHash ||
5553
+ descriptor.manifest.hashes.scoreHash !== descriptor.bundle.scoreHash ||
5554
+ descriptor.manifest.hashes.bundleHash !== descriptor.bundle.bundleHash) {
5555
+ errors.push("manifest hashes do not match the replay artifacts");
5556
+ }
5557
+ const summaryTables = buildRecordedSessionReplayProofSummaryTables(descriptor.bundle);
5558
+ if (canonicalJson(summaryTables) !== canonicalJson(descriptor.summaryTables)) {
5559
+ errors.push("summary-tables.json does not match bundle.json");
5560
+ }
5561
+ const coverageSnapshot = buildRecordedSessionReplayProofCoverageSnapshot(descriptor.bundle);
5562
+ if (canonicalJson(coverageSnapshot) !== canonicalJson(descriptor.coverageSnapshot)) {
5563
+ errors.push("coverage-snapshot.json does not match bundle.json");
5564
+ }
5565
+ const hardeningSnapshot = buildRecordedSessionReplayProofHardeningSnapshot(descriptor.bundle);
5566
+ if (canonicalJson(hardeningSnapshot) !== canonicalJson(descriptor.hardeningSnapshot)) {
5567
+ errors.push("hardening-snapshot.json does not match bundle.json");
5568
+ }
5569
+ const expectedSummaryText = buildRecordedSessionReplayProofSummary({
5570
+ bundle: descriptor.bundle,
5571
+ summaryTables: descriptor.summaryTables,
5572
+ coverageSnapshot: descriptor.coverageSnapshot,
5573
+ hardeningSnapshot: descriptor.hardeningSnapshot,
5574
+ semanticHashes: descriptor.hashes.semantic
5575
+ });
5576
+ if (descriptor.summaryText !== expectedSummaryText) {
5577
+ errors.push("summary.md does not match the canonical replay proof summary");
5578
+ }
5579
+ const expectedModeCount = descriptor.bundle.modes.length;
5580
+ if (descriptor.modeOutputs.length !== expectedModeCount) {
5581
+ errors.push("mode output count does not match bundle modes");
5582
+ }
5583
+ const bundleModes = new Map(descriptor.bundle.modes.map((mode) => [mode.mode, mode]));
5584
+ for (const modeOutput of descriptor.modeOutputs) {
5585
+ const expectedMode = bundleModes.get(modeOutput.mode);
5586
+ if (expectedMode === undefined) {
5587
+ errors.push(`unexpected mode output: ${modeOutput.mode}`);
5588
+ continue;
5589
+ }
5590
+ if (canonicalJson(expectedMode) !== canonicalJson(modeOutput.report)) {
5591
+ errors.push(`mode output drift detected for ${modeOutput.mode}`);
5592
+ }
5593
+ }
5594
+ const semanticHashVerification = verifyRecordedSessionReplayBundleHashes(descriptor.bundle);
5595
+ if (!semanticHashVerification.bundleHashMatches) {
5596
+ errors.push("bundleHash verification failed");
5597
+ }
5598
+ if (!semanticHashVerification.scoreHashMatches) {
5599
+ errors.push("scoreHash verification failed");
5600
+ }
5601
+ const expectedHashes = buildRecordedSessionReplayProofHashes(resolvedRoot, descriptor.manifest);
5602
+ const expectedFileDigests = new Map(expectedHashes.files.map((entry) => [entry.path, entry.digest]));
5603
+ let verifiedFileCount = 0;
5604
+ for (const fileEntry of descriptor.hashes.files ?? []) {
5605
+ if (expectedFileDigests.get(fileEntry.path) === fileEntry.digest) {
5606
+ verifiedFileCount += 1;
5607
+ }
5608
+ }
5609
+ const fileHashesMatch = canonicalJson(expectedHashes.files) === canonicalJson(descriptor.hashes.files ?? []);
5610
+ if (!fileHashesMatch) {
5611
+ errors.push("hashes.json file digests do not match the written artifacts");
5612
+ }
5613
+ if (canonicalJson(expectedHashes.semantic) !== canonicalJson(descriptor.hashes.semantic ?? {})) {
5614
+ errors.push("hashes.json semantic hashes do not match the manifest");
5615
+ }
5616
+ return {
5617
+ contract: RECORDED_SESSION_REPLAY_PROOF_VALIDATION_CONTRACT,
5618
+ ok: errors.length === 0,
5619
+ rootDir: resolvedRoot,
5620
+ expectedFileCount: expectedHashes.files.length,
5621
+ verifiedFileCount,
5622
+ fileHashesMatch,
5623
+ bundleHashMatches: semanticHashVerification.bundleHashMatches,
5624
+ scoreHashMatches: semanticHashVerification.scoreHashMatches,
5625
+ errors
5626
+ };
5627
+ }
5628
+ catch (error) {
5629
+ return {
5630
+ contract: RECORDED_SESSION_REPLAY_PROOF_VALIDATION_CONTRACT,
5631
+ ok: false,
5632
+ rootDir: resolvedRoot,
5633
+ expectedFileCount: 0,
5634
+ verifiedFileCount: 0,
5635
+ fileHashesMatch: false,
5636
+ bundleHashMatches: false,
5637
+ scoreHashMatches: false,
5638
+ errors: [toErrorMessage(error)]
5639
+ };
5640
+ }
5641
+ }
5642
+ export function writeRecordedSessionReplayProofBundle(input) {
5643
+ const resolvedRoot = path.resolve(normalizeNonEmptyString(input.rootDir, "rootDir"));
5644
+ const scratchRootParent = path.resolve(normalizeOptionalString(input.scratchRootDir) ?? tmpdir());
5645
+ mkdirSync(resolvedRoot, { recursive: true });
5646
+ mkdirSync(scratchRootParent, { recursive: true });
5647
+ rmSync(path.join(resolvedRoot, RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.modeDir), {
5648
+ recursive: true,
5649
+ force: true
5650
+ });
5651
+ for (const relativePath of [
5652
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.trace,
5653
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.fixture,
5654
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.bundle,
5655
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.environment,
5656
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.summary,
5657
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.summaryTables,
5658
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.coverageSnapshot,
5659
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.hardeningSnapshot,
5660
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.manifest,
5661
+ RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.hashes
5662
+ ]) {
5663
+ rmSync(path.join(resolvedRoot, relativePath), { force: true });
5664
+ }
5665
+ const trace = structuredClone(input.trace);
5666
+ const fixture = buildRecordedSessionReplayFixture(trace);
5667
+ const scratchRoot = mkdtempSync(path.join(scratchRootParent, "openclawbrain-recorded-session-replay-"));
5668
+ let bundle;
5669
+ try {
5670
+ bundle = runRecordedSessionReplay(scratchRoot, fixture);
5671
+ }
5672
+ finally {
5673
+ rmSync(scratchRoot, { recursive: true, force: true });
5674
+ }
5675
+ const environment = buildRecordedSessionReplayProofEnvironment();
5676
+ const summaryTables = buildRecordedSessionReplayProofSummaryTables(bundle);
5677
+ const coverageSnapshot = buildRecordedSessionReplayProofCoverageSnapshot(bundle);
5678
+ const hardeningSnapshot = buildRecordedSessionReplayProofHardeningSnapshot(bundle);
5679
+ const manifest = buildRecordedSessionReplayProofManifest(bundle);
5680
+ const summaryText = buildRecordedSessionReplayProofSummary({
5681
+ bundle,
5682
+ summaryTables,
5683
+ coverageSnapshot,
5684
+ hardeningSnapshot,
5685
+ semanticHashes: manifest.hashes
5686
+ });
5687
+ const modeRoot = path.join(resolvedRoot, RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.modeDir);
5688
+ mkdirSync(modeRoot, { recursive: true });
5689
+ writeFileSync(path.join(resolvedRoot, manifest.files.trace), canonicalJson(trace), "utf8");
5690
+ writeFileSync(path.join(resolvedRoot, manifest.files.fixture), canonicalJson(fixture), "utf8");
5691
+ writeFileSync(path.join(resolvedRoot, manifest.files.bundle), canonicalJson(bundle), "utf8");
5692
+ writeFileSync(path.join(resolvedRoot, manifest.files.environment), canonicalJson(environment), "utf8");
5693
+ writeFileSync(path.join(resolvedRoot, manifest.files.summaryTables), canonicalJson(summaryTables), "utf8");
5694
+ writeFileSync(path.join(resolvedRoot, manifest.files.coverageSnapshot), canonicalJson(coverageSnapshot), "utf8");
5695
+ writeFileSync(path.join(resolvedRoot, manifest.files.hardeningSnapshot), canonicalJson(hardeningSnapshot), "utf8");
5696
+ for (const mode of orderedRecordedSessionReplayModes(bundle.modes)) {
5697
+ writeFileSync(path.join(resolvedRoot, recordedSessionReplayModeOutputPath(mode.mode)), canonicalJson(mode), "utf8");
5698
+ }
5699
+ writeFileSync(path.join(resolvedRoot, manifest.files.summary), summaryText, "utf8");
5700
+ writeFileSync(path.join(resolvedRoot, RECORDED_SESSION_REPLAY_PROOF_BUNDLE_LAYOUT.manifest), canonicalJson(manifest), "utf8");
5701
+ const hashes = buildRecordedSessionReplayProofHashes(resolvedRoot, manifest);
5702
+ writeFileSync(path.join(resolvedRoot, manifest.files.hashes), canonicalJson(hashes), "utf8");
5703
+ const validation = validateRecordedSessionReplayProofBundle(resolvedRoot);
5704
+ if (!validation.ok) {
5705
+ throw new Error(`recorded session replay proof bundle is invalid: ${validation.errors.join("; ")}`);
5706
+ }
5707
+ return loadRecordedSessionReplayProofBundle(resolvedRoot);
5708
+ }
4778
5709
  export const OPERATOR_API_CONTRACT_ID = "openclaw_operator_api.v1";
4779
5710
  export const SUPPORTED_OPERATOR_API_FAMILIES = [
4780
5711
  "bootstrap_attach",
@@ -5737,7 +6668,128 @@ function summarizeTeacherLoopWatchState(input) {
5737
6668
  snapshotUpdatedAt: input.watchSnapshot.updatedAt,
5738
6669
  lastWatchHeartbeatAt,
5739
6670
  pollIntervalSeconds,
5740
- watchState: Number.isFinite(lagMs) && lagMs >= 0 && lagMs <= staleAfterMs ? "watching" : "stale_snapshot"
6671
+ watchState: Number.isFinite(lagMs) && lagMs >= 0 && lagMs <= staleAfterMs ? "watching" : "stale_snapshot"
6672
+ };
6673
+ }
6674
+ function emptyOperatorLearningAttribution(source, snapshotKind, detail) {
6675
+ return {
6676
+ available: false,
6677
+ source,
6678
+ snapshotKind,
6679
+ quality: "unavailable",
6680
+ nonZeroObservationCount: 0,
6681
+ skippedZeroRewardCount: 0,
6682
+ exactMatchCount: 0,
6683
+ heuristicMatchCount: 0,
6684
+ unmatchedCount: 0,
6685
+ ambiguousCount: 0,
6686
+ matchedByMode: {
6687
+ exactDecisionId: 0,
6688
+ exactSelectionDigest: 0,
6689
+ turnCompileEventId: 0,
6690
+ legacyHeuristic: 0
6691
+ },
6692
+ unmatchedByMode: {
6693
+ exactDecisionId: 0,
6694
+ exactSelectionDigest: 0,
6695
+ turnCompileEventId: 0,
6696
+ legacyHeuristic: 0
6697
+ },
6698
+ ambiguousByMode: {
6699
+ exactDecisionId: 0,
6700
+ exactSelectionDigest: 0,
6701
+ turnCompileEventId: 0,
6702
+ legacyHeuristic: 0
6703
+ },
6704
+ detail
6705
+ };
6706
+ }
6707
+ function sumObservationBindingModes(counts) {
6708
+ return (counts.exactDecisionId ?? 0)
6709
+ + (counts.exactSelectionDigest ?? 0)
6710
+ + (counts.turnCompileEventId ?? 0)
6711
+ + (counts.legacyHeuristic ?? 0);
6712
+ }
6713
+ function deriveObservationBindingQuality(summary) {
6714
+ if (summary.nonZeroObservationCount === 0) {
6715
+ return "no_nonzero_observations";
6716
+ }
6717
+ if (summary.ambiguousCount > 0) {
6718
+ if (summary.exactMatchCount > 0) {
6719
+ return "exact_with_ambiguous";
6720
+ }
6721
+ if (summary.heuristicMatchCount > 0) {
6722
+ return "heuristic_with_ambiguous";
6723
+ }
6724
+ return "ambiguous_present";
6725
+ }
6726
+ if (summary.unmatchedCount > 0) {
6727
+ if (summary.exactMatchCount > 0) {
6728
+ return "exact_with_unmatched";
6729
+ }
6730
+ if (summary.heuristicMatchCount > 0) {
6731
+ return "heuristic_with_unmatched";
6732
+ }
6733
+ return "unmatched_present";
6734
+ }
6735
+ if (summary.exactMatchCount > 0 && summary.heuristicMatchCount > 0) {
6736
+ return "exact_plus_heuristic";
6737
+ }
6738
+ if (summary.exactMatchCount > 0) {
6739
+ return "exact_only";
6740
+ }
6741
+ if (summary.heuristicMatchCount > 0) {
6742
+ return "heuristic_only";
6743
+ }
6744
+ return "no_matches";
6745
+ }
6746
+ function summarizeTeacherLoopObservationBinding(snapshot, sourceKind) {
6747
+ const stats = snapshot.learner.lastMaterialization?.candidate.routingBuild?.observationBindingStats ?? null;
6748
+ if (stats === null) {
6749
+ return emptyOperatorLearningAttribution("latest_materialization", sourceKind, snapshot.learner.lastMaterialization === null
6750
+ ? "no materialized candidate pack is visible in the teacher snapshot"
6751
+ : "latest materialization did not expose observation binding stats");
6752
+ }
6753
+ const exactMatchCount = (stats.matched.exactDecisionId ?? 0)
6754
+ + (stats.matched.exactSelectionDigest ?? 0)
6755
+ + (stats.matched.turnCompileEventId ?? 0);
6756
+ const heuristicMatchCount = stats.matched.legacyHeuristic ?? 0;
6757
+ const unmatchedCount = sumObservationBindingModes(stats.unmatched);
6758
+ const ambiguousCount = sumObservationBindingModes(stats.ambiguous);
6759
+ const summary = {
6760
+ available: true,
6761
+ source: "latest_materialization",
6762
+ snapshotKind: sourceKind,
6763
+ quality: "unavailable",
6764
+ nonZeroObservationCount: stats.nonZeroObservationCount ?? 0,
6765
+ skippedZeroRewardCount: stats.skippedZeroRewardCount ?? 0,
6766
+ exactMatchCount,
6767
+ heuristicMatchCount,
6768
+ unmatchedCount,
6769
+ ambiguousCount,
6770
+ matchedByMode: {
6771
+ exactDecisionId: stats.matched.exactDecisionId ?? 0,
6772
+ exactSelectionDigest: stats.matched.exactSelectionDigest ?? 0,
6773
+ turnCompileEventId: stats.matched.turnCompileEventId ?? 0,
6774
+ legacyHeuristic: stats.matched.legacyHeuristic ?? 0
6775
+ },
6776
+ unmatchedByMode: {
6777
+ exactDecisionId: stats.unmatched.exactDecisionId ?? 0,
6778
+ exactSelectionDigest: stats.unmatched.exactSelectionDigest ?? 0,
6779
+ turnCompileEventId: stats.unmatched.turnCompileEventId ?? 0,
6780
+ legacyHeuristic: stats.unmatched.legacyHeuristic ?? 0
6781
+ },
6782
+ ambiguousByMode: {
6783
+ exactDecisionId: stats.ambiguous.exactDecisionId ?? 0,
6784
+ exactSelectionDigest: stats.ambiguous.exactSelectionDigest ?? 0,
6785
+ turnCompileEventId: stats.ambiguous.turnCompileEventId ?? 0,
6786
+ legacyHeuristic: stats.ambiguous.legacyHeuristic ?? 0
6787
+ },
6788
+ detail: "teacher observation binding stats came from the latest materialized candidate"
6789
+ };
6790
+ return {
6791
+ ...summary,
6792
+ quality: deriveObservationBindingQuality(summary)
5741
6793
  };
5742
6794
  }
5743
6795
  function summarizeTeacherLoop(input) {
@@ -5779,6 +6831,7 @@ function summarizeTeacherLoop(input) {
5779
6831
  failureDetail: null,
5780
6832
  lastAppliedMaterializationJobId: null,
5781
6833
  lastMaterializedPackId: null,
6834
+ observationBinding: emptyOperatorLearningAttribution("unavailable", "missing", "no teacher snapshot path supplied"),
5782
6835
  lastObservedDelta: unavailableFromMissing,
5783
6836
  notes: [],
5784
6837
  detail: "no teacher snapshot path supplied"
@@ -5818,6 +6871,7 @@ function summarizeTeacherLoop(input) {
5818
6871
  failureDetail: null,
5819
6872
  lastAppliedMaterializationJobId: null,
5820
6873
  lastMaterializedPackId: null,
6874
+ observationBinding: emptyOperatorLearningAttribution("unavailable", "missing", "teacher snapshot could not be loaded"),
5821
6875
  lastObservedDelta: unavailableFromMissing,
5822
6876
  notes: [],
5823
6877
  detail: "teacher snapshot could not be loaded"
@@ -5866,6 +6920,7 @@ function summarizeTeacherLoop(input) {
5866
6920
  snapshot.learner.lastMaterialization?.jobId ??
5867
6921
  null,
5868
6922
  lastMaterializedPackId: snapshot.learner.lastMaterialization?.candidate.summary.packId ?? null,
6923
+ observationBinding: summarizeTeacherLoopObservationBinding(snapshot, loaded.sourceKind),
5869
6924
  lastObservedDelta: loaded.sourceKind === "watch_snapshot" && watchSnapshot !== null
5870
6925
  ? cloneLastObservedDelta(watchSnapshot.lastObservedDelta)
5871
6926
  : unavailableFromAsync,
@@ -5994,14 +7049,107 @@ function summarizeLearningWarningStates(input) {
5994
7049
  input.teacherSnapshot.queue.depth >= input.teacherSnapshot.queue.capacity) {
5995
7050
  warnings.add("teacher_queue_full");
5996
7051
  }
5997
- if (input.teacherSnapshot.diagnostics.latestFreshness === "stale") {
7052
+ if (input.teacherSnapshot.diagnostics.latestFreshness === "stale" && input.teacherSnapshot.diagnostics.lastNoOpReason !== "no_teacher_artifacts") {
5998
7053
  warnings.add("teacher_labels_stale");
5999
7054
  }
6000
- if (input.teacherSnapshot.diagnostics.lastNoOpReason === "no_teacher_artifacts") {
7055
+ if (input.teacherSnapshot.diagnostics.lastNoOpReason === "no_teacher_artifacts" &&
7056
+ summarizeTeacherNoArtifactCycle(input.teacherSnapshot.diagnostics.notes).shouldWarn) {
6001
7057
  warnings.add("teacher_no_artifacts");
6002
7058
  }
6003
7059
  return [...warnings];
6004
7060
  }
7061
+ export function summarizeTeacherNoArtifactCycle(notes) {
7062
+ const values = new Map();
7063
+ for (const note of notes ?? []) {
7064
+ const separator = note.indexOf("=");
7065
+ if (separator <= 0) {
7066
+ continue;
7067
+ }
7068
+ values.set(note.slice(0, separator), note.slice(separator + 1));
7069
+ }
7070
+ const readNullableNumber = (key) => {
7071
+ const raw = values.get(key);
7072
+ if (raw === undefined || raw === "unknown") {
7073
+ return null;
7074
+ }
7075
+ const parsed = Number.parseInt(raw, 10);
7076
+ return Number.isFinite(parsed) ? parsed : null;
7077
+ };
7078
+ const readNullableString = (key) => {
7079
+ const raw = values.get(key);
7080
+ return raw === undefined || raw === "unknown" ? null : raw;
7081
+ };
7082
+ const deterministicArtifactCount = readNullableNumber("teacher_last_cycle_deterministic_artifacts");
7083
+ const newDeterministicArtifactCount = readNullableNumber("teacher_last_cycle_new_deterministic_artifacts");
7084
+ const labelerCandidateCount = readNullableNumber("teacher_last_cycle_labeler_candidates");
7085
+ const labelerBudgetedCandidateCount = readNullableNumber("teacher_last_cycle_labeler_budgeted_candidates");
7086
+ const labelerStatus = readNullableString("teacher_last_cycle_labeler_status");
7087
+ const labelerDetail = readNullableString("teacher_last_cycle_labeler_detail");
7088
+ if (deterministicArtifactCount === null && labelerCandidateCount === null) {
7089
+ return {
7090
+ shouldWarn: true,
7091
+ detail: "the latest cycle produced no new teacher artifacts, and the snapshot did not say whether any teachable material was present"
7092
+ };
7093
+ }
7094
+ if ((deterministicArtifactCount ?? 0) === 0 && (labelerCandidateCount ?? 0) === 0) {
7095
+ return {
7096
+ shouldWarn: false,
7097
+ detail: "the latest cycle produced no new teacher artifacts because the current export had no eligible feedback, operator overrides, or matched interaction text"
7098
+ };
7099
+ }
7100
+ if ((deterministicArtifactCount ?? 0) > 0 &&
7101
+ (newDeterministicArtifactCount ?? 0) === 0 &&
7102
+ (labelerCandidateCount ?? 0) === 0) {
7103
+ return {
7104
+ shouldWarn: false,
7105
+ detail: "the latest cycle produced no new teacher artifacts because the current export only repeated supervision that was already captured"
7106
+ };
7107
+ }
7108
+ if ((labelerCandidateCount ?? 0) > 0) {
7109
+ if ((labelerBudgetedCandidateCount ?? labelerCandidateCount) === 0) {
7110
+ return {
7111
+ shouldWarn: true,
7112
+ detail: "the latest cycle produced no new teacher artifacts because candidate interactions exceeded the teacher labeler prompt budget"
7113
+ };
7114
+ }
7115
+ if (labelerStatus === "disabled") {
7116
+ return {
7117
+ shouldWarn: false,
7118
+ detail: "the latest cycle produced no new teacher artifacts because candidate interactions existed, but the background teacher labeler is disabled"
7119
+ };
7120
+ }
7121
+ if (labelerStatus === "ok") {
7122
+ return {
7123
+ shouldWarn: false,
7124
+ detail: "the latest cycle produced no new teacher artifacts because candidate interactions were evaluated but did not add a new reusable label"
7125
+ };
7126
+ }
7127
+ if (labelerStatus === "fail_open") {
7128
+ return {
7129
+ shouldWarn: true,
7130
+ detail: labelerDetail === null
7131
+ ? "the latest cycle produced no new teacher artifacts because the teacher labeler failed open while candidate interactions were present"
7132
+ : `the latest cycle produced no new teacher artifacts because the teacher labeler failed open while candidate interactions were present: ${labelerDetail}`
7133
+ };
7134
+ }
7135
+ if (labelerDetail === "no_labels_emitted") {
7136
+ return {
7137
+ shouldWarn: true,
7138
+ detail: "the latest cycle produced no new teacher artifacts even though candidate interactions were present; the teacher labeler emitted no reusable labels"
7139
+ };
7140
+ }
7141
+ return {
7142
+ shouldWarn: true,
7143
+ detail: labelerDetail === null
7144
+ ? "the latest cycle produced no new teacher artifacts even though candidate interactions were present"
7145
+ : `the latest cycle produced no new teacher artifacts even though candidate interactions were present: ${labelerDetail}`
7146
+ };
7147
+ }
7148
+ return {
7149
+ shouldWarn: true,
7150
+ detail: "the latest cycle produced no new teacher artifacts even though teachable material was present"
7151
+ };
7152
+ }
6005
7153
  function summarizeAlwaysOnLearning(input, active) {
6006
7154
  const unavailableLag = {
6007
7155
  activeEventRangeEnd: active?.eventRange.end ?? null,
@@ -6619,6 +7767,10 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode, profileId)
6619
7767
  installState: report.hook.installState,
6620
7768
  loadability: report.hook.loadability,
6621
7769
  loadProof: report.hook.loadProof,
7770
+ guardSeverity: report.hook.guardSeverity,
7771
+ guardActionability: report.hook.guardActionability,
7772
+ guardSummary: report.hook.guardSummary,
7773
+ guardAction: report.hook.guardAction,
6622
7774
  desynced: report.hook.desynced,
6623
7775
  detail: report.hook.detail
6624
7776
  },
@@ -6665,7 +7817,10 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode, profileId)
6665
7817
  ? "current profile would fail open to static context because no serving pack is available"
6666
7818
  : report.servePath.state === "hard_fail"
6667
7819
  ? "current profile cannot serve because the learned-route or activation requirement hard-failed"
6668
- : "current profile serve state has not been compile-probed yet"
7820
+ : "current profile serve state has not been compile-probed yet"
7821
+ },
7822
+ learningAttribution: {
7823
+ ...report.teacherLoop.observationBinding
6669
7824
  },
6670
7825
  passiveLearning,
6671
7826
  currentTurnAttribution: buildCurrentProfileTurnAttributionFromReport(report, policyMode, profileId)