@openclawbrain/openclaw 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +3 -1
  2. package/dist/extension/index.js +9 -1
  3. package/dist/extension/index.js.map +1 -1
  4. package/dist/extension/runtime-guard.js +6 -1
  5. package/dist/extension/runtime-guard.js.map +1 -1
  6. package/dist/src/cli.d.ts +18 -6
  7. package/dist/src/cli.js +1991 -293
  8. package/dist/src/cli.js.map +1 -1
  9. package/dist/src/daemon.d.ts +42 -1
  10. package/dist/src/daemon.js +360 -50
  11. package/dist/src/daemon.js.map +1 -1
  12. package/dist/src/index.d.ts +65 -1
  13. package/dist/src/index.js +627 -56
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/learning-spine.d.ts +3 -1
  16. package/dist/src/learning-spine.js +1 -0
  17. package/dist/src/learning-spine.js.map +1 -1
  18. package/dist/src/local-session-passive-learning.js +6 -1
  19. package/dist/src/local-session-passive-learning.js.map +1 -1
  20. package/dist/src/openclaw-home-layout.d.ts +17 -0
  21. package/dist/src/openclaw-home-layout.js +182 -0
  22. package/dist/src/openclaw-home-layout.js.map +1 -0
  23. package/dist/src/provider-config.d.ts +36 -0
  24. package/dist/src/provider-config.js +181 -25
  25. package/dist/src/provider-config.js.map +1 -1
  26. package/dist/src/resolve-activation-root.d.ts +3 -3
  27. package/dist/src/resolve-activation-root.js +21 -26
  28. package/dist/src/resolve-activation-root.js.map +1 -1
  29. package/dist/src/semantic-metadata.d.ts +4 -0
  30. package/dist/src/semantic-metadata.js +41 -0
  31. package/dist/src/semantic-metadata.js.map +1 -0
  32. package/dist/src/session-store.js +16 -5
  33. package/dist/src/session-store.js.map +1 -1
  34. package/dist/src/session-tail.d.ts +2 -0
  35. package/dist/src/session-tail.js +68 -16
  36. package/dist/src/session-tail.js.map +1 -1
  37. package/dist/src/shadow-extension-proof.js +4 -0
  38. package/dist/src/shadow-extension-proof.js.map +1 -1
  39. package/extension/index.ts +17 -0
  40. package/extension/runtime-guard.ts +7 -1
  41. package/package.json +7 -7
package/dist/src/cli.js CHANGED
@@ -1,21 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFileSync, execSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync, symlinkSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, symlinkSync } from "node:fs";
4
4
  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 { 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";
9
10
  import { exportBrain, importBrain } from "./import-export.js";
10
11
  import { buildNormalizedEventExport } from "@openclawbrain/contracts";
11
- import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
12
+ import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, reindexCandidatePackBuildResultWithEmbedder, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
12
13
  import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
13
14
  import { resolveActivationRoot } from "./resolve-activation-root.js";
14
- import { buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
15
+ import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
16
+ import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, loadWatchTeacherSnapshotState, loadRuntimeEventExportBundle, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
15
17
  import { appendLearningUpdateLogs } from "./learning-spine.js";
16
18
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
17
19
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
18
- import { readOpenClawBrainProviderConfig } from "./provider-config.js";
20
+ import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
21
+ const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
22
+ const OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV = "OPENCLAWBRAIN_EMBEDDER_PROVIDER";
23
+ const OPENCLAWBRAIN_EMBEDDER_MODEL_ENV = "OPENCLAWBRAIN_EMBEDDER_MODEL";
24
+ const OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV = "OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION";
25
+ const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
26
+ "qwen3.5:9b",
27
+ "qwen3.5:8b",
28
+ "qwen3:8b",
29
+ "qwen2.5:7b"
30
+ ];
19
31
  function quoteShellArg(value) {
20
32
  return `'${value.replace(/'/g, `"'"'`)}'`;
21
33
  }
@@ -26,23 +38,68 @@ function normalizeOptionalCliString(value) {
26
38
  const trimmed = value.trim();
27
39
  return trimmed.length > 0 ? trimmed : null;
28
40
  }
41
+ function readTruthyEnvFlag(name, env = process.env) {
42
+ const value = normalizeOptionalCliString(env[name]);
43
+ if (value === null) {
44
+ return false;
45
+ }
46
+ return ["1", "true", "yes", "on"].includes(value.toLowerCase());
47
+ }
29
48
  function getCliHomeDir() {
30
49
  return process.env.HOME ?? process.env.USERPROFILE ?? "~";
31
50
  }
32
51
  function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
33
- const resolvedHomeDir = path.resolve(homeDir);
34
- let entries;
35
- try {
36
- entries = readdirSync(resolvedHomeDir, { withFileTypes: true });
37
- }
38
- catch {
39
- return [];
40
- }
41
- return entries
42
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(".openclaw-"))
43
- .map((entry) => path.join(resolvedHomeDir, entry.name))
44
- .filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
45
- .sort((left, right) => left.localeCompare(right));
52
+ return discoverOpenClawHomes(homeDir).map((inspection) => inspection.openclawHome);
53
+ }
54
+ function findInstalledHookReferencesForActivationRoot(input) {
55
+ const resolvedActivationRoot = path.resolve(input.activationRoot);
56
+ const resolvedExcludedHome = input.excludingOpenClawHome === undefined || input.excludingOpenClawHome === null
57
+ ? null
58
+ : path.resolve(input.excludingOpenClawHome);
59
+ return discoverOpenClawHomes(input.homeDir ?? getCliHomeDir())
60
+ .filter((inspection) => resolvedExcludedHome === null || path.resolve(inspection.openclawHome) !== resolvedExcludedHome)
61
+ .flatMap((inspection) => {
62
+ const installedActivationRoot = resolveActivationRoot({
63
+ openclawHome: inspection.openclawHome,
64
+ quiet: true
65
+ });
66
+ if (installedActivationRoot.trim().length === 0) {
67
+ return [];
68
+ }
69
+ return path.resolve(installedActivationRoot) === resolvedActivationRoot
70
+ ? [{ openclawHome: inspection.openclawHome, inspection }]
71
+ : [];
72
+ })
73
+ .sort((left, right) => left.openclawHome.localeCompare(right.openclawHome));
74
+ }
75
+ function findOtherInstalledHookReferencesForActivationRoot(input) {
76
+ return findInstalledHookReferencesForActivationRoot(input);
77
+ }
78
+ function resolveWatchProfileRootsForActivationRoot(activationRoot, homeDir = getCliHomeDir()) {
79
+ const attachedProfileRoots = findInstalledHookReferencesForActivationRoot({
80
+ activationRoot,
81
+ homeDir
82
+ }).map((reference) => path.resolve(reference.openclawHome));
83
+ return attachedProfileRoots.length > 0 ? attachedProfileRoots : undefined;
84
+ }
85
+ function assertActivationRootPurgeIsNotShared(input) {
86
+ const sharedReferences = findOtherInstalledHookReferencesForActivationRoot({
87
+ activationRoot: input.activationRoot,
88
+ excludingOpenClawHome: input.openclawHome
89
+ });
90
+ if (sharedReferences.length === 0) {
91
+ return;
92
+ }
93
+ const attachedProfiles = sharedReferences
94
+ .map(({ openclawHome, inspection }) => ` - ${path.resolve(openclawHome)} (${describeOpenClawHomeInspection(inspection)})`)
95
+ .join("\n");
96
+ throw new Error([
97
+ `Refusing to purge activation root ${path.resolve(input.activationRoot)} because another installed OpenClaw profile still points at it.`,
98
+ "Other attached profiles:",
99
+ attachedProfiles,
100
+ "Use uninstall --keep-data or detach on this profile first, then remove the remaining profile hooks before purging shared brain data.",
101
+ "For Eagle dogfood, prefer its own activation root so CormorantAI stays untouched."
102
+ ].join("\n"));
46
103
  }
47
104
  function formatInstallOpenClawHomeSource(source) {
48
105
  switch (source) {
@@ -50,8 +107,8 @@ function formatInstallOpenClawHomeSource(source) {
50
107
  return "--openclaw-home";
51
108
  case "env":
52
109
  return "OPENCLAW_HOME";
53
- case "discovered_single_profile":
54
- return "single discovered live profile";
110
+ case "discovered_single_home":
111
+ return "single discovered install target";
55
112
  default:
56
113
  return source;
57
114
  }
@@ -75,24 +132,24 @@ function resolveInstallOpenClawHome(explicitOpenclawHome) {
75
132
  if (discoveredHomes.length === 1) {
76
133
  return {
77
134
  openclawHome: path.resolve(discoveredHomes[0]),
78
- openclawHomeSource: "discovered_single_profile"
135
+ openclawHomeSource: "discovered_single_home"
79
136
  };
80
137
  }
81
138
  if (discoveredHomes.length > 1) {
82
139
  const installPrefix = detectConsumerSafeOperatorCliPrefix();
83
- const targetChoices = discoveredHomes
84
- .map((candidate) => {
85
- const resolvedCandidate = path.resolve(candidate);
86
- return ` - ${resolvedCandidate}\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
140
+ const targetChoices = discoverOpenClawHomes()
141
+ .map((inspection) => {
142
+ const resolvedCandidate = path.resolve(inspection.openclawHome);
143
+ return ` - ${resolvedCandidate} (${describeOpenClawHomeInspection(inspection)})\n ${installPrefix} install --openclaw-home ${quoteShellArg(resolvedCandidate)}`;
87
144
  })
88
145
  .join("\n");
89
146
  throw new Error([
90
- "Refusing ambiguous live OpenClaw targets for install.",
147
+ "Refusing ambiguous OpenClaw install targets.",
91
148
  targetChoices,
92
- "Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one profile."
149
+ "Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one OpenClaw home."
93
150
  ].join("\n"));
94
151
  }
95
- throw new Error("No OpenClaw profile home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
152
+ throw new Error("No OpenClaw home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
96
153
  }
97
154
  function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
98
155
  const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
@@ -115,18 +172,24 @@ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
115
172
  source: "explicit"
116
173
  };
117
174
  }
