@openclawbrain/openclaw 0.1.10 → 0.1.12

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
+ };
854
1864
  }
855
- return "teaching";
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;
1915
+ }
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 = [
@@ -910,6 +1987,222 @@ function classifyCompileFailure(error, activationRoot) {
910
1987
  }
911
1988
  return failOpenCompileResult(error, resolvedActivationRoot);
912
1989
  }
1990
+ function uniqueNotes(notes) {
1991
+ return [...new Set(notes.filter((note) => note.length > 0))];
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
+ }
1999
+ function clampInteger(value, minimum, maximum) {
2000
+ return Math.min(maximum, Math.max(minimum, Math.round(value)));
2001
+ }
2002
+ export function deriveEmpiricalStructuralBudget(input) {
2003
+ const requestedStrategy = input.requestedStrategy ?? "fixed_v1";
2004
+ const defaultMaxContextBlocks = input.defaultMaxContextBlocks ?? 4;
2005
+ const minimumMaxContextBlocks = input.minimumMaxContextBlocks ?? 2;
2006
+ const maximumMaxContextBlocks = input.maximumMaxContextBlocks ?? 6;
2007
+ if (input.requestedMaxContextBlocks !== undefined) {
2008
+ const maxContextBlocks = clampInteger(input.requestedMaxContextBlocks, 0, Number.MAX_SAFE_INTEGER);
2009
+ return {
2010
+ requestedStrategy,
2011
+ effectiveStrategy: requestedStrategy,
2012
+ maxContextBlocks,
2013
+ defaultMaxContextBlocks,
2014
+ evidence: { split: 0, merge: 0, prune: 0, connect: 0 },
2015
+ evidenceTotal: 0,
2016
+ tendencies: { split: 0, merge: 0, prune: 0, connect: 0 },
2017
+ notes: [
2018
+ `requested_budget_strategy=${requestedStrategy}`,
2019
+ `requested_max_context_blocks=${maxContextBlocks}`,
2020
+ `resolved_budget_strategy=${requestedStrategy}`,
2021
+ `resolved_max_context_blocks=${maxContextBlocks}`,
2022
+ "structural_budget_source=caller_override"
2023
+ ]
2024
+ };
2025
+ }
2026
+ if (requestedStrategy !== "empirical_v1") {
2027
+ return {
2028
+ requestedStrategy,
2029
+ effectiveStrategy: requestedStrategy,
2030
+ maxContextBlocks: defaultMaxContextBlocks,
2031
+ defaultMaxContextBlocks,
2032
+ evidence: { split: 0, merge: 0, prune: 0, connect: 0 },
2033
+ evidenceTotal: 0,
2034
+ tendencies: { split: 0, merge: 0, prune: 0, connect: 0 },
2035
+ notes: [
2036
+ `requested_budget_strategy=${requestedStrategy}`,
2037
+ `resolved_budget_strategy=${requestedStrategy}`,
2038
+ `resolved_max_context_blocks=${defaultMaxContextBlocks}`,
2039
+ `structural_budget_source=${requestedStrategy === "fixed_v1" ? "fixed_default" : "fixed_fallback"}`
2040
+ ]
2041
+ };
2042
+ }
2043
+ const evidence = {
2044
+ split: Math.max(0, input.evolution?.structuralOps.split ?? 0),
2045
+ merge: Math.max(0, input.evolution?.structuralOps.merge ?? 0),
2046
+ prune: Math.max(0, Math.max(input.evolution?.structuralOps.prune ?? 0, input.evolution?.prunedBlockIds.length ?? 0)),
2047
+ connect: Math.max(0, input.evolution?.structuralOps.connect ?? 0)
2048
+ };
2049
+ const evidenceTotal = evidence.split + evidence.merge + evidence.prune + evidence.connect;
2050
+ if (evidenceTotal === 0) {
2051
+ return {
2052
+ requestedStrategy,
2053
+ effectiveStrategy: "fixed_v1",
2054
+ maxContextBlocks: defaultMaxContextBlocks,
2055
+ defaultMaxContextBlocks,
2056
+ evidence,
2057
+ evidenceTotal,
2058
+ tendencies: { split: 0, merge: 0, prune: 0, connect: 0 },
2059
+ notes: [
2060
+ `requested_budget_strategy=${requestedStrategy}`,
2061
+ "resolved_budget_strategy=fixed_v1",
2062
+ `resolved_max_context_blocks=${defaultMaxContextBlocks}`,
2063
+ "structural_budget_source=no_evidence_fallback"
2064
+ ]
2065
+ };
2066
+ }
2067
+ const tendencies = {
2068
+ split: evidence.split / evidenceTotal,
2069
+ merge: evidence.merge / evidenceTotal,
2070
+ prune: evidence.prune / evidenceTotal,
2071
+ connect: evidence.connect / evidenceTotal
2072
+ };
2073
+ const expansionPressure = tendencies.split + tendencies.connect;
2074
+ const contractionPressure = tendencies.merge + tendencies.prune;
2075
+ const directionalPressure = expansionPressure - contractionPressure;
2076
+ const maxContextBlocks = clampInteger(defaultMaxContextBlocks + directionalPressure * 2, minimumMaxContextBlocks, maximumMaxContextBlocks);
2077
+ return {
2078
+ requestedStrategy,
2079
+ effectiveStrategy: requestedStrategy,
2080
+ maxContextBlocks,
2081
+ defaultMaxContextBlocks,
2082
+ evidence,
2083
+ evidenceTotal,
2084
+ tendencies,
2085
+ notes: [
2086
+ `requested_budget_strategy=${requestedStrategy}`,
2087
+ `resolved_budget_strategy=${requestedStrategy}`,
2088
+ `resolved_max_context_blocks=${maxContextBlocks}`,
2089
+ "structural_budget_source=graph_evolution_empirical_v1",
2090
+ `structural_budget_evidence=split:${evidence.split},merge:${evidence.merge},prune:${evidence.prune},connect:${evidence.connect},total:${evidenceTotal}`,
2091
+ `structural_budget_tendencies=split:${tendencies.split.toFixed(4)},merge:${tendencies.merge.toFixed(4)},prune:${tendencies.prune.toFixed(4)},connect:${tendencies.connect.toFixed(4)}`,
2092
+ `structural_budget_pressures=expand:${expansionPressure.toFixed(4)},contract:${contractionPressure.toFixed(4)},directional:${directionalPressure.toFixed(4)}`
2093
+ ]
2094
+ };
2095
+ }
2096
+ export function deriveEmpiricalStructuralBudgetFromCompileSignals(input) {
2097
+ const requestedStrategy = input.requestedStrategy ?? "fixed_v1";
2098
+ const defaultMaxContextBlocks = input.defaultMaxContextBlocks ?? 4;
2099
+ const minimumMaxContextBlocks = input.minimumMaxContextBlocks ?? 2;
2100
+ const maximumMaxContextBlocks = input.maximumMaxContextBlocks ?? 6;
2101
+ if (input.requestedMaxContextBlocks !== undefined) {
2102
+ const maxContextBlocks = clampInteger(input.requestedMaxContextBlocks, 0, Number.MAX_SAFE_INTEGER);
2103
+ return {
2104
+ requestedStrategy,
2105
+ effectiveStrategy: requestedStrategy,
2106
+ maxContextBlocks,
2107
+ defaultMaxContextBlocks,
2108
+ evidence: { split: 0, merge: 0, prune: 0, connect: 0 },
2109
+ evidenceTotal: 0,
2110
+ tendencies: { split: 0, merge: 0, prune: 0, connect: 0 },
2111
+ notes: [
2112
+ `requested_budget_strategy=${requestedStrategy}`,
2113
+ `requested_max_context_blocks=${maxContextBlocks}`,
2114
+ `resolved_budget_strategy=${requestedStrategy}`,
2115
+ `resolved_max_context_blocks=${maxContextBlocks}`,
2116
+ "structural_budget_source=caller_override"
2117
+ ]
2118
+ };
2119
+ }
2120
+ if (requestedStrategy !== "empirical_v1") {
2121
+ return {
2122
+ requestedStrategy,
2123
+ effectiveStrategy: requestedStrategy,
2124
+ maxContextBlocks: defaultMaxContextBlocks,
2125
+ defaultMaxContextBlocks,
2126
+ evidence: { split: 0, merge: 0, prune: 0, connect: 0 },
2127
+ evidenceTotal: 0,
2128
+ tendencies: { split: 0, merge: 0, prune: 0, connect: 0 },
2129
+ notes: [
2130
+ `requested_budget_strategy=${requestedStrategy}`,
2131
+ `resolved_budget_strategy=${requestedStrategy}`,
2132
+ `resolved_max_context_blocks=${defaultMaxContextBlocks}`,
2133
+ `structural_budget_source=${requestedStrategy === "fixed_v1" ? "fixed_default" : "fixed_fallback"}`
2134
+ ]
2135
+ };
2136
+ }
2137
+ const compileEvidence = {
2138
+ expansionCandidates: Math.max(0, (input.structuralSignals?.matchedCandidateCount ?? 0) - (input.structuralSignals?.selectedMatchedCount ?? 0)),
2139
+ traversalActivations: Math.max(0, input.structuralSignals?.traversalActivatedCount ?? 0),
2140
+ overlapPruned: Math.max(0, input.structuralSignals?.overlapPrunedCount ?? 0)
2141
+ };
2142
+ const evidence = {
2143
+ split: compileEvidence.expansionCandidates,
2144
+ merge: 0,
2145
+ prune: compileEvidence.overlapPruned,
2146
+ connect: compileEvidence.traversalActivations
2147
+ };
2148
+ const evidenceTotal = evidence.split + evidence.merge + evidence.prune + evidence.connect;
2149
+ if (evidenceTotal === 0) {
2150
+ return {
2151
+ requestedStrategy,
2152
+ effectiveStrategy: "fixed_v1",
2153
+ maxContextBlocks: defaultMaxContextBlocks,
2154
+ defaultMaxContextBlocks,
2155
+ evidence,
2156
+ evidenceTotal,
2157
+ tendencies: { split: 0, merge: 0, prune: 0, connect: 0 },
2158
+ notes: [
2159
+ `requested_budget_strategy=${requestedStrategy}`,
2160
+ "resolved_budget_strategy=fixed_v1",
2161
+ `resolved_max_context_blocks=${defaultMaxContextBlocks}`,
2162
+ "structural_budget_source=no_compile_signal_evidence_fallback"
2163
+ ]
2164
+ };
2165
+ }
2166
+ const tendencies = {
2167
+ split: evidence.split / evidenceTotal,
2168
+ merge: 0,
2169
+ prune: evidence.prune / evidenceTotal,
2170
+ connect: evidence.connect / evidenceTotal
2171
+ };
2172
+ const expansionPressure = tendencies.split + tendencies.connect;
2173
+ const contractionPressure = tendencies.prune;
2174
+ const directionalPressure = expansionPressure - contractionPressure;
2175
+ const maxContextBlocks = clampInteger(defaultMaxContextBlocks + directionalPressure * 2, minimumMaxContextBlocks, maximumMaxContextBlocks);
2176
+ return {
2177
+ requestedStrategy,
2178
+ effectiveStrategy: requestedStrategy,
2179
+ maxContextBlocks,
2180
+ defaultMaxContextBlocks,
2181
+ evidence,
2182
+ evidenceTotal,
2183
+ tendencies,
2184
+ notes: [
2185
+ `requested_budget_strategy=${requestedStrategy}`,
2186
+ `resolved_budget_strategy=${requestedStrategy}`,
2187
+ `resolved_max_context_blocks=${maxContextBlocks}`,
2188
+ "structural_budget_source=compile_structural_signals_empirical_v1",
2189
+ `structural_budget_compile_evidence=matched_unselected:${compileEvidence.expansionCandidates},traversal:${compileEvidence.traversalActivations},overlap_pruned:${compileEvidence.overlapPruned},total:${evidenceTotal}`,
2190
+ `structural_budget_tendencies=split:${tendencies.split.toFixed(4)},merge:${tendencies.merge.toFixed(4)},prune:${tendencies.prune.toFixed(4)},connect:${tendencies.connect.toFixed(4)}`,
2191
+ `structural_budget_pressures=expand:${expansionPressure.toFixed(4)},contract:${contractionPressure.toFixed(4)},directional:${directionalPressure.toFixed(4)}`
2192
+ ]
2193
+ };
2194
+ }
2195
+ function resolveCompileBudget(target, input) {
2196
+ const pack = loadPackFromActivation(target.activationRoot, "active");
2197
+ return deriveEmpiricalStructuralBudget({
2198
+ requestedStrategy: input.budgetStrategy ?? "empirical_v1",
2199
+ ...(input.maxContextBlocks !== undefined ? { requestedMaxContextBlocks: input.maxContextBlocks } : {}),
2200
+ ...(pack?.graph.evolution !== undefined ? { evolution: pack.graph.evolution } : {}),
2201
+ defaultMaxContextBlocks: 4,
2202
+ minimumMaxContextBlocks: 2,
2203
+ maximumMaxContextBlocks: 6
2204
+ });
2205
+ }
913
2206
  export function resolveActivePackForCompile(activationRoot) {
914
2207
  const resolvedActivationRoot = path.resolve(normalizeNonEmptyString(activationRoot, "activationRoot"));
915
2208
  const inspection = inspectActivationState(resolvedActivationRoot);
@@ -932,11 +2225,12 @@ export function compileRuntimeContext(input) {
932
2225
  const runtimeHints = normalizeRuntimeHints(input.runtimeHints);
933
2226
  try {
934
2227
  const target = resolveActivePackForCompile(activationRoot);
2228
+ const resolvedBudget = resolveCompileBudget(target, input);
935
2229
  const compile = compileRuntimeFromActivation(activationRoot, {
936
2230
  contract: CONTRACT_IDS.runtimeCompile,
937
2231
  agentId,
938
2232
  userMessage: normalizeNonEmptyString(input.message, "message"),
939
- maxContextBlocks: normalizeNonNegativeInteger(input.maxContextBlocks, "maxContextBlocks", 4),
2233
+ maxContextBlocks: resolvedBudget.maxContextBlocks,
940
2234
  ...(input.maxContextChars !== undefined
941
2235
  ? { maxContextChars: normalizeNonNegativeInteger(input.maxContextChars, "maxContextChars", input.maxContextChars) }
942
2236
  : {}),
@@ -949,7 +2243,7 @@ export function compileRuntimeContext(input) {
949
2243
  ...compile.response,
950
2244
  diagnostics: {
951
2245
  ...compile.response.diagnostics,
952
- notes: [...compile.response.diagnostics.notes, "OpenClaw remains the runtime owner"]
2246
+ notes: uniqueNotes([...compile.response.diagnostics.notes, ...resolvedBudget.notes, "OpenClaw remains the runtime owner"])
953
2247
  }
954
2248
  };
955
2249
  return {
@@ -981,6 +2275,97 @@ function readDiagnosticNoteList(notes, prefix) {
981
2275
  .map((entry) => entry.trim())
982
2276
  .filter((entry) => entry.length > 0);
983
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
+ }
984
2369
  function sortedUniqueStrings(values) {
985
2370
  return [...new Set(values.filter((value) => value.length > 0))].sort((left, right) => left.localeCompare(right));
986
2371
  }
@@ -1147,6 +2532,7 @@ function buildAttachStatusCompileInput(activationRoot, compile) {
1147
2532
  agentId: normalizeOptionalString(compile?.agentId) ?? `${DEFAULT_AGENT_ID}-attach-status`,
1148
2533
  message: normalizeOptionalString(compile?.message) ?? DEFAULT_ATTACH_STATUS_MESSAGE,
1149
2534
  ...(compile?.maxContextBlocks !== undefined ? { maxContextBlocks: compile.maxContextBlocks } : {}),
2535
+ ...(compile?.budgetStrategy !== undefined ? { budgetStrategy: compile.budgetStrategy } : {}),
1150
2536
  ...(compile?.maxContextChars !== undefined ? { maxContextChars: compile.maxContextChars } : {}),
1151
2537
  ...(compile?.mode !== undefined ? { mode: compile.mode } : {}),
1152
2538
  ...(compile?.compactionMode !== undefined ? { compactionMode: compile.compactionMode } : {}),
@@ -1315,11 +2701,14 @@ export function bootstrapRuntimeAttach(input) {
1315
2701
  });
1316
2702
  const currentProfile = describeCurrentProfileBrainStatus({
1317
2703
  activationRoot,
1318
- updatedAt: activatedAt
2704
+ updatedAt: activatedAt,
2705
+ ...(input.brainAttachmentPolicy !== undefined ? { brainAttachmentPolicy: input.brainAttachmentPolicy } : {}),
2706
+ ...(input.profileId !== undefined ? { profileId: input.profileId } : {})
1319
2707
  });
1320
2708
  return {
1321
2709
  runtimeOwner: "openclaw",
1322
2710
  profileSelector,
2711
+ operatorReadScope: "current_profile_only",
1323
2712
  activationRoot,
1324
2713
  packRoot,
1325
2714
  packId: descriptor.manifest.packId,
@@ -1328,12 +2717,80 @@ export function bootstrapRuntimeAttach(input) {
1328
2717
  currentProfile,
1329
2718
  nextSteps: buildBootstrapRuntimeAttachNextSteps({
1330
2719
  activationRoot,
2720
+ profileSelector,
1331
2721
  currentProfile
1332
2722
  })
1333
2723
  };
1334
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
+ }
1335
2790
  function buildRuntimeTurnAttribution(input) {
2791
+ const profileSelector = normalizeRuntimeProfileSelector(input.turn.profileSelector, "profileSelector");
1336
2792
  const brainAttachmentPolicy = normalizeBrainAttachmentPolicy(input.turn.brainAttachmentPolicy);
2793
+ const profileId = normalizeOptionalString(input.turn.profileId) ?? null;
1337
2794
  if (input.compileResult.ok) {
1338
2795
  const notes = input.compileResult.compileResponse.diagnostics.notes;
1339
2796
  const contextAttribution = buildContextAttributionSummary({
@@ -1345,7 +2802,8 @@ function buildRuntimeTurnAttribution(input) {
1345
2802
  });
1346
2803
  return {
1347
2804
  hostRuntimeOwner: "openclaw",
1348
- profileSelector: "current_profile",
2805
+ profileSelector,
2806
+ profileId,
1349
2807
  brainAttachmentPolicy,
1350
2808
  brainStatus: "serving_active_pack",
1351
2809
  activePackId: input.compileResult.activePackId,
@@ -1353,6 +2811,17 @@ function buildRuntimeTurnAttribution(input) {
1353
2811
  routerIdentity: input.compileResult.compileResponse.diagnostics.routerIdentity,
1354
2812
  selectionDigest: input.compileResult.compileResponse.diagnostics.selectionDigest,
1355
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
+ }),
1356
2825
  contextEvidence: contextAttribution.evidence === "unprobed" ? null : contextAttribution.evidence
1357
2826
  };
1358
2827
  }
@@ -1363,7 +2832,8 @@ function buildRuntimeTurnAttribution(input) {
1363
2832
  });
1364
2833
  return {
1365
2834
  hostRuntimeOwner: "openclaw",
1366
- profileSelector: "current_profile",
2835
+ profileSelector,
2836
+ profileId,
1367
2837
  brainAttachmentPolicy,
1368
2838
  brainStatus: input.compileResult.hardRequirementViolated ? "hard_fail" : "fail_open_static_context",
1369
2839
  activePackId: null,
@@ -1371,6 +2841,17 @@ function buildRuntimeTurnAttribution(input) {
1371
2841
  routerIdentity: null,
1372
2842
  selectionDigest: null,
1373
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
+ }),
1374
2855
  contextEvidence: contextAttribution.evidence === "unprobed" ? null : contextAttribution.evidence
1375
2856
  };
1376
2857
  }
@@ -1388,10 +2869,6 @@ function buildCompileInteractionEvent(input) {
1388
2869
  sessionId: input.turn.sessionId,
1389
2870
  source: input.sourceStream
1390
2871
  });
1391
- const attribution = buildRuntimeTurnAttribution({
1392
- turn: input.turn,
1393
- compileResult: input.compileResult
1394
- });
1395
2872
  return createInteractionEvent({
1396
2873
  eventId,
1397
2874
  agentId: input.agentId,
@@ -1405,7 +2882,8 @@ function buildCompileInteractionEvent(input) {
1405
2882
  stream: input.sourceStream
1406
2883
  },
1407
2884
  packId: input.compileResult.compileResponse.packId,
1408
- attribution
2885
+ principal: buildRuntimeSelfPrincipal(input.turn),
2886
+ attribution: input.attribution
1409
2887
  });
1410
2888
  }
1411
2889
  function buildDeliveryInteractionEvent(input) {
@@ -1426,10 +2904,6 @@ function buildDeliveryInteractionEvent(input) {
1426
2904
  sessionId: input.turn.sessionId,
1427
2905
  source: input.sourceStream
1428
2906
  });
1429
- const attribution = buildRuntimeTurnAttribution({
1430
- turn: input.turn,
1431
- compileResult: input.compileResult
1432
- });
1433
2907
  return createInteractionEvent({
1434
2908
  eventId,
1435
2909
  agentId: input.agentId,
@@ -1442,17 +2916,13 @@ function buildDeliveryInteractionEvent(input) {
1442
2916
  runtimeOwner: "openclaw",
1443
2917
  stream: input.sourceStream
1444
2918
  },
1445
- attribution,
2919
+ attribution: input.attribution,
1446
2920
  ...(input.compileResult.ok ? { packId: input.compileResult.compileResponse.packId } : {}),
1447
2921
  ...(messageId !== undefined ? { messageId } : {})
1448
2922
  });
1449
2923
  }
1450
2924
  function buildFeedbackEvents(input) {
1451
2925
  const feedbackItems = input.turn.feedback ?? [];
1452
- const attribution = buildRuntimeTurnAttribution({
1453
- turn: input.turn,
1454
- compileResult: input.compileResult
1455
- });
1456
2926
  return feedbackItems.map((item, index) => {
1457
2927
  if (item === null) {
1458
2928
  throw new Error(`feedback[${index}] must be an object`);
@@ -1476,6 +2946,12 @@ function buildFeedbackEvents(input) {
1476
2946
  source: input.sourceStream
1477
2947
  });
1478
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
+ });
1479
2955
  return createFeedbackEvent({
1480
2956
  eventId,
1481
2957
  agentId: input.agentId,
@@ -1489,14 +2965,16 @@ function buildFeedbackEvents(input) {
1489
2965
  stream: input.sourceStream
1490
2966
  },
1491
2967
  content,
1492
- attribution,
2968
+ attribution: input.attribution,
1493
2969
  ...(messageId !== undefined ? { messageId } : {}),
2970
+ ...(principal === undefined ? {} : { principal }),
1494
2971
  ...(relatedInteractionId !== undefined ? { relatedInteractionId } : {})
1495
2972
  });
1496
2973
  });
