@openclawbrain/openclaw 0.2.3 → 0.3.0

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 (36) hide show
  1. package/dist/extension/index.d.ts +1 -0
  2. package/dist/extension/index.js +73 -0
  3. package/dist/extension/index.js.map +1 -0
  4. package/dist/extension/runtime-guard.d.ts +61 -0
  5. package/dist/extension/runtime-guard.js +230 -0
  6. package/dist/extension/runtime-guard.js.map +1 -0
  7. package/dist/src/cli.d.ts +8 -4
  8. package/dist/src/cli.js +501 -154
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/daemon.d.ts +7 -4
  11. package/dist/src/daemon.js +275 -47
  12. package/dist/src/daemon.js.map +1 -1
  13. package/dist/src/index.d.ts +150 -2
  14. package/dist/src/index.js +769 -139
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/learning-spine.d.ts +2 -1
  17. package/dist/src/learning-spine.js +8 -0
  18. package/dist/src/learning-spine.js.map +1 -1
  19. package/dist/src/ollama-client.d.ts +46 -0
  20. package/dist/src/ollama-client.js +231 -0
  21. package/dist/src/ollama-client.js.map +1 -0
  22. package/dist/src/provider-config.d.ts +28 -0
  23. package/dist/src/provider-config.js +150 -0
  24. package/dist/src/provider-config.js.map +1 -0
  25. package/dist/src/resolve-activation-root.d.ts +3 -3
  26. package/dist/src/resolve-activation-root.js +68 -21
  27. package/dist/src/resolve-activation-root.js.map +1 -1
  28. package/dist/src/shadow-extension-proof.d.ts +40 -0
  29. package/dist/src/shadow-extension-proof.js +214 -0
  30. package/dist/src/shadow-extension-proof.js.map +1 -0
  31. package/dist/src/teacher-labeler.d.ts +50 -0
  32. package/dist/src/teacher-labeler.js +424 -0
  33. package/dist/src/teacher-labeler.js.map +1 -0
  34. package/extension/index.ts +5 -1
  35. package/extension/runtime-guard.ts +17 -2
  36. package/package.json +8 -7
package/dist/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { execSync } from "node:child_process";
2
+ import { execFileSync, execSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, readSync, openSync, closeSync, realpathSync, rmSync, statSync, writeFileSync, appendFileSync, symlinkSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -9,11 +9,13 @@ import { parseDaemonArgs, runDaemonCommand } from "./daemon.js";
9
9
  import { exportBrain, importBrain } from "./import-export.js";
10
10
  import { buildNormalizedEventExport } from "@openclawbrain/contracts";
11
11
  import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "@openclawbrain/learner";
12
- import { inspectActivationState, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
12
+ import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
13
13
  import { resolveActivationRoot } from "./resolve-activation-root.js";
14
- import { buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatBootstrapRuntimeAttachReport, formatOperatorRollbackReport, loadRuntimeEventExportBundle, rollbackRuntimeAttach, resolveAsyncTeacherLiveLoopSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.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 { appendLearningUpdateLogs } from "./learning-spine.js";
15
16
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
16
17
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
18
+ import { readOpenClawBrainProviderConfig } from "./provider-config.js";
17
19
  function quoteShellArg(value) {
18
20
  return `'${value.replace(/'/g, `"'"'`)}'`;
19
21
  }
@@ -27,7 +29,7 @@ function normalizeOptionalCliString(value) {
27
29
  function getCliHomeDir() {
28
30
  return process.env.HOME ?? process.env.USERPROFILE ?? "~";
29
31
  }
30
- function discoverSetupCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
32
+ function discoverInstallCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
31
33
  const resolvedHomeDir = path.resolve(homeDir);
32
34
  let entries;
33
35
  try {
@@ -42,7 +44,7 @@ function discoverSetupCandidateOpenClawHomes(homeDir = getCliHomeDir()) {
42
44
  .filter((candidate) => existsSync(path.join(candidate, "openclaw.json")))
43
45
  .sort((left, right) => left.localeCompare(right));
44
46
  }
45
- function formatSetupOpenClawHomeSource(source) {
47
+ function formatInstallOpenClawHomeSource(source) {
46
48
  switch (source) {
47
49
  case "explicit":
48
50
  return "--openclaw-home";
@@ -54,7 +56,7 @@ function formatSetupOpenClawHomeSource(source) {
54
56
  return source;
55
57
  }
56
58
  }
57
- function resolveSetupOpenClawHome(explicitOpenclawHome) {
59
+ function resolveInstallOpenClawHome(explicitOpenclawHome) {
58
60
  const normalizedExplicitHome = normalizeOptionalCliString(explicitOpenclawHome);
59
61
  if (normalizedExplicitHome !== null) {
60
62
  return {
@@ -69,7 +71,7 @@ function resolveSetupOpenClawHome(explicitOpenclawHome) {
69
71
  openclawHomeSource: "env"
70
72
  };
71
73
  }
72
- const discoveredHomes = discoverSetupCandidateOpenClawHomes();
74
+ const discoveredHomes = discoverInstallCandidateOpenClawHomes();
73
75
  if (discoveredHomes.length === 1) {
74
76
  return {
75
77
  openclawHome: path.resolve(discoveredHomes[0]),
@@ -85,14 +87,14 @@ function resolveSetupOpenClawHome(explicitOpenclawHome) {
85
87
  })
86
88
  .join("\n");
87
89
  throw new Error([
88
- "Refusing ambiguous live OpenClaw targets for install/setup.",
90
+ "Refusing ambiguous live OpenClaw targets for install.",
89
91
  targetChoices,
90
92
  "Pass --openclaw-home <path> or set OPENCLAW_HOME to pin one profile."
91
93
  ].join("\n"));
92
94
  }
93
95
  throw new Error("No OpenClaw profile home found. Pass --openclaw-home <path> or set OPENCLAW_HOME.");
94
96
  }
95
- function resolveSetupActivationRoot(openclawHome, explicitActivationRoot) {
97
+ function resolveInstallActivationRoot(openclawHome, explicitActivationRoot) {
96
98
  const normalizedExplicitActivationRoot = normalizeOptionalCliString(explicitActivationRoot);
97
99
  if (normalizedExplicitActivationRoot !== null) {
98
100
  return {
@@ -105,7 +107,7 @@ function resolveSetupActivationRoot(openclawHome, explicitActivationRoot) {
105
107
  source: "default_from_openclaw_home"
106
108
  };
107
109
  }
108
- function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
110
+ function resolveInstallWorkspaceId(openclawHome, explicitWorkspaceId) {
109
111
  const normalizedExplicitWorkspaceId = normalizeOptionalCliString(explicitWorkspaceId);
110
112
  if (normalizedExplicitWorkspaceId !== null) {
111
113
  return {
@@ -124,7 +126,7 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
124
126
  }
125
127
  }
126
128
  catch {
127
- // Fall back to the profile-home name when setup is pointed at an incomplete or not-yet-readable profile.
129
+ // Fall back to the profile-home name when install is pointed at an incomplete or not-yet-readable profile.
128
130
  }
129
131
  const dirName = path.basename(openclawHome);
130
132
  if (dirName === ".openclaw") {
@@ -145,13 +147,13 @@ function resolveSetupWorkspaceId(openclawHome, explicitWorkspaceId) {
145
147
  source: "fallback"
146
148
  };
147
149
  }
148
- function formatSetupActivationRootSource(source) {
150
+ function formatInstallActivationRootSource(source) {
149
151
  if (source === "explicit") {
150
152
  return "explicit --activation-root";
151
153
  }
152
154
  return "default beside --openclaw-home";
153
155
  }
154
- function formatSetupWorkspaceIdSource(source) {
156
+ function formatInstallWorkspaceIdSource(source) {
155
157
  switch (source) {
156
158
  case "explicit":
157
159
  return "explicit --workspace-id";
@@ -262,7 +264,7 @@ function buildDoctorDeletedMessage(args) {
262
264
  const jsonCommand = buildStatusReplacementCommand(replacementInput, true);
263
265
  const lines = [
264
266
  "`doctor` is no longer a separate operator surface.",
265
- 'Use `openclawbrain status` as the human answer to "How\'s the brain?" and `status --json` for the canonical current-profile object.',
267
+ 'Use `openclawbrain status --activation-root <path>` as the human answer to "How\'s the brain?" and `status --json` for the canonical current-profile object.',
266
268
  "Use `describeAttachStatus()` or the proof helpers only when you need deeper activation diagnostics."
267
269
  ];
268
270
  if (json && jsonCommand !== null) {
@@ -276,36 +278,42 @@ function buildDoctorDeletedMessage(args) {
276
278
  }
277
279
  return lines.join(" ");
278
280
  }
281
+ function buildSetupDeletedMessage() {
282
+ return [
283
+ "`setup` has been removed.",
284
+ "Use `openclawbrain install` instead.",
285
+ "The install command still accepts the explicit targeting flags that setup used: `--openclaw-home`, `--activation-root`, `--workspace-id`, and `--shared`."
286
+ ].join(" ");
287
+ }
279
288
  function operatorCliHelp() {
280
289
  return [
281
290
  "Usage:",
282
291
  " openclawbrain install [--openclaw-home <path>] [options]",
283
- " openclawbrain setup [--openclaw-home <path>] [options] # compatibility alias",
284
292
  " openclawbrain detach --openclaw-home <path> [options]",
285
293
  " openclawbrain uninstall --openclaw-home <path> [--keep-data|--purge-data] [options]",
286
294
  " openclawbrain attach --activation-root <path> [options]",
287
- " openclawbrain <status|rollback> --activation-root <path> [options]",
288
- " openclawbrain context \"message\" [--activation-root <path>]",
289
- " openclawbrain history [--activation-root <path>] [--limit N] [--json]",
295
+ " openclawbrain <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options]",
296
+ " openclawbrain context \"message\" [--activation-root <path>|--openclaw-home <path>]",
297
+ " openclawbrain history [--activation-root <path>|--openclaw-home <path>] [--limit N] [--json]",
290
298
  " openclawbrain scan --session <trace.json> --root <path> [options]",
291
299
  " openclawbrain scan --live <event-export-path> --workspace <workspace.json> [options]",
292
- " openclawbrain learn [--activation-root <path>] [--json]",
293
- " openclawbrain watch [--activation-root <path>] [--scan-root <path>] [--interval <seconds>]",
294
- " openclawbrain daemon <start|stop|status|logs> [--activation-root <path>]",
295
- " openclawbrain-ops <status|rollback> --activation-root <path> [options] # compatibility alias",
300
+ " 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
+ " openclawbrain-ops <status|rollback> [--activation-root <path>|--openclaw-home <path>] [options] # compatibility alias",
296
304
  " openclawbrain-ops scan --session <trace.json> --root <path> [options] # compatibility alias",
297
305
  "",
298
306
  "Options:",
299
- " --openclaw-home <path> OpenClaw profile home dir for install/setup/detach/uninstall (e.g. ~/.openclaw-Tern). Auto-selects for install/setup when OPENCLAW_HOME is set or exactly one live profile home exists.",
300
- " --shared Set brain-attachment-policy to shared instead of dedicated (install/setup only).",
301
- " --activation-root <path> Activation root for attach/detach/uninstall; install/setup defaults to sibling .openclawbrain/activation next to the selected OpenClaw home.",
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.",
302
310
  " --keep-data Preserve activation data on uninstall; detach always behaves this way.",
303
311
  " --purge-data Remove activation data on uninstall; requires the installed profile hook or --activation-root.",
304
312
  " --restart <never|safe|external> Restart guidance mode for detach/uninstall. 'safe' is conservative; 'never' leaves restart entirely to the operator.",
305
313
  " --pack-root <path> Initial pack root directory (attach only; defaults to <activation-root>/packs/initial).",
306
- " --workspace-id <id> Workspace identifier for attach/install/setup provenance; install/setup defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
314
+ " --workspace-id <id> Workspace identifier for attach/install provenance; install defaults to openclaw.json.profile or the profile name, attach defaults to 'workspace'.",
307
315
  " --event-export <path> Event-export bundle root or normalized export JSON payload.",
308
- " --teacher-snapshot <path> Async teacher snapshot JSON from teacherLoop.snapshot()/flush(); keeps live-first, principal-priority, and passive-backfill learner truth explicit.",
316
+ " --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.",
309
317
  " --updated-at <iso> Observation time to use for freshness checks.",
310
318
  " --brain-attachment-policy <undeclared|dedicated|shared> Override attachment policy semantics for status inspection.",
311
319
  " --detailed Show verbose diagnostic output for status (default is human-friendly summary).",
@@ -329,14 +337,15 @@ function operatorCliHelp() {
329
337
  " 0. uninstall openclawbrain uninstall --openclaw-home <path> --keep-data|--purge-data — remove the profile hook and choose the data outcome explicitly",
330
338
  " 0. context openclawbrain context \"hello\" — preview the brain context that would be injected for a message",
331
339
  " 0. attach openclawbrain attach --activation-root <path>",
332
- " 1. status answer \"How's the brain?\" for the current profile on that activation root",
333
- " 2. status --json read the canonical current_profile_brain_status.v1 object for that same boundary",
334
- " 3. rollback --dry-run preview active <- previous, active -> candidate",
335
- " 4. rollback apply the rollback when the preview says ready",
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",
336
344
  " 5. scan --session replay one sanitized session trace across no_brain, seed_pack, and learned_replay",
337
345
  " 6. scan --live scan one live event export into teacher/learner state without claiming a daemon is running",
338
346
  " status --teacher-snapshot keeps the current live-first / principal-priority / passive-backfill learner order visible when that snapshot exists",
339
- " watch/daemon persist that snapshot at <activation-root>/async-teacher-live-loop.snapshot.json; --teacher-snapshot overrides the default path",
347
+ " 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",
340
349
  "",
341
350
  "Exit codes:",
342
351
  " status: 0 on successful inspection, 1 on input/read failure.",
@@ -398,6 +407,7 @@ function formatCurrentProfileStatusSummary(status, report) {
398
407
  `host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
399
408
  `profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
400
409
  `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)}`,
410
+ `activation state=${status.brainStatus.activationState} detail=${status.brain.detail}`,
401
411
  `brain pack=${status.brain.activePackId ?? "none"} state=${status.brain.state} init=${status.brain.initMode ?? "unknown"} routeFreshness=${status.brain.routeFreshness} lastPromotion=${status.brain.lastPromotionAt ?? "none"} router=${status.brain.routerIdentity ?? "none"}`,
402
412
  `serve state=${status.brainStatus.serveState} failOpen=${yesNo(status.brainStatus.failOpen)} hardFail=${yesNo(report.servePath.hardRequirementViolated)} usedRouteFn=${yesNo(status.brainStatus.usedLearnedRouteFn)} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)} detail=${status.brainStatus.detail}`,
403
413
  `route router=${report.servePath.routerIdentity ?? status.brain.routerIdentity ?? "none"} supervision=${report.servePath.refreshStatus ?? status.brain.routeFreshness} freshness=${report.servePath.freshnessChecksum ?? "none"}`,
@@ -409,7 +419,8 @@ function formatCurrentProfileStatusSummary(status, report) {
409
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}`,
410
420
  `path ${formatLearningPathSummary(report.learningPath)}`,