118
- try {
119
- const openclawConfigPath = path.join(openclawHome, "openclaw.json");
120
- const openclawConfig = JSON.parse(readFileSync(openclawConfigPath, "utf8"));
121
- if (typeof openclawConfig.profile === "string" && openclawConfig.profile.trim().length > 0) {
122
- return {
123
- workspaceId: openclawConfig.profile.trim(),
124
- source: "openclaw_json_profile"
125
- };
126
- }
175
+ const inspection = inspectOpenClawHome(openclawHome);
176
+ if (inspection.profileId !== null) {
177
+ return {
178
+ workspaceId: inspection.profileId,
179
+ source: inspection.profileSource === "directory_name"
180
+ ? "openclaw_home_dir"
181
+ : inspection.profileSource === "openclaw_json_profile"
182
+ ? "openclaw_json_profile"
183
+ : inspection.profileSource === "openclaw_json_single_profile_key"
184
+ ? "openclaw_json_single_profile_key"
185
+ : "fallback"
186
+ };
127
187
  }
128
- catch {
129
- // Fall back to the profile-home name when install is pointed at an incomplete or not-yet-readable profile.
188
+ if (inspection.layout === "shared_home_profiles_in_config" || inspection.layout === "single_openclaw_home") {
189
+ return {
190
+ workspaceId: "current_profile",
191
+ source: "current_profile_boundary"
192
+ };
130
193
  }
131
194
  const dirName = path.basename(openclawHome);
132
195
  if (dirName === ".openclaw") {
@@ -147,6 +210,24 @@ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
147
210
  source: "fallback"
148
211
  };
149
212
  }
213
+ function resolveInstallEmbedderProvisionSkip(explicitSkip) {
214
+ if (explicitSkip) {
215
+ return {
216
+ skipEmbedderProvision: true,
217
+ skipEmbedderProvisionSource: "flag"
218
+ };
219
+ }
220
+ if (readTruthyEnvFlag(OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV)) {
221
+ return {
222
+ skipEmbedderProvision: true,
223
+ skipEmbedderProvisionSource: "env"
224
+ };
225
+ }
226
+ return {
227
+ skipEmbedderProvision: false,
228
+ skipEmbedderProvisionSource: null
229
+ };
230
+ }
150
231
  function formatInstallActivationRootSource(source) {
151
232
  if (source === "explicit") {
152
233
  return "explicit --activation-root";
@@ -159,6 +240,10 @@ function formatInstallWorkspaceIdSource(source) {
159
240
  return "explicit --workspace-id";
160
241
  case "openclaw_json_profile":
161
242
  return "from openclaw.json profile";
243
+ case "openclaw_json_single_profile_key":
244
+ return "from the only openclaw.json profiles entry";
245
+ case "current_profile_boundary":
246
+ return "current_profile boundary for a shared OpenClaw home";
162
247
  case "openclaw_home_dir":
163
248
  return "from OpenClaw home dir";
164
249
  default:
@@ -289,29 +374,29 @@ function operatorCliHelp() {
289
374
  return [
290
375
  "Usage:",
291
376
  " openclawbrain install [--openclaw-home <path>] [options]",
377
+ " openclawbrain attach --openclaw-home <path> [options]",
378
+ " openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
379
+ " openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
380
+ " openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
292
381
  " openclawbrain detach --openclaw-home <path> [options]",
293
382
  " openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
294
- " openclawbrain attach --activation-root <path> [options]",
295
- " openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
296
383
  " openclawbrain context \"message\" [--activation-root <path>|--openclaw-home <path>]",
297
384
  " openclawbrain history [--activation-root <path>|--openclaw-home <path>] [--limit N] [--json]",
298
385
  " openclawbrain scan --session <trace.json> --root <path> [options]",
299
386
  " openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
300
387
  " openclawbrain learn [--activation-root <path>|--openclaw-home <path>] [--json]",
301
- " openclawbrain watch --activation-root <path> [--scan-root <path>] [--interval <seconds>]",
302
- " openclawbrain daemon <start|stop|status|logs> --activation-root <path> [--json]",
303
388
  " openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
304
389
  " openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
305
390
  "",
306
391
  "Options:",
307
- " --openclaw-home <path> OpenClaw profile home dir for install/detach/uninstall (e.g. ~/.openclaw-Tern). Also pins status/rollback/context/history/learn to that installed profile when applicable.",
308
- " --shared Set brain-attachment-policy to shared instead of dedicated (install only).",
309
- " --activation-root <path> Explicit activation root for attach/watch/daemon and other stateful commands; install defaults to sibling .openclawbrain/activation next to the selected OpenClaw home.",
392
+ " --openclaw-home <path> OpenClaw home dir for install/attach/detach/uninstall (e.g. ~/.openclaw-Tern or ~/.openclaw). Also pins status/rollback/context/history/learn to that installed target when applicable.",
393
+ " --shared Set brain-attachment-policy to shared instead of dedicated (install/attach only).",
394
+ ` --skip-embedder-provision Skip the default Ollama ${DEFAULT_OLLAMA_EMBEDDING_MODEL} pull before install/attach bootstrap. Use only when intentionally deferring embedder setup. Also supports ${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1.`,
395
+ " --activation-root <path> Explicit activation root for attach/watch/daemon and other stateful commands; install/attach default to sibling .openclawbrain/activation next to the selected OpenClaw home.",
310
396
  " --keep-data Preserve activation data on uninstall; detach always behaves this way.",
311
397
  " --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
312
398
  " --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
313
- " --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
314
- " --workspace-id <id> Workspace identifier for attach/install provenance; install defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
399
+ " --workspace-id <id> Workspace identifier for install/attach provenance; defaults to the detected profile target from openclaw.json when possible, otherwise the profile name or current_profile boundary.",
315
400
  " --event-export <path> Event-export bundle root or normalized export JSON payload.",
316
401
  " --teacher-snapshot <path> Canonical watch teacher snapshot JSON or raw async teacher snapshot JSON; keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
317
402
  " --updated-at <iso> Observation time to use for freshness checks.",
@@ -321,7 +406,7 @@ function operatorCliHelp() {
321
406
  " --session <path> Sanitized recorded-session trace JSON to replay.",
322
407
  " --live <path> Runtime event-export bundle root or normalized export JSON to scan once.",
323
408
  " --root <path> Output root for scan --session replay artifacts.",
324
- " --workspace <path> Workspace metadata JSON for scan --live candidate provenance.",
409
+ " --workspace <path> Workspace metadata JSON for scan --live candidate materialization.",
325
410
  " --pack-label <label> Candidate-pack label for scan --live. Defaults to scanner-live-cli.",
326
411
  " --observed-at <iso> Observation time for scan --live freshness checks.",
327
412
  " --snapshot-out <path> Write the one-shot scan --live snapshot JSON.",
@@ -331,25 +416,31 @@ function operatorCliHelp() {
331
416
  " --json Emit machine-readable JSON instead of text.",
332
417
  " --help Show this help.",
333
418
  "",
334
- "Common flow:",
335
- " 0. install openclawbrain install — attach the brain with sane defaults; pass --openclaw-home for explicit targeting on many-profile hosts",
336
- " 0. detach openclawbrain detach --openclaw-home <path> — remove the profile hook only; activation data stays in place",
337
- " 0. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data remove the profile hook and choose the data outcome explicitly",
338
- " 0. context openclawbrain context \"hello\" preview the brain context that would be injected for a message",
339
- " 0. attach openclawbrain attach --activation-root <path>",
340
- " 1. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
341
- " 2. status --json openclawbrain status --activation-root <path> --json read the canonical current_profile_brain_status.v1 object",
342
- " 3. rollback --dry-run openclawbrain rollback --activation-root <path> --dry-run preview active <- previous, active -> candidate",
343
- " 4. rollback openclawbrain rollback --activation-root <path> — apply the rollback when the preview says ready",
344
- " 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
345
- " 6. scan --live scan one live event export into teacher/learner state without claiming a daemon is running",
419
+ "Lifecycle flow:",
420
+ " 1. install openclawbrain install — safe first-time default; pass --openclaw-home when more than one OpenClaw home/layout is present",
421
+ " 2. attach openclawbrain attach --openclaw-home <path> [--activation-root <path>] explicit reattach/manual hook path for known brain data; use install first",
422
+ " 3. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
423
+ " 4. status --detailed openclawbrain status --activation-root <path> --detailed explain serve path, freshness, backlog, and failure mode",
424
+ " 5. watch openclawbrain watch --activation-root <path> — run the foreground learning/watch loop",
425
+ " 6. daemon start openclawbrain daemon start --activation-root <path> — keep watch running in the background on macOS",
426
+ " 7. daemon status openclawbrain daemon status --activation-root <path> — inspect the background watch state",
427
+ " 8. detach openclawbrain detach --openclaw-home <path> — remove the profile hookup only and keep brain data",
428
+ " 9. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data remove the hookup and choose the data outcome explicitly",
429
+ "",
430
+ "Advanced/operator surfaces:",
431
+ " context preview the brain context that would be injected for a message",
432
+ " rollback preview or apply active <- previous, active -> candidate pointer movement",
433
+ " scan inspect one recorded session or live event export without claiming a daemon is running",
434
+ " learn one-shot local-session learning pass against the resolved activation root",
346
435
  " status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
347
436
  " watch/daemon persist their operator snapshot at <activation-root>/watch/teacher-snapshot.json; --teacher-snapshot overrides the default path",
348
- " watch Ollama teacher labels: set OPENCLAWBRAIN_TEACHER_PROVIDER=ollama plus optional OPENCLAWBRAIN_TEACHER_BASE_URL, OPENCLAWBRAIN_TEACHER_MODEL, and OPENCLAWBRAIN_TEACHER_* budget vars",
437
+ " watch teacher defaults come from install-written provider-defaults.json under the activation root; OPENCLAWBRAIN_TEACHER_* and OPENCLAWBRAIN_EMBEDDER_* are host-shell overrides only, not live gateway wiring",
349
438
  "",
350
439
  "Exit codes:",
440
+ " install: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
351
441
  " status: 0 on successful inspection, 1 on input/read failure.",
352
442
  " rollback: 0 when ready/applied, 1 when blocked or on input/read failure.",
443
+ " attach: 0 on successful profile hookup/bootstrap, 1 on input/read failure.",
353
444
  " detach: 0 on successful unhook, 1 on input/read failure.",
354
445
  " uninstall: 0 on successful unhook/cleanup, 1 on input/read failure.",
355
446
  " scan: 0 on successful replay/scan, 1 on input/read failure."
@@ -380,6 +471,24 @@ function formatStructuralOps(report) {
380
471
  ? "none"
381
472
  : `split:${structuralOps.split},merge:${structuralOps.merge},prune:${structuralOps.prune},connect:${structuralOps.connect}`;
382
473
  }
474
+ function formatGraphConnectDiagnostics(diagnostics) {
475
+ if (diagnostics === null) {
476
+ return "none";
477
+ }
478
+ return `budget:${diagnostics.requestedBudget},threshold:${diagnostics.scoreThreshold},pairs:${diagnostics.appliedPairCount}/${diagnostics.candidatePairCount},edges:${diagnostics.createdEdgeCount}`;
479
+ }
480
+ function formatCompactGraphConnectDiagnostics(diagnostics) {
481
+ if (diagnostics === null) {
482
+ return "none";
483
+ }
484
+ return `pairs:${diagnostics.appliedPairCount},edges:${diagnostics.createdEdgeCount}`;
485
+ }
486
+ function formatGraphSummary(report) {
487
+ return (report.graph.latestMaterialization.operatorSummary ??
488
+ report.graph.operatorSummary ??
489
+ report.graph.latestMaterialization.detail ??
490
+ report.graph.detail);
491
+ }
383
492
  function formatScannerSurfaces(report) {
384
493
  return report.supervision.scanSurfaces.length === 0 ? "none" : report.supervision.scanSurfaces.join("|");
385
494
  }
@@ -391,7 +500,8 @@ function formatLearningBuckets(report) {
391
500
  return `pi:${buckets.principal_immediate},pb:${buckets.principal_backfill},live:${buckets.live},backfill:${buckets.backfill}`;
392
501
  }
393
502
  function formatLearningWarnings(report) {
394
- return report.learning.warningStates.length === 0 ? "none" : report.learning.warningStates.join("|");
503
+ const warnings = report.learning.warningStates.filter((warning) => warning !== "teacher_snapshot_unavailable");
504
+ return warnings.length === 0 ? "none" : warnings.join("|");
395
505
  }
396
506
  function formatLabelFlowSummary(labelFlow) {
397
507
  return `source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`;
@@ -399,11 +509,527 @@ function formatLabelFlowSummary(labelFlow) {
399
509
  function formatLearningPathSummary(learningPath) {
400
510
  return `source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`;
401
511
  }
402
- function formatCurrentProfileStatusSummary(status, report) {
512
+ function formatTeacherLoopSummary(report) {
513
+ const parts = [
514
+ `snapshot=${report.teacherLoop.sourcePath ?? "none"}`,
515
+ `kind=${report.teacherLoop.sourceKind}`,
516
+ `lastRun=${report.teacherLoop.lastRunAt ?? "none"}`,
517
+ `artifacts=${report.teacherLoop.artifactCount ?? "none"}`,
518
+ `freshness=${report.teacherLoop.latestFreshness}`,
519
+ `queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"}`,
520
+ `running=${yesNo(report.teacherLoop.running)}`
521
+ ];
522
+ if (report.teacherLoop.lastNoOpReason !== "none") {
523
+ parts.push(`noOp=${report.teacherLoop.lastNoOpReason}`);
524
+ }
525
+ if (report.teacherLoop.failureMode !== "none") {
526
+ const failureDetail = report.teacherLoop.failureDetail === null
527
+ ? report.teacherLoop.failureMode
528
+ : `${report.teacherLoop.failureMode}(${report.teacherLoop.failureDetail})`;
529
+ parts.push(`failure=${failureDetail}`);
530
+ }
531
+ return parts.join(" ");
532
+ }
533
+ function formatCompactValue(value, maxLength = 64) {
534
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}...`;
535
+ }
536
+ function formatCompactList(values, maxItems = 2, maxLength = 64) {
537
+ if (values.length === 0) {
538
+ return "none";
539
+ }
540
+ const visible = values.slice(0, maxItems).map((value) => formatCompactValue(value, maxLength));
541
+ return values.length > maxItems ? `${visible.join("|")}+${values.length - maxItems}more` : visible.join("|");
542
+ }
543
+ const SERVICE_RISK_FINDING_CODES = new Set([
544
+ "activation_broken_install",
545
+ "activation_stale_incomplete",
546
+ "active_missing",
547
+ "active_unhealthy",
548
+ "learned_route_missing",
549
+ "serve_path_fail_open",
550
+ "serve_path_hard_fail",
551
+ "serve_path_route_evidence_missing"
552
+ ]);
553
+ const DEGRADED_BRAIN_FINDING_CODES = new Set([
554
+ "bootstrap_waiting_for_first_export",
555
+ "serve_path_unprobed",
556
+ "brain_context_kernel_only",
557
+ "candidate_unhealthy",
558
+ "promotion_blocked",
559
+ "supervision_not_flowing",
560
+ "scan_surfaces_missing"
561
+ ]);
562
+ const COSMETIC_FINDING_CODES = new Set([
563
+ "last_promotion_unknown",
564
+ "rollback_blocked",
565
+ "supervision_unavailable",
566
+ "turn_attribution_partial",
567
+ "teacher_snapshot_unavailable"
568
+ ]);
569
+ const LEARNING_WARNING_MESSAGES = {
570
+ awaiting_first_export: "awaiting first export",
571
+ principal_live_backlog: "principal live backlog is ahead of serving",
572
+ principal_backfill_pending: "principal backfill is still queued",
573
+ active_pack_behind_latest_principal: "active pack is behind the latest principal correction",
574
+ passive_backfill_pending: "passive backfill remains queued",
575
+ teacher_queue_full: "teacher queue is full",
576
+ teacher_labels_stale: "teacher labels are stale",
577
+ teacher_no_artifacts: "teacher produced no artifacts",
578
+ teacher_snapshot_unavailable: "teacher snapshot is unavailable"
579
+ };
580
+ const TEACHER_NO_OP_MESSAGES = {
581
+ none: "the latest processed export produced teacher artifacts",
582
+ duplicate_export: "the latest cycle was a no-op because the export was already seen",
583
+ queue_full: "the latest cycle was a no-op because the teacher queue was full",
584
+ no_teacher_artifacts: "the latest cycle was a no-op because no teacher artifacts were produced",
585
+ empty_scan: "the latest cycle was a no-op because the scanner did not produce any events",
586
+ unavailable: "the latest cycle is not visible from the current operator snapshot"
587
+ };
588
+ function summarizeStatusInstallHook(openclawHome) {
589
+ if (openclawHome === null) {
590
+ return {
591
+ state: "unknown",
592
+ detail: "profile hook state is unknown from activation-root-only status; pin --openclaw-home to prove install state"
593
+ };
594
+ }
595
+ const extensionDir = path.join(path.resolve(openclawHome), "extensions", "openclawbrain");
596
+ const indexPath = path.join(extensionDir, "index.ts");
597
+ const runtimeGuardPath = path.join(extensionDir, "runtime-guard.js");
598
+ const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
599
+ if (existsSync(indexPath) && existsSync(runtimeGuardPath) && existsSync(manifestPath)) {
600
+ return {
601
+ state: "installed",
602
+ detail: `profile hook is installed at ${shortenPath(extensionDir)}`
603
+ };
604
+ }
605
+ return {
606
+ state: "not_installed",
607
+ detail: `profile hook is not present at ${shortenPath(extensionDir)}`
608
+ };
609
+ }
610
+ function summarizeStatusHookLoad(installHook, status) {
611
+ return {
612
+ installState: installHook.state === "unknown" ? "unverified" : installHook.state,
613
+ loadProof: status.attachment.state === "attached" && status.brainStatus.serveState === "serving_active_pack"
614
+ ? "status_probe_ready"
615
+ : "not_ready",
616
+ detail: installHook.detail
617
+ };
618
+ }
619
+ function runOllamaProbe(args, baseUrl) {
620
+ try {
621
+ execFileSync("ollama", [...args], {
622
+ stdio: "pipe",
623
+ timeout: 2_000,
624
+ env: {
625
+ ...process.env,
626
+ OLLAMA_HOST: baseUrl
627
+ }
628
+ });
629
+ return {
630
+ detected: true,
631
+ detail: `ollama responded to ${args.join(" ")} at ${baseUrl}`
632
+ };
633
+ }
634
+ catch (error) {
635
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
636
+ return {
637
+ detected: false,
638
+ detail: "ollama CLI was not found on PATH"
639
+ };
640
+ }
641
+ return {
642
+ detected: true,
643
+ detail: describeExecFailure(error)
644
+ };
645
+ }
646
+ }
647
+ function summarizeStatusEmbeddings(report, providerConfig) {
648
+ let embeddedEntryCount = null;
649
+ let totalEntryCount = null;
650
+ let models = [];
651
+ let liveState = "unknown";
652
+ let liveDetail = "no activation-ready active pack is available for embedding inspection";
653
+ if (report.active !== null && report.active.activationReady) {
654
+ try {
655
+ const activePack = loadPackFromActivation(report.activationRoot, "active", {
656
+ requireActivationReady: true
657
+ });
658
+ if (activePack !== null) {
659
+ totalEntryCount = activePack.vectors.entries.length;
660
+ embeddedEntryCount = activePack.vectors.entries.filter((entry) => entry.embedding !== undefined).length;
661
+ models = [...new Set(activePack.vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
662
+ liveState = embeddedEntryCount > 0 ? "yes" : "no";
663
+ liveDetail = `active pack stores ${embeddedEntryCount}/${totalEntryCount} numeric embeddings`;
664
+ }
665
+ }
666
+ catch (error) {
667
+ liveDetail = `embedding inspection failed: ${toErrorMessage(error)}`;
668
+ }
669
+ }
670
+ if (providerConfig.embedder.provider === "off") {
671
+ return {
672
+ provider: providerConfig.embedder.provider,
673
+ model: providerConfig.embedder.model,
674
+ provisionedState: "off",
675
+ liveState,
676
+ embeddedEntryCount,
677
+ totalEntryCount,
678
+ models,
679
+ detail: `${liveDetail}; embedder provider is off`
680
+ };
681
+ }
682
+ if (providerConfig.embedder.provider === "keywords") {
683
+ return {
684
+ provider: providerConfig.embedder.provider,
685
+ model: providerConfig.embedder.model,
686
+ provisionedState: "builtin",
687
+ liveState,
688
+ embeddedEntryCount,
689
+ totalEntryCount,
690
+ models,
691
+ detail: `${liveDetail}; keyword embedder needs no Ollama model provision`
692
+ };
693
+ }
694
+ const modelProbe = runOllamaProbe(["show", providerConfig.embedder.model], providerConfig.embedderBaseUrl);
695
+ return {
696
+ provider: providerConfig.embedder.provider,
697
+ model: providerConfig.embedder.model,
698
+ provisionedState: modelProbe.detected && /responded to/.test(modelProbe.detail) ? "confirmed" : "not_confirmed",
699
+ liveState,
700
+ embeddedEntryCount,
701
+ totalEntryCount,
702
+ models,
703
+ detail: `${liveDetail}; ollama model check: ${modelProbe.detail}`
704
+ };
705
+ }
706
+ function summarizeStatusLocalLlm(providerConfig) {
707
+ const detection = runOllamaProbe(["--version"], providerConfig.teacherBaseUrl);
708
+ const enabled = providerConfig.teacher.provider === "ollama";
709
+ if (enabled) {
710
+ return {
711
+ detected: detection.detected,
712
+ enabled,
713
+ provider: providerConfig.teacher.provider,
714
+ model: providerConfig.teacher.model,
715
+ detail: detection.detected
716
+ ? `teacher provider is ollama and the local LLM surface answered at ${providerConfig.teacherBaseUrl}`
717
+ : `teacher provider is ollama but the local LLM surface was not detected (${detection.detail})`
718
+ };
719
+ }
720
+ return {
721
+ detected: detection.detected,
722
+ enabled,
723
+ provider: providerConfig.teacher.provider,
724
+ model: providerConfig.teacher.model,
725
+ detail: detection.detected
726
+ ? `local Ollama is detectable, but teacher labeling is ${providerConfig.teacher.provider}`
727
+ : `teacher labeling is ${providerConfig.teacher.provider}; no local Ollama CLI was detected`
728
+ };
729
+ }
730
+ function summarizeStatusTeacher(report, providerConfig, localLlm) {
731
+ const enabled = providerConfig.teacher.provider === "ollama";
732
+ const latestCycle = report.teacherLoop.lastNoOpReason === "unavailable"
733
+ ? "unknown"
734
+ : report.teacherLoop.lastNoOpReason === "none"
735
+ ? "teacher_artifact"
736
+ : "no_op";
737
+ if (!enabled) {
738
+ return {
739
+ model: providerConfig.teacher.model,
740
+ enabled,
741
+ healthy: false,
742
+ stale: false,
743
+ idle: false,
744
+ latestCycle,
745
+ detail: `${providerConfig.teacher.model} is not enabled because teacher labeling is ${providerConfig.teacher.provider}`
746
+ };
747
+ }
748
+ if (!localLlm.detected) {
749
+ return {
750
+ model: providerConfig.teacher.model,
751
+ enabled,
752
+ healthy: false,
753
+ stale: null,
754
+ idle: false,
755
+ latestCycle,
756
+ detail: `${providerConfig.teacher.model} is configured on Ollama, but the local LLM surface is not answering at ${providerConfig.teacherBaseUrl}`
757
+ };
758
+ }
759
+ if (!report.teacherLoop.available) {
760
+ return {
761
+ model: providerConfig.teacher.model,
762
+ enabled,
763
+ healthy: null,
764
+ stale: null,
765
+ idle: null,
766
+ latestCycle,
767
+ detail: `${providerConfig.teacher.model} is enabled on Ollama, but no watch teacher snapshot is visible yet`
768
+ };
769
+ }
770
+ const stale = report.teacherLoop.latestFreshness === "stale" || report.teacherLoop.watchState === "stale_snapshot";
771
+ const idle = report.teacherLoop.running === false &&
772
+ (report.teacherLoop.queueDepth ?? 0) === 0 &&
773
+ report.teacherLoop.failureMode === "none";
774
+ const healthy = report.teacherLoop.failureMode === "none" &&
775
+ stale === false &&
776
+ report.teacherLoop.watchState !== "not_visible";
777
+ const cycleDetail = TEACHER_NO_OP_MESSAGES[report.teacherLoop.lastNoOpReason] ?? "the latest teacher cycle detail is unavailable";
778
+ if (report.teacherLoop.failureMode !== "none" && report.teacherLoop.failureMode !== "unavailable") {
779
+ return {
780
+ model: providerConfig.teacher.model,
781
+ enabled,
782
+ healthy: false,
783
+ stale,
784
+ idle,
785
+ latestCycle,
786
+ detail: report.teacherLoop.failureDetail === null
787
+ ? `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}`
788
+ : `${providerConfig.teacher.model} is enabled, but the watch loop recorded ${report.teacherLoop.failureMode}: ${report.teacherLoop.failureDetail}`
789
+ };
790
+ }
791
+ return {
792
+ model: providerConfig.teacher.model,
793
+ enabled,
794
+ healthy,
795
+ stale,
796
+ idle,
797
+ latestCycle,
798
+ detail: `${providerConfig.teacher.model} is enabled on Ollama; ${cycleDetail}`
799
+ };
800
+ }
801
+ function summarizeStatusEmbedder(embeddings) {
802
+ const provisioned = embeddings.provisionedState === "confirmed" || embeddings.provisionedState === "builtin"
803
+ ? true
804
+ : embeddings.provisionedState === "not_confirmed" || embeddings.provisionedState === "off"
805
+ ? false
806
+ : null;
807
+ const live = embeddings.liveState === "yes" ? true : embeddings.liveState === "no" ? false : null;
808
+ if (embeddings.provider === "off") {
809
+ return {
810
+ model: embeddings.model,
811
+ provisioned,
812
+ live,
813
+ detail: `${embeddings.model} is not provisioned because the embedder provider is off`
814
+ };
815
+ }
816
+ if (embeddings.provider === "keywords") {
817
+ return {
818
+ model: embeddings.model,
819
+ provisioned,
820
+ live,
821
+ detail: "keyword embeddings are builtin, so there is no Ollama model to provision"
822
+ };
823
+ }
824
+ if (provisioned === true && live === true) {
825
+ return {
826
+ model: embeddings.model,
827
+ provisioned,
828
+ live,
829
+ detail: `${embeddings.model} is confirmed on Ollama and the active pack stores live numeric embeddings`
830
+ };
831
+ }
832
+ if (provisioned === true && live === false) {
833
+ return {
834
+ model: embeddings.model,
835
+ provisioned,
836
+ live,
837
+ detail: `${embeddings.model} is confirmed on Ollama, but the active pack still has no live numeric embeddings`
838
+ };
839
+ }
840
+ if (provisioned === false && live === true) {
841
+ return {
842
+ model: embeddings.model,
843
+ provisioned,
844
+ live,
845
+ detail: `${embeddings.model} is not confirmed on Ollama, but the active pack already carries numeric embeddings from an earlier materialization`
846
+ };
847
+ }
848
+ return {
849
+ model: embeddings.model,
850
+ provisioned,
851
+ live,
852
+ detail: embeddings.detail
853
+ };
854
+ }
855
+ function summarizeStatusRouteFn(status, report) {
856
+ const freshness = report.servePath.refreshStatus ?? status.brain.routeFreshness;
857
+ if (!report.routeFn.available) {
858
+ return {
859
+ available: false,
860
+ freshness,
861
+ trainedAt: report.routeFn.trainedAt,
862
+ updatedAt: report.routeFn.updatedAt,
863
+ usedAt: report.routeFn.usedAt,
864
+ detail: report.routeFn.detail
865
+ };
866
+ }
867
+ let detail = report.routeFn.detail;
868
+ if (report.servePath.usedLearnedRouteFn === true) {
869
+ detail = `current serve proof used the learned route_fn; ${report.routeFn.detail}`;
870
+ }
871
+ else if (report.routeFn.usedAt !== null) {
872
+ detail = `current serve proof did not use the learned route_fn, but the active route_fn last served a learned turn at ${report.routeFn.usedAt}`;
873
+ }
874
+ else if (report.routeFn.updatedAt !== null) {
875
+ detail = `active route_fn was last updated at ${report.routeFn.updatedAt}, but no learned serve use is visible yet for the current pack`;
876
+ }
877
+ return {
878
+ available: true,
879
+ freshness,
880
+ trainedAt: report.routeFn.trainedAt,
881
+ updatedAt: report.routeFn.updatedAt,
882
+ usedAt: report.routeFn.usedAt,
883
+ detail
884
+ };
885
+ }
886
+ function pushUniqueAlert(target, value) {
887
+ const normalized = value.trim();
888
+ if (normalized.length === 0) {
889
+ return;
890
+ }
891
+ if (target.includes(normalized) === false) {
892
+ target.push(normalized);
893
+ }
894
+ }
895
+ function summarizeStatusAlerts(report, providerConfig, embeddings, localLlm) {
896
+ const buckets = {
897
+ serviceRisk: [],
898
+ degradedBrain: [],
899
+ cosmeticNoise: []
900
+ };
901
+ for (const finding of report.findings) {
902
+ if (finding.severity === "pass") {
903
+ continue;
904
+ }
905
+ if (SERVICE_RISK_FINDING_CODES.has(finding.code)) {
906
+ pushUniqueAlert(buckets.serviceRisk, finding.summary);
907
+ continue;
908
+ }
909
+ if (DEGRADED_BRAIN_FINDING_CODES.has(finding.code)) {
910
+ pushUniqueAlert(buckets.degradedBrain, finding.summary);
911
+ continue;
912
+ }
913
+ if (COSMETIC_FINDING_CODES.has(finding.code)) {
914
+ pushUniqueAlert(buckets.cosmeticNoise, finding.summary);
915
+ continue;
916
+ }
917
+ pushUniqueAlert(finding.severity === "fail" ? buckets.serviceRisk : buckets.degradedBrain, finding.summary);
918
+ }
919
+ for (const warningState of report.learning.warningStates) {
920
+ const message = LEARNING_WARNING_MESSAGES[warningState];
921
+ if (message === undefined) {
922
+ continue;
923
+ }
924
+ if (warningState === "teacher_snapshot_unavailable") {
925
+ pushUniqueAlert(buckets.cosmeticNoise, message);
926
+ }
927
+ else {
928
+ pushUniqueAlert(buckets.degradedBrain, message);
929
+ }
930
+ }
931
+ if (providerConfig.warnings.length > 0) {
932
+ pushUniqueAlert(buckets.cosmeticNoise, "provider env warnings forced fallback defaults");
933
+ }
934
+ if (localLlm.enabled && !localLlm.detected) {
935
+ pushUniqueAlert(buckets.degradedBrain, "local LLM is enabled but not detected");
936
+ }
937
+ if (embeddings.provider === "ollama" && embeddings.provisionedState !== "confirmed") {
938
+ pushUniqueAlert(buckets.degradedBrain, `embedder model ${embeddings.model} is not confirmed on Ollama`);
939
+ }
940
+ if (embeddings.provider === "ollama" && embeddings.liveState === "no") {
941
+ pushUniqueAlert(buckets.degradedBrain, "embedder is provisioned but the active pack has no live numeric embeddings");
942
+ }
943
+ return buckets;
944
+ }
945
+ function summarizeStatusWatchState(status) {
946
+ return status.passiveLearning.watchState;
947
+ }
948
+ function summarizeStatusServeReality(status) {
949
+ if (status.brainStatus.serveState === "serving_active_pack") {
950
+ return "proven_active_pack";
951
+ }
952
+ return status.brainStatus.serveState;
953
+ }
954
+ function summarizeStatusPromotionState(status) {
955
+ if (status.brain.state === "pg_promoted_pack_authoritative") {
956
+ return "promoted";
957
+ }
958
+ if (status.brain.state === "seed_state_authoritative") {
959
+ return status.passiveLearning.firstExportOccurred ? "seed_authoritative" : "awaiting_first_export";
960
+ }
961
+ return status.brain.state;
962
+ }
963
+ function formatStatusAlertLine(values) {
964
+ const normalized = values.map((value) => value.trim()).filter((value) => value.length > 0);
965
+ return normalized.length === 0 ? "none" : formatCompactList(normalized, 2, 64);
966
+ }
967
+ function formatStatusNullableNumber(value, unknown = "unknown") {
968
+ return value === null ? unknown : String(value);
969
+ }
970
+ function formatStatusNullableYesNo(value) {
971
+ return value === null ? "unknown" : yesNo(value);
972
+ }
973
+ function formatStatusNullableMilliseconds(value) {
974
+ return value === null ? "none" : `${value.toFixed(2)}ms`;
975
+ }
976
+ function formatStatusHotPathTiming(timing) {
977
+ return [
978
+ `hotPath=${formatStatusNullableMilliseconds(timing.totalMs)}`,
979
+ `route=${formatStatusNullableMilliseconds(timing.routeSelectionMs)}`,
980
+ `prompt=${formatStatusNullableMilliseconds(timing.promptAssemblyMs)}`,
981
+ `other=${formatStatusNullableMilliseconds(timing.otherMs)}`,
982
+ `background=${timing.backgroundWorkIncluded ? "included" : "excluded"}`
983
+ ].join(" ");
984
+ }
985
+ function formatStatusObservedDeltaTransition(delta) {
986
+ if (delta.latestPackTransition === null) {
987
+ return "none";
988
+ }
989
+ return `${delta.latestPackTransition.kind}:${delta.latestPackTransition.fromPackId ?? "none"}->${delta.latestPackTransition.toPackId}`;
990
+ }
991
+ function buildCompactStatusHeader(status, report, options) {
992
+ const installHook = summarizeStatusInstallHook(options.openclawHome);
993
+ const hookLoad = summarizeStatusHookLoad(installHook, status);
994
+ const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
995
+ const localLlm = summarizeStatusLocalLlm(options.providerConfig);
996
+ const teacher = summarizeStatusTeacher(report, options.providerConfig, localLlm);
997
+ const embedder = summarizeStatusEmbedder(embeddings);
998
+ const routeFn = summarizeStatusRouteFn(status, report);
999
+ const alerts = summarizeStatusAlerts(report, options.providerConfig, embeddings, localLlm);
1000
+ const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
1001
+ return [
1002
+ `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1003
+ `hook install=${hookLoad.installState} loadProof=${hookLoad.loadProof} detail=${hookLoad.detail}`,
1004
+ `passive firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)}`,
1005
+ `serving pack=${status.passiveLearning.currentServingPackId ?? "none"} lastExport=${status.passiveLearning.lastExportAt ?? "none"} lastPromotion=${status.passiveLearning.lastPromotionAt ?? "none"}`,
1006
+ `timing ${formatStatusHotPathTiming(status.brainStatus.timing)}`,
1007
+ `delta observed=${status.passiveLearning.lastObservedDelta.observedAt ?? "none"} exported=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.exported)} labeled=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.labeled)} promoted=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.promoted)} served=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.served)} transition=${formatStatusObservedDeltaTransition(status.passiveLearning.lastObservedDelta)}`,
1008
+ `changed ${status.passiveLearning.lastObservedDelta.explanation}`,
1009
+ `explain ${status.brain.summary}`,
1010
+ `graph blocks=${report.graph.blockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} latest=${report.graph.latestMaterialization.packId ?? "none"} latestChanged=${yesNo(report.graph.latestMaterialization.changed)} connect=${formatCompactGraphConnectDiagnostics(report.graph.latestMaterialization.connectDiagnostics ?? report.graph.connectDiagnostics)}`,
1011
+ `teacher model=${teacher.model} enabled=${yesNo(teacher.enabled)} healthy=${yesNo(teacher.healthy)} stale=${yesNo(teacher.stale)} idle=${yesNo(teacher.idle)} cycle=${teacher.latestCycle} why=${teacher.detail}`,
1012
+ `embedder model=${embedder.model} provisioned=${yesNo(embedder.provisioned)} live=${yesNo(embedder.live)} why=${embedder.detail}`,
1013
+ `routeFn available=${yesNo(routeFn.available)} freshness=${routeFn.freshness} trained=${routeFn.trainedAt ?? "none"} updated=${routeFn.updatedAt ?? "none"} used=${routeFn.usedAt ?? "none"} why=${routeFn.detail}`,
1014
+ `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
1015
+ `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
1016
+ `alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
1017
+ ];
1018
+ }
1019
+ function formatCurrentProfileStatusSummary(status, report, targetInspection, options) {
1020
+ const embeddings = summarizeStatusEmbeddings(report, options.providerConfig);
1021
+ const localLlm = summarizeStatusLocalLlm(options.providerConfig);
1022
+ const liveModels = embeddings.models.length === 0 ? "none" : embeddings.models.join("|");
403
1023
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
1024
+ const targetLine = targetInspection === null
1025
+ ? `target activation=${status.host.activationRoot} source=activation_root_only`
1026
+ : `target activation=${status.host.activationRoot} ${formatOpenClawTargetLine(targetInspection)} hook=${shortenPath(path.join(targetInspection.openclawHome, "extensions", "openclawbrain", "index.ts"))}`;
404
1027
  return [
405
1028
  `STATUS ${status.brainStatus.status}`,
1029
+ ...buildCompactStatusHeader(status, report, options),
406
1030
  `answer ${status.brain.summary}`,
1031
+ targetLine,
1032
+ ...(targetInspection === null ? [] : [`preflight ${formatOpenClawTargetExplanation(targetInspection)}`]),
407
1033
  `host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
408
1034
  `profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
409
1035
  `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)}`,
@@ -414,13 +1040,17 @@ function formatCurrentProfileStatusSummary(status, report) {
414
1040
  `budget requested=${report.servePath.requestedBudgetStrategy ?? "none"} resolved=${report.servePath.resolvedBudgetStrategy ?? "none"} maxBlocks=${report.servePath.resolvedMaxContextBlocks ?? "none"} source=${report.servePath.structuralBudgetSource ?? "none"} origin=${status.brainStatus.structuralDecision.origin} basis=${status.brainStatus.structuralDecision.basis}`,
415
1041
  `decision ${status.brainStatus.structuralDecision.detail}`,
416
1042
  `principal latest=${formatPrincipalLatest(report)} pending=${report.principal.pendingCount ?? report.learning.pendingPrincipalCount ?? "none"} checkpoint=${formatPrincipalCheckpointFrontier(report)} downstream=${yesNo(report.principal.servingDownstreamOfLatestCorrection)} lag=${report.learning.principalLagToPromotion.sequenceLag ?? "none"}`,
1043
+ `passive learner=${yesNo(status.passiveLearning.learnerRunning)} firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} watch=${status.passiveLearning.watchState} export=${status.passiveLearning.exportState} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)} detail=${status.passiveLearning.detail}`,
1044
+ `delta observed=${status.passiveLearning.lastObservedDelta.observedAt ?? "none"} exported=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.exported)} labeled=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.labeled)} promoted=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.promoted)} served=${formatStatusNullableYesNo(status.passiveLearning.lastObservedDelta.served)} transition=${formatStatusObservedDeltaTransition(status.passiveLearning.lastObservedDelta)} detail=${status.passiveLearning.lastObservedDelta.explanation}`,
417
1045
  `scanner flowing=${yesNo(report.supervision.flowing)} scan=${report.supervision.scanPolicy ?? "none"} surfaces=${formatScannerSurfaces(report)} labels=${report.supervision.humanLabelCount ?? "none"}/${report.supervision.selfLabelCount ?? "none"} attributable=${report.supervision.attributedEventCount ?? "none"}/${report.supervision.totalEventCount ?? "none"} digests=${report.supervision.selectionDigestCount ?? "none"}`,
418
1046
  `labels ${formatLabelFlowSummary(report.labelFlow)}`,
419
- `graph source=${report.graph.runtimePlasticitySource ?? "none"} ops=${formatStructuralOps(report)} changed=${yesNo(report.graph.changed)} pruned=${report.graph.prunedBlockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} summary=${report.graph.operatorSummary ?? report.graph.detail}`,
1047
+ `graph source=${report.graph.runtimePlasticitySource ?? "none"} blocks=${report.graph.blockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} ops=${formatStructuralOps(report)} latest=${report.graph.latestMaterialization.packId ?? "none"} latestChanged=${yesNo(report.graph.latestMaterialization.changed)} connect=${formatGraphConnectDiagnostics(report.graph.latestMaterialization.connectDiagnostics ?? report.graph.connectDiagnostics)} summary=${formatGraphSummary(report)}`,
420
1048
  `path ${formatLearningPathSummary(report.learningPath)}`,
421
1049
  `learning state=${report.learning.backlogState} bootstrapped=${yesNo(report.learning.bootstrapped)} mode=${report.learning.mode} next=${report.learning.nextPriorityLane} priority=${report.learning.nextPriorityBucket} pending=${report.learning.pendingLive ?? "none"}/${report.learning.pendingBackfill ?? "none"} buckets=${formatLearningBuckets(report)} warn=${formatLearningWarnings(report)} lastPack=${report.learning.lastMaterializedPackId ?? "none"} detail=${report.learning.detail}`,
422
- `teacher snapshot=${report.teacherLoop.sourcePath ?? "none"} kind=${report.teacherLoop.sourceKind} lastRun=${report.teacherLoop.lastRunAt ?? "none"} artifacts=${report.teacherLoop.artifactCount ?? "none"} freshness=${report.teacherLoop.latestFreshness} queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"} running=${yesNo(report.teacherLoop.running)} noOp=${report.teacherLoop.lastNoOpReason} failure=${report.teacherLoop.failureMode}${report.teacherLoop.failureDetail === null ? "" : `(${report.teacherLoop.failureDetail})`}`,
423
- `passive cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} slices=${report.teacherLoop.liveSlicesPerCycle ?? "none"}/${report.teacherLoop.backfillSlicesPerCycle ?? "none"} replayed=${report.teacherLoop.replayedBundleCount ?? "none"}/${report.teacherLoop.replayedEventCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} tail=${report.teacherLoop.sessionTailSessionsTracked ?? "none"}/${report.teacherLoop.sessionTailBridgedEventCount ?? "none"} tailState=${report.teacherLoop.localSessionTailNoopReason ?? "none"} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
1050
+ `teacherProof ${formatTeacherLoopSummary(report)}`,
1051
+ `watch cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} heartbeat=${report.teacherLoop.lastHeartbeatAt ?? "none"} interval=${report.teacherLoop.pollIntervalSeconds ?? "none"} replayed=${report.teacherLoop.replayedBundleCount ?? "none"}/${report.teacherLoop.replayedEventCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} tail=${report.teacherLoop.sessionTailSessionsTracked ?? "none"}/${report.teacherLoop.sessionTailBridgedEventCount ?? "none"} tailState=${report.teacherLoop.localSessionTailNoopReason ?? "none"} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
1052
+ `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
1053
+ `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
424
1054
  `rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
425
1055
  `proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
