@openclawbrain/cli 0.4.13 → 0.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { execFileSync, execSync } from "node:child_process";
2
+ import { execFileSync, execSync, spawnSync } from "node:child_process";
3
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";
@@ -9,21 +9,23 @@ import { DEFAULT_OLLAMA_EMBEDDING_MODEL, createOllamaEmbedder } from "@openclawb
9
9
  import { ensureManagedLearnerServiceForActivationRoot, inspectManagedLearnerService, removeManagedLearnerServiceForActivationRoot, parseDaemonArgs, runDaemonCommand } from "./daemon.js";
10
10
  import { exportBrain, importBrain } from "./import-export.js";
11
11
  import { buildNormalizedEventExport } from "@openclawbrain/contracts";
12
- import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, reindexCandidatePackBuildResultWithEmbedder, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "./local-learner.js";
12
+ import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "./local-learner.js";
13
13
  import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
14
14
  import { resolveActivationRoot } from "./resolve-activation-root.js";
15
15
  import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
16
16
  import { inspectOpenClawBrainHookStatus, inspectOpenClawBrainPluginAllowlist } from "./openclaw-hook-truth.js";
17
17
  import { describeOpenClawBrainInstallIdentity, describeOpenClawBrainInstallLayout, findInstalledOpenClawBrainPlugin, getOpenClawBrainKnownPluginIds, normalizeOpenClawBrainPluginsConfig, pinInstalledOpenClawBrainPluginActivationRoot, resolveOpenClawBrainInstallTarget } from "./openclaw-plugin-install.js";
18
+ import { buildOpenClawBrainConvergeRestartPlan, classifyOpenClawBrainConvergeVerification, describeOpenClawBrainConvergeChangeReasons, diffOpenClawBrainConvergeRuntimeFingerprint, finalizeOpenClawBrainConvergeResult, planOpenClawBrainConvergePluginAction } from "./install-converge.js";
18
19
  import { loadAttachmentPolicyDeclaration, resolveEffectiveAttachmentPolicyTruth, writeAttachmentPolicyDeclaration } from "./attachment-policy-truth.js";
19
20
  import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, clearOpenClawProfileRuntimeLoadProof, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, listOpenClawProfileRuntimeLoadProofs, loadRuntimeEventExportBundle, loadWatchTeacherSnapshotState, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveAttachmentRuntimeLoadProofsPath, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
20
21
  import { appendLearningUpdateLogs } from "./learning-spine.js";
21
22
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
23
+ import { reindexMaterializationCandidateWithEmbedder } from "./materialization-embedder.js";
22
24
  import { summarizePackVectorEmbeddingState } from "./embedding-status.js";
23
25
  import { buildTracedLearningBridgePayloadFromRuntime, buildTracedLearningStatusSurface, persistTracedLearningBridgeState } from "./traced-learning-bridge.js";
24
26
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
25
27
  import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
26
- import { formatOperatorLearningPathSummary } from "./status-learning-path.js";
28
+ import { formatOperatorLearningAttributionSummary, formatOperatorLearningPathSummary } from "./status-learning-path.js";
27
29
  import { buildProofCommandForOpenClawHome, buildProofCommandHelpSection, captureOperatorProofBundle, formatOperatorProofResult, parseProofCliArgs } from "./proof-command.js";
28
30
  const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
29
31
  const OPENCLAWBRAIN_EMBEDDER_PROVIDER_ENV = "OPENCLAWBRAIN_EMBEDDER_PROVIDER";
@@ -516,7 +518,7 @@ function operatorCliHelp() {
516
518
  " --help Show this help.",
517
519
  "",
518
520
  "Lifecycle flow:",
519
- " 1. install openclawbrain install — safe first-time default; writes the generated shadow hook or pins an already-installed native package plugin for one OpenClaw home",
521
+ " 1. install openclawbrain install — converge one OpenClaw home: plugin-manager install/update/repair, hook repair, conditional restart, then status verification",
520
522
  " 2. attach openclawbrain attach --openclaw-home <path> [--activation-root <path>] — explicit reattach/manual hook path for known brain data; use install first",
521
523
  " 3. status openclawbrain status --activation-root <path> — answer \"How's the brain?\" for that boundary",
522
524
  " 4. status --detailed openclawbrain status --activation-root <path> --detailed — explain serve path, freshness, backlog, and failure mode",
@@ -706,6 +708,10 @@ function summarizeStatusHookLoad(installHook, status) {
706
708
  installState: installHook.state === "unknown" ? "unverified" : installHook.state,
707
709
  loadability: installHook.loadability,
708
710
  loadProof: status.hook.loadProof,
711
+ guardSeverity: status.hook.guardSeverity,
712
+ guardActionability: status.hook.guardActionability,
713
+ guardSummary: status.hook.guardSummary,
714
+ guardAction: status.hook.guardAction,
709
715
  detail: status.hook.detail
710
716
  };
711
717
  }
@@ -1076,7 +1082,7 @@ function summarizeStatusTeacher(report, providerConfig, localLlm) {
1076
1082
  detail: `${providerConfig.teacher.model} is enabled on Ollama, but no watch teacher snapshot is visible yet`
1077
1083
  };
1078
1084
  }
1079
- const stale = report.teacherLoop.latestFreshness === "stale" || report.teacherLoop.watchState === "stale_snapshot";
1085
+ const stale = report.teacherLoop.watchState === "stale_snapshot" || (report.teacherLoop.latestFreshness === "stale" && report.teacherLoop.lastNoOpReason !== "no_teacher_artifacts");
1080
1086
  const idle = report.teacherLoop.running === false &&
1081
1087
  (report.teacherLoop.queueDepth ?? 0) === 0 &&
1082
1088
  report.teacherLoop.failureMode === "none";