411
421
  `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}`,
412
- `teacher snapshot=${report.teacherLoop.sourcePath ?? "none"} started=${report.teacherLoop.startedAt ?? "none"} heartbeat=${report.teacherLoop.lastHeartbeatAt ?? "none"} scan=${report.teacherLoop.lastScanAt ?? report.teacherLoop.lastProcessedAt ?? "none"} queue=${report.teacherLoop.queueDepth ?? "none"}/${report.teacherLoop.queueCapacity ?? "none"} running=${yesNo(report.teacherLoop.running)} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
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"}`,
413
424
  `rollback ready=${yesNo(report.rollback.allowed)} state=${report.rollback.state} previous=${report.rollback.previousPackId ?? "none"}`,
414
425
  `proof lastExport=${status.brain.lastExportAt ?? "none"} lastLearningUpdate=${status.brain.lastLearningUpdateAt ?? "none"} lastPromotion=${status.brain.lastPromotionAt ?? "none"}`,
415
426
  `logs root=${status.brain.logRoot ?? "none"}`,
@@ -445,20 +456,27 @@ function buildCleanupRestartGuidance(restart) {
445
456
  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.";
446
457
  }
447
458
  function buildStatusNextStep(status, report) {
459
+ const activationRootArg = quoteShellArg(status.host.activationRoot);
460
+ if (status.brainStatus.activationState === "broken_install") {
461
+ return "Repair or replace the activation root before trusting serve-path status again.";
462
+ }
463
+ if (status.brainStatus.activationState === "stale_incomplete") {
464
+ return "Clean up or repair the retained activation state before reattaching or promoting packs.";
465
+ }
448
466
  if (status.brainStatus.status === "fail") {
449
- return "Run `openclawbrain status --detailed` before changing lifecycle state so the serve-path failure is explicit.";
467
+ return `Run \`openclawbrain status --activation-root ${activationRootArg} --detailed\` before changing lifecycle state so the serve-path failure is explicit.`;
450
468
  }
451
469
  if (status.brainStatus.awaitingFirstExport) {
452
- return "Let the attached OpenClaw profile emit a real export, then rerun `openclawbrain status`.";
470
+ return `Let the attached OpenClaw profile emit a real export, then rerun \`openclawbrain status --activation-root ${activationRootArg}\`.`;
453
471
  }
454
472
  if (report.learning.warningStates.includes("principal_live_backlog") ||
455
473
  report.learning.warningStates.includes("active_pack_behind_latest_principal")) {
456
474
  return "A newer principal correction is still pending promotion; keep the current pack conservative until learner promotion lands.";
457
475
  }
458
476
  if (report.rollback.allowed) {
459
- return "Use `openclawbrain rollback --dry-run` before restoring the previous pack.";
477
+ return `Use \`openclawbrain rollback --activation-root ${activationRootArg} --dry-run\` before restoring the previous pack.`;
460
478
  }
461
- return "Use `openclawbrain status --detailed` when you need the full lifecycle, serve-path, and backlog proof.";
479
+ return `Use \`openclawbrain status --activation-root ${activationRootArg} --detailed\` when you need the full lifecycle, serve-path, and backlog proof.`;
462
480
  }