1497
2974
  }
1498
2975
  export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1499
2976
  const agentId = normalizeOptionalString(turn.agentId) ?? process.env.OPENCLAWBRAIN_AGENT_ID ?? DEFAULT_AGENT_ID;
2977
+ const profileSelector = normalizeRuntimeProfileSelector(turn.profileSelector, "profileSelector");
1500
2978
  const sessionId = normalizeNonEmptyString(turn.sessionId, "sessionId");
1501
2979
  const channel = normalizeNonEmptyString(turn.channel, "channel");
1502
2980
  const sourceStream = normalizeOptionalString(turn.sourceStream) ?? `openclaw/runtime/${channel}`;
@@ -1505,12 +2983,19 @@ export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1505
2983
  const normalizedTurn = {
1506
2984
  ...turn,
1507
2985
  agentId,
2986
+ profileSelector,
1508
2987
  channel,
1509
2988
  sessionId
1510
2989
  };
2990
+ const attribution = buildRuntimeTurnAttribution({
2991
+ turn: normalizedTurn,
2992
+ compileResult,
2993
+ sourceStream
2994
+ });
1511
2995
  const compileInteraction = buildCompileInteractionEvent({
1512
2996
  turn: normalizedTurn,
1513
2997
  compileResult,
2998
+ attribution,
1514
2999
  sourceStream,
1515
3000
  nextSequence,
1516
3001
  createdAt: compileCreatedAt,
@@ -1518,6 +3003,7 @@ export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1518
3003
  });