@@ -1343,7 +1349,7 @@ function buildCompactStatusHeader(status, report, options) {
1343
1349
  const tracedLearning = options.tracedLearning ?? buildTracedLearningStatusSurface(status.host.activationRoot);
1344
1350
  return [
1345
1351
  `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1346
- `hook install=${hookLoad.installState} loadability=${hookLoad.loadability} loadProof=${hookLoad.loadProof} layout=${status.hook.installLayout ?? "unverified"} additional=${status.hook.additionalInstallCount ?? 0} detail=${hookLoad.detail}`,
1352
+ `hook install=${hookLoad.installState} loadability=${hookLoad.loadability} loadProof=${hookLoad.loadProof} layout=${status.hook.installLayout ?? "unverified"} additional=${status.hook.additionalInstallCount ?? 0} severity=${hookLoad.guardSeverity} actionability=${hookLoad.guardActionability} summary=${hookLoad.guardSummary}`,
1347
1353
  `attachTruth current=${attachmentTruth.currentProfileLabel} hook=${attachmentTruth.hookFiles} config=${attachmentTruth.configLoad} runtime=${attachmentTruth.runtimeLoad} watcher=${attachmentTruth.watcher} attachedSet=${formatAttachedProfileTruthCompactList(attachmentTruth.attachedProfiles)} why=${attachmentTruth.detail}`,
1348
1354
  `passive firstExport=${yesNo(status.passiveLearning.firstExportOccurred)} backlog=${status.passiveLearning.backlogState} pending=${formatStatusNullableNumber(status.passiveLearning.pendingLive)}/${formatStatusNullableNumber(status.passiveLearning.pendingBackfill)}`,
1349
1355
  `serving pack=${status.passiveLearning.currentServingPackId ?? "none"} lastExport=${status.passiveLearning.lastExportAt ?? "none"} lastPromotion=${status.passiveLearning.lastPromotionAt ?? "none"}`,
@@ -1352,6 +1358,7 @@ function buildCompactStatusHeader(status, report, options) {
1352
1358
  `changed ${status.passiveLearning.lastObservedDelta.explanation}`,
1353
1359
  `explain ${status.brain.summary}`,
1354
1360
  `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)}`,
1361
+ `attribution ${formatOperatorLearningAttributionSummary({ status })}`,
1355
1362
  `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}`,
1356
1363
  `embedder model=${embedder.model} provisioned=${yesNo(embedder.provisioned)} live=${yesNo(embedder.live)} why=${embedder.detail}`,
1357
1364
  `routeFn available=${yesNo(routeFn.available)} freshness=${routeFn.freshness} trained=${routeFn.trainedAt ?? "none"} updated=${routeFn.updatedAt ?? "none"} used=${routeFn.usedAt ?? "none"} why=${routeFn.detail}`,
@@ -1389,6 +1396,7 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
1389
1396
  })}`,
1390
1397
  `host runtime=${status.host.runtimeOwner} activation=${status.host.activationRoot}`,
1391
1398
  `profile selector=${status.profile.selector}${profileIdSuffix} attachment=${status.attachment.state} policy=${status.attachment.policyMode}`,
1399
+ `guard severity=${status.hook.guardSeverity} actionability=${status.hook.guardActionability} action=${status.hook.guardAction} summary=${status.hook.guardSummary}`,
1392
1400
  `attachTruth current=${attachmentTruth.currentProfileLabel} hook=${attachmentTruth.hookFiles} config=${attachmentTruth.configLoad} runtime=${attachmentTruth.runtimeLoad} watcher=${attachmentTruth.watcher} detail=${attachmentTruth.detail}`,
1393
1401
  `attachedSet ${formatAttachedProfileTruthDetailedList(attachmentTruth.attachedProfiles)} proofPath=${shortenPath(attachmentTruth.runtimeProofPath)} proofError=${attachmentTruth.runtimeProofError ?? "none"}`,
1394
1402
  `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)}`,
@@ -1409,6 +1417,7 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
1409
1417
  learningPath: report.learningPath,
1410
1418
  tracedLearning