463
481
  function formatHumanFriendlyStatus(status, report) {
464
482
  // Brain status line
@@ -482,10 +500,11 @@ function formatHumanFriendlyStatus(status, report) {
482
500
  `Pack: ${packShort} (${state})`,
483
501
  `Activation: ${activationPath}`,
484
502
  `Policy: ${policy}`,
485
- `Lifecycle: attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
503
+ `Lifecycle: activation=${status.brainStatus.activationState} attachment=${status.attachment.state} serve=${status.brainStatus.serveState} awaitingFirstExport=${yesNo(status.brainStatus.awaitingFirstExport)}`,
486
504
  `Rollback: state=${report.rollback.state} ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"}`,
487
505
  `Backlog: principal=${principalFrontier} live=${pendingLive} backfill=${pendingBackfill} next=${nextLane}/${nextBucket}`,
488
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}`,
489
508
  `Learning: ${formatLearningPathSummary(report.learningPath)}`
490
509
  ];
491
510
  // Add learning/serve warnings if relevant
@@ -498,11 +517,22 @@ function formatHumanFriendlyStatus(status, report) {
498
517
  lines.push(`Next: ${buildStatusNextStep(status, report)}`);
499
518
  return lines.join("\n");
500
519
  }
501
- function requireActivationRoot(input, _command) {
502
- // Use the shared auto-detect chain for ALL commands:
503
- // explicit flag ~/.openclawbrain/activation → extension scan → clear error
520
+ function requireActivationRoot(input, openclawHome, command) {
521
+ const explicitActivationRoot = input.activationRoot.trim().length > 0 ? input.activationRoot : null;
522
+ if (explicitActivationRoot !== null) {
523
+ return path.resolve(explicitActivationRoot);
524
+ }
525
+ if (openclawHome !== null) {
526
+ return resolveActivationRoot({
527
+ openclawHome
528
+ });
529
+ }
530
+ throw new Error(`${command} requires --activation-root <path> or --openclaw-home <path>; unpinned host auto-resolution is no longer supported for ${command}.`);
531
+ }
532
+ function resolveCliActivationRoot(explicitActivationRoot, openclawHome) {
504
533
  return resolveActivationRoot({
505
- explicit: input.activationRoot.trim().length > 0 ? input.activationRoot : null,
534
+ explicit: explicitActivationRoot,
535
+ openclawHome
506
536
  });
507
537
  }
508
538
  function readJsonFile(filePath) {
@@ -571,11 +601,14 @@ export function parseOperatorCliArgs(argv) {
571
601
  if (args[0] === "doctor") {
572
602
  throw new Error(buildDoctorDeletedMessage(args.slice(1)));
573
603
  }
604
+ if (args[0] === "setup") {
605
+ throw new Error(buildSetupDeletedMessage());
606
+ }
574
607
  if (args[0] === "daemon") {
575
608
  args.shift();
576
609
  return parseDaemonArgs(args);
577
610
  }
578
- if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "setup" || args[0] === "detach" || args[0] === "uninstall" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
611
+ if (args[0] === "status" || args[0] === "rollback" || args[0] === "scan" || args[0] === "attach" || args[0] === "install" || args[0] === "detach" || args[0] === "uninstall" || args[0] === "context" || args[0] === "history" || args[0] === "learn" || args[0] === "watch" || args[0] === "export" || args[0] === "import" || args[0] === "reset") {
579
612
  command = args.shift();
580
613
  }
581
614
  if (command === "learn") {
@@ -598,6 +631,15 @@ export function parseOperatorCliArgs(argv) {
598
631
  index += 1;
599
632
  continue;
600
633
  }
634
+ if (arg === "--openclaw-home") {
635
+ const next = args[index + 1];
636
+ if (next === undefined) {
637
+ throw new Error("--openclaw-home requires a value");
638
+ }
639
+ openclawHome = next;
640
+ index += 1;
641
+ continue;
642
+ }
601
643
  if (arg.startsWith("--")) {
602
644
  throw new Error(`unknown argument for learn: ${arg}`);
603
645
  }
@@ -607,7 +649,7 @@ export function parseOperatorCliArgs(argv) {
607
649
  }
608
650
  return {
609
651
  command,
610
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
652
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
611
653
  json,
612
654
  help
613
655
  };
@@ -663,9 +705,12 @@ export function parseOperatorCliArgs(argv) {
663
705
  if (help) {
664
706
  return { command, activationRoot: "", scanRoot: null, interval: 30, json, help };
665
707
  }
708
+ if (activationRoot === null || activationRoot.trim().length === 0) {
709
+ throw new Error("watch requires --activation-root <path>");
710
+ }
666
711
  return {
667
712
  command,
668
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
713
+ activationRoot: path.resolve(activationRoot),
669
714
  scanRoot: watchScanRoot,
670
715
  interval: watchInterval,
671
716
  json,
@@ -693,6 +738,15 @@ export function parseOperatorCliArgs(argv) {
693
738
  index += 1;
694
739
  continue;
695
740
  }
741
+ if (arg === "--openclaw-home") {
742
+ const next = args[index + 1];
743
+ if (next === undefined) {
744
+ throw new Error("--openclaw-home requires a value");
745
+ }
746
+ openclawHome = next;
747
+ index += 1;
748
+ continue;
749
+ }
696
750
  if (arg.startsWith("--")) {
697
751
  throw new Error(`unknown argument for context: ${arg}`);
698
752
  }
@@ -707,7 +761,7 @@ export function parseOperatorCliArgs(argv) {
707
761
  return {
708
762
  command,
709
763
  message: messageParts.join(" "),
710
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
764
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
711
765
  json,
712
766
  help
713
767
  };
@@ -733,6 +787,15 @@ export function parseOperatorCliArgs(argv) {
733
787
  index += 1;
734
788
  continue;
735
789
  }
790
+ if (arg === "--openclaw-home") {
791
+ const next = args[index + 1];
792
+ if (next === undefined) {
793
+ throw new Error("--openclaw-home requires a value");
794
+ }
795
+ openclawHome = next;
796
+ index += 1;
797
+ continue;
798
+ }
736
799
  if (arg === "--limit") {
737
800
  const next = args[index + 1];
738
801
  if (next === undefined) {
@@ -755,7 +818,7 @@ export function parseOperatorCliArgs(argv) {
755
818
  }
756
819
  return {
757
820
  command,
758
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
821
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
759
822
  limit: historyLimit,
760
823
  json,
761
824
  help
@@ -781,6 +844,14 @@ export function parseOperatorCliArgs(argv) {
781
844
  index += 1;
782
845
  continue;
783
846
  }
847
+ if (arg === "--openclaw-home") {
848
+ const next = args[index + 1];
849
+ if (next === undefined)
850
+ throw new Error("--openclaw-home requires a value");
851
+ openclawHome = next;
852
+ index += 1;
853
+ continue;
854
+ }
784
855
  if (arg === "-o" || arg === "--output") {
785
856
  const next = args[index + 1];
786
857
  if (next === undefined)
@@ -798,7 +869,7 @@ export function parseOperatorCliArgs(argv) {
798
869
  throw new Error("export requires -o <output.tar.gz>");
799
870
  return {
800
871
  command,
801
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
872
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
802
873
  outputPath: path.resolve(outputPath),
803
874
  json,
804
875
  help,
@@ -829,6 +900,14 @@ export function parseOperatorCliArgs(argv) {
829
900
  index += 1;
830
901
  continue;
831
902
  }
903
+ if (arg === "--openclaw-home") {
904
+ const next = args[index + 1];
905
+ if (next === undefined)
906
+ throw new Error("--openclaw-home requires a value");
907
+ openclawHome = next;
908
+ index += 1;
909
+ continue;
910
+ }
832
911
  if (arg.startsWith("--"))
833
912
  throw new Error(`unknown argument for import: ${arg}`);
834
913
  if (archivePath === null) {
@@ -845,7 +924,7 @@ export function parseOperatorCliArgs(argv) {
845
924
  return {
846
925
  command,
847
926
  archivePath: path.resolve(archivePath),
848
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
927
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
849
928
  force,
850
929
  json,
851
930
  help,
@@ -875,13 +954,21 @@ export function parseOperatorCliArgs(argv) {
875
954
  index += 1;
876
955
  continue;
877
956
  }
957
+ if (arg === "--openclaw-home") {
958
+ const next = args[index + 1];
959
+ if (next === undefined)
960
+ throw new Error("--openclaw-home requires a value");
961
+ openclawHome = next;
962
+ index += 1;
963
+ continue;
964
+ }
878
965
  throw new Error(`unknown argument for reset: ${arg}`);
879
966
  }
880
967
  if (help)
881
968
  return { command, activationRoot: "", yes: false, json, help };
882
969
  return {
883
970
  command,
884
- activationRoot: resolveActivationRoot({ explicit: activationRoot }),
971
+ activationRoot: resolveCliActivationRoot(activationRoot, openclawHome),
885
972
  yes,
886
973
  json,
887
974
  help
@@ -1065,7 +1152,7 @@ export function parseOperatorCliArgs(argv) {
1065
1152
  if (command !== "uninstall" && purgeData) {
1066
1153
  throw new Error("--purge-data only applies to uninstall");
1067
1154
  }
1068
- if (command === "install" || command === "setup") {
1155
+ if (command === "install") {
1069
1156
  if (help) {
1070
1157
  return {
1071
1158
  command,
@@ -1080,9 +1167,9 @@ export function parseOperatorCliArgs(argv) {
1080
1167
  help
1081
1168
  };
1082
1169
  }
1083
- const resolvedOpenclawHome = resolveSetupOpenClawHome(openclawHome);
1084
- const resolvedActivationRoot = resolveSetupActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
1085
- const resolvedWorkspaceId = resolveSetupWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
1170
+ const resolvedOpenclawHome = resolveInstallOpenClawHome(openclawHome);
1171
+ const resolvedActivationRoot = resolveInstallActivationRoot(resolvedOpenclawHome.openclawHome, activationRoot);
1172
+ const resolvedWorkspaceId = resolveInstallWorkspaceId(resolvedOpenclawHome.openclawHome, workspaceId);
1086
1173
  return {
1087
1174
  command,
1088
1175
  openclawHome: resolvedOpenclawHome.openclawHome,
@@ -1219,6 +1306,7 @@ export function parseOperatorCliArgs(argv) {
1219
1306
  updatedAt,
1220
1307
  brainAttachmentPolicy
1221
1308
  },
1309
+ openclawHome: normalizeOptionalCliString(openclawHome),
1222
1310
  json,
1223
1311
  help,
1224
1312
  dryRun,
@@ -1282,6 +1370,36 @@ const LOCAL_WORKSPACE_EXTENSION_PACKAGES = [
1282
1370
  "provenance",
1283
1371
  "workspace-metadata"
1284
1372
  ];
1373
+ const OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV = "OPENCLAWBRAIN_EXTENSION_TARBALL_DIR";
1374
+ function resolveNpmCommand() {
1375
+ return process.platform === "win32" ? "npm.cmd" : "npm";
1376
+ }
1377
+ function resolveExtensionInstallReleaseTarballs() {
1378
+ const configuredDir = normalizeOptionalCliString(process.env[OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV]);
1379
+ if (configuredDir === null) {
1380
+ return null;
1381
+ }
1382
+ const artifactDir = path.resolve(configuredDir);
1383
+ let entries;
1384
+ try {
1385
+ entries = readdirSync(artifactDir, { withFileTypes: true });
1386
+ }
1387
+ catch (error) {
1388
+ const detail = error instanceof Error ? error.message : String(error);
1389
+ throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} is unreadable: ${artifactDir} (${detail})`);
1390
+ }
1391
+ const tarballs = entries
1392
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
1393
+ .map((entry) => path.join(artifactDir, entry.name))
1394
+ .sort((left, right) => left.localeCompare(right));
1395
+ if (tarballs.length === 0) {
1396
+ throw new Error(`${OPENCLAWBRAIN_EXTENSION_TARBALL_DIR_ENV} has no .tgz release artifacts: ${artifactDir}`);
1397
+ }
1398
+ return {
1399
+ artifactDir,
1400
+ tarballs
1401
+ };
1402
+ }
1285
1403
  function resolveLocalWorkspaceRootForExtensionInstall() {
1286
1404
  const candidates = [
1287
1405
  path.resolve(__dirname, "..", "..", "..", ".."),
@@ -1458,6 +1576,85 @@ function buildHistoryEntry(record, slot, isActive) {
1458
1576
  current: isActive
1459
1577
  };
1460
1578
  }
1579
+ function formatInspectionFindings(findings) {
1580
+ return findings.join("; ");
1581
+ }
1582
+ function buildInstallRefusalError(parsed, detail) {
1583
+ const purgeCommand = `openclawbrain uninstall --openclaw-home ${quoteShellArg(parsed.openclawHome)} ` +
1584
+ `--activation-root ${quoteShellArg(parsed.activationRoot)} --purge-data`;
1585
+ return new Error(`Refusing to reuse activation root ${path.resolve(parsed.activationRoot)}: ${detail}. ` +
1586
+ "Install only repairs an empty first-state root; it will not overwrite populated or broken activation state. " +
1587
+ `Inspect: ${buildInstallStatusCommand(parsed.activationRoot)}. ` +
1588
+ `Reset: ${purgeCommand}.`);
1589
+ }
1590
+ function inspectInstallActivationPlan(parsed) {
1591
+ const resolvedActivationRoot = path.resolve(parsed.activationRoot);
1592
+ const activationPointersPath = path.join(resolvedActivationRoot, "activation-pointers.json");
1593
+ if (!existsSync(resolvedActivationRoot)) {
1594
+ return {
1595
+ createActivationRoot: true,
1596
+ action: "bootstrap",
1597
+ resolution: "new_root",
1598
+ inspectionStep: "Activation state inspection: activation root is missing; bootstrapping first state.",
1599
+ activePackId: null
1600
+ };
1601
+ }
1602
+ const activationRootStats = statSync(resolvedActivationRoot);
1603
+ if (!activationRootStats.isDirectory()) {
1604
+ throw buildInstallRefusalError(parsed, "activation root path exists but is not a directory");
1605
+ }
1606
+ if (!existsSync(activationPointersPath)) {
1607
+ return {
1608
+ createActivationRoot: false,
1609
+ action: "bootstrap",
1610
+ resolution: "missing_pointers",
1611
+ inspectionStep: "Activation state inspection: activation root exists but activation-pointers.json is missing; bootstrapping first state.",
1612
+ activePackId: null
1613
+ };
1614
+ }
1615
+ let inspection;
1616
+ try {
1617
+ inspection = inspectActivationState(resolvedActivationRoot, new Date().toISOString());
1618
+ }
1619
+ catch (error) {
1620
+ const detail = error instanceof Error ? error.message : String(error);
1621
+ throw buildInstallRefusalError(parsed, `activation pointers could not be inspected (${detail})`);
1622
+ }
1623
+ if (inspection.active === null && inspection.candidate === null && inspection.previous === null) {
1624
+ return {
1625
+ createActivationRoot: false,
1626
+ action: "bootstrap",
1627
+ resolution: "empty_pointers",
1628
+ inspectionStep: "Activation state inspection: activation pointers are present but all slots are empty; bootstrapping first state.",
1629
+ activePackId: null
1630
+ };
1631
+ }
1632
+ const unhealthySlots = [inspection.active, inspection.candidate, inspection.previous]
1633
+ .filter((slot) => slot !== null && !slot.activationReady)
1634
+ .map((slot) => `${slot.slot}: ${formatInspectionFindings(slot.findings)}`);
1635
+ if (unhealthySlots.length > 0) {
1636
+ throw buildInstallRefusalError(parsed, `activation state contains unhealthy slots (${unhealthySlots.join(" | ")})`);
1637
+ }
1638
+ if (inspection.active === null) {
1639
+ const populatedSlots = [inspection.candidate, inspection.previous]
1640
+ .filter((slot) => slot !== null)
1641
+ .map((slot) => slot.slot);
1642
+ throw buildInstallRefusalError(parsed, `activation state is populated without an active pack (${populatedSlots.join(", ")})`);
1643
+ }
1644
+ if (inspection.candidate !== null && !inspection.promotion.allowed) {
1645
+ throw buildInstallRefusalError(parsed, `candidate slot is stale or incoherent (${formatInspectionFindings(inspection.promotion.findings)})`);
1646
+ }
1647
+ if (inspection.previous !== null && !inspection.rollback.allowed) {
1648
+ throw buildInstallRefusalError(parsed, `previous slot is stale or incoherent (${formatInspectionFindings(inspection.rollback.findings)})`);
1649
+ }
1650
+ return {
1651
+ createActivationRoot: false,
1652
+ action: "keep",
1653
+ resolution: "healthy_existing",
1654
+ inspectionStep: `Activation state inspection: active pack ${inspection.active.packId} is healthy; keeping existing activation state.`,
1655
+ activePackId: inspection.active.packId
1656
+ };
1657
+ }
1461
1658
  function runHistoryCommand(parsed) {
1462
1659
  const activationRoot = parsed.activationRoot;
1463
1660
  const pointersPath = path.join(activationRoot, "activation-pointers.json");
@@ -1539,29 +1736,25 @@ function runHistoryCommand(parsed) {
1539
1736
  }
1540
1737
  return 0;
1541
1738
  }
1542
- function runSetupCommand(parsed) {
1739
+ function runInstallCommand(parsed) {
1543
1740
  const steps = [];
1544
1741
  const commandLabel = parsed.command.toUpperCase();
1545
- const activationRootCreated = !existsSync(parsed.activationRoot);
1546
- let bootstrapped = false;
1547
- steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatSetupOpenClawHomeSource(parsed.openclawHomeSource)})`);
1742
+ steps.push(`Target OpenClaw profile home: ${parsed.openclawHome} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`);
1548
1743
  // 1. Validate --openclaw-home exists and has openclaw.json
1549
1744
  validateOpenClawHome(parsed.openclawHome);
1550
- // 2. Create activation root if needed
1551
- if (activationRootCreated) {
1745
+ // 2. Inspect the activation root before writing profile hook artifacts.
1746
+ const activationPlan = inspectInstallActivationPlan(parsed);
1747
+ // 3. Create activation root if needed
1748
+ if (activationPlan.createActivationRoot) {
1552
1749
  mkdirSync(parsed.activationRoot, { recursive: true });
1553
1750
  steps.push(`Created activation root: ${parsed.activationRoot}`);
1554
1751
  }
1555
1752
  else {
1556
1753
  steps.push(`Activation root exists: ${parsed.activationRoot}`);
1557
1754
  }
1558
- // 3. Run bootstrapRuntimeAttach if not already attached
1559
- const activationPointersPath = path.join(parsed.activationRoot, "activation-pointers.json");
1560
- if (existsSync(activationPointersPath)) {
1561
- steps.push("Brain already attached (activation-pointers.json exists), skipping bootstrap.");
1562
- }
1563
- else {
1564
- bootstrapped = true;
1755
+ steps.push(activationPlan.inspectionStep);
1756
+ // 4. Bootstrap only for safe empty first-state roots; otherwise keep the inspected healthy state.
1757
+ if (activationPlan.action === "bootstrap") {
1565
1758
  const packRoot = path.resolve(parsed.activationRoot, "packs", "initial");
1566
1759
  mkdirSync(packRoot, { recursive: true });
1567
1760
  const brainAttachmentPolicy = parsed.shared ? "shared" : "dedicated";
@@ -1570,27 +1763,30 @@ function runSetupCommand(parsed) {
1570
1763
  brainAttachmentPolicy,
1571
1764
  activationRoot: parsed.activationRoot,
1572
1765
  packRoot,
1573
- packLabel: "setup-cli",
1766
+ packLabel: "install-cli",
1574
1767
  workspace: {
1575
1768
  workspaceId: parsed.workspaceId,
1576
- snapshotId: `${parsed.workspaceId}@setup-${new Date().toISOString().slice(0, 10)}`,
1769
+ snapshotId: `${parsed.workspaceId}@install-${new Date().toISOString().slice(0, 10)}`,
1577
1770
  capturedAt: new Date().toISOString(),
1578
1771
  rootDir: parsed.openclawHome,
1579
- revision: "cli-setup-v1"
1772
+ revision: "cli-install-v1"
1580
1773
  },
1581
1774
  interactionEvents: [],
1582
1775
  feedbackEvents: []
1583
1776
  });
1584
- steps.push(`Bootstrapped brain attach: ${result.status}`);
1777
+ steps.push(`Bootstrapped brain attach: state=${result.currentProfile.brain.state} awaitingFirstExport=${yesNo(result.currentProfile.brainStatus.awaitingFirstExport)}`);
1778
+ }
1779
+ else {
1780
+ steps.push(`Kept inspected activation state: active pack ${activationPlan.activePackId}`);
1585
1781
  }
1586
- // 4-7. Write extension files
1782
+ // 5-8. Write extension files
1587
1783
  const extensionDir = path.join(parsed.openclawHome, "extensions", "openclawbrain");
1588
1784
  mkdirSync(extensionDir, { recursive: true });
1589
- // 4. Write index.ts
1785
+ // 5. Write index.ts
1590
1786
  const indexTsPath = path.join(extensionDir, "index.ts");
1591
1787
  writeFileSync(indexTsPath, buildExtensionIndexTs(parsed.activationRoot), "utf8");
1592
1788
  steps.push(`Wrote extension: ${indexTsPath}`);
1593
- // 4b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
1789
+ // 5b. Write runtime-guard files (imported by index.ts as ./runtime-guard.js)
1594
1790
  const runtimeGuardPaths = resolveExtensionRuntimeGuardPath();
1595
1791
  if (runtimeGuardPaths.ts !== null) {
1596
1792
  const runtimeGuardTsPath = path.join(extensionDir, "runtime-guard.ts");
@@ -1600,17 +1796,31 @@ function runSetupCommand(parsed) {
1600
1796
  const runtimeGuardJsPath = path.join(extensionDir, "runtime-guard.js");
1601
1797
  writeFileSync(runtimeGuardJsPath, readFileSync(runtimeGuardPaths.js, "utf8"), "utf8");
1602
1798
  steps.push(`Wrote extension runtime-guard: ${runtimeGuardJsPath}`);
1603
- // 5. Write package.json
1799
+ // 6. Write package.json
1604
1800
  const packageJsonPath = path.join(extensionDir, "package.json");
1605
1801
  writeFileSync(packageJsonPath, buildExtensionPackageJson(), "utf8");
1606
1802
  steps.push(`Wrote package.json: ${packageJsonPath}`);
1607
- // 6. npm install
1803
+ // 7. npm install
1804
+ const releaseTarballInstall = resolveExtensionInstallReleaseTarballs();
1608
1805
  try {
1609
- execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
1610
- steps.push("Ran npm install --ignore-scripts");
1806
+ if (releaseTarballInstall !== null) {
1807
+ execFileSync(resolveNpmCommand(), ["install", "--ignore-scripts", "--no-save", ...releaseTarballInstall.tarballs], { cwd: extensionDir, stdio: "pipe" });
1808
+ steps.push(`Installed extension dependencies from release artifacts: ${releaseTarballInstall.tarballs.length} tarballs from ${releaseTarballInstall.artifactDir}`);
1809
+ }
1810
+ else {
1811
+ execSync("npm install --ignore-scripts", { cwd: extensionDir, stdio: "pipe" });
1812
+ steps.push("Ran npm install --ignore-scripts");
1813
+ const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
1814
+ if (linkedPackages !== null) {
1815
+ steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
1816
+ }
1817
+ }
1611
1818
  }
1612
1819
  catch (err) {
1613
1820
  const message = err instanceof Error ? err.message : String(err);
1821
+ if (releaseTarballInstall !== null) {
1822
+ throw new Error(`Extension dependency install from release artifacts failed: ${message}`);
1823
+ }
1614
1824
  const linkedPackages = installExtensionFromLocalWorkspaceBuild(extensionDir);
1615
1825
  if (linkedPackages !== null) {
1616
1826
  steps.push(`Linked coherent local workspace packages: ${linkedPackages.join(", ")}`);
@@ -1619,19 +1829,19 @@ function runSetupCommand(parsed) {
1619
1829
  steps.push(`npm install failed (non-fatal): ${message}`);
1620
1830
  }
1621
1831
  }
1622
- // 7. Write plugin manifest
1832
+ // 8. Write plugin manifest
1623
1833
  const manifestPath = path.join(extensionDir, "openclaw.plugin.json");
1624
1834
  writeFileSync(manifestPath, buildExtensionPluginManifest(), "utf8");
1625
1835
  steps.push(`Wrote manifest: ${manifestPath}`);
1626
- // 8. Write BRAIN.md to workspace directories
1836
+ // 9. Write BRAIN.md to workspace directories
1627
1837
  const brainMdContent = [
1628
1838
  "## OpenClawBrain",
1629
1839
  `You have a learning brain attached at ${parsed.activationRoot}.`,
1630
1840
  "- It learns automatically from your conversations",
1631
1841
  '- Corrections matter — "no, actually X" teaches the brain X',
1632
1842
  "- You don't manage it — background daemon handles learning",
1633
- "- Check: `openclawbrain status`",
1634
- "- Rollback: `openclawbrain rollback`",
1843
+ `- Check: \`openclawbrain status --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
1844
+ `- Rollback: \`openclawbrain rollback --activation-root ${quoteShellArg(parsed.activationRoot)}\``,
1635
1845
  '- See what brain knows: `openclawbrain context "your question"`',
1636
1846
  ""
1637
1847
  ].join("\n");