1519
3004
  const feedbackEvents = buildFeedbackEvents({
1520
3005
  turn: normalizedTurn,
3006
+ attribution,
1521
3007
  sourceStream,
1522
3008
  nextSequence,
1523
3009
  defaultCreatedAt: compileCreatedAt,
@@ -1528,6 +3014,7 @@ export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1528
3014
  const deliveryInteraction = buildDeliveryInteractionEvent({
1529
3015
  turn: normalizedTurn,
1530
3016
  compileResult,
3017
+ attribution,
1531
3018
  sourceStream,
1532
3019
  nextSequence,
1533
3020
  defaultCreatedAt: compileCreatedAt,
@@ -1566,21 +3053,33 @@ export function writeRuntimeEventExportBundle(turn, normalizedEventExport) {
1566
3053
  payloadPath: RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT.payload,
1567
3054
  normalizedEventExport
1568
3055
  });
1569
- const manifestPath = path.join(resolvedRoot, RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT.manifest);
1570
- const payloadPath = path.join(resolvedRoot, manifest.payloadPath);
1571
- mkdirSync(path.dirname(payloadPath), { recursive: true });
1572
- writeFileSync(payloadPath, canonicalJson(normalizedEventExport), "utf8");
1573
- writeFileSync(manifestPath, canonicalJson(manifest), "utf8");
1574
- const descriptor = loadRuntimeEventExportBundle(resolvedRoot);
1575
- return {
1576
- ok: true,
1577
- wroteBundle: true,
1578
- normalizedEventExport,
1579
- rootDir: descriptor.rootDir,
1580
- manifestPath: descriptor.manifestPath,
1581
- payloadPath: descriptor.payloadPath,
1582
- manifest: descriptor.manifest
1583
- };
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
+ });
1584
3083
  }
