@openclawbrain/openclaw 0.3.1 → 0.3.2

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/cli.js CHANGED
@@ -5,19 +5,22 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  const __filename = fileURLToPath(import.meta.url);
7
7
  const __dirname = path.dirname(__filename);
8
- import { DEFAULT_OLLAMA_EMBEDDING_MODEL } from "@openclawbrain/compiler";
9
- import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
8
+ import { DEFAULT_OLLAMA_EMBEDDING_MODEL, createOllamaEmbedder } from "@openclawbrain/compiler";
9
+ import { ensureManagedLearnerServiceForActivationRoot, inspectManagedLearnerService, removeManagedLearnerServiceForActivationRoot, parseDaemonArgs, runDaemonCommand } from "./daemon.js";
10
10
  import { exportBrain, importBrain } from "./import-export.js";
11
11
  import { buildNormalizedEventExport } from "@openclawbrain/contracts";
12
- import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
12
+ import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, reindexCandidatePackBuildResultWithEmbedder, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
13
13
  import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
14
14
  import { resolveActivationRoot } from "./resolve-activation-root.js";
15
15
  import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
16
- import { buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
16
+ import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
17
17
  import { appendLearningUpdateLogs } from "./learning-spine.js";
18
18
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
19
19
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
20
- import { readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
20
+ import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
21
+ const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
22
+ const OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV = "OPENCLAWBRAIN_EMBEDDER_PROVIDER";
23
+ const OPENCLAWBRAIN_EMBEDDER_MODEL_ENV = "OPENCLAWBRAIN_EMBEDDER_MODEL";
21
24
  const OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV = "OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION";
22
25
  const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
23
26
  "qwen3.5:9b",
@@ -48,6 +51,56 @@ function getCliHomeDir() {
48
51
  function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
49
52
  return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
50
53
  }
54
+ function findInstalledHookReferencesForActivationRoot(input) {
55
+ const resolvedActivationRoot = path.resolve(input.activationRoot);
56
+ const resolvedExcludedHome = input.excludingOpenClawHome === undefined || input.excludingOpenClawHome === null
57
+ ? null
58
+ : path.resolve(input.excludingOpenClawHome);
59
+ return discoverOpenClawHomes(input.homeDir ?? getCliHomeDir())
60
+ .filter((inspection) => resolvedExcludedHome === null || path.resolve(inspection.openclawHome) !== resolvedExcludedHome)
61
+ .flatMap((inspection) => {
62
+ const installedActivationRoot = resolveActivationRoot({
63
+ openclawHome: inspection.openclawHome,
64
+ quiet: true
65
+ });
66
+ if (installedActivationRoot.trim().length === 0) {
67
+ return [];
68
+ }
69
+ return path.resolve(installedActivationRoot) === resolvedActivationRoot
70
+ ? [{ openclawHome: inspection.openclawHome, inspection }]
71
+ : [];
72
+ })
73
+ .sort((left, right) => left.openclawHome.localeCompare(right.openclawHome));
74
+ }
75
+ function findOtherInstalledHookReferencesForActivationRoot(input) {
76
+ return findInstalledHookReferencesForActivationRoot(input);
77
+ }
78
+ function resolveWatchProfileRootsForActivationRoot(activationRoot, homeDir = getCliHomeDir()) {
79
+ const attachedProfileRoots = findInstalledHookReferencesForActivationRoot({
80
+ activationRoot,
81
+ homeDir
82
+ }).map((reference) => path.resolve(reference.openclawHome));
83
+ return attachedProfileRoots.length > 0 ? attachedProfileRoots : undefined;
84
+ }
85
+ function assertActivationRootPurgeIsNotShared(input) {
86
+ const sharedReferences = findOtherInstalledHookReferencesForActivationRoot({
87
+ activationRoot: input.activationRoot,
88
+ excludingOpenClawHome: input.openclawHome
89
+ });
90
+ if (sharedReferences.length === 0) {
91
+ return;
92
+ }
93
+ const attachedProfiles = sharedReferences
94
+ .map(({ openclawHome, inspection }) => ` - ${path.resolve(openclawHome)} (${describeOpenClawHomeInspection(inspection)})`)
95
+ .join("\n");
96
+ throw new Error([
97
+ `Refusing to purge activation root ${path.resolve(input.activationRoot)} because another installed OpenClaw profile still points at it.`,
98
+ "Other attached profiles:",
99
+ attachedProfiles,
100
+ "Use uninstall --keep-data or detach on this profile first, then remove the remaining profile hooks before purging shared brain data.",
101
+ "For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
102
+ ].join("\n"));
103
+ }
51
104
  function formatInstallOpenClawHomeSource(source) {
52
105
  switch (source) {
53
106
  case "explicit":
@@ -418,6 +471,24 @@ function formatStructuralOps(report) {
418
471
  ? "none"
419
472
  : `split:${structuralOps.split},merge:${structuralOps.merge},prune:${structuralOps.prune},connect:${structuralOps.connect}`;
420
473
  }
474
+ function formatGraphConnectDiagnostics(diagnostics) {
475
+ if (diagnostics === null) {
476
+ return "none";
477
+ }
478
+ return `budget:${diagnostics.requestedBudget},threshold:${diagnostics.scoreThreshold},pairs:${diagnostics.appliedPairCount}/${diagnostics.candidatePairCount},edges:${diagnostics.createdEdgeCount}`;
479
+ }
480
+ function formatCompactGraphConnectDiagnostics(diagnostics) {
481
+ if (diagnostics === null) {
482
+ return "none";
483
+ }
484
+ return `pairs:${diagnostics.appliedPairCount},edges:${diagnostics.createdEdgeCount}`;
485
+ }
486
+ function formatGraphSummary(report) {
487
+ return (report.graph.latestMaterialization.operatorSummary ??
488
+ report.graph.operatorSummary ??
489
+ report.graph.latestMaterialization.detail ??
490
+ report.graph.detail);
491
+ }
421
492
  function formatScannerSurfaces(report) {
422
493
  return report.supervision.scanSurfaces.length === 0 ? "none" : report.supervision.scanSurfaces.join("|");
423
494
  }
@@ -506,6 +577,14 @@ const LEARNING_WARNING_MESSAGES = {
506
577
  teacher_no_artifacts: "teacher produced no artifacts",
507
578
  teacher_snapshot_unavailable: "teacher snapshot is unavailable"
508
579
  };
580
+ const TEACHER_NO_OP_MESSAGES = {
581
+ none: "the latest processed export produced teacher artifacts",
582
+ duplicate_export: "the latest cycle was a no-op because the export was already seen",
583
+ queue_full: "the latest cycle was a no-op because the teacher queue was full",
584
+ no_teacher_artifacts: "the latest cycle was a no-op because no teacher artifacts were produced",
585
+ empty_scan: "the latest cycle was a no-op because the scanner did not produce any events",
586
+ unavailable: "the latest cycle is not visible from the current operator snapshot"
587
+ };
509
588
  function summarizeStatusInstallHook(openclawHome) {
510
589
  if (openclawHome === null) {
511
590
  return {
@@ -528,6 +607,15 @@ function summarizeStatusInstallHook(openclawHome) {
528
607
  detail: `profile hook is not present at ${shortenPath(extensionDir)}`
529
608
  };
530
609
  }
610
+ function summarizeStatusHookLoad(installHook, status) {
611
+ return {
612
+ installState: installHook.state === "unknown" ? "unverified" : installHook.state,
613
+ loadProof: status.attachment.state === "attached" && status.brainStatus.serveState === "serving_active_pack"
614
+ ? "status_probe_ready"
615
+ : "not_ready",
616
+ detail: installHook.detail
617
+ };
618
+ }
531
619
  function runOllamaProbe(args, baseUrl) {
532
620
  try {
533
621
  execFileSync("ollama", [...args], {
@@ -639,6 +727,162 @@ function summarizeStatusLocalLlm(providerConfig) {
639
727
  : `teacher labeling is ${providerConfig.teacher.provider}; no local Ollama CLI was detected`
640
728
  };
641
729
  }
730
+ function summarizeStatusTeacher(report, providerConfig, localLlm) {
731
+ const enabled = providerConfig.teacher.provider === "ollama";
732
+ const latestCycle = report.teacherLoop.lastNoOpReason === "unavailable"
733
+ ? "unknown"
734
+ : report.teacherLoop.lastNoOpReason === "none"
735
+ ? "teacher_artifact"
736
+ : "no_op";
737
+ if (!enabled) {
738
+ return {
739
+ model: providerConfig.teacher.model,
740
+ enabled,
741
+ healthy: false,
742
+ stale: false,
743
+ idle: false,
744
+ latestCycle,
745
+ detail: `${providerConfig.teacher.model} is not enabled because teacher labeling is ${providerConfig.teacher.provider}`
746
+ };
747
+ }
748
+ if (!localLlm.detected) {
749
+ return {
750
+ model: providerConfig.teacher.model,
751
+ enabled,
752
+ healthy: false,
753
+ stale: null,
754
+ idle: false,
755
+ latestCycle,
756
+ detail: `${providerConfig.teacher.model} is configured on Ollama, but the local LLM surface is not answering at ${providerConfig.teacherBaseUrl}`
757
+ };
758
+ }
759
+ if (!report.teacherLoop.available) {
760
+ return {
761
+ model: providerConfig.teacher.model,
762
+ enabled,
763
+ healthy: null,
764
+ stale: null,
765
+ idle: null,
766
+ latestCycle,
767
+ detail: `${providerConfig.teacher.model} is enabled on Ollama, but no watch teacher snapshot is visible yet`
768
+ };
769
+ }
770
+ const stale = report.teacherLoop.latestFreshness === "stale" || report.teacherLoop.watchState === "stale_snapshot";
771
+ const idle = report.teacherLoop.running === false &&
772
+ (report.teacherLoop.queueDepth ?? 0) === 0 &&
773
+ report.teacherLoop.failureMode === "none";
774
+ const healthy = report.teacherLoop.failureMode === "none" &&
775
+ stale === false &&
776
+ report.teacherLoop.watchState !== "not_visible";
777
+ const cycleDetail = TEACHER_NO_OP_MESSAGES[report.teacherLoop.lastNoOpReason] ?? "the latest teacher cycle detail is unavailable";
778
+ if (report.teacherLoop.failureMode !== "none" && report.teacherLoop.failureMode !== "unavailable") {
779
+ return {
780
+ model: providerConfig.teacher.model,
781
+ enabled,
782
+ healthy: false,
783
+ stale,
784
+ idle,
785
+ latestCycle,
786
+ detail: report.teacherLoop.failureDetail === null
787
+ ? `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}`
788
+ : `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}: ${report.teacherLoop.failureDetail}`
789
+ };
790
+ }
791
+ return {
792
+ model: providerConfig.teacher.model,
793
+ enabled,
794
+ healthy,
795
+ stale,
796
+ idle,
797
+ latestCycle,
798
+ detail: `${providerConfig.teacher.model} is enabled on Ollama; ${cycleDetail}`
799
+ };
800
+ }
801
+ function summarizeStatusEmbedder(embeddings) {
802
+ const provisioned = embeddings.provisionedState === "confirmed" || embeddings.provisionedState === "builtin"
803
+ ? true
804
+ : embeddings.provisionedState === "not_confirmed" || embeddings.provisionedState === "off"
805
+ ? false
806
+ : null;
807
+ const live = embeddings.liveState === "yes" ? true : embeddings.liveState === "no" ? false : null;
808
+ if (embeddings.provider === "off") {
809
+ return {
810
+ model: embeddings.model,
811
+ provisioned,
812
+ live,
813
+ detail: `${embeddings.model} is not provisioned because the embedder provider is off`
814
+ };
815
+ }
816
+ if (embeddings.provider === "keywords") {
817
+ return {
818
+ model: embeddings.model,
819
+ provisioned,
820
+ live,
821
+ detail: "keyword embeddings are builtin, so there is no Ollama model to provision"
822
+ };
823
+ }
824
+ if (provisioned === true && live === true) {
825
+ return {
826
+ model: embeddings.model,
827
+ provisioned,
828
+ live,
829
+ detail: `${embeddings.model} is confirmed on Ollama and the active pack stores live numeric embeddings`
830
+ };
831
+ }
832
+ if (provisioned === true && live === false) {
833
+ return {
834
+ model: embeddings.model,
835
+ provisioned,
836
+ live,
837
+ detail: `${embeddings.model} is confirmed on Ollama, but the active pack still has no live numeric embeddings`
838
+ };
839
+ }
840
+ if (provisioned === false && live === true) {
841
+ return {
842
+ model: embeddings.model,
843
+ provisioned,
844
+ live,
845
+ detail: `${embeddings.model} is not confirmed on Ollama, but the active pack already carries numeric embeddings from an earlier materialization`
846
+ };
847
+ }
848
+ return {
849
+ model: embeddings.model,
850
+ provisioned,
851
+ live,
852
+ detail: embeddings.detail
853
+ };
854
+ }
855
+ function summarizeStatusRouteFn(status, report) {
856
+ const freshness = report.servePath.refreshStatus ?? status.brain.routeFreshness;
857
+ if (!report.routeFn.available) {
858
+ return {
859
+ available: false,
860
+ freshness,
861
+ trainedAt: report.routeFn.trainedAt,
862
+ updatedAt: report.routeFn.updatedAt,
863
+ usedAt: report.routeFn.usedAt,
864
+ detail: report.routeFn.detail
865
+ };
866
+ }
867
+ let detail = report.routeFn.detail;
868
+ if (report.servePath.usedLearnedRouteFn === true) {
869
+ detail = `current serve proof used the learned route_fn; ${report.routeFn.detail}`;
870
+ }
871
+ else if (report.routeFn.usedAt !== null) {
872
+ detail = `current serve proof did not use the learned route_fn, but the active route_fn last served a learned turn at ${report.routeFn.usedAt}`;
873
+ }
874
+ else if (report.routeFn.updatedAt !== null) {
875
+ detail = `active route_fn was last updated at ${report.routeFn.updatedAt}, but no learned serve use is visible yet for the current pack`;
876
+ }
877
+ return {
878
+ available: true,
879
+ freshness,
880
+ trainedAt: report.routeFn.trainedAt,
881
+ updatedAt: report.routeFn.updatedAt,
882
+ usedAt: report.routeFn.usedAt,
883
+ detail
884
+ };
885
+ }
642
886
  function pushUniqueAlert(target, value) {
643
887
  const normalized = value.trim();
644
888
  if (normalized.length === 0) {
@@ -698,11 +942,8 @@ function summarizeStatusAlerts(report, providerConfig, embeddings, localLlm) {
698
942
  }
699
943
  return buckets;
700
944
  }
701
- function summarizeStatusWatchState(report) {
702
- if (!report.teacherLoop.available || report.teacherLoop.sourceKind !== "watch_snapshot") {
703
- return "not_visible";
704
- }
705
- return report.teacherLoop.running === true ? "running" : "snapshot_only";
945
+ function summarizeStatusWatchState(status) {
946
+ return status.passiveLearning.watchState;
706
947
  }
707
948
  function summarizeStatusServeReality(status) {
708
949
  if (status.brainStatus.serveState === "serving_active_pack") {
@@ -710,36 +951,75 @@ function summarizeStatusServeReality(status) {
710
951
  }
711
952
  return status.brainStatus.serveState;
712
953
  }
954
+ function summarizeStatusPromotionState(status) {
955
+ if (status.brain.state === "pg_promoted_pack_authoritative") {
956
+ return "promoted";
957
+ }
958
+ if (status.brain.state === "seed_state_authoritative") {
959
+ return status.passiveLearning.firstExportOccurred ? "seed_authoritative" : "awaiting_first_export";
960
+ }
961
+ return status.brain.state;
962
+ }
713
963
  function formatStatusAlertLine(values) {
714
964
  const normalized = values.map((value) => value.trim()).filter((value) => value.length > 0);
715
965
  return normalized.length === 0 ? "none" : formatCompactList(normalized, 2, 64);
716
966
  }
717
- function summarizeStatusStartupToken(status) {
718
- if (status.attachment.state !== "attached") {
719
- return "BRAIN_NOT_YET_LOADED";
720
- }
721
- if (status.brainStatus.activationState === "broken_install" || status.brainStatus.activationState === "stale_incomplete" || status.brainStatus.activationState === "detached") {
722
- return "BRAIN_NOT_YET_LOADED";
967
+ function formatStatusNullableNumber(value, unknown = "unknown") {
968
+ return value === null ? unknown : String(value);
969
+ }
970
+ function formatStatusNullableYesNo(value) {
971
+ return value === null ? "unknown" : yesNo(value);
972
+ }
973
+ function formatStatusNullableMilliseconds(value) {
974
+ return value === null ? "none" : `${value.toFixed(2)}ms`;
975
+ }
976
+ function formatStatusHotPathTiming(timing) {
977
+ return [
978
+ `hotPath=${formatStatusNullableMilliseconds(timing.totalMs)}`,
979
+ `route=${formatStatusNullableMilliseconds(timing.routeSelectionMs)}`,
980
+ `prompt=${formatStatusNullableMilliseconds(timing.promptAssemblyMs)}`,
981
+ `other=${formatStatusNullableMilliseconds(timing.otherMs)}`,
982
+ `background=${timing.backgroundWorkIncluded ? "included" : "excluded"}`
983
+ ].join(" ");
984
+ }
985
+ function formatStatusObservedDeltaTransition(delta) {
986
+ if (delta.latestPackTransition === null) {
987
+ return "none";
723
988
  }
724
- return status.brainStatus.serveState === "serving_active_pack" ? "BRAIN_LOADED" : "BRAIN_NOT_YET_LOADED";
989
+ return `${delta.latestPackTransition.kind}:${delta.latestPackTransition.fromPackId ?? "none"}->${delta.latestPackTransition.toPackId}`;
725
990
  }
726
991
  function buildCompactStatusHeader(status, report, options) {
727
992
  const installHook = summarizeStatusInstallHook(options.openclawHome);
993
+ const hookLoad = summarizeStatusHookLoad(installHook, status);
728
994
  const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
729
995
  const localLlm = summarizeStatusLocalLlm(options.providerConfig);
996
+ const teacher = summarizeStatusTeacher(report, options.providerConfig, localLlm);
997
+ const embedder = summarizeStatusEmbedder(embeddings);
998
+ const routeFn = summarizeStatusRouteFn(status, report);
730
999
  const alerts = summarizeStatusAlerts(report, options.providerConfig, embeddings, localLlm);
731
- const promoted = status.brain.state === "pg_promoted_pack_authoritative" ? "yes" : "no";
732
1000
  const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
733
1001
  return [
734
- `reality hook=${installHook.state} attach=${status.attachment.state} watch=${summarizeStatusWatchState(report)} promoted=${promoted} serve=${summarizeStatusServeReality(status)}`,
735
- `startup ${summarizeStatusStartupToken(status)} init=${status.brainStatus.activationState} proof=status_probe`,
1002
+ `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1003
+ `hook install=${hookLoad.installState} loadProof=${hookLoad.loadProof} detail=${hookLoad.detail}`,
1004
+ `passive firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)}`,
1005
+ `serving pack=${status.passiveLearning.currentServingPackId ?? "none"} lastExport=${status.passiveLearning.lastExportAt ?? "none"} lastPromotion=${status.passiveLearning.lastPromotionAt ?? "none"}`,
1006
+ `timing ${formatStatusHotPathTiming(status.brainStatus.timing)}`,
1007
+ `delta observed=${status.passiveLearning.lastObservedDelta.observedAt ?? "none"} exported=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.exported)} labeled=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.labeled)} promoted=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.promoted)} served=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.served)} transition=${formatStatusObservedDeltaTransition(status.passiveLearning.lastObservedDelta)}`,
1008
+ `changed ${status.passiveLearning.lastObservedDelta.explanation}`,
736
1009
  `explain ${status.brain.summary}`,
1010
+ `graph blocks=${report.graph.blockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} latest=${report.graph.latestMaterialization.packId ?? "none"} latestChanged=${yesNo(report.graph.latestMaterialization.changed)} connect=${formatCompactGraphConnectDiagnostics(report.graph.latestMaterialization.connectDiagnostics ?? report.graph.connectDiagnostics)}`,
1011
+ `teacher model=${teacher.model} enabled=${yesNo(teacher.enabled)} healthy=${yesNo(teacher.healthy)} stale=${yesNo(teacher.stale)} idle=${yesNo(teacher.idle)} cycle=${teacher.latestCycle} why=${teacher.detail}`,
1012
+ `embedder model=${embedder.model} provisioned=${yesNo(embedder.provisioned)} live=${yesNo(embedder.live)} why=${embedder.detail}`,
1013
+ `routeFn available=${yesNo(routeFn.available)} freshness=${routeFn.freshness} trained=${routeFn.trainedAt ?? "none"} updated=${routeFn.updatedAt ?? "none"} used=${routeFn.usedAt ?? "none"} why=${routeFn.detail}`,
737
1014
  `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
738
1015
  `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
739
1016
  `alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
740
1017
  ];
741
1018
  }
742
1019
  function formatCurrentProfileStatusSummary(status, report, targetInspection, options) {
1020
+ const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
1021
+ const localLlm = summarizeStatusLocalLlm(options.providerConfig);
1022
+ const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
743
1023
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
744
1024
  const targetLine = targetInspection === null
745
1025
  ? `target activation=${status.host.activationRoot} source=activation_root_only`
@@ -760,13 +1040,17 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
760
1040
  `budget requested=${report.servePath.requestedBudgetStrategy ?? "none"} resolved=${report.servePath.resolvedBudgetStrategy ?? "none"} maxBlocks=${report.servePath.resolvedMaxContextBlocks ?? "none"} source=${report.servePath.structuralBudgetSource ?? "none"} origin=${status.brainStatus.structuralDecision.origin} basis=${status.brainStatus.structuralDecision.basis}`,
761
1041
  `decision ${status.brainStatus.structuralDecision.detail}`,
762
1042
  `principal latest=${formatPrincipalLatest(report)} pending=${report.principal.pendingCount ?? report.learning.pendingPrincipalCount ?? "none"} checkpoint=${formatPrincipalCheckpointFrontier(report)} downstream=${yesNo(report.principal.servingDownstreamOfLatestCorrection)} lag=${report.learning.principalLagToPromotion.sequenceLag ?? "none"}`,
1043
+ `passive learner=${yesNo(status.passiveLearning.learnerRunning)} firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} watch=${status.passiveLearning.watchState} export=${status.passiveLearning.exportState} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)} detail=${status.passiveLearning.detail}`,
1044
+ `delta observed=${status.passiveLearning.lastObservedDelta.observedAt ?? "none"} exported=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.exported)} labeled=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.labeled)} promoted=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.promoted)} served=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.served)} transition=${formatStatusObservedDeltaTransition(status.passiveLearning.lastObservedDelta)} detail=${status.passiveLearning.lastObservedDelta.explanation}`,
763
1045
  `scanner flowing=${yesNo(report.supervision.flowing)} scan=${report.supervision.scanPolicy ?? "none"} surfaces=${formatScannerSurfaces(report)} labels=${report.supervision.humanLabelCount ?? "none"}/${report.supervision.selfLabelCount ?? "none"} attributable=${report.supervision.attributedEventCount ?? "none"}/${report.supervision.totalEventCount ?? "none"} digests=${report.supervision.selectionDigestCount ?? "none"}`,
764
1046
  `labels ${formatLabelFlowSummary(report.labelFlow)}`,
765
- `graph source=${report.graph.runtimePlasticitySource ?? "none"} ops=${formatStructuralOps(report)} changed=${yesNo(report.graph.changed)} pruned=${report.graph.prunedBlockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} summary=${report.graph.operatorSummary ?? report.graph.detail}`,
1047
+ `graph source=${report.graph.runtimePlasticitySource ?? "none"} blocks=${report.graph.blockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} ops=${formatStructuralOps(report)} latest=${report.graph.latestMaterialization.packId ?? "none"} latestChanged=${yesNo(report.graph.latestMaterialization.changed)} connect=${formatGraphConnectDiagnostics(report.graph.latestMaterialization.connectDiagnostics ?? report.graph.connectDiagnostics)} summary=${formatGraphSummary(report)}`,
766
1048
  `path ${formatLearningPathSummary(report.learningPath)}`,
767
1049
  `learning state=${report.learning.backlogState} bootstrapped=${yesNo(report.learning.bootstrapped)} mode=${report.learning.mode} next=${report.learning.nextPriorityLane} priority=${report.learning.nextPriorityBucket} pending=${report.learning.pendingLive ?? "none"}/${report.learning.pendingBackfill ?? "none"} buckets=${formatLearningBuckets(report)} warn=${formatLearningWarnings(report)} lastPack=${report.learning.lastMaterializedPackId ?? "none"} detail=${report.learning.detail}`,
768
- `teacher ${formatTeacherLoopSummary(report)}`,
769
- `passive cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} slices=${report.teacherLoop.liveSlicesPerCycle ?? "none"}/${report.teacherLoop.backfillSlicesPerCycle ?? "none"} replayed=${report.teacherLoop.replayedBundleCount ?? "none"}/${report.teacherLoop.replayedEventCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} tail=${report.teacherLoop.sessionTailSessionsTracked ?? "none"}/${report.teacherLoop.sessionTailBridgedEventCount ?? "none"} tailState=${report.teacherLoop.localSessionTailNoopReason ?? "none"} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
1050
+ `teacherProof ${formatTeacherLoopSummary(report)}`,
1051
+ `watch cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} heartbeat=${report.teacherLoop.lastHeartbeatAt ?? "none"} interval=${report.teacherLoop.pollIntervalSeconds ?? "none"} replayed=${report.teacherLoop.replayedBundleCount ?? "none"}/${report.teacherLoop.replayedEventCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} tail=${report.teacherLoop.sessionTailSessionsTracked ?? "none"}/${report.teacherLoop.sessionTailBridgedEventCount ?? "none"} tailState=${report.teacherLoop.localSessionTailNoopReason ?? "none"} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
1052
+ `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
1053
+ `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
770
1054
  `rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
771
1055
  `proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
772
1056
  `logs root=${status.brain.logRoot ?? "none"}`,
@@ -795,6 +1079,15 @@ function formatOpenClawTargetExplanation(inspection) {
795
1079
  function buildInstallStatusCommand(activationRoot) {
796
1080
  return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
797
1081
  }
1082
+ function buildLearnerServiceStatusCommand(activationRoot) {
1083
+ return `openclawbrain daemon status --activation-root ${quoteShellArg(activationRoot)}`;
1084
+ }
1085
+ function buildGatewayRestartCommand(profileId) {
1086
+ return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway restart`;
1087
+ }
1088
+ function buildGatewayStatusCommand(profileId) {
1089
+ return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway status`;
1090
+ }
798
1091
  function buildInstallCommand(openclawHome) {
799
1092
  return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
800
1093
  }
@@ -978,6 +1271,12 @@ function writeInstallProviderDefaults(parsed) {
978
1271
  : "Teacher: no compatible local Ollama model detected; watch stays heuristic unless explicitly overridden"
979
1272
  };
980
1273
  }
1274
+ function shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall) {
1275
+ if (isInstall || activationPlan.action === "bootstrap") {
1276
+ return true;
1277
+ }
1278
+ return !existsSync(resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot));
1279
+ }
981
1280
  function buildInstallBrainFeedbackSummary(input) {
982
1281
  const providerDefaultsPath = resolveOpenClawBrainProviderDefaultsPath(input.parsed.activationRoot);
983
1282
  const embedderState = input.embedderProvision === null ? "unchanged" : input.embedderProvision.state;
@@ -985,15 +1284,61 @@ function buildInstallBrainFeedbackSummary(input) {
985
1284
  const teacherProvider = teacherDefaults?.provider ?? "unknown";
986
1285
  const teacherModel = teacherDefaults?.model ?? null;
987
1286
  const detectedLocalLlm = teacherDefaults?.detectedLocally ?? null;
1287
+ const profileName = input.targetInspection.profileId;
1288
+ const profileSource = input.targetInspection.profileSource;
1289
+ const casingGuidance = profileName === null
1290
+ ? "Exact OpenClaw --profile casing is unresolved here because this target stays on the host-selected current_profile boundary."
1291
+ : `Use the exact OpenClaw profile casing shown here for host-side restart/status commands: ${quoteShellArg(profileName)}.`;
1292
+ const attachment = input.parsed.shared
1293
+ ? {
1294
+ policy: "shared",
1295
+ activationRootMode: "shared_root_declared",
1296
+ sameGatewayProof: "not_checked_in",
1297
+ detail: "Shared activation root declared. Other profiles may point at this same root, but same-gateway many-profile load/serve proof is not checked in."
1298
+ }
1299
+ : {
1300
+ policy: "dedicated",
1301
+ activationRootMode: "dedicated_per_profile",
1302
+ sameGatewayProof: "not_applicable",
1303
+ detail: "Dedicated activation root for this profile/home boundary."
1304
+ };
1305
+ const restart = profileName === null
1306
+ ? {
1307
+ exactProfile: false,
1308
+ profile: null,
1309
+ profileSource,
1310
+ guidance: `Operator-owned restart step: this install did not infer an exact --profile token from ${shortenPath(input.targetInspection.openclawHome)}. ` +
1311
+ "If immediate load matters, restart the host-selected current_profile from OpenClaw itself; otherwise the next natural launch will pick up the hook.",
1312
+ restartCommand: null,
1313
+ gatewayStatusCommand: null
1314
+ }
1315
+ : {
1316
+ exactProfile: true,
1317
+ profile: profileName,
1318
+ profileSource,
1319
+ guidance: `Operator-owned restart step: if immediate load matters and profile ${quoteShellArg(profileName)} is already running, run ${buildGatewayRestartCommand(profileName)}. ` +
1320
+ `If it is stopped, the next launch of profile ${quoteShellArg(profileName)} will pick up the hook. ${casingGuidance}`,
1321
+ restartCommand: buildGatewayRestartCommand(profileName),
1322
+ gatewayStatusCommand: buildGatewayStatusCommand(profileName)
1323
+ };
988
1324
  const provedNow = input.activationPlan.action === "bootstrap"
989
- ? `hook written, activation root ready, seed/current-profile attach bootstrapped, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
990
- : `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved${input.providerDefaults === null ? "" : ", provider defaults written"}`;
991
- const notYetProved = input.activationPlan.action === "bootstrap"
992
- ? `OpenClaw has not reloaded this hook yet; restart plus status still must prove live startup/load and the first exported turn`
993
- : `OpenClaw has not reloaded this hook yet; this ${input.parsed.command} run does not itself prove live startup/load after restart`;
1325
+ ? `hook written, activation root ready, seed/current-profile attach bootstrapped, learner service ${input.learnerService.state}, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
1326
+ : `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved, learner service ${input.learnerService.state}${input.providerDefaults === null ? "" : ", provider defaults written"}`;
1327
+ const notYetProved = input.learnerService.state === "deferred"
1328
+ ? `OpenClaw has not reloaded this hook yet, and passive learner auto-start was deferred; restart plus status still must prove serve-path load, while learner-service start remains a separate operator check`
1329
+ : input.activationPlan.action === "bootstrap"
1330
+ ? `Passive learning is wired for this activation root, but OpenClaw has not reloaded the hook yet; restart plus status still must prove live startup/load and the first exported turn`
1331
+ : `Passive learning is wired for this activation root, but this ${input.parsed.command} run does not itself prove live startup/load after restart`;
994
1332
  return {
995
1333
  hookPath: input.extensionDir,
996
1334
  providerDefaultsPath,
1335
+ profile: {
1336
+ exactProfileName: profileName,
1337
+ profileSource,
1338
+ casingGuidance
1339
+ },
1340
+ attachment,
1341
+ restart,
997
1342
  embedder: {
998
1343
  provider: "ollama",
999
1344
  model: DEFAULT_OLLAMA_EMBEDDING_MODEL,
@@ -1004,6 +1349,14 @@ function buildInstallBrainFeedbackSummary(input) {
1004
1349
  model: teacherModel,
1005
1350
  detectedLocalLlm
1006
1351
  },
1352
+ learnerService: {
1353
+ state: input.learnerService.state,
1354
+ detail: input.learnerService.detail,
1355
+ plistPath: input.learnerService.plistPath,
1356
+ logPath: input.learnerService.logPath,
1357
+ configuredActivationRoot: input.learnerService.configuredActivationRoot,
1358
+ matchesRequestedActivationRoot: input.learnerService.matchesRequestedActivationRoot
1359
+ },
1007
1360
  startup: {
1008
1361
  token: "BRAIN_NOT_YET_LOADED",
1009
1362
  proof: "restart_required"
@@ -1012,19 +1365,28 @@ function buildInstallBrainFeedbackSummary(input) {
1012
1365
  notYetProved,
1013
1366
  lines: [
1014
1367
  `target ${formatOpenClawTargetLine(input.targetInspection)} source=${formatInstallOpenClawHomeSource(input.parsed.openclawHomeSource)}`,
1368
+ profileName === null
1369
+ ? "profile exactName=unresolved selector=current_profile casing=not_available"
1370
+ : `profile exactName=${quoteShellArg(profileName)} source=${profileSource} casing=preserved`,
1015
1371
  `hook written=${shortenPath(input.extensionDir)}`,
1016
1372
  `activation root=${shortenPath(input.parsed.activationRoot)} source=${formatInstallActivationRootSource(input.parsed.activationRootSource)}`,
1373
+ `attachment policy=${attachment.policy} rootMode=${attachment.activationRootMode} sameGatewayProof=${attachment.sameGatewayProof} detail=${attachment.detail}`,
1017
1374
  `defaults provider-defaults=${shortenPath(providerDefaultsPath)} state=${input.providerDefaults === null ? "unchanged" : "written"}`,
1018
1375
  `embedder provider=ollama model=${DEFAULT_OLLAMA_EMBEDDING_MODEL} state=${embedderState}`,
1019
1376
  `teacher provider=${teacherProvider} model=${teacherModel ?? "none"} localLLM=${detectedLocalLlm === null ? "unknown" : yesNo(detectedLocalLlm)}`,
1377
+ `learner state=${input.learnerService.state} detail=${input.learnerService.detail}`,
1378
+ `restart operator=manual exactProfile=${yesNo(restart.exactProfile)} command=${restart.restartCommand ?? "unavailable"}`,
1020
1379
  "startup BRAIN_NOT_YET_LOADED proof=restart_required",
1021
1380
  `provedNow ${provedNow}`,
1022
1381
  `notYet ${notYetProved}`
1023
1382
  ]
1024
1383
  };
1025
1384
  }
1026
- function buildInstallReloadGuidance() {
1027
- return "If this OpenClaw profile is currently running, restart it before expecting the new brain hook to load. If it is stopped, the next launch will pick it up.";
1385
+ function buildInstallReloadGuidance(input) {
1386
+ if (input.targetInspection.profileId === null) {
1387
+ return `Restart later from OpenClaw for the host-selected current_profile behind ${shortenPath(input.targetInspection.openclawHome)} if immediate load matters; this install did not infer an exact --profile token.`;
1388
+ }
1389
+ return `Restart now if immediate load matters: ${buildGatewayRestartCommand(input.targetInspection.profileId)}`;
1028
1390
  }
1029
1391
  const LEGACY_PROFILE_NOTE_FILENAMES = ["BRAIN.md", "brain.md"];
1030
1392
  const LEGACY_BRAIN_AGENTS_LINE = "5. Read `BRAIN.md` — your learning brain context";
@@ -1108,7 +1470,7 @@ function buildCleanupRestartGuidance(restart) {
1108
1470
  }
1109
1471
  return "If this OpenClaw profile is currently running, restart it before expecting the new hook state to take effect. If it is stopped, the next launch will pick it up.";
1110
1472
  }
1111
- function buildStatusNextStep(status, report) {
1473
+ function buildStatusNextStep(status, report, options) {
1112
1474
  const activationRootArg = quoteShellArg(status.host.activationRoot);
1113
1475
  if (status.brainStatus.activationState === "broken_install") {
1114
1476
  return "Repair or replace the activation root before trusting serve-path status again.";
@@ -1119,9 +1481,18 @@ function buildStatusNextStep(status, report) {
1119
1481
  if (status.brainStatus.status === "fail") {
1120
1482
  return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
1121
1483
  }
1484
+ if (options.openclawHome !== null && options.installHook.state === "not_installed") {
1485
+ return `Run \`${buildInstallCommand(options.openclawHome)}\` before expecting this OpenClaw home to load the brain hook.`;
1486
+ }
1122
1487
  if (status.brainStatus.awaitingFirstExport) {
1123
1488
  return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
1124
1489
  }
1490
+ if (options.openclawHome === null) {
1491
+ return `Pin \`--openclaw-home <path>\` when you need exact hook-install truth; activation-root-only status only proves this root's serve-path state.`;
1492
+ }
1493
+ if (options.installHook.state === "installed" && status.brainStatus.serveState === "serving_active_pack") {
1494
+ return "Check the OpenClaw startup log for the `[openclawbrain] BRAIN LOADED` breadcrumb when you need live hook-load proof.";
1495
+ }
1125
1496
  if (report.learning.warningStates.includes("principal_live_backlog") ||
1126
1497
  report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
1127
1498
  return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
@@ -1139,7 +1510,10 @@ function formatHumanFriendlyStatus(status, report, targetInspection, options) {
1139
1510
  `target ${formatOpenClawTargetLine(targetInspection)}`,
1140
1511
  `preflight ${formatOpenClawTargetExplanation(targetInspection)}`
1141
1512
  ]),
1142
- `next ${buildStatusNextStep(status, report)}`
1513
+ `next ${buildStatusNextStep(status, report, {
1514
+ openclawHome: options.openclawHome,
1515
+ installHook: summarizeStatusInstallHook(options.openclawHome)
1516
+ })}`
1143
1517
  ];
1144
1518
  return lines.join("\n");
1145
1519
  }
@@ -2139,7 +2513,7 @@ function buildExtensionIndexTs(activationRoot) {
2139
2513
  function buildExtensionPackageJson() {
2140
2514
  const packageMetadata = readOpenClawPackageMetadata();
2141
2515
  return JSON.stringify({
2142
- name: "openclawbrain-extension",
2516
+ name: "openclawbrain",
2143
2517
  version: packageMetadata.version,
2144
2518
  private: true,
2145
2519
  type: "module",
@@ -2242,6 +2616,56 @@ function buildHistoryEntry(record, slot, isActive) {
2242
2616
  current: isActive
2243
2617
  };
2244
2618
  }
2619
+ function ensureLifecycleLearnerService(activationRoot) {
2620
+ const outcome = ensureManagedLearnerServiceForActivationRoot(activationRoot);
2621
+ return {
2622
+ state: outcome.state,
2623
+ detail: outcome.detail,
2624
+ plistPath: outcome.inspection.plistPath,
2625
+ logPath: outcome.inspection.logPath,
2626
+ configuredActivationRoot: outcome.inspection.configuredActivationRoot,
2627
+ matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
2628
+ };
2629
+ }
2630
+ function resolveCleanupLearnerServiceOutcome(activationRoot, openclawHome) {
2631
+ if (activationRoot === null) {
2632
+ return {
2633
+ state: "unresolved",
2634
+ detail: "Learner service preservation is unresolved because the activation root could not be resolved from the installed profile hook.",
2635
+ plistPath: null,
2636
+ logPath: null,
2637
+ configuredActivationRoot: null,
2638
+ matchesRequestedActivationRoot: null
2639
+ };
2640
+ }
2641
+ const remainingProfiles = findOtherInstalledHookReferencesForActivationRoot({
2642
+ activationRoot,
2643
+ excludingOpenClawHome: openclawHome
2644
+ });
2645
+ if (remainingProfiles.length > 0) {
2646
+ const inspection = inspectManagedLearnerService(activationRoot);
2647
+ const attachedProfiles = remainingProfiles
2648
+ .map(({ openclawHome: profileHome }) => shortenPath(path.resolve(profileHome)))
2649
+ .join(", ");
2650
+ return {
2651
+ state: "preserved",
2652
+ detail: `Preserved the background learner service for ${path.resolve(activationRoot)} because other attached OpenClaw profiles still share this activation root: ${attachedProfiles}.`,
2653
+ plistPath: inspection.plistPath,
2654
+ logPath: inspection.logPath,
2655
+ configuredActivationRoot: inspection.configuredActivationRoot,
2656
+ matchesRequestedActivationRoot: inspection.matchesRequestedActivationRoot
2657
+ };
2658
+ }
2659
+ const outcome = removeManagedLearnerServiceForActivationRoot(activationRoot);
2660
+ return {
2661
+ state: outcome.state,
2662
+ detail: outcome.detail,
2663
+ plistPath: outcome.inspection.plistPath,
2664
+ logPath: outcome.inspection.logPath,
2665
+ configuredActivationRoot: outcome.inspection.configuredActivationRoot,
2666
+ matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
2667
+ };
2668
+ }
2245
2669
  function formatInspectionFindings(findings) {
2246
2670
  return findings.join("; ");
2247
2671
  }
@@ -2438,11 +2862,11 @@ function runProfileHookAttachCommand(parsed) {
2438
2862
  }
2439
2863
  steps.push(activationPlan.inspectionStep);
2440
2864
  // 5. Persist install-written local provider defaults so watch/learning surfaces do not depend on gateway env wiring.
2441
- const providerDefaults = isInstall || activationPlan.action === "bootstrap"
2865
+ const providerDefaults = shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall)
2442
2866
  ? writeInstallProviderDefaults(parsed)
2443
2867
  : null;
2444
2868
  if (providerDefaults === null) {
2445
- steps.push("Skipped provider-default refresh because explicit attach is reusing existing activation data.");
2869
+ steps.push("Preserved existing provider-defaults.json because explicit attach is reusing existing activation data.");
2446
2870
  }
2447
2871
  else {
2448
2872
  steps.push(providerDefaults.detail);
@@ -2528,16 +2952,36 @@ function runProfileHookAttachCommand(parsed) {
2528
2952
  const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
2529
2953
  writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
2530
2954
  steps.push(`Wrote manifest: ${manifestPath}`);
2531
- const restartGuidance = buildInstallReloadGuidance();
2955
+ const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
2956
+ steps.push(learnerService.detail);
2957
+ const brainFeedback = buildInstallBrainFeedbackSummary({
2958
+ parsed,
2959
+ targetInspection,
2960
+ extensionDir,
2961
+ activationPlan,
2962
+ learnerService,
2963
+ embedderProvision,
2964
+ providerDefaults
2965
+ });
2966
+ const restartGuidance = buildInstallReloadGuidance({
2967
+ targetInspection
2968
+ });
2532
2969
  const nextSteps = [
2533
2970
  restartGuidance,
2971
+ brainFeedback.restart.gatewayStatusCommand === null
2972
+ ? null
2973
+ : `Confirm gateway after restart: ${brainFeedback.restart.gatewayStatusCommand}`,
2534
2974
  `Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`,
2975
+ `Check learner service: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`,
2535
2976
  embedderProvision !== null && embedderProvision.state === "skipped"
2536
2977
  ? `Provision default embedder later: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`
2537
2978
  : null
2538
2979
  ].filter((step) => step !== null);
2539
2980
  const preflightSummary = [
2540
2981
  `Hook: installed at ${shortenPath(extensionDir)}`,
2982
+ parsed.shared
2983
+ ? "Attachment policy: shared activation root declared; same-gateway many-profile load/serve proof is still not checked in"
2984
+ : "Attachment policy: dedicated activation root for this profile/home boundary",
2541
2985
  activationPlan.action === "bootstrap"
2542
2986
  ? "Attachment: seed/current-profile attach created; restart plus status will prove later serve-path use"
2543
2987
  : `Attachment: existing active pack ${activationPlan.activePackId} kept in place; restart plus status will prove later serve-path use`,
@@ -2546,6 +2990,7 @@ function runProfileHookAttachCommand(parsed) {
2546
2990
  : embedderProvision.state === "ensured"
2547
2991
  ? `Embedder: default Ollama model ${embedderProvision.model} was ensured before bootstrap`
2548
2992
  : `Embedder: default Ollama model ${embedderProvision.model} was intentionally skipped`,
2993
+ `Learner: background service ${learnerService.state} for the exact activation root/profile boundary`,
2549
2994
  `Serve path: install alone does not prove serving; restart the profile and run ${buildInstallStatusCommand(parsed.activationRoot)}`
2550
2995
  ];
2551
2996
  const lifecycleSummary = [
@@ -2554,7 +2999,11 @@ function runProfileHookAttachCommand(parsed) {
2554
2999
  : "Lifecycle mode: attach (explicit reattach/manual profile hookup)",
2555
3000
  `OpenClaw target: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
2556
3001
  `Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`,
3002
+ brainFeedback.profile.exactProfileName === null
3003
+ ? "Profile token: current_profile only; this install did not infer an exact --profile token"
3004
+ : `Profile token: use exact OpenClaw profile casing ${quoteShellArg(brainFeedback.profile.exactProfileName)} for host-side restart/status commands`,
2557
3005
  `Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
3006
+ `Attachment policy: ${brainFeedback.attachment.policy} (${brainFeedback.attachment.detail})`,
2558
3007
  `Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
2559
3008
  embedderProvision === null
2560
3009
  ? "Embedder: unchanged because no bootstrap was needed"
@@ -2563,6 +3012,7 @@ function runProfileHookAttachCommand(parsed) {
2563
3012
  : `Embedder: skipped default Ollama model ${embedderProvision.model} via ${parsed.skipEmbedderProvisionSource === "flag" ? "--skip-embedder-provision" : OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}`,
2564
3013
  ...(providerDefaults === null ? [] : [`${providerDefaults.lifecycleSummary} (${shortenPath(providerDefaults.path)})`]),
2565
3014
  `Profile hook: installed at ${shortenPath(extensionDir)}`,
3015
+ `Learner service: ${learnerService.state} for ${shortenPath(parsed.activationRoot)}`,
2566
3016
  activationPlan.resolution === "new_root"
2567
3017
  ? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
2568
3018
  : activationPlan.resolution === "missing_pointers"
@@ -2580,14 +3030,6 @@ function runProfileHookAttachCommand(parsed) {
2580
3030
  ? `Install: kept healthy active pack ${activationPlan.activePackId} in place`
2581
3031
  : `Attach: rewired the profile hook to healthy active pack ${activationPlan.activePackId}`
2582
3032
  ];
2583
- const brainFeedback = buildInstallBrainFeedbackSummary({
2584
- parsed,
2585
- targetInspection,
2586
- extensionDir,
2587
- activationPlan,
2588
- embedderProvision,
2589
- providerDefaults
2590
- });
2591
3033
  // 9. Print summary
2592
3034
  if (parsed.json) {
2593
3035
  console.log(JSON.stringify({
@@ -2642,11 +3084,16 @@ function runProfileHookAttachCommand(parsed) {
2642
3084
  teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
2643
3085
  embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
2644
3086
  },
3087
+ learnerService,
2645
3088
  brainFeedback: {
2646
3089
  hookPath: brainFeedback.hookPath,
2647
3090
  providerDefaultsPath: brainFeedback.providerDefaultsPath,
3091
+ profile: brainFeedback.profile,
3092
+ attachment: brainFeedback.attachment,
3093
+ restart: brainFeedback.restart,
2648
3094
  embedder: brainFeedback.embedder,
2649
3095
  teacher: brainFeedback.teacher,
3096
+ learnerService: brainFeedback.learnerService,
2650
3097
  startup: brainFeedback.startup,
2651
3098
  provedNow: brainFeedback.provedNow,
2652
3099
  notYetProved: brainFeedback.notYetProved,
@@ -2666,8 +3113,12 @@ function runProfileHookAttachCommand(parsed) {
2666
3113
  for (const line of brainFeedback.lines) {
2667
3114
  console.log(` ${line}`);
2668
3115
  }
2669
- console.log(`Next: ${restartGuidance}`);
3116
+ console.log(`Restart: ${restartGuidance}`);
3117
+ if (brainFeedback.restart.gatewayStatusCommand !== null) {
3118
+ console.log(`Gateway: Confirm OpenClaw after restart: ${brainFeedback.restart.gatewayStatusCommand}`);
3119
+ }
2670
3120
  console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
3121
+ console.log(`Learner: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`);
2671
3122
  if (embedderProvision !== null && embedderProvision.state === "skipped") {
2672
3123
  console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
2673
3124
  }
@@ -2689,6 +3140,85 @@ function validateOpenClawHome(openclawHome) {
2689
3140
  throw new Error(`openclaw.json not found in ${openclawHome}`);
2690
3141
  }
2691
3142
  }
3143
+ function readJsonObjectRecord(value) {
3144
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3145
+ return null;
3146
+ }
3147
+ return value;
3148
+ }
3149
+ function readOpenClawJsonConfig(openclawHome) {
3150
+ const openclawJsonPath = path.join(openclawHome, "openclaw.json");
3151
+ let parsed;
3152
+ try {
3153
+ parsed = JSON.parse(readFileSync(openclawJsonPath, "utf8"));
3154
+ }
3155
+ catch (error) {
3156
+ throw new Error(`Failed to read ${openclawJsonPath}: ${toErrorMessage(error)}`);
3157
+ }
3158
+ const config = readJsonObjectRecord(parsed);
3159
+ if (config === null) {
3160
+ throw new Error(`Failed to read ${openclawJsonPath}: openclaw.json must contain a top-level object`);
3161
+ }
3162
+ return {
3163
+ path: openclawJsonPath,
3164
+ config
3165
+ };
3166
+ }
3167
+ function scrubOpenClawBrainPluginConfig(openclawHome) {
3168
+ const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3169
+ const plugins = readJsonObjectRecord(config.plugins);
3170
+ if (plugins === null) {
3171
+ return {
3172
+ path: openclawJsonPath,
3173
+ changed: false,
3174
+ detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
3175
+ };
3176
+ }
3177
+ const changes = [];
3178
+ let changed = false;
3179
+ if (Array.isArray(plugins.allow)) {
3180
+ const filteredAllow = plugins.allow.filter((entry) => entry !== "openclawbrain");
3181
+ if (filteredAllow.length !== plugins.allow.length) {
3182
+ changed = true;
3183
+ changes.push("removed plugins.allow entry");
3184
+ if (filteredAllow.length > 0) {
3185
+ plugins.allow = filteredAllow;
3186
+ }
3187
+ else {
3188
+ delete plugins.allow;
3189
+ }
3190
+ }
3191
+ }
3192
+ const entries = readJsonObjectRecord(plugins.entries);
3193
+ if (entries !== null && Object.prototype.hasOwnProperty.call(entries, "openclawbrain")) {
3194
+ delete entries.openclawbrain;
3195
+ changed = true;
3196
+ changes.push("removed plugins.entries.openclawbrain");
3197
+ }
3198
+ if (entries !== null && Object.keys(entries).length === 0 && Object.prototype.hasOwnProperty.call(plugins, "entries")) {
3199
+ delete plugins.entries;
3200
+ changed = true;
3201
+ changes.push("removed empty plugins.entries container");
3202
+ }
3203
+ if (Object.keys(plugins).length === 0 && Object.prototype.hasOwnProperty.call(config, "plugins")) {
3204
+ delete config.plugins;
3205
+ changed = true;
3206
+ changes.push("removed empty plugins container");
3207
+ }
3208
+ if (changed) {
3209
+ writeFileSync(openclawJsonPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3210
+ return {
3211
+ path: openclawJsonPath,
3212
+ changed: true,
3213
+ detail: `Scrubbed stale openclawbrain plugin config in ${openclawJsonPath}: ${changes.join(", ")}`
3214
+ };
3215
+ }
3216
+ return {
3217
+ path: openclawJsonPath,
3218
+ changed: false,
3219
+ detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
3220
+ };
3221
+ }
2692
3222
  function resolveCleanupActivationRoot(openclawHome, explicitActivationRoot) {
2693
3223
  if (explicitActivationRoot !== null) {
2694
3224
  return path.resolve(explicitActivationRoot);
@@ -2732,6 +3262,8 @@ function runDetachCommand(parsed) {
2732
3262
  const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2733
3263
  steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2734
3264
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3265
+ const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3266
+ const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
2735
3267
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2736
3268
  const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2737
3269
  const activationData = summarizeKeptActivationData(activationRoot);
@@ -2739,14 +3271,17 @@ function runDetachCommand(parsed) {
2739
3271
  const nextSteps = [
2740
3272
  restartGuidance,
2741
3273
  activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
3274
+ activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
2742
3275
  `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2743
3276
  ].filter((step) => step !== null);
3277
+ steps.push(pluginConfigCleanup.detail);
2744
3278
  if (legacyResidue.removedNotes.length > 0) {
2745
3279
  steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
2746
3280
  }
2747
3281
  if (legacyResidue.updatedAgents.length > 0) {
2748
3282
  steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
2749
3283
  }
3284
+ steps.push(learnerService.detail);
2750
3285
  steps.push(activationData.activationDataDetail);
2751
3286
  steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
2752
3287
  if (parsed.json) {
@@ -2764,6 +3299,8 @@ function runDetachCommand(parsed) {
2764
3299
  activationRoot,
2765
3300
  dataAction: "kept",
2766
3301
  activationDataState: activationData.activationDataState,
3302
+ pluginConfigCleanup,
3303
+ learnerService,
2767
3304
  removedLegacyNotes: legacyResidue.removedNotes,
2768
3305
  updatedAgents: legacyResidue.updatedAgents,
2769
3306
  restartMode: parsed.restart,
@@ -2786,9 +3323,12 @@ function runDetachCommand(parsed) {
2786
3323
  else {
2787
3324
  console.log("Brain data: preserved, but the activation root could not be resolved from the removed hook.");
2788
3325
  }
3326
+ console.log(`Config: ${pluginConfigCleanup.detail}`);
3327
+ console.log(`Learner: ${learnerService.detail}`);
2789
3328
  console.log(`Next: ${restartGuidance}`);
2790
3329
  if (activationRoot !== null) {
2791
3330
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
3331
+ console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
2792
3332
  }
2793
3333
  console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
2794
3334
  }
@@ -2800,6 +3340,20 @@ function runUninstallCommand(parsed) {
2800
3340
  const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2801
3341
  steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2802
3342
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3343
+ if (parsed.dataMode === "purge" && activationRoot !== null) {
3344
+ assertActivationRootPurgeIsNotShared({
3345
+ activationRoot,
3346
+ openclawHome: parsed.openclawHome
3347
+ });
3348
+ }
3349
+ const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3350
+ const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
3351
+ if (parsed.dataMode === "purge" &&
3352
+ activationRoot !== null &&
3353
+ learnerService.state === "preserved" &&
3354
+ learnerService.matchesRequestedActivationRoot !== false) {
3355
+ throw new Error(`Refusing to purge activation root ${path.resolve(activationRoot)} because the background learner service for this exact root could not be removed. ${learnerService.detail}`);
3356
+ }
2803
3357
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2804
3358
  const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2805
3359
  let activationData;
@@ -2830,16 +3384,19 @@ function runUninstallCommand(parsed) {
2830
3384
  const nextSteps = [
2831
3385
  restartGuidance,
2832
3386
  parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
3387
+ activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
2833
3388
  parsed.dataMode === "keep"
2834
3389
  ? `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2835
3390
  : `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
2836
3391
  ].filter((step) => step !== null);
3392
+ steps.push(pluginConfigCleanup.detail);
2837
3393
  if (legacyResidue.removedNotes.length > 0) {
2838
3394
  steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
2839
3395
  }
2840
3396
  if (legacyResidue.updatedAgents.length > 0) {
2841
3397
  steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
2842
3398
  }
3399
+ steps.push(learnerService.detail);
2843
3400
  steps.push(activationData.activationDataDetail);
2844
3401
  steps.push(parsed.dataMode === "purge"
2845
3402
  ? "Uninstall removed the OpenClaw profile hook and activation data."
@@ -2859,6 +3416,8 @@ function runUninstallCommand(parsed) {
2859
3416
  activationRoot,
2860
3417
  dataAction: parsed.dataMode,
2861
3418
  activationDataState: activationData.activationDataState,
3419
+ pluginConfigCleanup,
3420
+ learnerService,
2862
3421
  removedLegacyNotes: legacyResidue.removedNotes,
2863
3422
  updatedAgents: legacyResidue.updatedAgents,
2864
3423
  restartMode: parsed.restart,
@@ -2879,10 +3438,15 @@ function runUninstallCommand(parsed) {
2879
3438
  if (activationRoot !== null) {
2880
3439
  console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
2881
3440
  }
3441
+ console.log(`Config: ${pluginConfigCleanup.detail}`);
3442
+ console.log(`Learner: ${learnerService.detail}`);
2882
3443
  console.log(`Next: ${restartGuidance}`);
2883
3444
  if (parsed.dataMode === "keep" && activationRoot !== null) {
2884
3445
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
2885
3446
  }
3447
+ if (activationRoot !== null) {
3448
+ console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
3449
+ }
2886
3450
  if (parsed.dataMode === "keep") {
2887
3451
  console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
2888
3452
  }
@@ -3454,13 +4018,62 @@ function exportLocalSessionTailChangesToScanRoot(input) {
3454
4018
  warnings
3455
4019
  };
3456
4020
  }
3457
- function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId) {
3458
- const materialization = snapshot?.learner?.lastMaterialization ?? null;
4021
+ function summarizeVectorEmbeddingState(vectors) {
4022
+ if (vectors === null || vectors === undefined) {
4023
+ return {
4024
+ vectorEntryCount: null,
4025
+ numericEmbeddingEntryCount: null,
4026
+ embeddingModels: []
4027
+ };
4028
+ }
4029
+ const embeddingModels = [...new Set(vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
4030
+ return {
4031
+ vectorEntryCount: vectors.entries.length,
4032
+ numericEmbeddingEntryCount: vectors.entries.filter((entry) => entry.embedding !== undefined).length,
4033
+ embeddingModels
4034
+ };
4035
+ }
4036
+ function buildWatchEmbedTracePoint(input) {
4037
+ const summary = summarizeVectorEmbeddingState(input.vectors);
4038
+ return {
4039
+ slot: input.slot,
4040
+ packId: input.packId,
4041
+ runtimeEmbedderPresent: input.embedder !== null,
4042
+ runtimeEmbedderModel: input.embedder?.model ?? null,
4043
+ vectorEntryCount: summary.vectorEntryCount,
4044
+ numericEmbeddingEntryCount: summary.numericEmbeddingEntryCount,
4045
+ embeddingModels: summary.embeddingModels,
4046
+ error: input.error ?? null
4047
+ };
4048
+ }
4049
+ function buildWatchEmbedTracePointFromPack(input) {
4050
+ return buildWatchEmbedTracePoint({
4051
+ slot: input.slot,
4052
+ packId: input.pack?.manifest.packId ?? null,
4053
+ embedder: input.embedder,
4054
+ vectors: input.pack?.vectors,
4055
+ error: input.error ?? null
4056
+ });
4057
+ }
4058
+ function formatWatchEmbedTracePoint(label, point) {
4059
+ const models = point.embeddingModels.length === 0 ? "none" : point.embeddingModels.join("|");
4060
+ const slot = point.slot ?? "build";
4061
+ const packId = point.packId ?? "unknown";
4062
+ const embedderState = point.runtimeEmbedderPresent ? `present:${point.runtimeEmbedderModel ?? "unknown"}` : "null";
4063
+ const counts = point.vectorEntryCount === null || point.numericEmbeddingEntryCount === null
4064
+ ? "vectors=unknown numeric=unknown"
4065
+ : `vectors=${point.vectorEntryCount} numeric=${point.numericEmbeddingEntryCount}`;
4066
+ const error = point.error === null ? "" : ` error=${point.error}`;
4067
+ return `embed-trace ${label} slot=${slot} pack=${packId} runtimeEmbedder=${embedderState} ${counts} models=${models}${error}`;
4068
+ }
4069
+ async function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId, embedder, log) {
4070
+ let materialization = snapshot?.learner?.lastMaterialization ?? null;
3459
4071
  if (materialization === null) {
3460
4072
  return {
3461
4073
  lastHandledMaterializationPackId,
3462
4074
  logLine: null,
3463
4075
  materializedPackId: null,
4076
+ embedInstrumentation: null,
3464
4077
  failure: null
3465
4078
  };
3466
4079
  }
@@ -3472,10 +4085,38 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
3472
4085
  lastHandledMaterializationPackId,
3473
4086
  logLine: null,
3474
4087
  materializedPackId: packId,
4088
+ embedInstrumentation: null,
3475
4089
  failure: null
3476
4090
  };
3477
4091
  }
4092
+ if (embedder !== null) {
4093
+ materialization = {
4094
+ ...materialization,
4095
+ candidate: await reindexCandidatePackBuildResultWithEmbedder(materialization.candidate, embedder)
4096
+ };
4097
+ if (snapshot?.learner !== undefined && snapshot.learner !== null) {
4098
+ snapshot.learner.lastMaterialization = materialization;
4099
+ }
4100
+ }
3478
4101
  const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
4102
+ const observedAt = new Date().toISOString();
4103
+ const beforeCandidateMaterialization = buildWatchEmbedTracePoint({
4104
+ slot: null,
4105
+ packId,
4106
+ embedder,
4107
+ vectors: materialization?.candidate?.payloads?.vectors
4108
+ });
4109
+ let embedInstrumentation = {
4110
+ observedAt,
4111
+ candidatePackId: packId,
4112
+ promotionAllowed: null,
4113
+ promotionFindings: [],
4114
+ beforeCandidateMaterialization,
4115
+ afterCandidateMaterialization: null,
4116
+ afterStage: null,
4117
+ afterPromote: null
4118
+ };
4119
+ log?.(formatWatchEmbedTracePoint("before_materialize", beforeCandidateMaterialization));
3479
4120
  try {
3480
4121
  const candidateRootDir = path.resolve(activationRoot, "packs", packId);
3481
4122
  mkdirSync(candidateRootDir, { recursive: true });
@@ -3487,27 +4128,81 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
3487
4128
  activeBeforePack = null;
3488
4129
  }
3489
4130
  const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
4131
+ embedInstrumentation = {
4132
+ ...embedInstrumentation,
4133
+ afterCandidateMaterialization: buildWatchEmbedTracePointFromPack({
4134
+ slot: "candidate",
4135
+ pack: candidateDescriptor,
4136
+ embedder
4137
+ })
4138
+ };
4139
+ if (embedInstrumentation.afterCandidateMaterialization !== null) {
4140
+ log?.(formatWatchEmbedTracePoint("after_materialize", embedInstrumentation.afterCandidateMaterialization));
4141
+ }
3490
4142
  appendLearningUpdateLogs({
3491
4143
  activationRoot,
3492
4144
  materialization,
3493
4145
  activeBeforePack,
3494
4146
  candidateDescriptor
3495
4147
  });
3496
- const now = new Date().toISOString();
4148
+ const now = observedAt;
3497
4149
  stageCandidatePack(activationRoot, candidateRootDir, {
3498
4150
  updatedAt: now,
3499
4151
  reason: `watch_stage:${materialization.reason}:${materialization.lane}`
3500
4152
  });
3501
4153
  const inspection = inspectActivationState(activationRoot, now);
4154
+ let stagedPack = null;
4155
+ let stagedPackError = null;
4156
+ try {
4157
+ stagedPack = loadPackFromActivation(activationRoot, "candidate", { requireActivationReady: true });
4158
+ }
4159
+ catch (error) {
4160
+ stagedPackError = formatWatchError(error);
4161
+ }
4162
+ embedInstrumentation = {
4163
+ ...embedInstrumentation,
4164
+ promotionAllowed: inspection.promotion.allowed,
4165
+ promotionFindings: [...inspection.promotion.findings],
4166
+ afterStage: buildWatchEmbedTracePointFromPack({
4167
+ slot: "candidate",
4168
+ pack: stagedPack,
4169
+ embedder,
4170
+ error: stagedPackError
4171
+ })
4172
+ };
4173
+ if (embedInstrumentation.afterStage !== null) {
4174
+ log?.(formatWatchEmbedTracePoint("after_stage", embedInstrumentation.afterStage));
4175
+ }
3502
4176
  if (inspection.promotion.allowed) {
3503
4177
  promoteCandidatePack(activationRoot, {
3504
4178
  updatedAt: now,
3505
4179
  reason: `watch_promote:${materialization.reason}:${materialization.lane}`
3506
4180
  });
4181
+ let promotedPack = null;
4182
+ let promotedPackError = null;
4183
+ try {
4184
+ promotedPack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
4185
+ }
4186
+ catch (error) {
4187
+ promotedPackError = formatWatchError(error);
4188
+ }
4189
+ embedInstrumentation = {
4190
+ ...embedInstrumentation,
4191
+ afterPromote: buildWatchEmbedTracePointFromPack({
4192
+ slot: "active",
4193
+ pack: promotedPack,
4194
+ embedder,
4195
+ error: promotedPackError
4196
+ })
4197
+ };
4198
+ if (embedInstrumentation.afterPromote !== null) {
4199
+ log?.(formatWatchEmbedTracePoint("after_promote", embedInstrumentation.afterPromote));
4200
+ }
3507
4201
  return {
3508
4202
  lastHandledMaterializationPackId: packId,
3509
4203
  materializedPackId: packId,
3510
4204
  logLine: `Promoted ${shortPackId} → active`,
4205
+ embedInstrumentation,
3511
4206
  failure: null
3512
4207
  };
3513
4208
  }
@@ -3515,15 +4210,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
3515
4210
  lastHandledMaterializationPackId: packId,
3516
4211
  materializedPackId: packId,
3517
4212
  logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
4213
+ embedInstrumentation,
3518
4214
  failure: null
3519
4215
  };
3520
4216
  }
3521
4217
  catch (error) {
3522
4218
  const message = error instanceof Error ? error.message : String(error);
4219
+ embedInstrumentation = {
4220
+ ...embedInstrumentation,
4221
+ afterCandidateMaterialization: embedInstrumentation.afterCandidateMaterialization ??
4222
+ buildWatchEmbedTracePoint({
4223
+ slot: "candidate",
4224
+ packId,
4225
+ embedder,
4226
+ vectors: null,
4227
+ error: message
4228
+ })
4229
+ };
3523
4230
  return {
3524
4231
  lastHandledMaterializationPackId,
3525
4232
  materializedPackId: packId,
3526
4233
  logLine: `Promotion failed for ${shortPackId}: ${message}`,
4234
+ embedInstrumentation,
3527
4235
  failure: {
3528
4236
  mode: "materialization_failed",
3529
4237
  detail: message,
@@ -3569,12 +4277,178 @@ function resolveWatchTeacherLabelerConfig(input, activationRoot) {
3569
4277
  warnings
3570
4278
  };
3571
4279
  }
4280
+ function resolveWatchEmbedderConfig(input, activationRoot) {
4281
+ if (input !== undefined) {
4282
+ return {
4283
+ embedder: input,
4284
+ warnings: []
4285
+ };
4286
+ }
4287
+ const defaultsResult = readOpenClawBrainProviderDefaults(activationRoot);
4288
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
4289
+ env: process.env,
4290
+ activationRoot,
4291
+ defaults: defaultsResult.defaults
4292
+ });
4293
+ const warnings = [...new Set([
4294
+ ...defaultsResult.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning)),
4295
+ ...providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning))
4296
+ ])];
4297
+ const explicitEnv = typeof process.env[OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV] === "string" ||
4298
+ typeof process.env[OPENCLAWBRAIN_EMBEDDER_MODEL_ENV] === "string" ||
4299
+ typeof process.env[OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV] === "string";
4300
+ // Legacy install-written provider-defaults.json files can predate embedder fields entirely.
4301
+ // If a persisted defaults file exists, treat that activation root as explicitly configured and
4302
+ // let provider-config resolution fill in the embedder fallback instead of silently dropping to null.
4303
+ const explicitDefaults = defaultsResult.defaults !== null;
4304
+ if (!explicitEnv && !explicitDefaults) {
4305
+ return {
4306
+ embedder: null,
4307
+ warnings
4308
+ };
4309
+ }
4310
+ if (providerConfig.embedder.provider !== "ollama") {
4311
+ return {
4312
+ embedder: null,
4313
+ warnings
4314
+ };
4315
+ }
4316
+ return {
4317
+ embedder: createOllamaEmbedder({
4318
+ baseUrl: providerConfig.embedderBaseUrl,
4319
+ model: providerConfig.embedder.model
4320
+ }),
4321
+ warnings
4322
+ };
4323
+ }
4324
+ function summarizeWatchLatestUserMessage(localPoll) {
4325
+ let latest = null;
4326
+ for (const change of localPoll.changes) {
4327
+ if (change.lastUserMessageAt === null || change.lastUserMessageText === null) {
4328
+ continue;
4329
+ }
4330
+ const candidate = {
4331
+ at: change.lastUserMessageAt,
4332
+ text: change.lastUserMessageText,
4333
+ sessionId: change.sessionId
4334
+ };
4335
+ if (latest === null || Date.parse(candidate.at) >= Date.parse(latest.at)) {
4336
+ latest = candidate;
4337
+ }
4338
+ }
4339
+ return latest;
4340
+ }
4341
+ function summarizeWatchPackTransition(input) {
4342
+ const beforeActivePackId = input.before?.active?.packId ?? input.before?.pointers.active?.packId ?? null;
4343
+ const afterActivePackId = input.after?.active?.packId ?? input.after?.pointers.active?.packId ?? null;
4344
+ if (afterActivePackId !== null && beforeActivePackId !== afterActivePackId) {
4345
+ return {
4346
+ kind: "promoted_active",
4347
+ fromPackId: beforeActivePackId,
4348
+ toPackId: afterActivePackId
4349
+ };
4350
+ }
4351
+ const beforeCandidatePackId = input.before?.candidate?.packId ?? input.before?.pointers.candidate?.packId ?? null;
4352
+ const afterCandidatePackId = input.after?.candidate?.packId ?? input.after?.pointers.candidate?.packId ?? null;
4353
+ if (afterCandidatePackId !== null && beforeCandidatePackId !== afterCandidatePackId) {
4354
+ return {
4355
+ kind: "staged_candidate",
4356
+ fromPackId: beforeCandidatePackId,
4357
+ toPackId: afterCandidatePackId
4358
+ };
4359
+ }
4360
+ return null;
4361
+ }
4362
+ function truncateWatchMessage(text, maxLength = 96) {
4363
+ const normalized = text.replace(/\s+/gu, " ").trim();
4364
+ if (normalized.length <= maxLength) {
4365
+ return normalized;
4366
+ }
4367
+ return `${normalized.slice(0, maxLength - 1)}…`;
4368
+ }
4369
+ function buildWatchLastObservedDelta(input) {
4370
+ const exported = input.exported.exportedBundleCount > 0 ||
4371
+ input.exported.exportedEventCount > 0;
4372
+ const labeled = (input.snapshotAfter.diagnostics.emittedArtifactCount ?? 0) >
4373
+ (input.snapshotBefore.diagnostics.emittedArtifactCount ?? 0);
4374
+ const latestPackTransition = summarizeWatchPackTransition({
4375
+ before: input.beforeInspection,
4376
+ after: input.afterInspection
4377
+ });
4378
+ const promoted = latestPackTransition?.kind === "promoted_active";
4379
+ const afterActivePackId = input.afterInspection?.active?.packId ?? input.afterInspection?.pointers.active?.packId ?? null;
4380
+ const served = promoted && afterActivePackId === latestPackTransition?.toPackId && input.afterInspection?.active?.activationReady === true;
4381
+ const latestUserMessage = summarizeWatchLatestUserMessage(input.localPoll);
4382
+ const selectedBackfillOnly = !exported && input.scanResult.selected.length > 0;
4383
+ const cycleDidNothing = !exported && !labeled && !promoted && !served;
4384
+ let explanation;
4385
+ if (latestUserMessage === null) {
4386
+ if (selectedBackfillOnly) {
4387
+ explanation =
4388
+ "No new local user message was exported in this cycle; the learner only revisited stored exports, so this pass does not prove a new last-turn change.";
4389
+ }
4390
+ else if (cycleDidNothing) {
4391
+ explanation = "No new local user message or learner-visible export was observed in this cycle, so nothing changed.";
4392
+ }
4393
+ else if (promoted && latestPackTransition !== null) {
4394
+ explanation =
4395
+ `No new local user message was exported in this cycle; pack ${latestPackTransition.toPackId} moved into ${latestPackTransition.kind === "promoted_active" ? "active serving" : "the candidate slot"} from previously accumulated learner state.`;
4396
+ }
4397
+ else {
4398
+ explanation =
4399
+ "This cycle observed learner activity, but it did not include a new local user message, so the latest last-turn delta cannot be attributed to a fresh user turn.";
4400
+ }
4401
+ }
4402
+ else {
4403
+ const quotedMessage = `"${truncateWatchMessage(latestUserMessage.text)}"`;
4404
+ if (exported && labeled && promoted && served && latestPackTransition !== null) {
4405
+ explanation =
4406
+ `Latest user message ${quotedMessage} was exported, labeled, promoted into pack ${latestPackTransition.toPackId}, and is now served from the active pack.`;
4407
+ }
4408
+ else if (exported && labeled && !promoted) {
4409
+ explanation =
4410
+ `Latest user message ${quotedMessage} was exported and labeled, but it has not been promoted into the serving pack yet.`;
4411
+ }
4412
+ else if (exported && !labeled && !promoted) {
4413
+ explanation =
4414
+ `Latest user message ${quotedMessage} was exported, but it did not add a new teacher label or change the serving pack in this cycle.`;
4415
+ }
4416
+ else if (exported && !labeled && promoted && latestPackTransition !== null) {
4417
+ explanation =
4418
+ `Latest user message ${quotedMessage} was exported, but this cycle's promotion to pack ${latestPackTransition.toPackId} is not backed by a new teacher label from that message alone.`;
4419
+ }
4420
+ else if (!exported && labeled) {
4421
+ explanation =
4422
+ `Latest user message ${quotedMessage} was already in stored exports; this cycle only labeled or replayed it, without a new local export.`;
4423
+ }
4424
+ else if (cycleDidNothing) {
4425
+ explanation = `Latest user message ${quotedMessage} did not produce a new export, label, or serving-pack change in this cycle.`;
4426
+ }
4427
+ else {
4428
+ explanation =
4429
+ `Latest user message ${quotedMessage} changed learner state this cycle, but the local artifacts do not prove a clean export-to-serve handoff yet.`;
4430
+ }
4431
+ }
4432
+ return {
4433
+ available: true,
4434
+ observedAt: input.observedAt,
4435
+ exported,
4436
+ labeled,
4437
+ promoted,
4438
+ served,
4439
+ latestPackTransition,
4440
+ explanation
4441
+ };
4442
+ }
3572
4443
  export async function createWatchCommandRuntime(input) {
3573
4444
  const activationRoot = path.resolve(input.activationRoot);
3574
4445
  const bootstrapObservedAt = new Date().toISOString();
3575
4446
  const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
3576
4447
  ? path.resolve(input.scanRoot)
3577
4448
  : path.resolve(activationRoot, "event-exports");
4449
+ const pollIntervalSeconds = Number.isInteger(input.pollIntervalSeconds) && (input.pollIntervalSeconds ?? 0) > 0
4450
+ ? input.pollIntervalSeconds
4451
+ : DEFAULT_WATCH_POLL_INTERVAL_SECONDS;
3578
4452
  const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
3579
4453
  const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
3580
4454
  const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
@@ -3586,14 +4460,25 @@ export async function createWatchCommandRuntime(input) {
3586
4460
  log(`Scan root: ${shortenPath(scanRoot)}`);
3587
4461
  log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
3588
4462
  const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
4463
+ const resolvedEmbedder = resolveWatchEmbedderConfig(input.embedder, activationRoot);
3589
4464
  const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
3590
4465
  for (const warning of resolvedTeacherLabeler.warnings) {
3591
4466
  startupWarnings.push(`teacher_config_warning:${warning}`);
3592
4467
  log(`Teacher config warning: ${warning}`);
3593
4468
  }
4469
+ for (const warning of resolvedEmbedder.warnings) {
4470
+ startupWarnings.push(`embedder_config_warning:${warning}`);
4471
+ log(`Embedder config warning: ${warning}`);
4472
+ }
3594
4473
  if (teacherLabeler?.provider === "ollama") {
3595
4474
  log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
3596
4475
  }
4476
+ if (resolvedEmbedder.embedder !== null) {
4477
+ log(`Embedder: provider=ollama model=${resolvedEmbedder.embedder.model}`);
4478
+ }
4479
+ else {
4480
+ log("Embedder: numeric pack materialization is not configured; watch will keep keyword/weight vectors only.");
4481
+ }
3597
4482
  const scanner = createRuntimeEventExportScanner({ scanRoot });
3598
4483
  let lastServeTimeFallbackReason = null;
3599
4484
  const baseTeacherLoopInput = {
@@ -3630,10 +4515,23 @@ export async function createWatchCommandRuntime(input) {
3630
4515
  };
3631
4516
  let teacherLoop;
3632
4517
  let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
4518
+ let lastEmbedInstrumentation = restoredTeacherState.embedInstrumentation;
4519
+ let restoredLastObservedDelta = restoredTeacherState.lastObservedDelta;
3633
4520
  if (restoredTeacherState.error !== null) {
3634
4521
  const message = restoredTeacherState.error;
3635
4522
  startupWarnings.push(`teacher_snapshot_reset:${message}`);
3636
4523
  lastHandledMaterializationPackId = null;
4524
+ lastEmbedInstrumentation = null;
4525
+ restoredLastObservedDelta = {
4526
+ available: true,
4527
+ observedAt: bootstrapObservedAt,
4528
+ exported: false,
4529
+ labeled: false,
4530
+ promoted: false,
4531
+ served: false,
4532
+ latestPackTransition: null,
4533
+ explanation: "Watch reset an unreadable teacher snapshot, so no prior last-turn delta can be trusted."
4534
+ };
3637
4535
  log(`Teacher snapshot reset: ${message}`);
3638
4536
  teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
3639
4537
  }
@@ -3648,6 +4546,17 @@ export async function createWatchCommandRuntime(input) {
3648
4546
  const message = formatWatchError(error);
3649
4547
  startupWarnings.push(`teacher_snapshot_reset:${message}`);
3650
4548
  lastHandledMaterializationPackId = null;
4549
+ lastEmbedInstrumentation = null;
4550
+ restoredLastObservedDelta = {
4551
+ available: true,
4552
+ observedAt: bootstrapObservedAt,
4553
+ exported: false,
4554
+ labeled: false,
4555
+ promoted: false,
4556
+ served: false,
4557
+ latestPackTransition: null,
4558
+ explanation: "Watch reset an unusable teacher snapshot, so no prior last-turn delta can be trusted."
4559
+ };
3651
4560
  log(`Teacher snapshot reset: ${message}`);
3652
4561
  teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
3653
4562
  }
@@ -3656,11 +4565,19 @@ export async function createWatchCommandRuntime(input) {
3656
4565
  const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
3657
4566
  log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
3658
4567
  }
4568
+ const resolvedProfileRoots = input.profileRoots === undefined
4569
+ ? resolveWatchProfileRootsForActivationRoot(activationRoot)
4570
+ : [...new Set(input.profileRoots.map((root) => path.resolve(root)))];
4571
+ if (input.profileRoots === undefined && resolvedProfileRoots !== undefined) {
4572
+ log(`Session tail scope: attached OpenClaw home${resolvedProfileRoots.length === 1 ? "" : "s"} ${resolvedProfileRoots
4573
+ .map((root) => shortenPath(root))
4574
+ .join(", ")}`);
4575
+ }
3659
4576
  let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
3660
4577
  let localSessionTail;
3661
4578
  try {
3662
4579
  localSessionTail = createOpenClawLocalSessionTail({
3663
- ...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
4580
+ ...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
3664
4581
  cursor: restoredCursor,
3665
4582
  emitExistingOnFirstPoll: restoredCursor.length === 0
3666
4583
  });
@@ -3670,7 +4587,7 @@ export async function createWatchCommandRuntime(input) {
3670
4587
  log(`Session tail cursor reset: ${message}`);
3671
4588
  restoredCursor = [];
3672
4589
  localSessionTail = createOpenClawLocalSessionTail({
3673
- ...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
4590
+ ...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
3674
4591
  emitExistingOnFirstPoll: true
3675
4592
  });
3676
4593
  persistWatchSessionTailCursor(sessionTailCursorPath, []);
@@ -3691,8 +4608,11 @@ export async function createWatchCommandRuntime(input) {
3691
4608
  log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
3692
4609
  }
3693
4610
  let bootstrapSnapshot = teacherLoop.snapshot();
3694
- const replayPromotion = applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId);
4611
+ const replayPromotion = await applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId, resolvedEmbedder.embedder, log);
3695
4612
  lastHandledMaterializationPackId = replayPromotion.lastHandledMaterializationPackId;
4613
+ if (replayPromotion.embedInstrumentation !== null) {
4614
+ lastEmbedInstrumentation = replayPromotion.embedInstrumentation;
4615
+ }
3696
4616
  if (replayPromotion.logLine !== null) {
3697
4617
  log(replayPromotion.logLine);
3698
4618
  bootstrapSnapshot = teacherLoop.snapshot();
@@ -3700,6 +4620,7 @@ export async function createWatchCommandRuntime(input) {
3700
4620
  const bootstrapCursor = localSessionTail.snapshot();
3701
4621
  persistWatchTeacherSnapshot(teacherSnapshotPath, {
3702
4622
  lastRunAt: bootstrapObservedAt,
4623
+ pollIntervalSeconds,
3703
4624
  scanRoot,
3704
4625
  sessionTailCursorPath,
3705
4626
  sessionTailCursorUpdatedAt: bootstrapObservedAt,
@@ -3715,26 +4636,44 @@ export async function createWatchCommandRuntime(input) {
3715
4636
  lastTeacherError: null,
3716
4637
  localSessionTailNoopReason: null,
3717
4638
  lastHandledMaterializationPackId,
4639
+ lastObservedDelta: restoredLastObservedDelta.available
4640
+ ? restoredLastObservedDelta
4641
+ : {
4642
+ available: true,
4643
+ observedAt: bootstrapObservedAt,
4644
+ exported: false,
4645
+ labeled: false,
4646
+ promoted: false,
4647
+ served: false,
4648
+ latestPackTransition: null,
4649
+ explanation: "Watch bootstrapped its state, but no new local user-message delta has been observed yet."
4650
+ },
4651
+ embedInstrumentation: lastEmbedInstrumentation,
3718
4652
  failure: replayPromotion.failure,
3719
4653
  snapshot: bootstrapSnapshot
3720
4654
  });
3721
4655
  return {
3722
4656
  activationRoot,
3723
4657
  scanRoot,
4658
+ pollIntervalSeconds,
3724
4659
  sessionTailCursorPath,
3725
4660
  teacherSnapshotPath,
3726
4661
  startupWarnings,
3727
4662
  lastTeacherError: null,
3728
4663
  replayState,
3729
4664
  lastHandledMaterializationPackId,
4665
+ lastEmbedInstrumentation,
3730
4666
  scanner,
3731
4667
  teacherLoop,
3732
- localSessionTail
4668
+ localSessionTail,
4669
+ embedder: resolvedEmbedder.embedder
3733
4670
  };
3734
4671
  }
3735
4672
  export async function runWatchCommandPass(runtime, options = {}) {
3736
4673
  const log = options.log ?? watchLog;
3737
4674
  const observedAt = options.observedAt ?? new Date().toISOString();
4675
+ const snapshotBefore = runtime.teacherLoop.snapshot();
4676
+ const beforeInspection = inspectActivationState(runtime.activationRoot, observedAt);
3738
4677
  const localPoll = runtime.localSessionTail.pollOnce({
3739
4678
  observedAt
3740
4679
  });
@@ -3768,9 +4707,12 @@ export async function runWatchCommandPass(runtime, options = {}) {
3768
4707
  const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
3769
4708
  runtime.lastTeacherError = null;
3770
4709
  snapshot = ingestResult.snapshot;
3771
- const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
4710
+ const promotion = await applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId, runtime.embedder, log);
3772
4711
  runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
3773
4712
  materializedPackId = promotion.materializedPackId;
4713
+ if (promotion.embedInstrumentation !== null) {
4714
+ runtime.lastEmbedInstrumentation = promotion.embedInstrumentation;
4715
+ }
3774
4716
  failure = promotion.failure;
3775
4717
  if (promotion.logLine !== null) {
3776
4718
  log(promotion.logLine);
@@ -3802,8 +4744,20 @@ export async function runWatchCommandPass(runtime, options = {}) {
3802
4744
  snapshot = runtime.teacherLoop.snapshot();
3803
4745
  }
3804
4746
  }
4747
+ const afterInspection = inspectActivationState(runtime.activationRoot, observedAt);
4748
+ const lastObservedDelta = buildWatchLastObservedDelta({
4749
+ observedAt,
4750
+ localPoll,
4751
+ exported,
4752
+ scanResult,
4753
+ snapshotBefore,
4754
+ snapshotAfter: snapshot,
4755
+ beforeInspection,
4756
+ afterInspection
4757
+ });
3805
4758
  persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
3806
4759
  lastRunAt: observedAt,
4760
+ pollIntervalSeconds: runtime.pollIntervalSeconds,
3807
4761
  scanRoot: runtime.scanRoot,
3808
4762
  sessionTailCursorPath: runtime.sessionTailCursorPath,
3809
4763
  sessionTailCursorUpdatedAt: observedAt,
@@ -3819,6 +4773,8 @@ export async function runWatchCommandPass(runtime, options = {}) {
3819
4773
  lastTeacherError: runtime.lastTeacherError,
3820
4774
  localSessionTailNoopReason: localPoll.noopReason,
3821
4775
  lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
4776
+ lastObservedDelta,
4777
+ embedInstrumentation: runtime.lastEmbedInstrumentation,
3822
4778
  failure,
3823
4779
  snapshot
3824
4780
  });
@@ -3839,6 +4795,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
3839
4795
  scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
3840
4796
  scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
3841
4797
  materialized: materializedPackId,
4798
+ lastObservedDelta,
3842
4799
  diagnostics: snapshot.diagnostics ?? null,
3843
4800
  localSessionTailNoopReason: localPoll.noopReason
3844
4801
  }));
@@ -3856,6 +4813,7 @@ async function runWatchCommand(parsed) {
3856
4813
  const runtime = await createWatchCommandRuntime({
3857
4814
  activationRoot: parsed.activationRoot,
3858
4815
  scanRoot: parsed.scanRoot,
4816
+ pollIntervalSeconds: parsed.interval,
3859
4817
  log: watchLog
3860
4818
  });
3861
4819
  watchLog(`Interval: ${parsed.interval}s`);
@@ -4147,7 +5105,10 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
4147
5105
  }
4148
5106
  else {
4149
5107
  const report = buildOperatorSurfaceReport(operatorInput);
4150
- const providerConfig = readOpenClawBrainProviderConfig(process.env);
5108
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
5109
+ env: process.env,
5110
+ activationRoot
5111
+ });
4151
5112
  if (statusOrRollback.detailed) {
4152
5113
  console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
4153
5114
  openclawHome: statusOrRollback.openclawHome,