@openclawbrain/cli 0.4.14 → 0.4.15

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.
@@ -255,6 +255,134 @@ function extractStatusSignals(statusText) {
255
255
  proofError: statusText.match(/proofError=([^\s]+)/)?.[1] ?? null,
256
256
  };
257
257
  }
258
+ function extractDetailedStatusLine(statusText, prefix) {
259
+ const normalizedPrefix = `${prefix} `;
260
+ return statusText.split(/\r?\n/).find((line) => line.startsWith(normalizedPrefix)) ?? null;
261
+ }
262
+ function extractKeyValuePairs(line) {
263
+ if (typeof line !== "string") {
264
+ return {};
265
+ }
266
+ const pairs = {};
267
+ for (const match of line.matchAll(/([A-Za-z][A-Za-z0-9]*)=([^\s]+)/g)) {
268
+ pairs[match[1]] = match[2];
269
+ }
270
+ return pairs;
271
+ }
272
+ function extractAttachedProfileCoverageEntries(line) {
273
+ if (typeof line !== "string") {
274
+ return [];
275
+ }
276
+ const normalized = line.replace(/^attachedSet\s+/, "");
277
+ const proofPathIndex = normalized.indexOf(" proofPath=");
278
+ const entriesText = (proofPathIndex === -1 ? normalized : normalized.slice(0, proofPathIndex)).trim();
279
+ if (entriesText.length === 0 || entriesText === "none") {
280
+ return [];
281
+ }
282
+ const entries = [];
283
+ let index = 0;
284
+ while (index < entriesText.length) {
285
+ while (index < entriesText.length && entriesText[index] === " ") {
286
+ index += 1;
287
+ }
288
+ if (index >= entriesText.length) {
289
+ break;
290
+ }
291
+ const bracketStart = entriesText.indexOf("[", index);
292
+ if (bracketStart === -1) {
293
+ break;
294
+ }
295
+ const bracketEnd = entriesText.indexOf("]", bracketStart + 1);
296
+ if (bracketEnd === -1) {
297
+ break;
298
+ }
299
+ const rawLabel = entriesText.slice(index, bracketStart).trim();
300
+ const fields = extractKeyValuePairs(entriesText.slice(bracketStart + 1, bracketEnd));
301
+ entries.push({
302
+ label: rawLabel.replace(/^\*/, "").trim(),
303
+ current: rawLabel.startsWith("*"),
304
+ hookFiles: fields.hook ?? "unknown",
305
+ configLoad: fields.config ?? "unknown",
306
+ runtimeLoad: fields.runtime ?? "unknown",
307
+ loadedAt: fields.loadedAt ?? null,
308
+ coverageState: fields.hook === "present" && fields.config === "allows_load" && fields.runtime === "proven"
309
+ ? "covered"
310
+ : "attention"
311
+ });
312
+ index = bracketEnd + 1;
313
+ }
314
+ return entries;
315
+ }
316
+ function buildCoverageSnapshot({ attachedSetLine, runtimeLoadProofSnapshot, openclawHome }) {
317
+ const parsedEntries = extractAttachedProfileCoverageEntries(attachedSetLine);
318
+ const proofProfiles = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
319
+ ? runtimeLoadProofSnapshot.value.profiles
320
+ : [];
321
+ const profiles = parsedEntries.length > 0
322
+ ? parsedEntries
323
+ : proofProfiles.map((profile) => ({
324
+ label: `${profile?.profileId ?? "current_profile"}@${canonicalizeExistingProofPath(profile?.openclawHome ?? "")}`,
325
+ current: canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome),
326
+ hookFiles: "unknown",
327
+ configLoad: "unknown",
328
+ runtimeLoad: "proven",
329
+ loadedAt: profile?.loadedAt ?? null,
330
+ coverageState: "covered"
331
+ }));
332
+ const runtimeProvenCount = profiles.filter((entry) => entry.runtimeLoad === "proven").length;
333
+ return {
334
+ contract: "openclaw_operator_profile_coverage_snapshot.v1",
335
+ generatedAt: new Date().toISOString(),
336
+ openclawHome: canonicalizeExistingProofPath(openclawHome),
337
+ attachedProfileCount: profiles.length,
338
+ runtimeProofProfileCount: proofProfiles.length,
339
+ hookReadyCount: profiles.filter((entry) => entry.hookFiles === "present").length,
340
+ configReadyCount: profiles.filter((entry) => entry.configLoad === "allows_load").length,
341
+ runtimeProvenCount,
342
+ coverageRate: profiles.length === 0 ? null : runtimeProvenCount / profiles.length,
343
+ profiles
344
+ };
345
+ }
346
+ function buildHardeningSnapshot({ attachTruthLine, serveLine, routeFnLine, verdict, statusSignals }) {
347
+ const attachTruth = extractKeyValuePairs(attachTruthLine);
348
+ const serve = extractKeyValuePairs(serveLine);
349
+ const routeFn = extractKeyValuePairs(routeFnLine);
350
+ return {
351
+ contract: "openclaw_operator_hardening_snapshot.v1",
352
+ generatedAt: new Date().toISOString(),
353
+ statusSignals: {
354
+ statusOk: statusSignals.statusOk,
355
+ loadProofReady: statusSignals.loadProofReady,
356
+ runtimeProven: statusSignals.runtimeProven,
357
+ serveActivePack: statusSignals.serveActivePack,
358
+ routeFnAvailable: statusSignals.routeFnAvailable,
359
+ },
360
+ attachTruth: {
361
+ current: attachTruth.current ?? null,
362
+ hook: attachTruth.hook ?? null,
363
+ config: attachTruth.config ?? null,
364
+ runtime: attachTruth.runtime ?? null,
365
+ watcher: attachTruth.watcher ?? null,
366
+ },
367
+ serve: {
368
+ state: serve.state ?? null,
369
+ failOpen: serve.failOpen ?? null,
370
+ hardFail: serve.hardFail ?? null,
371
+ usedRouteFn: serve.usedRouteFn ?? null,
372
+ awaitingFirstExport: serve.awaitingFirstExport ?? null,
373
+ },
374
+ routeFn: {
375
+ available: routeFn.available ?? null,
376
+ freshness: routeFn.freshness ?? null,
377
+ },
378
+ verdict: {
379
+ verdict: verdict.verdict,
380
+ severity: verdict.severity,
381
+ missingProofCount: Array.isArray(verdict.missingProofs) ? verdict.missingProofs.length : 0,
382
+ warningCount: Array.isArray(verdict.warnings) ? verdict.warnings.length : 0,
383
+ }
384
+ };
385
+ }
258
386
 