426
1056
  `logs root=${status.brain.logRoot ?? "none"}`,
@@ -437,14 +1067,399 @@ function shortenPath(fullPath) {
437
1067
  }
438
1068
  return fullPath;
439
1069
  }
1070
+ function formatOpenClawTargetLine(inspection) {
1071
+ const profilePart = inspection.profileId === null
1072
+ ? "profile=current_profile"
1073
+ : `profile=${inspection.profileId} via ${formatOpenClawHomeProfileSource(inspection.profileSource)}`;
1074
+ return `home=${shortenPath(inspection.openclawHome)} layout=${formatOpenClawHomeLayout(inspection.layout)} ${profilePart}`;
1075
+ }
1076
+ function formatOpenClawTargetExplanation(inspection) {
1077
+ return describeOpenClawHomeInspection(inspection);
1078
+ }
440
1079
  function buildInstallStatusCommand(activationRoot) {
441
1080
  return `openclawbrain status --activation-root ${quoteShellArg(activationRoot)}`;
442
1081
  }
1082
+ function buildLearnerServiceStatusCommand(activationRoot) {
1083
+ return `openclawbrain daemon status --activation-root ${quoteShellArg(activationRoot)}`;
1084
+ }
1085
+ function buildGatewayRestartCommand(profileId) {
1086
+ return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway restart`;
1087
+ }
1088
+ function buildGatewayStatusCommand(profileId) {
1089
+ return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway status`;
1090
+ }
443
1091
  function buildInstallCommand(openclawHome) {
444
1092
  return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
445
1093
  }
