@openclawbrain/openclaw 0.3.1 → 0.3.3

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.
Files changed (35) hide show
  1. package/dist/extension/index.js +17 -1
  2. package/dist/extension/index.js.map +1 -1
  3. package/dist/extension/runtime-guard.js +5 -0
  4. package/dist/extension/runtime-guard.js.map +1 -1
  5. package/dist/src/attachment-truth.d.ts +34 -0
  6. package/dist/src/attachment-truth.js +215 -0
  7. package/dist/src/attachment-truth.js.map +1 -0
  8. package/dist/src/cli.d.ts +7 -1
  9. package/dist/src/cli.js +1414 -67
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/daemon.d.ts +42 -1
  12. package/dist/src/daemon.js +360 -50
  13. package/dist/src/daemon.js.map +1 -1
  14. package/dist/src/index.d.ts +89 -1
  15. package/dist/src/index.js +764 -105
  16. package/dist/src/index.js.map +1 -1
  17. package/dist/src/learning-spine.d.ts +3 -1
  18. package/dist/src/learning-spine.js +1 -0
  19. package/dist/src/learning-spine.js.map +1 -1
  20. package/dist/src/local-session-passive-learning.js +6 -1
  21. package/dist/src/local-session-passive-learning.js.map +1 -1
  22. package/dist/src/openclaw-hook-truth.d.ts +25 -0
  23. package/dist/src/openclaw-hook-truth.js +154 -0
  24. package/dist/src/openclaw-hook-truth.js.map +1 -0
  25. package/dist/src/semantic-metadata.d.ts +4 -0
  26. package/dist/src/semantic-metadata.js +41 -0
  27. package/dist/src/semantic-metadata.js.map +1 -0
  28. package/dist/src/session-tail.d.ts +2 -0
  29. package/dist/src/session-tail.js +54 -14
  30. package/dist/src/session-tail.js.map +1 -1
  31. package/dist/src/shadow-extension-proof.js +4 -0
  32. package/dist/src/shadow-extension-proof.js.map +1 -1
  33. package/extension/index.ts +19 -1
  34. package/extension/runtime-guard.ts +6 -0
  35. package/package.json +7 -7
package/dist/src/cli.js CHANGED
@@ -5,19 +5,23 @@ 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 { inspectOpenClawBrainHookStatus, inspectOpenClawBrainPluginAllowlist } from "./openclaw-hook-truth.js";
17
+ import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, clearOpenClawProfileRuntimeLoadProof, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, listOpenClawProfileRuntimeLoadProofs, loadRuntimeEventExportBundle, loadWatchTeacherSnapshotState, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveAttachmentRuntimeLoadProofsPath, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
17
18
  import { appendLearningUpdateLogs } from "./learning-spine.js";