259
387
  function hasPackagedHookSource(pluginInspectText) {
260
388
  return /Source:\s+.*(?:@openclawbrain[\\/]+openclaw|openclawbrain)[\\/]+dist[\\/]+extension[\\/]+index\.js/m.test(pluginInspectText);
@@ -357,7 +485,7 @@ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, brea
357
485
  };
358
486
  }
359
487
 
360
- function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot }) {
488
+ function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, guardLine, attributionLine, learningPathLine, coverageSnapshot, hardeningSnapshot }) {
361
489
  const passed = [];
362
490
  const missing = [];
363
491
  const warnings = Array.isArray(verdict.warnings) ? verdict.warnings : [];
@@ -413,6 +541,33 @@ function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspec
413
541
  "## Warnings",
414
542
  ...(warnings.length === 0 ? ["- none"] : warnings.map((item) => `- ${item}`)),
415
543
  "",
544
+ "## Runtime Guard",
545
+ ...(guardLine === null
546
+ ? ["- runtime guard line not reported by detailed status"]
547
+ : [`- ${guardLine}`]),
548
+ "",
549
+ "## Learning Attribution",
550
+ ...(attributionLine === null
551
+ ? ["- attribution line not reported by detailed status"]
552
+ : [`- ${attributionLine}`]),
553
+ ...(learningPathLine === null
554
+ ? []
555
+ : [`- ${learningPathLine}`]),
556
+ "",
557
+ "## Coverage snapshot",
558
+ `- attached profiles: ${coverageSnapshot.attachedProfileCount}`,
559
+ `- runtime-proven profiles: ${coverageSnapshot.runtimeProvenCount}/${coverageSnapshot.attachedProfileCount}`,
560
+ `- coverage rate: ${coverageSnapshot.coverageRate === null ? "none" : coverageSnapshot.coverageRate.toFixed(3)}`,
561
+ ...(coverageSnapshot.profiles.length === 0
562
+ ? ["- per-profile: none"]
563
+ : coverageSnapshot.profiles.map((entry) => `- ${entry.current ? "*" : ""}${entry.label} coverage=${entry.coverageState} hook=${entry.hookFiles} config=${entry.configLoad} runtime=${entry.runtimeLoad} loadedAt=${entry.loadedAt ?? "none"}`)),
564
+ "",
565
+ "## Hardening snapshot",
566
+ `- status signals: statusOk=${hardeningSnapshot.statusSignals.statusOk} loadProofReady=${hardeningSnapshot.statusSignals.loadProofReady} runtimeProven=${hardeningSnapshot.statusSignals.runtimeProven} serveActivePack=${hardeningSnapshot.statusSignals.serveActivePack} routeFnAvailable=${hardeningSnapshot.statusSignals.routeFnAvailable}`,
567
+ `- serve: state=${hardeningSnapshot.serve.state ?? "none"} failOpen=${hardeningSnapshot.serve.failOpen ?? "none"} hardFail=${hardeningSnapshot.serve.hardFail ?? "none"} usedRouteFn=${hardeningSnapshot.serve.usedRouteFn ?? "none"}`,
568
+ `- attachTruth: current=${hardeningSnapshot.attachTruth.current ?? "none"} hook=${hardeningSnapshot.attachTruth.hook ?? "none"} config=${hardeningSnapshot.attachTruth.config ?? "none"} runtime=${hardeningSnapshot.attachTruth.runtime ?? "none"}`,
569
+ `- proof verdict: ${hardeningSnapshot.verdict.verdict} severity=${hardeningSnapshot.verdict.severity} warnings=${hardeningSnapshot.verdict.warningCount}`,
570
+ "",
416
571
  "## Step ledger",
417
572
  ...steps.map((step) => `- ${step.stepId}: ${step.skipped ? "skipped" : `${step.resultClass} (${step.captureState})`} - ${step.summary}`),
418
573
  ];
