@openclawbrain/openclaw 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { compileRuntimeFromActivation } from "@openclawbrain/compiler";
6
6
  import { CONTRACT_IDS, buildNormalizedEventExport, canonicalJson, checksumJsonPayload, createFeedbackEvent, createInteractionEvent, sortNormalizedEvents, validateKernelSurface, validateNormalizedEventExport } from "@openclawbrain/contracts";
7
- import { describeNormalizedEventExportObservability } from "@openclawbrain/event-export";
7
+ import { classifyFeedbackSignalContent, describeNormalizedEventExportObservability } from "@openclawbrain/event-export";
8
8
  import { DEFAULT_TEACHER_SUPERVISION_STALE_AFTER_MS, advanceAlwaysOnLearningRuntime, buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, materializeAlwaysOnLearningCandidatePack, materializeCandidatePackFromNormalizedEventExport } from "@openclawbrain/learner";
9
9
  import { LEARNING_SPINE_LOG_LAYOUT, activatePack, describeActivationObservability, describeActivationTarget, describePackCompileTarget, inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, rollbackActivePack, stageCandidatePack } from "@openclawbrain/pack-format";
10
10
  import { appendLearningUpdateLogs, appendServeTimeRouteDecisionLog } from "./learning-spine.js";
@@ -21,11 +21,14 @@ export const RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT = {
21
21
  manifest: "manifest.json",
22
22
  payload: "normalized-event-export.json"
23
23
  };