@@ -1705,16 +1915,24 @@ function runSetupCommand(parsed) {
1705
1915
  `Check status: ${buildInstallStatusCommand(parsed.activationRoot)}`
1706
1916
  ];
1707
1917
  const lifecycleSummary = [
1708
- `OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${formatSetupOpenClawHomeSource(parsed.openclawHomeSource)})`,
1709
- `Activation root: ${shortenPath(parsed.activationRoot)} (${formatSetupActivationRootSource(parsed.activationRootSource)})`,
1710
- `Workspace ID: ${parsed.workspaceId} (${formatSetupWorkspaceIdSource(parsed.workspaceIdSource)})`,
1918
+ `OpenClaw profile: ${shortenPath(parsed.openclawHome)} (${formatInstallOpenClawHomeSource(parsed.openclawHomeSource)})`,
1919
+ `Activation root: ${shortenPath(parsed.activationRoot)} (${formatInstallActivationRootSource(parsed.activationRootSource)})`,
1920
+ `Workspace ID: ${parsed.workspaceId} (${formatInstallWorkspaceIdSource(parsed.workspaceIdSource)})`,
1711
1921
  `Profile hook: installed at ${shortenPath(extensionDir)}`,
1712
- activationRootCreated
1922
+ activationPlan.resolution === "new_root"
1713
1923
  ? `Activation data: initialized at ${shortenPath(parsed.activationRoot)}`
1714
- : `Activation data: reused at ${shortenPath(parsed.activationRoot)}`,
1715
- bootstrapped
1716
- ? "Brain attach: bootstrapped a seed/current-profile attach"
1717
- : "Brain attach: existing activation state kept in place"
1924
+ : activationPlan.resolution === "missing_pointers"
1925
+ ? `Activation data: repaired missing pointers at ${shortenPath(parsed.activationRoot)}`
1926
+ : activationPlan.resolution === "empty_pointers"
1927
+ ? `Activation data: repaired empty pointers at ${shortenPath(parsed.activationRoot)}`
1928
+ : `Activation data: reused healthy state at ${shortenPath(parsed.activationRoot)}`,
1929
+ activationPlan.action === "bootstrap"
1930
+ ? activationPlan.resolution === "new_root"
1931
+ ? "Brain attach: bootstrapped a seed/current-profile attach"
1932
+ : 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`
1718
1936
  ];
1719
1937
  // 9. Print summary
1720
1938
  if (parsed.json) {
@@ -2341,16 +2559,9 @@ function formatTimestamp() {
2341
2559
  function watchLog(message) {
2342
2560
  console.log(`${formatTimestamp()} ${message}`);
2343
2561
  }
2344
- function resolveOperatorTeacherSnapshotPath(activationRoot, explicitPath) {
2345
- if (explicitPath !== null && explicitPath !== undefined) {
2346
- return explicitPath;
2347
- }
2348
- const asyncSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(activationRoot);
2349
- return existsSync(asyncSnapshotPath) ? asyncSnapshotPath : null;
2562
+ function formatWatchError(error) {
2563
+ return error instanceof Error ? error.message : String(error);
2350
2564
  }
2351
- const WATCH_STATE_DIRNAME = "watch";
2352
- const WATCH_SESSION_TAIL_CURSOR_BASENAME = "session-tail-cursor.json";
2353
- const WATCH_TEACHER_SNAPSHOT_BASENAME = "teacher-snapshot.json";
2354
2565
  function sanitizeWatchPathSegment(value) {
2355
2566
  const sanitized = value
2356
2567
  .replace(/[^a-zA-Z0-9._-]+/g, "-")
@@ -2358,15 +2569,6 @@ function sanitizeWatchPathSegment(value) {
2358
2569
  .slice(0, 96);
2359
2570
  return sanitized.length > 0 ? sanitized : "session";
2360
2571
  }
2361
- function resolveWatchStateRoot(activationRoot) {
2362
- return path.resolve(activationRoot, WATCH_STATE_DIRNAME);
2363
- }
2364
- function resolveWatchSessionTailCursorPath(activationRoot) {
2365
- return path.join(resolveWatchStateRoot(activationRoot), WATCH_SESSION_TAIL_CURSOR_BASENAME);
2366
- }
2367
- function resolveWatchTeacherSnapshotPath(activationRoot) {
2368
- return path.join(resolveWatchStateRoot(activationRoot), WATCH_TEACHER_SNAPSHOT_BASENAME);
2369
- }
2370
2572
  function readOptionalJsonFile(filePath) {
2371
2573
  if (!existsSync(filePath)) {
2372
2574
  return null;
@@ -2400,28 +2602,8 @@ function persistWatchSessionTailCursor(cursorPath, cursor) {
2400
2602
  cursor
2401
2603
  });
2402
2604
  }
2403
- function loadWatchTeacherSnapshotState(snapshotPath) {
2404
- const parsed = readOptionalJsonFile(snapshotPath);
2405
- return {
2406
- lastHandledMaterializationPackId: parsed !== null && typeof parsed.lastHandledMaterializationPackId === "string"
2407
- ? parsed.lastHandledMaterializationPackId
2408
- : null
2409
- };
2410
- }
2411
- function persistWatchTeacherSnapshot(snapshotPath, input) {
2412
- writeJsonFile(snapshotPath, {
2413
- contract: "openclaw_watch_teacher_snapshot.v1",
2414
- runtimeOwner: "openclaw",
2415
- updatedAt: new Date().toISOString(),
2416
- scanRoot: input.scanRoot,
2417
- replayedBundleCount: input.replayedBundleCount,
2418
- replayedEventCount: input.replayedEventCount,
2419
- exportedBundleCount: input.exportedBundleCount,
2420
- exportedEventCount: input.exportedEventCount,
2421
- localSessionTailNoopReason: input.localSessionTailNoopReason,
2422
- lastHandledMaterializationPackId: input.lastHandledMaterializationPackId,
2423
- snapshot: input.snapshot
2424
- });
2605
+ function countWatchCursorBridgedEvents(cursor) {
2606
+ return cursor.reduce((sum, entry) => sum + entry.bridgedEventCount, 0);
2425
2607
  }
2426
2608
  function listWatchRuntimeEventExportBundleRoots(scanRoot) {
2427
2609
  if (!existsSync(scanRoot)) {
@@ -2528,7 +2710,8 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2528
2710
  return {
2529
2711
  lastHandledMaterializationPackId,
2530
2712
  logLine: null,
2531
- materializedPackId: null
2713
+ materializedPackId: null,
2714
+ failure: null
2532
2715
  };
2533
2716
  }
2534
2717
  const packId = typeof materialization?.candidate?.summary?.packId === "string"
@@ -2538,14 +2721,28 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2538
2721
  return {
2539
2722
  lastHandledMaterializationPackId,
2540
2723
  logLine: null,
2541
- materializedPackId: packId
2724
+ materializedPackId: packId,
2725
+ failure: null
2542
2726
  };
2543
2727
  }
2544
2728
  const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
2545
2729
  try {
2546
2730
  const candidateRootDir = path.resolve(activationRoot, "packs", packId);
2547
2731
  mkdirSync(candidateRootDir, { recursive: true });
2548
- materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
2732
+ let activeBeforePack = null;
2733
+ try {
2734
+ activeBeforePack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
2735
+ }
2736
+ catch {
2737
+ activeBeforePack = null;
2738
+ }
2739
+ const candidateDescriptor = materializeAlwaysOnLearningCandidatePack(candidateRootDir, materialization);
2740
+ appendLearningUpdateLogs({
2741
+ activationRoot,
2742
+ materialization,
2743
+ activeBeforePack,
2744
+ candidateDescriptor
2745
+ });
2549
2746
  const now = new Date().toISOString();
2550
2747
  stageCandidatePack(activationRoot, candidateRootDir, {
2551
2748
  updatedAt: now,
@@ -2560,13 +2757,15 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2560
2757
  return {
2561
2758
  lastHandledMaterializationPackId: packId,
2562
2759
  materializedPackId: packId,
2563
- logLine: `Promoted ${shortPackId} → active`
2760
+ logLine: `Promoted ${shortPackId} → active`,
2761
+ failure: null
2564
2762
  };
2565
2763
  }
2566
2764
  return {
2567
2765
  lastHandledMaterializationPackId: packId,
2568
2766
  materializedPackId: packId,
2569
- logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`
2767
+ logLine: `Staged ${shortPackId} (promotion blocked: ${inspection.promotion.findings.join(", ")})`,
2768
+ failure: null
2570
2769
  };