@@ -634,15 +789,28 @@ export function captureOperatorProofBundle(options) {
634
789
  const gatewayLogPath = extractGatewayLogPath(gatewayStatusCapture.stdout);
635
790
  const activationRoot = extractActivationRoot(statusCapture.stdout, options.activationRoot ?? null);
636
791
  const statusSignals = extractStatusSignals(statusCapture.stdout);
792
+ const attachTruthLine = extractDetailedStatusLine(statusCapture.stdout, "attachTruth");
793
+ const attachedSetLine = extractDetailedStatusLine(statusCapture.stdout, "attachedSet");
794
+ const serveLine = extractDetailedStatusLine(statusCapture.stdout, "serve");
795
+ const routeFnLine = extractDetailedStatusLine(statusCapture.stdout, "routeFn");
796
+ const guardLine = extractDetailedStatusLine(statusCapture.stdout, "guard");
797
+ const attributionLine = extractDetailedStatusLine(statusCapture.stdout, "attribution");
798
+ const learningPathLine = extractDetailedStatusLine(statusCapture.stdout, "path");
637
799
  const runtimeLoadProofPath = normalizeReportedProofPath(statusSignals.proofPath)
638
800
  ?? path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
639
801
  const runtimeLoadProofSnapshot = readJsonSnapshot(runtimeLoadProofPath);
640
802
  const gatewayLogText = readTextIfExists(gatewayLogPath);
641
803
  const breadcrumbs = extractStartupBreadcrumbs(gatewayLogText, bundleStartedAt);
804
+ const coverageSnapshot = buildCoverageSnapshot({
805
+ attachedSetLine,
806
+ runtimeLoadProofSnapshot,
807
+ openclawHome: options.openclawHome,
808
+ });
642
809
  writeText(path.join(bundleDir, "extracted-startup-breadcrumbs.log"), breadcrumbs.all.length === 0
643
810
  ? "<no matching breadcrumbs found>\n"
644
811
  : `${breadcrumbs.all.map((entry) => entry.line).join("\n")}\n`);
645
812
  writeJson(path.join(bundleDir, "runtime-load-proof.json"), runtimeLoadProofSnapshot);
813
+ writeJson(path.join(bundleDir, "coverage-snapshot.json"), coverageSnapshot);
646
814
  const verdict = buildVerdict({
647
815
  steps,
648
816
  gatewayStatus: gatewayStatusCapture.stdout,
@@ -652,6 +820,13 @@ export function captureOperatorProofBundle(options) {
652
820
  runtimeLoadProofSnapshot,
653
821
  openclawHome: options.openclawHome,
654
822
  });
823
+ const hardeningSnapshot = buildHardeningSnapshot({
824
+ attachTruthLine,
825
+ serveLine,
826
+ routeFnLine,
827
+ verdict,
828
+ statusSignals,
829
+ });
655
830
  writeJson(path.join(bundleDir, "steps.json"), {
656
831
  bundleStartedAt,
657
832
  openclawHome: canonicalizeExistingProofPath(options.openclawHome),
@@ -664,6 +839,8 @@ export function captureOperatorProofBundle(options) {
664
839
  bundleStartedAt,
665
840
  verdict,
666
841
  statusSignals,
842
+ coverageSnapshot,
843
+ hardeningSnapshot,
667
844
  breadcrumbs: {
668
845
  allCount: breadcrumbs.all.length,
669
846
  postBundleCount: breadcrumbs.afterBundleStart.length,
@@ -671,7 +848,11 @@ export function captureOperatorProofBundle(options) {
671
848
  },
672
849
  runtimeLoadProofPath,
673
850
  runtimeLoadProofError: runtimeLoadProofSnapshot.error,
851
+ guardLine,
852
+ attributionLine,
853
+ learningPathLine,
674
854
  });
855
+ writeJson(path.join(bundleDir, "hardening-snapshot.json"), hardeningSnapshot);
675
856
  writeText(path.join(bundleDir, "summary.md"), buildSummary({
676
857
  options,
677
858
  steps,
@@ -681,6 +862,11 @@ export function captureOperatorProofBundle(options) {
681
862
  statusSignals,
682
863
  breadcrumbs,
683
864
  runtimeLoadProofSnapshot,
865
+ guardLine,
866
+ attributionLine,
867
+ learningPathLine,
868
+ coverageSnapshot,
869
+ hardeningSnapshot,
684
870
  }));
685
871
  return {
686
872
  ok: true,
@@ -691,14 +877,21 @@ export function captureOperatorProofBundle(options) {
691
877
  gatewayLogPath,
692
878
  runtimeLoadProofPath,
693
879
  runtimeLoadProofSnapshot,
880
+ coverageSnapshot,
881
+ hardeningSnapshot,
694
882
  verdict,
695
883
  statusSignals,
884
+ guardLine,
885
+ attributionLine,
886
+ learningPathLine,
696
887
  steps,
697
888
  summaryPath: path.join(bundleDir, "summary.md"),
698
889
  stepsPath: path.join(bundleDir, "steps.json"),
699
890
  verdictPath: path.join(bundleDir, "verdict.json"),
700
891
  breadcrumbPath: path.join(bundleDir, "extracted-startup-breadcrumbs.log"),
701
892
  runtimeLoadProofSnapshotPath: path.join(bundleDir, "runtime-load-proof.json"),
893
+ coverageSnapshotPath: path.join(bundleDir, "coverage-snapshot.json"),
894
+ hardeningSnapshotPath: path.join(bundleDir, "hardening-snapshot.json"),
702
895
  };
703
896
  }
704
897
 
@@ -713,6 +906,8 @@ export function formatOperatorProofResult(result) {
713
906
  ` Verdict: ${result.verdictPath}`,
714
907
  ` Breadcrumbs: ${result.breadcrumbPath}`,
715
908
  ` Runtime proof: ${result.runtimeLoadProofSnapshotPath}`,
909
+ ` Coverage snapshot: ${result.coverageSnapshotPath}`,
910
+ ` Hardening snapshot: ${result.hardeningSnapshotPath}`,
716
911
  ];
717
912
  return lines.join("\n");
718
913
  }
@@ -39,8 +39,75 @@ function normalizeNonNegativeInteger(value, fieldName, fallbackValue) {
39
39
  return value;
40
40
  }
41
41
 
42
- function normalizeMode(value) {
43
- return value ?? "heuristic";
42
+ const RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG = {
43
+ vector_only: {
44
+ routeMode: "heuristic",
45
+ selectionMode: "flat_rank_v1",
46
+ },
47
+ graph_prior_only: {
48
+ routeMode: "heuristic",
49
+ selectionMode: "graph_walk_v1",
50
+ },
51
+ learned_route: {
52
+ routeMode: "learned",
53
+ selectionMode: "graph_walk_v1",
54
+ },
55
+ };
56
+
57
+ function resolveRuntimeComparativeReplayMode(value) {
58
+ return Object.prototype.hasOwnProperty.call(RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG, value)
59
+ ? value
60
+ : null;
61
+ }
62
+
63
+ function resolveCompileModePlan(modeValue, selectionModeValue) {
64
+ const requestedSelectionMode = normalizeCompileSelectionMode(selectionModeValue);
65
+ const comparativeMode = resolveRuntimeComparativeReplayMode(modeValue);
66
+
67
+ if (comparativeMode === null) {
68
+ if (modeValue === undefined) {
69
+ return {
70
+ comparativeMode: null,
71
+ routeMode: "heuristic",
72
+ selectionMode: requestedSelectionMode,
73
+ };
74
+ }
75
+
76
+ if (modeValue === "heuristic" || modeValue === "learned") {
77
+ return {
78
+ comparativeMode: null,
79
+ routeMode: modeValue,
80
+ selectionMode: requestedSelectionMode,
81
+ };
82
+ }
83
+
84
+ throw new Error(
85
+ "mode must be heuristic, learned, vector_only, graph_prior_only, or learned_route",
86
+ );
87
+ }
88
+
89
+ const plan = RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG[comparativeMode];
90
+
91
+ if (requestedSelectionMode !== undefined && requestedSelectionMode !== plan.selectionMode) {
92
+ throw new Error(
93
+ `selectionMode ${requestedSelectionMode} conflicts with comparative mode ${comparativeMode}, expected ${plan.selectionMode}`,
94
+ );
95
+ }
96
+
97
+ return {
98
+ comparativeMode,
99
+ routeMode: plan.routeMode,
100
+ selectionMode: plan.selectionMode,
101
+ };
102
+ }
103
+
104
+ function resolveSyntheticTurnMode(value) {
105
+ const comparativeMode = resolveRuntimeComparativeReplayMode(value);
106
+ if (comparativeMode !== null) {
107
+ return RUNTIME_COMPARATIVE_REPLAY_MODE_CONFIG[comparativeMode].routeMode;
108
+ }
109
+
110
+ return value === "heuristic" || value === "learned" ? value : undefined;
44
111
  }
45
112
 
46
113
  function normalizeCompileSelectionMode(value) {
@@ -83,7 +150,7 @@ function formatPromptContext(compileResponse) {
83
150
  }
84
151
 
85
152
  for (const block of compileResponse.selectedContext) {
86
- lines.push(`SOURCE: ${block.source}`);
153
+ lines.push(`PROVENANCE_REF: ctx_${block.id}`);
87
154
  lines.push(`BLOCK_ID: ${block.id}`);
88
155
  lines.push(block.text.trim());
89
156
  lines.push("");
@@ -273,8 +340,10 @@ function appendCompileServeRouteDecisionLog(input) {
273
340
  syntheticTurn.budgetStrategy = input.compileInput.budgetStrategy;
274
341
  }
275
342
 
276
- if (input.compileInput.mode === "heuristic" || input.compileInput.mode === "learned") {
277
- syntheticTurn.mode = input.compileInput.mode;
343
+ const syntheticTurnMode = resolveSyntheticTurnMode(input.compileInput.mode);
344
+
345
+ if (syntheticTurnMode !== undefined) {
346
+ syntheticTurn.mode = syntheticTurnMode;
278
347
  }
279
348
 
280
349
  if (input.compileInput.runtimeHints !== undefined) {
@@ -456,6 +525,7 @@ export function compileRuntimeContext(input) {
456
525
  let activationRoot = fallbackActivationRoot;
457
526
  let agentId = process.env.OPENCLAWBRAIN_AGENT_ID ?? DEFAULT_AGENT_ID;
458
527
  let runtimeHints = [];
528
+ let comparativeMode = null;
459
529
  let selectionMode;
460
530
  let userMessage = "";
461
531
  let maxContextChars;
@@ -470,13 +540,15 @@ export function compileRuntimeContext(input) {
470
540
  activationRoot = path.resolve(normalizeNonEmptyString(input.activationRoot, "activationRoot"));
471
541
  agentId = normalizeOptionalString(input.agentId) ?? process.env.OPENCLAWBRAIN_AGENT_ID ?? DEFAULT_AGENT_ID;
472
542
  runtimeHints = normalizeRuntimeHints(input.runtimeHints);
473
- selectionMode = normalizeCompileSelectionMode(input.selectionMode);
474
543
  userMessage = normalizeNonEmptyString(input.message, "message");
475
544
  maxContextChars =
476
545
  input.maxContextChars !== undefined
477
546
  ? normalizeNonNegativeInteger(input.maxContextChars, "maxContextChars", input.maxContextChars)
478
547
  : undefined;
479
- mode = normalizeMode(input.mode);
548
+ const compileModePlan = resolveCompileModePlan(input.mode, input.selectionMode);
549
+ comparativeMode = compileModePlan.comparativeMode;
550
+ mode = compileModePlan.routeMode;
551
+ selectionMode = compileModePlan.selectionMode;
480
552
  } catch (error) {
481
553
  result = failOpenCompileResult(
482
554
  error,
@@ -518,6 +590,7 @@ export function compileRuntimeContext(input) {
518
590
  },
519
591
  );
520
592
  routeSelectionMs = elapsedMsFrom(routeSelectionStartedAtNs);
593
+ const selectionEngine = selectionMode ?? "flat_rank_v1";
521
594
  const compileResponse = {
522
595
  ...compile.response,
523
596
  diagnostics: {
@@ -525,6 +598,13 @@ export function compileRuntimeContext(input) {
525
598
  notes: uniqueNotes([
526
599
  ...compile.response.diagnostics.notes,
527
600
  ...resolvedBudget.notes,
601
+ `selection_engine=${selectionEngine}`,
602
+ ...(comparativeMode === null
603
+ ? []
604
+ : [
605
+ `comparative_mode=${comparativeMode}`,
606
+ `comparative_mode_plan=${mode}+${selectionEngine}`,
607
+ ]),
528
608
  "OpenClaw remains the runtime owner",
529
609
  ]),
530
610
  },
@@ -7,14 +7,43 @@ function isSeedAwaitingFirstPromotion(status) {
7
7
  function normalizeOptionalString(value) {
8
8
  return typeof value === "string" && value.trim().length > 0 ? value : null;
9
9
  }
10
+ function formatOperatorLearningAttributionSummary({ status }) {
11
+ const attribution = status?.learningAttribution ?? null;
12
+ if (!attribution) {
13
+ return "quality=unavailable source=unavailable detail=no_learning_attribution_surface";
14
+ }
15
+ const source = [normalizeOptionalString(attribution.source), normalizeOptionalString(attribution.snapshotKind)]
16
+ .filter((value) => value !== null)
17
+ .join("/");
18
+ if (attribution.available !== true) {
19
+ return `quality=${normalizeOptionalString(attribution.quality) ?? "unavailable"} source=${source || "unavailable"} detail=${normalizeOptionalString(attribution.detail) ?? "unavailable"}`;
20
+ }
21
+ const matchedByMode = attribution.matchedByMode ?? {};
22
+ return [
23
+ `quality=${normalizeOptionalString(attribution.quality) ?? "unavailable"}`,
24
+ `source=${source || "latest_materialization"}`,
25
+ `nonZero=${attribution.nonZeroObservationCount ?? 0}`,
26
+ `exact=${attribution.exactMatchCount ?? 0}`,
27
+ `heuristic=${attribution.heuristicMatchCount ?? 0}`,
28
+ `unmatched=${attribution.unmatchedCount ?? 0}`,
29
+ `ambiguous=${attribution.ambiguousCount ?? 0}`,
30
+ `modes=decision:${matchedByMode.exactDecisionId ?? 0}|digest:${matchedByMode.exactSelectionDigest ?? 0}|compile:${matchedByMode.turnCompileEventId ?? 0}|heuristic:${matchedByMode.legacyHeuristic ?? 0}`
31
+ ].join(" ");
32
+ }
10
33
  export function formatOperatorLearningPathSummary({ status, learningPath, tracedLearning }) {
34
+ const attribution = status?.learningAttribution ?? null;
11
35
  if (!isSeedAwaitingFirstPromotion(status)) {
12
- return formatRawLearningPathSummary(learningPath);
36
+ const rawSummary = formatRawLearningPathSummary(learningPath);
37
+ const bindingQuality = normalizeOptionalString(attribution?.quality);
38
+ return bindingQuality === null || bindingQuality === "unavailable"
39
+ ? rawSummary
40
+ : `${rawSummary} bindingQuality=${bindingQuality}`;
13
41
  }
14
42
  const detailParts = [
15
43
  "detail=seed_state_awaiting_first_promotion",
16
44
  `tracedPg=${normalizeOptionalString(tracedLearning?.pgVersionUsed) ?? "none"}`,
17
- `tracedPack=${normalizeOptionalString(tracedLearning?.materializedPackId) ?? "none"}`
45
+ `tracedPack=${normalizeOptionalString(tracedLearning?.materializedPackId) ?? "none"}`,
46
+ `bindingQuality=${normalizeOptionalString(attribution?.quality) ?? "unavailable"}`
18
47
  ];
19
48
  return [
20
49
  "source=seed_state",
@@ -26,3 +55,4 @@ export function formatOperatorLearningPathSummary({ status, learningPath, traced
26
55
  ...detailParts
27
56
  ].join(" ");
28
57
  }
58
+ export { formatOperatorLearningAttributionSummary };
@@ -80,6 +80,14 @@ function readInteractionActivePackGraphChecksum(interaction) {
80
80
  ?? undefined;
81
81
  }
82
82
 
83
+ function readInteractionExplicitTurnCompileEventId(interaction) {
84
+ return normalizeOptionalString(interaction?.turnCompileEventId)
85
+ ?? normalizeOptionalString(toRecord(interaction?.routeMetadata)?.turnCompileEventId)
86
+ ?? normalizeOptionalString(toRecord(interaction?.decisionProvenance)?.turnCompileEventId)
87
+ ?? normalizeOptionalString(toRecord(interaction?.metadata)?.turnCompileEventId)
88
+ ?? undefined;
89
+ }
90
+
83
91
  function buildDecisionTimestamps(decision) {
84
92
  const timestamps = [];
85
93
  const turnCreatedAt = toTimestamp(decision.turnCreatedAt);
@@ -139,8 +147,10 @@ export function createServeTimeDecisionMatcher(decisions, options = {}) {
139
147
  const decisionsByRecordId = new Map();
140
148
  const decisionsBySelectionDigest = new Map();
141
149
  const ambiguousSelectionDigests = new Set();
142
- const exactDecisions = new Map();
150
+ const decisionsByTurnCompileEventId = new Map();
151
+ const ambiguousTurnCompileEventIds = new Set();
143
152
  const fallbackDecisions = new Map();
153
+ const ambiguousFallbackDecisionKeys = new Set();
144
154
  const decisionsBySessionChannel = new Map();
145
155
  const globalFallbackDecisions = [];
146
156
 
@@ -164,15 +174,27 @@ export function createServeTimeDecisionMatcher(decisions, options = {}) {
164
174
  }
165
175
  }
166
176
  const turnCompileEventId = normalizeOptionalString(decision.turnCompileEventId);
167
- if (turnCompileEventId !== undefined && !exactDecisions.has(turnCompileEventId)) {
168
- exactDecisions.set(turnCompileEventId, decision);
177
+ if (turnCompileEventId !== undefined) {
178
+ if (decisionsByTurnCompileEventId.has(turnCompileEventId)) {
179
+ decisionsByTurnCompileEventId.delete(turnCompileEventId);
180
+ ambiguousTurnCompileEventIds.add(turnCompileEventId);
181
+ }
182
+ else if (!ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
183
+ decisionsByTurnCompileEventId.set(turnCompileEventId, decision);
184
+ }
169
185
  }
170
186
  for (const candidateKey of [
171
187
  buildCandidateKey(decision.sessionId, decision.channel, decision.turnCreatedAt),
172
188
  buildCandidateKey(decision.sessionId, decision.channel, decision.recordedAt),
173
189
  ]) {
174
- if (candidateKey !== null && !fallbackDecisions.has(candidateKey)) {
175
- fallbackDecisions.set(candidateKey, decision);
190
+ if (candidateKey !== null) {
191
+ if (fallbackDecisions.has(candidateKey)) {
192
+ fallbackDecisions.delete(candidateKey);
193
+ ambiguousFallbackDecisionKeys.add(candidateKey);
194
+ }
195
+ else if (!ambiguousFallbackDecisionKeys.has(candidateKey)) {
196
+ fallbackDecisions.set(candidateKey, decision);
197
+ }
176
198
  }
177
199
  }
178
200
  const sessionChannelKey = buildSessionChannelKey(decision.sessionId, decision.channel);
@@ -203,22 +225,37 @@ export function createServeTimeDecisionMatcher(decisions, options = {}) {
203
225
  if (decisionRecordId !== undefined) {
204
226
  return decisionsByRecordId.get(decisionRecordId) ?? null;
205
227
  }
206
- const selectionDigestKey = buildSelectionDigestKey(
207
- readInteractionSelectionDigest(interaction),
208
- readInteractionActivePackGraphChecksum(interaction),
209
- );
228
+ const interactionSelectionDigest = readInteractionSelectionDigest(interaction);
229
+ const interactionGraphChecksum = readInteractionActivePackGraphChecksum(interaction);
230
+ const selectionDigestKey = buildSelectionDigestKey(interactionSelectionDigest, interactionGraphChecksum);
210
231
  if (selectionDigestKey !== null) {
211
232
  if (ambiguousSelectionDigests.has(selectionDigestKey)) {
212
233
  return null;
213
234
  }
214
235
  return decisionsBySelectionDigest.get(selectionDigestKey) ?? null;
215
236
  }
216
- const exact = exactDecisions.get(interaction.eventId);
237
+ if (interactionSelectionDigest !== undefined || interactionGraphChecksum !== undefined) {
238
+ return null;
239
+ }
240
+ const explicitTurnCompileEventId = readInteractionExplicitTurnCompileEventId(interaction);
241
+ if (explicitTurnCompileEventId !== undefined) {
242
+ if (ambiguousTurnCompileEventIds.has(explicitTurnCompileEventId)) {
243
+ return null;
244
+ }
245
+ return decisionsByTurnCompileEventId.get(explicitTurnCompileEventId) ?? null;
246
+ }
247
+ const softTurnCompileEventId = normalizeOptionalString(interaction.eventId);
248
+ const exact = softTurnCompileEventId === undefined || ambiguousTurnCompileEventIds.has(softTurnCompileEventId)
249
+ ? undefined
250
+ : decisionsByTurnCompileEventId.get(softTurnCompileEventId);
217
251
  if (exact !== undefined) {
218
252
  return exact;
219
253
  }
220
254
  const exactFallbackKey = buildCandidateKey(interaction.sessionId, interaction.channel, interaction.createdAt);
221
255
  if (exactFallbackKey !== null) {
256
+ if (ambiguousFallbackDecisionKeys.has(exactFallbackKey)) {
257
+ return null;
258
+ }
222
259
  const fallback = fallbackDecisions.get(exactFallbackKey);
223
260
  if (fallback !== undefined) {
224
261
  return fallback;
@@ -18,6 +18,22 @@ function normalizeOptionalString(value) {
18
18
  function normalizeSource(value) {
19
19
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
20
20
  }
21
+ function summarizeBridgeSource(value) {
22
+ const source = normalizeSource(value);
23
+ if (source === null) {
24
+ return null;
25
+ }
26
+ const summarized = {
27
+ command: normalizeOptionalString(source.command),
28
+ bridge: normalizeOptionalString(source.bridge),
29
+ brainRoot: normalizeOptionalString(source.brainRoot),
30
+ stateDbPath: normalizeOptionalString(source.stateDbPath),
31
+ persistedKey: normalizeOptionalString(source.persistedKey),
32
+ candidatePackVersion: Number.isFinite(source.candidatePackVersion) ? Math.trunc(source.candidatePackVersion) : undefined,
33
+ candidateUpdateCount: normalizeCount(source.candidateUpdateCount)
34
+ };
35
+ return Object.fromEntries(Object.entries(summarized).filter(([, candidate]) => candidate !== null && candidate !== undefined));
36
+ }
21
37
  function normalizeBridgePayload(payload) {
22
38
  if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
23
39
  throw new Error("expected traced-learning bridge payload object");
@@ -579,7 +595,7 @@ export function mergeTracedLearningBridgePayload(payload, persisted) {
579
595
  supervisionCount: persistedBridge.supervisionCount,
580
596
  routerUpdateCount: persistedBridge.routerUpdateCount,
581
597
  teacherArtifactCount: persistedBridge.teacherArtifactCount,
582
- source: persistedBridge.source
598
+ source: summarizeBridgeSource(persistedBridge.source)
583
599
  }
584
600
  }
585
601
  });