1411
1419
  })}`,
1420
+ `attribution ${formatOperatorLearningAttributionSummary({ status })}`,
1412
1421
  `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}`,
1413
1422
  `traced ${formatTracedLearningSurface(tracedLearning)}`,
1414
1423
  `teacherProof ${formatTeacherLoopSummary(report)}`,
@@ -1452,6 +1461,184 @@ function buildGatewayRestartCommand(profileId) {
1452
1461
  function buildGatewayStatusCommand(profileId) {
1453
1462
  return `env -i HOME="$HOME" PATH="$PATH" openclaw --profile ${quoteShellArg(profileId)} gateway status`;
1454
1463
  }
1464
+ function buildGatewayRestartArgs(profileId) {
1465
+ return profileId === null ? ["gateway", "restart"] : ["gateway", "restart", "--profile", profileId];
1466
+ }
1467
+ function shellJoin(parts) {
1468
+ return parts
1469
+ .map((part) => {
1470
+ if (/^[A-Za-z0-9_./:@=-]+$/.test(part)) {
1471
+ return part;
1472
+ }
1473
+ return JSON.stringify(part);
1474
+ })
1475
+ .join(" ");
1476
+ }
1477
+ function runCapturedExternalCommand(command, args, options = {}) {
1478
+ const result = spawnSync(command, args, {
1479
+ cwd: options.cwd ?? process.cwd(),
1480
+ env: options.env ?? process.env,
1481
+ encoding: "utf8",
1482
+ stdio: "pipe"
1483
+ });
1484
+ return {
1485
+ command,
1486
+ args,
1487
+ shellCommand: shellJoin([command, ...args]),
1488
+ stdout: result.stdout ?? "",
1489
+ stderr: result.stderr ?? "",
1490
+ exitCode: typeof result.status === "number" ? result.status : null,
1491
+ signal: result.signal ?? null,
1492
+ error: result.error ? toErrorMessage(result.error) : null
1493
+ };
1494
+ }
1495
+ function summarizeCapturedCommandFailure(capture) {
1496
+ const parts = [];
1497
+ if (capture.error !== null) {
1498
+ parts.push(capture.error);
1499
+ }
1500
+ if (capture.stderr.trim().length > 0) {
1501
+ parts.push(capture.stderr.trim());
1502
+ }
1503
+ if (capture.stdout.trim().length > 0) {
1504
+ parts.push(capture.stdout.trim());
1505
+ }
1506
+ if (capture.exitCode !== null) {
1507
+ parts.push(`exitCode=${capture.exitCode}`);
1508
+ }
1509
+ return parts.length === 0 ? "no command output was captured" : parts.join(" | ");
1510
+ }
1511
+ function readTextFileIfExists(filePath) {
1512
+ if (filePath === null || !existsSync(filePath)) {
1513
+ return null;
1514
+ }
1515
+ try {
1516
+ return readFileSync(filePath, "utf8");
1517
+ }
1518
+ catch {
1519
+ return null;
1520
+ }
1521
+ }
1522
+ function readInstallRuntimeFingerprint(openclawHome) {
1523
+ const installedPlugin = findInstalledOpenClawBrainPlugin(openclawHome);
1524
+ const selectedInstall = installedPlugin.selectedInstall;
1525
+ const hook = inspectOpenClawBrainHookStatus(openclawHome);
1526
+ const resolvedActivationRoot = resolveActivationRoot({
1527
+ openclawHome,
1528
+ quiet: true
1529
+ });
1530
+ const { config } = readOpenClawJsonConfig(openclawHome);
1531
+ return {
1532
+ selectedInstall: selectedInstall === null
1533
+ ? null
1534
+ : {
1535
+ extensionDir: selectedInstall.extensionDir,
1536
+ manifestId: selectedInstall.manifestId,
1537
+ installId: selectedInstall.installId,
1538
+ packageName: selectedInstall.packageName,
1539
+ installLayout: selectedInstall.installLayout
1540
+ },
1541
+ installLayout: hook.installLayout,
1542
+ hookPath: hook.hookPath,
1543
+ hookState: hook.installState,
1544
+ loadability: hook.loadability,
1545
+ activationRoot: resolvedActivationRoot.trim().length === 0 ? null : path.resolve(resolvedActivationRoot),
1546
+ loaderSource: readTextFileIfExists(selectedInstall?.loaderEntryPath ?? null),
1547
+ runtimeGuardSource: readTextFileIfExists(selectedInstall?.runtimeGuardPath ?? null),
1548
+ pluginsConfig: JSON.stringify(config.plugins ?? null)
1549
+ };
1550
+ }
1551
+ function runOpenClawBrainConvergePluginStep(openclawHome) {
1552
+ const before = readInstallRuntimeFingerprint(openclawHome);
1553
+ const plan = planOpenClawBrainConvergePluginAction(before);
1554
+ const commandArgs = plan.action === "install"
1555
+ ? ["plugins", "install", plan.packageSpec]
1556
+ : ["plugins", "update", plan.pluginId];
1557
+ const capture = runCapturedExternalCommand("openclaw", commandArgs);
1558
+ if (capture.error !== null || capture.exitCode !== 0) {
1559
+ const hasAuthoritativeNativePlugin = before.selectedInstall !== null && before.installLayout === "native_package_plugin";
1560
+ if (plan.action === "update" && hasAuthoritativeNativePlugin) {
1561
+ return {
1562
+ plan,
1563
+ command: capture.shellCommand,
1564
+ changed: false,
1565
+ changeReasons: [],
1566
+ detail: `Skipped plugin-manager refresh because the existing split-package plugin is already authoritative and \
1567
+ \`${capture.shellCommand}\` failed: ${summarizeCapturedCommandFailure(capture)}`,
1568
+ warning: `plugin-manager refresh skipped after \`${capture.shellCommand}\` failed; keeping the existing authoritative split-package plugin for this converge run`,
1569
+ capture,
1570
+ before,
1571
+ after: before
1572
+ };
1573
+ }
1574
+ throw new Error(`OpenClaw plugin-manager ${plan.action} failed for ${path.resolve(openclawHome)}. Tried \`${capture.shellCommand}\`. Detail: ${summarizeCapturedCommandFailure(capture)}`);
1575
+ }
1576
+ const after = readInstallRuntimeFingerprint(openclawHome);
1577
+ const diff = diffOpenClawBrainConvergeRuntimeFingerprint(before, after);
1578
+ return {
1579
+ plan,
1580
+ command: capture.shellCommand,
1581
+ changed: diff.changed,
1582
+ changeReasons: diff.reasons,
1583
+ detail: diff.changed
1584
+ ? `${plan.action === "install" ? "Installed" : "Refreshed"} plugin-manager state: ${describeOpenClawBrainConvergeChangeReasons(diff.reasons)}`
1585
+ : `${plan.action === "install" ? "Ran install" : "Ran update"} through the OpenClaw plugin manager, but no runtime-affecting plugin delta was detected`,
1586
+ warning: null,
1587
+ capture,
1588
+ before,
1589
+ after
1590
+ };
1591
+ }
1592
+ function inspectInstallConvergeVerification(parsed) {
1593
+ const targetInspection = inspectOpenClawHome(parsed.openclawHome);
1594
+ const operatorInput = {
1595
+ activationRoot: parsed.activationRoot,
1596
+ eventExportPath: null,
1597
+ teacherSnapshotPath: resolveOperatorTeacherSnapshotPath(parsed.activationRoot, null),
1598
+ updatedAt: null,
1599
+ brainAttachmentPolicy: null,
1600
+ openclawHome: parsed.openclawHome,
1601
+ ...(targetInspection.profileId === null ? {} : { profileId: targetInspection.profileId })
1602
+ };
1603
+ const status = describeCurrentProfileBrainStatus(operatorInput);
1604
+ const report = buildOperatorSurfaceReport(operatorInput);
1605
+ const normalizedStatusAndReport = applyAttachmentPolicyTruth(status, report);
1606
+ const installHook = summarizeStatusInstallHook(parsed.openclawHome);
1607
+ const attachmentTruth = summarizeStatusAttachmentTruth({
1608
+ activationRoot: parsed.activationRoot,
1609
+ openclawHome: parsed.openclawHome,
1610
+ status: normalizedStatusAndReport.status
1611
+ });
1612
+ const displayedStatus = summarizeDisplayedStatus(normalizedStatusAndReport.status, installHook);
1613
+ const routeFn = summarizeStatusRouteFn(normalizedStatusAndReport.status, normalizedStatusAndReport.report);
1614
+ return {
1615
+ targetInspection,
1616
+ status: normalizedStatusAndReport.status,
1617
+ report: normalizedStatusAndReport.report,
1618
+ installHook,
1619
+ attachmentTruth,
1620
+ displayedStatus,
1621
+ routeFn,
1622
+ nextStep: buildStatusNextStep(normalizedStatusAndReport.status, normalizedStatusAndReport.report, {
1623
+ openclawHome: parsed.openclawHome,
1624
+ installHook
1625
+ }),
1626
+ summaryLine: `STATUS ${displayedStatus}; hook=${installHook.state}/${installHook.loadability}; guard=${normalizedStatusAndReport.status.hook.guardSeverity}/${normalizedStatusAndReport.status.hook.guardActionability}; runtime=${attachmentTruth.runtimeLoad}; loadProof=${normalizedStatusAndReport.status.hook.loadProof}; serve=${normalizedStatusAndReport.status.brainStatus.serveState}`,
1627
+ facts: {
1628
+ installLayout: normalizedStatusAndReport.status.hook.installLayout ?? installHook.installLayout ?? null,
1629
+ installState: installHook.state,
1630
+ loadability: installHook.loadability,
1631
+ guardSeverity: normalizedStatusAndReport.status.hook.guardSeverity,
1632
+ guardActionability: normalizedStatusAndReport.status.hook.guardActionability,
1633
+ displayedStatus,
1634
+ runtimeLoad: attachmentTruth.runtimeLoad,
1635
+ loadProof: normalizedStatusAndReport.status.hook.loadProof,
1636
+ serveState: normalizedStatusAndReport.status.brainStatus.serveState,
1637
+ routeFnAvailable: routeFn.available,
1638
+ awaitingFirstExport: normalizedStatusAndReport.status.brainStatus.awaitingFirstExport
1639
+ }
1640
+ };
1641
+ }
1455
1642
  function buildInstallCommand(openclawHome) {
1456
1643
  return `openclawbrain install --openclaw-home ${quoteShellArg(openclawHome)}`;
1457
1644
  }