2571
2770
  }
2572
2771
  catch (error) {
@@ -2574,26 +2773,77 @@ function applyWatchMaterialization(activationRoot, snapshot, lastHandledMaterial
2574
2773
  return {
2575
2774
  lastHandledMaterializationPackId,
2576
2775
  materializedPackId: packId,
2577
- logLine: `Promotion failed for ${shortPackId}: ${message}`
2776
+ logLine: `Promotion failed for ${shortPackId}: ${message}`,
2777
+ failure: {
2778
+ mode: "materialization_failed",
2779
+ detail: message,
2780
+ at: new Date().toISOString()
2781
+ }
2782
+ };
2783
+ }
2784
+ }
2785
+ function resolveWatchTeacherLabelerConfig(input) {
2786
+ if (input !== undefined) {
2787
+ return {
2788
+ teacherLabeler: input,
2789
+ warnings: []
2578
2790
  };
2579
2791
  }
2792
+ const providerConfig = readOpenClawBrainProviderConfig(process.env);
2793
+ const warnings = providerConfig.warnings.filter((warning) => /OPENCLAWBRAIN_TEACHER_/u.test(warning));
2794
+ if (providerConfig.teacher.provider !== "ollama") {
2795
+ return {
2796
+ teacherLabeler: null,
2797
+ warnings
2798
+ };
2799
+ }
2800
+ return {
2801
+ teacherLabeler: {
2802
+ provider: "ollama",
2803
+ baseUrl: providerConfig.teacherBaseUrl,
2804
+ model: providerConfig.teacher.model,
2805
+ ...(providerConfig.teacher.timeoutMs === undefined ? {} : { timeoutMs: providerConfig.teacher.timeoutMs }),
2806
+ ...(providerConfig.teacher.maxPromptChars === undefined ? {} : { maxPromptChars: providerConfig.teacher.maxPromptChars }),
2807
+ ...(providerConfig.teacher.maxResponseChars === undefined ? {} : { maxResponseChars: providerConfig.teacher.maxResponseChars }),
2808
+ ...(providerConfig.teacher.maxOutputTokens === undefined ? {} : { maxOutputTokens: providerConfig.teacher.maxOutputTokens }),
2809
+ ...(providerConfig.teacher.maxArtifactsPerExport === undefined
2810
+ ? {}
2811
+ : { maxArtifactsPerExport: providerConfig.teacher.maxArtifactsPerExport }),
2812
+ ...(providerConfig.teacher.maxInteractionsPerExport === undefined
2813
+ ? {}
2814
+ : { maxInteractionsPerExport: providerConfig.teacher.maxInteractionsPerExport })
2815
+ },
2816
+ warnings
2817
+ };
2580
2818
  }