18
19
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
19
20
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
20
- import { readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
21
+ import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
22
+ const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
23
+ const OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV = "OPENCLAWBRAIN_EMBEDDER_PROVIDER";
24
+ const OPENCLAWBRAIN_EMBEDDER_MODEL_ENV = "OPENCLAWBRAIN_EMBEDDER_MODEL";
21
25
  const OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV = "OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION";
22
26
  const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
23
27
  "qwen3.5:9b",
@@ -35,6 +39,15 @@ function normalizeOptionalCliString(value) {
35
39
  const trimmed = value.trim();
36
40
  return trimmed.length > 0 ? trimmed : null;
37
41
  }
42
+ function canonicalizeExistingCliPath(filePath) {
43
+ const resolvedPath = path.resolve(filePath);
44
+ try {
45
+ return realpathSync(resolvedPath);
46
+ }
47
+ catch {
48
+ return resolvedPath;
49
+ }
50
+ }
38
51
  function readTruthyEnvFlag(name, env = process.env) {
39
52
  const value = normalizeOptionalCliString(env[name]);
40
53
  if (value === null) {
@@ -48,6 +61,132 @@ function getCliHomeDir() {
48
61
  function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
49
62
  return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
50
63
  }
64
+ function summarizeSharedActivationRootReferenceProof(reference) {
65
+ switch (reference.installState) {
66
+ case "installed":
67
+ return "hook installed and loadable";
68
+ case "blocked_by_allowlist":
69
+ return "hook files exist but OpenClaw will not load them because plugins.allow blocks openclawbrain";
70
+ case "not_installed":
71
+ default:
72
+ return "hook files are incomplete, so serve-path attachment is not proven";
73
+ }
74
+ }
75
+ function partitionSharedActivationRootHookReferences(references) {
76
+ return references.reduce((result, reference) => {
77
+ if (reference.serveAttachmentState === "attached") {
78
+ result.attached.push(reference);
79
+ }
80
+ else {
81
+ result.halfAttached.push(reference);
82
+ }
83
+ return result;
84
+ }, {
85
+ attached: [],
86
+ halfAttached: []
87
+ });
88
+ }
89
+ function formatSharedActivationRootReferenceList(references, options = {}) {
90
+ return references
91
+ .map((reference) => {
92
+ const parts = [path.resolve(reference.openclawHome)];
93
+ if (options.includeInspection) {
94
+ parts.push(describeOpenClawHomeInspection(reference.inspection));
95
+ }
96
+ if (options.includeProof) {
97
+ parts.push(summarizeSharedActivationRootReferenceProof(reference));
98
+ }
99
+ return ` - ${parts.join(" | ")}`;
100
+ })
101
+ .join("\n");
102
+ }
103
+ function findInstalledHookReferencesForActivationRoot(input) {
104
+ const resolvedActivationRoot = path.resolve(input.activationRoot);
105
+ const resolvedExcludedHome = input.excludingOpenClawHome === undefined || input.excludingOpenClawHome === null
106
+ ? null
107
+ : path.resolve(input.excludingOpenClawHome);
108
+ return discoverOpenClawHomes(input.homeDir ?? getCliHomeDir())
109
+ .filter((inspection) => resolvedExcludedHome === null || path.resolve(inspection.openclawHome) !== resolvedExcludedHome)
110
+ .flatMap((inspection) => {
111
+ const installedActivationRoot = resolveActivationRoot({
112
+ openclawHome: inspection.openclawHome,
113
+ quiet: true
114
+ });
115
+ if (installedActivationRoot.trim().length === 0) {
116
+ return [];
117
+ }
118
+ const installHook = summarizeStatusInstallHook(inspection.openclawHome);
119
+ const reference = {
120
+ openclawHome: inspection.openclawHome,
121
+ inspection,
122
+ installState: installHook.state === "installed" || installHook.state === "blocked_by_allowlist"
123
+ ? installHook.state
124
+ : "not_installed",
125
+ serveAttachmentState: installHook.state === "installed" ? "attached" : "half_attached",
126
+ hookDetail: installHook.detail
127
+ };
128
+ return path.resolve(installedActivationRoot) === resolvedActivationRoot
129
+ ? [reference]
130
+ : [];
131
+ })
132
+ .sort((left, right) => left.openclawHome.localeCompare(right.openclawHome));
133
+ }
134
+ function findOtherInstalledHookReferencesForActivationRoot(input) {
135
+ return findInstalledHookReferencesForActivationRoot(input);
136
+ }
137
+ function resolveWatchProfileRootsForActivationRoot(activationRoot, homeDir = getCliHomeDir()) {
138
+ const references = findInstalledHookReferencesForActivationRoot({
139
+ activationRoot,
140
+ homeDir
141
+ });
142
+ const partitioned = partitionSharedActivationRootHookReferences(references);
143
+ const attachedProfileRoots = partitioned.attached.map((reference) => path.resolve(reference.openclawHome));
144
+ return {
145
+ attachedProfileRoots: attachedProfileRoots.length > 0 || references.length > 0
146
+ ? attachedProfileRoots
147
+ : undefined,
148
+ halfAttachedReferences: partitioned.halfAttached
149
+ };
150
+ }
151
+ function assertActivationRootPurgeIsNotShared(input) {
152
+ const sharedReferences = findOtherInstalledHookReferencesForActivationRoot({
153
+ activationRoot: input.activationRoot,
154
+ excludingOpenClawHome: input.openclawHome
155
+ });
156
+ if (sharedReferences.length === 0) {
157
+ return;
158
+ }
159
+ const partitioned = partitionSharedActivationRootHookReferences(sharedReferences);
160
+ const attachedProfiles = formatSharedActivationRootReferenceList(partitioned.attached, {
161
+ includeInspection: true,
162
+ includeProof: true
163
+ });
164
+ const halfAttachedProfiles = formatSharedActivationRootReferenceList(partitioned.halfAttached, {
165
+ includeInspection: true,
166
+ includeProof: true
167
+ });
168
+ throw new Error(partitioned.attached.length > 0
169
+ ? [
170
+ `Refusing to purge activation root ${path.resolve(input.activationRoot)} because another installed OpenClaw profile still points at it.`,
171
+ "Other attached profiles:",
172
+ attachedProfiles,
173
+ ...(partitioned.halfAttached.length === 0
174
+ ? []
175
+ : [
176
+ "Other half-attached profiles:",
177
+ halfAttachedProfiles
178
+ ]),
179
+ "Use uninstall --keep-data or detach on this profile first, then remove or repair the remaining profile hooks before purging shared brain data.",
180
+ "For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
181
+ ].join("\n")
182
+ : [
183
+ `Refusing to purge activation root ${path.resolve(input.activationRoot)} because another OpenClaw profile still points at it, but its hook is not loadable yet.`,
184
+ "Other half-attached profiles:",
185
+ halfAttachedProfiles,
186
+ "Repair or remove those half-attached profile hooks before purging shared brain data so status stays explicit instead of drifting into a missing-root surprise.",
187
+ "For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
188
+ ].join("\n"));
189
+ }
51
190
  function formatInstallOpenClawHomeSource(source) {
52
191
  switch (source) {
53
192
  case "explicit":
@@ -418,6 +557,24 @@ function formatStructuralOps(report) {
418
557
  ? "none"
419
558
  : `split:${structuralOps.split},merge:${structuralOps.merge},prune:${structuralOps.prune},connect:${structuralOps.connect}`;
420
559
  }
560
+ function formatGraphConnectDiagnostics(diagnostics) {
561
+ if (diagnostics === null) {
562
+ return "none";
563
+ }
564
+ return `budget:${diagnostics.requestedBudget},threshold:${diagnostics.scoreThreshold},pairs:${diagnostics.appliedPairCount}/${diagnostics.candidatePairCount},edges:${diagnostics.createdEdgeCount}`;
565
+ }
566
+ function formatCompactGraphConnectDiagnostics(diagnostics) {
567
+ if (diagnostics === null) {
568
+ return "none";
569
+ }
570
+ return `pairs:${diagnostics.appliedPairCount},edges:${diagnostics.createdEdgeCount}`;
571
+ }
572
+ function formatGraphSummary(report) {
573
+ return (report.graph.latestMaterialization.operatorSummary ??
574
+ report.graph.operatorSummary ??
575
+ report.graph.latestMaterialization.detail ??
576
+ report.graph.detail);
577
+ }
421
578
  function formatScannerSurfaces(report) {
422
579
  return report.supervision.scanSurfaces.length === 0 ? "none" : report.supervision.scanSurfaces.join("|");
423
580
  }
@@ -470,6 +627,8 @@ function formatCompactList(values, maxItems = 2, maxLength = 64) {
470
627
  return values.length > maxItems ? `${visible.join("|")}+${values.length - maxItems}more` : visible.join("|");
471
628
  }
472
629
  const SERVICE_RISK_FINDING_CODES = new Set([
630
+ "hook_desynced",
631
+ "current_profile_not_attached",
473
632
  "activation_broken_install",
474
633
  "activation_stale_incomplete",
475
634
  "active_missing",
@@ -480,6 +639,7 @@ const SERVICE_RISK_FINDING_CODES = new Set([
480
639
  "serve_path_route_evidence_missing"
481
640
  ]);
482
641
  const DEGRADED_BRAIN_FINDING_CODES = new Set([
642
+ "attachment_scope_partial",
483
643
  "bootstrap_waiting_for_first_export",
484
644
  "serve_path_unprobed",
485
645
  "brain_context_kernel_only",
@@ -506,26 +666,189 @@ const LEARNING_WARNING_MESSAGES = {
506
666
  teacher_no_artifacts: "teacher produced no artifacts",
507
667
  teacher_snapshot_unavailable: "teacher snapshot is unavailable"
508
668
  };
669
+ const TEACHER_NO_OP_MESSAGES = {
670
+ none: "the latest processed export produced teacher artifacts",
671
+ duplicate_export: "the latest cycle was a no-op because the export was already seen",
672
+ queue_full: "the latest cycle was a no-op because the teacher queue was full",
673
+ no_teacher_artifacts: "the latest cycle was a no-op because no teacher artifacts were produced",
674
+ empty_scan: "the latest cycle was a no-op because the scanner did not produce any events",
675
+ unavailable: "the latest cycle is not visible from the current operator snapshot"
676
+ };
509
677
  function summarizeStatusInstallHook(openclawHome) {
678
+ const hook = inspectOpenClawBrainHookStatus(openclawHome);
679
+ return {
680
+ state: hook.installState === "unverified" ? "unknown" : hook.installState,
681
+ detail: hook.detail
682
+ };
683
+ }
684
+ function summarizeStatusHookLoad(installHook, status) {
685
+ return {
686
+ installState: installHook.state === "unknown" ? "unverified" : installHook.state,
687
+ loadProof: status.hook.loadProof,
688
+ detail: status.hook.detail
689
+ };
690
+ }
691
+ function summarizeStatusConfigLoad(openclawHome) {
510
692
  if (openclawHome === null) {
511
693
  return {
512
- state: "unknown",
513
- detail: "profile hook state is unknown from activation-root-only status; pin --openclaw-home to prove install state"
694
+ state: "unverified",
695
+ detail: "plugin allowlist state is unknown from activation-root-only status; pin --openclaw-home to prove config load state"
514
696
  };
515
697
  }
516
- const extensionDir = path.join(path.resolve(openclawHome), "extensions", "openclawbrain");
517
- const indexPath = path.join(extensionDir, "index.ts");
518
- const runtimeGuardPath = path.join(extensionDir, "runtime-guard.js");
519
- const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
520
- if (existsSync(indexPath) && existsSync(runtimeGuardPath) && existsSync(manifestPath)) {
698
+ const allowlist = inspectOpenClawBrainPluginAllowlist(openclawHome);
699
+ if (allowlist.state === "blocked") {
700
+ return {
701
+ state: "blocked",
702
+ detail: allowlist.detail
703
+ };
704
+ }
705
+ if (allowlist.state === "invalid") {
521
706
  return {
522
- state: "installed",
523
- detail: `profile hook is installed at ${shortenPath(extensionDir)}`
707
+ state: "invalid",
708
+ detail: allowlist.detail
524
709
  };
525
710
  }
526
711
  return {
527
- state: "not_installed",
528
- detail: `profile hook is not present at ${shortenPath(extensionDir)}`
712
+ state: "allows_load",
713
+ detail: allowlist.detail
714
+ };
715
+ }
716
+ function summarizeStatusHookFilesState(installHook) {
717
+ if (installHook.state === "installed" || installHook.state === "blocked_by_allowlist") {
718
+ return "present";
719
+ }
720
+ if (installHook.state === "not_installed") {
721
+ return "missing";
722
+ }
723
+ return "unverified";
724
+ }
725
+ function summarizeStatusAttachmentWatcher(status) {
726
+ if (status.passiveLearning.watchState === "watching") {
727
+ return "alive";
728
+ }
729
+ if (status.passiveLearning.watchState === "stale_snapshot") {
730
+ return "stale";
731
+ }
732
+ return "not_visible";
733
+ }
734
+ function formatAttachedProfileIdentity(reference) {
735
+ const profileLabel = reference.inspection.profileId ?? "current_profile";
736
+ return `${profileLabel}@${shortenPath(path.resolve(reference.openclawHome))}`;
737
+ }
738
+ function buildStatusAttachedProfileTruth(input) {
739
+ const resolvedOpenClawHome = canonicalizeExistingCliPath(input.reference.openclawHome);
740
+ const installHook = summarizeStatusInstallHook(resolvedOpenClawHome);
741
+ const configLoad = summarizeStatusConfigLoad(resolvedOpenClawHome);
742
+ const runtimeProof = input.runtimeProofByHome.get(resolvedOpenClawHome);
743
+ const currentOpenClawHome = input.currentOpenClawHome === null ? null : canonicalizeExistingCliPath(input.currentOpenClawHome);
744
+ const hookFiles = summarizeStatusHookFilesState(installHook);
745
+ return {
746
+ label: formatAttachedProfileIdentity(input.reference),
747
+ openclawHome: resolvedOpenClawHome,
748
+ current: currentOpenClawHome !== null && currentOpenClawHome === resolvedOpenClawHome,
749
+ hookFiles,
750
+ configLoad: configLoad.state,
751
+ runtimeLoad: input.runtimeProofError !== null
752
+ ? "proof_error"
753
+ : hookFiles !== "present"
754
+ ? "not_proven"
755
+ : runtimeProof !== undefined
756
+ ? "proven"
757
+ : "not_proven",
758
+ runtimeLoadedAt: runtimeProof?.loadedAt ?? null
759
+ };
760
+ }
761
+ function buildCurrentStatusAttachmentTruthDetail(input) {
762
+ const attachedProfileCount = input.attachedProfiles.length;
763
+ const attachedProfilesDetail = attachedProfileCount === 0
764
+ ? "no attached profiles were discovered for this activation root"
765
+ : `attached profile set has ${attachedProfileCount} discoverable ${attachedProfileCount === 1 ? "profile" : "profiles"}`;
766
+ if (input.openclawHome === null) {
767
+ return ("current-profile hook/config/runtime load truth is unverified without --openclaw-home; " +
768
+ `${attachedProfilesDetail}`);
769
+ }
770
+ if (input.runtimeProofError !== null) {
771
+ return (`current profile ${input.currentProfileLabel} could not prove runtime load because ${shortenPath(input.runtimeProofPath)} is unreadable: ${input.runtimeProofError}; ` +
772
+ `${attachedProfilesDetail}`);
773
+ }
774
+ const hookDetail = input.hookFiles === "present"
775
+ ? "hook files are present"
776
+ : input.hookFiles === "missing"
777
+ ? "hook files are missing"
778
+ : "hook files are unverified";
779
+ const configDetail = input.configLoad === "allows_load"
780
+ ? "config allows load"
781
+ : input.configLoad === "blocked"
782
+ ? "config blocks load"
783
+ : input.configLoad === "invalid"
784
+ ? "config is invalid for load proof"
785
+ : "config load is unverified";
786
+ const runtimeDetail = input.runtimeLoad === "proven"
787
+ ? "runtime load is proven"
788
+ : input.runtimeLoad === "not_proven"
789
+ ? "runtime load is not yet proven"
790
+ : input.runtimeLoad === "proof_error"
791
+ ? "runtime load proof is broken"
792
+ : "runtime load is unverified";
793
+ const watcherDetail = input.watcher === "alive"
794
+ ? "watcher is alive"
795
+ : input.watcher === "stale"
796
+ ? "watcher visibility is stale"
797
+ : "watcher is not visible";
798
+ return `current profile ${input.currentProfileLabel}: ${hookDetail}, ${configDetail}, ${runtimeDetail}, ${watcherDetail}; ${attachedProfilesDetail}`;
799
+ }
800
+ function summarizeStatusAttachmentTruth(input) {
801
+ const runtimeProofs = listOpenClawProfileRuntimeLoadProofs(input.activationRoot);
802
+ const runtimeProofByHome = new Map();
803
+ for (const proof of runtimeProofs.proofs?.profiles ?? []) {
804
+ runtimeProofByHome.set(canonicalizeExistingCliPath(proof.openclawHome), {
805
+ loadedAt: proof.loadedAt
806
+ });
807
+ }
808
+ const attachedProfiles = findInstalledHookReferencesForActivationRoot({
809
+ activationRoot: input.activationRoot
810
+ }).map((reference) => buildStatusAttachedProfileTruth({
811
+ reference,
812
+ currentOpenClawHome: input.openclawHome,
813
+ runtimeProofByHome,
814
+ runtimeProofError: runtimeProofs.error
815
+ }));
816
+ const currentInspection = input.openclawHome === null ? null : inspectOpenClawHome(input.openclawHome);
817
+ const currentProfileLabel = currentInspection?.profileId ?? "current_profile";
818
+ const installHook = summarizeStatusInstallHook(input.openclawHome);
819
+ const configLoad = summarizeStatusConfigLoad(input.openclawHome);
820
+ const currentRuntimeProof = input.openclawHome === null ? undefined : runtimeProofByHome.get(canonicalizeExistingCliPath(input.openclawHome));
821
+ const hookFiles = summarizeStatusHookFilesState(installHook);
822
+ const watcher = summarizeStatusAttachmentWatcher(input.status);
823
+ const runtimeLoad = input.openclawHome === null
824
+ ? "unverified"
825
+ : runtimeProofs.error !== null
826
+ ? "proof_error"
827
+ : hookFiles !== "present"
828
+ ? "not_proven"
829
+ : currentRuntimeProof !== undefined
830
+ ? "proven"
831
+ : "not_proven";
832
+ return {
833
+ currentProfileLabel,
834
+ hookFiles,
835
+ configLoad: configLoad.state,
836
+ runtimeLoad,
837
+ watcher,
838
+ attachedProfiles,
839
+ runtimeProofPath: runtimeProofs.path,
840
+ runtimeProofError: runtimeProofs.error,
841
+ detail: buildCurrentStatusAttachmentTruthDetail({
842
+ openclawHome: input.openclawHome,
843
+ currentProfileLabel,
844
+ hookFiles,
845
+ configLoad: configLoad.state,
846
+ runtimeLoad,
847
+ watcher,
848
+ attachedProfiles,
849
+ runtimeProofPath: runtimeProofs.path,
850
+ runtimeProofError: runtimeProofs.error
851
+ })
529
852
  };
530
853
  }
531
854
  function runOllamaProbe(args, baseUrl) {
@@ -639,6 +962,162 @@ function summarizeStatusLocalLlm(providerConfig) {
639
962
  : `teacher labeling is ${providerConfig.teacher.provider}; no local Ollama CLI was detected`
640
963
  };
641
964
  }
965
+ function summarizeStatusTeacher(report, providerConfig, localLlm) {
966
+ const enabled = providerConfig.teacher.provider === "ollama";
967
+ const latestCycle = report.teacherLoop.lastNoOpReason === "unavailable"
968
+ ? "unknown"
969
+ : report.teacherLoop.lastNoOpReason === "none"
970
+ ? "teacher_artifact"
971
+ : "no_op";
972
+ if (!enabled) {
973
+ return {
974
+ model: providerConfig.teacher.model,
975
+ enabled,
976
+ healthy: false,
977
+ stale: false,
978
+ idle: false,
979
+ latestCycle,
980
+ detail: `${providerConfig.teacher.model} is not enabled because teacher labeling is ${providerConfig.teacher.provider}`
981
+ };
982
+ }
983
+ if (!localLlm.detected) {
984
+ return {
985
+ model: providerConfig.teacher.model,
986
+ enabled,
987
+ healthy: false,
988
+ stale: null,
989
+ idle: false,
990
+ latestCycle,
991
+ detail: `${providerConfig.teacher.model} is configured on Ollama, but the local LLM surface is not answering at ${providerConfig.teacherBaseUrl}`
992
+ };
993
+ }
994
+ if (!report.teacherLoop.available) {
995
+ return {
996
+ model: providerConfig.teacher.model,
997
+ enabled,
998
+ healthy: null,
999
+ stale: null,
1000
+ idle: null,
1001
+ latestCycle,
1002
+ detail: `${providerConfig.teacher.model} is enabled on Ollama, but no watch teacher snapshot is visible yet`
1003
+ };
1004
+ }
1005
+ const stale = report.teacherLoop.latestFreshness === "stale" || report.teacherLoop.watchState === "stale_snapshot";
1006
+ const idle = report.teacherLoop.running === false &&
1007
+ (report.teacherLoop.queueDepth ?? 0) === 0 &&
1008
+ report.teacherLoop.failureMode === "none";
1009
+ const healthy = report.teacherLoop.failureMode === "none" &&
1010
+ stale === false &&
1011
+ report.teacherLoop.watchState !== "not_visible";
1012
+ const cycleDetail = TEACHER_NO_OP_MESSAGES[report.teacherLoop.lastNoOpReason] ?? "the latest teacher cycle detail is unavailable";
1013
+ if (report.teacherLoop.failureMode !== "none" && report.teacherLoop.failureMode !== "unavailable") {
1014
+ return {
1015
+ model: providerConfig.teacher.model,
1016
+ enabled,
1017
+ healthy: false,
1018
+ stale,
1019
+ idle,
1020
+ latestCycle,
1021
+ detail: report.teacherLoop.failureDetail === null
1022
+ ? `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}`
1023
+ : `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}: ${report.teacherLoop.failureDetail}`
1024
+ };
1025
+ }
1026
+ return {
1027
+ model: providerConfig.teacher.model,
1028
+ enabled,
1029
+ healthy,
1030
+ stale,
1031
+ idle,
1032
+ latestCycle,
1033
+ detail: `${providerConfig.teacher.model} is enabled on Ollama; ${cycleDetail}`
1034
+ };
1035
+ }
1036
+ function summarizeStatusEmbedder(embeddings) {
1037
+ const provisioned = embeddings.provisionedState === "confirmed" || embeddings.provisionedState === "builtin"
1038
+ ? true
1039
+ : embeddings.provisionedState === "not_confirmed" || embeddings.provisionedState === "off"
1040
+ ? false
1041
+ : null;
1042
+ const live = embeddings.liveState === "yes" ? true : embeddings.liveState === "no" ? false : null;
1043
+ if (embeddings.provider === "off") {
1044
+ return {
1045
+ model: embeddings.model,
1046
+ provisioned,
1047
+ live,
1048
+ detail: `${embeddings.model} is not provisioned because the embedder provider is off`
1049
+ };
1050
+ }
1051
+ if (embeddings.provider === "keywords") {
1052
+ return {
1053
+ model: embeddings.model,
1054
+ provisioned,
1055
+ live,
1056
+ detail: "keyword embeddings are builtin, so there is no Ollama model to provision"
1057
+ };
1058
+ }
1059
+ if (provisioned === true && live === true) {
1060
+ return {
1061
+ model: embeddings.model,
1062
+ provisioned,
1063
+ live,
1064
+ detail: `${embeddings.model} is confirmed on Ollama and the active pack stores live numeric embeddings`
1065
+ };
1066
+ }
1067
+ if (provisioned === true && live === false) {
1068
+ return {
1069
+ model: embeddings.model,
1070
+ provisioned,
1071
+ live,
1072
+ detail: `${embeddings.model} is confirmed on Ollama, but the active pack still has no live numeric embeddings`
1073
+ };
1074
+ }
1075
+ if (provisioned === false && live === true) {
1076
+ return {
1077
+ model: embeddings.model,
1078
+ provisioned,
1079
+ live,
1080
+ detail: `${embeddings.model} is not confirmed on Ollama, but the active pack already carries numeric embeddings from an earlier materialization`
1081
+ };
1082
+ }
1083
+ return {
1084
+ model: embeddings.model,
1085
+ provisioned,
1086
+ live,
1087
+ detail: embeddings.detail
1088
+ };
1089
+ }
1090
+ function summarizeStatusRouteFn(status, report) {
1091
+ const freshness = report.servePath.refreshStatus ?? status.brain.routeFreshness;
1092
+ if (!report.routeFn.available) {
1093
+ return {
1094
+ available: false,
1095
+ freshness,
1096
+ trainedAt: report.routeFn.trainedAt,
1097
+ updatedAt: report.routeFn.updatedAt,
1098
+ usedAt: report.routeFn.usedAt,
1099
+ detail: report.routeFn.detail
1100
+ };
1101
+ }
1102
+ let detail = report.routeFn.detail;
1103
+ if (report.servePath.usedLearnedRouteFn === true) {
1104
+ detail = `current serve proof used the learned route_fn; ${report.routeFn.detail}`;
1105
+ }
1106
+ else if (report.routeFn.usedAt !== null) {
1107
+ 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}`;
1108
+ }
1109
+ else if (report.routeFn.updatedAt !== null) {
1110
+ detail = `active route_fn was last updated at ${report.routeFn.updatedAt}, but no learned serve use is visible yet for the current pack`;
1111
+ }
1112
+ return {
1113
+ available: true,
1114
+ freshness,
1115
+ trainedAt: report.routeFn.trainedAt,
1116
+ updatedAt: report.routeFn.updatedAt,
1117
+ usedAt: report.routeFn.usedAt,
1118
+ detail
1119
+ };
1120
+ }
642
1121
  function pushUniqueAlert(target, value) {
643
1122
  const normalized = value.trim();
644
1123
  if (normalized.length === 0) {
@@ -698,11 +1177,8 @@ function summarizeStatusAlerts(report, providerConfig, embeddings, localLlm) {
698
1177
  }
699
1178
  return buckets;
700
1179
  }
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";
1180
+ function summarizeStatusWatchState(status) {
1181
+ return status.passiveLearning.watchState;
706
1182
  }
707
1183
  function summarizeStatusServeReality(status) {
708
1184
  if (status.brainStatus.serveState === "serving_active_pack") {
@@ -710,48 +1186,128 @@ function summarizeStatusServeReality(status) {
710
1186
  }
711
1187
  return status.brainStatus.serveState;
712
1188
  }
1189
+ function summarizeStatusPromotionState(status) {
1190
+ if (status.brain.state === "pg_promoted_pack_authoritative") {
1191
+ return "promoted";
1192
+ }
1193
+ if (status.brain.state === "seed_state_authoritative") {
1194
+ return status.passiveLearning.firstExportOccurred ? "seed_authoritative" : "awaiting_first_export";
1195
+ }
1196
+ return status.brain.state;
1197
+ }
713
1198
  function formatStatusAlertLine(values) {
714
1199
  const normalized = values.map((value) => value.trim()).filter((value) => value.length > 0);
715
1200
  return normalized.length === 0 ? "none" : formatCompactList(normalized, 2, 64);
716
1201
  }
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";
1202
+ function formatStatusNullableNumber(value, unknown = "unknown") {
1203
+ return value === null ? unknown : String(value);
1204
+ }
1205
+ function formatStatusNullableYesNo(value) {
1206
+ return value === null ? "unknown" : yesNo(value);
1207
+ }
1208
+ function formatStatusNullableMilliseconds(value) {
1209
+ return value === null ? "none" : `${value.toFixed(2)}ms`;
1210
+ }
1211
+ function formatStatusHotPathTiming(timing) {
1212
+ return [
1213
+ `hotPath=${formatStatusNullableMilliseconds(timing.totalMs)}`,
1214
+ `route=${formatStatusNullableMilliseconds(timing.routeSelectionMs)}`,
1215
+ `prompt=${formatStatusNullableMilliseconds(timing.promptAssemblyMs)}`,
1216
+ `other=${formatStatusNullableMilliseconds(timing.otherMs)}`,
1217
+ `background=${timing.backgroundWorkIncluded ? "included" : "excluded"}`
1218
+ ].join(" ");
1219
+ }
1220
+ function formatStatusObservedDeltaTransition(delta) {
1221
+ if (delta.latestPackTransition === null) {
1222
+ return "none";
723
1223
  }
724
- return status.brainStatus.serveState === "serving_active_pack" ? "BRAIN_LOADED" : "BRAIN_NOT_YET_LOADED";
1224
+ return `${delta.latestPackTransition.kind}:${delta.latestPackTransition.fromPackId ?? "none"}->${delta.latestPackTransition.toPackId}`;
1225
+ }
1226
+ function formatAttachedProfileTruthCompact(entry) {
1227
+ const currentPrefix = entry.current ? "*" : "";
1228
+ return (`${currentPrefix}${entry.label}` +
1229
+ `[hook=${entry.hookFiles} config=${entry.configLoad} runtime=${entry.runtimeLoad}` +
1230
+ `${entry.runtimeLoadedAt === null ? "" : `@${entry.runtimeLoadedAt}`}]`);
1231
+ }
1232
+ function formatAttachedProfileTruthCompactList(entries) {
1233
+ return entries.length === 0
1234
+ ? "none"
1235
+ : formatCompactList(entries.map((entry) => formatAttachedProfileTruthCompact(entry)), 2, 80);
1236
+ }
1237
+ function formatAttachedProfileTruthDetailedList(entries) {
1238
+ return entries.length === 0
1239
+ ? "none"
1240
+ : entries
1241
+ .map((entry) => `${entry.current ? "*" : ""}${entry.label}` +
1242
+ `[hook=${entry.hookFiles} config=${entry.configLoad} runtime=${entry.runtimeLoad} loadedAt=${entry.runtimeLoadedAt ?? "none"}]`)
1243
+ .join(" ");
1244
+ }
1245
+ function summarizeDisplayedStatus(status, installHook) {
1246
+ return installHook.state === "blocked_by_allowlist" ? "fail" : status.brainStatus.status;
725
1247
  }
726
1248
  function buildCompactStatusHeader(status, report, options) {
727
1249
  const installHook = summarizeStatusInstallHook(options.openclawHome);
1250
+ const hookLoad = summarizeStatusHookLoad(installHook, status);
728
1251
  const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
729
1252
  const localLlm = summarizeStatusLocalLlm(options.providerConfig);
1253
+ const teacher = summarizeStatusTeacher(report, options.providerConfig, localLlm);
1254
+ const embedder = summarizeStatusEmbedder(embeddings);
1255
+ const routeFn = summarizeStatusRouteFn(status, report);
730
1256
  const alerts = summarizeStatusAlerts(report, options.providerConfig, embeddings, localLlm);
731
- const promoted = status.brain.state === "pg_promoted_pack_authoritative" ? "yes" : "no";
732
1257
  const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
1258
+ const attachmentTruth = summarizeStatusAttachmentTruth({
1259
+ activationRoot: status.host.activationRoot,
1260
+ openclawHome: options.openclawHome,
1261
+ status
1262
+ });
733
1263
  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`,
1264
+ `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1265
+ `hook install=${hookLoad.installState} loadProof=${hookLoad.loadProof} detail=${hookLoad.detail}`,
1266
+ `attachTruth current=${attachmentTruth.currentProfileLabel} hook=${attachmentTruth.hookFiles} config=${attachmentTruth.configLoad} runtime=${attachmentTruth.runtimeLoad} watcher=${attachmentTruth.watcher} attachedSet=${formatAttachedProfileTruthCompactList(attachmentTruth.attachedProfiles)} why=${attachmentTruth.detail}`,
1267
+ `passive firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)}`,
1268
+ `serving pack=${status.passiveLearning.currentServingPackId ?? "none"} lastExport=${status.passiveLearning.lastExportAt ?? "none"} lastPromotion=${status.passiveLearning.lastPromotionAt ?? "none"}`,
1269
+ `timing ${formatStatusHotPathTiming(status.brainStatus.timing)}`,
1270
+ `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)}`,
1271
+ `changed ${status.passiveLearning.lastObservedDelta.explanation}`,
736
1272
  `explain ${status.brain.summary}`,
1273
+ `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)}`,
1274
+ `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}`,
1275
+ `embedder model=${embedder.model} provisioned=${yesNo(embedder.provisioned)} live=${yesNo(embedder.live)} why=${embedder.detail}`,
1276
+ `routeFn available=${yesNo(routeFn.available)} freshness=${routeFn.freshness} trained=${routeFn.trainedAt ?? "none"} updated=${routeFn.updatedAt ?? "none"} used=${routeFn.usedAt ?? "none"} why=${routeFn.detail}`,
737
1277
  `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
738
1278
  `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
739
1279
  `alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
740
1280
  ];
741
1281
  }
742
1282
  function formatCurrentProfileStatusSummary(status, report, targetInspection, options) {
1283
+ const installHook = summarizeStatusInstallHook(options.openclawHome);
1284
+ const displayedStatus = summarizeDisplayedStatus(status, installHook);
1285
+ const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
1286
+ const localLlm = summarizeStatusLocalLlm(options.providerConfig);
1287
+ const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
1288
+ const attachmentTruth = summarizeStatusAttachmentTruth({
1289
+ activationRoot: status.host.activationRoot,
1290
+ openclawHome: options.openclawHome,
1291
+ status
1292
+ });
743
1293
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
744
1294
  const targetLine = targetInspection === null
745
1295
  ? `target activation=${status.host.activationRoot} source=activation_root_only`
746
1296
  : `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${shortenPath(path.join(targetInspection.openclawHome, "extensions", "openclawbrain", "index.ts"))}`;
747
1297
  return [
748
- `STATUS ${status.brainStatus.status}`,
1298
+ `STATUS ${displayedStatus}`,
749
1299
  ...buildCompactStatusHeader(status, report, options),
750
1300
  `answer ${status.brain.summary}`,
751
1301
  targetLine,
752
1302
  ...(targetInspection === null ? [] : [`preflight ${formatOpenClawTargetExplanation(targetInspection)}`]),
1303
+ `next ${buildStatusNextStep(status, report, {
1304
+ openclawHome: options.openclawHome,
1305
+ installHook
1306
+ })}`,
753
1307
  `host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
754
1308
  `profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
1309
+ `attachTruth current=${attachmentTruth.currentProfileLabel} hook=${attachmentTruth.hookFiles} config=${attachmentTruth.configLoad} runtime=${attachmentTruth.runtimeLoad} watcher=${attachmentTruth.watcher} detail=${attachmentTruth.detail}`,
1310
+ `attachedSet ${formatAttachedProfileTruthDetailedList(attachmentTruth.attachedProfiles)} proofPath=${shortenPath(attachmentTruth.runtimeProofPath)} proofError=${attachmentTruth.runtimeProofError ?? "none"}`,
755
1311
  `manyProfile surface=${report.manyProfile.operatorSurface} policy=${report.manyProfile.declaredAttachmentPolicy} intent=${report.manyProfile.sameGatewayIntent} checkedProof=${report.manyProfile.checkedInProofTopology} sameGatewayProof=${yesNo(report.manyProfile.sameGatewayProof)} sharedWriteProof=${yesNo(report.manyProfile.sharedWriteSafetyProof)}`,
756
1312
  `activation state=${status.brainStatus.activationState} detail=${status.brain.detail}`,
757
1313
  `brain pack=${status.brain.activePackId ?? "none"} state=${status.brain.state} init=${status.brain.initMode ?? "unknown"} routeFreshness=${status.brain.routeFreshness} lastPromotion=${status.brain.lastPromotionAt ?? "none"} router=${status.brain.routerIdentity ?? "none"}`,
@@ -760,13 +1316,17 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
760
1316
  `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
1317
  `decision ${status.brainStatus.structuralDecision.detail}`,
762
1318
  `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"}`,
1319
+ `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}`,
1320
+ `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
1321
  `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
1322
  `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}`,
1323
+ `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
1324
  `path ${formatLearningPathSummary(report.learningPath)}`,
767
1325
  `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"}`,
1326
+ `teacherProof ${formatTeacherLoopSummary(report)}`,
1327
+ `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"}`,
1328
+ `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
1329
+ `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
770
1330
  `rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
771
1331
  `proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
772
1332
  `logs root=${status.brain.logRoot ?? "none"}`,
@@ -795,6 +1355,15 @@ function formatOpenClawTargetExplanation(inspection) {
795
1355
  function buildInstallStatusCommand(activationRoot) {
796
1356
  return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
797
1357
  }
1358
+ function buildLearnerServiceStatusCommand(activationRoot) {
1359
+ return `openclawbrain daemon status --activation-root ${quoteShellArg(activationRoot)}`;
1360
+ }
1361
+ function buildGatewayRestartCommand(profileId) {
1362
+ return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway restart`;
1363
+ }
1364
+ function buildGatewayStatusCommand(profileId) {
1365
+ return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway status`;
1366
+ }
798
1367
  function buildInstallCommand(openclawHome) {
799
1368
  return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
800
1369
  }
@@ -978,6 +1547,12 @@ function writeInstallProviderDefaults(parsed) {
978
1547
  : "Teacher: no compatible local Ollama model detected; watch stays heuristic unless explicitly overridden"
979
1548
  };
980
1549
  }
1550
+ function shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall) {
1551
+ if (isInstall || activationPlan.action === "bootstrap") {
1552
+ return true;
1553
+ }
1554
+ return !existsSync(resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot));
1555
+ }
981
1556
  function buildInstallBrainFeedbackSummary(input) {
982
1557
  const providerDefaultsPath = resolveOpenClawBrainProviderDefaultsPath(input.parsed.activationRoot);
983
1558
  const embedderState = input.embedderProvision === null ? "unchanged" : input.embedderProvision.state;
@@ -985,15 +1560,61 @@ function buildInstallBrainFeedbackSummary(input) {
985
1560
  const teacherProvider = teacherDefaults?.provider ?? "unknown";
986
1561
  const teacherModel = teacherDefaults?.model ?? null;
987
1562
  const detectedLocalLlm = teacherDefaults?.detectedLocally ?? null;
1563
+ const profileName = input.targetInspection.profileId;
1564
+ const profileSource = input.targetInspection.profileSource;
1565
+ const casingGuidance = profileName === null
1566
+ ? "Exact OpenClaw --profile casing is unresolved here because this target stays on the host-selected current_profile boundary."
1567
+ : `Use the exact OpenClaw profile casing shown here for host-side restart/status commands: ${quoteShellArg(profileName)}.`;
1568
+ const attachment = input.parsed.shared
1569
+ ? {
1570
+ policy: "shared",
1571
+ activationRootMode: "shared_root_declared",
1572
+ sameGatewayProof: "not_checked_in",
1573
+ 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."
1574
+ }
1575
+ : {
1576
+ policy: "dedicated",
1577
+ activationRootMode: "dedicated_per_profile",
1578
+ sameGatewayProof: "not_applicable",
1579
+ detail: "Dedicated activation root for this profile/home boundary."
1580
+ };
1581
+ const restart = profileName === null
1582
+ ? {
1583
+ exactProfile: false,
1584
+ profile: null,
1585
+ profileSource,
1586
+ guidance: `Operator-owned restart step: this install did not infer an exact --profile token from ${shortenPath(input.targetInspection.openclawHome)}. ` +
1587
+ "If immediate load matters, restart the host-selected current_profile from OpenClaw itself; otherwise the next natural launch will pick up the hook.",
1588
+ restartCommand: null,
1589
+ gatewayStatusCommand: null
1590
+ }
1591
+ : {
1592
+ exactProfile: true,
1593
+ profile: profileName,
1594
+ profileSource,
1595
+ guidance: `Operator-owned restart step: if immediate load matters and profile ${quoteShellArg(profileName)} is already running, run ${buildGatewayRestartCommand(profileName)}. ` +
1596
+ `If it is stopped, the next launch of profile ${quoteShellArg(profileName)} will pick up the hook. ${casingGuidance}`,
1597
+ restartCommand: buildGatewayRestartCommand(profileName),
1598
+ gatewayStatusCommand: buildGatewayStatusCommand(profileName)
1599
+ };
988
1600
  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`;
1601
+ ? `hook written, activation root ready, seed/current-profile attach bootstrapped, learner service ${input.learnerService.state}, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
1602
+ : `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved, learner service ${input.learnerService.state}${input.providerDefaults === null ? "" : ", provider defaults written"}`;
1603
+ const notYetProved = input.learnerService.state === "deferred"
1604
+ ? `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`
1605
+ : input.activationPlan.action === "bootstrap"
1606
+ ? `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`
1607
+ : `Passive learning is wired for this activation root, but this ${input.parsed.command} run does not itself prove live startup/load after restart`;
994
1608
  return {
995
1609
  hookPath: input.extensionDir,
996
1610
  providerDefaultsPath,
1611
+ profile: {
1612
+ exactProfileName: profileName,
1613
+ profileSource,
1614
+ casingGuidance
1615
+ },
1616
+ attachment,
1617
+ restart,
997
1618
  embedder: {
998
1619
  provider: "ollama",
999
1620
  model: DEFAULT_OLLAMA_EMBEDDING_MODEL,
@@ -1004,6 +1625,14 @@ function buildInstallBrainFeedbackSummary(input) {
1004
1625
  model: teacherModel,
1005
1626
  detectedLocalLlm
1006
1627
  },
1628
+ learnerService: {
1629
+ state: input.learnerService.state,
1630
+ detail: input.learnerService.detail,
1631
+ plistPath: input.learnerService.plistPath,
1632
+ logPath: input.learnerService.logPath,
1633
+ configuredActivationRoot: input.learnerService.configuredActivationRoot,
1634
+ matchesRequestedActivationRoot: input.learnerService.matchesRequestedActivationRoot
1635
+ },
1007
1636
  startup: {
1008
1637
  token: "BRAIN_NOT_YET_LOADED",
1009
1638
  proof: "restart_required"
@@ -1012,19 +1641,28 @@ function buildInstallBrainFeedbackSummary(input) {
1012
1641
  notYetProved,
1013
1642
  lines: [
1014
1643
  `target ${formatOpenClawTargetLine(input.targetInspection)} source=${formatInstallOpenClawHomeSource(input.parsed.openclawHomeSource)}`,
1644
+ profileName === null
1645
+ ? "profile exactName=unresolved selector=current_profile casing=not_available"
1646
+ : `profile exactName=${quoteShellArg(profileName)} source=${profileSource} casing=preserved`,
1015
1647
  `hook written=${shortenPath(input.extensionDir)}`,
1016
1648
  `activation root=${shortenPath(input.parsed.activationRoot)} source=${formatInstallActivationRootSource(input.parsed.activationRootSource)}`,
1649
+ `attachment policy=${attachment.policy} rootMode=${attachment.activationRootMode} sameGatewayProof=${attachment.sameGatewayProof} detail=${attachment.detail}`,
1017
1650
  `defaults provider-defaults=${shortenPath(providerDefaultsPath)} state=${input.providerDefaults === null ? "unchanged" : "written"}`,
1018
1651
  `embedder provider=ollama model=${DEFAULT_OLLAMA_EMBEDDING_MODEL} state=${embedderState}`,
1019
1652
  `teacher provider=${teacherProvider} model=${teacherModel ?? "none"} localLLM=${detectedLocalLlm === null ? "unknown" : yesNo(detectedLocalLlm)}`,
1653
+ `learner state=${input.learnerService.state} detail=${input.learnerService.detail}`,
1654
+ `restart operator=manual exactProfile=${yesNo(restart.exactProfile)} command=${restart.restartCommand ?? "unavailable"}`,
1020
1655
  "startup BRAIN_NOT_YET_LOADED proof=restart_required",
1021
1656
  `provedNow ${provedNow}`,
1022
1657
  `notYet ${notYetProved}`
1023
1658
  ]
1024
1659
  };
1025
1660
  }
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.";
1661
+ function buildInstallReloadGuidance(input) {
1662
+ if (input.targetInspection.profileId === null) {
1663
+ 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.`;
1664
+ }
1665
+ return `Restart now if immediate load matters: ${buildGatewayRestartCommand(input.targetInspection.profileId)}`;
1028
1666
  }
1029
1667
  const LEGACY_PROFILE_NOTE_FILENAMES = ["BRAIN.md", "brain.md"];
1030
1668
  const LEGACY_BRAIN_AGENTS_LINE = "5. Read `BRAIN.md` — your learning brain context";
@@ -1108,8 +1746,21 @@ function buildCleanupRestartGuidance(restart) {
1108
1746
  }
1109
1747
  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
1748
  }
1111
- function buildStatusNextStep(status, report) {
1749
+ function buildStatusNextStep(status, report, options) {
1112
1750
  const activationRootArg = quoteShellArg(status.host.activationRoot);
1751
+ const attachmentTruth = summarizeStatusAttachmentTruth({
1752
+ activationRoot: status.host.activationRoot,
1753
+ openclawHome: options.openclawHome,
1754
+ status
1755
+ });
1756
+ if (options.installHook.state === "blocked_by_allowlist") {
1757
+ if (options.openclawHome === null) {
1758
+ return "Repair the OpenClaw plugin allowlist mismatch before trusting serve-path status again.";
1759
+ }
1760
+ return ("Repair the OpenClaw plugin allowlist mismatch " +
1761
+ `(rerun ${buildInstallCommand(options.openclawHome)} or ${buildAttachCommand(options.openclawHome, status.host.activationRoot)}) ` +
1762
+ "before trusting serve-path status again.");
1763
+ }
1113
1764
  if (status.brainStatus.activationState === "broken_install") {
1114
1765
  return "Repair or replace the activation root before trusting serve-path status again.";
1115
1766
  }
@@ -1119,9 +1770,27 @@ function buildStatusNextStep(status, report) {
1119
1770
  if (status.brainStatus.status === "fail") {
1120
1771
  return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
1121
1772
  }
1773
+ if (options.openclawHome !== null && options.installHook.state === "not_installed") {
1774
+ return `Run \`${buildInstallCommand(options.openclawHome)}\` before expecting this OpenClaw home to load the brain hook.`;
1775
+ }
1776
+ if (options.openclawHome !== null &&
1777
+ attachmentTruth.hookFiles === "present" &&
1778
+ attachmentTruth.configLoad === "allows_load" &&
1779
+ attachmentTruth.runtimeLoad === "not_proven") {
1780
+ return "Restart the exact OpenClaw profile or wait for its next launch, then rerun status until runtime load becomes proven instead of assumed from on-disk state.";
1781
+ }
1122
1782
  if (status.brainStatus.awaitingFirstExport) {
1123
1783
  return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
1124
1784
  }
1785
+ if (options.openclawHome === null) {
1786
+ return `Pin \`--openclaw-home <path>\` when you need exact hook-install truth; activation-root-only status only proves this root's serve-path state.`;
1787
+ }
1788
+ if (attachmentTruth.runtimeLoad === "proof_error") {
1789
+ return "Repair the runtime-load proof file before trusting attach truth again; status now knows the exact file that broke.";
1790
+ }
1791
+ if (options.installHook.state === "installed" && status.brainStatus.serveState === "serving_active_pack") {
1792
+ return "Check the OpenClaw startup log for the `[openclawbrain] BRAIN LOADED` breadcrumb when you need live hook-load proof.";
1793
+ }
1125
1794
  if (report.learning.warningStates.includes("principal_live_backlog") ||
1126
1795
  report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
1127
1796
  return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
@@ -1132,14 +1801,19 @@ function buildStatusNextStep(status, report) {
1132
1801
  return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
1133
1802
  }
1134
1803
  function formatHumanFriendlyStatus(status, report, targetInspection, options) {
1804
+ const installHook = summarizeStatusInstallHook(options.openclawHome);
1805
+ const displayedStatus = summarizeDisplayedStatus(status, installHook);
1135
1806
  const lines = [
1136
- `STATUS ${status.brainStatus.status}`,
1807
+ `STATUS ${displayedStatus}`,
1137
1808
  ...buildCompactStatusHeader(status, report, options),
1138
1809
  ...(targetInspection === null ? [] : [
1139
1810
  `target ${formatOpenClawTargetLine(targetInspection)}`,
1140
1811
  `preflight ${formatOpenClawTargetExplanation(targetInspection)}`
1141
1812
  ]),
1142
- `next ${buildStatusNextStep(status, report)}`
1813
+ `next ${buildStatusNextStep(status, report, {
1814
+ openclawHome: options.openclawHome,
1815
+ installHook
1816
+ })}`
1143
1817
  ];
1144
1818
  return lines.join("\n");
1145
1819
  }
@@ -2139,7 +2813,7 @@ function buildExtensionIndexTs(activationRoot) {
2139
2813
  function buildExtensionPackageJson() {
2140
2814
  const packageMetadata = readOpenClawPackageMetadata();
2141
2815
  return JSON.stringify({
2142
- name: "openclawbrain-extension",
2816
+ name: "openclawbrain",
2143
2817
  version: packageMetadata.version,
2144
2818
  private: true,
2145
2819
  type: "module",
@@ -2242,6 +2916,67 @@ function buildHistoryEntry(record, slot, isActive) {
2242
2916
  current: isActive
2243
2917
  };
2244
2918
  }
2919
+ function ensureLifecycleLearnerService(activationRoot) {
2920
+ const outcome = ensureManagedLearnerServiceForActivationRoot(activationRoot);
2921
+ return {
2922
+ state: outcome.state,
2923
+ detail: outcome.detail,
2924
+ plistPath: outcome.inspection.plistPath,
2925
+ logPath: outcome.inspection.logPath,
2926
+ configuredActivationRoot: outcome.inspection.configuredActivationRoot,
2927
+ matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
2928
+ };
2929
+ }
2930
+ function resolveCleanupLearnerServiceOutcome(activationRoot, openclawHome) {
2931
+ if (activationRoot === null) {
2932
+ return {
2933
+ state: "unresolved",
2934
+ detail: "Learner service preservation is unresolved because the activation root could not be resolved from the installed profile hook.",
2935
+ plistPath: null,
2936
+ logPath: null,
2937
+ configuredActivationRoot: null,
2938
+ matchesRequestedActivationRoot: null
2939
+ };
2940
+ }
2941
+ const remainingProfiles = findOtherInstalledHookReferencesForActivationRoot({
2942
+ activationRoot,
2943
+ excludingOpenClawHome: openclawHome
2944
+ });
2945
+ const partitioned = partitionSharedActivationRootHookReferences(remainingProfiles);
2946
+ if (partitioned.attached.length > 0) {
2947
+ const inspection = inspectManagedLearnerService(activationRoot);
2948
+ const attachedProfiles = partitioned.attached
2949
+ .map(({ openclawHome: profileHome }) => shortenPath(path.resolve(profileHome)))
2950
+ .join(", ");
2951
+ const halfAttachedNote = partitioned.halfAttached.length === 0
2952
+ ? ""
2953
+ : ` Half-attached hooks still point at this root but were not counted as attached: ${partitioned.halfAttached
2954
+ .map((reference) => `${shortenPath(path.resolve(reference.openclawHome))} (${summarizeSharedActivationRootReferenceProof(reference)})`)
2955
+ .join(", ")}.`;
2956
+ return {
2957
+ state: "preserved",
2958
+ detail: `Preserved the background learner service for ${path.resolve(activationRoot)} because other attached OpenClaw profiles still share this activation root: ${attachedProfiles}.${halfAttachedNote}`,
2959
+ plistPath: inspection.plistPath,
2960
+ logPath: inspection.logPath,
2961
+ configuredActivationRoot: inspection.configuredActivationRoot,
2962
+ matchesRequestedActivationRoot: inspection.matchesRequestedActivationRoot
2963
+ };
2964
+ }
2965
+ const outcome = removeManagedLearnerServiceForActivationRoot(activationRoot);
2966
+ const halfAttachedNote = partitioned.halfAttached.length === 0
2967
+ ? ""
2968
+ : ` Ignored half-attached OpenClaw profile hooks that still point at this activation root because they do not prove serve-path attachment: ${partitioned.halfAttached
2969
+ .map((reference) => `${shortenPath(path.resolve(reference.openclawHome))} (${summarizeSharedActivationRootReferenceProof(reference)})`)
2970
+ .join(", ")}.`;
2971
+ return {
2972
+ state: outcome.state,
2973
+ detail: `${outcome.detail}${halfAttachedNote}`,
2974
+ plistPath: outcome.inspection.plistPath,
2975
+ logPath: outcome.inspection.logPath,
2976
+ configuredActivationRoot: outcome.inspection.configuredActivationRoot,
2977
+ matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
2978
+ };
2979
+ }
2245
2980
  function formatInspectionFindings(findings) {
2246
2981
  return findings.join("; ");
2247
2982
  }
@@ -2438,11 +3173,11 @@ function runProfileHookAttachCommand(parsed) {
2438
3173
  }
2439
3174
  steps.push(activationPlan.inspectionStep);
2440
3175
  // 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"
3176
+ const providerDefaults = shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall)
2442
3177
  ? writeInstallProviderDefaults(parsed)
2443
3178
  : null;
2444
3179
  if (providerDefaults === null) {
2445
- steps.push("Skipped provider-default refresh because explicit attach is reusing existing activation data.");
3180
+ steps.push("Preserved existing provider-defaults.json because explicit attach is reusing existing activation data.");
2446
3181
  }
2447
3182
  else {
2448
3183
  steps.push(providerDefaults.detail);
@@ -2528,16 +3263,38 @@ function runProfileHookAttachCommand(parsed) {
2528
3263
  const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
2529
3264
  writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
2530
3265
  steps.push(`Wrote manifest: ${manifestPath}`);
2531
- const restartGuidance = buildInstallReloadGuidance();
3266
+ const pluginConfigRepair = ensureOpenClawBrainPluginConfig(parsed.openclawHome);
3267
+ steps.push(pluginConfigRepair.detail);
3268
+ const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
3269
+ steps.push(learnerService.detail);
3270
+ const brainFeedback = buildInstallBrainFeedbackSummary({
3271
+ parsed,
3272
+ targetInspection,
3273
+ extensionDir,
3274
+ activationPlan,
3275
+ learnerService,
3276
+ embedderProvision,
3277
+ providerDefaults
3278
+ });
3279
+ const restartGuidance = buildInstallReloadGuidance({
3280
+ targetInspection
3281
+ });
2532
3282
  const nextSteps = [
2533
3283
  restartGuidance,
3284
+ brainFeedback.restart.gatewayStatusCommand === null
3285
+ ? null
3286
+ : `Confirm gateway after restart: ${brainFeedback.restart.gatewayStatusCommand}`,
2534
3287
  `Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`,
3288
+ `Check learner service: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`,
2535
3289
  embedderProvision !== null && embedderProvision.state === "skipped"
2536
3290
  ? `Provision default embedder later: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`
2537
3291
  : null
2538
3292
  ].filter((step) => step !== null);
2539
3293
  const preflightSummary = [
2540
3294
  `Hook: installed at ${shortenPath(extensionDir)}`,
3295
+ parsed.shared
3296
+ ? "Attachment policy: shared activation root declared; same-gateway many-profile load/serve proof is still not checked in"
3297
+ : "Attachment policy: dedicated activation root for this profile/home boundary",
2541
3298
  activationPlan.action === "bootstrap"
2542
3299
  ? "Attachment: seed/current-profile attach created; restart plus status will prove later serve-path use"
2543
3300
  : `Attachment: existing active pack ${activationPlan.activePackId} kept in place; restart plus status will prove later serve-path use`,
@@ -2546,6 +3303,7 @@ function runProfileHookAttachCommand(parsed) {
2546
3303
  : embedderProvision.state === "ensured"
2547
3304
  ? `Embedder: default Ollama model ${embedderProvision.model} was ensured before bootstrap`
2548
3305
  : `Embedder: default Ollama model ${embedderProvision.model} was intentionally skipped`,
3306
+ `Learner: background service ${learnerService.state} for the exact activation root/profile boundary`,
2549
3307
  `Serve path: install alone does not prove serving; restart the profile and run ${buildInstallStatusCommand(parsed.activationRoot)}`
2550
3308
  ];
2551
3309
  const lifecycleSummary = [
@@ -2554,7 +3312,11 @@ function runProfileHookAttachCommand(parsed) {
2554
3312
  : "Lifecycle mode: attach (explicit reattach/manual profile hookup)",
2555
3313
  `OpenClaw target: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
2556
3314
  `Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`,
3315
+ brainFeedback.profile.exactProfileName === null
3316
+ ? "Profile token: current_profile only; this install did not infer an exact --profile token"
3317
+ : `Profile token: use exact OpenClaw profile casing ${quoteShellArg(brainFeedback.profile.exactProfileName)} for host-side restart/status commands`,
2557
3318
  `Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
3319
+ `Attachment policy: ${brainFeedback.attachment.policy} (${brainFeedback.attachment.detail})`,
2558
3320
  `Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
2559
3321
  embedderProvision === null
2560
3322
  ? "Embedder: unchanged because no bootstrap was needed"
@@ -2563,6 +3325,7 @@ function runProfileHookAttachCommand(parsed) {
2563
3325
  : `Embedder: skipped default Ollama model ${embedderProvision.model} via ${parsed.skipEmbedderProvisionSource === "flag" ? "--skip-embedder-provision" : OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}`,
2564
3326
  ...(providerDefaults === null ? [] : [`${providerDefaults.lifecycleSummary} (${shortenPath(providerDefaults.path)})`]),
2565
3327
  `Profile hook: installed at ${shortenPath(extensionDir)}`,
3328
+ `Learner service: ${learnerService.state} for ${shortenPath(parsed.activationRoot)}`,
2566
3329
  activationPlan.resolution === "new_root"
2567
3330
  ? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
2568
3331
  : activationPlan.resolution === "missing_pointers"
@@ -2580,14 +3343,6 @@ function runProfileHookAttachCommand(parsed) {
2580
3343
  ? `Install: kept healthy active pack ${activationPlan.activePackId} in place`
2581
3344
  : `Attach: rewired the profile hook to healthy active pack ${activationPlan.activePackId}`
2582
3345
  ];
2583
- const brainFeedback = buildInstallBrainFeedbackSummary({
2584
- parsed,
2585
- targetInspection,
2586
- extensionDir,
2587
- activationPlan,
2588
- embedderProvision,
2589
- providerDefaults
2590
- });
2591
3346
  // 9. Print summary
2592
3347
  if (parsed.json) {
2593
3348
  console.log(JSON.stringify({
@@ -2642,11 +3397,17 @@ function runProfileHookAttachCommand(parsed) {
2642
3397
  teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
2643
3398
  embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
2644
3399
  },
3400
+ pluginConfigRepair,
3401
+ learnerService,
2645
3402
  brainFeedback: {
2646
3403
  hookPath: brainFeedback.hookPath,
2647
3404
  providerDefaultsPath: brainFeedback.providerDefaultsPath,
3405
+ profile: brainFeedback.profile,
3406
+ attachment: brainFeedback.attachment,
3407
+ restart: brainFeedback.restart,
2648
3408
  embedder: brainFeedback.embedder,
2649
3409
  teacher: brainFeedback.teacher,
3410
+ learnerService: brainFeedback.learnerService,
2650
3411
  startup: brainFeedback.startup,
2651
3412
  provedNow: brainFeedback.provedNow,
2652
3413
  notYetProved: brainFeedback.notYetProved,
@@ -2666,8 +3427,12 @@ function runProfileHookAttachCommand(parsed) {
2666
3427
  for (const line of brainFeedback.lines) {
2667
3428
  console.log(` ${line}`);
2668
3429
  }
2669
- console.log(`Next: ${restartGuidance}`);
3430
+ console.log(`Restart: ${restartGuidance}`);
3431
+ if (brainFeedback.restart.gatewayStatusCommand !== null) {
3432
+ console.log(`Gateway: Confirm OpenClaw after restart: ${brainFeedback.restart.gatewayStatusCommand}`);
3433
+ }
2670
3434
  console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
3435
+ console.log(`Learner: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`);
2671
3436
  if (embedderProvision !== null && embedderProvision.state === "skipped") {
2672
3437
  console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
2673
3438
  }
@@ -2689,6 +3454,125 @@ function validateOpenClawHome(openclawHome) {
2689
3454
  throw new Error(`openclaw.json not found in ${openclawHome}`);
2690
3455
  }
2691
3456
  }
3457
+ function readJsonObjectRecord(value) {
3458
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3459
+ return null;
3460
+ }
3461
+ return value;
3462
+ }
3463
+ function readOpenClawJsonConfig(openclawHome) {
3464
+ const openclawJsonPath = path.join(openclawHome, "openclaw.json");
3465
+ let parsed;
3466
+ try {
3467
+ parsed = JSON.parse(readFileSync(openclawJsonPath, "utf8"));
3468
+ }
3469
+ catch (error) {
3470
+ throw new Error(`Failed to read ${openclawJsonPath}: ${toErrorMessage(error)}`);
3471
+ }
3472
+ const config = readJsonObjectRecord(parsed);
3473
+ if (config === null) {
3474
+ throw new Error(`Failed to read ${openclawJsonPath}: openclaw.json must contain a top-level object`);
3475
+ }
3476
+ return {
3477
+ path: openclawJsonPath,
3478
+ config
3479
+ };
3480
+ }
3481
+ function ensureOpenClawBrainPluginConfig(openclawHome) {
3482
+ const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3483
+ const plugins = readJsonObjectRecord(config.plugins);
3484
+ if (plugins === null) {
3485
+ return {
3486
+ path: openclawJsonPath,
3487
+ changed: false,
3488
+ detail: `Left ${shortenPath(openclawJsonPath)} unchanged because plugins.allow is not configured`
3489
+ };
3490
+ }
3491
+ if (!Object.prototype.hasOwnProperty.call(plugins, "allow")) {
3492
+ return {
3493
+ path: openclawJsonPath,
3494
+ changed: false,
3495
+ detail: `Left ${shortenPath(openclawJsonPath)} unchanged because plugins.allow is not configured`
3496
+ };
3497
+ }
3498
+ if (!Array.isArray(plugins.allow)) {
3499
+ return {
3500
+ path: openclawJsonPath,
3501
+ changed: false,
3502
+ detail: `Left ${shortenPath(openclawJsonPath)} unchanged because plugins.allow is not an array`
3503
+ };
3504
+ }
3505
+ if (plugins.allow.includes("openclawbrain")) {
3506
+ return {
3507
+ path: openclawJsonPath,
3508
+ changed: false,
3509
+ detail: `Verified ${shortenPath(openclawJsonPath)} plugins.allow already includes openclawbrain`
3510
+ };
3511
+ }
3512
+ plugins.allow = [...plugins.allow, "openclawbrain"];
3513
+ config.plugins = plugins;
3514
+ writeFileSync(openclawJsonPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
3515
+ return {
3516
+ path: openclawJsonPath,
3517
+ changed: true,
3518
+ detail: `Repaired ${shortenPath(openclawJsonPath)} plugins.allow by adding openclawbrain`
3519
+ };
3520
+ }
3521
+ function scrubOpenClawBrainPluginConfig(openclawHome) {
3522
+ const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3523
+ const plugins = readJsonObjectRecord(config.plugins);
3524
+ if (plugins === null) {
3525
+ return {
3526
+ path: openclawJsonPath,
3527
+ changed: false,
3528
+ detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
3529
+ };
3530
+ }
3531
+ const changes = [];
3532
+ let changed = false;
3533
+ if (Array.isArray(plugins.allow)) {
3534
+ const filteredAllow = plugins.allow.filter((entry) => entry !== "openclawbrain");
3535
+ if (filteredAllow.length !== plugins.allow.length) {
3536
+ changed = true;
3537
+ changes.push("removed plugins.allow entry");
3538
+ if (filteredAllow.length > 0) {
3539
+ plugins.allow = filteredAllow;
3540
+ }
3541
+ else {
3542
+ delete plugins.allow;
3543
+ }
3544
+ }
3545
+ }
3546
+ const entries = readJsonObjectRecord(plugins.entries);
3547
+ if (entries !== null && Object.prototype.hasOwnProperty.call(entries, "openclawbrain")) {
3548
+ delete entries.openclawbrain;
3549
+ changed = true;
3550
+ changes.push("removed plugins.entries.openclawbrain");
3551
+ }
3552
+ if (entries !== null && Object.keys(entries).length === 0 && Object.prototype.hasOwnProperty.call(plugins, "entries")) {
3553
+ delete plugins.entries;
3554
+ changed = true;
3555
+ changes.push("removed empty plugins.entries container");
3556
+ }
3557
+ if (Object.keys(plugins).length === 0 && Object.prototype.hasOwnProperty.call(config, "plugins")) {
3558
+ delete config.plugins;
3559
+ changed = true;
3560
+ changes.push("removed empty plugins container");
3561
+ }
3562
+ if (changed) {
3563
+ writeFileSync(openclawJsonPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3564
+ return {
3565
+ path: openclawJsonPath,
3566
+ changed: true,
3567
+ detail: `Scrubbed stale openclawbrain plugin config in ${openclawJsonPath}: ${changes.join(", ")}`
3568
+ };
3569
+ }
3570
+ return {
3571
+ path: openclawJsonPath,
3572
+ changed: false,
3573
+ detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
3574
+ };
3575
+ }
2692
3576
  function resolveCleanupActivationRoot(openclawHome, explicitActivationRoot) {
2693
3577
  if (explicitActivationRoot !== null) {
2694
3578
  return path.resolve(explicitActivationRoot);
@@ -2726,12 +3610,32 @@ function summarizeKeptActivationData(activationRoot) {
2726
3610
  function buildRestartGuidance(restart) {
2727
3611
  return buildCleanupRestartGuidance(restart);
2728
3612
  }
3613
+ function clearCleanupRuntimeLoadProof(activationRoot, openclawHome, steps) {
3614
+ if (activationRoot === null) {
3615
+ return;
3616
+ }
3617
+ try {
3618
+ const cleared = clearOpenClawProfileRuntimeLoadProof({
3619
+ activationRoot,
3620
+ openclawHome
3621
+ });
3622
+ if (cleared) {
3623
+ steps.push(`Cleared runtime-load proof for ${shortenPath(openclawHome)} from ${shortenPath(resolveAttachmentRuntimeLoadProofsPath(activationRoot))}`);
3624
+ }
3625
+ }
3626
+ catch (error) {
3627
+ steps.push(`Runtime-load proof cleanup failed open at ${shortenPath(resolveAttachmentRuntimeLoadProofsPath(activationRoot))}: ${toErrorMessage(error)}`);
3628
+ }
3629
+ }
2729
3630
  function runDetachCommand(parsed) {
2730
3631
  const steps = [];
2731
3632
  validateOpenClawHome(parsed.openclawHome);
2732
3633
  const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2733
3634
  steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2734
3635
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3636
+ clearCleanupRuntimeLoadProof(activationRoot, parsed.openclawHome, steps);
3637
+ const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3638
+ const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
2735
3639
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2736
3640
  const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2737
3641
  const activationData = summarizeKeptActivationData(activationRoot);
@@ -2739,14 +3643,17 @@ function runDetachCommand(parsed) {
2739
3643
  const nextSteps = [
2740
3644
  restartGuidance,
2741
3645
  activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
3646
+ activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
2742
3647
  `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2743
3648
  ].filter((step) => step !== null);
3649
+ steps.push(pluginConfigCleanup.detail);
2744
3650
  if (legacyResidue.removedNotes.length > 0) {
2745
3651
  steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
2746
3652
  }
2747
3653
  if (legacyResidue.updatedAgents.length > 0) {
2748
3654
  steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
2749
3655
  }
3656
+ steps.push(learnerService.detail);
2750
3657
  steps.push(activationData.activationDataDetail);
2751
3658
  steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
2752
3659
  if (parsed.json) {
@@ -2764,6 +3671,8 @@ function runDetachCommand(parsed) {
2764
3671
  activationRoot,
2765
3672
  dataAction: "kept",
2766
3673
  activationDataState: activationData.activationDataState,
3674
+ pluginConfigCleanup,
3675
+ learnerService,
2767
3676
  removedLegacyNotes: legacyResidue.removedNotes,
2768
3677
  updatedAgents: legacyResidue.updatedAgents,
2769
3678
  restartMode: parsed.restart,
@@ -2786,9 +3695,12 @@ function runDetachCommand(parsed) {
2786
3695
  else {
2787
3696
  console.log("Brain data: preserved, but the activation root could not be resolved from the removed hook.");
2788
3697
  }
3698
+ console.log(`Config: ${pluginConfigCleanup.detail}`);
3699
+ console.log(`Learner: ${learnerService.detail}`);
2789
3700
  console.log(`Next: ${restartGuidance}`);
2790
3701
  if (activationRoot !== null) {
2791
3702
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
3703
+ console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
2792
3704
  }
2793
3705
  console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
2794
3706
  }
@@ -2800,6 +3712,21 @@ function runUninstallCommand(parsed) {
2800
3712
  const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2801
3713
  steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2802
3714
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3715
+ clearCleanupRuntimeLoadProof(activationRoot, parsed.openclawHome, steps);
3716
+ if (parsed.dataMode === "purge" && activationRoot !== null) {
3717
+ assertActivationRootPurgeIsNotShared({
3718
+ activationRoot,
3719
+ openclawHome: parsed.openclawHome
3720
+ });
3721
+ }
3722
+ const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3723
+ const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
3724
+ if (parsed.dataMode === "purge" &&
3725
+ activationRoot !== null &&
3726
+ learnerService.state === "preserved" &&
3727
+ learnerService.matchesRequestedActivationRoot !== false) {
3728
+ 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}`);
3729
+ }
2803
3730
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
2804
3731
  const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2805
3732
  let activationData;
@@ -2830,16 +3757,19 @@ function runUninstallCommand(parsed) {
2830
3757
  const nextSteps = [
2831
3758
  restartGuidance,
2832
3759
  parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
3760
+ activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
2833
3761
  parsed.dataMode === "keep"
2834
3762
  ? `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2835
3763
  : `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
2836
3764
  ].filter((step) => step !== null);
3765
+ steps.push(pluginConfigCleanup.detail);
2837
3766
  if (legacyResidue.removedNotes.length > 0) {
2838
3767
  steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
2839
3768
  }
2840
3769
  if (legacyResidue.updatedAgents.length > 0) {
2841
3770
  steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
2842
3771
  }
3772
+ steps.push(learnerService.detail);
2843
3773
  steps.push(activationData.activationDataDetail);
2844
3774
  steps.push(parsed.dataMode === "purge"
2845
3775
  ? "Uninstall removed the OpenClaw profile hook and activation data."
@@ -2859,6 +3789,8 @@ function runUninstallCommand(parsed) {
2859
3789
  activationRoot,
2860
3790
  dataAction: parsed.dataMode,
2861
3791
  activationDataState: activationData.activationDataState,
3792
+ pluginConfigCleanup,
3793
+ learnerService,
2862
3794
  removedLegacyNotes: legacyResidue.removedNotes,
2863
3795
  updatedAgents: legacyResidue.updatedAgents,
2864
3796
  restartMode: parsed.restart,
@@ -2879,10 +3811,15 @@ function runUninstallCommand(parsed) {
2879
3811
  if (activationRoot !== null) {
2880
3812
  console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
2881
3813
  }
3814
+ console.log(`Config: ${pluginConfigCleanup.detail}`);
3815
+ console.log(`Learner: ${learnerService.detail}`);
2882
3816
  console.log(`Next: ${restartGuidance}`);
2883
3817
  if (parsed.dataMode === "keep" && activationRoot !== null) {
2884
3818
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
2885
3819
  }
3820
+ if (activationRoot !== null) {
3821
+ console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
3822
+ }
2886
3823
  if (parsed.dataMode === "keep") {
2887
3824
  console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
2888
3825
  }
@@ -3454,13 +4391,62 @@ function exportLocalSessionTailChangesToScanRoot(input) {
3454
4391
  warnings
3455
4392
  };
3456
4393
  }
3457
- function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId) {
3458
- const materialization = snapshot?.learner?.lastMaterialization ?? null;
4394
+ function summarizeVectorEmbeddingState(vectors) {
4395
+ if (vectors === null || vectors === undefined) {
4396
+ return {
4397
+ vectorEntryCount: null,
4398
+ numericEmbeddingEntryCount: null,
4399
+ embeddingModels: []
4400
+ };
4401
+ }
4402
+ const embeddingModels = [...new Set(vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
4403
+ return {
4404
+ vectorEntryCount: vectors.entries.length,
4405
+ numericEmbeddingEntryCount: vectors.entries.filter((entry) => entry.embedding !== undefined).length,
4406
+ embeddingModels
4407
+ };
4408
+ }
4409
+ function buildWatchEmbedTracePoint(input) {
4410
+ const summary = summarizeVectorEmbeddingState(input.vectors);
4411
+ return {
4412
+ slot: input.slot,
4413
+ packId: input.packId,
4414
+ runtimeEmbedderPresent: input.embedder !== null,
4415
+ runtimeEmbedderModel: input.embedder?.model ?? null,
4416
+ vectorEntryCount: summary.vectorEntryCount,
4417
+ numericEmbeddingEntryCount: summary.numericEmbeddingEntryCount,
4418
+ embeddingModels: summary.embeddingModels,
4419
+ error: input.error ?? null
4420
+ };
4421
+ }
4422
+ function buildWatchEmbedTracePointFromPack(input) {
4423
+ return buildWatchEmbedTracePoint({
4424
+ slot: input.slot,
4425
+ packId: input.pack?.manifest.packId ?? null,
4426
+ embedder: input.embedder,
4427
+ vectors: input.pack?.vectors,
4428
+ error: input.error ?? null
4429
+ });
4430
+ }
4431
+ function formatWatchEmbedTracePoint(label, point) {
4432
+ const models = point.embeddingModels.length === 0 ? "none" : point.embeddingModels.join("|");
4433
+ const slot = point.slot ?? "build";
4434
+ const packId = point.packId ?? "unknown";
4435
+ const embedderState = point.runtimeEmbedderPresent ? `present:${point.runtimeEmbedderModel ?? "unknown"}` : "null";
4436
+ const counts = point.vectorEntryCount === null || point.numericEmbeddingEntryCount === null
4437
+ ? "vectors=unknown numeric=unknown"
4438
+ : `vectors=${point.vectorEntryCount} numeric=${point.numericEmbeddingEntryCount}`;
4439
+ const error = point.error === null ? "" : ` error=${point.error}`;
4440
+ return `embed-trace ${label} slot=${slot} pack=${packId} runtimeEmbedder=${embedderState} ${counts} models=${models}${error}`;
4441
+ }
4442
+ async function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId, embedder, log) {
4443
+ let materialization = snapshot?.learner?.lastMaterialization ?? null;
3459
4444
  if (materialization === null) {
3460
4445
  return {
3461
4446
  lastHandledMaterializationPackId,
3462
4447
  logLine: null,
3463
4448
  materializedPackId: null,
4449
+ embedInstrumentation: null,
3464
4450
  failure: null
3465
4451
  };
3466
4452
  }
@@ -3472,10 +4458,38 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
3472
4458
  lastHandledMaterializationPackId,
3473
4459
  logLine: null,
3474
4460
  materializedPackId: packId,
4461
+ embedInstrumentation: null,
3475
4462
  failure: null
3476
4463
  };
3477
4464
  }
4465
+ if (embedder !== null) {
4466
+ materialization = {
4467
+ ...materialization,
4468
+ candidate: await reindexCandidatePackBuildResultWithEmbedder(materialization.candidate, embedder)
4469
+ };
4470
+ if (snapshot?.learner !== undefined && snapshot.learner !== null) {
4471
+ snapshot.learner.lastMaterialization = materialization;
4472
+ }
4473
+ }
3478
4474
  const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
4475
+ const observedAt = new Date().toISOString();
4476
+ const beforeCandidateMaterialization = buildWatchEmbedTracePoint({
4477
+ slot: null,
4478
+ packId,
4479
+ embedder,
4480
+ vectors: materialization?.candidate?.payloads?.vectors
4481
+ });
4482
+ let embedInstrumentation = {
4483
+ observedAt,
4484
+ candidatePackId: packId,
4485
+ promotionAllowed: null,
4486
+ promotionFindings: [],
4487
+ beforeCandidateMaterialization,
4488
+ afterCandidateMaterialization: null,
4489
+ afterStage: null,
4490
+ afterPromote: null
4491
+ };
4492
+ log?.(formatWatchEmbedTracePoint("before_materialize", beforeCandidateMaterialization));
3479
4493
  try {
3480
4494
  const candidateRootDir = path.resolve(activationRoot, "packs", packId);
3481
4495
  mkdirSync(candidateRootDir, { recursive: true });
@@ -3487,27 +4501,81 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
3487
4501
  activeBeforePack = null;
3488
4502
  }
3489
4503
  const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
4504
+ embedInstrumentation = {
4505
+ ...embedInstrumentation,
4506
+ afterCandidateMaterialization: buildWatchEmbedTracePointFromPack({
4507
+ slot: "candidate",
4508
+ pack: candidateDescriptor,
4509
+ embedder
4510
+ })
4511
+ };
4512
+ if (embedInstrumentation.afterCandidateMaterialization !== null) {
4513
+ log?.(formatWatchEmbedTracePoint("after_materialize", embedInstrumentation.afterCandidateMaterialization));
4514
+ }
3490
4515
  appendLearningUpdateLogs({
3491
4516
  activationRoot,
3492
4517
  materialization,
3493
4518
  activeBeforePack,
3494
4519
  candidateDescriptor
3495
4520
  });
3496
- const now = new Date().toISOString();
4521
+ const now = observedAt;
3497
4522
  stageCandidatePack(activationRoot, candidateRootDir, {
3498
4523
  updatedAt: now,
3499
4524
  reason: `watch_stage:${materialization.reason}:${materialization.lane}`
3500
4525
  });
3501
4526
  const inspection = inspectActivationState(activationRoot, now);
4527
+ let stagedPack = null;
4528
+ let stagedPackError = null;
4529
+ try {
4530
+ stagedPack = loadPackFromActivation(activationRoot, "candidate", { requireActivationReady: true });
4531
+ }
4532
+ catch (error) {
4533
+ stagedPackError = formatWatchError(error);
4534
+ }
4535
+ embedInstrumentation = {
4536
+ ...embedInstrumentation,
4537
+ promotionAllowed: inspection.promotion.allowed,
4538
+ promotionFindings: [...inspection.promotion.findings],
4539
+ afterStage: buildWatchEmbedTracePointFromPack({
4540
+ slot: "candidate",
4541
+ pack: stagedPack,
4542
+ embedder,
4543
+ error: stagedPackError
4544
+ })
4545
+ };
4546
+ if (embedInstrumentation.afterStage !== null) {
4547
+ log?.(formatWatchEmbedTracePoint("after_stage", embedInstrumentation.afterStage));
4548
+ }
3502
4549
  if (inspection.promotion.allowed) {
3503
4550
  promoteCandidatePack(activationRoot, {
3504
4551
  updatedAt: now,
3505
4552
  reason: `watch_promote:${materialization.reason}:${materialization.lane}`
3506
4553
  });
4554
+ let promotedPack = null;
4555
+ let promotedPackError = null;
4556
+ try {
4557
+ promotedPack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
4558
+ }
4559
+ catch (error) {
4560
+ promotedPackError = formatWatchError(error);
4561
+ }
4562
+ embedInstrumentation = {
4563
+ ...embedInstrumentation,
4564
+ afterPromote: buildWatchEmbedTracePointFromPack({
4565
+ slot: "active",
4566
+ pack: promotedPack,
4567
+ embedder,
4568
+ error: promotedPackError
4569
+ })
4570
+ };
4571
+ if (embedInstrumentation.afterPromote !== null) {
4572
+ log?.(formatWatchEmbedTracePoint("after_promote", embedInstrumentation.afterPromote));
4573
+ }
3507
4574
  return {
3508
4575
  lastHandledMaterializationPackId: packId,
3509
4576
  materializedPackId: packId,
3510
4577
  logLine: `Promoted ${shortPackId} → active`,
4578
+ embedInstrumentation,
3511
4579
  failure: null
3512
4580
  };
3513
4581
  }
@@ -3515,15 +4583,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
3515
4583
  lastHandledMaterializationPackId: packId,
3516
4584
  materializedPackId: packId,
3517
4585
  logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
4586
+ embedInstrumentation,
3518
4587
  failure: null
3519
4588
  };
3520
4589
  }
3521
4590
  catch (error) {
3522
4591
  const message = error instanceof Error ? error.message : String(error);
4592
+ embedInstrumentation = {
4593
+ ...embedInstrumentation,
4594
+ afterCandidateMaterialization: embedInstrumentation.afterCandidateMaterialization ??
4595
+ buildWatchEmbedTracePoint({
4596
+ slot: "candidate",
4597
+ packId,
4598
+ embedder,
4599
+ vectors: null,
4600
+ error: message
4601
+ })
4602
+ };
3523
4603
  return {
3524
4604
  lastHandledMaterializationPackId,
3525
4605
  materializedPackId: packId,
3526
4606
  logLine: `Promotion failed for ${shortPackId}: ${message}`,
4607
+ embedInstrumentation,
3527
4608
  failure: {
3528
4609
  mode: "materialization_failed",
3529
4610
  detail: message,
@@ -3569,12 +4650,178 @@ function resolveWatchTeacherLabelerConfig(input, activationRoot) {
3569
4650
  warnings
3570
4651
  };
3571
4652
  }
4653
+ function resolveWatchEmbedderConfig(input, activationRoot) {
4654
+ if (input !== undefined) {
4655
+ return {
4656
+ embedder: input,
4657
+ warnings: []
4658
+ };
4659
+ }
4660
+ const defaultsResult = readOpenClawBrainProviderDefaults(activationRoot);
4661
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
4662
+ env: process.env,
4663
+ activationRoot,
4664
+ defaults: defaultsResult.defaults
4665
+ });
4666
+ const warnings = [...new Set([
4667
+ ...defaultsResult.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning)),
4668
+ ...providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning))
4669
+ ])];
4670
+ const explicitEnv = typeof process.env[OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV] === "string" ||
4671
+ typeof process.env[OPENCLAWBRAIN_EMBEDDER_MODEL_ENV] === "string" ||
4672
+ typeof process.env[OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV] === "string";
4673
+ // Legacy install-written provider-defaults.json files can predate embedder fields entirely.
4674
+ // If a persisted defaults file exists, treat that activation root as explicitly configured and
4675
+ // let provider-config resolution fill in the embedder fallback instead of silently dropping to null.
4676
+ const explicitDefaults = defaultsResult.defaults !== null;
4677
+ if (!explicitEnv && !explicitDefaults) {
4678
+ return {
4679
+ embedder: null,
4680
+ warnings
4681
+ };
4682
+ }
4683
+ if (providerConfig.embedder.provider !== "ollama") {
4684
+ return {
4685
+ embedder: null,
4686
+ warnings
4687
+ };
4688
+ }
4689
+ return {
4690
+ embedder: createOllamaEmbedder({
4691
+ baseUrl: providerConfig.embedderBaseUrl,
4692
+ model: providerConfig.embedder.model
4693
+ }),
4694
+ warnings
4695
+ };
4696
+ }
4697
+ function summarizeWatchLatestUserMessage(localPoll) {
4698
+ let latest = null;
4699
+ for (const change of localPoll.changes) {
4700
+ if (change.lastUserMessageAt === null || change.lastUserMessageText === null) {
4701
+ continue;
4702
+ }
4703
+ const candidate = {
4704
+ at: change.lastUserMessageAt,
4705
+ text: change.lastUserMessageText,
4706
+ sessionId: change.sessionId
4707
+ };
4708
+ if (latest === null || Date.parse(candidate.at) >= Date.parse(latest.at)) {
4709
+ latest = candidate;
4710
+ }
4711
+ }
4712
+ return latest;
4713
+ }
4714
+ function summarizeWatchPackTransition(input) {
4715
+ const beforeActivePackId = input.before?.active?.packId ?? input.before?.pointers.active?.packId ?? null;
4716
+ const afterActivePackId = input.after?.active?.packId ?? input.after?.pointers.active?.packId ?? null;
4717
+ if (afterActivePackId !== null && beforeActivePackId !== afterActivePackId) {
4718
+ return {
4719
+ kind: "promoted_active",
4720
+ fromPackId: beforeActivePackId,
4721
+ toPackId: afterActivePackId
4722
+ };
4723
+ }
4724
+ const beforeCandidatePackId = input.before?.candidate?.packId ?? input.before?.pointers.candidate?.packId ?? null;
4725
+ const afterCandidatePackId = input.after?.candidate?.packId ?? input.after?.pointers.candidate?.packId ?? null;
4726
+ if (afterCandidatePackId !== null && beforeCandidatePackId !== afterCandidatePackId) {
4727
+ return {
4728
+ kind: "staged_candidate",
4729
+ fromPackId: beforeCandidatePackId,
4730
+ toPackId: afterCandidatePackId
4731
+ };
4732
+ }
4733
+ return null;
4734
+ }
4735
+ function truncateWatchMessage(text, maxLength = 96) {
4736
+ const normalized = text.replace(/\s+/gu, " ").trim();
4737
+ if (normalized.length <= maxLength) {
4738
+ return normalized;
4739
+ }
4740
+ return `${normalized.slice(0, maxLength - 1)}…`;
4741
+ }
4742
+ function buildWatchLastObservedDelta(input) {
4743
+ const exported = input.exported.exportedBundleCount > 0 ||
4744
+ input.exported.exportedEventCount > 0;
4745
+ const labeled = (input.snapshotAfter.diagnostics.emittedArtifactCount ?? 0) >
4746
+ (input.snapshotBefore.diagnostics.emittedArtifactCount ?? 0);
4747
+ const latestPackTransition = summarizeWatchPackTransition({
4748
+ before: input.beforeInspection,
4749
+ after: input.afterInspection
4750
+ });
4751
+ const promoted = latestPackTransition?.kind === "promoted_active";
4752
+ const afterActivePackId = input.afterInspection?.active?.packId ?? input.afterInspection?.pointers.active?.packId ?? null;
4753
+ const served = promoted && afterActivePackId === latestPackTransition?.toPackId && input.afterInspection?.active?.activationReady === true;
4754
+ const latestUserMessage = summarizeWatchLatestUserMessage(input.localPoll);
4755
+ const selectedBackfillOnly = !exported && input.scanResult.selected.length > 0;
4756
+ const cycleDidNothing = !exported && !labeled && !promoted && !served;
4757
+ let explanation;
4758
+ if (latestUserMessage === null) {
4759
+ if (selectedBackfillOnly) {
4760
+ explanation =
4761
+ "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.";
4762
+ }
4763
+ else if (cycleDidNothing) {
4764
+ explanation = "No new local user message or learner-visible export was observed in this cycle, so nothing changed.";
4765
+ }
4766
+ else if (promoted && latestPackTransition !== null) {
4767
+ explanation =
4768
+ `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.`;
4769
+ }
4770
+ else {
4771
+ explanation =
4772
+ "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.";
4773
+ }
4774
+ }
4775
+ else {
4776
+ const quotedMessage = `"${truncateWatchMessage(latestUserMessage.text)}"`;
4777
+ if (exported && labeled && promoted && served && latestPackTransition !== null) {
4778
+ explanation =
4779
+ `Latest user message ${quotedMessage} was exported, labeled, promoted into pack ${latestPackTransition.toPackId}, and is now served from the active pack.`;
4780
+ }
4781
+ else if (exported && labeled && !promoted) {
4782
+ explanation =
4783
+ `Latest user message ${quotedMessage} was exported and labeled, but it has not been promoted into the serving pack yet.`;
4784
+ }
4785
+ else if (exported && !labeled && !promoted) {
4786
+ explanation =
4787
+ `Latest user message ${quotedMessage} was exported, but it did not add a new teacher label or change the serving pack in this cycle.`;
4788
+ }
4789
+ else if (exported && !labeled && promoted && latestPackTransition !== null) {
4790
+ explanation =
4791
+ `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.`;
4792
+ }
4793
+ else if (!exported && labeled) {
4794
+ explanation =
4795
+ `Latest user message ${quotedMessage} was already in stored exports; this cycle only labeled or replayed it, without a new local export.`;
4796
+ }
4797
+ else if (cycleDidNothing) {
4798
+ explanation = `Latest user message ${quotedMessage} did not produce a new export, label, or serving-pack change in this cycle.`;
4799
+ }
4800
+ else {
4801
+ explanation =
4802
+ `Latest user message ${quotedMessage} changed learner state this cycle, but the local artifacts do not prove a clean export-to-serve handoff yet.`;
4803
+ }
4804
+ }
4805
+ return {
4806
+ available: true,
4807
+ observedAt: input.observedAt,
4808
+ exported,
4809
+ labeled,
4810
+ promoted,
4811
+ served,
4812
+ latestPackTransition,
4813
+ explanation
4814
+ };
4815
+ }
3572
4816
  export async function createWatchCommandRuntime(input) {
3573
4817
  const activationRoot = path.resolve(input.activationRoot);
3574
4818
  const bootstrapObservedAt = new Date().toISOString();
3575
4819
  const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
3576
4820
  ? path.resolve(input.scanRoot)
3577
4821
  : path.resolve(activationRoot, "event-exports");
4822
+ const pollIntervalSeconds = Number.isInteger(input.pollIntervalSeconds) && (input.pollIntervalSeconds ?? 0) > 0
4823
+ ? input.pollIntervalSeconds
4824
+ : DEFAULT_WATCH_POLL_INTERVAL_SECONDS;
3578
4825
  const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
3579
4826
  const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
3580
4827
  const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
@@ -3586,14 +4833,25 @@ export async function createWatchCommandRuntime(input) {
3586
4833
  log(`Scan root: ${shortenPath(scanRoot)}`);
3587
4834
  log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
3588
4835
  const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
4836
+ const resolvedEmbedder = resolveWatchEmbedderConfig(input.embedder, activationRoot);
3589
4837
  const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
3590
4838
  for (const warning of resolvedTeacherLabeler.warnings) {
3591
4839
  startupWarnings.push(`teacher_config_warning:${warning}`);
3592
4840
  log(`Teacher config warning: ${warning}`);
3593
4841
  }
4842
+ for (const warning of resolvedEmbedder.warnings) {
4843
+ startupWarnings.push(`embedder_config_warning:${warning}`);
4844
+ log(`Embedder config warning: ${warning}`);
4845
+ }
3594
4846
  if (teacherLabeler?.provider === "ollama") {
3595
4847
  log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
3596
4848
  }
4849
+ if (resolvedEmbedder.embedder !== null) {
4850
+ log(`Embedder: provider=ollama model=${resolvedEmbedder.embedder.model}`);
4851
+ }
4852
+ else {
4853
+ log("Embedder: numeric pack materialization is not configured; watch will keep keyword/weight vectors only.");
4854
+ }
3597
4855
  const scanner = createRuntimeEventExportScanner({ scanRoot });
3598
4856
  let lastServeTimeFallbackReason = null;
3599
4857
  const baseTeacherLoopInput = {
@@ -3630,10 +4888,23 @@ export async function createWatchCommandRuntime(input) {
3630
4888
  };
3631
4889
  let teacherLoop;
3632
4890
  let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
4891
+ let lastEmbedInstrumentation = restoredTeacherState.embedInstrumentation;
4892
+ let restoredLastObservedDelta = restoredTeacherState.lastObservedDelta;
3633
4893
  if (restoredTeacherState.error !== null) {
3634
4894
  const message = restoredTeacherState.error;
3635
4895
  startupWarnings.push(`teacher_snapshot_reset:${message}`);
3636
4896
  lastHandledMaterializationPackId = null;
4897
+ lastEmbedInstrumentation = null;
4898
+ restoredLastObservedDelta = {
4899
+ available: true,
4900
+ observedAt: bootstrapObservedAt,
4901
+ exported: false,
4902
+ labeled: false,
4903
+ promoted: false,
4904
+ served: false,
4905
+ latestPackTransition: null,
4906
+ explanation: "Watch reset an unreadable teacher snapshot, so no prior last-turn delta can be trusted."
4907
+ };
3637
4908
  log(`Teacher snapshot reset: ${message}`);
3638
4909
  teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
3639
4910
  }
@@ -3648,6 +4919,17 @@ export async function createWatchCommandRuntime(input) {
3648
4919
  const message = formatWatchError(error);
3649
4920
  startupWarnings.push(`teacher_snapshot_reset:${message}`);
3650
4921
  lastHandledMaterializationPackId = null;
4922
+ lastEmbedInstrumentation = null;
4923
+ restoredLastObservedDelta = {
4924
+ available: true,
4925
+ observedAt: bootstrapObservedAt,
4926
+ exported: false,
4927
+ labeled: false,
4928
+ promoted: false,
4929
+ served: false,
4930
+ latestPackTransition: null,
4931
+ explanation: "Watch reset an unusable teacher snapshot, so no prior last-turn delta can be trusted."
4932
+ };
3651
4933
  log(`Teacher snapshot reset: ${message}`);
3652
4934
  teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
3653
4935
  }
@@ -3656,11 +4938,28 @@ export async function createWatchCommandRuntime(input) {
3656
4938
  const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
3657
4939
  log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
3658
4940
  }
4941
+ const resolvedWatchProfileScope = input.profileRoots === undefined
4942
+ ? resolveWatchProfileRootsForActivationRoot(activationRoot)
4943
+ : {
4944
+ attachedProfileRoots: [...new Set(input.profileRoots.map((root) => path.resolve(root)))],
4945
+ halfAttachedReferences: []
4946
+ };
4947
+ const resolvedProfileRoots = resolvedWatchProfileScope.attachedProfileRoots;
4948
+ if (input.profileRoots === undefined && resolvedProfileRoots !== undefined && resolvedProfileRoots.length > 0) {
4949
+ log(`Session tail scope: attached OpenClaw home${resolvedProfileRoots.length === 1 ? "" : "s"} ${resolvedProfileRoots
4950
+ .map((root) => shortenPath(root))
4951
+ .join(", ")}`);
4952
+ }
4953
+ if (input.profileRoots === undefined && resolvedWatchProfileScope.halfAttachedReferences.length > 0) {
4954
+ log(`Session tail scope skipped half-attached OpenClaw home${resolvedWatchProfileScope.halfAttachedReferences.length === 1 ? "" : "s"} ${resolvedWatchProfileScope.halfAttachedReferences
4955
+ .map((reference) => `${shortenPath(path.resolve(reference.openclawHome))} (${summarizeSharedActivationRootReferenceProof(reference)})`)
4956
+ .join(", ")}`);
4957
+ }
3659
4958
  let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
3660
4959
  let localSessionTail;
3661
4960
  try {
3662
4961
  localSessionTail = createOpenClawLocalSessionTail({
3663
- ...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
4962
+ ...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
3664
4963
  cursor: restoredCursor,
3665
4964
  emitExistingOnFirstPoll: restoredCursor.length === 0
3666
4965
  });
@@ -3670,7 +4969,7 @@ export async function createWatchCommandRuntime(input) {
3670
4969
  log(`Session tail cursor reset: ${message}`);
3671
4970
  restoredCursor = [];
3672
4971
  localSessionTail = createOpenClawLocalSessionTail({
3673
- ...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
4972
+ ...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
3674
4973
  emitExistingOnFirstPoll: true
3675
4974
  });
3676
4975
  persistWatchSessionTailCursor(sessionTailCursorPath, []);
@@ -3691,8 +4990,11 @@ export async function createWatchCommandRuntime(input) {
3691
4990
  log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
3692
4991
  }
3693
4992
  let bootstrapSnapshot = teacherLoop.snapshot();
3694
- const replayPromotion = applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId);
4993
+ const replayPromotion = await applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId, resolvedEmbedder.embedder, log);
3695
4994
  lastHandledMaterializationPackId = replayPromotion.lastHandledMaterializationPackId;
4995
+ if (replayPromotion.embedInstrumentation !== null) {
4996
+ lastEmbedInstrumentation = replayPromotion.embedInstrumentation;
4997
+ }
3696
4998
  if (replayPromotion.logLine !== null) {
3697
4999
  log(replayPromotion.logLine);
3698
5000
  bootstrapSnapshot = teacherLoop.snapshot();
@@ -3700,6 +5002,7 @@ export async function createWatchCommandRuntime(input) {
3700
5002
  const bootstrapCursor = localSessionTail.snapshot();
3701
5003
  persistWatchTeacherSnapshot(teacherSnapshotPath, {
3702
5004
  lastRunAt: bootstrapObservedAt,
5005
+ pollIntervalSeconds,
3703
5006
  scanRoot,
3704
5007
  sessionTailCursorPath,
3705
5008
  sessionTailCursorUpdatedAt: bootstrapObservedAt,
@@ -3715,26 +5018,44 @@ export async function createWatchCommandRuntime(input) {
3715
5018
  lastTeacherError: null,
3716
5019
  localSessionTailNoopReason: null,
3717
5020
  lastHandledMaterializationPackId,
5021
+ lastObservedDelta: restoredLastObservedDelta.available
5022
+ ? restoredLastObservedDelta
5023
+ : {
5024
+ available: true,
5025
+ observedAt: bootstrapObservedAt,
5026
+ exported: false,
5027
+ labeled: false,
5028
+ promoted: false,
5029
+ served: false,
5030
+ latestPackTransition: null,
5031
+ explanation: "Watch bootstrapped its state, but no new local user-message delta has been observed yet."
5032
+ },
5033
+ embedInstrumentation: lastEmbedInstrumentation,
3718
5034
  failure: replayPromotion.failure,
3719
5035
  snapshot: bootstrapSnapshot
3720
5036
  });
3721
5037
  return {
3722
5038
  activationRoot,
3723
5039
  scanRoot,
5040
+ pollIntervalSeconds,
3724
5041
  sessionTailCursorPath,
3725
5042
  teacherSnapshotPath,
3726
5043
  startupWarnings,
3727
5044
  lastTeacherError: null,
3728
5045
  replayState,
3729
5046
  lastHandledMaterializationPackId,
5047
+ lastEmbedInstrumentation,
3730
5048
  scanner,
3731
5049
  teacherLoop,
3732
- localSessionTail
5050
+ localSessionTail,
5051
+ embedder: resolvedEmbedder.embedder
3733
5052
  };
3734
5053
  }
3735
5054
  export async function runWatchCommandPass(runtime, options = {}) {
3736
5055
  const log = options.log ?? watchLog;
3737
5056
  const observedAt = options.observedAt ?? new Date().toISOString();
5057
+ const snapshotBefore = runtime.teacherLoop.snapshot();
5058
+ const beforeInspection = inspectActivationState(runtime.activationRoot, observedAt);
3738
5059
  const localPoll = runtime.localSessionTail.pollOnce({
3739
5060
  observedAt
3740
5061
  });
@@ -3768,9 +5089,12 @@ export async function runWatchCommandPass(runtime, options = {}) {
3768
5089
  const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
3769
5090
  runtime.lastTeacherError = null;
3770
5091
  snapshot = ingestResult.snapshot;
3771
- const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
5092
+ const promotion = await applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId, runtime.embedder, log);
3772
5093
  runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
3773
5094
  materializedPackId = promotion.materializedPackId;
5095
+ if (promotion.embedInstrumentation !== null) {
5096
+ runtime.lastEmbedInstrumentation = promotion.embedInstrumentation;
5097
+ }
3774
5098
  failure = promotion.failure;
3775
5099
  if (promotion.logLine !== null) {
3776
5100
  log(promotion.logLine);
@@ -3802,8 +5126,20 @@ export async function runWatchCommandPass(runtime, options = {}) {
3802
5126
  snapshot = runtime.teacherLoop.snapshot();
3803
5127
  }
3804
5128
  }
5129
+ const afterInspection = inspectActivationState(runtime.activationRoot, observedAt);
5130
+ const lastObservedDelta = buildWatchLastObservedDelta({
5131
+ observedAt,
5132
+ localPoll,
5133
+ exported,
5134
+ scanResult,
5135
+ snapshotBefore,
5136
+ snapshotAfter: snapshot,
5137
+ beforeInspection,
5138
+ afterInspection
5139
+ });
3805
5140
  persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
3806
5141
  lastRunAt: observedAt,
5142
+ pollIntervalSeconds: runtime.pollIntervalSeconds,
3807
5143
  scanRoot: runtime.scanRoot,
3808
5144
  sessionTailCursorPath: runtime.sessionTailCursorPath,
3809
5145
  sessionTailCursorUpdatedAt: observedAt,
@@ -3819,6 +5155,8 @@ export async function runWatchCommandPass(runtime, options = {}) {
3819
5155
  lastTeacherError: runtime.lastTeacherError,
3820
5156
  localSessionTailNoopReason: localPoll.noopReason,
3821
5157
  lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
5158
+ lastObservedDelta,
5159
+ embedInstrumentation: runtime.lastEmbedInstrumentation,
3822
5160
  failure,
3823
5161
  snapshot
3824
5162
  });
@@ -3839,6 +5177,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
3839
5177
  scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
3840
5178
  scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
3841
5179
  materialized: materializedPackId,
5180
+ lastObservedDelta,
3842
5181
  diagnostics: snapshot.diagnostics ?? null,
3843
5182
  localSessionTailNoopReason: localPoll.noopReason
3844
5183
  }));
@@ -3856,6 +5195,7 @@ async function runWatchCommand(parsed) {
3856
5195
  const runtime = await createWatchCommandRuntime({
3857
5196
  activationRoot: parsed.activationRoot,
3858
5197
  scanRoot: parsed.scanRoot,
5198
+ pollIntervalSeconds: parsed.interval,
3859
5199
  log: watchLog
3860
5200
  });
3861
5201
  watchLog(`Interval: ${parsed.interval}s`);
@@ -4139,6 +5479,10 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
4139
5479
  const operatorInput = {
4140
5480
  ...statusOrRollback.input,
4141
5481
  activationRoot,
5482
+ openclawHome: statusOrRollback.openclawHome,
5483
+ ...(targetInspection?.profileId === null || targetInspection?.profileId === undefined
5484
+ ? {}
5485
+ : { profileId: targetInspection.profileId }),
4142
5486
  teacherSnapshotPath: resolveOperatorTeacherSnapshotPath(activationRoot, statusOrRollback.input.teacherSnapshotPath)
4143
5487
  };
4144
5488
  const status = describeCurrentProfileBrainStatus(operatorInput);
@@ -4147,7 +5491,10 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
4147
5491
  }
4148
5492
  else {
4149
5493
  const report = buildOperatorSurfaceReport(operatorInput);
4150
- const providerConfig = readOpenClawBrainProviderConfig(process.env);
5494
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
5495
+ env: process.env,
5496
+ activationRoot
5497
+ });
4151
5498
  if (statusOrRollback.detailed) {
4152
5499
  console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
4153
5500
  openclawHome: statusOrRollback.openclawHome,