@@ -2933,10 +3120,133 @@ function buildExtensionPluginManifest() {
2933
3120
  name: "OpenClawBrain",
2934
3121
  description: "Learned memory and context from OpenClawBrain",
2935
3122
  version: packageMetadata.version,
3123
+ uiHints: {
3124
+ brainRoot: {
3125
+ label: "Brain Root",
3126
+ help: "Directory containing OpenClawBrain state.db and immutable packs"
3127
+ },
3128
+ brainEmbeddingProvider: {
3129
+ label: "Embedding Provider",
3130
+ help: "Provider used for learned retrieval embeddings"
3131
+ },
3132
+ brainEmbeddingModel: {
3133
+ label: "Embedding Model",
3134
+ help: "Embedding model used for init, retrieval, and brain_teach"
3135
+ },
3136
+ brainEmbeddingBaseUrl: {
3137
+ label: "Embedding Base URL",
3138
+ help: "Optional base URL override for the embedding provider endpoint"
3139
+ },
3140
+ brainMaxCompileMs: {
3141
+ label: "Brain Compile Deadline",
3142
+ help: "Soft wall-clock deadline in milliseconds for brain assembly phase-boundary checks"
3143
+ },
3144
+ brainBudgetFraction: {
3145
+ label: "Brain Budget Fraction",
3146
+ help: "Fraction of the available token budget reserved for retrieval before final prompt clipping"
3147
+ },
3148
+ brainMaxHops: {
3149
+ label: "Brain Max Hops",
3150
+ help: "Maximum learned-retrieval graph expansion depth per query"
3151
+ },
3152
+ brainMaxFanoutPerNode: {
3153
+ label: "Brain Max Fanout Per Node",
3154
+ help: "Maximum accepted traversals from a single source node expansion"
3155
+ },
3156
+ brainMaxFrontierSize: {
3157
+ label: "Brain Max Frontier Size",
3158
+ help: "Maximum traversal frontier size during learned retrieval"
3159
+ },
3160
+ brainMaxSeeds: {
3161
+ label: "Brain Max Seeds",
3162
+ help: "Maximum seed nodes admitted into learned retrieval before graph expansion"
3163
+ },
3164
+ brainSemanticThreshold: {
3165
+ label: "Brain Semantic Threshold",
3166
+ help: "Minimum semantic similarity required for seed admission during learned retrieval"
3167
+ },
3168
+ brainShadowMode: {
3169
+ label: "Brain Shadow Mode",
3170
+ help: "Run learned retrieval for telemetry only without injecting brain context into prompts"
3171
+ },
3172
+ brainWorkerMode: {
3173
+ label: "Worker Mode",
3174
+ help: "Run the learner in a supervised child process (default) or fall back to in-process mode"
3175
+ },
3176
+ brainWorkerHeartbeatTimeoutMs: {
3177
+ label: "Worker Heartbeat Timeout",
3178
+ help: "Milliseconds to wait before treating the supervised learner worker as stalled"
3179
+ },
3180
+ brainWorkerRestartDelayMs: {
3181
+ label: "Worker Restart Delay",
3182
+ help: "Milliseconds to wait before restarting the supervised learner worker after exit or crash"
3183
+ }
3184
+ },
2936
3185
  configSchema: {
2937
3186
  type: "object",
2938
3187
  additionalProperties: false,
2939
- properties: {}
3188
+ properties: {
3189
+ brainEnabled: {
3190
+ type: "boolean"
3191
+ },
3192
+ brainRoot: {
3193
+ type: "string"
3194
+ },
3195
+ brainEmbeddingProvider: {
3196
+ type: "string"
3197
+ },
3198
+ brainEmbeddingModel: {
3199
+ type: "string"
3200
+ },
3201
+ brainEmbeddingBaseUrl: {
3202
+ type: "string"
3203
+ },
3204
+ brainMaxCompileMs: {
3205
+ type: "integer",
3206
+ minimum: 0
3207
+ },
3208
+ brainBudgetFraction: {
3209
+ type: "number",
3210
+ minimum: 0,
3211
+ maximum: 1
3212
+ },
3213
+ brainMaxHops: {
3214
+ type: "integer",
3215
+ minimum: 1
3216
+ },
3217
+ brainMaxFanoutPerNode: {
3218
+ type: "integer",
3219
+ minimum: 1
3220
+ },
3221
+ brainMaxFrontierSize: {
3222
+ type: "integer",
3223
+ minimum: 1
3224
+ },
3225
+ brainMaxSeeds: {
3226
+ type: "integer",
3227
+ minimum: 1
3228
+ },
3229
+ brainSemanticThreshold: {
3230
+ type: "number",
3231
+ minimum: 0,
3232
+ maximum: 1
3233
+ },
3234
+ brainShadowMode: {
3235
+ type: "boolean"
3236
+ },
3237
+ brainWorkerMode: {
3238
+ type: "string",
3239
+ enum: ["child", "in_process"]
3240
+ },
3241
+ brainWorkerHeartbeatTimeoutMs: {
3242
+ type: "integer",
3243
+ minimum: 1000
3244
+ },
3245
+ brainWorkerRestartDelayMs: {
3246
+ type: "integer",
3247
+ minimum: 0
3248
+ }
3249
+ }
2940
3250
  }
2941
3251
  }, null, 2) + "\n";