2581
2819
  export async function createWatchCommandRuntime(input) {
2582
2820
  const activationRoot = path.resolve(input.activationRoot);
2821
+ const bootstrapObservedAt = new Date().toISOString();
2583
2822
  const scanRoot = input.scanRoot !== undefined && input.scanRoot !== null
2584
2823
  ? path.resolve(input.scanRoot)
2585
2824
  : path.resolve(activationRoot, "event-exports");
2586
2825
  const sessionTailCursorPath = resolveWatchSessionTailCursorPath(activationRoot);
2587
2826
  const teacherSnapshotPath = resolveWatchTeacherSnapshotPath(activationRoot);
2827
+ const restoredTeacherState = loadWatchTeacherSnapshotState(teacherSnapshotPath);
2588
2828
  const log = input.log ?? watchLog;
2829
+ const startupWarnings = [];
2589
2830
  mkdirSync(scanRoot, { recursive: true });
2590
2831
  mkdirSync(resolveWatchStateRoot(activationRoot), { recursive: true });
2591
2832
  log(`Watch starting — activation: ${shortenPath(activationRoot)}`);
2592
2833
  log(`Scan root: ${shortenPath(scanRoot)}`);
2593
2834
  log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
2835
+ const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler);
2836
+ const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
2837
+ for (const warning of resolvedTeacherLabeler.warnings) {
2838
+ startupWarnings.push(`teacher_env_warning:${warning}`);
2839
+ log(`Teacher env warning: ${warning}`);
2840
+ }
2841
+ if (teacherLabeler?.provider === "ollama") {
2842
+ log(`Teacher labeler: provider=ollama model=${teacherLabeler.model ?? "qwen3.5:9b"}`);
2843
+ }
2594
2844
  const scanner = createRuntimeEventExportScanner({ scanRoot });