1585
3084
  export function runRuntimeTurn(turn, options = {}) {
1586
3085
  const agentId = normalizeOptionalString(turn.agentId);
@@ -1589,6 +3088,7 @@ export function runRuntimeTurn(turn, options = {}) {
1589
3088
  message: normalizeNonEmptyString(turn.userMessage, "userMessage"),
1590
3089
  ...(agentId !== undefined ? { agentId } : {}),
1591
3090
  ...(turn.maxContextBlocks !== undefined ? { maxContextBlocks: turn.maxContextBlocks } : {}),
3091
+ ...(turn.budgetStrategy !== undefined ? { budgetStrategy: turn.budgetStrategy } : {}),
1592
3092
  ...(turn.mode !== undefined ? { mode: turn.mode } : {}),
1593
3093
  ...(turn.runtimeHints !== undefined ? { runtimeHints: turn.runtimeHints } : {})
1594
3094
  };
@@ -1701,6 +3201,7 @@ export function runContinuousProductLoopTurn(input) {
1701
3201
  const mergedHistory = mergeRuntimeEventHistory(currentState, normalizedEventExport);
1702
3202
  currentState.interactionEvents = mergedHistory.interactionEvents;
1703
3203
  currentState.feedbackEvents = mergedHistory.feedbackEvents;
3204
+ const compileStructuralSignals = turnResult.ok ? turnResult.compileResponse.diagnostics.structuralSignals : undefined;
1704
3205
  try {
1705
3206
  let activeBeforePack = null;
1706
3207
  try {
@@ -1719,6 +3220,7 @@ export function runContinuousProductLoopTurn(input) {
1719
3220
  builtAt: normalizeIsoTimestamp(input.candidateBuiltAt, "candidateBuiltAt", normalizedEventExport.range.lastCreatedAt ?? normalizedEventExport.range.firstCreatedAt),
1720
3221
  ...(input.offlineArtifacts !== undefined ? { offlineArtifacts: input.offlineArtifacts } : {}),
1721
3222
  ...(input.structuralOps !== undefined ? { structuralOps: input.structuralOps } : {}),
3223
+ ...(compileStructuralSignals !== undefined ? { compileStructuralSignals } : {}),
1722
3224
  ...(input.sparseFeedback !== undefined ? { sparseFeedback: input.sparseFeedback } : {}),
1723
3225
  ...(input.liveSliceSize !== undefined ? { liveSliceSize: input.liveSliceSize } : {}),
1724
3226
  ...(input.backfillSliceSize !== undefined ? { backfillSliceSize: input.backfillSliceSize } : {}),
@@ -2029,6 +3531,34 @@ function buildReplayTurnScore(input) {
2029
3531
  qualityScore: Math.min(100, compileScore + phraseScore)
2030
3532
  };
2031
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
+ }
2032
3562
  function buildRecordedSessionTurnReport(turnFixture, result, options) {
2033
3563
  const compileOk = result.ok;
2034
3564
  const selectedContextTexts = compileOk ? result.compileResponse.selectedContext.map((block) => block.text) : [];
@@ -2038,6 +3568,7 @@ function buildRecordedSessionTurnReport(turnFixture, result, options) {
2038
3568
  texts: selectedContextTexts,
2039
3569
  expectedContextPhrases: turnFixture.expectedContextPhrases
2040
3570
  });
3571
+ const observability = buildRecordedSessionTurnObservability(result);
2041
3572
  const eventExportDigest = result.eventExport.ok === true ? result.eventExport.normalizedEventExport.provenance.exportDigest : null;
2042
3573
  return {
2043
3574
  turnId: turnFixture.turnId,
@@ -2058,9 +3589,78 @@ function buildRecordedSessionTurnReport(turnFixture, result, options) {
2058
3589
  qualityScore: scoring.qualityScore,
2059
3590
  compileActiveVersion: options.compileActiveVersion,
2060
3591
  promoted: options.promoted,
3592
+ observability,
2061
3593
  warnings: [...result.warnings]
2062
3594
  };
2063
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
+ }
2064
3664
  function buildRecordedSessionReplayModeSummary(mode, turns) {
2065
3665
  const compileOkCount = turns.filter((turn) => turn.compileOk).length;
2066
3666
  const phraseHitCount = turns.reduce((sum, turn) => sum + turn.phraseHits.length, 0);
@@ -2069,6 +3669,7 @@ function buildRecordedSessionReplayModeSummary(mode, turns) {
2069
3669
  const promotionCount = turns.filter((turn) => turn.promoted).length;
2070
3670
  const qualityScore = turns.length === 0 ? 0 : Math.round(turns.reduce((sum, turn) => sum + turn.qualityScore, 0) / turns.length);
2071
3671
  const packIds = uniqueStringsInOrder(turns.map((turn) => turn.activePackId).filter(isPresent));
3672
+ const scannerEvidence = buildRecordedSessionReplayScannerEvidence(mode, turns);
2072
3673
  const base = {
2073
3674
  mode,
2074
3675
  qualityScore,
@@ -2077,7 +3678,8 @@ function buildRecordedSessionReplayModeSummary(mode, turns) {
2077
3678
  phraseCount,
2078
3679
  usedLearnedRouteTurnCount,
2079
3680
  promotionCount,
2080
- packIds
3681
+ packIds,
3682
+ scannerEvidence
2081
3683
  };
2082
3684
  return {
2083
3685
  ...base,
@@ -2115,6 +3717,7 @@ function buildRecordedSessionReplayScoreHash(modes) {
2115
3717
  usedLearnedRouteTurnCount: mode.summary.usedLearnedRouteTurnCount,
2116
3718
  promotionCount: mode.summary.promotionCount,
2117
3719
  packIds: mode.summary.packIds,
3720
+ scannerEvidence: mode.summary.scannerEvidence,
2118
3721
  scoreHash: mode.summary.scoreHash
2119
3722
  })));
2120
3723
  }
@@ -2145,6 +3748,17 @@ function recordedSessionReplayBundleBase(bundle) {
2145
3748
  expectedContextPhrases: [...turn.expectedContextPhrases],
2146
3749
  phraseHits: [...turn.phraseHits],
2147
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
+ },
2148
3762
  warnings: [...turn.warnings]
2149
3763
  }))
2150
3764
  })),