24
- function normalizeBootstrapProfileSelector(value) {
25
- if (value === undefined || value === null || value === "current_profile") {
26
- return "current_profile";
24
+ function normalizeRuntimeProfileSelector(value, fieldName, fallback = "current_profile") {
25
+ if (value === undefined || value === null) {
26
+ return fallback;
27
27
  }
28
- throw new Error('bootstrapRuntimeAttach only supports profileSelector="current_profile"');
28
+ return normalizeNonEmptyString(value, fieldName);
29
+ }
30
+ function normalizeBootstrapProfileSelector(value) {
31
+ return normalizeRuntimeProfileSelector(value, "profileSelector");
29
32
  }
30
33
  function quoteShellArg(value) {
31
34
  return `'${value.replace(/'/g, `'"'"'`)}'`;
@@ -57,10 +60,13 @@ function buildBootstrapAttachRollbackDryRunCommand(activationRoot) {
57
60
  return `${detectBootstrapOperatorCliPrefix()} rollback --activation-root ${quoteShellArg(activationRoot)} --dry-run`;
58
61
  }
59
62
  function buildBootstrapRuntimeAttachNextSteps(input) {
63
+ const profileSpecific = input.profileSelector !== "current_profile";
60
64
  const nextSteps = [
61
65
  {
62
66
  id: "inspect_current_profile_status",
63
- detail: 'Ask the attached gateway "How\'s the brain?" with the compact current-profile status summary; add --json for the canonical object.',
67
+ detail: profileSpecific
68
+ ? `Inspect the canonical current-profile status for this activation root; explicit attach/export attribution for profileSelector="${input.profileSelector}" stays on the runtime path.`
69
+ : 'Ask the attached gateway "How\'s the brain?" with the compact current-profile status summary; add --json for the canonical object.',
64
70
  command: buildBootstrapAttachStatusCommand(input.activationRoot)
65
71
  },
66
72
  {
@@ -72,14 +78,18 @@ function buildBootstrapRuntimeAttachNextSteps(input) {
72
78
  if (input.currentProfile.brainStatus.awaitingFirstExport) {
73
79
  nextSteps.push({
74
80
  id: "record_next_current_profile_turn",
75
- detail: "Export the next live current-profile turn so refresh/promote can advance beyond seed_state_authoritative.",
81
+ detail: profileSpecific
82
+ ? `Export the next live turn with profileSelector="${input.profileSelector}" so refresh/promote can advance beyond seed_state_authoritative without widening the operator read scope.`
83
+ : "Export the next live current-profile turn so refresh/promote can advance beyond seed_state_authoritative.",
76
84
  command: null
77
85
  });
78
86
  }
79
87
  else {
80
88
  nextSteps.push({
81
89
  id: "continue_current_profile_learning_loop",
82
- detail: "Keep exporting current-profile turns and refresh/promote newer activation-ready packs off the hot path.",
90
+ detail: profileSpecific
91
+ ? `Keep exporting turns with profileSelector="${input.profileSelector}" and refresh/promote newer activation-ready packs off the hot path.`
92
+ : "Keep exporting current-profile turns and refresh/promote newer activation-ready packs off the hot path.",
83
93
  command: null
84
94
  });
85
95
  }
@@ -87,12 +97,16 @@ function buildBootstrapRuntimeAttachNextSteps(input) {
87
97
  }
88
98
  export function formatBootstrapRuntimeAttachReport(result) {
89
99
  const [nextStep, ...followUps] = result.nextSteps;
100
+ const profileIdSuffix = result.currentProfile.profile.profileId === null ? "" : ` id=${result.currentProfile.profile.profileId}`;
90
101
  const lines = [
91
102
  `ATTACH ${result.currentProfile.brainStatus.status}`,
92
- `profile selector=${result.profileSelector} attachment=${result.currentProfile.attachment.state}`,
103
+ `profile selector=${result.profileSelector}${profileIdSuffix} attachment=${result.currentProfile.attachment.state} policy=${result.currentProfile.attachment.policyMode} operator_scope=${result.operatorReadScope}`,
93
104
  `activation root=${result.activationRoot}`,
94
105
  `brain pack=${result.packId} state=${result.currentProfile.brain.state} serve=${result.currentProfile.brainStatus.serveState}`
95
106
  ];
107
+ if (result.profileSelector !== "current_profile") {
108
+ lines.push("boundary canonical status remains current_profile-only; explicit attached profile attribution stays on the runtime attach/export path");
109
+ }
96
110
  if (nextStep !== undefined) {
97
111
  lines.push(`next ${nextStep.detail}`);
98
112
  if (nextStep.command !== null) {
@@ -267,6 +281,7 @@ function buildContinuousTurnExport(turn, loopRoot) {
267
281
  const exportSeed = checksumJsonPayload({
268
282
  sessionId: turn.sessionId,
269
283
  channel: turn.channel,
284
+ userId: turn.userId ?? null,
270
285
  sourceStream: turn.sourceStream ?? null,
271
286
  userMessage: turn.userMessage,
272
287
  createdAt: turn.createdAt ?? null,
@@ -287,7 +302,9 @@ function buildContinuousTurnExport(turn, loopRoot) {
287
302
  .filter((item) => item !== null)
288
303
  .map((item) => ({
289
304
  content: item.content,
305
+ actorName: item.actorName ?? null,
290
306
  createdAt: item.createdAt ?? null,
307
+ priorityHint: item.priorityHint ?? null,
291
308
  sequence: item.sequence ?? null,
292
309
  kind: item.kind ?? null,
293
310
  messageId: item.messageId ?? null,
@@ -581,6 +598,129 @@ export class AsyncTeacherLiveLoop {
581
598
  reason: null
582
599
  };
583
600
  }
601
+ enqueueScannedEventExport(scannedEventExport, options = {}) {
602
+ const built = buildNormalizedEventExportFromScannedEvents(scannedEventExport);
603
+ if (!built.ok) {
604
+ return {
605
+ accepted: false,
606
+ exportDigest: null,
607
+ queueDepth: this.queue.length,
608
+ notes: [...this.diagnostics.notes],
609
+ reason: built.reason,
610
+ warnings: [...built.warnings],
611
+ error: built.error,
612
+ scanner: cloneScannerExportManifest(built.scanner)
613
+ };
614
+ }
615
+ const enqueue = this.enqueueNormalizedEventExport(built.normalizedEventExport, {
616
+ observedAt: options.observedAt ?? built.scanner.producedAt
617
+ });
618
+ return {
619
+ accepted: enqueue.accepted,
620
+ exportDigest: enqueue.exportDigest,
621
+ queueDepth: enqueue.queueDepth,
622
+ notes: [...enqueue.notes],
623
+ reason: enqueue.reason,
624
+ warnings: [...built.warnings],
625
+ error: null,
626
+ scanner: cloneScannerExportManifest(built.scanner)
627
+ };
628
+ }
629
+ async ingestRuntimeEventExportScannerScan(scan) {
630
+ if (scan.selected.length === 0) {
631
+ this.diagnostics.lastNoOpReason = "empty_scan";
632
+ this.refreshNotes();
633
+ const snapshot = this.snapshot();
634
+ return {
635
+ runtimeOwner: "openclaw",
636
+ scanRoot: scan.scanRoot,
637
+ scannedAt: scan.scannedAt,
638
+ selectedCount: 0,
639
+ acceptedCount: 0,
640
+ duplicateCount: 0,
641
+ droppedCount: 0,
642
+ liveAcceptedCount: 0,
643
+ backfillAcceptedCount: 0,
644
+ duplicateScannerDigestCount: scan.duplicateExportDigests.length,
645
+ staleSkippedCount: scan.staleSkippedExportDigests.length,
646
+ invalidBundleCount: scan.invalidBundles.length,
647
+ noOpReason: "empty_scan",
648
+ notes: [...snapshot.diagnostics.notes],
649
+ results: [],
650
+ snapshot
651
+ };
652
+ }
653
+ const results = [];
654
+ let acceptedCount = 0;
655
+ let duplicateCount = 0;
656
+ let droppedCount = 0;
657
+ let liveAcceptedCount = 0;
658
+ let backfillAcceptedCount = 0;
659
+ for (const hit of scan.selected) {
660
+ let enqueue = this.enqueueNormalizedEventExport(hit.normalizedEventExport, {
661
+ observedAt: hit.exportedAt
662
+ });
663
+ if (!enqueue.accepted && enqueue.reason === "queue_full") {
664
+ await this.flush();
665
+ enqueue = this.enqueueNormalizedEventExport(hit.normalizedEventExport, {
666
+ observedAt: hit.exportedAt
667
+ });
668
+ }
669
+ if (enqueue.accepted) {
670
+ acceptedCount += 1;
671
+ if (hit.lane === "live") {
672
+ liveAcceptedCount += 1;
673
+ }
674
+ else {
675
+ backfillAcceptedCount += 1;
676
+ }
677
+ }
678
+ else if (enqueue.reason === "duplicate_export") {
679
+ duplicateCount += 1;
680
+ }
681
+ else if (enqueue.reason === "queue_full") {
682
+ droppedCount += 1;
683
+ }
684
+ results.push({
685
+ lane: hit.lane,
686
+ exportDigest: hit.exportDigest,
687
+ exportName: hit.exportName,
688
+ exportedAt: hit.exportedAt,
689
+ eventRange: {
690
+ start: hit.eventRange.start,
691
+ end: hit.eventRange.end,
692
+ count: hit.eventRange.count
693
+ },
694
+ accepted: enqueue.accepted,
695
+ queueDepth: enqueue.queueDepth,
696
+ reason: enqueue.reason
697
+ });
698
+ }
699
+ const snapshot = acceptedCount > 0 ? await this.flush() : this.snapshot();
700
+ const noOpReason = acceptedCount > 0
701
+ ? "none"
702
+ : duplicateCount === scan.selected.length
703
+ ? "duplicate_exports"
704
+ : "queue_full";
705
+ return {
706
+ runtimeOwner: "openclaw",
707
+ scanRoot: scan.scanRoot,
708
+ scannedAt: scan.scannedAt,
709
+ selectedCount: scan.selected.length,
710
+ acceptedCount,
711
+ duplicateCount,
712
+ droppedCount,
713
+ liveAcceptedCount,
714
+ backfillAcceptedCount,
715
+ duplicateScannerDigestCount: scan.duplicateExportDigests.length,
716
+ staleSkippedCount: scan.staleSkippedExportDigests.length,
717
+ invalidBundleCount: scan.invalidBundles.length,
718
+ noOpReason,
719
+ notes: [...snapshot.diagnostics.notes],
720
+ results,
721
+ snapshot
722
+ };
723
+ }
584
724
  async flush() {
585
725
  await this.ensureDrain();
586
726
  return this.snapshot();
@@ -684,6 +824,118 @@ export class AsyncTeacherLiveLoop {
684
824
  export function createAsyncTeacherLiveLoop(input) {
685
825
  return new AsyncTeacherLiveLoop(input);
686
826
  }
827
+ function normalizeScannerStringArray(value, fieldName) {
828
+ if (!Array.isArray(value)) {
829
+ throw new Error(`${fieldName} must be an array of strings`);
830
+ }
831
+ return value.map((entry, index) => normalizeNonEmptyString(entry, `${fieldName}[${index}]`));
832
+ }
833
+ function normalizeScannerLiveWorkspaceInput(workspace) {
834
+ const normalized = {
835
+ workspaceId: normalizeNonEmptyString(workspace.workspaceId, "workspace.workspaceId"),
836
+ snapshotId: normalizeNonEmptyString(workspace.snapshotId, "workspace.snapshotId"),
837
+ capturedAt: normalizeIsoTimestamp(workspace.capturedAt, "workspace.capturedAt"),
838
+ rootDir: path.resolve(normalizeNonEmptyString(workspace.rootDir, "workspace.rootDir")),
839
+ branch: normalizeOptionalString(workspace.branch) ?? null,
840
+ revision: normalizeOptionalString(workspace.revision) ?? null,
841
+ dirty: workspace.dirty === true,
842
+ manifestDigest: normalizeOptionalString(workspace.manifestDigest) ?? null
843
+ };
844
+ if (workspace.labels !== undefined) {
845
+ normalized.labels = normalizeScannerStringArray(workspace.labels, "workspace.labels");
846
+ }
847
+ if (workspace.files !== undefined) {
848
+ normalized.files = normalizeScannerStringArray(workspace.files, "workspace.files");
849
+ }
850
+ return normalized;
851
+ }
852
+ export function scanRecordedSession(input) {
853
+ const rootDir = path.resolve(normalizeNonEmptyString(input.rootDir, "rootDir"));
854
+ const fixture = buildRecordedSessionReplayFixture(structuredClone(input.trace));
855
+ const bundle = runRecordedSessionReplay(rootDir, fixture);
856
+ return {
857
+ runtimeOwner: "openclaw",
858
+ scanMode: "session",
859
+ rootDir,
860
+ fixtureHash: fixture.fixtureHash,
861
+ bundle
862
+ };
863
+ }
864
+ export function scanLiveEventExport(input) {
865
+ const normalizedEventExport = structuredClone(input.normalizedEventExport);
866
+ const validationErrors = validateNormalizedEventExport(normalizedEventExport);
867
+ if (validationErrors.length > 0) {
868
+ throw new Error(`normalized event export is invalid: ${validationErrors.join("; ")}`);
869
+ }
870
+ const workspace = normalizeScannerLiveWorkspaceInput(input.workspace);
871
+ const packLabel = normalizeOptionalString(input.packLabel) ?? "scanner-live-cli";
872
+ const observedAt = normalizeIsoTimestamp(input.observedAt, "observedAt", normalizedEventExport.range.lastCreatedAt ?? normalizedEventExport.range.firstCreatedAt ?? new Date().toISOString());
873
+ const teacherArtifacts = buildTeacherSupervisionArtifactsFromNormalizedEventExport({
874
+ normalizedEventExport,
875
+ observedAt,
876
+ staleAfterMs: input.staleAfterMs ?? DEFAULT_TEACHER_SUPERVISION_STALE_AFTER_MS
877
+ });
878
+ const learnerResult = advanceAlwaysOnLearningRuntime({
879
+ packLabel,
880
+ workspace,
881
+ interactionEvents: normalizedEventExport.interactionEvents,
882
+ feedbackEvents: normalizedEventExport.feedbackEvents,
883
+ teacherSupervisionArtifacts: teacherArtifacts,
884
+ learnedRouting: input.learnedRouting ?? true,
885
+ state: createAlwaysOnLearningRuntimeState(),
886
+ builtAt: normalizeIsoTimestamp(input.builtAt, "builtAt", observedAt),
887
+ ...(input.liveSliceSize !== undefined ? { liveSliceSize: input.liveSliceSize } : {}),
888
+ ...(input.backfillSliceSize !== undefined ? { backfillSliceSize: input.backfillSliceSize } : {})
889
+ });
890
+ const latestFreshness = latestTeacherFreshness(teacherArtifacts);
891
+ const lastNoOpReason = teacherArtifacts.length === 0 ? "no_teacher_artifacts" : "none";
892
+ const snapshot = {
893
+ runtimeOwner: "openclaw",
894
+ queue: {
895
+ capacity: 1,
896
+ depth: 0,
897
+ running: false
898
+ },
899
+ teacher: {
900
+ artifactCount: teacherArtifacts.length,
901
+ artifacts: cloneTeacherSupervisionArtifacts(teacherArtifacts),
902
+ latestFreshness
903
+ },
904
+ learner: {
905
+ state: structuredClone(learnerResult.state),
906
+ lastMaterialization: cloneAlwaysOnLearningMaterializationJobOrNull(learnerResult.materialization)
907
+ },
908
+ diagnostics: {
909
+ acceptedExportCount: 1,
910
+ processedExportCount: 1,
911
+ duplicateExportCount: 0,
912
+ droppedExportCount: 0,
913
+ emittedArtifactCount: teacherArtifacts.length,
914
+ dedupedArtifactCount: 0,
915
+ lastProcessedAt: observedAt,
916
+ latestFreshness,
917
+ lastNoOpReason,
918
+ notes: buildAsyncTeacherLoopNotes({
919
+ queueDepth: 0,
920
+ latestFreshness,
921
+ artifactCount: teacherArtifacts.length,
922
+ emittedArtifactCount: teacherArtifacts.length,
923
+ dedupedArtifactCount: 0,
924
+ sparseFeedback: learnerResult.state.sparseFeedback,
925
+ noOpReason: lastNoOpReason,
926
+ materialization: learnerResult.materialization
927
+ })
928
+ }
929
+ };
930
+ return {
931
+ runtimeOwner: "openclaw",
932
+ scanMode: "live",
933
+ observedAt,
934
+ packLabel,
935
+ supervision: buildCanonicalSupervision(normalizedEventExport),
936
+ snapshot
937
+ };
938
+ }
687
939
  function readJsonFile(filePath) {
688
940
  return JSON.parse(readFileSync(filePath, "utf8"));
689
941
  }
@@ -715,7 +967,8 @@ export function buildRuntimeEventExportBundleManifest(input) {
715
967
  feedbackCount: input.normalizedEventExport.provenance.feedbackCount,
716
968
  sourceStreams: [...input.normalizedEventExport.provenance.sourceStreams],
717
969
  contracts: [...input.normalizedEventExport.provenance.contracts]
718
- }
970
+ },
971
+ ...(input.scanner !== undefined ? { scanner: cloneScannerExportManifestOrNull(input.scanner) } : {})
719
972
  };
720
973
  }
721
974
  export function validateRuntimeEventExportBundleManifest(value, normalizedEventExport) {
@@ -735,6 +988,12 @@ export function validateRuntimeEventExportBundleManifest(value, normalizedEventE
735
988
  if (value.payloadDigest.length === 0) {
736
989
  errors.push("payloadDigest is required");
737
990
  }
991
+ if (value.scanner !== undefined && value.scanner !== null) {
992
+ errors.push(...validateScannerExportManifest(value.scanner).map((message) => `scanner ${message}`));
993
+ if (normalizedEventExport !== undefined && normalizedEventExport.range.count === 0) {
994
+ errors.push("scanner bundles require at least one exported event");
995
+ }
996
+ }
738
997
  if (normalizedEventExport !== undefined) {
739
998
  const exportErrors = validateNormalizedEventExport(normalizedEventExport);
740
999
  if (exportErrors.length > 0) {
@@ -744,7 +1003,8 @@ export function validateRuntimeEventExportBundleManifest(value, normalizedEventE
744
1003
  exportName: value.exportName,
745
1004
  exportedAt: value.exportedAt,
746
1005
  payloadPath: value.payloadPath,
747
- normalizedEventExport
1006
+ normalizedEventExport,
1007
+ ...(value.scanner !== undefined ? { scanner: value.scanner } : {})
748
1008
  });
749
1009
  if (rebuilt.payloadDigest !== value.payloadDigest) {
750
1010
  errors.push("event export bundle payloadDigest does not match the supplied normalized event export");
@@ -752,6 +1012,9 @@ export function validateRuntimeEventExportBundleManifest(value, normalizedEventE
752
1012
  if (canonicalJson(rebuilt.summary) !== canonicalJson(value.summary)) {
753
1013
  errors.push("event export bundle summary does not match the supplied normalized event export");
754
1014
  }
1015
+ if (canonicalJson(rebuilt.scanner ?? null) !== canonicalJson(value.scanner ?? null)) {
1016
+ errors.push("event export bundle scanner metadata does not match the supplied normalized event export");
1017
+ }
755
1018
  }
756
1019
  return errors;
757
1020
  }
@@ -773,6 +1036,709 @@ export function loadRuntimeEventExportBundle(rootDir) {
773
1036
  normalizedEventExport
774
1037
  };
775
1038
  }
1039
+ function cloneScannerExportManifest(value) {
1040
+ return {
1041
+ scannerId: value.scannerId,
1042
+ lane: value.lane,
1043
+ status: value.status,
1044
+ producedAt: value.producedAt,
1045
+ sourceManifestPath: value.sourceManifestPath,
1046
+ sourceManifestDigest: value.sourceManifestDigest,
1047
+ warnings: [...value.warnings],
1048
+ failures: [...value.failures]
1049
+ };
1050
+ }
1051
+ function cloneScannerExportManifestOrNull(value) {
1052
+ return value === null ? null : cloneScannerExportManifest(value);
1053
+ }
1054
+ function validateScannerExportManifest(value) {
1055
+ const errors = [];
1056
+ if (normalizeOptionalString(value.scannerId) === undefined) {
1057
+ errors.push("scannerId is required");
1058
+ }
1059
+ if (normalizeOptionalString(value.lane) === undefined) {
1060
+ errors.push("lane is required");
1061
+ }
1062
+ if (!["complete", "partial", "failed"].includes(value.status)) {
1063
+ errors.push("status must be complete, partial, or failed");
1064
+ }
1065
+ if (Number.isNaN(Date.parse(value.producedAt))) {
1066
+ errors.push("producedAt must be an ISO timestamp");
1067
+ }
1068
+ if (value.sourceManifestPath !== null && normalizeOptionalString(value.sourceManifestPath) === undefined) {
1069
+ errors.push("sourceManifestPath must be null or a non-empty string");
1070
+ }
1071
+ if (value.sourceManifestDigest !== null && normalizeOptionalString(value.sourceManifestDigest) === undefined) {
1072
+ errors.push("sourceManifestDigest must be null or a non-empty string");
1073
+ }
1074
+ if (!Array.isArray(value.warnings) || value.warnings.some((warning) => normalizeOptionalString(warning) === undefined)) {
1075
+ errors.push("warnings must contain only non-empty strings");
1076
+ }
1077
+ if (!Array.isArray(value.failures) || value.failures.some((failure) => normalizeOptionalString(failure) === undefined)) {
1078
+ errors.push("failures must contain only non-empty strings");
1079
+ }
1080
+ if (value.status === "failed" && value.failures.length === 0) {
1081
+ errors.push("failed scanner manifests require at least one failure");
1082
+ }
1083
+ return errors;
1084
+ }
1085
+ function normalizeScannerExportWarnings(value) {
1086
+ const warnings = new Set();
1087
+ for (const warning of value.warnings) {
1088
+ const normalized = normalizeOptionalString(warning);
1089
+ if (normalized !== undefined) {
1090
+ warnings.add(normalized);
1091
+ }
1092
+ }
1093
+ for (const failure of value.failures) {
1094
+ const normalized = normalizeOptionalString(failure);
1095
+ if (normalized !== undefined) {
1096
+ warnings.add(`scanner_failure:${normalized}`);
1097
+ }
1098
+ }
1099
+ return [...warnings];
1100
+ }
1101
+ export function buildNormalizedEventExportFromScannedEvents(input) {
1102
+ const scanner = cloneScannerExportManifest(input.scanner);
1103
+ const scannerErrors = validateScannerExportManifest(scanner);
1104
+ if (scannerErrors.length > 0) {
1105
+ return {
1106
+ ok: false,
1107
+ normalizedEventExport: null,
1108
+ scanner,
1109
+ warnings: normalizeScannerExportWarnings(scanner),
1110
+ reason: "invalid_scanner_manifest",
1111
+ error: `scanner manifest is invalid: ${scannerErrors.join("; ")}`
1112
+ };
1113
+ }
1114
+ const eventCount = input.interactionEvents.length + input.feedbackEvents.length;
1115
+ if (eventCount === 0) {
1116
+ return {
1117
+ ok: false,
1118
+ normalizedEventExport: null,
1119
+ scanner,
1120
+ warnings: normalizeScannerExportWarnings(scanner),
1121
+ reason: scanner.status === "failed" ? "scan_failed" : "no_events",
1122
+ error: scanner.status === "failed"
1123
+ ? "scanner failed before producing any interaction or feedback events"
1124
+ : "scanner output did not contain any interaction or feedback events"
1125
+ };
1126
+ }
1127
+ const normalizedEventExport = buildNormalizedEventExport({
1128
+ interactionEvents: [...input.interactionEvents],
1129
+ feedbackEvents: [...input.feedbackEvents]
1130
+ });
1131
+ const exportErrors = validateNormalizedEventExport(normalizedEventExport);
1132
+ if (exportErrors.length > 0) {
1133
+ return {
1134
+ ok: false,
1135
+ normalizedEventExport: null,
1136
+ scanner,
1137
+ warnings: normalizeScannerExportWarnings(scanner),
1138
+ reason: "invalid_scanner_manifest",
1139
+ error: `scanner output produced an invalid normalized event export: ${exportErrors.join("; ")}`
1140
+ };
1141
+ }
1142
+ return {
1143
+ ok: true,
1144
+ normalizedEventExport,
1145
+ scanner,
1146
+ warnings: normalizeScannerExportWarnings(scanner)
1147
+ };
1148
+ }
1149
+ function writeNormalizedEventExportBundleFiles(input) {
1150
+ const resolvedRoot = path.resolve(input.rootDir);
1151
+ const manifestPath = path.join(resolvedRoot, RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT.manifest);
1152
+ const payloadPath = path.join(resolvedRoot, input.manifest.payloadPath);
1153
+ mkdirSync(path.dirname(payloadPath), { recursive: true });
1154
+ writeFileSync(payloadPath, canonicalJson(input.normalizedEventExport), "utf8");
1155
+ writeFileSync(manifestPath, canonicalJson(input.manifest), "utf8");
1156
+ const descriptor = loadRuntimeEventExportBundle(resolvedRoot);
1157
+ return {
1158
+ ok: true,
1159
+ wroteBundle: true,
1160
+ normalizedEventExport: descriptor.normalizedEventExport,
1161
+ rootDir: descriptor.rootDir,
1162
+ manifestPath: descriptor.manifestPath,
1163
+ payloadPath: descriptor.payloadPath,
1164
+ manifest: descriptor.manifest
1165
+ };
1166
+ }
1167
+ const RUNTIME_EVENT_EXPORT_SCANNER_CHECKPOINT_CONTRACT = "runtime_event_export_scanner_checkpoint.v1";
1168
+ export const DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_LIVE_TAIL_BUNDLES = 2;
1169
+ export const DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_BACKFILL_BUNDLES_PER_PASS = 1;
1170
+ export const DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_STALE_HISTORY_MS = 1000 * 60 * 60 * 24 * 7;
1171
+ export const DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_CHECKPOINT_BASENAME = ".openclawbrain-scanner-checkpoint.json";
1172
+ function validateRuntimeEventExportScannerBundleCursor(value, fieldName) {
1173
+ const errors = [];
1174
+ if (value.exportDigest.length === 0) {
1175
+ errors.push(`${fieldName}.exportDigest is required`);
1176
+ }
1177
+ if (value.exportName.length === 0) {
1178
+ errors.push(`${fieldName}.exportName is required`);
1179
+ }
1180
+ if (Number.isNaN(Date.parse(value.exportedAt))) {
1181
+ errors.push(`${fieldName}.exportedAt must be an ISO timestamp`);
1182
+ }
1183
+ if (!Number.isInteger(value.eventRange.start) || value.eventRange.start < 0) {
1184
+ errors.push(`${fieldName}.eventRange.start must be a non-negative integer`);
1185
+ }
1186
+ if (!Number.isInteger(value.eventRange.end) || value.eventRange.end < 0) {
1187
+ errors.push(`${fieldName}.eventRange.end must be a non-negative integer`);
1188
+ }
1189
+ if (!Number.isInteger(value.eventRange.count) || value.eventRange.count < 0) {
1190
+ errors.push(`${fieldName}.eventRange.count must be a non-negative integer`);
1191
+ }
1192
+ return errors;
1193
+ }
1194
+ export function createRuntimeEventExportScannerCheckpoint(input) {
1195
+ return {
1196
+ contract: RUNTIME_EVENT_EXPORT_SCANNER_CHECKPOINT_CONTRACT,
1197
+ runtimeOwner: "openclaw",
1198
+ scanRoot: path.resolve(normalizeNonEmptyString(input.scanRoot, "scanRoot")),
1199
+ updatedAt: normalizeIsoTimestamp(input.updatedAt, "updatedAt", new Date().toISOString()),
1200
+ live: {
1201
+ after: null
1202
+ },
1203
+ backfill: {
1204
+ before: null,
1205
+ exhausted: false,
1206
+ staleBefore: null
1207
+ },
1208
+ processedExportDigests: [],
1209
+ stats: {
1210
+ scanPasses: 0,
1211
+ liveBundlesScanned: 0,
1212
+ backfillBundlesScanned: 0,
1213
+ duplicateBundlesSkipped: 0,
1214
+ staleBundlesSkipped: 0,
1215
+ invalidBundlesSkipped: 0
1216
+ }
1217
+ };
1218
+ }
1219
+ export function validateRuntimeEventExportScannerCheckpoint(value) {
1220
+ const errors = [];
1221
+ if (value.contract !== RUNTIME_EVENT_EXPORT_SCANNER_CHECKPOINT_CONTRACT) {
1222
+ errors.push("runtime_event_export_scanner_checkpoint.v1 contract is required");
1223
+ }
1224
+ if (value.runtimeOwner !== "openclaw") {
1225
+ errors.push("runtime event export scanner checkpoint runtimeOwner must be openclaw");
1226
+ }
1227
+ if (value.scanRoot.length === 0) {
1228
+ errors.push("runtime event export scanner checkpoint scanRoot is required");
1229
+ }
1230
+ if (Number.isNaN(Date.parse(value.updatedAt))) {
1231
+ errors.push("runtime event export scanner checkpoint updatedAt must be an ISO timestamp");
1232
+ }
1233
+ if (value.live.after !== null) {
1234
+ errors.push(...validateRuntimeEventExportScannerBundleCursor(value.live.after, "live.after"));
1235
+ }
1236
+ if (value.backfill.before !== null) {
1237
+ errors.push(...validateRuntimeEventExportScannerBundleCursor(value.backfill.before, "backfill.before"));
1238
+ }
1239
+ if (value.backfill.staleBefore !== null && Number.isNaN(Date.parse(value.backfill.staleBefore))) {
1240
+ errors.push("runtime event export scanner checkpoint backfill.staleBefore must be null or an ISO timestamp");
1241
+ }
1242
+ if (new Set(value.processedExportDigests).size !== value.processedExportDigests.length) {
1243
+ errors.push("runtime event export scanner checkpoint processedExportDigests must be unique");
1244
+ }
1245
+ for (const [label, count] of Object.entries(value.stats)) {
1246
+ if (!Number.isInteger(count) || count < 0) {
1247
+ errors.push(`runtime event export scanner checkpoint stats.${label} must be a non-negative integer`);
1248
+ }
1249
+ }
1250
+ return errors;
1251
+ }
1252
+ export function loadRuntimeEventExportScannerCheckpoint(checkpointPath) {
1253
+ const resolvedPath = path.resolve(normalizeNonEmptyString(checkpointPath, "checkpointPath"));
1254
+ const checkpoint = readJsonFile(resolvedPath);
1255
+ const errors = validateRuntimeEventExportScannerCheckpoint(checkpoint);
1256
+ if (errors.length > 0) {
1257
+ throw new Error(`runtime event export scanner checkpoint is invalid: ${errors.join("; ")}`);
1258
+ }
1259
+ return structuredClone(checkpoint);
1260
+ }
1261
+ function writeRuntimeEventExportScannerCheckpoint(checkpointPath, checkpoint) {
1262
+ const resolvedPath = path.resolve(checkpointPath);
1263
+ const errors = validateRuntimeEventExportScannerCheckpoint(checkpoint);
1264
+ if (errors.length > 0) {
1265
+ throw new Error(`runtime event export scanner checkpoint is invalid: ${errors.join("; ")}`);
1266
+ }
1267
+ mkdirSync(path.dirname(resolvedPath), { recursive: true });
1268
+ writeFileSync(resolvedPath, canonicalJson(checkpoint), "utf8");
1269
+ }
1270
+ function normalizePositiveInteger(value, fieldName, fallbackValue) {
1271
+ const resolved = value ?? fallbackValue;
1272
+ if (!Number.isInteger(resolved) || resolved <= 0) {
1273
+ throw new Error(`${fieldName} must be a positive integer`);
1274
+ }
1275
+ return resolved;
1276
+ }
1277
+ function normalizeNonNegativeDurationMs(value, fieldName, fallbackValue) {
1278
+ const resolved = value ?? fallbackValue;
1279
+ if (!Number.isInteger(resolved) || resolved < 0) {
1280
+ throw new Error(`${fieldName} must be a non-negative integer`);
1281
+ }
1282
+ return resolved;
1283
+ }
1284
+ function buildRuntimeEventExportScannerBundleCursor(descriptor) {
1285
+ return {
1286
+ exportDigest: descriptor.normalizedEventExport.provenance.exportDigest,
1287
+ exportName: descriptor.manifest.exportName,
1288
+ exportedAt: descriptor.manifest.exportedAt,
1289
+ eventRange: {
1290
+ start: descriptor.normalizedEventExport.range.start,
1291
+ end: descriptor.normalizedEventExport.range.end,
1292
+ count: descriptor.normalizedEventExport.range.count
1293
+ }
1294
+ };
1295
+ }
1296
+ function compareRuntimeEventExportScannerBundleCursor(left, right) {
1297
+ if (left.eventRange.end !== right.eventRange.end) {
1298
+ return left.eventRange.end - right.eventRange.end;
1299
+ }
1300
+ if (left.eventRange.start !== right.eventRange.start) {
1301
+ return left.eventRange.start - right.eventRange.start;
1302
+ }
1303
+ if (left.exportedAt !== right.exportedAt) {
1304
+ return left.exportedAt.localeCompare(right.exportedAt);
1305
+ }
1306
+ return left.exportDigest.localeCompare(right.exportDigest);
1307
+ }
1308
+ function buildRuntimeEventExportScannerHit(bundle, queueEntry) {
1309
+ return {
1310
+ lane: queueEntry.lane,
1311
+ rootDir: queueEntry.rootDir,
1312
+ exportDigest: queueEntry.exportDigest,
1313
+ exportName: queueEntry.exportName,
1314
+ exportedAt: queueEntry.exportedAt,
1315
+ eventRange: {
1316
+ start: queueEntry.eventRange.start,
1317
+ end: queueEntry.eventRange.end,
1318
+ count: queueEntry.eventRange.count
1319
+ },
1320
+ priorityBucket: queueEntry.priorityBucket,
1321
+ priorityScore: queueEntry.priorityScore,
1322
+ priorityReasons: [...queueEntry.priorityReasons],
1323
+ humanLabelCount: queueEntry.humanLabelCount,
1324
+ feedbackCount: queueEntry.feedbackCount,
1325
+ teacherRoles: [...queueEntry.teacherRoles],
1326
+ teacherAuthorities: [...queueEntry.teacherAuthorities],
1327
+ priorityClasses: [...queueEntry.priorityClasses],
1328
+ scopedPrincipalEventCount: queueEntry.scopedPrincipalEventCount,
1329
+ supersedingPrincipalEventCount: queueEntry.supersedingPrincipalEventCount,
1330
+ staleHistory: queueEntry.staleHistory,
1331
+ ageMsFromLatest: queueEntry.ageMsFromLatest,
1332
+ normalizedEventExport: structuredClone(bundle.descriptor.normalizedEventExport)
1333
+ };
1334
+ }
1335
+ function scannerTeacherRoleWeight(role) {
1336
+ switch (role) {
1337
+ case "principal":
1338
+ return 4;
1339
+ case "admin":
1340
+ return 3;
1341
+ case "operator":
1342
+ return 1.5;
1343
+ case "user":
1344
+ case "assistant":
1345
+ case "system":
1346
+ default:
1347
+ return 0;
1348
+ }
1349
+ }
1350
+ function scannerTeacherAuthorityWeight(authority) {
1351
+ switch (authority) {
1352
+ case "binding":
1353
+ return 4;
1354
+ case "primary_human":
1355
+ return 3;
1356
+ case "high":
1357
+ return 2;
1358
+ case "normal":
1359
+ return 0.5;
1360
+ case "background":
1361
+ default:
1362
+ return 0;
1363
+ }
1364
+ }
1365
+ function scannerPrincipalPriorityWeight(priorityClass) {
1366
+ switch (priorityClass) {
1367
+ case "critical":
1368
+ return 4;
1369
+ case "high":
1370
+ return 3;
1371
+ case "normal":
1372
+ return 1;
1373
+ case "low":
1374
+ default:
1375
+ return 0;
1376
+ }
1377
+ }
1378
+ function scannerFeedbackPriorityWeight(kind) {
1379
+ switch (kind) {
1380
+ case "correction":
1381
+ return 5;
1382
+ case "teaching":
1383
+ return 4;
1384
+ case "approval":
1385
+ return 2;
1386
+ case "suppression":
1387
+ default:
1388
+ return 1;
1389
+ }
1390
+ }
1391
+ function isPrincipalHeavyScannerBundle(learningSurface) {
1392
+ return (learningSurface.principalSummary.teacherRoles.some((role) => role === "principal" || role === "admin") ||
1393
+ learningSurface.principalSummary.teacherAuthorities.some((authority) => authority === "binding" || authority === "primary_human" || authority === "high") ||
1394
+ learningSurface.principalSummary.priorityClasses.some((priorityClass) => priorityClass === "critical" || priorityClass === "high") ||
1395
+ learningSurface.principalSummary.supersedingEventCount > 0);
1396
+ }
1397
+ function buildRuntimeEventExportScannerQueueEntry(input) {
1398
+ const learningSurface = input.bundle.descriptor.normalizedEventExport.provenance.learningSurface;
1399
+ const ageMsFromLatest = input.latestExportedAt === null ? null : Math.max(0, Date.parse(input.latestExportedAt) - Date.parse(input.bundle.cursor.exportedAt));
1400
+ const recencyScore = ageMsFromLatest === null ? 0 : roundMetric(clamp(3 - ageMsFromLatest / 86_400_000, 0, 3));
1401
+ const feedbackScore = input.bundle.descriptor.normalizedEventExport.feedbackEvents.reduce((sum, event) => sum + scannerFeedbackPriorityWeight(event.kind), 0);
1402
+ const roleScore = learningSurface.principalSummary.teacherRoles.reduce((sum, role) => sum + scannerTeacherRoleWeight(role), 0);
1403
+ const authorityScore = learningSurface.principalSummary.teacherAuthorities.reduce((sum, authority) => sum + scannerTeacherAuthorityWeight(authority), 0);
1404
+ const priorityClassScore = learningSurface.principalSummary.priorityClasses.reduce((sum, priorityClass) => sum + scannerPrincipalPriorityWeight(priorityClass), 0);
1405
+ const principalHeavy = isPrincipalHeavyScannerBundle(learningSurface);
1406
+ const staleHistory = input.staleBefore !== null && input.bundle.cursor.exportedAt < input.staleBefore;
1407
+ const priorityBucket = staleHistory
1408
+ ? "stale_history"
1409
+ : input.lane === "live"
1410
+ ? "live"
1411
+ : principalHeavy
1412
+ ? "principal_backfill"
1413
+ : "backfill";
1414
+ const priorityScore = roundMetric(feedbackScore +
1415
+ roleScore +
1416
+ authorityScore +
1417
+ priorityClassScore +
1418
+ learningSurface.principalSummary.scopedEventCount +
1419
+ learningSurface.principalSummary.supersedingEventCount * 3 +
1420
+ learningSurface.labelHarvest.humanLabels +
1421
+ recencyScore +
1422
+ (principalHeavy ? 6 : 0));
1423
+ const priorityReasons = [];
1424
+ if (input.lane === "live") {
1425
+ priorityReasons.push("live_tail");
1426
+ }
1427
+ if (principalHeavy) {
1428
+ priorityReasons.push("principal_heavy");
1429
+ }
1430
+ for (const role of learningSurface.principalSummary.teacherRoles) {
1431
+ if (role === "principal" || role === "admin") {
1432
+ priorityReasons.push(`teacher_role=${role}`);
1433
+ }
1434
+ }
1435
+ for (const authority of learningSurface.principalSummary.teacherAuthorities) {
1436
+ if (authority === "binding" || authority === "primary_human" || authority === "high") {
1437
+ priorityReasons.push(`teacher_authority=${authority}`);
1438
+ }
1439
+ }
1440
+ for (const priorityClass of learningSurface.principalSummary.priorityClasses) {
1441
+ if (priorityClass === "critical" || priorityClass === "high") {
1442
+ priorityReasons.push(`priority_class=${priorityClass}`);
1443
+ }
1444
+ }
1445
+ if (learningSurface.principalSummary.supersedingEventCount > 0) {
1446
+ priorityReasons.push(`superseding_events=${learningSurface.principalSummary.supersedingEventCount}`);
1447
+ }
1448
+ if (learningSurface.labelHarvest.humanLabels > 0) {
1449
+ priorityReasons.push(`human_labels=${learningSurface.labelHarvest.humanLabels}`);
1450
+ }
1451
+ if (staleHistory) {
1452
+ priorityReasons.push("stale_history_floor");
1453
+ }
1454
+ return {
1455
+ lane: input.lane,
1456
+ rootDir: input.bundle.descriptor.rootDir,
1457
+ exportDigest: input.bundle.cursor.exportDigest,
1458
+ exportName: input.bundle.cursor.exportName,
1459
+ exportedAt: input.bundle.cursor.exportedAt,
1460
+ eventRange: {
1461
+ start: input.bundle.cursor.eventRange.start,
1462
+ end: input.bundle.cursor.eventRange.end,
1463
+ count: input.bundle.cursor.eventRange.count
1464
+ },
1465
+ priorityBucket,
1466
+ priorityScore,
1467
+ priorityReasons,
1468
+ humanLabelCount: learningSurface.labelHarvest.humanLabels,
1469
+ feedbackCount: input.bundle.descriptor.normalizedEventExport.feedbackEvents.length,
1470
+ teacherRoles: [...learningSurface.principalSummary.teacherRoles],
1471
+ teacherAuthorities: [...learningSurface.principalSummary.teacherAuthorities],
1472
+ priorityClasses: [...learningSurface.principalSummary.priorityClasses],
1473
+ scopedPrincipalEventCount: learningSurface.principalSummary.scopedEventCount,
1474
+ supersedingPrincipalEventCount: learningSurface.principalSummary.supersedingEventCount,
1475
+ staleHistory,
1476
+ ageMsFromLatest
1477
+ };
1478
+ }
1479
+ function compareRuntimeEventExportScannerQueueEntry(left, right) {
1480
+ const bucketOrder = {
1481
+ live: 0,
1482
+ principal_backfill: 1,
1483
+ backfill: 2,
1484
+ stale_history: 3
1485
+ };
1486
+ if (bucketOrder[left.priorityBucket] !== bucketOrder[right.priorityBucket]) {
1487
+ return bucketOrder[left.priorityBucket] - bucketOrder[right.priorityBucket];
1488
+ }
1489
+ if (right.priorityScore !== left.priorityScore) {
1490
+ return right.priorityScore - left.priorityScore;
1491
+ }
1492
+ if (right.eventRange.end !== left.eventRange.end) {
1493
+ return right.eventRange.end - left.eventRange.end;
1494
+ }
1495
+ if (right.eventRange.start !== left.eventRange.start) {
1496
+ return right.eventRange.start - left.eventRange.start;
1497
+ }
1498
+ if (right.exportedAt !== left.exportedAt) {
1499
+ return right.exportedAt.localeCompare(left.exportedAt);
1500
+ }
1501
+ return right.exportDigest.localeCompare(left.exportDigest);
1502
+ }
1503
+ function listRuntimeEventExportBundleRoots(scanRoot) {
1504
+ if (!existsSync(scanRoot)) {
1505
+ return [];
1506
+ }
1507
+ return readdirSync(scanRoot, { withFileTypes: true })
1508
+ .filter((entry) => entry.isDirectory())
1509
+ .map((entry) => path.join(scanRoot, entry.name))
1510
+ .sort((left, right) => left.localeCompare(right));
1511
+ }
1512
+ function discoverRuntimeEventExportBundles(scanRoot) {
1513
+ const loaded = [];
1514
+ const invalidBundles = [];
1515
+ for (const rootDir of listRuntimeEventExportBundleRoots(scanRoot)) {
1516
+ try {
1517
+ const descriptor = loadRuntimeEventExportBundle(rootDir);
1518
+ loaded.push({
1519
+ descriptor,
1520
+ cursor: buildRuntimeEventExportScannerBundleCursor(descriptor)
1521
+ });
1522
+ }
1523
+ catch (error) {
1524
+ invalidBundles.push({
1525
+ rootDir,
1526
+ error: toErrorMessage(error)
1527
+ });
1528
+ }
1529
+ }
1530
+ loaded.sort((left, right) => compareRuntimeEventExportScannerBundleCursor(left.cursor, right.cursor));
1531
+ const deduped = [];
1532
+ const seenDigests = new Set();
1533
+ const duplicateExportDigests = [];
1534
+ for (const bundle of loaded) {
1535
+ if (seenDigests.has(bundle.cursor.exportDigest)) {
1536
+ duplicateExportDigests.push(bundle.cursor.exportDigest);
1537
+ continue;
1538
+ }
1539
+ seenDigests.add(bundle.cursor.exportDigest);
1540
+ deduped.push(bundle);
1541
+ }
1542
+ return {
1543
+ bundles: deduped,
1544
+ invalidBundles,
1545
+ duplicateExportDigests: [...new Set(duplicateExportDigests)]
1546
+ };
1547
+ }
1548
+ function delayRuntimeEventExportScannerPoll(ms) {
1549
+ return new Promise((resolve) => {
1550
+ setTimeout(resolve, ms);
1551
+ });
1552
+ }
1553
+ export class RuntimeEventExportScanner {
1554
+ scanRoot;
1555
+ checkpointPath;
1556
+ liveTailBundles;
1557
+ backfillBundlesPerPass;
1558
+ staleHistoryMs;
1559
+ checkpoint;
1560
+ constructor(input) {
1561
+ this.scanRoot = path.resolve(normalizeNonEmptyString(input.scanRoot, "scanRoot"));
1562
+ this.checkpointPath = path.resolve(input.checkpointPath ?? path.join(this.scanRoot, DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_CHECKPOINT_BASENAME));
1563
+ this.liveTailBundles = normalizePositiveInteger(input.liveTailBundles, "liveTailBundles", DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_LIVE_TAIL_BUNDLES);
1564
+ this.backfillBundlesPerPass = normalizePositiveInteger(input.backfillBundlesPerPass, "backfillBundlesPerPass", DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_BACKFILL_BUNDLES_PER_PASS);
1565
+ this.staleHistoryMs = normalizeNonNegativeDurationMs(input.staleHistoryMs, "staleHistoryMs", DEFAULT_RUNTIME_EVENT_EXPORT_SCANNER_STALE_HISTORY_MS);
1566
+ this.checkpoint = existsSync(this.checkpointPath)
1567
+ ? loadRuntimeEventExportScannerCheckpoint(this.checkpointPath)
1568
+ : createRuntimeEventExportScannerCheckpoint({ scanRoot: this.scanRoot });
1569
+ if (this.checkpoint.scanRoot !== this.scanRoot) {
1570
+ throw new Error(`runtime event export scanner checkpoint scanRoot mismatch: checkpoint=${this.checkpoint.scanRoot} scanner=${this.scanRoot}`);
1571
+ }
1572
+ }
1573
+ snapshot() {
1574
+ return structuredClone(this.checkpoint);
1575
+ }
1576
+ scanOnce(options = {}) {
1577
+ const scannedAt = normalizeIsoTimestamp(options.scannedAt, "scannedAt", new Date().toISOString());
1578
+ const discovered = discoverRuntimeEventExportBundles(this.scanRoot);
1579
+ const processedDigests = new Set(this.checkpoint.processedExportDigests);
1580
+ const latestExportedAt = discovered.bundles[discovered.bundles.length - 1]?.cursor.exportedAt ?? null;
1581
+ const staleBefore = latestExportedAt === null ? null : new Date(Date.parse(latestExportedAt) - this.staleHistoryMs).toISOString();
1582
+ const staleSkipped = new Set();
1583
+ const isFreshEnough = (bundle) => {
1584
+ if (staleBefore === null) {
1585
+ return true;
1586
+ }
1587
+ return bundle.cursor.exportedAt >= staleBefore;
1588
+ };
1589
+ const live = this.checkpoint.live.after === null
1590
+ ? discovered.bundles.filter((bundle) => !processedDigests.has(bundle.cursor.exportDigest) && isFreshEnough(bundle)).slice(-this.liveTailBundles)
1591
+ : discovered.bundles
1592
+ .filter((bundle) => !processedDigests.has(bundle.cursor.exportDigest) &&
1593
+ compareRuntimeEventExportScannerBundleCursor(bundle.cursor, this.checkpoint.live.after) > 0 &&
1594
+ isFreshEnough(bundle))
1595
+ .slice(0, this.liveTailBundles);
1596
+ const liveQueue = live.map((bundle) => buildRuntimeEventExportScannerQueueEntry({
1597
+ lane: "live",
1598
+ bundle,
1599
+ latestExportedAt,
1600
+ staleBefore
1601
+ }));
1602
+ const nextLiveAfter = live.length > 0 ? live[live.length - 1]?.cursor ?? null : this.checkpoint.live.after;
1603
+ const backfillFrontier = nextLiveAfter;
1604
+ const liveDigests = new Set(live.map((bundle) => bundle.cursor.exportDigest));
1605
+ const prioritizedBackfillCandidates = [];
1606
+ const staleHistoryQueue = [];
1607
+ for (const bundle of discovered.bundles) {
1608
+ if (processedDigests.has(bundle.cursor.exportDigest) || liveDigests.has(bundle.cursor.exportDigest)) {
1609
+ continue;
1610
+ }
1611
+ const queueEntry = buildRuntimeEventExportScannerQueueEntry({
1612
+ lane: "backfill",
1613
+ bundle,
1614
+ latestExportedAt,
1615
+ staleBefore
1616
+ });
1617
+ if (queueEntry.staleHistory) {
1618
+ staleHistoryQueue.push(queueEntry);
1619
+ staleSkipped.add(bundle.cursor.exportDigest);
1620
+ processedDigests.add(bundle.cursor.exportDigest);
1621
+ continue;
1622
+ }
1623
+ if (backfillFrontier !== null &&
1624
+ compareRuntimeEventExportScannerBundleCursor(bundle.cursor, backfillFrontier) < 0) {
1625
+ prioritizedBackfillCandidates.push({
1626
+ bundle,
1627
+ queueEntry
1628
+ });
1629
+ }
1630
+ }
1631
+ prioritizedBackfillCandidates.sort((left, right) => compareRuntimeEventExportScannerQueueEntry(left.queueEntry, right.queueEntry));
1632
+ const selectedBackfillCandidates = prioritizedBackfillCandidates.slice(0, this.backfillBundlesPerPass);
1633
+ const backfill = selectedBackfillCandidates.map((candidate) => candidate.bundle);
1634
+ for (const bundle of [...live, ...backfill]) {
1635
+ processedDigests.add(bundle.cursor.exportDigest);
1636
+ }
1637
+ const backfillQueue = prioritizedBackfillCandidates.map((candidate) => candidate.queueEntry);
1638
+ const selectedLive = liveQueue.map((queueEntry, index) => buildRuntimeEventExportScannerHit(live[index], queueEntry));
1639
+ const selectedBackfill = selectedBackfillCandidates.map((candidate) => buildRuntimeEventExportScannerHit(candidate.bundle, candidate.queueEntry));
1640
+ const nextBackfillBefore = backfill.length > 0
1641
+ ? [...backfill].sort((left, right) => compareRuntimeEventExportScannerBundleCursor(left.cursor, right.cursor))[0]?.cursor ?? null
1642
+ : this.checkpoint.backfill.before;
1643
+ const exhausted = backfillFrontier === null
1644
+ ? true
1645
+ : !prioritizedBackfillCandidates.some((candidate) => !processedDigests.has(candidate.bundle.cursor.exportDigest));
1646
+ this.checkpoint = {
1647
+ contract: RUNTIME_EVENT_EXPORT_SCANNER_CHECKPOINT_CONTRACT,
1648
+ runtimeOwner: "openclaw",
1649
+ scanRoot: this.scanRoot,
1650
+ updatedAt: scannedAt,
1651
+ live: {
1652
+ after: nextLiveAfter === null ? null : { ...nextLiveAfter, eventRange: { ...nextLiveAfter.eventRange } }
1653
+ },
1654
+ backfill: {
1655
+ before: nextBackfillBefore === null ? null : { ...nextBackfillBefore, eventRange: { ...nextBackfillBefore.eventRange } },
1656
+ exhausted,
1657
+ staleBefore
1658
+ },
1659
+ processedExportDigests: [...processedDigests],
1660
+ stats: {
1661
+ scanPasses: this.checkpoint.stats.scanPasses + 1,
1662
+ liveBundlesScanned: this.checkpoint.stats.liveBundlesScanned + selectedLive.length,
1663
+ backfillBundlesScanned: this.checkpoint.stats.backfillBundlesScanned + selectedBackfill.length,
1664
+ duplicateBundlesSkipped: this.checkpoint.stats.duplicateBundlesSkipped + discovered.duplicateExportDigests.length,
1665
+ staleBundlesSkipped: this.checkpoint.stats.staleBundlesSkipped + staleSkipped.size,
1666
+ invalidBundlesSkipped: this.checkpoint.stats.invalidBundlesSkipped + discovered.invalidBundles.length
1667
+ }
1668
+ };
1669
+ writeRuntimeEventExportScannerCheckpoint(this.checkpointPath, this.checkpoint);
1670
+ return {
1671
+ runtimeOwner: "openclaw",
1672
+ scanRoot: this.scanRoot,
1673
+ checkpointPath: this.checkpointPath,
1674
+ scannedAt,
1675
+ live: selectedLive,
1676
+ backfill: selectedBackfill,
1677
+ selected: [...selectedLive, ...selectedBackfill],
1678
+ queue: {
1679
+ ageFloor: {
1680
+ newestExportedAt: latestExportedAt,
1681
+ staleBefore,
1682
+ staleHistoryMs: this.staleHistoryMs
1683
+ },
1684
+ live: liveQueue,
1685
+ backfill: backfillQueue,
1686
+ staleHistory: staleHistoryQueue.sort(compareRuntimeEventExportScannerQueueEntry)
1687
+ },
1688
+ duplicateExportDigests: discovered.duplicateExportDigests,
1689
+ staleSkippedExportDigests: [...staleSkipped],
1690
+ invalidBundles: discovered.invalidBundles,
1691
+ idle: selectedLive.length === 0 && selectedBackfill.length === 0,
1692
+ checkpoint: this.snapshot()
1693
+ };
1694
+ }
1695
+ async runLoop(options = {}) {
1696
+ const pollIntervalMs = normalizeNonNegativeDurationMs(options.pollIntervalMs, "pollIntervalMs", 0);
1697
+ const maxPasses = normalizePositiveInteger(options.maxPasses, "maxPasses", 1);
1698
+ const stopWhenIdle = options.stopWhenIdle !== false;
1699
+ let passCount = 0;
1700
+ let liveBundlesScanned = 0;
1701
+ let backfillBundlesScanned = 0;
1702
+ let lastScan = null;
1703
+ let stoppedReason = "max_passes";
1704
+ while (passCount < maxPasses) {
1705
+ if (options.signal?.aborted) {
1706
+ stoppedReason = "aborted";
1707
+ break;
1708
+ }
1709
+ lastScan = this.scanOnce();
1710
+ passCount += 1;
1711
+ liveBundlesScanned += lastScan.live.length;
1712
+ backfillBundlesScanned += lastScan.backfill.length;
1713
+ if (options.onPass !== undefined) {
1714
+ await options.onPass(lastScan);
1715
+ }
1716
+ if (stopWhenIdle && lastScan.idle) {
1717
+ stoppedReason = "idle";
1718
+ break;
1719
+ }
1720
+ if (passCount >= maxPasses) {
1721
+ stoppedReason = "max_passes";
1722
+ break;
1723
+ }
1724
+ if (pollIntervalMs > 0) {
1725
+ await delayRuntimeEventExportScannerPoll(pollIntervalMs);
1726
+ }
1727
+ }
1728
+ return {
1729
+ runtimeOwner: "openclaw",
1730
+ passCount,
1731
+ liveBundlesScanned,
1732
+ backfillBundlesScanned,
1733
+ stoppedReason,
1734
+ lastScan,
1735
+ checkpoint: this.snapshot()
1736
+ };
1737
+ }
1738
+ }
1739
+ export function createRuntimeEventExportScanner(input) {
1740
+ return new RuntimeEventExportScanner(input);
1741
+ }
776
1742
  function normalizeNonEmptyString(value, fieldName) {
777
1743
  if (typeof value !== "string" || value.trim().length === 0) {
778
1744
  throw new Error(`${fieldName} is required`);
@@ -836,23 +1802,134 @@ function isFeedbackKind(value) {
836
1802
  function isPresent(value) {
837
1803
  return value !== null;
838
1804
  }
839
- export function classifyFeedbackKind(content) {
840
- const normalized = content.trim().toLowerCase();
841
- if (normalized.length === 0) {
842
- return "teaching";
1805
+ const KNOWN_SCANNER_REALITY_PRINCIPALS = {
1806
+ bihua: {
1807
+ teacherIdentity: "bihua",
1808
+ teacherRole: "principal",
1809
+ teacherAuthority: "binding",
1810
+ priorityClass: "critical"
1811
+ },
1812
+ jonathan: {
1813
+ teacherIdentity: "jonathan",
1814
+ teacherRole: "admin",
1815
+ teacherAuthority: "high",
1816
+ priorityClass: "high"
1817
+ },
1818
+ "jonathan gu": {
1819
+ teacherIdentity: "jonathan",
1820
+ teacherRole: "admin",
1821
+ teacherAuthority: "high",
1822
+ priorityClass: "high"
843
1823
  }
844
- if (/\b(stop|suppress|mute|silence|pause|don't send|do not send)\b/u.test(normalized)) {
845
- return "suppression";
1824
+ };
1825
+ function normalizePrincipalPriorityHint(value, fieldName) {
1826
+ if (value === undefined || value === null) {
1827
+ return undefined;
846
1828
  }
847
- if (/\b(approved|approve|looks good|ship it|exactly right|that works)\b/u.test(normalized)) {
848
- return "approval";
1829
+ if (value === "critical" || value === "high" || value === "normal" || value === "low") {
1830
+ return value;
849
1831
  }
850
- if (/\b(wrong|incorrect|correction|not right|do this instead|not x[, ]+y)\b/u.test(normalized) ||
851
- normalized.startsWith("no") ||
852
- normalized.startsWith("wrong")) {
853
- return "correction";
1832
+ throw new Error(`${fieldName} must be critical, high, normal, or low`);
1833
+ }
1834
+ function normalizePrincipalActorName(value) {
1835
+ const normalized = normalizeOptionalString(value);
1836
+ return normalized === undefined ? undefined : normalized.toLowerCase().replace(/\s+/gu, " ");
1837
+ }
1838
+ function slugifyPrincipalIdentityFragment(value) {
1839
+ return value
1840
+ .toLowerCase()
1841
+ .replace(/[^a-z0-9]+/gu, "-")
1842
+ .replace(/^-+|-+$/gu, "");
1843
+ }
1844
+ function buildRuntimePrincipalScope(input) {
1845
+ const scopeKey = [
1846
+ `profile:${input.profileSelector}`,
1847
+ input.profileId === undefined ? null : `profile_id:${input.profileId}`,
1848
+ `session:${input.sessionId}`,
1849
+ input.userId === undefined ? null : `user:${input.userId}`,
1850
+ input.interactionId === undefined ? null : `interaction:${input.interactionId}`,
1851
+ input.messageId === undefined ? null : `message:${input.messageId}`
1852
+ ]
1853
+ .filter(isPresent)
1854
+ .join("|");
1855
+ if (input.messageId !== undefined) {
1856
+ return {
1857
+ kind: "message",
1858
+ profileSelector: input.profileSelector,
1859
+ sessionId: input.sessionId,
1860
+ ...(input.interactionId === undefined ? {} : { interactionId: input.interactionId }),
1861
+ messageId: input.messageId,
1862
+ scopeKey
1863
+ };
1864
+ }
1865
+ if (input.interactionId !== undefined) {
1866
+ return {
1867
+ kind: "interaction",
1868
+ profileSelector: input.profileSelector,
1869
+ sessionId: input.sessionId,
1870
+ interactionId: input.interactionId,
1871
+ scopeKey
1872
+ };
1873
+ }
1874
+ return {
1875
+ kind: "session",
1876
+ profileSelector: input.profileSelector,
1877
+ sessionId: input.sessionId,
1878
+ scopeKey
1879
+ };
1880
+ }
1881
+ function buildRuntimeSelfPrincipal(turn) {
1882
+ const profileSelector = normalizeRuntimeProfileSelector(turn.profileSelector, "profileSelector");
1883
+ const profileId = normalizeOptionalString(turn.profileId);
1884
+ const userId = normalizeOptionalString(turn.userId);
1885
+ return {
1886
+ teacherIdentity: "openclaw/self",
1887
+ teacherRole: "assistant",
1888
+ teacherAuthority: "background",
1889
+ priorityClass: "low",
1890
+ principalScope: buildRuntimePrincipalScope({
1891
+ profileSelector,
1892
+ ...(profileId === undefined ? {} : { profileId }),
1893
+ sessionId: turn.sessionId,
1894
+ ...(userId === undefined ? {} : { userId })
1895
+ })
1896
+ };
1897
+ }
1898
+ function resolveRuntimeFeedbackPrincipal(input) {
1899
+ const actorName = normalizePrincipalActorName(input.feedback.actorName);
1900
+ const knownPrincipal = actorName === undefined ? undefined : KNOWN_SCANNER_REALITY_PRINCIPALS[actorName];
1901
+ const profileSelector = normalizeRuntimeProfileSelector(input.turn.profileSelector, "profileSelector");
1902
+ const profileId = normalizeOptionalString(input.turn.profileId);
1903
+ const userId = normalizeOptionalString(input.turn.userId);
1904
+ const priorityHint = normalizePrincipalPriorityHint(input.feedback.priorityHint, "feedback.priorityHint");
1905
+ const userIdentityFragment = userId === undefined ? undefined : slugifyPrincipalIdentityFragment(userId);
1906
+ const actorIdentityFragment = actorName === undefined ? undefined : slugifyPrincipalIdentityFragment(actorName);
1907
+ const teacherIdentity = knownPrincipal?.teacherIdentity ??
1908
+ (userIdentityFragment === undefined || userIdentityFragment.length === 0
1909
+ ? actorIdentityFragment === undefined || actorIdentityFragment.length === 0
1910
+ ? undefined
1911
+ : `scanner/actor/${actorIdentityFragment}`
1912
+ : `scanner/user/${userIdentityFragment}`);
1913
+ if (teacherIdentity === undefined) {
1914
+ return undefined;
854
1915
  }
855
- return "teaching";
1916
+ return {
1917
+ teacherIdentity,
1918
+ teacherRole: knownPrincipal?.teacherRole ?? "user",
1919
+ teacherAuthority: knownPrincipal?.teacherAuthority ?? "normal",
1920
+ priorityClass: priorityHint ?? knownPrincipal?.priorityClass ?? "normal",
1921
+ principalScope: buildRuntimePrincipalScope({
1922
+ profileSelector,
1923
+ ...(profileId === undefined ? {} : { profileId }),
1924
+ sessionId: input.turn.sessionId,
1925
+ ...(userId === undefined ? {} : { userId }),
1926
+ ...(input.relatedInteractionId === undefined ? {} : { interactionId: input.relatedInteractionId }),
1927
+ ...(input.messageId === undefined ? {} : { messageId: input.messageId })
1928
+ })
1929
+ };
1930
+ }
1931
+ export function classifyFeedbackKind(content) {
1932
+ return classifyFeedbackSignalContent(content)?.kind ?? "teaching";
856
1933
  }
857
1934
  export function formatPromptContext(compileResponse) {
858
1935
  const lines = [
@@ -913,6 +1990,12 @@ function classifyCompileFailure(error, activationRoot) {
913
1990
  function uniqueNotes(notes) {
914
1991
  return [...new Set(notes.filter((note) => note.length > 0))];
915
1992
  }
1993
+ function roundMetric(value) {
1994
+ return Math.round(value * 100) / 100;
1995
+ }
1996
+ function clamp(value, min, max) {
1997
+ return Math.min(max, Math.max(min, value));
1998
+ }
916
1999
  function clampInteger(value, minimum, maximum) {
917
2000
  return Math.min(maximum, Math.max(minimum, Math.round(value)));
918
2001
  }
@@ -1192,6 +2275,97 @@ function readDiagnosticNoteList(notes, prefix) {
1192
2275
  .map((entry) => entry.trim())
1193
2276
  .filter((entry) => entry.length > 0);
1194
2277
  }
2278
+ function parseDiagnosticInteger(value) {
2279
+ if (value === null) {
2280
+ return null;
2281
+ }
2282
+ const parsed = Number.parseInt(value, 10);
2283
+ return Number.isInteger(parsed) ? parsed : null;
2284
+ }
2285
+ function parseStructuralBudgetStrategy(value) {
2286
+ return value === "fixed_v1" || value === "empirical_v1" ? value : null;
2287
+ }
2288
+ function summarizeStructuralDecisionFromNotes(notes) {
2289
+ const requestedBudgetStrategy = parseStructuralBudgetStrategy(readDiagnosticNoteValue(notes, "requested_budget_strategy="));
2290
+ const resolvedBudgetStrategy = parseStructuralBudgetStrategy(readDiagnosticNoteValue(notes, "resolved_budget_strategy="));
2291
+ const resolvedMaxContextBlocks = parseDiagnosticInteger(readDiagnosticNoteValue(notes, "resolved_max_context_blocks="));
2292
+ const requestedMaxContextBlocks = parseDiagnosticInteger(readDiagnosticNoteValue(notes, "requested_max_context_blocks="));
2293
+ const structuralBudgetSource = readDiagnosticNoteValue(notes, "structural_budget_source=");
2294
+ switch (structuralBudgetSource) {
2295
+ case "caller_override":
2296
+ return {
2297
+ origin: "manual_caller_shape",
2298
+ basis: "caller_override",
2299
+ requestedBudgetStrategy,
2300
+ resolvedBudgetStrategy,
2301
+ resolvedMaxContextBlocks,
2302
+ detail: `manual caller shaping fixed the serve-path structural budget at ${requestedMaxContextBlocks ?? resolvedMaxContextBlocks ?? "unknown"} blocks; empirical and default-path control were bypassed`
2303
+ };
2304
+ case "compile_structural_signals_empirical_v1":
2305
+ return {
2306
+ origin: "empirical_control",
2307
+ basis: "compile_structural_signals",
2308
+ requestedBudgetStrategy,
2309
+ resolvedBudgetStrategy,
2310
+ resolvedMaxContextBlocks,
2311
+ detail: `empirical control resolved the serve-path structural budget to ${resolvedMaxContextBlocks ?? "unknown"} blocks from prior compile structural signals; no manual caller shaping was applied`
2312
+ };
2313
+ case "graph_evolution_empirical_v1":
2314
+ return {
2315
+ origin: "empirical_control",
2316
+ basis: "graph_evolution",
2317
+ requestedBudgetStrategy,
2318
+ resolvedBudgetStrategy,
2319
+ resolvedMaxContextBlocks,
2320
+ detail: `empirical control resolved the serve-path structural budget to ${resolvedMaxContextBlocks ?? "unknown"} blocks from active-pack graph evolution evidence; no manual caller shaping was applied`
2321
+ };
2322
+ case "fixed_default":
2323
+ return {
2324
+ origin: "default_path_control",
2325
+ basis: "fixed_default",
2326
+ requestedBudgetStrategy,
2327
+ resolvedBudgetStrategy,
2328
+ resolvedMaxContextBlocks,
2329
+ detail: `default-path fixed-budget control set the serve-path structural budget to ${resolvedMaxContextBlocks ?? "unknown"} blocks because empirical control was not requested`
2330
+ };
2331
+ case "fixed_fallback":
2332
+ return {
2333
+ origin: "default_path_control",
2334
+ basis: "fixed_fallback",
2335
+ requestedBudgetStrategy,
2336
+ resolvedBudgetStrategy,
2337
+ resolvedMaxContextBlocks,
2338
+ detail: `default-path fixed-budget fallback set the serve-path structural budget to ${resolvedMaxContextBlocks ?? "unknown"} blocks; no manual caller shaping was applied`
2339
+ };
2340
+ case "no_evidence_fallback":
2341
+ return {
2342
+ origin: "default_path_control",
2343
+ basis: "no_evidence_fallback",
2344
+ requestedBudgetStrategy,
2345
+ resolvedBudgetStrategy,
2346
+ resolvedMaxContextBlocks,
2347
+ detail: `default-path fixed-budget fallback kept the serve-path structural budget at ${resolvedMaxContextBlocks ?? "unknown"} blocks because graph-evolution evidence was absent`
2348
+ };
2349
+ case "no_compile_signal_evidence_fallback":
2350
+ return {
2351
+ origin: "default_path_control",
2352
+ basis: "no_compile_signal_evidence_fallback",
2353
+ requestedBudgetStrategy,
2354
+ resolvedBudgetStrategy,
2355
+ resolvedMaxContextBlocks,
2356
+ detail: `default-path fixed-budget fallback kept the serve-path structural budget at ${resolvedMaxContextBlocks ?? "unknown"} blocks because compile-signal evidence was absent`
2357
+ };
2358
+ default:
2359
+ return {
2360
+ origin: "unknown",
2361
+ basis: "unknown",
2362
+ requestedBudgetStrategy,
2363
+ resolvedBudgetStrategy,
2364
+ resolvedMaxContextBlocks,
2365
+ detail: "structural decision attribution is unavailable from the compile notes"
2366
+ };
2367
+ }
2368
+ }
1195
2369
  function sortedUniqueStrings(values) {
1196
2370
  return [...new Set(values.filter((value) => value.length > 0))].sort((left, right) => left.localeCompare(right));
1197
2371
  }
@@ -1527,11 +2701,14 @@ export function bootstrapRuntimeAttach(input) {
1527
2701
  });
1528
2702
  const currentProfile = describeCurrentProfileBrainStatus({
1529
2703
  activationRoot,
1530
- updatedAt: activatedAt
2704
+ updatedAt: activatedAt,
2705
+ ...(input.brainAttachmentPolicy !== undefined ? { brainAttachmentPolicy: input.brainAttachmentPolicy } : {}),
2706
+ ...(input.profileId !== undefined ? { profileId: input.profileId } : {})
1531
2707
  });
1532
2708
  return {
1533
2709
  runtimeOwner: "openclaw",
1534
2710
  profileSelector,
2711
+ operatorReadScope: "current_profile_only",
1535
2712
  activationRoot,
1536
2713
  packRoot,
1537
2714
  packId: descriptor.manifest.packId,
@@ -1540,12 +2717,80 @@ export function bootstrapRuntimeAttach(input) {
1540
2717
  currentProfile,
1541
2718
  nextSteps: buildBootstrapRuntimeAttachNextSteps({
1542
2719
  activationRoot,
2720
+ profileSelector,
1543
2721
  currentProfile
1544
2722
  })
1545
2723
  };
1546
2724
  }
2725
+ function normalizeFingerprintEntries(values) {
2726
+ return uniqueStringsInOrder((values ?? [])
2727
+ .map((value) => normalizeOptionalString(value))
2728
+ .filter((value) => value !== undefined));
2729
+ }
2730
+ function buildRuntimeContextFingerprint(input) {
2731
+ const promptContextFingerprints = normalizeFingerprintEntries(input.turn.contextFingerprint?.promptContextFingerprints);
2732
+ const workspaceInjectionSurfaceDigest = input.turn.contextFingerprint?.workspaceInjectionSurface === undefined ||
2733
+ input.turn.contextFingerprint?.workspaceInjectionSurface === null
2734
+ ? null
2735
+ : checksumJsonPayload(input.turn.contextFingerprint.workspaceInjectionSurface);
2736
+ const promptContextDigest = promptContextFingerprints.length === 0 && workspaceInjectionSurfaceDigest === null
2737
+ ? null
2738
+ : checksumJsonPayload({
2739
+ promptContextFingerprints,
2740
+ workspaceInjectionSurfaceDigest
2741
+ });
2742
+ const runtimeHints = normalizeFingerprintEntries(input.turn.runtimeHints);
2743
+ const runtimeHintsDigest = runtimeHints.length === 0 ? null : checksumJsonPayload(runtimeHints);
2744
+ const profileId = normalizeOptionalString(input.turn.profileId);
2745
+ const profileLineage = uniqueStringsInOrder([
2746
+ "host:openclaw",
2747
+ `profile:${input.profileSelector}`,
2748
+ profileId === undefined ? undefined : `profile_id:${profileId}`,
2749
+ `attachment_policy:${input.brainAttachmentPolicy}`,
2750
+ ...normalizeFingerprintEntries(input.turn.contextFingerprint?.profileLineage)
2751
+ ].filter((value) => value !== undefined));
2752
+ const sessionLineage = uniqueStringsInOrder([
2753
+ `session:${input.turn.sessionId}`,
2754
+ `channel:${input.turn.channel}`,
2755
+ `source_stream:${input.sourceStream}`,
2756
+ ...normalizeFingerprintEntries(input.turn.contextFingerprint?.sessionLineage)
2757
+ ]);
2758
+ const brainLineage = uniqueStringsInOrder([
2759
+ `brain_status:${input.brainStatus}`,
2760
+ `active_pack:${input.activePackId ?? "none"}`,
2761
+ `router:${input.routerIdentity ?? "none"}`,
2762
+ `used_learned_route_fn:${input.usedLearnedRouteFn === null ? "unknown" : String(input.usedLearnedRouteFn)}`
2763
+ ]);
2764
+ const profileLineageDigest = checksumJsonPayload(profileLineage);
2765
+ const sessionLineageDigest = checksumJsonPayload(sessionLineage);
2766
+ const brainLineageDigest = checksumJsonPayload(brainLineage);
2767
+ return {
2768
+ selectionDigest: input.selectionDigest,
2769
+ promptContextDigest,
2770
+ promptContextFingerprints,
2771
+ workspaceInjectionSurfaceDigest,
2772
+ runtimeHintsDigest,
2773
+ runtimeHints,
2774
+ profileLineageDigest,
2775
+ profileLineage,
2776
+ sessionLineageDigest,
2777
+ sessionLineage,
2778
+ brainLineageDigest,
2779
+ brainLineage,
2780
+ digest: checksumJsonPayload({
2781
+ selectionDigest: input.selectionDigest,
2782
+ promptContextDigest,
2783
+ runtimeHintsDigest,
2784
+ profileLineageDigest,
2785
+ sessionLineageDigest,
2786
+ brainLineageDigest
2787
+ })
2788
+ };
2789
+ }
1547
2790
  function buildRuntimeTurnAttribution(input) {
2791
+ const profileSelector = normalizeRuntimeProfileSelector(input.turn.profileSelector, "profileSelector");
1548
2792
  const brainAttachmentPolicy = normalizeBrainAttachmentPolicy(input.turn.brainAttachmentPolicy);
2793
+ const profileId = normalizeOptionalString(input.turn.profileId) ?? null;
1549
2794
  if (input.compileResult.ok) {
1550
2795
  const notes = input.compileResult.compileResponse.diagnostics.notes;
1551
2796
  const contextAttribution = buildContextAttributionSummary({
@@ -1557,7 +2802,8 @@ function buildRuntimeTurnAttribution(input) {
1557
2802
  });
1558
2803
  return {
1559
2804
  hostRuntimeOwner: "openclaw",
1560
- profileSelector: "current_profile",
2805
+ profileSelector,
2806
+ profileId,
1561
2807
  brainAttachmentPolicy,
1562
2808
  brainStatus: "serving_active_pack",
1563
2809
  activePackId: input.compileResult.activePackId,
@@ -1565,6 +2811,17 @@ function buildRuntimeTurnAttribution(input) {
1565
2811
  routerIdentity: input.compileResult.compileResponse.diagnostics.routerIdentity,
1566
2812
  selectionDigest: input.compileResult.compileResponse.diagnostics.selectionDigest,
1567
2813
  selectionTiers: readDiagnosticNoteValue(notes, "selection_tiers="),
2814
+ contextFingerprint: buildRuntimeContextFingerprint({
2815
+ turn: input.turn,
2816
+ sourceStream: input.sourceStream,
2817
+ profileSelector,
2818
+ brainAttachmentPolicy,
2819
+ brainStatus: "serving_active_pack",
2820
+ activePackId: input.compileResult.activePackId,
2821
+ usedLearnedRouteFn: input.compileResult.compileResponse.diagnostics.usedLearnedRouteFn,
2822
+ routerIdentity: input.compileResult.compileResponse.diagnostics.routerIdentity,
2823
+ selectionDigest: input.compileResult.compileResponse.diagnostics.selectionDigest
2824
+ }),
1568
2825
  contextEvidence: contextAttribution.evidence === "unprobed" ? null : contextAttribution.evidence
1569
2826
  };
1570
2827
  }
@@ -1575,7 +2832,8 @@ function buildRuntimeTurnAttribution(input) {
1575
2832
  });
1576
2833
  return {
1577
2834
  hostRuntimeOwner: "openclaw",
1578
- profileSelector: "current_profile",
2835
+ profileSelector,
2836
+ profileId,
1579
2837
  brainAttachmentPolicy,
1580
2838
  brainStatus: input.compileResult.hardRequirementViolated ? "hard_fail" : "fail_open_static_context",
1581
2839
  activePackId: null,
@@ -1583,6 +2841,17 @@ function buildRuntimeTurnAttribution(input) {
1583
2841
  routerIdentity: null,
1584
2842
  selectionDigest: null,
1585
2843
  selectionTiers: null,
2844
+ contextFingerprint: buildRuntimeContextFingerprint({
2845
+ turn: input.turn,
2846
+ sourceStream: input.sourceStream,
2847
+ profileSelector,
2848
+ brainAttachmentPolicy,
2849
+ brainStatus: input.compileResult.hardRequirementViolated ? "hard_fail" : "fail_open_static_context",
2850
+ activePackId: null,
2851
+ usedLearnedRouteFn: null,
2852
+ routerIdentity: null,
2853
+ selectionDigest: null
2854
+ }),
1586
2855
  contextEvidence: contextAttribution.evidence === "unprobed" ? null : contextAttribution.evidence
1587
2856
  };
1588
2857
  }
@@ -1600,10 +2869,6 @@ function buildCompileInteractionEvent(input) {
1600
2869
  sessionId: input.turn.sessionId,
1601
2870
  source: input.sourceStream
1602
2871
  });
1603
- const attribution = buildRuntimeTurnAttribution({
1604
- turn: input.turn,
1605
- compileResult: input.compileResult
1606
- });
1607
2872
  return createInteractionEvent({
1608
2873
  eventId,
1609
2874
  agentId: input.agentId,
@@ -1617,7 +2882,8 @@ function buildCompileInteractionEvent(input) {
1617
2882
  stream: input.sourceStream
1618
2883
  },
1619
2884
  packId: input.compileResult.compileResponse.packId,
1620
- attribution
2885
+ principal: buildRuntimeSelfPrincipal(input.turn),
2886
+ attribution: input.attribution
1621
2887
  });
1622
2888
  }
1623
2889
  function buildDeliveryInteractionEvent(input) {
@@ -1638,10 +2904,6 @@ function buildDeliveryInteractionEvent(input) {
1638
2904
  sessionId: input.turn.sessionId,
1639
2905
  source: input.sourceStream
1640
2906
  });
1641
- const attribution = buildRuntimeTurnAttribution({
1642
- turn: input.turn,
1643
- compileResult: input.compileResult
1644
- });
1645
2907
  return createInteractionEvent({
1646
2908
  eventId,
1647
2909
  agentId: input.agentId,
@@ -1654,17 +2916,13 @@ function buildDeliveryInteractionEvent(input) {
1654
2916
  runtimeOwner: "openclaw",
1655
2917
  stream: input.sourceStream
1656
2918
  },
1657
- attribution,
2919
+ attribution: input.attribution,
1658
2920
  ...(input.compileResult.ok ? { packId: input.compileResult.compileResponse.packId } : {}),
1659
2921
  ...(messageId !== undefined ? { messageId } : {})
1660
2922
  });
1661
2923
  }
1662
2924
  function buildFeedbackEvents(input) {
1663
2925
  const feedbackItems = input.turn.feedback ?? [];
1664
- const attribution = buildRuntimeTurnAttribution({
1665
- turn: input.turn,
1666
- compileResult: input.compileResult
1667
- });
1668
2926
  return feedbackItems.map((item, index) => {
1669
2927
  if (item === null) {
1670
2928
  throw new Error(`feedback[${index}] must be an object`);
@@ -1688,6 +2946,12 @@ function buildFeedbackEvents(input) {
1688
2946
  source: input.sourceStream
1689
2947
  });
1690
2948
  const relatedInteractionId = normalizeOptionalString(item.relatedInteractionId) ?? input.compileInteraction?.eventId;
2949
+ const principal = resolveRuntimeFeedbackPrincipal({
2950
+ turn: input.turn,
2951
+ feedback: item,
2952
+ ...(relatedInteractionId === undefined ? {} : { relatedInteractionId }),
2953
+ ...(messageId === undefined ? {} : { messageId })
2954
+ });
1691
2955
  return createFeedbackEvent({
1692
2956
  eventId,
1693
2957
  agentId: input.agentId,
@@ -1701,14 +2965,16 @@ function buildFeedbackEvents(input) {
1701
2965
  stream: input.sourceStream
1702
2966
  },
1703
2967
  content,
1704
- attribution,
2968
+ attribution: input.attribution,
1705
2969
  ...(messageId !== undefined ? { messageId } : {}),
2970
+ ...(principal === undefined ? {} : { principal }),
1706
2971
  ...(relatedInteractionId !== undefined ? { relatedInteractionId } : {})
1707
2972
  });
1708
2973
  });
1709
2974
  }
1710
2975
  export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1711
2976
  const agentId = normalizeOptionalString(turn.agentId) ?? process.env.OPENCLAWBRAIN_AGENT_ID ?? DEFAULT_AGENT_ID;
2977
+ const profileSelector = normalizeRuntimeProfileSelector(turn.profileSelector, "profileSelector");
1712
2978
  const sessionId = normalizeNonEmptyString(turn.sessionId, "sessionId");
1713
2979
  const channel = normalizeNonEmptyString(turn.channel, "channel");
1714
2980
  const sourceStream = normalizeOptionalString(turn.sourceStream) ?? `openclaw/runtime/${channel}`;
@@ -1717,12 +2983,19 @@ export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1717
2983
  const normalizedTurn = {
1718
2984
  ...turn,
1719
2985
  agentId,
2986
+ profileSelector,
1720
2987
  channel,
1721
2988
  sessionId
1722
2989
  };
2990
+ const attribution = buildRuntimeTurnAttribution({
2991
+ turn: normalizedTurn,
2992
+ compileResult,
2993
+ sourceStream
2994
+ });
1723
2995
  const compileInteraction = buildCompileInteractionEvent({
1724
2996
  turn: normalizedTurn,
1725
2997
  compileResult,
2998
+ attribution,
1726
2999
  sourceStream,
1727
3000
  nextSequence,
1728
3001
  createdAt: compileCreatedAt,
@@ -1730,6 +3003,7 @@ export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1730
3003
  });
1731
3004
  const feedbackEvents = buildFeedbackEvents({
1732
3005
  turn: normalizedTurn,
3006
+ attribution,
1733
3007
  sourceStream,
1734
3008
  nextSequence,
1735
3009
  defaultCreatedAt: compileCreatedAt,
@@ -1740,6 +3014,7 @@ export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1740
3014
  const deliveryInteraction = buildDeliveryInteractionEvent({
1741
3015
  turn: normalizedTurn,
1742
3016
  compileResult,
3017
+ attribution,
1743
3018
  sourceStream,
1744
3019
  nextSequence,
1745
3020
  defaultCreatedAt: compileCreatedAt,
@@ -1778,21 +3053,33 @@ export function writeRuntimeEventExportBundle(turn, normalizedEventExport) {
1778
3053
  payloadPath: RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT.payload,
1779
3054
  normalizedEventExport
1780
3055
  });
1781
- const manifestPath = path.join(resolvedRoot, RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT.manifest);
1782
- const payloadPath = path.join(resolvedRoot, manifest.payloadPath);
1783
- mkdirSync(path.dirname(payloadPath), { recursive: true });
1784
- writeFileSync(payloadPath, canonicalJson(normalizedEventExport), "utf8");
1785
- writeFileSync(manifestPath, canonicalJson(manifest), "utf8");
1786
- const descriptor = loadRuntimeEventExportBundle(resolvedRoot);
1787
- return {
1788
- ok: true,
1789
- wroteBundle: true,
1790
- normalizedEventExport,
1791
- rootDir: descriptor.rootDir,
1792
- manifestPath: descriptor.manifestPath,
1793
- payloadPath: descriptor.payloadPath,
1794
- manifest: descriptor.manifest
1795
- };
3056
+ return writeNormalizedEventExportBundleFiles({
3057
+ rootDir: resolvedRoot,
3058
+ manifest,
3059
+ normalizedEventExport
3060
+ });
3061
+ }
3062
+ export function writeScannedEventExportBundle(input) {
3063
+ const built = buildNormalizedEventExportFromScannedEvents(input.scannedEventExport);
3064
+ if (!built.ok) {
3065
+ return built;
3066
+ }
3067
+ const rootDir = normalizeNonEmptyString(input.rootDir, "rootDir");
3068
+ const exportName = normalizeOptionalString(input.exportName) ??
3069
+ `${built.scanner.scannerId}-${built.scanner.lane}-${built.normalizedEventExport.range.start}-${built.normalizedEventExport.range.end}`;
3070
+ const exportedAt = normalizeIsoTimestamp(input.exportedAt, "exportedAt", built.scanner.producedAt);
3071
+ const manifest = buildRuntimeEventExportBundleManifest({
3072
+ exportName,
3073
+ exportedAt,
3074
+ payloadPath: RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT.payload,
3075
+ normalizedEventExport: built.normalizedEventExport,
3076
+ scanner: built.scanner
3077
+ });
3078
+ return writeNormalizedEventExportBundleFiles({
3079
+ rootDir,
3080
+ manifest,
3081
+ normalizedEventExport: built.normalizedEventExport
3082
+ });
1796
3083
  }
1797
3084
  export function runRuntimeTurn(turn, options = {}) {
1798
3085
  const agentId = normalizeOptionalString(turn.agentId);
@@ -1914,6 +3201,7 @@ export function runContinuousProductLoopTurn(input) {
1914
3201
  const mergedHistory = mergeRuntimeEventHistory(currentState, normalizedEventExport);
1915
3202
  currentState.interactionEvents = mergedHistory.interactionEvents;
1916
3203
  currentState.feedbackEvents = mergedHistory.feedbackEvents;
3204
+ const compileStructuralSignals = turnResult.ok ? turnResult.compileResponse.diagnostics.structuralSignals : undefined;
1917
3205
  try {
1918
3206
  let activeBeforePack = null;
1919
3207
  try {
@@ -1932,6 +3220,7 @@ export function runContinuousProductLoopTurn(input) {
1932
3220
  builtAt: normalizeIsoTimestamp(input.candidateBuiltAt, "candidateBuiltAt", normalizedEventExport.range.lastCreatedAt ?? normalizedEventExport.range.firstCreatedAt),
1933
3221
  ...(input.offlineArtifacts !== undefined ? { offlineArtifacts: input.offlineArtifacts } : {}),
1934
3222
  ...(input.structuralOps !== undefined ? { structuralOps: input.structuralOps } : {}),
3223
+ ...(compileStructuralSignals !== undefined ? { compileStructuralSignals } : {}),
1935
3224
  ...(input.sparseFeedback !== undefined ? { sparseFeedback: input.sparseFeedback } : {}),
1936
3225
  ...(input.liveSliceSize !== undefined ? { liveSliceSize: input.liveSliceSize } : {}),
1937
3226
  ...(input.backfillSliceSize !== undefined ? { backfillSliceSize: input.backfillSliceSize } : {}),
@@ -2242,6 +3531,34 @@ function buildReplayTurnScore(input) {
2242
3531
  qualityScore: Math.min(100, compileScore + phraseScore)
2243
3532
  };
2244
3533
  }
3534
+ function buildRecordedSessionTurnObservability(result) {
3535
+ if (!result.eventExport.ok) {
3536
+ return {
3537
+ scanPolicy: null,
3538
+ scanSurfaces: [],
3539
+ humanLabelCount: 0,
3540
+ selfLabelCount: 0,
3541
+ totalEventCount: 0,
3542
+ attributedEventCount: 0,
3543
+ selectionDigestCount: 0,
3544
+ freshestSourceStream: null,
3545
+ freshestCreatedAt: null
3546
+ };
3547
+ }
3548
+ const observability = describeNormalizedEventExportObservability(result.eventExport.normalizedEventExport);
3549
+ const freshestSource = observability.supervisionFreshnessBySource[0] ?? null;
3550
+ return {
3551
+ scanPolicy: observability.learningSurface.scanPolicy,
3552
+ scanSurfaces: [...observability.learningSurface.scanSurfaces],
3553
+ humanLabelCount: observability.learningSurface.humanLabelCount,
3554
+ selfLabelCount: observability.learningSurface.selfLabelCount,
3555
+ totalEventCount: observability.attributionCoverage.totalEventCount,
3556
+ attributedEventCount: observability.attributionCoverage.attributedEventCount,
3557
+ selectionDigestCount: observability.attributionCoverage.selectionDigestCount,
3558
+ freshestSourceStream: observability.teacherFreshness.sourceStream ?? freshestSource?.sourceStream ?? null,
3559
+ freshestCreatedAt: observability.teacherFreshness.freshestCreatedAt ?? freshestSource?.freshestCreatedAt ?? null
3560
+ };
3561
+ }
2245
3562
  function buildRecordedSessionTurnReport(turnFixture, result, options) {
2246
3563
  const compileOk = result.ok;
2247
3564
  const selectedContextTexts = compileOk ? result.compileResponse.selectedContext.map((block) => block.text) : [];
@@ -2251,6 +3568,7 @@ function buildRecordedSessionTurnReport(turnFixture, result, options) {
2251
3568
  texts: selectedContextTexts,
2252
3569
  expectedContextPhrases: turnFixture.expectedContextPhrases
2253
3570
  });
3571
+ const observability = buildRecordedSessionTurnObservability(result);
2254
3572
  const eventExportDigest = result.eventExport.ok === true ? result.eventExport.normalizedEventExport.provenance.exportDigest : null;
2255
3573
  return {
2256
3574
  turnId: turnFixture.turnId,
@@ -2271,9 +3589,78 @@ function buildRecordedSessionTurnReport(turnFixture, result, options) {
2271
3589
  qualityScore: scoring.qualityScore,
2272
3590
  compileActiveVersion: options.compileActiveVersion,
2273
3591
  promoted: options.promoted,
3592
+ observability,
2274
3593
  warnings: [...result.warnings]
2275
3594
  };
2276
3595
  }
3596
+ function countRecordedSessionActivePackChanges(turns) {
3597
+ let changes = 0;
3598
+ let previousPackId = null;
3599
+ for (const turn of turns) {
3600
+ if (turn.activePackId === null) {
3601
+ continue;
3602
+ }
3603
+ if (previousPackId !== null && previousPackId !== turn.activePackId) {
3604
+ changes += 1;
3605
+ }
3606
+ previousPackId = turn.activePackId;
3607
+ }
3608
+ return changes;
3609
+ }
3610
+ function buildRecordedSessionReplayScannerWarnings(mode, turns, activePackChangeCount) {
3611
+ const warnings = [];
3612
+ const exportTurnCount = turns.filter((turn) => turn.observability.totalEventCount > 0).length;
3613
+ const attributedTurnCount = turns.filter((turn) => turn.observability.attributedEventCount > 0).length;
3614
+ const selectionDigestTurnCount = turns.filter((turn) => turn.observability.selectionDigestCount > 0).length;
3615
+ const humanLabelCount = turns.reduce((sum, turn) => sum + turn.observability.humanLabelCount, 0);
3616
+ const scanSurfaceCount = new Set(turns.flatMap((turn) => turn.observability.scanSurfaces)).size;
3617
+ if (exportTurnCount === 0) {
3618
+ warnings.push("no_export_observability");
3619
+ }
3620
+ if (scanSurfaceCount === 0) {
3621
+ warnings.push("scan_surfaces_missing");
3622
+ }
3623
+ if (humanLabelCount === 0) {
3624
+ warnings.push("human_labels_missing");
3625
+ }
3626
+ if (attributedTurnCount === 0) {
3627
+ warnings.push("turn_attribution_missing");
3628
+ }
3629
+ if (turns.some((turn) => turn.compileOk) && selectionDigestTurnCount === 0) {
3630
+ warnings.push("selection_digest_missing");
3631
+ }
3632
+ if (mode === "learned_replay" && activePackChangeCount === 0) {
3633
+ warnings.push("active_pack_never_moved");
3634
+ }
3635
+ return warnings;
3636
+ }
3637
+ function buildRecordedSessionReplayScannerEvidence(mode, turns) {
3638
+ const exportTurnCount = turns.filter((turn) => turn.observability.totalEventCount > 0).length;
3639
+ const scanSurfaces = uniqueStringsInOrder(turns.flatMap((turn) => turn.observability.scanSurfaces));
3640
+ const attributedTurnCount = turns.filter((turn) => turn.observability.attributedEventCount > 0).length;
3641
+ const selectionDigestTurnCount = turns.filter((turn) => turn.observability.selectionDigestCount > 0).length;
3642
+ const activePackChangeCount = countRecordedSessionActivePackChanges(turns);
3643
+ const latestObservedTurn = [...turns]
3644
+ .filter((turn) => turn.observability.freshestCreatedAt !== null)
3645
+ .sort((left, right) => (right.observability.freshestCreatedAt ?? "").localeCompare(left.observability.freshestCreatedAt ?? ""))[0];
3646
+ return {
3647
+ exportTurnCount,
3648
+ scanPolicy: turns.find((turn) => turn.observability.scanPolicy !== null)?.observability.scanPolicy ?? null,
3649
+ scanSurfaceCount: scanSurfaces.length,
3650
+ scanSurfaces,
3651
+ humanLabelCount: turns.reduce((sum, turn) => sum + turn.observability.humanLabelCount, 0),
3652
+ selfLabelCount: turns.reduce((sum, turn) => sum + turn.observability.selfLabelCount, 0),
3653
+ totalEventCount: turns.reduce((sum, turn) => sum + turn.observability.totalEventCount, 0),
3654
+ attributedEventCount: turns.reduce((sum, turn) => sum + turn.observability.attributedEventCount, 0),
3655
+ attributedTurnCount,
3656
+ selectionDigestCount: turns.reduce((sum, turn) => sum + turn.observability.selectionDigestCount, 0),
3657
+ selectionDigestTurnCount,
3658
+ activePackChangeCount,
3659
+ freshestSourceStream: latestObservedTurn?.observability.freshestSourceStream ?? null,
3660
+ freshestCreatedAt: latestObservedTurn?.observability.freshestCreatedAt ?? null,
3661
+ warnings: buildRecordedSessionReplayScannerWarnings(mode, turns, activePackChangeCount)
3662
+ };
3663
+ }
2277
3664
  function buildRecordedSessionReplayModeSummary(mode, turns) {
2278
3665
  const compileOkCount = turns.filter((turn) => turn.compileOk).length;
2279
3666
  const phraseHitCount = turns.reduce((sum, turn) => sum + turn.phraseHits.length, 0);
@@ -2282,6 +3669,7 @@ function buildRecordedSessionReplayModeSummary(mode, turns) {
2282
3669
  const promotionCount = turns.filter((turn) => turn.promoted).length;
2283
3670
  const qualityScore = turns.length === 0 ? 0 : Math.round(turns.reduce((sum, turn) => sum + turn.qualityScore, 0) / turns.length);
2284
3671
  const packIds = uniqueStringsInOrder(turns.map((turn) => turn.activePackId).filter(isPresent));
3672
+ const scannerEvidence = buildRecordedSessionReplayScannerEvidence(mode, turns);
2285
3673
  const base = {
2286
3674
  mode,
2287
3675
  qualityScore,
@@ -2290,7 +3678,8 @@ function buildRecordedSessionReplayModeSummary(mode, turns) {
2290
3678
  phraseCount,
2291
3679
  usedLearnedRouteTurnCount,
2292
3680
  promotionCount,
2293
- packIds
3681
+ packIds,
3682
+ scannerEvidence
2294
3683
  };
2295
3684
  return {
2296
3685
  ...base,
@@ -2328,6 +3717,7 @@ function buildRecordedSessionReplayScoreHash(modes) {
2328
3717
  usedLearnedRouteTurnCount: mode.summary.usedLearnedRouteTurnCount,
2329
3718
  promotionCount: mode.summary.promotionCount,
2330
3719
  packIds: mode.summary.packIds,
3720
+ scannerEvidence: mode.summary.scannerEvidence,
2331
3721
  scoreHash: mode.summary.scoreHash
2332
3722
  })));
2333
3723
  }
@@ -2358,6 +3748,17 @@ function recordedSessionReplayBundleBase(bundle) {
2358
3748
  expectedContextPhrases: [...turn.expectedContextPhrases],
2359
3749
  phraseHits: [...turn.phraseHits],
2360
3750
  missedPhrases: [...turn.missedPhrases],
3751
+ observability: {
3752
+ scanPolicy: turn.observability.scanPolicy,
3753
+ scanSurfaces: [...turn.observability.scanSurfaces],
3754
+ humanLabelCount: turn.observability.humanLabelCount,
3755
+ selfLabelCount: turn.observability.selfLabelCount,
3756
+ totalEventCount: turn.observability.totalEventCount,
3757
+ attributedEventCount: turn.observability.attributedEventCount,
3758
+ selectionDigestCount: turn.observability.selectionDigestCount,
3759
+ freshestSourceStream: turn.observability.freshestSourceStream,
3760
+ freshestCreatedAt: turn.observability.freshestCreatedAt
3761
+ },
2361
3762
  warnings: [...turn.warnings]
2362
3763
  }))
2363
3764
  })),
@@ -2749,6 +4150,39 @@ function summarizeCandidateAheadBy(candidateAheadBy) {
2749
4150
  .map(([field]) => field)
2750
4151
  .sort();
2751
4152
  }
4153
+ function summarizeManyProfileSupport(policyMode) {
4154
+ if (policyMode === "shared") {
4155
+ return {
4156
+ operatorSurface: "current_profile_only",
4157
+ declaredAttachmentPolicy: "shared",
4158
+ sameGatewayIntent: "shared_attachment_declared",
4159
+ checkedInProofTopology: "two_local_gateways_dedicated_only",
4160
+ sameGatewayProof: false,
4161
+ sharedWriteSafetyProof: false,
4162
+ detail: "The Host declares that multiple Profiles may intentionally attach to this Brain activation root, but the shipped operator read stays current-profile-only and this repo still does not prove same-gateway attachment behavior or shared write safety."
4163
+ };
4164
+ }
4165
+ if (policyMode === "dedicated") {
4166
+ return {
4167
+ operatorSurface: "current_profile_only",
4168
+ declaredAttachmentPolicy: "dedicated",
4169
+ sameGatewayIntent: "dedicated_current_profile_boundary",
4170
+ checkedInProofTopology: "two_local_gateways_dedicated_only",
4171
+ sameGatewayProof: false,
4172
+ sharedWriteSafetyProof: false,
4173
+ detail: "The Host declares one current Profile per Brain activation root. The checked-in many-profile proof is still narrower: two local gateways with dedicated brains, not same-gateway many-profile attachment inside one running host."
4174
+ };
4175
+ }
4176
+ return {
4177
+ operatorSurface: "current_profile_only",
4178
+ declaredAttachmentPolicy: "undeclared",
4179
+ sameGatewayIntent: "undeclared",
4180
+ checkedInProofTopology: "two_local_gateways_dedicated_only",
4181
+ sameGatewayProof: false,
4182
+ sharedWriteSafetyProof: false,
4183
+ detail: "The Host has not declared shared-vs-dedicated attachment policy. Keep the operator read current-profile-only, do not infer profile exclusivity from activation state alone, and do not claim same-gateway many-profile proof."
4184
+ };
4185
+ }
2752
4186
  function isAwaitingFirstExportSlot(slot) {
2753
4187
  return slot !== null && slot.eventRange.count === 0;
2754
4188
  }
@@ -2833,6 +4267,7 @@ function summarizeGraphObservability(active, observability) {
2833
4267
  };
2834
4268
  }
2835
4269
  function summarizeServePath(compile) {
4270
+ const structuralDecision = summarizeStructuralDecisionFromNotes(compile?.notes ?? []);
2836
4271
  if (compile === null) {
2837
4272
  return {
2838
4273
  state: "unprobed",
@@ -2850,6 +4285,7 @@ function summarizeServePath(compile) {
2850
4285
  structuralBudgetSource: null,
2851
4286
  structuralBudgetEvidence: null,
2852
4287
  structuralBudgetPressures: null,
4288
+ structuralDecision,
2853
4289
  contextAttribution: buildContextAttributionSummary({
2854
4290
  fallbackToStaticContext: false,
2855
4291
  hardRequirementViolated: false,
@@ -2876,11 +4312,11 @@ function summarizeServePath(compile) {
2876
4312
  structuralBudgetSource: null,
2877
4313
  structuralBudgetEvidence: null,
2878
4314
  structuralBudgetPressures: null,
4315
+ structuralDecision,
2879
4316
  contextAttribution: compile.contextAttribution,
2880
4317
  error: compile.error
2881
4318
  };
2882
4319
  }
2883
- const resolvedMaxContextBlocksValue = readDiagnosticNoteValue(compile.notes, "resolved_max_context_blocks=");
2884
4320
  return {
2885
4321
  state: "serving_active_pack",
2886
4322
  fallbackToStaticContext: compile.fallbackToStaticContext,
@@ -2893,11 +4329,12 @@ function summarizeServePath(compile) {
2893
4329
  freshnessChecksum: readDiagnosticNoteValue(compile.notes, "router_freshness_checksum="),
2894
4330
  requestedBudgetStrategy: readDiagnosticNoteValue(compile.notes, "requested_budget_strategy="),
2895
4331
  resolvedBudgetStrategy: readDiagnosticNoteValue(compile.notes, "resolved_budget_strategy="),
2896
- resolvedMaxContextBlocks: resolvedMaxContextBlocksValue === null ? null : Number.parseInt(resolvedMaxContextBlocksValue, 10),
4332
+ resolvedMaxContextBlocks: structuralDecision.resolvedMaxContextBlocks,
2897
4333
  structuralBudgetSource: readDiagnosticNoteValue(compile.notes, "structural_budget_source="),
2898
4334
  structuralBudgetEvidence: readDiagnosticNoteValue(compile.notes, "structural_budget_compile_evidence=") ??
2899
4335
  readDiagnosticNoteValue(compile.notes, "structural_budget_evidence="),
2900
4336
  structuralBudgetPressures: readDiagnosticNoteValue(compile.notes, "structural_budget_pressures="),
4337
+ structuralDecision,
2901
4338
  contextAttribution: compile.contextAttribution,
2902
4339
  error: compile.error
2903
4340
  };
@@ -3046,11 +4483,18 @@ function summarizeSupervision(input) {
3046
4483
  exportDigest: null,
3047
4484
  exportedAt: null,
3048
4485
  flowing: null,
4486
+ scanPolicy: null,
4487
+ scanSurfaceCount: 0,
4488
+ scanSurfaces: [],
3049
4489
  sourceCount: 0,
3050
4490
  freshestSourceStream: null,
3051
4491
  freshestCreatedAt: null,
3052
4492
  freshestKind: null,
3053
4493
  humanLabelCount: null,
4494
+ selfLabelCount: null,
4495
+ attributedEventCount: null,
4496
+ totalEventCount: null,
4497
+ selectionDigestCount: null,
3054
4498
  sources: [],
3055
4499
  detail: "no event export path supplied"
3056
4500
  };
@@ -3065,11 +4509,18 @@ function summarizeSupervision(input) {
3065
4509
  exportDigest: observability.exportDigest,
3066
4510
  exportedAt: loaded.exportedAt,
3067
4511
  flowing,
4512
+ scanPolicy: observability.learningSurface.scanPolicy,
4513
+ scanSurfaceCount: observability.learningSurface.scanSurfaces.length,
4514
+ scanSurfaces: [...observability.learningSurface.scanSurfaces],
3068
4515
  sourceCount: observability.supervisionFreshnessBySource.length,
3069
4516
  freshestSourceStream: observability.teacherFreshness.sourceStream ?? freshestSource?.sourceStream ?? null,
3070
4517
  freshestCreatedAt: observability.teacherFreshness.freshestCreatedAt ?? freshestSource?.freshestCreatedAt ?? null,
3071
4518
  freshestKind: observability.teacherFreshness.freshestKind ?? freshestSource?.freshestKind ?? null,
3072
4519
  humanLabelCount: observability.teacherFreshness.humanLabelCount,
4520
+ selfLabelCount: observability.learningSurface.selfLabelCount,
4521
+ attributedEventCount: observability.attributionCoverage.attributedEventCount,
4522
+ totalEventCount: observability.attributionCoverage.totalEventCount,
4523
+ selectionDigestCount: observability.attributionCoverage.selectionDigestCount,
3073
4524
  sources: [...observability.teacherFreshness.sources],
3074
4525
  detail: flowing
3075
4526
  ? "human supervision is visible in the supplied export"
@@ -3134,6 +4585,53 @@ function summarizeTeacherLoop(input) {
3134
4585
  detail: "async teacher diagnostics loaded"
3135
4586
  };
3136
4587
  }
4588
+ function summarizeLearningBacklogState(plan, principalLagStatus) {
4589
+ if (!plan.bootstrapped && plan.pending.total === 0) {
4590
+ return "awaiting_first_export";
4591
+ }
4592
+ if (plan.nextPriorityBucket === "principal_immediate") {
4593
+ return "principal_live_priority";
4594
+ }
4595
+ if (plan.nextPriorityBucket === "principal_backfill") {
4596
+ return "principal_backfill_priority";
4597
+ }
4598
+ if (plan.nextPriorityBucket === "live") {
4599
+ return "live_priority";
4600
+ }
4601
+ if (plan.nextPriorityBucket === "backfill") {
4602
+ return "backfill_only";
4603
+ }
4604
+ return principalLagStatus === "pending_promotion" ? "principal_live_priority" : "caught_up";
4605
+ }
4606
+ function summarizeLearningWarningStates(input) {
4607
+ const warnings = new Set();
4608
+ if (!input.plan.bootstrapped && input.plan.pending.total === 0) {
4609
+ warnings.add("awaiting_first_export");
4610
+ }
4611
+ if (input.plan.pending.byBucket.principal_immediate > 0) {
4612
+ warnings.add("principal_live_backlog");
4613
+ }
4614
+ if (input.plan.pending.byBucket.principal_backfill > 0) {
4615
+ warnings.add("principal_backfill_pending");
4616
+ }
4617
+ if (input.principalLagStatus === "pending_promotion") {
4618
+ warnings.add("active_pack_behind_latest_principal");
4619
+ }
4620
+ if (input.plan.pending.backfill > 0) {
4621
+ warnings.add("passive_backfill_pending");
4622
+ }
4623
+ if (input.teacherSnapshot.queue.capacity > 0 &&
4624
+ input.teacherSnapshot.queue.depth >= input.teacherSnapshot.queue.capacity) {
4625
+ warnings.add("teacher_queue_full");
4626
+ }
4627
+ if (input.teacherSnapshot.diagnostics.latestFreshness === "stale") {
4628
+ warnings.add("teacher_labels_stale");
4629
+ }
4630
+ if (input.teacherSnapshot.diagnostics.lastNoOpReason === "no_teacher_artifacts") {
4631
+ warnings.add("teacher_no_artifacts");
4632
+ }
4633
+ return [...warnings];
4634
+ }
3137
4635
  function summarizeAlwaysOnLearning(input, active) {
3138
4636
  const unavailableLag = {
3139
4637
  activeEventRangeEnd: active?.eventRange.end ?? null,
@@ -3149,15 +4647,21 @@ function summarizeAlwaysOnLearning(input, active) {
3149
4647
  bootstrapped: null,
3150
4648
  mode: "unavailable",
3151
4649
  nextPriorityLane: "unavailable",
4650
+ nextPriorityBucket: "unavailable",
4651
+ backlogState: "unavailable",
3152
4652
  pendingLive: null,
3153
4653
  pendingBackfill: null,
3154
4654
  pendingTotal: null,
4655
+ pendingByBucket: null,
3155
4656
  freshLivePriority: null,
3156
4657
  principalCheckpointCount: null,
3157
4658
  pendingPrincipalCount: null,
3158
4659
  oldestUnlearnedPrincipalEvent: null,
4660
+ newestPendingPrincipalEvent: null,
4661
+ leadingPrincipalCheckpoint: null,
3159
4662
  principalCheckpoints: [],
3160
4663
  principalLagToPromotion: unavailableLag,
4664
+ warningStates: ["teacher_snapshot_unavailable"],
3161
4665
  learnedRange: null,
3162
4666
  materializationCount: null,
3163
4667
  lastMaterializedAt: null,
@@ -3176,15 +4680,21 @@ function summarizeAlwaysOnLearning(input, active) {
3176
4680
  bootstrapped: null,
3177
4681
  mode: "unavailable",
3178
4682
  nextPriorityLane: "unavailable",
4683
+ nextPriorityBucket: "unavailable",
4684
+ backlogState: "unavailable",
3179
4685
  pendingLive: null,
3180
4686
  pendingBackfill: null,
3181
4687
  pendingTotal: null,
4688
+ pendingByBucket: null,
3182
4689
  freshLivePriority: null,
3183
4690
  principalCheckpointCount: null,
3184
4691
  pendingPrincipalCount: null,
3185
4692
  oldestUnlearnedPrincipalEvent: null,
4693
+ newestPendingPrincipalEvent: null,
4694
+ leadingPrincipalCheckpoint: null,
3186
4695
  principalCheckpoints: [],
3187
4696
  principalLagToPromotion: unavailableLag,
4697
+ warningStates: ["teacher_snapshot_unavailable"],
3188
4698
  learnedRange: null,
3189
4699
  materializationCount: null,
3190
4700
  lastMaterializedAt: null,
@@ -3207,30 +4717,43 @@ function summarizeAlwaysOnLearning(input, active) {
3207
4717
  const sequenceLag = latestPrincipalSequence === null || activeEventRangeEnd === null
3208
4718
  ? null
3209
4719
  : Math.max(latestPrincipalSequence - activeEventRangeEnd, 0);
4720
+ const principalLagStatus = sequenceLag === null
4721
+ ? "unavailable"
4722
+ : sequenceLag === 0
4723
+ ? "caught_up"
4724
+ : "pending_promotion";
4725
+ const backlogState = summarizeLearningBacklogState(plan, principalLagStatus);
4726
+ const warningStates = summarizeLearningWarningStates({
4727
+ plan,
4728
+ principalLagStatus,
4729
+ teacherSnapshot: snapshot
4730
+ });
3210
4731
  return {
3211
4732
  available: true,
3212
4733
  sourcePath: path.resolve(teacherSnapshotPath),
3213
4734
  bootstrapped: plan.bootstrapped,
3214
4735
  mode: plan.mode,
3215
4736
  nextPriorityLane: plan.nextPriorityLane,
4737
+ nextPriorityBucket: plan.nextPriorityBucket,
4738
+ backlogState,
3216
4739
  pendingLive: plan.pending.live,
3217
4740
  pendingBackfill: plan.pending.backfill,
3218
4741
  pendingTotal: plan.pending.total,
4742
+ pendingByBucket: { ...plan.pending.byBucket },
3219
4743
  freshLivePriority: plan.pending.freshLivePriority,
3220
4744
  principalCheckpointCount: plan.principalBacklog.principalCount,
3221
4745
  pendingPrincipalCount: plan.principalBacklog.pendingEventCount,
3222
4746
  oldestUnlearnedPrincipalEvent: plan.principalBacklog.oldestUnlearnedEvent,
4747
+ newestPendingPrincipalEvent: plan.principalBacklog.newestPendingEvent,
4748
+ leadingPrincipalCheckpoint: plan.principalBacklog.checkpoints[0] ?? null,
3223
4749
  principalCheckpoints: plan.principalBacklog.checkpoints,
3224
4750
  principalLagToPromotion: {
3225
4751
  activeEventRangeEnd,
3226
4752
  latestPrincipalSequence,
3227
4753
  sequenceLag,
3228
- status: sequenceLag === null
3229
- ? "unavailable"
3230
- : sequenceLag === 0
3231
- ? "caught_up"
3232
- : "pending_promotion"
4754
+ status: principalLagStatus
3233
4755
  },
4756
+ warningStates,
3234
4757
  learnedRange: plan.learnedRange === null ? null : { ...plan.learnedRange },
3235
4758
  materializationCount: plan.materialization.count,
3236
4759
  lastMaterializedAt: plan.materialization.lastMaterializedAt,
@@ -3238,13 +4761,17 @@ function summarizeAlwaysOnLearning(input, active) {
3238
4761
  lastMaterializationLane: plan.materialization.lastLane,
3239
4762
  lastMaterializationPriority: plan.materialization.lastPriority,
3240
4763
  lastMaterializedPackId: snapshot.learner.lastMaterialization?.candidate.summary.packId ?? null,
3241
- detail: plan.pending.freshLivePriority
3242
- ? "fresh live slices remain ahead of passive catch-up"
3243
- : plan.pending.backfill > 0
3244
- ? "passive backfill remains queued behind the current live state"
3245
- : plan.bootstrapped
3246
- ? "fast-init has handed off to the current learned export without queued backlog"
3247
- : "learner is waiting for the first export"
4764
+ detail: plan.nextPriorityBucket === "principal_immediate"
4765
+ ? "principal-priority live slices are next; passive backfill stays behind live intake"
4766
+ : plan.nextPriorityBucket === "live"
4767
+ ? "fresh live slices are next; passive backfill stays behind live intake"
4768
+ : plan.nextPriorityBucket === "principal_backfill"
4769
+ ? "live intake is clear; principal-priority passive backfill is next"
4770
+ : plan.nextPriorityBucket === "backfill"
4771
+ ? "live intake is clear; passive backfill is next"
4772
+ : plan.bootstrapped
4773
+ ? "fast-init has handed off to the current learned export without queued backlog"
4774
+ : "learner is waiting for the first export"
3248
4775
  };
3249
4776
  }
3250
4777
  function buildOperatorFindings(report) {
@@ -3277,7 +4804,7 @@ function buildOperatorFindings(report) {
3277
4804
  }
3278
4805
  if (report.servePath.state === "serving_active_pack") {
3279
4806
  push("pass", "serve_path_verified", `serve path compiles from active pack ${report.servePath.activePackId ?? "unknown-pack"}`, `selection=${report.servePath.selectionMode ?? "unknown"}; tiers=${report.servePath.contextAttribution.selectionTiers ?? "unknown"}; router=${report.servePath.routerIdentity ?? "none"}; routeFreshness=${report.servePath.freshnessChecksum ?? "unknown"}`);
3280
- push("pass", "structural_budget_visible", `structural budget resolves to ${report.servePath.resolvedMaxContextBlocks ?? "unknown"} blocks`, `requested=${report.servePath.requestedBudgetStrategy ?? "unknown"}; resolved=${report.servePath.resolvedBudgetStrategy ?? "unknown"}; source=${report.servePath.structuralBudgetSource ?? "unknown"}; evidence=${report.servePath.structuralBudgetEvidence ?? "none"}; pressures=${report.servePath.structuralBudgetPressures ?? "none"}`);
4807
+ push("pass", "structural_budget_visible", `structural budget resolves to ${report.servePath.resolvedMaxContextBlocks ?? "unknown"} blocks`, `origin=${report.servePath.structuralDecision.origin}; basis=${report.servePath.structuralDecision.basis}; requested=${report.servePath.requestedBudgetStrategy ?? "unknown"}; resolved=${report.servePath.resolvedBudgetStrategy ?? "unknown"}; source=${report.servePath.structuralBudgetSource ?? "unknown"}; evidence=${report.servePath.structuralBudgetEvidence ?? "none"}; pressures=${report.servePath.structuralBudgetPressures ?? "none"}`);
3281
4808
  }
3282
4809
  else if (report.servePath.state === "fail_open_static_context") {
3283
4810
  push("warn", "serve_path_fail_open", "serve path would fail open to static context", report.servePath.error ?? "compile probe fell back to static context");
@@ -3334,6 +4861,22 @@ function buildOperatorFindings(report) {
3334
4861
  else {
3335
4862
  push("warn", "supervision_not_flowing", "the supplied export does not yet show human supervision", `sourcePath=${report.supervision.sourcePath ?? "unknown"}; exportDigest=${report.supervision.exportDigest ?? "unknown"}`);
3336
4863
  }
4864
+ if (report.supervision.available) {
4865
+ if (report.supervision.scanSurfaceCount > 0) {
4866
+ push("pass", "scan_surfaces_visible", `scanner surfaces are visible: ${formatList(report.supervision.scanSurfaces)}`, `scanPolicy=${report.supervision.scanPolicy ?? "unknown"}; humanLabels=${report.supervision.humanLabelCount ?? 0}; selfLabels=${report.supervision.selfLabelCount ?? 0}`);
4867
+ }
4868
+ else {
4869
+ push("warn", "scan_surfaces_missing", "scanner surfaces are not visible in the supplied export", `exportDigest=${report.supervision.exportDigest ?? "unknown"}`);
4870
+ }
4871
+ if (report.supervision.totalEventCount !== null && report.supervision.totalEventCount > 0) {
4872
+ if (report.supervision.attributedEventCount === report.supervision.totalEventCount) {
4873
+ push("pass", "turn_attribution_visible", `all supplied events are attributable: ${report.supervision.attributedEventCount}/${report.supervision.totalEventCount}`, `selectionDigests=${report.supervision.selectionDigestCount ?? 0}`);
4874
+ }
4875
+ else {
4876
+ push("warn", "turn_attribution_partial", `some supplied events are unattributed: ${report.supervision.attributedEventCount ?? 0}/${report.supervision.totalEventCount}`, `selectionDigests=${report.supervision.selectionDigestCount ?? 0}`);
4877
+ }
4878
+ }
4879
+ }
3337
4880
  if (!report.teacherLoop.available) {
3338
4881
  push("warn", "teacher_snapshot_unavailable", "last async no-op reason is not inspectable yet", "pass `--teacher-snapshot <snapshot.json>` to inspect duplicate/no-op handling");
3339
4882
  }
@@ -3415,7 +4958,7 @@ function summarizeCurrentProfileBrainStatusLevel(input) {
3415
4958
  }
3416
4959
  return input.routeFreshness === "updated" ? "ok" : "warn";
3417
4960
  }
3418
- function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
4961
+ function buildCurrentProfileBrainStatusFromReport(report, policyMode, profileId) {
3419
4962
  const attached = report.active !== null;
3420
4963
  const awaitingFirstExport = isAwaitingFirstExportSlot(report.active);
3421
4964
  const routerIdentity = report.servePath.routerIdentity ?? report.learnedRouting.routerIdentity ?? report.active?.routerIdentity ?? null;
@@ -3440,6 +4983,7 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
3440
4983
  profile: {
3441
4984
  noun: "Profile",
3442
4985
  selector: "current_profile",
4986
+ profileId,
3443
4987
  detail: attached
3444
4988
  ? "The Host resolves the current Profile through the active Attachment boundary only."
3445
4989
  : "The current Profile has no active Brain attachment visible at the Host boundary."
@@ -3496,6 +5040,7 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
3496
5040
  usedLearnedRouteFn: report.servePath.usedLearnedRouteFn,
3497
5041
  failOpen: report.servePath.fallbackToStaticContext,
3498
5042
  awaitingFirstExport,
5043
+ structuralDecision: report.servePath.structuralDecision,
3499
5044
  detail: report.servePath.state === "serving_active_pack"
3500
5045
  ? awaitingFirstExport
3501
5046
  ? `current profile is serving seed-state pack ${activePackId ?? "unknown"} while awaiting the first exported turn`
@@ -3514,6 +5059,7 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
3514
5059
  export function buildOperatorSurfaceReport(input) {
3515
5060
  const activationRoot = path.resolve(normalizeNonEmptyString(input.activationRoot, "activationRoot"));
3516
5061
  const updatedAt = normalizeIsoTimestamp(input.updatedAt, "updatedAt", new Date().toISOString());
5062
+ const brainAttachmentPolicy = normalizeBrainAttachmentPolicy(input.brainAttachmentPolicy);
3517
5063
  const inspection = inspectActivationState(activationRoot, updatedAt);
3518
5064
  const observability = describeActivationObservability(activationRoot, "active", {
3519
5065
  updatedAt
@@ -3571,7 +5117,8 @@ export function buildOperatorSurfaceReport(input) {
3571
5117
  supervision: summarizeSupervision(input),
3572
5118
  learning: summarizeAlwaysOnLearning(input, active),
3573
5119
  teacherLoop: summarizeTeacherLoop(input),
3574
- principal: summarizePrincipalObservability(input, active)
5120
+ principal: summarizePrincipalObservability(input, active),
5121
+ manyProfile: summarizeManyProfileSupport(brainAttachmentPolicy)
3575
5122
  };
3576
5123
  const findings = buildOperatorFindings(reportBase);
3577
5124
  return {
@@ -3582,7 +5129,7 @@ export function buildOperatorSurfaceReport(input) {
3582
5129
  }
3583
5130
  export function describeCurrentProfileBrainStatus(input) {
3584
5131
  const report = buildOperatorSurfaceReport(input);
3585
- return buildCurrentProfileBrainStatusFromReport(report, normalizeBrainAttachmentPolicy(input.brainAttachmentPolicy));
5132
+ return buildCurrentProfileBrainStatusFromReport(report, report.manyProfile.declaredAttachmentPolicy, normalizeOptionalString(input.profileId) ?? null);
3586
5133
  }
3587
5134
  export function formatOperatorRollbackReport(result) {
3588
5135
  const header = result.allowed ? (result.dryRun ? "ROLLBACK ready" : "ROLLBACK ok") : "ROLLBACK blocked";
@@ -3650,4 +5197,9 @@ export { CONTRACT_IDS, buildNormalizedEventExport, createFeedbackEvent, createIn
3650
5197
  export { describeNormalizedEventExportObservability } from "@openclawbrain/event-export";
3651
5198
  export { describeCompileFallbackUsage } from "@openclawbrain/compiler";
3652
5199
  export { describeActivationObservability, inspectActivationState, rollbackActivePack } from "@openclawbrain/pack-format";
5200
+ export { createOpenClawLocalSessionTail, OpenClawLocalSessionTail } from "./session-tail.js";
5201
+ export { discoverOpenClawMainSessionStores, loadOpenClawSessionIndex, readOpenClawAcpStreamFile, readOpenClawSessionFile } from "./session-store.js";
5202
+ export { buildPassiveLearningSessionExportFromOpenClawSessionStore, buildPassiveLearningStoreExportFromOpenClawSessionIndex } from "./local-session-passive-learning.js";
5203
+ export { resolveActivationRoot } from "./resolve-activation-root.js";
5204
+ export { runDaemonCommand, parseDaemonArgs } from "./daemon.js";
3653
5205
  //# sourceMappingURL=index.js.map