2595
2845
  let lastServeTimeFallbackReason = null;
2596
- const teacherLoop = createAsyncTeacherLiveLoop({
2846
+ const baseTeacherLoopInput = {
2597
2847
  packLabel: "watch-cli",
2598
2848
  workspace: {
2599
2849
  workspaceId: "watch-cli",
@@ -2603,6 +2853,7 @@ export async function createWatchCommandRuntime(input) {
2603
2853
  revision: "watch-cli-v2"
2604
2854
  },
2605
2855
  learnedRouting: true,
2856
+ ...(teacherLabeler !== null ? { teacherLabeler } : {}),
2606
2857
  resolveLearnedRoutingState: () => {
2607
2858
  const resolved = resolveServeTimeLearningRuntimeInput(activationRoot);
2608
2859
  if (resolved.fallbackReason !== null && resolved.fallbackReason !== lastServeTimeFallbackReason) {
@@ -2620,11 +2871,38 @@ export async function createWatchCommandRuntime(input) {
2620
2871
  persistBaseline(activationRoot, state);
2621
2872
  }
2622
2873
  catch (error) {
2623
- const message = error instanceof Error ? error.message : String(error);
2624
- log(`Baseline persist failed: ${message}`);
2874
+ log(`Baseline persist failed: ${formatWatchError(error)}`);
2625
2875
  }
2626
2876
  }
2627
- });
2877
+ };
2878
+ let teacherLoop;
2879
+ let lastHandledMaterializationPackId = restoredTeacherState.lastHandledMaterializationPackId;
2880
+ if (restoredTeacherState.error !== null) {
2881
+ const message = restoredTeacherState.error;
2882
+ startupWarnings.push(`teacher_snapshot_reset:${message}`);
2883
+ lastHandledMaterializationPackId = null;
2884
+ log(`Teacher snapshot reset: ${message}`);
2885
+ teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
2886
+ }
2887
+ else {
2888
+ try {
2889
+ teacherLoop = createAsyncTeacherLiveLoop({
2890
+ ...baseTeacherLoopInput,
2891
+ ...(restoredTeacherState.snapshot !== null ? { resumeFromSnapshot: restoredTeacherState.snapshot } : {})
2892
+ });
2893
+ }
2894
+ catch (error) {
2895
+ const message = formatWatchError(error);
2896
+ startupWarnings.push(`teacher_snapshot_reset:${message}`);
2897
+ lastHandledMaterializationPackId = null;
2898
+ log(`Teacher snapshot reset: ${message}`);
2899
+ teacherLoop = createAsyncTeacherLiveLoop(baseTeacherLoopInput);
2900
+ }
2901
+ }
2902
+ if (restoredTeacherState.snapshot !== null && startupWarnings.length === 0) {
2903
+ const restoredSeenExportCount = restoredTeacherState.snapshot.state?.seenExportDigests.length ?? 0;
2904
+ log(`Restored teacher snapshot: seen=${restoredSeenExportCount} artifacts=${restoredTeacherState.snapshot.teacher.artifactCount}`);
2905
+ }
2628
2906
  let restoredCursor = loadWatchSessionTailCursor(sessionTailCursorPath);
2629
2907
  let localSessionTail;
2630
2908
  try {
@@ -2644,8 +2922,18 @@ export async function createWatchCommandRuntime(input) {
2644
2922
  });
2645
2923
  persistWatchSessionTailCursor(sessionTailCursorPath, []);
2646
2924
  }
2647
- let lastHandledMaterializationPackId = loadWatchTeacherSnapshotState(teacherSnapshotPath).lastHandledMaterializationPackId;
2648
- const replayState = await replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot);
2925
+ let replayState = {
2926
+ replayedBundleCount: 0,
2927
+ replayedEventCount: 0
2928
+ };
2929
+ try {
2930
+ replayState = await replayWatchScanRootIntoTeacherLoop(teacherLoop, scanRoot);
2931
+ }
2932
+ catch (error) {
2933
+ const message = formatWatchError(error);
2934
+ startupWarnings.push(`teacher_replay_failed:${message}`);
2935
+ log(`Async teacher replay fail-open: ${message}`);
2936
+ }
2649
2937
  if (replayState.replayedBundleCount > 0) {
2650
2938
  log(`Replayed ${replayState.replayedBundleCount} stored export bundle${replayState.replayedBundleCount === 1 ? "" : "s"} (${replayState.replayedEventCount} event${replayState.replayedEventCount === 1 ? "" : "s"})`);
2651
2939
  }
@@ -2656,14 +2944,25 @@ export async function createWatchCommandRuntime(input) {
2656
2944
  log(replayPromotion.logLine);
2657
2945
  bootstrapSnapshot = teacherLoop.snapshot();
2658
2946
  }
2947
+ const bootstrapCursor = localSessionTail.snapshot();
2659
2948
  persistWatchTeacherSnapshot(teacherSnapshotPath, {
2949
+ lastRunAt: bootstrapObservedAt,
2660
2950
  scanRoot,
2951
+ sessionTailCursorPath,
2952
+ sessionTailCursorUpdatedAt: bootstrapObservedAt,
2953
+ sessionTailSessionsTracked: bootstrapCursor.length,
2954
+ sessionTailBridgedEventCount: countWatchCursorBridgedEvents(bootstrapCursor),
2955
+ scannerCheckpointPath: scanner.checkpointPath,
2956
+ scannerCheckpoint: scanner.snapshot(),
2661
2957
  replayedBundleCount: replayState.replayedBundleCount,
2662
2958
  replayedEventCount: replayState.replayedEventCount,
2663
2959
  exportedBundleCount: 0,
2664
2960
  exportedEventCount: 0,
2961
+ startupWarnings,
2962
+ lastTeacherError: null,
2665
2963
  localSessionTailNoopReason: null,
2666
2964
  lastHandledMaterializationPackId,
2965
+ failure: replayPromotion.failure,
2667
2966
  snapshot: bootstrapSnapshot
2668
2967
  });