2942
3252
  }
@@ -3238,7 +3548,7 @@ function runHistoryCommand(parsed) {
3238
3548
  }
3239
3549
  return 0;
3240
3550
  }
3241
- function runProfileHookAttachCommand(parsed) {
3551
+ function executeProfileHookAttachCommand(parsed) {
3242
3552
  const steps = [];
3243
3553
  const commandLabel = parsed.command.toUpperCase();
3244
3554
  const isInstall = parsed.command === "install";
@@ -3458,106 +3768,273 @@ function runProfileHookAttachCommand(parsed) {
3458
3768
  ? `Install: kept healthy active pack ${activationPlan.activePackId} in place`
3459
3769
  : `Attach: rewired the profile hook to healthy active pack ${activationPlan.activePackId}`
3460
3770
  ];
3461
- // 9. Print summary
3462
- if (parsed.json) {
3463
- console.log(JSON.stringify({
3464
- command: parsed.command,
3465
- openclawHome: parsed.openclawHome,
3466
- openclawHomeSource: parsed.openclawHomeSource,
3467
- openclawTarget: {
3468
- layout: targetInspection.layout,
3469
- detail: describeOpenClawHomeInspection(targetInspection),
3470
- profileId: targetInspection.profileId,
3471
- profileSource: targetInspection.profileSource,
3472
- configuredProfileIds: targetInspection.configuredProfileIds
3771
+ return {
3772
+ command: parsed.command,
3773
+ commandLabel,
3774
+ openclawHome: parsed.openclawHome,
3775
+ openclawHomeSource: parsed.openclawHomeSource,
3776
+ openclawTarget: {
3777
+ layout: targetInspection.layout,
3778
+ detail: describeOpenClawHomeInspection(targetInspection),
3779
+ profileId: targetInspection.profileId,
3780
+ profileSource: targetInspection.profileSource,
3781
+ configuredProfileIds: targetInspection.configuredProfileIds
3782
+ },
3783
+ activationRoot: parsed.activationRoot,
3784
+ resolvedInputs: {
3785
+ activationRoot: {
3786
+ value: parsed.activationRoot,
3787
+ source: parsed.activationRootSource
3473
3788
  },
3474
- activationRoot: parsed.activationRoot,
3475
- resolvedInputs: {
3476
- activationRoot: {
3477
- value: parsed.activationRoot,
3478
- source: parsed.activationRootSource
3479
- },
3480
- workspaceId: {
3481
- value: parsed.workspaceId,
3482
- source: parsed.workspaceIdSource
3483
- }
3789
+ workspaceId: {
3790
+ value: parsed.workspaceId,
3791
+ source: parsed.workspaceIdSource
3792
+ }
3793
+ },
3794
+ workspaceId: parsed.workspaceId,
3795
+ shared: parsed.shared,
3796
+ embedderProvision: embedderProvision === null
3797
+ ? null
3798
+ : {
3799
+ skipped: parsed.skipEmbedderProvision,
3800
+ source: parsed.skipEmbedderProvisionSource,
3801
+ model: embedderProvision.model,
3802
+ baseUrl: embedderProvision.baseUrl
3484
3803
  },
3485
- workspaceId: parsed.workspaceId,
3486
- shared: parsed.shared,
3487
- embedderProvision: embedderProvision === null
3488
- ? null
3489
- : {
3490
- skipped: parsed.skipEmbedderProvision,
3491
- source: parsed.skipEmbedderProvisionSource,
3492
- model: embedderProvision.model,
3493
- baseUrl: embedderProvision.baseUrl
3494
- },
3495
- providerDefaults: providerDefaults === null
3496
- ? null
3497
- : {
3498
- path: providerDefaults.path,
3499
- teacher: providerDefaults.defaults.teacher === undefined
3500
- ? null
3501
- : {
3502
- provider: providerDefaults.defaults.teacher.provider ?? null,
3503
- model: providerDefaults.defaults.teacher.model ?? null,
3504
- detectedLocally: providerDefaults.defaults.teacher.detectedLocally ?? false
3505
- },
3506
- embedder: providerDefaults.defaults.embedder === undefined
3507
- ? null
3508
- : {
3509
- provider: providerDefaults.defaults.embedder.provider ?? null,
3510
- model: providerDefaults.defaults.embedder.model ?? null
3511
- },
3512
- teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
3513
- embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
3514
- },
3515
- pluginConfigRepair,
3516
- learnerService,
3517
- brainFeedback: {
3518
- hookPath: brainFeedback.hookPath,
3519
- hookLayout: brainFeedback.hookLayout,
3520
- providerDefaultsPath: brainFeedback.providerDefaultsPath,
3521
- profile: brainFeedback.profile,
3522
- attachment: brainFeedback.attachment,
3523
- restart: brainFeedback.restart,
3524
- embedder: brainFeedback.embedder,
3525
- teacher: brainFeedback.teacher,
3526
- learnerService: brainFeedback.learnerService,
3527
- startup: brainFeedback.startup,
3528
- provedNow: brainFeedback.provedNow,
3529
- notYetProved: brainFeedback.notYetProved,
3530
- lines: brainFeedback.lines
3804
+ providerDefaults: providerDefaults === null
3805
+ ? null
3806
+ : {
3807
+ path: providerDefaults.path,
3808
+ teacher: providerDefaults.defaults.teacher === undefined
3809
+ ? null
3810
+ : {
3811
+ provider: providerDefaults.defaults.teacher.provider ?? null,
3812
+ model: providerDefaults.defaults.teacher.model ?? null,
3813
+ detectedLocally: providerDefaults.defaults.teacher.detectedLocally ?? false
3814
+ },
3815
+ embedder: providerDefaults.defaults.embedder === undefined
3816
+ ? null
3817
+ : {
3818
+ provider: providerDefaults.defaults.embedder.provider ?? null,
3819
+ model: providerDefaults.defaults.embedder.model ?? null
3820
+ },
3821
+ teacherBaseUrl: providerDefaults.defaults.teacherBaseUrl ?? null,
3822
+ embedderBaseUrl: providerDefaults.defaults.embedderBaseUrl ?? null
3531
3823
  },
3532
- extensionDir,
3533
- lifecycleSummary,
3534
- preflightSummary,
3535
- restartGuidance,
3536
- nextSteps,
3537
- steps
3538
- }, null, 2));
3824
+ pluginConfigRepair,
3825
+ learnerService,
3826
+ brainFeedback: {
3827
+ hookPath: brainFeedback.hookPath,
3828
+ hookLayout: brainFeedback.hookLayout,
3829
+ providerDefaultsPath: brainFeedback.providerDefaultsPath,
3830
+ profile: brainFeedback.profile,
3831
+ attachment: brainFeedback.attachment,
3832
+ restart: brainFeedback.restart,
3833
+ embedder: brainFeedback.embedder,
3834
+ teacher: brainFeedback.teacher,
3835
+ learnerService: brainFeedback.learnerService,
3836
+ startup: brainFeedback.startup,
3837
+ provedNow: brainFeedback.provedNow,
3838
+ notYetProved: brainFeedback.notYetProved,
3839
+ lines: brainFeedback.lines
3840
+ },
3841
+ extensionDir,
3842
+ lifecycleSummary,
3843
+ preflightSummary,
3844
+ restartGuidance,
3845
+ nextSteps,
3846
+ steps
3847
+ };
3848
+ }
3849
+ function emitProfileHookAttachCommandResult(result, parsed) {
3850
+ if (parsed.json) {
3851
+ console.log(JSON.stringify(result, null, 2));
3852
+ return;
3539
3853
  }
3540
- else {
3541
- console.log(`${commandLabel} complete\n`);
3542
- console.log("Brain feedback:");
3543
- for (const line of brainFeedback.lines) {
3544
- console.log(` ${line}`);
3545
- }
3546
- console.log(`Restart: ${restartGuidance}`);
3547
- if (brainFeedback.restart.gatewayStatusCommand !== null) {
3548
- console.log(`Gateway: Confirm OpenClaw after restart: ${brainFeedback.restart.gatewayStatusCommand}`);
3549
- }
3550
- console.log(`Check: ${buildInstallStatusCommand(parsed.activationRoot)}`);
3551
- console.log(`Proof: ${buildProofCommandForOpenClawHome(parsed.openclawHome)}`);
3552
- console.log(`Learner: ${buildLearnerServiceStatusCommand(parsed.activationRoot)}`);
3553
- if (embedderProvision !== null && embedderProvision.state === "skipped") {
3554
- console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(embedderProvision.baseUrl, embedderProvision.model)}`);
3555
- }
3854
+ console.log(`${result.commandLabel} complete\n`);
3855
+ console.log("Brain feedback:");
3856
+ for (const line of result.brainFeedback.lines) {
3857
+ console.log(` ${line}`);
3858
+ }
3859
+ console.log(`Restart: ${result.restartGuidance}`);
3860
+ if (result.brainFeedback.restart.gatewayStatusCommand !== null) {
3861
+ console.log(`Gateway: Confirm OpenClaw after restart: ${result.brainFeedback.restart.gatewayStatusCommand}`);
3862
+ }
3863
+ console.log(`Check: ${buildInstallStatusCommand(result.activationRoot)}`);
3864
+ console.log(`Proof: ${buildProofCommandForOpenClawHome(result.openclawHome)}`);
3865
+ console.log(`Learner: ${buildLearnerServiceStatusCommand(result.activationRoot)}`);
3866
+ if (result.embedderProvision !== null && result.embedderProvision.skipped) {
3867
+ console.log(`Embedder: ${buildInstallEmbedderProvisionCommand(result.embedderProvision.baseUrl, result.embedderProvision.model)}`);
3556
3868
  }
3869
+ }
3870
+ function runProfileHookAttachCommand(parsed) {
3871
+ const result = executeProfileHookAttachCommand(parsed);
3872
+ emitProfileHookAttachCommandResult(result, parsed);
3557
3873
  return 0;
3558
3874
  }
3875
+ function emitInstallConvergeResult(result, parsed) {
3876
+ if (parsed.json) {
3877
+ console.log(JSON.stringify(result, null, 2));
3878
+ return;
3879
+ }
3880
+ const heading = result.verdict.verdict === "failed"
3881
+ ? "INSTALL converge failed"
3882
+ : "INSTALL converge complete";
3883
+ console.log(`${heading}\n`);
3884
+ console.log(`Plugin: ${result.plugin.detail}`);
3885
+ console.log(`Attach: ${result.attach.detail}`);
3886
+ console.log(`Restart: ${result.restart.detail}`);
3887
+ console.log(`Verify: ${result.verification.summaryLine}`);
3888
+ console.log(`Verdict: ${result.verdict.verdict}`);
3889
+ console.log(`Why: ${result.verdict.why}`);
3890
+ if (result.verdict.warnings.length > 0) {
3891
+ console.log(`Warnings: ${result.verdict.warnings.join("; ")}`);
3892
+ }
3893
+ console.log(`Next: ${result.verification.nextStep}`);
3894
+ console.log(`Proof: ${buildProofCommandForOpenClawHome(result.openclawHome)}`);
3895
+ console.log(`Learner: ${buildLearnerServiceStatusCommand(result.activationRoot)}`);
3896
+ }
3559
3897
  function runInstallCommand(parsed) {
3560
- return runProfileHookAttachCommand(parsed);
3898
+ let pluginResult = null;
3899
+ let attachResult = null;
3900
+ try {
3901
+ validateOpenClawHome(parsed.openclawHome);
3902
+ pluginResult = runOpenClawBrainConvergePluginStep(parsed.openclawHome);
3903
+ attachResult = executeProfileHookAttachCommand(parsed);
3904
+ }
3905
+ catch (error) {
3906
+ const verdict = finalizeOpenClawBrainConvergeResult({
3907
+ stepFailure: toErrorMessage(error),
3908
+ verification: null,
3909
+ warnings: []
3910
+ });
3911
+ const failureResult = {
3912
+ command: "install",
3913
+ openclawHome: parsed.openclawHome,
3914
+ activationRoot: parsed.activationRoot,
3915
+ plugin: pluginResult ?? {
3916
+ action: null,
3917
+ command: null,
3918
+ changed: false,
3919
+ changeReasons: [],
3920
+ detail: "plugin-manager converge did not complete"
3921
+ },
3922
+ attach: attachResult ?? {
3923
+ changed: false,
3924
+ changeReasons: [],
3925
+ detail: "under-the-hood install/attach repair did not complete",
3926
+ hookLayout: null,
3927
+ hookPath: null
3928
+ },
3929
+ restart: {
3930
+ required: false,
3931
+ automatic: false,
3932
+ performed: false,
3933
+ command: null,
3934
+ detail: "restart did not run because install failed earlier",
3935
+ error: null
3936
+ },
3937
+ verification: {
3938
+ summaryLine: "verification did not run because install failed earlier",
3939
+ nextStep: "Fix the install failure and rerun openclawbrain install.",
3940
+ displayedStatus: "unknown",
3941
+ installState: "unknown",
3942
+ loadability: "unknown",
3943
+ runtimeLoad: "unknown",
3944
+ loadProof: "unknown",
3945
+ serveState: "unknown",
3946
+ routeFnAvailable: false,
3947
+ awaitingFirstExport: false
3948
+ },
3949
+ verdict
3950
+ };
3951
+ emitInstallConvergeResult(failureResult, parsed);
3952
+ return 1;
3953
+ }
3954
+ const attachDiff = diffOpenClawBrainConvergeRuntimeFingerprint(pluginResult.after, readInstallRuntimeFingerprint(parsed.openclawHome));
3955
+ const changeReasons = [...new Set([...pluginResult.changeReasons, ...attachDiff.reasons])];
3956
+ const restartPlan = buildOpenClawBrainConvergeRestartPlan({
3957
+ profileName: attachResult.brainFeedback.profile.exactProfileName,
3958
+ changeReasons
3959
+ });
3960
+ let restartCapture = null;
3961
+ let restartError = null;
3962
+ if (restartPlan.required && restartPlan.automatic) {
3963
+ restartCapture = runCapturedExternalCommand("openclaw", buildGatewayRestartArgs(attachResult.brainFeedback.profile.exactProfileName));
3964
+ if (restartCapture.error !== null || restartCapture.exitCode !== 0) {
3965
+ restartError = `Automatic restart failed after converge. Tried \`${restartCapture.shellCommand}\`. Detail: ${summarizeCapturedCommandFailure(restartCapture)}`;
3966
+ }
3967
+ }
3968
+ const verificationSnapshot = inspectInstallConvergeVerification(parsed);
3969
+ const verification = classifyOpenClawBrainConvergeVerification({
3970
+ ...verificationSnapshot.facts,
3971
+ restartRequired: restartPlan.required,
3972
+ restartPerformed: restartPlan.required && restartPlan.automatic && restartError === null
3973
+ });
3974
+ const convergeWarnings = [];
3975
+ if (pluginResult.warning) {
3976
+ convergeWarnings.push(pluginResult.warning);
3977
+ }
3978
+ if (restartError !== null) {
3979
+ convergeWarnings.push(restartError);
3980
+ }
3981
+ const verdict = finalizeOpenClawBrainConvergeResult({
3982
+ stepFailure: null,
3983
+ verification,
3984
+ warnings: convergeWarnings
3985
+ });
3986
+ const result = {
3987
+ command: "install",
3988
+ openclawHome: parsed.openclawHome,
3989
+ activationRoot: parsed.activationRoot,
3990
+ plugin: {
3991
+ action: pluginResult.plan.action,
3992
+ command: pluginResult.command,
3993
+ changed: pluginResult.changed,
3994
+ changeReasons: pluginResult.changeReasons,
3995
+ detail: pluginResult.detail
3996
+ },
3997
+ attach: {
3998
+ changed: attachDiff.changed,
3999
+ changeReasons: attachDiff.reasons,
4000
+ detail: attachDiff.changed
4001
+ ? `Converged hook/install repair: ${describeOpenClawBrainConvergeChangeReasons(attachDiff.reasons)}`
4002
+ : "Converged hook/install repair confirmed the selected activation root and hook wiring without additional runtime-affecting changes",
4003
+ hookLayout: attachResult.brainFeedback.hookLayout,
4004
+ hookPath: attachResult.brainFeedback.hookPath
4005
+ },
4006
+ restart: {
4007
+ required: restartPlan.required,
4008
+ automatic: restartPlan.automatic,
4009
+ performed: restartPlan.required && restartPlan.automatic && restartError === null,
4010
+ command: restartCapture?.shellCommand ?? null,
4011
+ detail: restartPlan.required
4012
+ ? restartPlan.automatic
4013
+ ? restartError === null
4014
+ ? `Ran automatic gateway restart: ${restartCapture.shellCommand}`
4015
+ : restartError
4016
+ : `${restartPlan.detail} Restart it manually, then rerun status.`
4017
+ : restartPlan.detail,
4018
+ error: restartError
4019
+ },
4020
+ verification: {
4021
+ summaryLine: verificationSnapshot.summaryLine,
4022
+ nextStep: verificationSnapshot.nextStep,
4023
+ displayedStatus: verificationSnapshot.displayedStatus,
4024
+ installState: verificationSnapshot.facts.installState,
4025
+ installLayout: verificationSnapshot.facts.installLayout,
4026
+ loadability: verificationSnapshot.facts.loadability,
4027
+ runtimeLoad: verificationSnapshot.facts.runtimeLoad,
4028
+ loadProof: verificationSnapshot.facts.loadProof,
4029
+ serveState: verificationSnapshot.facts.serveState,
4030
+ routeFnAvailable: verificationSnapshot.facts.routeFnAvailable,
4031
+ awaitingFirstExport: verificationSnapshot.facts.awaitingFirstExport
4032
+ },
4033
+ verdict,
4034
+ underlyingInstall: attachResult
4035
+ };
4036
+ emitInstallConvergeResult(result, parsed);
4037
+ return verdict.verdict === "failed" || verdict.verdict === "manual_action_required" ? 1 : 0;
3561
4038
  }
3562
4039
  function runAttachCommand(parsed) {
3563
4040
  return runProfileHookAttachCommand(parsed);
@@ -4023,7 +4500,7 @@ function persistWatchTracedLearningBridgeSurface(input) {
4023
4500
  }
4024
4501
  }));
4025
4502
  }
4026
- function runLearnCommand(parsed) {
4503
+ async function runLearnCommand(parsed) {
4027
4504
  const learnStatePath = path.join(parsed.activationRoot, "learn-cli-state.json");
4028
4505
  const teacherSnapshotPath = resolveAsyncTeacherLiveLoopSnapshotPath(parsed.activationRoot);
4029
4506
  function isLearnRuntimeStateLike(value) {
@@ -4236,6 +4713,7 @@ function runLearnCommand(parsed) {
4236
4713
  return 0;
4237
4714
  }
4238
4715
  const learningExport = normalizedEventExport;
4716
+ const resolvedEmbedder = resolveCliEmbedderConfig(undefined, activationRoot);
4239
4717
  const serveTimeLearning = resolveServeTimeLearningRuntimeInput(activationRoot);
4240
4718
  const learnerResult = drainAlwaysOnLearningRuntime({
4241
4719
  packLabel: "learn-cli",
@@ -4258,7 +4736,11 @@ function runLearnCommand(parsed) {
4258
4736
  ...(serveTimeLearning.baselineState !== undefined ? { baselineState: serveTimeLearning.baselineState } : {}),
4259
4737
  activationRoot
4260
4738
  });
4261
- const lastMaterialization = learnerResult.materializations.at(-1) ?? null;
4739
+ let lastMaterialization = learnerResult.materializations.at(-1) ?? null;
4740
+ lastMaterialization = await reindexMaterializationCandidateWithEmbedder(lastMaterialization, resolvedEmbedder.embedder);
4741
+ if (lastMaterialization !== null && learnerResult.materializations.length > 0) {
4742
+ learnerResult.materializations[learnerResult.materializations.length - 1] = lastMaterialization;
4743
+ }
4262
4744
  const plan = describeAlwaysOnLearningRuntimeState(learnerResult.state, lastMaterialization);
4263
4745
  const learningPath = summarizeLearningPathFromMaterialization(lastMaterialization);
4264
4746
  const graphEvolution = lastMaterialization?.candidate.payloads.graph.evolution;
@@ -4651,14 +5133,9 @@ async function applyWatchMaterialization(activationRoot, snapshot, lastHandledMa
4651
5133
  failure: null
4652
5134
  };
4653
5135
  }
4654
- if (embedder !== null) {
4655
- materialization = {
4656
- ...materialization,
4657
- candidate: await reindexCandidatePackBuildResultWithEmbedder(materialization.candidate, embedder)
4658
- };
4659
- if (snapshot?.learner !== undefined && snapshot.learner !== null) {
4660
- snapshot.learner.lastMaterialization = materialization;
4661
- }
5136
+ materialization = await reindexMaterializationCandidateWithEmbedder(materialization, embedder);
5137
+ if (snapshot?.learner !== undefined && snapshot.learner !== null) {
5138
+ snapshot.learner.lastMaterialization = materialization;
4662
5139
  }
4663
5140
  const shortPackId = packId.length > 16 ? packId.slice(0, 16) : packId;
4664
5141
  const observedAt = new Date().toISOString();
@@ -4839,7 +5316,7 @@ function resolveWatchTeacherLabelerConfig(input, activationRoot) {
4839
5316
  warnings
4840
5317
  };
4841
5318
  }
4842
- function resolveWatchEmbedderConfig(input, activationRoot) {
5319
+ function resolveCliEmbedderConfig(input, activationRoot) {
4843
5320
  if (input !== undefined) {
4844
5321
  return {
4845
5322
  embedder: input,
@@ -5023,7 +5500,7 @@ export async function createWatchCommandRuntime(input) {
5023
5500
  log(`Scan root: ${shortenPath(scanRoot)}`);
5024
5501
  log(`State: cursor=${shortenPath(sessionTailCursorPath)} snapshot=${shortenPath(teacherSnapshotPath)}`);
5025
5502
  const resolvedTeacherLabeler = resolveWatchTeacherLabelerConfig(input.teacherLabeler, activationRoot);
5026
- const resolvedEmbedder = resolveWatchEmbedderConfig(input.embedder, activationRoot);
5503
+ const resolvedEmbedder = resolveCliEmbedderConfig(input.embedder, activationRoot);
5027
5504
  const teacherLabeler = resolvedTeacherLabeler.teacherLabeler;
5028
5505
  for (const warning of resolvedTeacherLabeler.warnings) {
5029
5506
  startupWarnings.push(`teacher_config_warning:${warning}`);
@@ -5644,7 +6121,12 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
5644
6121
  return runHistoryCommand(parsed);
5645
6122
  }
5646
6123
  if (parsed.command === "learn") {
5647
- return runLearnCommand(parsed);
6124
+ runLearnCommand(parsed).then((code) => { process.exitCode = code; }, (error) => {
6125
+ console.error("[openclawbrain] learn failed");
6126
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
6127
+ process.exitCode = 1;
6128
+ });
6129
+ return 0;
5648
6130
  }
5649
6131
  if (parsed.command === "watch") {
5650
6132
  // Watch is async — bridge to sync CLI entry by scheduling and returning 0.