@openclawbrain/cli 0.4.14 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);
@@ -270,8 +398,10 @@ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, brea
270
398
  const runtimeProofMatched = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
271
399
  && runtimeLoadProofSnapshot.value.profiles.some((profile) => canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome));
272
400
  const runtimeTruthGaps = [];
273
- if (!statusSignals.statusOk)
274
- runtimeTruthGaps.push("status_ok");
401
+ const strongRuntimeTruth = statusSignals.loadProofReady
402
+ && statusSignals.runtimeProven
403
+ && statusSignals.serveActivePack
404
+ && statusSignals.routeFnAvailable;
275
405
  if (!statusSignals.loadProofReady)
276
406
  runtimeTruthGaps.push("load_proof");
277
407
  if (!statusSignals.runtimeProven)
@@ -282,6 +412,15 @@ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, brea
282
412
  runtimeTruthGaps.push("route_fn");
283
413
  const warningCodes = [];
284
414
  const warnings = [];
415
+ if (!statusSignals.statusOk) {
416
+ if (strongRuntimeTruth) {
417
+ warningCodes.push("status_warn");
418
+ warnings.push("detailed status did not return STATUS ok, but loadProof/runtime/serve/routeFn proofs stayed healthy");
419
+ }
420
+ else {
421
+ runtimeTruthGaps.push("status_ok");
422
+ }
423
+ }
285
424
  if (!gatewayHealthy) {
286
425
  warningCodes.push("gateway_health");
287
426
  warnings.push("gateway status did not confirm runtime running and RPC probe ok");
@@ -357,7 +496,7 @@ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, brea
357
496
  };
358
497
  }
359
498
 