446
- function buildInstallReloadGuidance() {
447
- 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.";
1094
+ function buildAttachCommand(openclawHome, activationRoot = null) {
1095
+ const parts = ["openclawbrain", "attach", "--openclaw-home", quoteShellArg(openclawHome)];
1096
+ if (activationRoot !== null) {
1097
+ parts.push("--activation-root", quoteShellArg(activationRoot));
1098
+ }
1099
+ return parts.join(" ");
1100
+ }
1101
+ function buildInstallEmbedderProvisionCommand(baseUrl, model) {
1102
+ return `OLLAMA_HOST=${quoteShellArg(baseUrl)} ollama pull ${quoteShellArg(model)}`;
1103
+ }
1104
+ function describeExecOutput(value) {
1105
+ if (typeof value === "string") {
1106
+ const normalized = value.trim();
1107
+ return normalized.length > 0 ? normalized : null;
1108
+ }
1109
+ if (value instanceof Buffer) {
1110
+ const normalized = value.toString("utf8").trim();
1111
+ return normalized.length > 0 ? normalized : null;
1112
+ }
1113
+ return null;
1114
+ }
1115
+ function describeExecFailure(error) {
1116
+ if (error instanceof Error) {
1117
+ const childError = error;
1118
+ if (childError.code === "ENOENT") {
1119
+ return "ollama was not found on PATH";
1120
+ }
1121
+ const stderr = describeExecOutput(childError.stderr);
1122
+ if (stderr !== null) {
1123
+ return stderr;
1124
+ }
1125
+ const stdout = describeExecOutput(childError.stdout);
1126
+ if (stdout !== null) {
1127
+ return stdout;
1128
+ }
1129
+ const message = childError.message.trim();
1130
+ if (message.length > 0) {
1131
+ return message;
1132
+ }
1133
+ }
1134
+ return String(error);
1135
+ }
1136
+ function toErrorMessage(error) {
1137
+ return error instanceof Error ? error.message : String(error);
1138
+ }
1139
+ function ensureInstallEmbedderReady(parsed) {
1140
+ const providerConfig = readOpenClawBrainProviderConfig(process.env);
1141
+ const model = DEFAULT_OLLAMA_EMBEDDING_MODEL;
1142
+ const baseUrl = providerConfig.embedderBaseUrl;
1143
+ if (parsed.skipEmbedderProvision) {
1144
+ const skipReason = parsed.skipEmbedderProvisionSource === "flag"
1145
+ ? "--skip-embedder-provision"
1146
+ : `${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1`;
1147
+ return {
1148
+ state: "skipped",
1149
+ model,
1150
+ baseUrl,
1151
+ detail: `Skipped default embedder provisioning (${skipReason}); ${parsed.command} continued only because the operator explicitly opted out. ` +
1152
+ `Provision it later with ${buildInstallEmbedderProvisionCommand(baseUrl, model)}.`
1153
+ };
1154
+ }
1155
+ try {
1156
+ execFileSync("ollama", ["pull", model], {
1157
+ stdio: "pipe",
1158
+ env: {
1159
+ ...process.env,
1160
+ OLLAMA_HOST: baseUrl
1161
+ }
1162
+ });
1163
+ }
1164
+ catch (error) {
1165
+ const detail = describeExecFailure(error);
1166
+ throw new Error(`Default embedder provisioning failed before brain init. Tried ${buildInstallEmbedderProvisionCommand(baseUrl, model)}. ` +
1167
+ `${parsed.command === "install" ? "Install" : "Attach"} stops here so the bootstrap path does not quietly continue without ${model}. ` +
1168
+ `Fix Ollama and rerun ${parsed.command}, or explicitly skip with --skip-embedder-provision or ${OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}=1. ` +
1169
+ `Detail: ${detail}`);
1170
+ }
1171
+ return {
1172
+ state: "ensured",
1173
+ model,
1174
+ baseUrl,
1175
+ detail: `Ensured default embedder before brain bootstrap: ${buildInstallEmbedderProvisionCommand(baseUrl, model)}`
1176
+ };
1177
+ }
1178
+ function parseOllamaListModelNames(output) {
1179
+ return output
1180
+ .split(/\r?\n/u)
1181
+ .map((line) => line.trim())
1182
+ .filter((line) => line.length > 0 && !/^name\s+/iu.test(line))
1183
+ .map((line) => line.split(/\s+/u)[0] ?? "")
1184
+ .filter((name) => name.length > 0);
1185
+ }
1186
+ function selectCompatibleLocalTeacherModel(models) {
1187
+ const normalized = models.map((model) => model.trim()).filter((model) => model.length > 0);
1188
+ for (const prefix of INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES) {
1189
+ const exact = normalized.find((model) => model === prefix);
1190
+ if (exact !== undefined) {
1191
+ return exact;
1192
+ }
1193
+ const variant = normalized.find((model) => model.startsWith(`${prefix}-`) ||
1194
+ model.startsWith(`${prefix}_`) ||
1195
+ model.startsWith(`${prefix}.`));
1196
+ if (variant !== undefined) {
1197
+ return variant;
1198
+ }
1199
+ }
1200
+ return null;
1201
+ }
1202
+ function detectInstallTeacherDefaults(baseUrl) {
1203
+ try {
1204
+ const output = execFileSync("ollama", ["list"], {
1205
+ stdio: "pipe",
1206
+ env: {
1207
+ ...process.env,
1208
+ OLLAMA_HOST: baseUrl
1209
+ }
1210
+ }).toString("utf8");
1211
+ const availableModels = parseOllamaListModelNames(output);
1212
+ const model = selectCompatibleLocalTeacherModel(availableModels);
1213
+ if (model === null) {
1214
+ return {
1215
+ provider: "heuristic",
1216
+ model: null,
1217
+ baseUrl,
1218
+ availableModels,
1219
+ detectionDetail: availableModels.length === 0
1220
+ ? `No compatible local Ollama teacher model detected on ${baseUrl}; watch keeps heuristic teacher defaults.`
1221
+ : `No compatible local Ollama teacher model detected on ${baseUrl}; saw ${availableModels.join(", ")} and kept heuristic teacher defaults.`
1222
+ };
1223
+ }
1224
+ return {
1225
+ provider: "ollama",
1226
+ model,
1227
+ baseUrl,
1228
+ availableModels,
1229
+ detectionDetail: `Detected compatible local Ollama teacher model ${model} on ${baseUrl}; watch will enable it by default from the installed activation root.`
1230
+ };
1231
+ }
1232
+ catch (error) {
1233
+ const detail = describeExecFailure(error);
1234
+ return {
1235
+ provider: "heuristic",
1236
+ model: null,
1237
+ baseUrl,
1238
+ availableModels: [],
1239
+ detectionDetail: `Local Ollama teacher autodetect failed on ${baseUrl}; kept heuristic teacher defaults. Detail: ${detail}`
1240
+ };
1241
+ }
1242
+ }
1243
+ function writeInstallProviderDefaults(parsed) {
1244
+ const providerConfig = readOpenClawBrainProviderConfig(process.env);
1245
+ const teacherDetection = detectInstallTeacherDefaults(providerConfig.teacherBaseUrl);
1246
+ const defaultsPath = resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot);
1247
+ const defaults = {
1248
+ contract: "openclawbrain_provider_defaults.v1",
1249
+ writtenAt: new Date().toISOString(),
1250
+ source: "install",
1251
+ teacherBaseUrl: providerConfig.teacherBaseUrl,
1252
+ embedderBaseUrl: providerConfig.embedderBaseUrl,
1253
+ teacher: {
1254
+ provider: teacherDetection.provider,
1255
+ model: teacherDetection.model,
1256
+ detectedLocally: teacherDetection.provider === "ollama",
1257
+ detectedFromModel: teacherDetection.model
1258
+ },
1259
+ embedder: {
1260
+ provider: "ollama",
1261
+ model: DEFAULT_OLLAMA_EMBEDDING_MODEL
1262
+ }
1263
+ };
1264
+ writeFileSync(defaultsPath, JSON.stringify(defaults, null, 2) + "\n", "utf8");
1265
+ return {
1266
+ path: defaultsPath,
1267
+ defaults,
1268
+ detail: `Wrote local provider defaults: ${teacherDetection.detectionDetail}`,
1269
+ lifecycleSummary: teacherDetection.provider === "ollama" && teacherDetection.model !== null
1270
+ ? `Teacher: auto-enabled local Ollama model ${teacherDetection.model} from install-written defaults`
1271
+ : "Teacher: no compatible local Ollama model detected; watch stays heuristic unless explicitly overridden"
1272
+ };
1273
+ }
1274
+ function shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall) {
1275
+ if (isInstall || activationPlan.action === "bootstrap") {
1276
+ return true;
1277
+ }
1278
+ return !existsSync(resolveOpenClawBrainProviderDefaultsPath(parsed.activationRoot));
1279
+ }
1280
+ function buildInstallBrainFeedbackSummary(input) {
1281
+ const providerDefaultsPath = resolveOpenClawBrainProviderDefaultsPath(input.parsed.activationRoot);
1282
+ const embedderState = input.embedderProvision === null ? "unchanged" : input.embedderProvision.state;
1283
+ const teacherDefaults = input.providerDefaults?.defaults.teacher;
1284
+ const teacherProvider = teacherDefaults?.provider ?? "unknown";
1285
+ const teacherModel = teacherDefaults?.model ?? null;
1286
+ const detectedLocalLlm = teacherDefaults?.detectedLocally ?? null;
1287
+ const profileName = input.targetInspection.profileId;
1288
+ const profileSource = input.targetInspection.profileSource;
1289
+ const casingGuidance = profileName === null
1290
+ ? "Exact OpenClaw --profile casing is unresolved here because this target stays on the host-selected current_profile boundary."
1291
+ : `Use the exact OpenClaw profile casing shown here for host-side restart/status commands: ${quoteShellArg(profileName)}.`;
1292
+ const attachment = input.parsed.shared
1293
+ ? {
1294
+ policy: "shared",
1295
+ activationRootMode: "shared_root_declared",
1296
+ sameGatewayProof: "not_checked_in",
1297
+ detail: "Shared activation root declared. Other profiles may point at this same root, but same-gateway many-profile load/serve proof is not checked in."
1298
+ }
1299
+ : {
1300
+ policy: "dedicated",
1301
+ activationRootMode: "dedicated_per_profile",
1302
+ sameGatewayProof: "not_applicable",
1303
+ detail: "Dedicated activation root for this profile/home boundary."
1304
+ };
1305
+ const restart = profileName === null
1306
+ ? {
1307
+ exactProfile: false,
1308
+ profile: null,
1309
+ profileSource,
1310
+ guidance: `Operator-owned restart step: this install did not infer an exact --profile token from ${shortenPath(input.targetInspection.openclawHome)}. ` +
1311
+ "If immediate load matters, restart the host-selected current_profile from OpenClaw itself; otherwise the next natural launch will pick up the hook.",
1312
+ restartCommand: null,
1313
+ gatewayStatusCommand: null
1314
+ }
1315
+ : {
1316
+ exactProfile: true,
1317
+ profile: profileName,
1318
+ profileSource,
1319
+ guidance: `Operator-owned restart step: if immediate load matters and profile ${quoteShellArg(profileName)} is already running, run ${buildGatewayRestartCommand(profileName)}. ` +
1320
+ `If it is stopped, the next launch of profile ${quoteShellArg(profileName)} will pick up the hook. ${casingGuidance}`,
1321
+ restartCommand: buildGatewayRestartCommand(profileName),
1322
+ gatewayStatusCommand: buildGatewayStatusCommand(profileName)
1323
+ };
1324
+ const provedNow = input.activationPlan.action === "bootstrap"
1325
+ ? `hook written, activation root ready, seed/current-profile attach bootstrapped, learner service ${input.learnerService.state}, provider defaults ${input.providerDefaults === null ? "kept" : "written"}`
1326
+ : `hook written, activation root kept, active pack ${input.activationPlan.activePackId ?? "unknown"} preserved, learner service ${input.learnerService.state}${input.providerDefaults === null ? "" : ", provider defaults written"}`;
1327
+ const notYetProved = input.learnerService.state === "deferred"
1328
+ ? `OpenClaw has not reloaded this hook yet, and passive learner auto-start was deferred; restart plus status still must prove serve-path load, while learner-service start remains a separate operator check`
1329
+ : input.activationPlan.action === "bootstrap"
1330
+ ? `Passive learning is wired for this activation root, but OpenClaw has not reloaded the hook yet; restart plus status still must prove live startup/load and the first exported turn`
1331
+ : `Passive learning is wired for this activation root, but this ${input.parsed.command} run does not itself prove live startup/load after restart`;
1332
+ return {
1333
+ hookPath: input.extensionDir,
1334
+ providerDefaultsPath,
1335
+ profile: {
1336
+ exactProfileName: profileName,
1337
+ profileSource,
1338
+ casingGuidance
1339
+ },
1340
+ attachment,
1341
+ restart,
1342
+ embedder: {
1343
+ provider: "ollama",
1344
+ model: DEFAULT_OLLAMA_EMBEDDING_MODEL,
1345
+ state: embedderState
1346
+ },
1347
+ teacher: {
1348
+ provider: teacherProvider,
1349
+ model: teacherModel,
1350
+ detectedLocalLlm
1351
+ },
1352
+ learnerService: {
1353
+ state: input.learnerService.state,
1354
+ detail: input.learnerService.detail,
1355
+ plistPath: input.learnerService.plistPath,
1356
+ logPath: input.learnerService.logPath,
1357
+ configuredActivationRoot: input.learnerService.configuredActivationRoot,
1358
+ matchesRequestedActivationRoot: input.learnerService.matchesRequestedActivationRoot
1359
+ },
1360
+ startup: {
1361
+ token: "BRAIN_NOT_YET_LOADED",
1362
+ proof: "restart_required"
1363
+ },
1364
+ provedNow,
1365
+ notYetProved,
1366
+ lines: [
1367
+ `target ${formatOpenClawTargetLine(input.targetInspection)} source=${formatInstallOpenClawHomeSource(input.parsed.openclawHomeSource)}`,
1368
+ profileName === null
1369
+ ? "profile exactName=unresolved selector=current_profile casing=not_available"
1370
+ : `profile exactName=${quoteShellArg(profileName)} source=${profileSource} casing=preserved`,
1371
+ `hook written=${shortenPath(input.extensionDir)}`,
1372
+ `activation root=${shortenPath(input.parsed.activationRoot)} source=${formatInstallActivationRootSource(input.parsed.activationRootSource)}`,
1373
+ `attachment policy=${attachment.policy} rootMode=${attachment.activationRootMode} sameGatewayProof=${attachment.sameGatewayProof} detail=${attachment.detail}`,
1374
+ `defaults provider-defaults=${shortenPath(providerDefaultsPath)} state=${input.providerDefaults === null ? "unchanged" : "written"}`,
1375
+ `embedder provider=ollama model=${DEFAULT_OLLAMA_EMBEDDING_MODEL} state=${embedderState}`,
1376
+ `teacher provider=${teacherProvider} model=${teacherModel ?? "none"} localLLM=${detectedLocalLlm === null ? "unknown" : yesNo(detectedLocalLlm)}`,
1377
+ `learner state=${input.learnerService.state} detail=${input.learnerService.detail}`,
1378
+ `restart operator=manual exactProfile=${yesNo(restart.exactProfile)} command=${restart.restartCommand ?? "unavailable"}`,
1379
+ "startup BRAIN_NOT_YET_LOADED proof=restart_required",
1380
+ `provedNow ${provedNow}`,
1381
+ `notYet ${notYetProved}`
1382
+ ]
1383
+ };
1384
+ }
1385
+ function buildInstallReloadGuidance(input) {
1386
+ if (input.targetInspection.profileId === null) {
1387
+ return `Restart later from OpenClaw for the host-selected current_profile behind ${shortenPath(input.targetInspection.openclawHome)} if immediate load matters; this install did not infer an exact --profile token.`;
1388
+ }
1389
+ return `Restart now if immediate load matters: ${buildGatewayRestartCommand(input.targetInspection.profileId)}`;
1390
+ }
1391
+ const LEGACY_PROFILE_NOTE_FILENAMES = ["BRAIN.md", "brain.md"];
1392
+ const LEGACY_BRAIN_AGENTS_LINE = "5. Read `BRAIN.md` — your learning brain context";
1393
+ function isLegacyBrainAdvisoryContent(content) {
1394
+ return content.includes("## OpenClawBrain")
1395
+ && content.includes("You have a learning brain attached at ")
1396
+ && content.includes("openclawbrain status --activation-root")
1397
+ && content.includes("openclawbrain rollback --activation-root");
1398
+ }
1399
+ function writeUpdatedTextFile(filePath, nextText, previousText) {
1400
+ const normalizedNextText = previousText.endsWith("\n") ? `${nextText}\n` : nextText;
1401
+ writeFileSync(filePath, normalizedNextText, "utf8");
1402
+ }
1403
+ function collectProfileResidueDirs(openclawHome) {
1404
+ const directories = [path.resolve(openclawHome)];
1405
+ try {
1406
+ const entries = readdirSync(openclawHome, { withFileTypes: true });
1407
+ for (const entry of entries) {
1408
+ if (entry.isDirectory() && entry.name.startsWith("workspace-")) {
1409
+ directories.push(path.join(openclawHome, entry.name));
1410
+ }
1411
+ }
1412
+ }
1413
+ catch {
1414
+ // Residue cleanup stays best-effort.
1415
+ }
1416
+ return directories;
1417
+ }
1418
+ function removeLegacyProfileResidue(openclawHome) {
1419
+ const removedNotes = [];
1420
+ const updatedAgents = [];
1421
+ for (const directory of collectProfileResidueDirs(openclawHome)) {
1422
+ for (const fileName of LEGACY_PROFILE_NOTE_FILENAMES) {
1423
+ const notePath = path.join(directory, fileName);
1424
+ if (!existsSync(notePath)) {
1425
+ continue;
1426
+ }
1427
+ try {
1428
+ const content = readFileSync(notePath, "utf8");
1429
+ if (!isLegacyBrainAdvisoryContent(content)) {
1430
+ continue;
1431
+ }
1432
+ }
1433
+ catch {
1434
+ continue;
1435
+ }
1436
+ rmSync(notePath, { force: true });
1437
+ removedNotes.push(notePath);
1438
+ }
1439
+ const agentsPath = path.join(directory, "AGENTS.md");
1440
+ if (!existsSync(agentsPath)) {
1441
+ continue;
1442
+ }
1443
+ let agentsContent;
1444
+ try {
1445
+ agentsContent = readFileSync(agentsPath, "utf8");
1446
+ }
1447
+ catch {
1448
+ continue;
1449
+ }
1450
+ const nextContent = agentsContent
1451
+ .split("\n")
1452
+ .filter((line) => line.trim() !== LEGACY_BRAIN_AGENTS_LINE)
1453
+ .join("\n");
1454
+ if (nextContent !== agentsContent) {
1455
+ writeUpdatedTextFile(agentsPath, nextContent, agentsContent);
1456
+ updatedAgents.push(agentsPath);
1457
+ }
1458
+ }
1459
+ return {
1460
+ removedNotes,
1461
+ updatedAgents
1462
+ };
448
1463
  }