2669
2968
  return {
@@ -2671,6 +2970,8 @@ export async function createWatchCommandRuntime(input) {
2671
2970
  scanRoot,
2672
2971
  sessionTailCursorPath,
2673
2972
  teacherSnapshotPath,
2973
+ startupWarnings,
2974
+ lastTeacherError: null,
2674
2975
  replayState,
2675
2976
  lastHandledMaterializationPackId,
2676
2977
  scanner,
@@ -2684,6 +2985,7 @@ export async function runWatchCommandPass(runtime, options = {}) {
2684
2985
  const localPoll = runtime.localSessionTail.pollOnce({
2685
2986
  observedAt
2686
2987
  });
2988
+ const scannerCheckpointBeforeScan = runtime.scanner.snapshot();
2687
2989
  const exported = exportLocalSessionTailChangesToScanRoot({
2688
2990
  scanRoot: runtime.scanRoot,
2689
2991
  polledAt: localPoll.polledAt,
@@ -2703,31 +3005,71 @@ export async function runWatchCommandPass(runtime, options = {}) {
2703
3005
  const totalEvents = scanResult.selected.reduce((sum, hit) => sum + hit.eventRange.count, 0);
2704
3006
  let snapshot = runtime.teacherLoop.snapshot();
2705
3007
  let materializedPackId = null;
3008
+ let failure = null;
2706
3009
  if (totalSelected === 0) {
2707
3010
  log("Scanning... no changes");
2708
3011
  }
2709
3012
  else {
2710
3013
  log(`Scanning... ${totalSelected} export bundle${totalSelected === 1 ? "" : "s"} selected, ${totalEvents} event${totalEvents === 1 ? "" : "s"}`);
2711
- const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
2712
- snapshot = ingestResult.snapshot;
2713
- const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
2714
- runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
2715
- materializedPackId = promotion.materializedPackId;
2716
- if (promotion.logLine !== null) {
2717
- log(promotion.logLine);
3014
+ try {
3015
+ const ingestResult = await runtime.teacherLoop.ingestRuntimeEventExportScannerScan(scanResult);
3016
+ runtime.lastTeacherError = null;
3017
+ snapshot = ingestResult.snapshot;
3018
+ const promotion = applyWatchMaterialization(runtime.activationRoot, snapshot, runtime.lastHandledMaterializationPackId);
3019
+ runtime.lastHandledMaterializationPackId = promotion.lastHandledMaterializationPackId;
3020
+ materializedPackId = promotion.materializedPackId;
3021
+ failure = promotion.failure;
3022
+ if (promotion.logLine !== null) {
3023
+ log(promotion.logLine);
3024
+ snapshot = runtime.teacherLoop.snapshot();
3025
+ }
3026
+ }
3027
+ catch (error) {
3028
+ const message = formatWatchError(error);
3029
+ runtime.lastTeacherError = message;
3030
+ failure = {
3031
+ mode: "teacher_fail_open",
3032
+ detail: message,
3033
+ at: observedAt
3034
+ };
3035
+ log(`Async teacher fail-open: ${message}`);
3036
+ try {
3037
+ runtime.scanner.restoreCheckpoint(scannerCheckpointBeforeScan);
3038
+ }
3039
+ catch (restoreError) {
3040
+ const restoreMessage = formatWatchError(restoreError);
3041
+ runtime.lastTeacherError = `${message}; scanner checkpoint restore failed: ${restoreMessage}`;
3042
+ failure = {
3043
+ mode: "teacher_fail_open",
3044
+ detail: runtime.lastTeacherError,
3045
+ at: observedAt
3046
+ };
3047
+ log(`Scanner checkpoint restore failed: ${restoreMessage}`);
3048
+ }
2718
3049
  snapshot = runtime.teacherLoop.snapshot();
2719
3050
  }
2720
3051
  }
2721
3052
  persistWatchTeacherSnapshot(runtime.teacherSnapshotPath, {
3053
+ lastRunAt: observedAt,
2722
3054
  scanRoot: runtime.scanRoot,
3055
+ sessionTailCursorPath: runtime.sessionTailCursorPath,
3056
+ sessionTailCursorUpdatedAt: observedAt,
3057
+ sessionTailSessionsTracked: localPoll.cursor.length,
3058
+ sessionTailBridgedEventCount: countWatchCursorBridgedEvents(localPoll.cursor),
3059
+ scannerCheckpointPath: runtime.scanner.checkpointPath,
3060
+ scannerCheckpoint: runtime.scanner.snapshot(),
2723
3061
  replayedBundleCount: runtime.replayState.replayedBundleCount,
2724
3062
  replayedEventCount: runtime.replayState.replayedEventCount,
2725
3063
  exportedBundleCount: exported.exportedBundleCount,
2726
3064
  exportedEventCount: exported.exportedEventCount,
3065
+ startupWarnings: runtime.startupWarnings,
3066
+ lastTeacherError: runtime.lastTeacherError,
2727
3067
  localSessionTailNoopReason: localPoll.noopReason,
2728
3068
  lastHandledMaterializationPackId: runtime.lastHandledMaterializationPackId,
3069
+ failure,
2729
3070
  snapshot
2730
3071
  });
3072
+ const persistedScannerCheckpoint = runtime.scanner.snapshot();
2731
3073
  if (options.json) {
2732
3074
  console.log(JSON.stringify({
2733
3075
  timestamp: observedAt,
@@ -2739,6 +3081,10 @@ export async function runWatchCommandPass(runtime, options = {}) {
2739
3081
  events: totalEvents,
2740
3082
  live: scanResult.live.length,
2741
3083
  backfill: scanResult.backfill.length,
3084
+ sessionTailSessionsTracked: localPoll.cursor.length,
3085
+ sessionTailBridgedEvents: countWatchCursorBridgedEvents(localPoll.cursor),
3086
+ scannerProcessedBundles: persistedScannerCheckpoint.processedExportDigests.length,
3087
+ scannerLiveAfter: persistedScannerCheckpoint.live.after?.exportDigest ?? null,
2742
3088
  materialized: materializedPackId,
2743
3089
  diagnostics: snapshot.diagnostics ?? null,
2744
3090
  localSessionTailNoopReason: localPoll.noopReason
@@ -2842,12 +3188,13 @@ function resetActivationRoot(activationRoot) {
2842
3188
  function runResetCommand(parsed) {
2843
3189
  if (parsed.help) {
2844
3190
  console.log([
2845
- "Usage: openclawbrain reset [--activation-root <path>] [--yes] [--json]",
3191
+ "Usage: openclawbrain reset [--activation-root <path>|--openclaw-home <path>] [--yes] [--json]",
2846
3192
  "",
2847
3193
  "Wipes all learned state and returns the brain to seed state.",
2848
3194
  "",
2849
3195
  "Options:",
2850
3196
  " --activation-root <path> Activation root (auto-detected if omitted)",
3197
+ " --openclaw-home <path> Pin auto-detection to one installed OpenClaw profile",
2851
3198
  " --yes, -y Skip confirmation prompt",
2852
3199
  " --json Emit machine-readable JSON output",
2853
3200
  " --help Show this help"
@@ -2898,7 +3245,7 @@ function runResetCommand(parsed) {
2898
3245
  }
2899
3246
  console.log(" Activation pointers reset to seed state.");
2900
3247
  console.log(`\nBrain at ${shortenPath(activationRoot)} is now in seed state.`);
2901
- console.log("Run `openclawbrain status` to verify.");
3248
+ console.log(`Run \`openclawbrain status --activation-root ${quoteShellArg(activationRoot)}\` to verify.`);
2902
3249
  }
2903
3250
  return 0;
2904
3251
  }
@@ -2973,8 +3320,8 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
2973
3320
  });
2974
3321
  return 0;
2975
3322
  }
2976
- if (parsed.command === "install" || parsed.command === "setup") {
2977
- return runSetupCommand(parsed);
3323
+ if (parsed.command === "install") {
3324
+ return runInstallCommand(parsed);
2978
3325
  }
2979
3326
  if (parsed.command === "detach") {
2980
3327
  return runDetachCommand(parsed);
@@ -3044,7 +3391,7 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
3044
3391
  }
3045
3392
  // At this point only status/rollback commands remain
3046
3393
  const statusOrRollback = parsed;
3047
- const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.command);
3394
+ const activationRoot = requireActivationRoot(statusOrRollback.input, statusOrRollback.openclawHome, statusOrRollback.command);
3048
3395
  if (statusOrRollback.command === "rollback") {
3049
3396
  const result = rollbackRuntimeAttach({
3050
3397
  activationRoot,