360
- function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot }) {
499
+ function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspectText, statusSignals, breadcrumbs, runtimeLoadProofSnapshot, guardLine, attributionLine, learningPathLine, coverageSnapshot, hardeningSnapshot }) {
361
500
  const passed = [];
362
501
  const missing = [];
363
502
  const warnings = Array.isArray(verdict.warnings) ? verdict.warnings : [];
@@ -413,6 +552,33 @@ function buildSummary({ options, steps, verdict, gatewayStatusText, pluginInspec
413
552
  "## Warnings",
414
553
  ...(warnings.length === 0 ? ["- none"] : warnings.map((item) => `- ${item}`)),
415
554
  "",
555
+ "## Runtime Guard",
556
+ ...(guardLine === null
557
+ ? ["- runtime guard line not reported by detailed status"]
558
+ : [`- ${guardLine}`]),
559
+ "",
560
+ "## Learning Attribution",
561
+ ...(attributionLine === null
562
+ ? ["- attribution line not reported by detailed status"]
563
+ : [`- ${attributionLine}`]),
564
+ ...(learningPathLine === null
565
+ ? []
566
+ : [`- ${learningPathLine}`]),
567
+ "",
568
+ "## Coverage snapshot",
569
+ `- attached profiles: ${coverageSnapshot.attachedProfileCount}`,
570
+ `- runtime-proven profiles: ${coverageSnapshot.runtimeProvenCount}/${coverageSnapshot.attachedProfileCount}`,
571
+ `- coverage rate: ${coverageSnapshot.coverageRate === null ? "none" : coverageSnapshot.coverageRate.toFixed(3)}`,
572
+ ...(coverageSnapshot.profiles.length === 0
573
+ ? ["- per-profile: none"]
574
+ : coverageSnapshot.profiles.map((entry) => `- ${entry.current ? "*" : ""}${entry.label} coverage=${entry.coverageState} hook=${entry.hookFiles} config=${entry.configLoad} runtime=${entry.runtimeLoad} loadedAt=${entry.loadedAt ?? "none"}`)),
575
+ "",
576
+ "## Hardening snapshot",
577
+ `- status signals: statusOk=${hardeningSnapshot.statusSignals.statusOk} loadProofReady=${hardeningSnapshot.statusSignals.loadProofReady} runtimeProven=${hardeningSnapshot.statusSignals.runtimeProven} serveActivePack=${hardeningSnapshot.statusSignals.serveActivePack} routeFnAvailable=${hardeningSnapshot.statusSignals.routeFnAvailable}`,
578
+ `- serve: state=${hardeningSnapshot.serve.state ?? "none"} failOpen=${hardeningSnapshot.serve.failOpen ?? "none"} hardFail=${hardeningSnapshot.serve.hardFail ?? "none"} usedRouteFn=${hardeningSnapshot.serve.usedRouteFn ?? "none"}`,
579
+ `- attachTruth: current=${hardeningSnapshot.attachTruth.current ?? "none"} hook=${hardeningSnapshot.attachTruth.hook ?? "none"} config=${hardeningSnapshot.attachTruth.config ?? "none"} runtime=${hardeningSnapshot.attachTruth.runtime ?? "none"}`,
580
+ `- proof verdict: ${hardeningSnapshot.verdict.verdict} severity=${hardeningSnapshot.verdict.severity} warnings=${hardeningSnapshot.verdict.warningCount}`,
581
+ "",
416
582
  "## Step ledger",
417
583
  ...steps.map((step) => `- ${step.stepId}: ${step.skipped ? "skipped" : `${step.resultClass} (${step.captureState})`} - ${step.summary}`),
418
584
  ];
@@ -442,6 +608,17 @@ function buildGatewayArgs(action, profileName) {
442
608
  : ["gateway", action, "--profile", profileName];
443
609
  }
444
610
 
611
+ function buildGatewayStatusArgs(profileName, gatewayUrl, gatewayToken) {
612
+ const args = buildGatewayArgs("status", profileName);
613
+ if (gatewayUrl !== null) {
614
+ args.push("--url", gatewayUrl);
615
+ }
616
+ if (gatewayToken !== null) {
617
+ args.push("--token", gatewayToken);
618
+ }
619
+ return args;
620
+ }
621
+
445
622
  export function buildProofCommandForOpenClawHome(openclawHome) {
446
623
  return `openclawbrain proof --openclaw-home ${quoteShellArg(path.resolve(openclawHome))}`;
447
624
  }
@@ -454,6 +631,8 @@ export function buildProofCommandHelpSection() {
454
631
  " --skip-install Capture proof without rerunning install first (proof only).",
455
632
  " --skip-restart Capture proof without restarting OpenClaw first (proof only).",
456
633
  ` --plugin-id <id> Plugin id for \`openclaw plugins inspect\` (proof only; default: ${DEFAULT_OPERATOR_PROOF_PLUGIN_ID}).`,
634
+ " --gateway-url <url> Override the gateway-status probe target for proof capture (proof only).",
635
+ " --gateway-token <token> Gateway token to use with --gateway-url or other non-default proof probes.",
457
636
  ` --timeout-ms <ms> Per-step timeout in ms for proof capture (proof only; default: ${DEFAULT_OPERATOR_PROOF_TIMEOUT_MS}).`,
458
637
  ],
459
638
  lifecycle: " 5. proof openclawbrain proof --openclaw-home <path> - capture one durable operator proof bundle after install/restart/status",
@@ -469,6 +648,8 @@ export function parseProofCliArgs(argv, options = {}) {
469
648
  let skipInstall = false;
470
649
  let skipRestart = false;
471
650
  let pluginId = DEFAULT_OPERATOR_PROOF_PLUGIN_ID;
651
+ let gatewayUrl = null;
652
+ let gatewayToken = null;
472
653
  let timeoutMs = DEFAULT_OPERATOR_PROOF_TIMEOUT_MS;
473
654
  let json = false;
474
655
  let help = false;
@@ -526,6 +707,24 @@ export function parseProofCliArgs(argv, options = {}) {
526
707
  index += 1;
527
708
  continue;
528
709
  }
710
+ if (arg === "--gateway-url") {
711
+ const next = argv[index + 1];
712
+ if (next === undefined) {
713
+ throw new Error("--gateway-url requires a value");
714
+ }
715
+ gatewayUrl = next;
716
+ index += 1;
717
+ continue;
718
+ }
719
+ if (arg === "--gateway-token") {
720
+ const next = argv[index + 1];
721
+ if (next === undefined) {
722
+ throw new Error("--gateway-token requires a value");
723
+ }
724
+ gatewayToken = next;
725
+ index += 1;
726
+ continue;
727
+ }
529
728
  if (arg === "--timeout-ms") {
530
729
  const next = argv[index + 1];
531
730
  if (next === undefined) {
@@ -550,6 +749,8 @@ export function parseProofCliArgs(argv, options = {}) {
550
749
  skipInstall,
551
750
  skipRestart,
552
751
  pluginId,
752
+ gatewayUrl,
753
+ gatewayToken,
553
754
  timeoutMs,
554
755
  json,
555
756
  help
@@ -570,6 +771,8 @@ export function parseProofCliArgs(argv, options = {}) {
570
771
  skipInstall,
571
772
  skipRestart,
572
773
  pluginId,
774
+ gatewayUrl,
775
+ gatewayToken,
573
776
  timeoutMs,
574
777
  json,
575
778
  help
@@ -628,21 +831,38 @@ export function captureOperatorProofBundle(options) {
628
831
  }
629
832
  addStep("01-install", "install", cliInvocation.command, [...cliInvocation.args, "install", "--openclaw-home", options.openclawHome], { skipped: options.skipInstall === true });
630
833
  addStep("02-restart", "gateway restart", "openclaw", buildGatewayArgs("restart", gatewayProfile), { skipped: options.skipRestart === true });
631
- const gatewayStatusCapture = addStep("03-gateway-status", "gateway status", "openclaw", buildGatewayArgs("status", gatewayProfile));
834
+ const gatewayStatusCapture = addStep("03-gateway-status", "gateway status", "openclaw", buildGatewayStatusArgs(
835
+ gatewayProfile,
836
+ normalizeOptionalCliString(options.gatewayUrl ?? null),
837
+ normalizeOptionalCliString(options.gatewayToken ?? null),
838
+ ));
632
839
  const pluginInspectCapture = addStep("04-plugin-inspect", "plugin inspect", "openclaw", ["plugins", "inspect", options.pluginId]);
633
840
  const statusCapture = addStep("05-detailed-status", "detailed status", cliInvocation.command, [...cliInvocation.args, "status", "--openclaw-home", options.openclawHome, "--detailed"]);
634
841
  const gatewayLogPath = extractGatewayLogPath(gatewayStatusCapture.stdout);
635
842
  const activationRoot = extractActivationRoot(statusCapture.stdout, options.activationRoot ?? null);
636
843
  const statusSignals = extractStatusSignals(statusCapture.stdout);
844
+ const attachTruthLine = extractDetailedStatusLine(statusCapture.stdout, "attachTruth");
845
+ const attachedSetLine = extractDetailedStatusLine(statusCapture.stdout, "attachedSet");
846
+ const serveLine = extractDetailedStatusLine(statusCapture.stdout, "serve");
847
+ const routeFnLine = extractDetailedStatusLine(statusCapture.stdout, "routeFn");
848
+ const guardLine = extractDetailedStatusLine(statusCapture.stdout, "guard");
849
+ const attributionLine = extractDetailedStatusLine(statusCapture.stdout, "attribution");
850
+ const learningPathLine = extractDetailedStatusLine(statusCapture.stdout, "path");
637
851
  const runtimeLoadProofPath = normalizeReportedProofPath(statusSignals.proofPath)
638
852
  ?? path.join(activationRoot, "attachment-truth", "runtime-load-proofs.json");
639
853
  const runtimeLoadProofSnapshot = readJsonSnapshot(runtimeLoadProofPath);
640
854
  const gatewayLogText = readTextIfExists(gatewayLogPath);
641
855
  const breadcrumbs = extractStartupBreadcrumbs(gatewayLogText, bundleStartedAt);
856
+ const coverageSnapshot = buildCoverageSnapshot({
857
+ attachedSetLine,
858
+ runtimeLoadProofSnapshot,
859
+ openclawHome: options.openclawHome,
860
+ });
642
861
  writeText(path.join(bundleDir, "extracted-startup-breadcrumbs.log"), breadcrumbs.all.length === 0
643
862
  ? "<no matching breadcrumbs found>\n"
644
863
  : `${breadcrumbs.all.map((entry) => entry.line).join("\n")}\n`);
645
864
  writeJson(path.join(bundleDir, "runtime-load-proof.json"), runtimeLoadProofSnapshot);
865
+ writeJson(path.join(bundleDir, "coverage-snapshot.json"), coverageSnapshot);
646
866
  const verdict = buildVerdict({
647
867
  steps,
648
868
  gatewayStatus: gatewayStatusCapture.stdout,
@@ -652,6 +872,13 @@ export function captureOperatorProofBundle(options) {
652
872
  runtimeLoadProofSnapshot,
653
873
  openclawHome: options.openclawHome,
654
874
  });
875
+ const hardeningSnapshot = buildHardeningSnapshot({
876
+ attachTruthLine,
877
+ serveLine,
878
+ routeFnLine,
879
+ verdict,
880
+ statusSignals,
881
+ });
655
882
  writeJson(path.join(bundleDir, "steps.json"), {
656
883
  bundleStartedAt,
657
884
  openclawHome: canonicalizeExistingProofPath(options.openclawHome),
@@ -664,6 +891,8 @@ export function captureOperatorProofBundle(options) {
664
891
  bundleStartedAt,
665
892
  verdict,
666
893
  statusSignals,
894
+ coverageSnapshot,
895
+ hardeningSnapshot,
667
896
  breadcrumbs: {
668
897
  allCount: breadcrumbs.all.length,
669
898
  postBundleCount: breadcrumbs.afterBundleStart.length,
@@ -671,7 +900,11 @@ export function captureOperatorProofBundle(options) {
671
900
  },
672
901
  runtimeLoadProofPath,
673
902
  runtimeLoadProofError: runtimeLoadProofSnapshot.error,
903
+ guardLine,
904
+ attributionLine,
905
+ learningPathLine,
674
906
  });
907
+ writeJson(path.join(bundleDir, "hardening-snapshot.json"), hardeningSnapshot);
675
908
  writeText(path.join(bundleDir, "summary.md"), buildSummary({
676
909
  options,
677
910
  steps,
@@ -681,6 +914,11 @@ export function captureOperatorProofBundle(options) {
681
914
  statusSignals,
682
915
  breadcrumbs,
683
916
  runtimeLoadProofSnapshot,
917
+ guardLine,
918
+ attributionLine,
919
+ learningPathLine,
920
+ coverageSnapshot,
921
+ hardeningSnapshot,
684
922
  }));
685
923
  return {
686
924
  ok: true,
@@ -691,14 +929,21 @@ export function captureOperatorProofBundle(options) {
691
929
  gatewayLogPath,
692
930
  runtimeLoadProofPath,
693
931
  runtimeLoadProofSnapshot,
932
+ coverageSnapshot,
933
+ hardeningSnapshot,
694
934
  verdict,
695
935
  statusSignals,
936
+ guardLine,
937
+ attributionLine,
938
+ learningPathLine,
696
939
  steps,
697
940
  summaryPath: path.join(bundleDir, "summary.md"),
698
941
  stepsPath: path.join(bundleDir, "steps.json"),
699
942
  verdictPath: path.join(bundleDir, "verdict.json"),
700
943
  breadcrumbPath: path.join(bundleDir, "extracted-startup-breadcrumbs.log"),
701
944
  runtimeLoadProofSnapshotPath: path.join(bundleDir, "runtime-load-proof.json"),
945
+ coverageSnapshotPath: path.join(bundleDir, "coverage-snapshot.json"),
946
+ hardeningSnapshotPath: path.join(bundleDir, "hardening-snapshot.json"),
702
947
  };
703
948
  }
704
949
 
@@ -713,6 +958,8 @@ export function formatOperatorProofResult(result) {
713
958
  ` Verdict: ${result.verdictPath}`,
714
959
  ` Breadcrumbs: ${result.breadcrumbPath}`,
715
960
  ` Runtime proof: ${result.runtimeLoadProofSnapshotPath}`,
961
+ ` Coverage snapshot: ${result.coverageSnapshotPath}`,
962
+ ` Hardening snapshot: ${result.hardeningSnapshotPath}`,
716
963
  ];
717
964
  return lines.join("\n");
718
965
  }
@@ -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 };