449
1464
  function buildCleanupRestartGuidance(restart) {
450
1465
  if (restart === "never") {
@@ -455,7 +1470,7 @@ function buildCleanupRestartGuidance(restart) {
455
1470
  }
456
1471
  return "If this OpenClaw profile is currently running, restart it before expecting the new hook state to take effect. If it is stopped, the next launch will pick it up.";
457
1472
  }
458
- function buildStatusNextStep(status, report) {
1473
+ function buildStatusNextStep(status, report, options) {
459
1474
  const activationRootArg = quoteShellArg(status.host.activationRoot);
460
1475
  if (status.brainStatus.activationState === "broken_install") {
461
1476
  return "Repair or replace the activation root before trusting serve-path status again.";
@@ -466,9 +1481,18 @@ function buildStatusNextStep(status, report) {
466
1481
  if (status.brainStatus.status === "fail") {
467
1482
  return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
468
1483
  }
1484
+ if (options.openclawHome !== null && options.installHook.state === "not_installed") {
1485
+ return `Run \`${buildInstallCommand(options.openclawHome)}\` before expecting this OpenClaw home to load the brain hook.`;
1486
+ }
469
1487
  if (status.brainStatus.awaitingFirstExport) {
470
1488
  return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
471
1489
  }
1490
+ if (options.openclawHome === null) {
1491
+ return `Pin \`--openclaw-home <path>\` when you need exact hook-install truth; activation-root-only status only proves this root's serve-path state.`;
1492
+ }
1493
+ if (options.installHook.state === "installed" && status.brainStatus.serveState === "serving_active_pack") {
1494
+ return "Check the OpenClaw startup log for the `[openclawbrain] BRAIN LOADED` breadcrumb when you need live hook-load proof.";
1495
+ }
472
1496
  if (report.learning.warningStates.includes("principal_live_backlog") ||
473
1497
  report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
474
1498
  return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
@@ -478,43 +1502,19 @@ function buildStatusNextStep(status, report) {
478
1502
  }
479
1503
  return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
480
1504
  }
481
- function formatHumanFriendlyStatus(status, report) {
482
- // Brain status line
483
- const brainActive = status.brainStatus.status === "ok" || status.brainStatus.serveState === "serving_active_pack";
484
- const brainIcon = brainActive ? "Active ✓" : status.brainStatus.status === "fail" ? "Inactive ✗" : `${status.brainStatus.status}`;
485
- // Pack line
486
- const packId = status.brain.activePackId ?? "none";
487
- const packShort = packId.length > 9 ? packId.slice(0, 9) : packId;
488
- const state = status.brain.state ?? "unknown";
489
- // Activation root
490
- const activationPath = shortenPath(status.host.activationRoot);
491
- // Policy
492
- const policy = status.attachment.policyMode ?? report.manyProfile.declaredAttachmentPolicy ?? "undeclared";
493
- const principalFrontier = formatPrincipalCheckpointFrontier(report);
494
- const pendingLive = String(report.learning.pendingLive ?? "none");
495
- const pendingBackfill = String(report.learning.pendingBackfill ?? "none");
496
- const nextLane = report.learning.nextPriorityLane ?? "none";
497
- const nextBucket = report.learning.nextPriorityBucket ?? "none";
1505
+ function formatHumanFriendlyStatus(status, report, targetInspection, options) {
498
1506
  const lines = [
499
- `Brain: ${brainIcon}`,
500
- `Pack: ${packShort} (${state})`,
501
- `Activation: ${activationPath}`,
502
- `Policy: ${policy}`,
503
- `Lifecycle: activation=${status.brainStatus.activationState} attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
504
- `Rollback: state=${report.rollback.state} ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"}`,
505
- `Backlog: principal=${principalFrontier} live=${pendingLive} backfill=${pendingBackfill} next=${nextLane}/${nextBucket}`,
506
- `Labels: ${formatLabelFlowSummary(report.labelFlow)}`,
507
- `Teacher: lastRun=${report.teacherLoop.lastRunAt ?? "none"} artifacts=${report.teacherLoop.artifactCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} cadence=${report.teacherLoop.learningCadence}/${report.teacherLoop.scanPolicy} failure=${report.teacherLoop.failureMode}`,
508
- `Learning: ${formatLearningPathSummary(report.learningPath)}`
1507
+ `STATUS ${status.brainStatus.status}`,
1508
+ ...buildCompactStatusHeader(status, report, options),
1509
+ ...(targetInspection === null ? [] : [
1510
+ `target ${formatOpenClawTargetLine(targetInspection)}`,
1511
+ `preflight ${formatOpenClawTargetExplanation(targetInspection)}`
1512
+ ]),
1513
+ `next ${buildStatusNextStep(status, report, {
1514
+ openclawHome: options.openclawHome,
1515
+ installHook: summarizeStatusInstallHook(options.openclawHome)
1516
+ })}`
509
1517
  ];
510
- // Add learning/serve warnings if relevant
511
- if (report.learning.warningStates.length > 0) {
512
- lines.push(`Warnings: ${report.learning.warningStates.join(", ")}`);
513
- }
514
- if (status.brainStatus.awaitingFirstExport) {
515
- lines.push(`Note: Awaiting first event export`);
516
- }
517
- lines.push(`Next: ${buildStatusNextStep(status, report)}`);
518
1518
  return lines.join("\n");
519
1519
  }
520
1520
  function requireActivationRoot(input, openclawHome, command) {
@@ -560,10 +1560,18 @@ function formatScanSessionSummary(result) {
560
1560
  function formatScanLiveSummary(result, snapshotOutPath) {
561
1561
  const materializedPackId = result.snapshot.learner.lastMaterialization?.candidate.summary.packId ?? "none";
562
1562
  const materializationReason = result.snapshot.learner.lastMaterialization?.reason ?? "none";
1563
+ const teacherSummary = [
1564
+ `artifacts=${result.snapshot.teacher.artifactCount}`,
1565
+ `freshness=${result.snapshot.teacher.latestFreshness}`,
1566
+ `humanLabels=${result.supervision.humanLabelCount}`
1567
+ ];
1568
+ if (result.snapshot.diagnostics.lastNoOpReason !== "none") {
1569
+ teacherSummary.push(`noop=${result.snapshot.diagnostics.lastNoOpReason}`);
1570
+ }
563
1571
  return [
564
1572
  "SCAN live ok",
565
1573
  `source digest=${result.supervision.exportDigest} session=${result.supervision.sessionId ?? "none"} channel=${result.supervision.channel ?? "none"} range=${result.supervision.eventRange.start}-${result.supervision.eventRange.end}/${result.supervision.eventRange.count}`,
566
- `teacher artifacts=${result.snapshot.teacher.artifactCount} freshness=${result.snapshot.teacher.latestFreshness} humanLabels=${result.supervision.humanLabelCount} noop=${result.snapshot.diagnostics.lastNoOpReason}`,
1574
+ `teacher ${teacherSummary.join(" ")}`,
567
1575
  `labels source=${result.labelFlow.source} human=${result.labelFlow.humanLabelCount ?? "none"} self=${result.labelFlow.selfLabelCount ?? "none"} implicitPositive=${result.labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${result.labelFlow.asyncTeacherArtifactCount ?? "none"}`,
568
1576
  `path source=${result.learningPath.source} pg=${result.learningPath.policyGradientVersion} method=${result.learningPath.policyGradientMethod ?? "none"} target=${result.learningPath.targetConstruction ?? "none"} connect=${result.learningPath.connectOpsFired ?? "none"} trajectories=${result.learningPath.reconstructedTrajectoryCount ?? "none"}`,
569
1577
  `learner packLabel=${result.packLabel} materialized=${materializedPackId} reason=${materializationReason}`,
@@ -583,12 +1591,12 @@ export function parseOperatorCliArgs(argv) {
583
1591
  let rootDir = null;
584
1592
  let workspacePath = null;
585
1593
  let packLabel = null;
586
- let packRoot = null;
587
1594
  let workspaceId = null;
588
1595
  let observedAt = null;
589
1596
  let snapshotOutPath = null;
590
1597
  let openclawHome = null;
591
1598
  let shared = false;
1599
+ let skipEmbedderProvision = false;
592
1600
  let keepData = false;
593
1601
  let purgeData = false;
594
1602
  let restart = "safe";
@@ -992,6 +2000,10 @@ export function parseOperatorCliArgs(argv) {
992
2000
  shared = true;
993
2001
  continue;
994
2002
  }
2003
+ if (arg === "--skip-embedder-provision") {
2004
+ skipEmbedderProvision = true;
2005
+ continue;
2006
+ }
995
2007
  if (arg === "--keep-data") {
996
2008
  keepData = true;
997
2009
  continue;
@@ -1125,14 +2137,6 @@ export function parseOperatorCliArgs(argv) {
1125
2137
  index += 1;
1126
2138
  continue;
1127
2139
  }
1128
- if (arg === "--pack-root") {
1129
- if (next === undefined) {
1130
- throw new Error("--pack-root requires a value");
1131
- }
1132
- packRoot = next;
1133
- index += 1;
1134
- continue;
1135
- }
1136
2140
  if (arg === "--workspace-id") {
1137
2141
  if (next === undefined) {
1138
2142
  throw new Error("--workspace-id requires a value");
@@ -1146,13 +2150,64 @@ export function parseOperatorCliArgs(argv) {
1146
2150
  if (command !== "detach" && command !== "uninstall" && restartExplicitlySet) {
1147
2151
  throw new Error("--restart only applies to detach/uninstall");
1148
2152
  }
2153
+ if (command !== "install" && command !== "attach" && shared) {
2154
+ throw new Error("--shared only applies to install/attach");
2155
+ }
2156
+ if (command !== "install" && command !== "attach" && skipEmbedderProvision) {
2157
+ throw new Error("--skip-embedder-provision only applies to install/attach");
2158
+ }
1149
2159
  if (command !== "uninstall" && keepData) {
1150
2160
  throw new Error("--keep-data only applies to uninstall; use detach to preserve activation data");
1151
2161
  }
1152
2162
  if (command !== "uninstall" && purgeData) {
1153
2163
  throw new Error("--purge-data only applies to uninstall");
1154
2164
  }
1155
- if (command === "install") {
2165
+ if (command !== "install" && command !== "attach" && workspaceId !== null) {
2166
+ throw new Error("--workspace-id only applies to install/attach");
2167
+ }
2168
+ if (command !== "scan" && packLabel !== null) {
2169
+ throw new Error("--pack-label only applies to scan --live");
2170
+ }
2171
+ if ((command === "install" || command === "attach") && brainAttachmentPolicy !== null) {
2172
+ throw new Error(`${command} uses dedicated by default or --shared for shared mode; --brain-attachment-policy only applies to status/rollback inspection`);
2173
+ }
2174
+ if (command === "install") {
2175
+ if (help) {
2176
+ return {
2177
+ command,
2178
+ openclawHome: "",
2179
+ openclawHomeSource: "explicit",
2180
+ activationRoot: "",
2181
+ activationRootSource: "explicit",
2182
+ shared: false,
2183
+ workspaceId: "",
2184
+ workspaceIdSource: "explicit",
2185
+ skipEmbedderProvision: false,
2186
+ skipEmbedderProvisionSource: null,
2187
+ json,
2188
+ help
2189
+ };
2190
+ }
2191
+ const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
2192
+ const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
2193
+ const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
2194
+ const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
2195
+ return {
2196
+ command,
2197
+ openclawHome: resolvedOpenclawHome.openclawHome,
2198
+ openclawHomeSource: resolvedOpenclawHome.openclawHomeSource,
2199
+ activationRoot: resolvedActivationRoot.activationRoot,
2200
+ activationRootSource: resolvedActivationRoot.source,
2201
+ shared,
2202
+ workspaceId: resolvedWorkspaceId.workspaceId,
2203
+ workspaceIdSource: resolvedWorkspaceId.source,
2204
+ skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
2205
+ skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
2206
+ json,
2207
+ help
2208
+ };
2209
+ }
2210
+ if (command === "attach") {
1156
2211
  if (help) {
1157
2212
  return {
1158
2213
  command,
@@ -1163,22 +2218,30 @@ export function parseOperatorCliArgs(argv) {
1163
2218
  shared: false,
1164
2219
  workspaceId: "",
1165
2220
  workspaceIdSource: "explicit",
2221
+ skipEmbedderProvision: false,
2222
+ skipEmbedderProvisionSource: null,
1166
2223
  json,
1167
2224
  help
1168
2225
  };
1169
2226
  }
1170
- const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
1171
- const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
1172
- const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
2227
+ if (openclawHome === null || openclawHome.trim().length === 0) {
2228
+ throw new Error("--openclaw-home is required for attach; use install for the first-time default path");
2229
+ }
2230
+ const resolvedOpenclawHome = path.resolve(openclawHome);
2231
+ const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome, activationRoot);
2232
+ const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome, workspaceId);
2233
+ const resolvedEmbedderProvisionSkip = resolveInstallEmbedderProvisionSkip(skipEmbedderProvision);
1173
2234
  return {
1174
2235
  command,
1175
- openclawHome: resolvedOpenclawHome.openclawHome,
1176
- openclawHomeSource: resolvedOpenclawHome.openclawHomeSource,
2236
+ openclawHome: resolvedOpenclawHome,
2237
+ openclawHomeSource: "explicit",
1177
2238
  activationRoot: resolvedActivationRoot.activationRoot,
1178
2239
  activationRootSource: resolvedActivationRoot.source,
1179
2240
  shared,
1180
2241
  workspaceId: resolvedWorkspaceId.workspaceId,
1181
2242
  workspaceIdSource: resolvedWorkspaceId.source,
2243
+ skipEmbedderProvision: resolvedEmbedderProvisionSkip.skipEmbedderProvision,
2244
+ skipEmbedderProvisionSource: resolvedEmbedderProvisionSkip.skipEmbedderProvisionSource,
1182
2245
  json,
1183
2246
  help
1184
2247
  };
@@ -1240,30 +2303,6 @@ export function parseOperatorCliArgs(argv) {
1240
2303
  help
1241
2304
  };
1242
2305
  }
1243
- if (command === "attach") {
1244
- if (help) {
1245
- return { command, activationRoot: "", packRoot: "", packLabel: "", workspaceId: "", brainAttachmentPolicy: null, json, help };
1246
- }
1247
- if (activationRoot === null || activationRoot.trim().length === 0) {
1248
- throw new Error("--activation-root is required for attach");
1249
- }
1250
- const resolvedActivationRoot = path.resolve(activationRoot);
1251
- const resolvedPackRoot = packRoot !== null
1252
- ? path.resolve(packRoot)
1253
- : path.resolve(resolvedActivationRoot, "packs", "initial");
1254
- const resolvedWorkspaceId = workspaceId ?? "workspace";
1255
- const resolvedPackLabel = packLabel ?? "cli-attach";
1256
- return {
1257
- command,
1258
- activationRoot: resolvedActivationRoot,
1259
- packRoot: resolvedPackRoot,
1260
- packLabel: resolvedPackLabel,
1261
- workspaceId: resolvedWorkspaceId,
1262
- brainAttachmentPolicy: brainAttachmentPolicy,
1263
- json,
1264
- help
1265
- };
1266
- }
1267
2306
  if (command === "scan") {
1268
2307
  if ((sessionPath === null && livePath === null) || (sessionPath !== null && livePath !== null)) {
1269
2308
  throw new Error("scan requires exactly one of --session or --live");
@@ -1347,6 +2386,7 @@ function resolveExtensionRuntimeGuardPath() {
1347
2386
  path.resolve(__dirname, "..", "..", "extension", "runtime-guard.ts"),
1348
2387
  ];
1349
2388
  const jsCandidates = [
2389
+ path.resolve(__dirname, "..", "dist", "extension", "runtime-guard.js"),
1350
2390
  path.resolve(__dirname, "extension", "runtime-guard.js"),
1351
2391
  path.resolve(__dirname, "..", "extension", "runtime-guard.js"),
1352
2392
  ];
@@ -1473,7 +2513,7 @@ function buildExtensionIndexTs(activationRoot) {
1473
2513
  function buildExtensionPackageJson() {
1474
2514
  const packageMetadata = readOpenClawPackageMetadata();
1475
2515
  return JSON.stringify({
1476
- name: "openclawbrain-extension",
2516
+ name: "openclawbrain",
1477
2517
  version: packageMetadata.version,
1478
2518
  private: true,
1479
2519
  type: "module",
@@ -1576,6 +2616,56 @@ function buildHistoryEntry(record, slot, isActive) {
1576
2616
  current: isActive
1577
2617
  };
1578
2618
  }
2619
+ function ensureLifecycleLearnerService(activationRoot) {
2620
+ const outcome = ensureManagedLearnerServiceForActivationRoot(activationRoot);
2621
+ return {
2622
+ state: outcome.state,
2623
+ detail: outcome.detail,
2624
+ plistPath: outcome.inspection.plistPath,
2625
+ logPath: outcome.inspection.logPath,
2626
+ configuredActivationRoot: outcome.inspection.configuredActivationRoot,
2627
+ matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
2628
+ };
2629
+ }
2630
+ function resolveCleanupLearnerServiceOutcome(activationRoot, openclawHome) {
2631
+ if (activationRoot === null) {
2632
+ return {
2633
+ state: "unresolved",
2634
+ detail: "Learner service preservation is unresolved because the activation root could not be resolved from the installed profile hook.",
2635
+ plistPath: null,
2636
+ logPath: null,
2637
+ configuredActivationRoot: null,
2638
+ matchesRequestedActivationRoot: null
2639
+ };
2640
+ }
2641
+ const remainingProfiles = findOtherInstalledHookReferencesForActivationRoot({
2642
+ activationRoot,
2643
+ excludingOpenClawHome: openclawHome
2644
+ });
2645
+ if (remainingProfiles.length > 0) {
2646
+ const inspection = inspectManagedLearnerService(activationRoot);
2647
+ const attachedProfiles = remainingProfiles
2648
+ .map(({ openclawHome: profileHome }) => shortenPath(path.resolve(profileHome)))
2649
+ .join(", ");
2650
+ return {
2651
+ state: "preserved",
2652
+ detail: `Preserved the background learner service for ${path.resolve(activationRoot)} because other attached OpenClaw profiles still share this activation root: ${attachedProfiles}.`,
2653
+ plistPath: inspection.plistPath,
2654
+ logPath: inspection.logPath,
2655
+ configuredActivationRoot: inspection.configuredActivationRoot,
2656
+ matchesRequestedActivationRoot: inspection.matchesRequestedActivationRoot
2657
+ };
2658
+ }
2659
+ const outcome = removeManagedLearnerServiceForActivationRoot(activationRoot);
2660
+ return {
2661
+ state: outcome.state,
2662
+ detail: outcome.detail,
2663
+ plistPath: outcome.inspection.plistPath,
2664
+ logPath: outcome.inspection.logPath,
2665
+ configuredActivationRoot: outcome.inspection.configuredActivationRoot,
2666
+ matchesRequestedActivationRoot: outcome.inspection.matchesRequestedActivationRoot
2667
+ };
2668
+ }
1579
2669
  function formatInspectionFindings(findings) {
1580
2670
  return findings.join("; ");
1581
2671
  }
@@ -1736,15 +2826,33 @@ function runHistoryCommand(parsed) {
1736
2826
  }
1737
2827
  return 0;
1738
2828
  }
1739
- function runInstallCommand(parsed) {
2829
+ function runProfileHookAttachCommand(parsed) {
1740
2830
  const steps = [];
1741
2831
  const commandLabel = parsed.command.toUpperCase();
1742
- steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
2832
+ const isInstall = parsed.command === "install";
2833
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
2834
+ const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
2835
+ steps.push(`Target OpenClaw home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
2836
+ steps.push(isInstall
2837
+ ? "Lifecycle mode: install is the safe first-time default for wiring one profile to one activation root."
2838
+ : "Lifecycle mode: attach is the explicit reattach/manual profile-hook path; use install for first-time setup.");
2839
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2840
+ steps.push(`Target hook path: ${extensionDir}`);
1743
2841
  // 1. Validate --openclaw-home exists and has openclaw.json
1744
2842
  validateOpenClawHome(parsed.openclawHome);
1745
2843
  // 2. Inspect the activation root before writing profile hook artifacts.
1746
2844
  const activationPlan = inspectInstallActivationPlan(parsed);
1747
- // 3. Create activation root if needed
2845
+ // 3. Ensure the default embedder exists before bootstrap unless the operator explicitly opts out.
2846
+ const embedderProvision = activationPlan.action === "bootstrap"
2847
+ ? ensureInstallEmbedderReady(parsed)
2848
+ : null;
2849
+ if (embedderProvision === null) {
2850
+ steps.push("Skipped bootstrap-time embedder provisioning because attach/install is reusing healthy activation state.");
2851
+ }
2852
+ else {
2853
+ steps.push(embedderProvision.detail);
2854
+ }
2855
+ // 4. Create activation root if needed
1748
2856
  if (activationPlan.createActivationRoot) {
1749
2857
  mkdirSync(parsed.activationRoot, { recursive: true });
1750
2858
  steps.push(`Created activation root: ${parsed.activationRoot}`);
@@ -1753,7 +2861,17 @@ function runInstallCommand(parsed) {
1753
2861
  steps.push(`Activation root exists: ${parsed.activationRoot}`);
1754
2862
  }
1755
2863
  steps.push(activationPlan.inspectionStep);
1756
- // 4. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
2864
+ // 5. Persist install-written local provider defaults so watch/learning surfaces do not depend on gateway env wiring.
2865
+ const providerDefaults = shouldWriteProfileHookProviderDefaults(parsed, activationPlan, isInstall)
2866
+ ? writeInstallProviderDefaults(parsed)
2867
+ : null;
2868
+ if (providerDefaults === null) {
2869
+ steps.push("Preserved existing provider-defaults.json because explicit attach is reusing existing activation data.");
2870
+ }
2871
+ else {
2872
+ steps.push(providerDefaults.detail);
2873
+ }
2874
+ // 6. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
1757
2875
  if (activationPlan.action === "bootstrap") {
1758
2876
  const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
1759
2877
  mkdirSync(packRoot, { recursive: true });
@@ -1763,13 +2881,13 @@ function runInstallCommand(parsed) {
1763
2881
  brainAttachmentPolicy,
1764
2882
  activationRoot: parsed.activationRoot,
1765
2883
  packRoot,
1766
- packLabel: "install-cli",
2884
+ packLabel: isInstall ? "install-cli" : "attach-cli",
1767
2885
  workspace: {
1768
2886
  workspaceId: parsed.workspaceId,
1769
- snapshotId: `${parsed.workspaceId}@install-${new Date().toISOString().slice(0, 10)}`,
2887
+ snapshotId: `${parsed.workspaceId}@${parsed.command}-${new Date().toISOString().slice(0, 10)}`,
1770
2888
  capturedAt: new Date().toISOString(),
1771
2889
  rootDir: parsed.openclawHome,
1772
- revision: "cli-install-v1"
2890
+ revision: isInstall ? "cli-install-v1" : "cli-attach-v1"
1773
2891
  },
1774
2892
  interactionEvents: [],
1775
2893
  feedbackEvents: []
@@ -1777,10 +2895,11 @@ function runInstallCommand(parsed) {
1777
2895
  steps.push(`Bootstrapped brain attach: state=${result.currentProfile.brain.state} awaitingFirstExport=${yesNo(result.currentProfile.brainStatus.awaitingFirstExport)}`);
1778
2896
  }
1779
2897
  else {
1780
- steps.push(`Kept inspected activation state: active pack ${activationPlan.activePackId}`);
2898
+ steps.push(isInstall
2899
+ ? `Kept inspected activation state: active pack ${activationPlan.activePackId}`
2900
+ : `Reused inspected activation state for explicit attach: active pack ${activationPlan.activePackId}`);
1781
2901
  }
1782
- // 5-8. Write extension files
1783
- const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
2902
+ // 7-10. Write extension files
1784
2903
  mkdirSync(extensionDir, { recursive: true });
1785
2904
  // 5. Write index.ts
1786
2905
  const indexTsPath = path.join(extensionDir, "index.ts");
@@ -1833,92 +2952,67 @@ function runInstallCommand(parsed) {
1833
2952
  const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
1834
2953
  writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
1835
2954
  steps.push(`Wrote manifest: ${manifestPath}`);
1836
- // 9. Write BRAIN.md to workspace directories
1837
- const brainMdContent = [
1838
- "## OpenClawBrain",
1839
- `You have a learning brain attached at ${parsed.activationRoot}.`,
1840
- "- It learns automatically from your conversations",
1841
- '- Corrections matter — "no, actually X" teaches the brain X',
1842
- "- You don't manage it — background daemon handles learning",
1843
- `- Check: \`openclawbrain status --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
1844
- `- Rollback: \`openclawbrain rollback --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
1845
- '- See what brain knows: `openclawbrain context "your question"`',
1846
- ""
1847
- ].join("\n");
1848
- const agentsMdBrainRef = "\n5. Read `BRAIN.md` — your learning brain context\n";
1849
- try {
1850
- const entries = readdirSync(parsed.openclawHome, { withFileTypes: true });
1851
- const workspaceDirs = entries
1852
- .filter(e => e.isDirectory() && e.name.startsWith("workspace-"))
1853
- .map(e => path.join(parsed.openclawHome, e.name));
1854
- // If no workspace-* dirs found, check if openclawHome itself is a workspace
1855
- if (workspaceDirs.length === 0) {
1856
- workspaceDirs.push(parsed.openclawHome);
1857
- }
1858
- for (const wsDir of workspaceDirs) {
1859
- const brainMdPath = path.join(wsDir, "BRAIN.md");
1860
- writeFileSync(brainMdPath, brainMdContent, "utf8");
1861
- steps.push(`Wrote BRAIN.md: ${brainMdPath}`);
1862
- // If AGENTS.md exists, append brain reference to startup sequence
1863
- const agentsMdPath = path.join(wsDir, "AGENTS.md");
1864
- if (existsSync(agentsMdPath)) {
1865
- const agentsContent = readFileSync(agentsMdPath, "utf8");
1866
- if (!agentsContent.includes("BRAIN.md")) {
1867
- // Find the startup sequence section and append after the last numbered item
1868
- const startupMarker = "## Session Startup";
1869
- if (agentsContent.includes(startupMarker)) {
1870
- // Find the numbered list in the startup section and append after last item
1871
- const lines = agentsContent.split("\n");
1872
- let lastNumberedIdx = -1;
1873
- let inStartup = false;
1874
- for (let i = 0; i < lines.length; i++) {
1875
- const line = lines[i] ?? "";
1876
- if (line.includes(startupMarker)) {
1877
- inStartup = true;
1878
- continue;
1879
- }
1880
- if (inStartup && /^\d+\.\s/.test(line.trim())) {
1881
- lastNumberedIdx = i;
1882
- }
1883
- if (inStartup && line.startsWith("## ") && !line.includes(startupMarker)) {
1884
- break;
1885
- }
1886
- }
1887
- if (lastNumberedIdx >= 0) {
1888
- lines.splice(lastNumberedIdx + 1, 0, agentsMdBrainRef.trimEnd());
1889
- writeFileSync(agentsMdPath, lines.join("\n"), "utf8");
1890
- steps.push(`Updated AGENTS.md startup sequence: ${agentsMdPath}`);
1891
- }
1892
- else {
1893
- appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
1894
- steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
1895
- }
1896
- }
1897
- else {
1898
- appendFileSync(agentsMdPath, agentsMdBrainRef, "utf8");
1899
- steps.push(`Appended BRAIN.md reference to AGENTS.md: ${agentsMdPath}`);
1900
- }
1901
- }
1902
- else {
1903
- steps.push(`AGENTS.md already references BRAIN.md: ${agentsMdPath}`);
1904
- }
1905
- }
1906
- }
1907
- }
1908
- catch (err) {
1909
- const message = err instanceof Error ? err.message : String(err);
1910
- steps.push(`BRAIN.md generation failed (non-fatal): ${message}`);
1911
- }
1912
- const restartGuidance = buildInstallReloadGuidance();
2955
+ const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
2956
+ steps.push(learnerService.detail);
2957
+ const brainFeedback = buildInstallBrainFeedbackSummary({
2958
+ parsed,
2959
+ targetInspection,
2960
+ extensionDir,
2961
+ activationPlan,
2962
+ learnerService,
2963
+ embedderProvision,
2964
+ providerDefaults
2965
+ });
2966
+ const restartGuidance = buildInstallReloadGuidance({
2967
+ targetInspection
2968
+ });
1913
2969
  const nextSteps = [
1914
2970
  restartGuidance,
1915
- `Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`
2971
+ brainFeedback.restart.gatewayStatusCommand === null
2972
+ ? null
2973
+ : `Confirm gateway after restart: ${brainFeedback.restart.gatewayStatusCommand}`,
2974
+ `Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`,
2975
+ `Check learner service: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`,
2976
+ embedderProvision !== null && embedderProvision.state === "skipped"
2977
+ ? `Provision default embedder later: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`
2978
+ : null
2979
+ ].filter((step) => step !== null);
2980
+ const preflightSummary = [
2981
+ `Hook: installed at ${shortenPath(extensionDir)}`,
2982
+ parsed.shared
2983
+ ? "Attachment policy: shared activation root declared; same-gateway many-profile load/serve proof is still not checked in"
2984
+ : "Attachment policy: dedicated activation root for this profile/home boundary",
2985
+ activationPlan.action === "bootstrap"
2986
+ ? "Attachment: seed/current-profile attach created; restart plus status will prove later serve-path use"
2987
+ : `Attachment: existing active pack ${activationPlan.activePackId} kept in place; restart plus status will prove later serve-path use`,
2988
+ embedderProvision === null
2989
+ ? "Embedder: unchanged because no bootstrap was needed"
2990
+ : embedderProvision.state === "ensured"
2991
+ ? `Embedder: default Ollama model ${embedderProvision.model} was ensured before bootstrap`
2992
+ : `Embedder: default Ollama model ${embedderProvision.model} was intentionally skipped`,
2993
+ `Learner: background service ${learnerService.state} for the exact activation root/profile boundary`,
2994
+ `Serve path: install alone does not prove serving; restart the profile and run ${buildInstallStatusCommand(parsed.activationRoot)}`
1916
2995
  ];
1917
2996
  const lifecycleSummary = [
1918
- `OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
2997
+ isInstall
2998
+ ? "Lifecycle mode: install (safe first-time/default profile hookup)"
2999
+ : "Lifecycle mode: attach (explicit reattach/manual profile hookup)",
3000
+ `OpenClaw target: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
3001
+ `Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`,
3002
+ brainFeedback.profile.exactProfileName === null
3003
+ ? "Profile token: current_profile only; this install did not infer an exact --profile token"
3004
+ : `Profile token: use exact OpenClaw profile casing ${quoteShellArg(brainFeedback.profile.exactProfileName)} for host-side restart/status commands`,
1919
3005
  `Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
3006
+ `Attachment policy: ${brainFeedback.attachment.policy} (${brainFeedback.attachment.detail})`,
1920
3007
  `Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
3008
+ embedderProvision === null
3009
+ ? "Embedder: unchanged because no bootstrap was needed"
3010
+ : embedderProvision.state === "ensured"
3011
+ ? `Embedder: ensured default Ollama model ${embedderProvision.model} before brain init`
3012
+ : `Embedder: skipped default Ollama model ${embedderProvision.model} via ${parsed.skipEmbedderProvisionSource === "flag" ? "--skip-embedder-provision" : OPENCLAWBRAIN_INSTALL_SKIP_EMBEDDER_PROVISION_ENV}`,
3013
+ ...(providerDefaults === null ? [] : [`${providerDefaults.lifecycleSummary} (${shortenPath(providerDefaults.path)})`]),
1921
3014
  `Profile hook: installed at ${shortenPath(extensionDir)}`,
3015
+ `Learner service: ${learnerService.state} for ${shortenPath(parsed.activationRoot)}`,
1922
3016
  activationPlan.resolution === "new_root"
1923
3017
  ? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
1924
3018
  : activationPlan.resolution === "missing_pointers"
@@ -1928,11 +3022,13 @@ function runInstallCommand(parsed) {
1928
3022
  : `Activation data: reused healthy state at ${shortenPath(parsed.activationRoot)}`,
1929
3023
  activationPlan.action === "bootstrap"
1930
3024
  ? activationPlan.resolution === "new_root"
1931
- ? "Brain attach: bootstrapped a seed/current-profile attach"
3025
+ ? `${isInstall ? "Install" : "Attach"}: bootstrapped a seed/current-profile brain`
1932
3026
  : activationPlan.resolution === "missing_pointers"
1933
- ? "Brain attach: repaired missing activation pointers and bootstrapped a seed/current-profile attach"
1934
- : "Brain attach: repaired empty activation pointers and bootstrapped a seed/current-profile attach"
1935
- : `Brain attach: kept healthy active pack ${activationPlan.activePackId} in place`
3027
+ ? `${isInstall ? "Install" : "Attach"}: repaired missing activation pointers and bootstrapped a seed/current-profile brain`
3028
+ : `${isInstall ? "Install" : "Attach"}: repaired empty activation pointers and bootstrapped a seed/current-profile brain`
3029
+ : isInstall
3030
+ ? `Install: kept healthy active pack ${activationPlan.activePackId} in place`
3031
+ : `Attach: rewired the profile hook to healthy active pack ${activationPlan.activePackId}`
1936
3032
  ];
1937
3033
  // 9. Print summary
1938
3034
  if (parsed.json) {
@@ -1940,6 +3036,13 @@ function runInstallCommand(parsed) {
1940
3036
  command: parsed.command,
1941
3037
  openclawHome: parsed.openclawHome,
1942
3038
  openclawHomeSource: parsed.openclawHomeSource,
3039
+ openclawTarget: {
3040
+ layout: targetInspection.layout,
3041
+ detail: describeOpenClawHomeInspection(targetInspection),
3042
+ profileId: targetInspection.profileId,
3043
+ profileSource: targetInspection.profileSource,
3044
+ configuredProfileIds: targetInspection.configuredProfileIds
3045
+ },
1943
3046
  activationRoot: parsed.activationRoot,
1944
3047
  resolvedInputs: {
1945
3048
  activationRoot: {
@@ -1953,8 +3056,52 @@ function runInstallCommand(parsed) {
1953
3056
  },
1954
3057
  workspaceId: parsed.workspaceId,
1955
3058
  shared: parsed.shared,
3059
+ embedderProvision: embedderProvision === null
3060
+ ? null
3061
+ : {
3062
+ skipped: parsed.skipEmbedderProvision,
3063
+ source: parsed.skipEmbedderProvisionSource,
3064
+ model: embedderProvision.model,
3065
+ baseUrl: embedderProvision.baseUrl
3066
+ },
3067
+ providerDefaults: providerDefaults === null
3068
+ ? null
3069
+ : {
3070
+ path: providerDefaults.path,
3071
+ teacher: providerDefaults.defaults.teacher === undefined
3072
+ ? null
3073
+ : {
3074
+ provider: providerDefaults.defaults.teacher.provider ?? null,
3075
+ model: providerDefaults.defaults.teacher.model ?? null,
3076
+ detectedLocally: providerDefaults.defaults.teacher.detectedLocally ?? false
3077
+ },
3078
+ embedder: providerDefaults.defaults.embedder === undefined
3079
+ ? null
3080
+ : {
3081
+ provider: providerDefaults.defaults.embedder.provider ?? null,
3082
+ model: providerDefaults.defaults.embedder.model ?? null
3083
+ },
3084
+ teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
3085
+ embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
3086
+ },
3087
+ learnerService,
3088
+ brainFeedback: {
3089
+ hookPath: brainFeedback.hookPath,
3090
+ providerDefaultsPath: brainFeedback.providerDefaultsPath,
3091
+ profile: brainFeedback.profile,
3092
+ attachment: brainFeedback.attachment,
3093
+ restart: brainFeedback.restart,
3094
+ embedder: brainFeedback.embedder,
3095
+ teacher: brainFeedback.teacher,
3096
+ learnerService: brainFeedback.learnerService,
3097
+ startup: brainFeedback.startup,
3098
+ provedNow: brainFeedback.provedNow,
3099
+ notYetProved: brainFeedback.notYetProved,
3100
+ lines: brainFeedback.lines
3101
+ },
1956
3102
  extensionDir,
1957
3103
  lifecycleSummary,
3104
+ preflightSummary,
1958
3105
  restartGuidance,
1959
3106
  nextSteps,
1960
3107
  steps
@@ -1962,19 +3109,28 @@ function runInstallCommand(parsed) {
1962
3109
  }
1963
3110
  else {
1964
3111
  console.log(`${commandLabel} complete\n`);
1965
- for (const step of steps) {
1966
- console.log(` ✓ ${step}`);
1967
- }
1968
- console.log("");
1969
- console.log("Lifecycle:");
1970
- for (const line of lifecycleSummary) {
3112
+ console.log("Brain feedback:");
3113
+ for (const line of brainFeedback.lines) {
1971
3114
  console.log(` ${line}`);
1972
3115
  }
1973
- console.log(`Next: ${restartGuidance}`);
3116
+ console.log(`Restart: ${restartGuidance}`);
3117
+ if (brainFeedback.restart.gatewayStatusCommand !== null) {
3118
+ console.log(`Gateway: Confirm OpenClaw after restart: ${brainFeedback.restart.gatewayStatusCommand}`);
3119
+ }
1974
3120
  console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
3121
+ console.log(`Learner: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`);
3122
+ if (embedderProvision !== null && embedderProvision.state === "skipped") {
3123
+ console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
3124
+ }
1975
3125
  }
1976
3126
  return 0;
1977
3127
  }
3128
+ function runInstallCommand(parsed) {
3129
+ return runProfileHookAttachCommand(parsed);
3130
+ }
3131
+ function runAttachCommand(parsed) {
3132
+ return runProfileHookAttachCommand(parsed);
3133
+ }
1978
3134
  function validateOpenClawHome(openclawHome) {
1979
3135
  if (!existsSync(openclawHome)) {
1980
3136
  throw new Error(`--openclaw-home directory does not exist: ${openclawHome}`);
@@ -1984,6 +3140,85 @@ function validateOpenClawHome(openclawHome) {
1984
3140
  throw new Error(`openclaw.json not found in ${openclawHome}`);
1985
3141
  }
1986
3142
  }
3143
+ function readJsonObjectRecord(value) {
3144
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3145
+ return null;
3146
+ }
3147
+ return value;
3148
+ }
3149
+ function readOpenClawJsonConfig(openclawHome) {
3150
+ const openclawJsonPath = path.join(openclawHome, "openclaw.json");
3151
+ let parsed;
3152
+ try {
3153
+ parsed = JSON.parse(readFileSync(openclawJsonPath, "utf8"));
3154
+ }
3155
+ catch (error) {
3156
+ throw new Error(`Failed to read ${openclawJsonPath}: ${toErrorMessage(error)}`);
3157
+ }
3158
+ const config = readJsonObjectRecord(parsed);
3159
+ if (config === null) {
3160
+ throw new Error(`Failed to read ${openclawJsonPath}: openclaw.json must contain a top-level object`);
3161
+ }
3162
+ return {
3163
+ path: openclawJsonPath,
3164
+ config
3165
+ };
3166
+ }
3167
+ function scrubOpenClawBrainPluginConfig(openclawHome) {
3168
+ const { path: openclawJsonPath, config } = readOpenClawJsonConfig(openclawHome);
3169
+ const plugins = readJsonObjectRecord(config.plugins);
3170
+ if (plugins === null) {
3171
+ return {
3172
+ path: openclawJsonPath,
3173
+ changed: false,
3174
+ detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
3175
+ };
3176
+ }
3177
+ const changes = [];
3178
+ let changed = false;
3179
+ if (Array.isArray(plugins.allow)) {
3180
+ const filteredAllow = plugins.allow.filter((entry) => entry !== "openclawbrain");
3181
+ if (filteredAllow.length !== plugins.allow.length) {
3182
+ changed = true;
3183
+ changes.push("removed plugins.allow entry");
3184
+ if (filteredAllow.length > 0) {
3185
+ plugins.allow = filteredAllow;
3186
+ }
3187
+ else {
3188
+ delete plugins.allow;
3189
+ }
3190
+ }
3191
+ }
3192
+ const entries = readJsonObjectRecord(plugins.entries);
3193
+ if (entries !== null && Object.prototype.hasOwnProperty.call(entries, "openclawbrain")) {
3194
+ delete entries.openclawbrain;
3195
+ changed = true;
3196
+ changes.push("removed plugins.entries.openclawbrain");
3197
+ }
3198
+ if (entries !== null && Object.keys(entries).length === 0 && Object.prototype.hasOwnProperty.call(plugins, "entries")) {
3199
+ delete plugins.entries;
3200
+ changed = true;
3201
+ changes.push("removed empty plugins.entries container");
3202
+ }
3203
+ if (Object.keys(plugins).length === 0 && Object.prototype.hasOwnProperty.call(config, "plugins")) {
3204
+ delete config.plugins;
3205
+ changed = true;
3206
+ changes.push("removed empty plugins container");
3207
+ }
3208
+ if (changed) {
3209
+ writeFileSync(openclawJsonPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3210
+ return {
3211
+ path: openclawJsonPath,
3212
+ changed: true,
3213
+ detail: `Scrubbed stale openclawbrain plugin config in ${openclawJsonPath}: ${changes.join(", ")}`
3214
+ };
3215
+ }
3216
+ return {
3217
+ path: openclawJsonPath,
3218
+ changed: false,
3219
+ detail: `No stale openclawbrain plugin config found in ${openclawJsonPath}`
3220
+ };
3221
+ }
1987
3222
  function resolveCleanupActivationRoot(openclawHome, explicitActivationRoot) {
1988
3223
  if (explicitActivationRoot !== null) {
1989
3224
  return path.resolve(explicitActivationRoot);
@@ -2024,25 +3259,50 @@ function buildRestartGuidance(restart) {
2024
3259
  function runDetachCommand(parsed) {
2025
3260
  const steps = [];
2026
3261
  validateOpenClawHome(parsed.openclawHome);
3262
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
3263
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2027
3264
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3265
+ const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3266
+ const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
2028
3267
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
3268
+ const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2029
3269
  const activationData = summarizeKeptActivationData(activationRoot);
2030
3270
  const restartGuidance = buildRestartGuidance(parsed.restart);
2031
3271
  const nextSteps = [
2032
3272
  restartGuidance,
2033
3273
  activationRoot === null ? null : `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}`,
2034
- `Reattach later: ${buildInstallCommand(parsed.openclawHome)}`
3274
+ activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
3275
+ `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
2035
3276
  ].filter((step) => step !== null);
3277
+ steps.push(pluginConfigCleanup.detail);
3278
+ if (legacyResidue.removedNotes.length > 0) {
3279
+ steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
3280
+ }
3281
+ if (legacyResidue.updatedAgents.length > 0) {
3282
+ steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
3283
+ }
3284
+ steps.push(learnerService.detail);
2036
3285
  steps.push(activationData.activationDataDetail);
2037
3286
  steps.push("Detach only removes the OpenClaw profile hook; it does not delete OpenClawBrain data.");
2038
3287
  if (parsed.json) {
2039
3288
  console.log(JSON.stringify({
2040
3289
  command: "detach",
2041
3290
  openclawHome: parsed.openclawHome,
3291
+ openclawTarget: {
3292
+ layout: targetInspection.layout,
3293
+ detail: describeOpenClawHomeInspection(targetInspection),
3294
+ profileId: targetInspection.profileId,
3295
+ profileSource: targetInspection.profileSource,
3296
+ configuredProfileIds: targetInspection.configuredProfileIds
3297
+ },
2042
3298
  extensionDir,
2043
3299
  activationRoot,
2044
3300
  dataAction: "kept",
2045
3301
  activationDataState: activationData.activationDataState,
3302
+ pluginConfigCleanup,
3303
+ learnerService,
3304
+ removedLegacyNotes: legacyResidue.removedNotes,
3305
+ updatedAgents: legacyResidue.updatedAgents,
2046
3306
  restartMode: parsed.restart,
2047
3307
  restartGuidance,
2048
3308
  nextSteps,
@@ -2055,26 +3315,47 @@ function runDetachCommand(parsed) {
2055
3315
  console.log(` ✓ ${step}`);
2056
3316
  }
2057
3317
  console.log("");
2058
- console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
3318
+ console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} is detached from the brain hook.`);
3319
+ console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
2059
3320
  if (activationRoot !== null) {
2060
3321
  console.log(`Brain data: ${shortenPath(activationRoot)} remains available for inspection or reattach.`);
2061
3322
  }
2062
3323
  else {
2063
3324
  console.log("Brain data: preserved, but the activation root could not be resolved from the removed hook.");
2064
3325
  }
3326
+ console.log(`Config: ${pluginConfigCleanup.detail}`);
3327
+ console.log(`Learner: ${learnerService.detail}`);
2065
3328
  console.log(`Next: ${restartGuidance}`);
2066
3329
  if (activationRoot !== null) {
2067
3330
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
3331
+ console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
2068
3332
  }
2069
- console.log(`Reattach: ${buildInstallCommand(parsed.openclawHome)}`);
3333
+ console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
2070
3334
  }
2071
3335
  return 0;
2072
3336
  }
2073
3337
  function runUninstallCommand(parsed) {
2074
3338
  const steps = [];
2075
3339
  validateOpenClawHome(parsed.openclawHome);
3340
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
3341
+ steps.push(`Detected layout: ${formatOpenClawTargetExplanation(targetInspection)}`);
2076
3342
  const activationRoot = resolveCleanupActivationRoot(parsed.openclawHome, parsed.activationRoot);
3343
+ if (parsed.dataMode === "purge" && activationRoot !== null) {
3344
+ assertActivationRootPurgeIsNotShared({
3345
+ activationRoot,
3346
+ openclawHome: parsed.openclawHome
3347
+ });
3348
+ }
3349
+ const learnerService = resolveCleanupLearnerServiceOutcome(activationRoot, parsed.openclawHome);
3350
+ const pluginConfigCleanup = scrubOpenClawBrainPluginConfig(parsed.openclawHome);
3351
+ if (parsed.dataMode === "purge" &&
3352
+ activationRoot !== null &&
3353
+ learnerService.state === "preserved" &&
3354
+ learnerService.matchesRequestedActivationRoot !== false) {
3355
+ throw new Error(`Refusing to purge activation root ${path.resolve(activationRoot)} because the background learner service for this exact root could not be removed. ${learnerService.detail}`);
3356
+ }
2077
3357
  const extensionDir = removeProfileHookup(parsed.openclawHome, steps);
3358
+ const legacyResidue = removeLegacyProfileResidue(parsed.openclawHome);
2078
3359
  let activationData;
2079
3360
  if (parsed.dataMode === "purge") {
2080
3361
  if (activationRoot === null) {
@@ -2103,8 +3384,19 @@ function runUninstallCommand(parsed) {
2103
3384
  const nextSteps = [
2104
3385
  restartGuidance,
2105
3386
  parsed.dataMode === "keep" && activationRoot !== null ? `Inspect preserved data: ${buildInstallStatusCommand(activationRoot)}` : null,
2106
- `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
3387
+ activationRoot === null ? null : `Inspect learner service: ${buildLearnerServiceStatusCommand(activationRoot)}`,
3388
+ parsed.dataMode === "keep"
3389
+ ? `Reattach later: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`
3390
+ : `Reinstall later: ${buildInstallCommand(parsed.openclawHome)}`
2107
3391
  ].filter((step) => step !== null);
3392
+ steps.push(pluginConfigCleanup.detail);
3393
+ if (legacyResidue.removedNotes.length > 0) {
3394
+ steps.push(`Removed legacy profile notes: ${legacyResidue.removedNotes.map((notePath) => shortenPath(notePath)).join(", ")}`);
3395
+ }
3396
+ if (legacyResidue.updatedAgents.length > 0) {
3397
+ steps.push(`Removed legacy AGENTS.md brain references: ${legacyResidue.updatedAgents.map((agentsPath) => shortenPath(agentsPath)).join(", ")}`);
3398
+ }
3399
+ steps.push(learnerService.detail);
2108
3400
  steps.push(activationData.activationDataDetail);
2109
3401
  steps.push(parsed.dataMode === "purge"
2110
3402
  ? "Uninstall removed the OpenClaw profile hook and activation data."
@@ -2113,10 +3405,21 @@ function runUninstallCommand(parsed) {
2113
3405
  console.log(JSON.stringify({
2114
3406
  command: "uninstall",
2115
3407
  openclawHome: parsed.openclawHome,
3408
+ openclawTarget: {
3409
+ layout: targetInspection.layout,
3410
+ detail: describeOpenClawHomeInspection(targetInspection),
3411
+ profileId: targetInspection.profileId,
3412
+ profileSource: targetInspection.profileSource,
3413
+ configuredProfileIds: targetInspection.configuredProfileIds
3414
+ },
2116
3415
  extensionDir,
2117
3416
  activationRoot,
2118
3417
  dataAction: parsed.dataMode,
2119
3418
  activationDataState: activationData.activationDataState,
3419
+ pluginConfigCleanup,
3420
+ learnerService,
3421
+ removedLegacyNotes: legacyResidue.removedNotes,
3422
+ updatedAgents: legacyResidue.updatedAgents,
2120
3423
  restartMode: parsed.restart,
2121
3424
  restartGuidance,
2122
3425
  nextSteps,
@@ -2129,16 +3432,27 @@ function runUninstallCommand(parsed) {
2129
3432
  console.log(` ✓ ${step}`);
2130
3433
  }
2131
3434
  console.log("");
2132
- console.log(`Lifecycle: OpenClaw profile ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
3435
+ console.log(`Lifecycle: OpenClaw home ${shortenPath(parsed.openclawHome)} no longer has the brain hook installed.`);
3436
+ console.log(`Target: ${formatOpenClawTargetExplanation(targetInspection)}`);
2133
3437
  console.log(`Data mode: ${parsed.dataMode === "purge" ? "purged" : "kept"}`);
2134
3438
  if (activationRoot !== null) {
2135
3439
  console.log(`Activation: ${parsed.dataMode === "purge" ? shortenPath(activationRoot) : `${shortenPath(activationRoot)} preserved`}`);
2136
3440
  }
3441
+ console.log(`Config: ${pluginConfigCleanup.detail}`);
3442
+ console.log(`Learner: ${learnerService.detail}`);
2137
3443
  console.log(`Next: ${restartGuidance}`);
2138
3444
  if (parsed.dataMode === "keep" && activationRoot !== null) {
2139
3445
  console.log(`Check: ${buildInstallStatusCommand(activationRoot)}`);
2140
3446
  }
2141
- console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
3447
+ if (activationRoot !== null) {
3448
+ console.log(`Service: ${buildLearnerServiceStatusCommand(activationRoot)}`);
3449
+ }
3450
+ if (parsed.dataMode === "keep") {
3451
+ console.log(`Reattach: ${buildAttachCommand(parsed.openclawHome, activationRoot)}`);
3452
+ }
3453
+ else {
3454
+ console.log(`Reinstall: ${buildInstallCommand(parsed.openclawHome)}`);
3455
+ }
2142
3456
  }
2143
3457
  return 0;
2144
3458
  }
@@ -2704,13 +4018,62 @@ function exportLocalSessionTailChangesToScanRoot(input) {
2704
4018
  warnings
2705
4019
  };
2706
4020
  }
2707
- function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId) {
2708
- const materialization = snapshot?.learner?.lastMaterialization ?? null;
4021
+ function summarizeVectorEmbeddingState(vectors) {
4022
+ if (vectors === null || vectors === undefined) {
4023
+ return {
4024
+ vectorEntryCount: null,
4025
+ numericEmbeddingEntryCount: null,
4026
+ embeddingModels: []
4027
+ };
4028
+ }
4029
+ const embeddingModels = [...new Set(vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
4030
+ return {
4031
+ vectorEntryCount: vectors.entries.length,
4032
+ numericEmbeddingEntryCount: vectors.entries.filter((entry) => entry.embedding !== undefined).length,
4033
+ embeddingModels
4034
+ };
4035
+ }
4036
+ function buildWatchEmbedTracePoint(input) {
4037
+ const summary = summarizeVectorEmbeddingState(input.vectors);
4038
+ return {
4039
+ slot: input.slot,
4040
+ packId: input.packId,
4041
+ runtimeEmbedderPresent: input.embedder !== null,
4042
+ runtimeEmbedderModel: input.embedder?.model ?? null,
4043
+ vectorEntryCount: summary.vectorEntryCount,
4044
+ numericEmbeddingEntryCount: summary.numericEmbeddingEntryCount,
4045
+ embeddingModels: summary.embeddingModels,
4046
+ error: input.error ?? null
4047
+ };
4048
+ }
4049
+ function buildWatchEmbedTracePointFromPack(input) {
4050
+ return buildWatchEmbedTracePoint({
4051
+ slot: input.slot,
4052
+ packId: input.pack?.manifest.packId ?? null,
4053
+ embedder: input.embedder,
4054
+ vectors: input.pack?.vectors,
4055
+ error: input.error ?? null
4056
+ });
4057
+ }
4058
+ function formatWatchEmbedTracePoint(label, point) {
4059
+ const models = point.embeddingModels.length === 0 ? "none" : point.embeddingModels.join("|");
4060
+ const slot = point.slot ?? "build";
4061
+ const packId = point.packId ?? "unknown";
4062
+ const embedderState = point.runtimeEmbedderPresent ? `present:${point.runtimeEmbedderModel ?? "unknown"}` : "null";
4063
+ const counts = point.vectorEntryCount === null || point.numericEmbeddingEntryCount === null
4064
+ ? "vectors=unknown numeric=unknown"
4065
+ : `vectors=${point.vectorEntryCount} numeric=${point.numericEmbeddingEntryCount}`;
4066
+ const error = point.error === null ? "" : ` error=${point.error}`;
4067
+ return `embed-trace ${label} slot=${slot} pack=${packId} runtimeEmbedder=${embedderState} ${counts} models=${models}${error}`;
4068
+ }
4069
+ async function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterializationPackId, embedder, log) {
4070
+ let materialization = snapshot?.learner?.lastMaterialization ?? null;
2709
4071
  if (materialization === null) {
2710
4072
  return {
2711
4073
  lastHandledMaterializationPackId,
2712
4074
  logLine: null,
2713
4075
  materializedPackId: null,
4076
+ embedInstrumentation: null,
2714
4077
  failure: null
2715
4078
  };
2716
4079
  }
@@ -2722,10 +4085,38 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2722
4085
  lastHandledMaterializationPackId,
2723
4086
  logLine: null,
2724
4087
  materializedPackId: packId,
4088
+ embedInstrumentation: null,
2725
4089
  failure: null
2726
4090
  };
2727
4091
  }
4092
+ if (embedder !== null) {
4093
+ materialization = {
4094
+ ...materialization,
4095
+ candidate: await reindexCandidatePackBuildResultWithEmbedder(materialization.candidate, embedder)
4096
+ };
4097
+ if (snapshot?.learner !== undefined && snapshot.learner !== null) {
4098
+ snapshot.learner.lastMaterialization = materialization;
4099
+ }
4100
+ }
2728
4101
  const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
4102
+ const observedAt = new Date().toISOString();
4103
+ const beforeCandidateMaterialization = buildWatchEmbedTracePoint({
4104
+ slot: null,
4105
+ packId,
4106
+ embedder,
4107
+ vectors: materialization?.candidate?.payloads?.vectors
4108
+ });
4109
+ let embedInstrumentation = {
4110
+ observedAt,
4111
+ candidatePackId: packId,
4112
+ promotionAllowed: null,
4113
+ promotionFindings: [],
4114
+ beforeCandidateMaterialization,
4115
+ afterCandidateMaterialization: null,
4116
+ afterStage: null,
4117
+ afterPromote: null
4118
+ };
4119
+ log?.(formatWatchEmbedTracePoint("before_materialize", beforeCandidateMaterialization));
2729
4120
  try {
2730
4121
  const candidateRootDir = path.resolve(activationRoot, "packs", packId);
2731
4122
  mkdirSync(candidateRootDir, { recursive: true });
@@ -2737,27 +4128,81 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2737
4128
  activeBeforePack = null;
2738
4129
  }
2739
4130
  const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
4131
+ embedInstrumentation = {
4132
+ ...embedInstrumentation,
4133
+ afterCandidateMaterialization: buildWatchEmbedTracePointFromPack({
4134
+ slot: "candidate",
4135
+ pack: candidateDescriptor,
4136
+ embedder
4137
+ })
4138
+ };
4139
+ if (embedInstrumentation.afterCandidateMaterialization !== null) {
4140
+ log?.(formatWatchEmbedTracePoint("after_materialize", embedInstrumentation.afterCandidateMaterialization));
4141
+ }
2740
4142
  appendLearningUpdateLogs({
2741
4143
  activationRoot,
2742
4144
  materialization,
2743
4145
  activeBeforePack,
2744
4146
  candidateDescriptor
2745
4147
  });
2746
- const now = new Date().toISOString();
4148
+ const now = observedAt;
2747
4149
  stageCandidatePack(activationRoot, candidateRootDir, {
2748
4150
  updatedAt: now,
2749
4151
  reason: `watch_stage:${materialization.reason}:${materialization.lane}`
2750
4152
  });
2751
4153
  const inspection = inspectActivationState(activationRoot, now);
4154
+ let stagedPack = null;
4155
+ let stagedPackError = null;
4156
+ try {
4157
+ stagedPack = loadPackFromActivation(activationRoot, "candidate", { requireActivationReady: true });
4158
+ }
4159
+ catch (error) {
4160
+ stagedPackError = formatWatchError(error);
4161
+ }
4162
+ embedInstrumentation = {
4163
+ ...embedInstrumentation,
4164
+ promotionAllowed: inspection.promotion.allowed,
4165
+ promotionFindings: [...inspection.promotion.findings],
4166
+ afterStage: buildWatchEmbedTracePointFromPack({
4167
+ slot: "candidate",
4168
+ pack: stagedPack,
4169
+ embedder,
4170
+ error: stagedPackError
4171
+ })
4172
+ };
4173
+ if (embedInstrumentation.afterStage !== null) {
4174
+ log?.(formatWatchEmbedTracePoint("after_stage", embedInstrumentation.afterStage));
4175
+ }
2752
4176
  if (inspection.promotion.allowed) {
2753
4177
  promoteCandidatePack(activationRoot, {
2754
4178
  updatedAt: now,
2755
4179
  reason: `watch_promote:${materialization.reason}:${materialization.lane}`
2756
4180
  });
4181
+ let promotedPack = null;
4182
+ let promotedPackError = null;
4183
+ try {
4184
+ promotedPack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
4185
+ }
4186
+ catch (error) {
4187
+ promotedPackError = formatWatchError(error);
4188
+ }
4189
+ embedInstrumentation = {
4190
+ ...embedInstrumentation,
4191
+ afterPromote: buildWatchEmbedTracePointFromPack({
4192
+ slot: "active",
4193
+ pack: promotedPack,
4194
+ embedder,
4195
+ error: promotedPackError
4196
+ })
4197
+ };
4198
+ if (embedInstrumentation.afterPromote !== null) {
4199
+ log?.(formatWatchEmbedTracePoint("after_promote", embedInstrumentation.afterPromote));
4200
+ }
2757
4201
  return {
2758
4202
  lastHandledMaterializationPackId: packId,
2759
4203
  materializedPackId: packId,
2760
4204
  logLine: `Promoted ${shortPackId} → active`,
4205
+ embedInstrumentation,
2761
4206
  failure: null
2762
4207
  };
2763
4208
  }
@@ -2765,15 +4210,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2765
4210
  lastHandledMaterializationPackId: packId,
2766
4211
  materializedPackId: packId,
2767
4212
  logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
4213
+ embedInstrumentation,
2768
4214
  failure: null
2769
4215
  };
2770
4216
  }
2771
4217
  catch (error) {
2772
4218
  const message = error instanceof Error ? error.message : String(error);
4219
+ embedInstrumentation = {
4220
+ ...embedInstrumentation,
4221
+ afterCandidateMaterialization: embedInstrumentation.afterCandidateMaterialization ??
4222
+ buildWatchEmbedTracePoint({
4223
+ slot: "candidate",
4224
+ packId,
4225
+ embedder,
4226
+ vectors: null,
4227
+ error: message
4228
+ })
4229
+ };
2773
4230
  return {
2774
4231
  lastHandledMaterializationPackId,
2775
4232
  materializedPackId: packId,
2776
4233
  logLine: `Promotion failed for ${shortPackId}: ${message}`,
4234
+ embedInstrumentation,
2777
4235
  failure: {
2778
4236
  mode: "materialization_failed",
2779
4237
  detail: message,
@@ -2782,15 +4240,18 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2782
4240
  };
2783
4241
  }
2784
4242
  }
2785
- function resolveWatchTeacherLabelerConfig(input) {
4243
+ function resolveWatchTeacherLabelerConfig(input, activationRoot) {
2786
4244
  if (input !== undefined) {
2787
4245
  return {
2788
4246
  teacherLabeler: input,
2789
4247
  warnings: []
2790
4248
  };
2791
4249
  }
2792
- const providerConfig = readOpenClawBrainProviderConfig(process.env);
2793
- const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_/u.test(warning));
4250
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
4251
+ env: process.env,
4252
+ activationRoot
4253
+ });
4254
+ const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_|provider defaults/u.test(warning));
2794
4255
  if (providerConfig.teacher.provider !== "ollama") {
2795
4256
  return {
2796
4257
  teacherLabeler: null,
@@ -2816,12 +4277,178 @@ function resolveWatchTeacherLabelerConfig(input) {
2816
4277
  warnings
2817
4278
  };
2818
4279
  }
4280
+ function resolveWatchEmbedderConfig(input, activationRoot) {
4281
+ if (input !== undefined) {
4282
+ return {
4283
+ embedder: input,
4284
+ warnings: []
4285
+ };
4286
+ }
4287
+ const defaultsResult = readOpenClawBrainProviderDefaults(activationRoot);
4288
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
4289
+ env: process.env,
4290
+ activationRoot,
4291
+ defaults: defaultsResult.defaults
4292
+ });
4293
+ const warnings = [...new Set([
4294
+ ...defaultsResult.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning)),
4295
+ ...providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_EMBEDDER_|provider defaults/u.test(warning))
4296
+ ])];
4297
+ const explicitEnv = typeof process.env[OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV] === "string" ||
4298
+ typeof process.env[OPENCLAWBRAIN_EMBEDDER_MODEL_ENV] === "string" ||
4299
+ typeof process.env[OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV] === "string";
4300
+ // Legacy install-written provider-defaults.json files can predate embedder fields entirely.
4301
+ // If a persisted defaults file exists, treat that activation root as explicitly configured and
4302
+ // let provider-config resolution fill in the embedder fallback instead of silently dropping to null.
4303
+ const explicitDefaults = defaultsResult.defaults !== null;
4304
+ if (!explicitEnv && !explicitDefaults) {
4305
+ return {
4306
+ embedder: null,
4307
+ warnings
4308
+ };
4309
+ }
4310
+ if (providerConfig.embedder.provider !== "ollama") {
4311
+ return {
4312
+ embedder: null,
4313
+ warnings
4314
+ };
4315
+ }
4316
+ return {
4317
+ embedder: createOllamaEmbedder({
4318
+ baseUrl: providerConfig.embedderBaseUrl,
4319
+ model: providerConfig.embedder.model
4320
+ }),
4321
+ warnings
4322
+ };
4323
+ }
4324
+ function summarizeWatchLatestUserMessage(localPoll) {
4325
+ let latest = null;
4326
+ for (const change of localPoll.changes) {
4327
+ if (change.lastUserMessageAt === null || change.lastUserMessageText === null) {
4328
+ continue;
4329
+ }
4330
+ const candidate = {
4331
+ at: change.lastUserMessageAt,
4332
+ text: change.lastUserMessageText,
4333
+ sessionId: change.sessionId
4334
+ };
4335
+ if (latest === null || Date.parse(candidate.at) >= Date.parse(latest.at)) {
4336
+ latest = candidate;
4337
+ }
4338
+ }
4339
+ return latest;
4340
+ }
4341
+ function summarizeWatchPackTransition(input) {
4342
+ const beforeActivePackId = input.before?.active?.packId ?? input.before?.pointers.active?.packId ?? null;
4343
+ const afterActivePackId = input.after?.active?.packId ?? input.after?.pointers.active?.packId ?? null;
4344
+ if (afterActivePackId !== null && beforeActivePackId !== afterActivePackId) {
4345
+ return {
4346
+ kind: "promoted_active",
4347
+ fromPackId: beforeActivePackId,
4348
+ toPackId: afterActivePackId
4349
+ };
4350
+ }
4351
+ const beforeCandidatePackId = input.before?.candidate?.packId ?? input.before?.pointers.candidate?.packId ?? null;
4352
+ const afterCandidatePackId = input.after?.candidate?.packId ?? input.after?.pointers.candidate?.packId ?? null;
4353
+ if (afterCandidatePackId !== null && beforeCandidatePackId !== afterCandidatePackId) {
4354
+ return {
4355
+ kind: "staged_candidate",
4356
+ fromPackId: beforeCandidatePackId,
4357
+ toPackId: afterCandidatePackId
4358
+ };
4359
+ }
4360
+ return null;
4361
+ }
4362
+ function truncateWatchMessage(text, maxLength = 96) {
4363
+ const normalized = text.replace(/\s+/gu, " ").trim();
4364
+ if (normalized.length <= maxLength) {
4365
+ return normalized;
4366
+ }
4367
+ return `${normalized.slice(0, maxLength - 1)}…`;
4368
+ }
4369
+ function buildWatchLastObservedDelta(input) {
4370
+ const exported = input.exported.exportedBundleCount > 0 ||
4371
+ input.exported.exportedEventCount > 0;
4372
+ const labeled = (input.snapshotAfter.diagnostics.emittedArtifactCount ?? 0) >
4373
+ (input.snapshotBefore.diagnostics.emittedArtifactCount ?? 0);
4374
+ const latestPackTransition = summarizeWatchPackTransition({
4375
+ before: input.beforeInspection,
4376
+ after: input.afterInspection
4377
+ });
4378
+ const promoted = latestPackTransition?.kind === "promoted_active";
4379
+ const afterActivePackId = input.afterInspection?.active?.packId ?? input.afterInspection?.pointers.active?.packId ?? null;
4380
+ const served = promoted && afterActivePackId === latestPackTransition?.toPackId && input.afterInspection?.active?.activationReady === true;
4381
+ const latestUserMessage = summarizeWatchLatestUserMessage(input.localPoll);
4382
+ const selectedBackfillOnly = !exported && input.scanResult.selected.length > 0;
4383
+ const cycleDidNothing = !exported && !labeled && !promoted && !served;
4384
+ let explanation;
4385
+ if (latestUserMessage === null) {
4386
+ if (selectedBackfillOnly) {
4387
+ explanation =
4388
+ "No new local user message was exported in this cycle; the learner only revisited stored exports, so this pass does not prove a new last-turn change.";
4389
+ }
4390
+ else if (cycleDidNothing) {
4391
+ explanation = "No new local user message or learner-visible export was observed in this cycle, so nothing changed.";
4392
+ }
4393
+ else if (promoted && latestPackTransition !== null) {
4394
+ explanation =
4395
+ `No new local user message was exported in this cycle; pack ${latestPackTransition.toPackId} moved into ${latestPackTransition.kind === "promoted_active" ? "active serving" : "the candidate slot"} from previously accumulated learner state.`;
4396
+ }
4397
+ else {
4398
+ explanation =
4399
+ "This cycle observed learner activity, but it did not include a new local user message, so the latest last-turn delta cannot be attributed to a fresh user turn.";
4400
+ }
4401
+ }
4402
+ else {
4403
+ const quotedMessage = `"${truncateWatchMessage(latestUserMessage.text)}"`;
4404
+ if (exported && labeled && promoted && served && latestPackTransition !== null) {
4405
+ explanation =
4406
+ `Latest user message ${quotedMessage} was exported, labeled, promoted into pack ${latestPackTransition.toPackId}, and is now served from the active pack.`;
4407
+ }
4408
+ else if (exported && labeled && !promoted) {
4409
+ explanation =
4410
+ `Latest user message ${quotedMessage} was exported and labeled, but it has not been promoted into the serving pack yet.`;
4411
+ }
4412
+ else if (exported && !labeled && !promoted) {
4413
+ explanation =
4414
+ `Latest user message ${quotedMessage} was exported, but it did not add a new teacher label or change the serving pack in this cycle.`;
4415
+ }
4416
+ else if (exported && !labeled && promoted && latestPackTransition !== null) {
4417
+ explanation =
4418
+ `Latest user message ${quotedMessage} was exported, but this cycle's promotion to pack ${latestPackTransition.toPackId} is not backed by a new teacher label from that message alone.`;
4419
+ }
4420
+ else if (!exported && labeled) {
4421
+ explanation =
4422
+ `Latest user message ${quotedMessage} was already in stored exports; this cycle only labeled or replayed it, without a new local export.`;
4423
+ }
4424
+ else if (cycleDidNothing) {
4425
+ explanation = `Latest user message ${quotedMessage} did not produce a new export, label, or serving-pack change in this cycle.`;
4426
+ }
4427
+ else {
4428
+ explanation =
4429
+ `Latest user message ${quotedMessage} changed learner state this cycle, but the local artifacts do not prove a clean export-to-serve handoff yet.`;
4430
+ }
4431
+ }
4432
+ return {
4433
+ available: true,
4434
+ observedAt: input.observedAt,
4435
+ exported,
4436
+ labeled,
4437
+ promoted,
4438
+ served,
4439
+ latestPackTransition,
4440
+ explanation
4441
+ };
4442
+ }
2819
4443
  export async function createWatchCommandRuntime(input) {
2820
4444
  const activationRoot = path.resolve(input.activationRoot);
2821
4445
  const bootstrapObservedAt = new Date().toISOString();
2822
4446
  const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
2823
4447
  ? path.resolve(input.scanRoot)
2824
4448
  : path.resolve(activationRoot, "event-exports");
4449
+ const pollIntervalSeconds = Number.isInteger(input.pollIntervalSeconds) && (input.pollIntervalSeconds ?? 0) > 0
4450
+ ? input.pollIntervalSeconds
4451
+ : DEFAULT_WATCH_POLL_INTERVAL_SECONDS;
2825
4452
  const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
2826
4453
  const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
2827
4454
  const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
@@ -2832,15 +4459,26 @@ export async function createWatchCommandRuntime(input) {
2832
4459
  log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
2833
4460
  log(`Scan root: ${shortenPath(scanRoot)}`);
2834
4461
  log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
2835
- const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler);
4462
+ const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
4463
+ const resolvedEmbedder = resolveWatchEmbedderConfig(input.embedder, activationRoot);
2836
4464
  const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
2837
4465
  for (const warning of resolvedTeacherLabeler.warnings) {
2838
- startupWarnings.push(`teacher_env_warning:${warning}`);
2839
- log(`Teacher env warning: ${warning}`);
4466
+ startupWarnings.push(`teacher_config_warning:${warning}`);
4467
+ log(`Teacher config warning: ${warning}`);
4468
+ }
4469
+ for (const warning of resolvedEmbedder.warnings) {
4470
+ startupWarnings.push(`embedder_config_warning:${warning}`);
4471
+ log(`Embedder config warning: ${warning}`);
2840
4472
  }
2841
4473
  if (teacherLabeler?.provider === "ollama") {
2842
4474
  log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
2843
4475
  }
4476
+ if (resolvedEmbedder.embedder !== null) {
4477
+ log(`Embedder: provider=ollama model=${resolvedEmbedder.embedder.model}`);
4478
+ }
4479
+ else {
4480
+ log("Embedder: numeric pack materialization is not configured; watch will keep keyword/weight vectors only.");
4481
+ }
2844
4482
  const scanner = createRuntimeEventExportScanner({ scanRoot });
2845
4483
  let lastServeTimeFallbackReason = null;
2846
4484
  const baseTeacherLoopInput = {
@@ -2877,10 +4515,23 @@ export async function createWatchCommandRuntime(input) {
2877
4515
  };
2878
4516
  let teacherLoop;
2879
4517
  let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
4518
+ let lastEmbedInstrumentation = restoredTeacherState.embedInstrumentation;
4519
+ let restoredLastObservedDelta = restoredTeacherState.lastObservedDelta;
2880
4520
  if (restoredTeacherState.error !== null) {
2881
4521
  const message = restoredTeacherState.error;
2882
4522
  startupWarnings.push(`teacher_snapshot_reset:${message}`);
2883
4523
  lastHandledMaterializationPackId = null;
4524
+ lastEmbedInstrumentation = null;
4525
+ restoredLastObservedDelta = {
4526
+ available: true,
4527
+ observedAt: bootstrapObservedAt,
4528
+ exported: false,
4529
+ labeled: false,
4530
+ promoted: false,
4531
+ served: false,
4532
+ latestPackTransition: null,
4533
+ explanation: "Watch reset an unreadable teacher snapshot, so no prior last-turn delta can be trusted."
4534
+ };
2884
4535
  log(`Teacher snapshot reset: ${message}`);
2885
4536
  teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
2886
4537
  }
@@ -2895,6 +4546,17 @@ export async function createWatchCommandRuntime(input) {
2895
4546
  const message = formatWatchError(error);
2896
4547
  startupWarnings.push(`teacher_snapshot_reset:${message}`);
2897
4548
  lastHandledMaterializationPackId = null;
4549
+ lastEmbedInstrumentation = null;
4550
+ restoredLastObservedDelta = {
4551
+ available: true,
4552
+ observedAt: bootstrapObservedAt,
4553
+ exported: false,
4554
+ labeled: false,
4555
+ promoted: false,
4556
+ served: false,
4557
+ latestPackTransition: null,
4558
+ explanation: "Watch reset an unusable teacher snapshot, so no prior last-turn delta can be trusted."
4559
+ };
2898
4560
  log(`Teacher snapshot reset: ${message}`);
2899
4561
  teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
2900
4562
  }
@@ -2903,11 +4565,19 @@ export async function createWatchCommandRuntime(input) {
2903
4565
  const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
2904
4566
  log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
2905
4567
  }
4568
+ const resolvedProfileRoots = input.profileRoots === undefined
4569
+ ? resolveWatchProfileRootsForActivationRoot(activationRoot)
4570
+ : [...new Set(input.profileRoots.map((root) => path.resolve(root)))];
4571
+ if (input.profileRoots === undefined && resolvedProfileRoots !== undefined) {
4572
+ log(`Session tail scope: attached OpenClaw home${resolvedProfileRoots.length === 1 ? "" : "s"} ${resolvedProfileRoots
4573
+ .map((root) => shortenPath(root))
4574
+ .join(", ")}`);
4575
+ }
2906
4576
  let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
2907
4577
  let localSessionTail;
2908
4578
  try {
2909
4579
  localSessionTail = createOpenClawLocalSessionTail({
2910
- ...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
4580
+ ...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
2911
4581
  cursor: restoredCursor,
2912
4582
  emitExistingOnFirstPoll: restoredCursor.length === 0
2913
4583
  });
@@ -2917,7 +4587,7 @@ export async function createWatchCommandRuntime(input) {
2917
4587
  log(`Session tail cursor reset: ${message}`);
2918
4588
  restoredCursor = [];
2919
4589
  localSessionTail = createOpenClawLocalSessionTail({
2920
- ...(input.profileRoots === undefined ? {} : { profileRoots: input.profileRoots }),
4590
+ ...(resolvedProfileRoots === undefined ? {} : { profileRoots: resolvedProfileRoots }),
2921
4591
  emitExistingOnFirstPoll: true
2922
4592
  });
2923
4593
  persistWatchSessionTailCursor(sessionTailCursorPath, []);
@@ -2938,8 +4608,11 @@ export async function createWatchCommandRuntime(input) {
2938
4608
  log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
2939
4609
  }
2940
4610
  let bootstrapSnapshot = teacherLoop.snapshot();
2941
- const replayPromotion = applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId);
4611
+ const replayPromotion = await applyWatchMaterialization(activationRoot, bootstrapSnapshot, lastHandledMaterializationPackId, resolvedEmbedder.embedder, log);
2942
4612
  lastHandledMaterializationPackId = replayPromotion.lastHandledMaterializationPackId;
4613
+ if (replayPromotion.embedInstrumentation !== null) {
4614
+ lastEmbedInstrumentation = replayPromotion.embedInstrumentation;
4615
+ }
2943
4616
  if (replayPromotion.logLine !== null) {
2944
4617
  log(replayPromotion.logLine);
2945
4618
  bootstrapSnapshot = teacherLoop.snapshot();
@@ -2947,6 +4620,7 @@ export async function createWatchCommandRuntime(input) {
2947
4620
  const bootstrapCursor = localSessionTail.snapshot();
2948
4621
  persistWatchTeacherSnapshot(teacherSnapshotPath, {
2949
4622
  lastRunAt: bootstrapObservedAt,
4623
+ pollIntervalSeconds,
2950
4624
  scanRoot,
2951
4625
  sessionTailCursorPath,
2952
4626
  sessionTailCursorUpdatedAt: bootstrapObservedAt,
@@ -2962,26 +4636,44 @@ export async function createWatchCommandRuntime(input) {
2962
4636
  lastTeacherError: null,
2963
4637
  localSessionTailNoopReason: null,
2964
4638
  lastHandledMaterializationPackId,
4639
+ lastObservedDelta: restoredLastObservedDelta.available
4640
+ ? restoredLastObservedDelta
4641
+ : {
4642
+ available: true,
4643
+ observedAt: bootstrapObservedAt,
4644
+ exported: false,
4645
+ labeled: false,
4646
+ promoted: false,
4647
+ served: false,
4648
+ latestPackTransition: null,
4649
+ explanation: "Watch bootstrapped its state, but no new local user-message delta has been observed yet."
4650
+ },
4651
+ embedInstrumentation: lastEmbedInstrumentation,
2965
4652
  failure: replayPromotion.failure,
2966
4653
  snapshot: bootstrapSnapshot
2967
4654
  });
2968
4655
  return {
2969
4656
  activationRoot,
2970
4657
  scanRoot,
4658
+ pollIntervalSeconds,
2971
4659
  sessionTailCursorPath,
2972
4660
  teacherSnapshotPath,
2973
4661
  startupWarnings,
2974
4662
  lastTeacherError: null,
2975
4663
  replayState,
2976
4664
  lastHandledMaterializationPackId,
4665
+ lastEmbedInstrumentation,
2977
4666
  scanner,
2978
4667
  teacherLoop,
2979
- localSessionTail
4668
+ localSessionTail,
4669
+ embedder: resolvedEmbedder.embedder
2980
4670
  };
2981
4671
  }
2982
4672
  export async function runWatchCommandPass(runtime, options = {}) {
2983
4673
  const log = options.log ?? watchLog;
2984
4674
  const observedAt = options.observedAt ?? new Date().toISOString();
4675
+ const snapshotBefore = runtime.teacherLoop.snapshot();
4676
+ const beforeInspection = inspectActivationState(runtime.activationRoot, observedAt);
2985
4677
  const localPoll = runtime.localSessionTail.pollOnce({
2986
4678
  observedAt
2987
4679
  });
@@ -3015,9 +4707,12 @@ export async function runWatchCommandPass(runtime, options = {}) {
3015
4707
  const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
3016
4708
  runtime.lastTeacherError = null;
3017
4709
  snapshot = ingestResult.snapshot;
3018
- const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
4710
+ const promotion = await applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId, runtime.embedder, log);
3019
4711
  runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
3020
4712
  materializedPackId = promotion.materializedPackId;
4713
+ if (promotion.embedInstrumentation !== null) {
4714
+ runtime.lastEmbedInstrumentation = promotion.embedInstrumentation;
4715
+ }
3021
4716
  failure = promotion.failure;
3022
4717
  if (promotion.logLine !== null) {
3023
4718
  log(promotion.logLine);
@@ -3049,8 +4744,20 @@ export async function runWatchCommandPass(runtime, options = {}) {
3049
4744
  snapshot = runtime.teacherLoop.snapshot();
3050
4745
  }
3051
4746
  }
4747
+ const afterInspection = inspectActivationState(runtime.activationRoot, observedAt);
4748
+ const lastObservedDelta = buildWatchLastObservedDelta({
4749
+ observedAt,
4750
+ localPoll,
4751
+ exported,
4752
+ scanResult,
4753
+ snapshotBefore,
4754
+ snapshotAfter: snapshot,
4755
+ beforeInspection,
4756
+ afterInspection
4757
+ });
3052
4758
  persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
3053
4759
  lastRunAt: observedAt,
4760
+ pollIntervalSeconds: runtime.pollIntervalSeconds,
3054
4761
  scanRoot: runtime.scanRoot,
3055
4762
  sessionTailCursorPath: runtime.sessionTailCursorPath,
3056
4763
  sessionTailCursorUpdatedAt: observedAt,
@@ -3066,6 +4773,8 @@ export async function runWatchCommandPass(runtime, options = {}) {
3066
4773
  lastTeacherError: runtime.lastTeacherError,
3067
4774
  localSessionTailNoopReason: localPoll.noopReason,
3068
4775
  lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
4776
+ lastObservedDelta,
4777
+ embedInstrumentation: runtime.lastEmbedInstrumentation,
3069
4778
  failure,
3070
4779
  snapshot
3071
4780
  });
@@ -3086,6 +4795,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
3086
4795
  scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
3087
4796
  scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
3088
4797
  materialized: materializedPackId,
4798
+ lastObservedDelta,
3089
4799
  diagnostics: snapshot.diagnostics ?? null,
3090
4800
  localSessionTailNoopReason: localPoll.noopReason
3091
4801
  }));
@@ -3103,6 +4813,7 @@ async function runWatchCommand(parsed) {
3103
4813
  const runtime = await createWatchCommandRuntime({
3104
4814
  activationRoot: parsed.activationRoot,
3105
4815
  scanRoot: parsed.scanRoot,
4816
+ pollIntervalSeconds: parsed.interval,
3106
4817
  log: watchLog
3107
4818
  });
3108
4819
  watchLog(`Interval: ${parsed.interval}s`);
@@ -3330,31 +5041,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3330
5041
  return runUninstallCommand(parsed);
3331
5042
  }
3332
5043
  if (parsed.command === "attach") {
3333
- mkdirSync(parsed.activationRoot, { recursive: true });
3334
- mkdirSync(parsed.packRoot, { recursive: true });
3335
- const result = bootstrapRuntimeAttach({
3336
- profileSelector: "current_profile",
3337
- ...(parsed.brainAttachmentPolicy != null ? { brainAttachmentPolicy: parsed.brainAttachmentPolicy } : {}),
3338
- activationRoot: parsed.activationRoot,
3339
- packRoot: parsed.packRoot,
3340
- packLabel: parsed.packLabel,
3341
- workspace: {
3342
- workspaceId: parsed.workspaceId,
3343
- snapshotId: `${parsed.workspaceId}@bootstrap-${new Date().toISOString().slice(0, 10)}`,
3344
- capturedAt: new Date().toISOString(),
3345
- rootDir: process.cwd(),
3346
- revision: "cli-bootstrap-v1"
3347
- },
3348
- interactionEvents: [],
3349
- feedbackEvents: []
3350
- });
3351
- if (parsed.json) {
3352
- console.log(JSON.stringify(result, null, 2));
3353
- }
3354
- else {
3355
- console.log(formatBootstrapRuntimeAttachReport(result));
3356
- }
3357
- return 0;
5044
+ return runAttachCommand(parsed);
3358
5045
  }
3359
5046
  if (parsed.command === "scan") {
3360
5047
  if (parsed.sessionPath !== null) {
@@ -3392,6 +5079,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3392
5079
  // At this point only status/rollback commands remain
3393
5080
  const statusOrRollback = parsed;
3394
5081
  const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
5082
+ const targetInspection = statusOrRollback.openclawHome === null ? null : inspectOpenClawHome(statusOrRollback.openclawHome);
3395
5083
  if (statusOrRollback.command === "rollback") {
3396
5084
  const result = rollbackRuntimeAttach({
3397
5085
  activationRoot,
@@ -3417,11 +5105,21 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3417
5105
  }
3418
5106
  else {
3419
5107
  const report = buildOperatorSurfaceReport(operatorInput);
5108
+ const providerConfig = readOpenClawBrainProviderConfigFromSources({
5109
+ env: process.env,
5110
+ activationRoot
5111
+ });
3420
5112
  if (statusOrRollback.detailed) {
3421
- console.log(formatCurrentProfileStatusSummary(status, report));
5113
+ console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
5114
+ openclawHome: statusOrRollback.openclawHome,
5115
+ providerConfig
5116
+ }));
3422
5117
  }
3423
5118
  else {
3424
- console.log(formatHumanFriendlyStatus(status, report));
5119
+ console.log(formatHumanFriendlyStatus(status, report, targetInspection, {
5120
+ openclawHome: statusOrRollback.openclawHome,
5121
+ providerConfig
5122
+ }));
3425
5123
  }
3426
5124
  }
3427
5125
  return 0;