@@ -2536,6 +4150,39 @@ function summarizeCandidateAheadBy(candidateAheadBy) {
2536
4150
  .map(([field]) => field)
2537
4151
  .sort();
2538
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
+ }
2539
4186
  function isAwaitingFirstExportSlot(slot) {
2540
4187
  return slot !== null && slot.eventRange.count === 0;
2541
4188
  }
@@ -2571,7 +4218,56 @@ function summarizeBrainState(active, observability) {
2571
4218
  detail
2572
4219
  };
2573
4220
  }
4221
+ function summarizeGraphObservability(active, observability) {
4222
+ if (active === null) {
4223
+ return {
4224
+ available: false,
4225
+ runtimePlasticitySource: null,
4226
+ structuralOps: null,
4227
+ changed: null,
4228
+ operationsApplied: [],
4229
+ liveBlockCount: null,
4230
+ prunedBlockCount: null,
4231
+ prePruneBlockCount: null,
4232
+ strongestBlockId: null,
4233
+ operatorSummary: null,
4234
+ detail: "no active pack is pinned, so there is no structural graph surface to inspect"
4235
+ };
4236
+ }
4237
+ const graphEvolution = observability.graphEvolutionLog;
4238
+ if (graphEvolution === null) {
4239
+ return {
4240
+ available: false,
4241
+ runtimePlasticitySource: observability.graphDynamics.runtimePlasticitySource,
4242
+ structuralOps: null,
4243
+ changed: null,
4244
+ operationsApplied: [],
4245
+ liveBlockCount: null,
4246
+ prunedBlockCount: null,
4247
+ prePruneBlockCount: null,
4248
+ strongestBlockId: null,
4249
+ operatorSummary: null,
4250
+ detail: "active pack is present, but the graph evolution log is missing from activation observability"
4251
+ };
4252
+ }
4253
+ return {
4254
+ available: true,
4255
+ runtimePlasticitySource: observability.graphDynamics.runtimePlasticitySource,
4256
+ structuralOps: { ...graphEvolution.structuralOps },
4257
+ changed: graphEvolution.structuralEvolutionSummary.changed,
4258
+ operationsApplied: [...graphEvolution.structuralEvolutionSummary.operationsApplied],
4259
+ liveBlockCount: graphEvolution.structuralEvolutionSummary.liveBlockCount,
4260
+ prunedBlockCount: graphEvolution.structuralEvolutionSummary.prunedBlockCount,
4261
+ prePruneBlockCount: graphEvolution.structuralEvolutionSummary.prePruneBlockCount,
4262
+ strongestBlockId: graphEvolution.strongestBlockId,
4263
+ operatorSummary: graphEvolution.structuralEvolutionSummary.operatorSummary,
4264
+ detail: graphEvolution.structuralEvolutionSummary.changed
4265
+ ? graphEvolution.structuralEvolutionSummary.operatorSummary
4266
+ : "active pack graph is stable with no structural evolution beyond the current promoted artifact"
4267
+ };
4268
+ }
2574
4269
  function summarizeServePath(compile) {
4270
+ const structuralDecision = summarizeStructuralDecisionFromNotes(compile?.notes ?? []);
2575
4271
  if (compile === null) {
2576
4272
  return {
2577
4273
  state: "unprobed",
@@ -2583,6 +4279,13 @@ function summarizeServePath(compile) {
2583
4279
  selectionMode: null,
2584
4280
  refreshStatus: null,
2585
4281
  freshnessChecksum: null,
4282
+ requestedBudgetStrategy: null,
4283
+ resolvedBudgetStrategy: null,
4284
+ resolvedMaxContextBlocks: null,
4285
+ structuralBudgetSource: null,
4286
+ structuralBudgetEvidence: null,
4287
+ structuralBudgetPressures: null,
4288
+ structuralDecision,
2586
4289
  contextAttribution: buildContextAttributionSummary({
2587
4290
  fallbackToStaticContext: false,
2588
4291
  hardRequirementViolated: false,
@@ -2603,6 +4306,13 @@ function summarizeServePath(compile) {
2603
4306
  selectionMode: null,
2604
4307
  refreshStatus: null,
2605
4308
  freshnessChecksum: null,
4309
+ requestedBudgetStrategy: null,
4310
+ resolvedBudgetStrategy: null,
4311
+ resolvedMaxContextBlocks: null,
4312
+ structuralBudgetSource: null,
4313
+ structuralBudgetEvidence: null,
4314
+ structuralBudgetPressures: null,
4315
+ structuralDecision,
2606
4316
  contextAttribution: compile.contextAttribution,
2607
4317
  error: compile.error
2608
4318
  };
@@ -2617,6 +4327,14 @@ function summarizeServePath(compile) {
2617
4327
  selectionMode: readDiagnosticNoteValue(compile.notes, "selection_mode="),
2618
4328
  refreshStatus: readDiagnosticNoteValue(compile.notes, "router_refresh_status="),
2619
4329
  freshnessChecksum: readDiagnosticNoteValue(compile.notes, "router_freshness_checksum="),
4330
+ requestedBudgetStrategy: readDiagnosticNoteValue(compile.notes, "requested_budget_strategy="),
4331
+ resolvedBudgetStrategy: readDiagnosticNoteValue(compile.notes, "resolved_budget_strategy="),
4332
+ resolvedMaxContextBlocks: structuralDecision.resolvedMaxContextBlocks,
4333
+ structuralBudgetSource: readDiagnosticNoteValue(compile.notes, "structural_budget_source="),
4334
+ structuralBudgetEvidence: readDiagnosticNoteValue(compile.notes, "structural_budget_compile_evidence=") ??
4335
+ readDiagnosticNoteValue(compile.notes, "structural_budget_evidence="),
4336
+ structuralBudgetPressures: readDiagnosticNoteValue(compile.notes, "structural_budget_pressures="),
4337
+ structuralDecision,
2620
4338
  contextAttribution: compile.contextAttribution,
2621
4339
  error: compile.error
2622
4340
  };
@@ -2765,11 +4483,18 @@ function summarizeSupervision(input) {
2765
4483
  exportDigest: null,
2766
4484
  exportedAt: null,
2767
4485
  flowing: null,
4486
+ scanPolicy: null,
4487
+ scanSurfaceCount: 0,
4488
+ scanSurfaces: [],
2768
4489
  sourceCount: 0,
2769
4490
  freshestSourceStream: null,
2770
4491
  freshestCreatedAt: null,
2771
4492
  freshestKind: null,
2772
4493
  humanLabelCount: null,
4494
+ selfLabelCount: null,
4495
+ attributedEventCount: null,
4496
+ totalEventCount: null,
4497
+ selectionDigestCount: null,
2773
4498
  sources: [],
2774
4499
  detail: "no event export path supplied"
2775
4500
  };
@@ -2784,11 +4509,18 @@ function summarizeSupervision(input) {
2784
4509
  exportDigest: observability.exportDigest,
2785
4510
  exportedAt: loaded.exportedAt,
2786
4511
  flowing,
4512
+ scanPolicy: observability.learningSurface.scanPolicy,
4513
+ scanSurfaceCount: observability.learningSurface.scanSurfaces.length,
4514
+ scanSurfaces: [...observability.learningSurface.scanSurfaces],
2787
4515
  sourceCount: observability.supervisionFreshnessBySource.length,
2788
4516
  freshestSourceStream: observability.teacherFreshness.sourceStream ?? freshestSource?.sourceStream ?? null,
2789
4517
  freshestCreatedAt: observability.teacherFreshness.freshestCreatedAt ?? freshestSource?.freshestCreatedAt ?? null,
2790
4518
  freshestKind: observability.teacherFreshness.freshestKind ?? freshestSource?.freshestKind ?? null,
2791
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,
2792
4524
  sources: [...observability.teacherFreshness.sources],
2793
4525
  detail: flowing
2794
4526
  ? "human supervision is visible in the supplied export"
@@ -2853,6 +4585,53 @@ function summarizeTeacherLoop(input) {
2853
4585
  detail: "async teacher diagnostics loaded"
2854
4586
  };
2855
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
+ }
2856
4635
  function summarizeAlwaysOnLearning(input, active) {
2857
4636
  const unavailableLag = {
2858
4637
  activeEventRangeEnd: active?.eventRange.end ?? null,
@@ -2868,15 +4647,21 @@ function summarizeAlwaysOnLearning(input, active) {
2868
4647
  bootstrapped: null,
2869
4648
  mode: "unavailable",
2870
4649
  nextPriorityLane: "unavailable",
4650
+ nextPriorityBucket: "unavailable",
4651
+ backlogState: "unavailable",
2871
4652
  pendingLive: null,
2872
4653
  pendingBackfill: null,
2873
4654
  pendingTotal: null,
4655
+ pendingByBucket: null,
2874
4656
  freshLivePriority: null,
2875
4657
  principalCheckpointCount: null,
2876
4658
  pendingPrincipalCount: null,
2877
4659
  oldestUnlearnedPrincipalEvent: null,
4660
+ newestPendingPrincipalEvent: null,
4661
+ leadingPrincipalCheckpoint: null,
2878
4662
  principalCheckpoints: [],
2879
4663
  principalLagToPromotion: unavailableLag,
4664
+ warningStates: ["teacher_snapshot_unavailable"],
2880
4665
  learnedRange: null,
2881
4666
  materializationCount: null,
2882
4667
  lastMaterializedAt: null,
@@ -2895,15 +4680,21 @@ function summarizeAlwaysOnLearning(input, active) {
2895
4680
  bootstrapped: null,
2896
4681
  mode: "unavailable",
2897
4682
  nextPriorityLane: "unavailable",
4683
+ nextPriorityBucket: "unavailable",
4684
+ backlogState: "unavailable",
2898
4685
  pendingLive: null,
2899
4686
  pendingBackfill: null,
2900
4687
  pendingTotal: null,
4688
+ pendingByBucket: null,
2901
4689
  freshLivePriority: null,
2902
4690
  principalCheckpointCount: null,
2903
4691
  pendingPrincipalCount: null,
2904
4692
  oldestUnlearnedPrincipalEvent: null,
4693
+ newestPendingPrincipalEvent: null,
4694
+ leadingPrincipalCheckpoint: null,
2905
4695
  principalCheckpoints: [],
2906
4696
  principalLagToPromotion: unavailableLag,
4697
+ warningStates: ["teacher_snapshot_unavailable"],
2907
4698
  learnedRange: null,
2908
4699
  materializationCount: null,
2909
4700
  lastMaterializedAt: null,
@@ -2926,30 +4717,43 @@ function summarizeAlwaysOnLearning(input, active) {
2926
4717
  const sequenceLag = latestPrincipalSequence === null || activeEventRangeEnd === null
2927
4718
  ? null
2928
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
+ });
2929
4731
  return {
2930
4732
  available: true,
2931
4733
  sourcePath: path.resolve(teacherSnapshotPath),
2932
4734
  bootstrapped: plan.bootstrapped,
2933
4735
  mode: plan.mode,
2934
4736
  nextPriorityLane: plan.nextPriorityLane,
4737
+ nextPriorityBucket: plan.nextPriorityBucket,
4738
+ backlogState,
2935
4739
  pendingLive: plan.pending.live,
2936
4740
  pendingBackfill: plan.pending.backfill,
2937
4741
  pendingTotal: plan.pending.total,
4742
+ pendingByBucket: { ...plan.pending.byBucket },
2938
4743
  freshLivePriority: plan.pending.freshLivePriority,
2939
4744
  principalCheckpointCount: plan.principalBacklog.principalCount,
2940
4745
  pendingPrincipalCount: plan.principalBacklog.pendingEventCount,
2941
4746
  oldestUnlearnedPrincipalEvent: plan.principalBacklog.oldestUnlearnedEvent,
4747
+ newestPendingPrincipalEvent: plan.principalBacklog.newestPendingEvent,
4748
+ leadingPrincipalCheckpoint: plan.principalBacklog.checkpoints[0] ?? null,
2942
4749
  principalCheckpoints: plan.principalBacklog.checkpoints,
2943
4750
  principalLagToPromotion: {
2944
4751
  activeEventRangeEnd,
2945
4752
  latestPrincipalSequence,
2946
4753
  sequenceLag,
2947
- status: sequenceLag === null
2948
- ? "unavailable"
2949
- : sequenceLag === 0
2950
- ? "caught_up"
2951
- : "pending_promotion"
4754
+ status: principalLagStatus
2952
4755
  },
4756
+ warningStates,
2953
4757
  learnedRange: plan.learnedRange === null ? null : { ...plan.learnedRange },
2954
4758
  materializationCount: plan.materialization.count,
2955
4759
  lastMaterializedAt: plan.materialization.lastMaterializedAt,
@@ -2957,13 +4761,17 @@ function summarizeAlwaysOnLearning(input, active) {
2957
4761
  lastMaterializationLane: plan.materialization.lastLane,
2958
4762
  lastMaterializationPriority: plan.materialization.lastPriority,
2959
4763
  lastMaterializedPackId: snapshot.learner.lastMaterialization?.candidate.summary.packId ?? null,
2960
- detail: plan.pending.freshLivePriority
2961
- ? "fresh live slices remain ahead of passive catch-up"
2962
- : plan.pending.backfill > 0
2963
- ? "passive backfill remains queued behind the current live state"
2964
- : plan.bootstrapped
2965
- ? "fast-init has handed off to the current learned export without queued backlog"
2966
- : "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"
2967
4775
  };
2968
4776
  }
2969
4777
  function buildOperatorFindings(report) {
@@ -2996,6 +4804,7 @@ function buildOperatorFindings(report) {
2996
4804
  }
2997
4805
  if (report.servePath.state === "serving_active_pack") {
2998
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"}`);
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"}`);
2999
4808
  }
3000
4809
  else if (report.servePath.state === "fail_open_static_context") {
3001
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");
@@ -3052,6 +4861,22 @@ function buildOperatorFindings(report) {
3052
4861
  else {
3053
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"}`);
3054
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
+ }
3055
4880
  if (!report.teacherLoop.available) {
3056
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");
3057
4882
  }
@@ -3133,7 +4958,7 @@ function summarizeCurrentProfileBrainStatusLevel(input) {
3133
4958
  }
3134
4959
  return input.routeFreshness === "updated" ? "ok" : "warn";
3135
4960
  }
3136
- function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
4961
+ function buildCurrentProfileBrainStatusFromReport(report, policyMode, profileId) {
3137
4962
  const attached = report.active !== null;
3138
4963
  const awaitingFirstExport = isAwaitingFirstExportSlot(report.active);
3139
4964
  const routerIdentity = report.servePath.routerIdentity ?? report.learnedRouting.routerIdentity ?? report.active?.routerIdentity ?? null;
@@ -3158,6 +4983,7 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
3158
4983
  profile: {
3159
4984
  noun: "Profile",
3160
4985
  selector: "current_profile",
4986
+ profileId,
3161
4987
  detail: attached
3162
4988
  ? "The Host resolves the current Profile through the active Attachment boundary only."
3163
4989
  : "The current Profile has no active Brain attachment visible at the Host boundary."
@@ -3214,6 +5040,7 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
3214
5040
  usedLearnedRouteFn: report.servePath.usedLearnedRouteFn,
3215
5041
  failOpen: report.servePath.fallbackToStaticContext,
3216
5042
  awaitingFirstExport,
5043
+ structuralDecision: report.servePath.structuralDecision,
3217
5044
  detail: report.servePath.state === "serving_active_pack"
3218
5045
  ? awaitingFirstExport
3219
5046
  ? `current profile is serving seed-state pack ${activePackId ?? "unknown"} while awaiting the first exported turn`
@@ -3232,6 +5059,7 @@ function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
3232
5059
  export function buildOperatorSurfaceReport(input) {
3233
5060
  const activationRoot = path.resolve(normalizeNonEmptyString(input.activationRoot, "activationRoot"));
3234
5061
  const updatedAt = normalizeIsoTimestamp(input.updatedAt, "updatedAt", new Date().toISOString());
5062
+ const brainAttachmentPolicy = normalizeBrainAttachmentPolicy(input.brainAttachmentPolicy);
3235
5063
  const inspection = inspectActivationState(activationRoot, updatedAt);
3236
5064
  const observability = describeActivationObservability(activationRoot, "active", {
3237
5065
  updatedAt
@@ -3249,6 +5077,7 @@ export function buildOperatorSurfaceReport(input) {
3249
5077
  candidateAheadBy: summarizeCandidateAheadBy(observability.promotionFreshness.candidateAheadBy)
3250
5078
  },
3251
5079
  brain: summarizeBrainState(active, observability),
5080
+ graph: summarizeGraphObservability(active, observability),
3252
5081
  learnedRouting: {
3253
5082
  required: observability.learnedRouteFn.required,
3254
5083
  available: observability.learnedRouteFn.available,
@@ -3288,7 +5117,8 @@ export function buildOperatorSurfaceReport(input) {
3288
5117
  supervision: summarizeSupervision(input),
3289
5118
  learning: summarizeAlwaysOnLearning(input, active),
3290
5119
  teacherLoop: summarizeTeacherLoop(input),
3291
- principal: summarizePrincipalObservability(input, active)
5120
+ principal: summarizePrincipalObservability(input, active),
5121
+ manyProfile: summarizeManyProfileSupport(brainAttachmentPolicy)
3292
5122
  };
3293
5123
  const findings = buildOperatorFindings(reportBase);
3294
5124
  return {
@@ -3299,7 +5129,7 @@ export function buildOperatorSurfaceReport(input) {
3299
5129
  }
3300
5130
  export function describeCurrentProfileBrainStatus(input) {
3301
5131
  const report = buildOperatorSurfaceReport(input);
3302
- return buildCurrentProfileBrainStatusFromReport(report, normalizeBrainAttachmentPolicy(input.brainAttachmentPolicy));
5132
+ return buildCurrentProfileBrainStatusFromReport(report, report.manyProfile.declaredAttachmentPolicy, normalizeOptionalString(input.profileId) ?? null);
3303
5133
  }
3304
5134
  export function formatOperatorRollbackReport(result) {
3305
5135
  const header = result.allowed ? (result.dryRun ? "ROLLBACK ready" : "ROLLBACK ok") : "ROLLBACK blocked";
@@ -3367,4 +5197,7 @@ export { CONTRACT_IDS, buildNormalizedEventExport, createFeedbackEvent, createIn
3367
5197
  export { describeNormalizedEventExportObservability } from "@openclawbrain/event-export";
3368
5198
  export { describeCompileFallbackUsage } from "@openclawbrain/compiler";
3369
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";
3370
5203
  //# sourceMappingURL=index.js.map