@openclawbrain/cli 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,18 +1,31 @@
1
1
  # @openclawbrain/cli
2
2
 
3
- Staged operator split for OpenClawBrain.
3
+ `@openclawbrain/cli@0.4.2` is the repo's next operator CLI package surface for OpenClawBrain. The latest published CLI remains `0.4.1` until this repo state is shipped.
4
4
 
5
- - This package carries the `openclawbrain` CLI, daemon controls, import/export helpers, and install/status/operator management code.
6
- - `@openclawbrain/openclaw` is the plugin/runtime payload.
7
- - Public install docs are not switched yet; this package exists in-repo so the split can be verified before release.
5
+ Primary public flow:
6
+
7
+ ```bash
8
+ openclaw plugins install @openclawbrain/openclaw@0.4.0
9
+ npx @openclawbrain/cli@0.4.2 install --openclaw-home ~/.openclaw
10
+ openclaw gateway restart
11
+ npx @openclawbrain/cli@0.4.2 status --openclaw-home ~/.openclaw --detailed
12
+ ```
13
+
14
+ Patch note for `0.4.2`: the CLI now persists declared attachment policy during install/attach so later `status` reads stop underreporting shared installs as `policy=null` / `undeclared`, and the package tarball now carries the full operator module surface plus traced-learning bridge needed for the canonical brain-store status path.
15
+
16
+ Current caveat: some hosts still warn about a plugin id mismatch because the plugin manifest uses `openclawbrain` while the package/entry hint uses `openclaw`. The install still works; treat that warning as currently cosmetic.
17
+
18
+ This package carries the `openclawbrain` CLI, daemon controls, import/export helpers, and install/status/operator management code. `@openclawbrain/openclaw` is the plugin/runtime payload.
8
19
 
9
20
  ## Commands
10
21
 
11
22
  ```bash
12
- openclawbrain install --openclaw-home ~/.openclaw
13
- openclawbrain status --openclaw-home ~/.openclaw --detailed
14
- openclawbrain rollback --activation-root /var/openclawbrain/activation --dry-run
15
- openclawbrain daemon status --activation-root /var/openclawbrain/activation
23
+ npx @openclawbrain/cli@0.4.2 install --openclaw-home ~/.openclaw
24
+ npx @openclawbrain/cli@0.4.2 status --openclaw-home ~/.openclaw --detailed
25
+ npx @openclawbrain/cli@0.4.2 rollback --activation-root /var/openclawbrain/activation --dry-run
26
+ npx @openclawbrain/cli@0.4.2 daemon status --activation-root /var/openclawbrain/activation
16
27
  ```
17
28
 
18
- The old `openclawbrain-ops` alias stays wired to the same entrypoint for staged compatibility.
29
+ If the CLI is already on your `PATH`, `openclawbrain ...` is the same command surface. The docs lead with `npx` because that is the clean-host public-registry lane that already passed on `redogfood`.
30
+
31
+ The old `openclawbrain-ops` alias stays wired to the same entrypoint for compatibility.
@@ -0,0 +1,175 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const ATTACHMENT_POLICY_DECLARATION_CONTRACT = "openclawbrain.attachment-policy-declaration.v1";
5
+ const ATTACHMENT_POLICY_DECLARATION_DIRNAME = "attachment-truth";
6
+ const ATTACHMENT_POLICY_DECLARATION_BASENAME = "policy-declaration.json";
7
+
8
+ function toErrorMessage(error) {
9
+ return error instanceof Error ? error.message : String(error);
10
+ }
11
+
12
+ function readRecord(value) {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
14
+ return null;
15
+ }
16
+
17
+ return value;
18
+ }
19
+
20
+ function normalizeOptionalString(value) {
21
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
22
+ }
23
+
24
+ function normalizeIsoTimestamp(value, fieldName) {
25
+ const normalized = normalizeOptionalString(value);
26
+
27
+ if (normalized === null) {
28
+ throw new Error(`${fieldName} is required`);
29
+ }
30
+
31
+ if (Number.isNaN(Date.parse(normalized))) {
32
+ throw new Error(`${fieldName} must be an ISO timestamp`);
33
+ }
34
+
35
+ return new Date(normalized).toISOString();
36
+ }
37
+
38
+ function normalizeAttachmentPolicy(value, fieldName = "policy") {
39
+ if (value === "undeclared" || value === "dedicated" || value === "shared") {
40
+ return value;
41
+ }
42
+
43
+ throw new Error(`${fieldName} must be one of undeclared, dedicated, or shared`);
44
+ }
45
+
46
+ function normalizeOptionalAttachmentPolicy(value, fieldName) {
47
+ if (value === null || value === undefined) {
48
+ return null;
49
+ }
50
+
51
+ return normalizeAttachmentPolicy(value, fieldName);
52
+ }
53
+
54
+ function validateAttachmentPolicyDeclaration(activationRoot, value) {
55
+ const record = readRecord(value);
56
+
57
+ if (record === null) {
58
+ throw new Error("attachment policy declaration must contain an object");
59
+ }
60
+
61
+ if (record.contract !== ATTACHMENT_POLICY_DECLARATION_CONTRACT) {
62
+ throw new Error(`attachment policy declaration contract must be ${ATTACHMENT_POLICY_DECLARATION_CONTRACT}`);
63
+ }
64
+
65
+ if (typeof record.activationRoot !== "string" || record.activationRoot.trim().length === 0) {
66
+ throw new Error("attachment policy declaration activationRoot must be a non-empty string");
67
+ }
68
+
69
+ const resolvedActivationRoot = path.resolve(record.activationRoot);
70
+ if (resolvedActivationRoot !== activationRoot) {
71
+ throw new Error(`attachment policy declaration activationRoot mismatch: expected ${activationRoot}, received ${resolvedActivationRoot}`);
72
+ }
73
+
74
+ return {
75
+ contract: ATTACHMENT_POLICY_DECLARATION_CONTRACT,
76
+ activationRoot,
77
+ updatedAt: normalizeIsoTimestamp(record.updatedAt, "updatedAt"),
78
+ policy: normalizeAttachmentPolicy(record.policy),
79
+ source: normalizeOptionalString(record.source) ?? "unknown",
80
+ openclawHome: normalizeOptionalString(record.openclawHome),
81
+ };
82
+ }
83
+
84
+ export function resolveAttachmentPolicyDeclarationPath(activationRoot) {
85
+ return path.resolve(activationRoot, ATTACHMENT_POLICY_DECLARATION_DIRNAME, ATTACHMENT_POLICY_DECLARATION_BASENAME);
86
+ }
87
+
88
+ export function loadAttachmentPolicyDeclaration(activationRoot) {
89
+ const resolvedActivationRoot = path.resolve(activationRoot);
90
+ const declarationPath = resolveAttachmentPolicyDeclarationPath(resolvedActivationRoot);
91
+
92
+ if (!existsSync(declarationPath)) {
93
+ return {
94
+ path: declarationPath,
95
+ declaration: null,
96
+ error: null,
97
+ };
98
+ }
99
+
100
+ try {
101
+ return {
102
+ path: declarationPath,
103
+ declaration: validateAttachmentPolicyDeclaration(
104
+ resolvedActivationRoot,
105
+ JSON.parse(readFileSync(declarationPath, "utf8")),
106
+ ),
107
+ error: null,
108
+ };
109
+ } catch (error) {
110
+ return {
111
+ path: declarationPath,
112
+ declaration: null,
113
+ error: toErrorMessage(error),
114
+ };
115
+ }
116
+ }
117
+
118
+ export function writeAttachmentPolicyDeclaration(input) {
119
+ const activationRoot = path.resolve(input.activationRoot);
120
+ const declarationPath = resolveAttachmentPolicyDeclarationPath(activationRoot);
121
+ const declaration = {
122
+ contract: ATTACHMENT_POLICY_DECLARATION_CONTRACT,
123
+ activationRoot,
124
+ updatedAt: normalizeIsoTimestamp(input.updatedAt ?? new Date().toISOString(), "updatedAt"),
125
+ policy: normalizeAttachmentPolicy(input.policy),
126
+ source: normalizeOptionalString(input.source) ?? "cli",
127
+ openclawHome: normalizeOptionalString(input.openclawHome),
128
+ };
129
+
130
+ mkdirSync(path.dirname(declarationPath), { recursive: true });
131
+ writeFileSync(declarationPath, `${JSON.stringify(declaration, null, 2)}\n`, "utf8");
132
+
133
+ return {
134
+ path: declarationPath,
135
+ declaration,
136
+ };
137
+ }
138
+
139
+ export function resolveEffectiveAttachmentPolicyTruth(input) {
140
+ const referenceCount =
141
+ typeof input.referenceCount === "number" && Number.isInteger(input.referenceCount) && input.referenceCount >= 0
142
+ ? input.referenceCount
143
+ : 0;
144
+ const discoverablePolicy = referenceCount > 1 ? "shared" : null;
145
+ const statusPolicy = normalizeOptionalAttachmentPolicy(input.statusPolicy ?? null, "statusPolicy");
146
+ const reportPolicy = normalizeOptionalAttachmentPolicy(input.reportPolicy ?? null, "reportPolicy");
147
+ const declaredPolicy = normalizeOptionalAttachmentPolicy(input.declaredPolicy ?? null, "declaredPolicy");
148
+ const effectivePolicy =
149
+ discoverablePolicy ??
150
+ (statusPolicy !== null && statusPolicy !== "undeclared"
151
+ ? statusPolicy
152
+ : reportPolicy !== null && reportPolicy !== "undeclared"
153
+ ? reportPolicy
154
+ : declaredPolicy);
155
+
156
+ return {
157
+ effectivePolicy,
158
+ statusPolicy:
159
+ effectivePolicy === null
160
+ ? statusPolicy
161
+ : discoverablePolicy !== null
162
+ ? discoverablePolicy
163
+ : statusPolicy === null || statusPolicy === "undeclared"
164
+ ? effectivePolicy
165
+ : statusPolicy,
166
+ reportPolicy:
167
+ effectivePolicy === null
168
+ ? reportPolicy
169
+ : discoverablePolicy !== null
170
+ ? discoverablePolicy
171
+ : reportPolicy === null || reportPolicy === "undeclared"
172
+ ? effectivePolicy
173
+ : reportPolicy,
174
+ };
175
+ }
package/dist/src/cli.js CHANGED
@@ -15,9 +15,11 @@ 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, pinInstalledOpenClawBrainPluginActivationRoot, resolveOpenClawBrainInstallTarget } from "./openclaw-plugin-install.js";
18
+ import { loadAttachmentPolicyDeclaration, resolveEffectiveAttachmentPolicyTruth, writeAttachmentPolicyDeclaration } from "./attachment-policy-truth.js";
18
19
  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";
19
20
  import { appendLearningUpdateLogs } from "./learning-spine.js";
20
21
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
22
+ import { buildTracedLearningStatusSurface, loadBrainStoreTracedLearningBridge, mergeTracedLearningBridgePayload, persistBrainStoreTracedLearningBridge, writeTracedLearningBridge } from "./traced-learning-bridge.js";
21
23
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
22
24
  import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
23
25
  const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
@@ -861,6 +863,56 @@ function summarizeStatusAttachmentTruth(input) {
861
863
  })
862
864
  };
863
865
  }
866
+ function normalizeAttachmentPolicyMode(value) {
867
+ return value === "undeclared" || value === "dedicated" || value === "shared"
868
+ ? value
869
+ : null;
870
+ }
871
+ function applyAttachmentPolicyTruth(status, report) {
872
+ const referenceCount = findInstalledHookReferencesForActivationRoot({
873
+ activationRoot: status.host.activationRoot
874
+ }).length;
875
+ const declaration = loadAttachmentPolicyDeclaration(status.host.activationRoot);
876
+ const resolvedPolicy = resolveEffectiveAttachmentPolicyTruth({
877
+ statusPolicy: normalizeAttachmentPolicyMode(status.attachment.policyMode),
878
+ reportPolicy: report === null
879
+ ? null
880
+ : normalizeAttachmentPolicyMode(report.manyProfile.declaredAttachmentPolicy),
881
+ declaredPolicy: declaration.declaration?.policy ?? null,
882
+ referenceCount
883
+ });
884
+ const effectivePolicy = resolvedPolicy.effectivePolicy;
885
+ if (effectivePolicy === null) {
886
+ return {
887
+ status,
888
+ report
889
+ };
890
+ }
891
+ const nextStatusPolicy = resolvedPolicy.statusPolicy;
892
+ const nextReportPolicy = report === null
893
+ ? null
894
+ : resolvedPolicy.reportPolicy;
895
+ return {
896
+ status: nextStatusPolicy === status.attachment.policyMode
897
+ ? status
898
+ : {
899
+ ...status,
900
+ attachment: {
901
+ ...status.attachment,
902
+ policyMode: nextStatusPolicy
903
+ }
904
+ },
905
+ report: report === null || nextReportPolicy === report.manyProfile.declaredAttachmentPolicy
906
+ ? report
907
+ : {
908
+ ...report,
909
+ manyProfile: {
910
+ ...report.manyProfile,
911
+ declaredAttachmentPolicy: nextReportPolicy
912
+ }
913
+ }
914
+ };
915
+ }
864
916
  function runOllamaProbe(args, baseUrl) {
865
917
  try {
866
918
  execFileSync("ollama", [...args], {
@@ -1257,6 +1309,10 @@ function summarizeDisplayedStatus(status, installHook) {
1257
1309
  ? "fail"
1258
1310
  : status.brainStatus.status;
1259
1311
  }
1312
+ function formatTracedLearningSurface(surface) {
1313
+ const detail = surface.error === null ? surface.detail : `${surface.detail}: ${surface.error}`;
1314
+ return `present=${yesNo(surface.present)} updated=${surface.updatedAt ?? "none"} routes=${surface.routeTraceCount} supervision=${surface.supervisionCount} updates=${surface.routerUpdateCount} teacher=${surface.teacherArtifactCount} pg=${surface.pgVersionUsed ?? "none"} pack=${surface.materializedPackId ?? "none"} detail=${detail}`;
1315
+ }
1260
1316
  function buildCompactStatusHeader(status, report, options) {
1261
1317
  const installHook = summarizeStatusInstallHook(options.openclawHome);
1262
1318
  const hookLoad = summarizeStatusHookLoad(installHook, status);
@@ -1272,6 +1328,7 @@ function buildCompactStatusHeader(status, report, options) {
1272
1328
  openclawHome: options.openclawHome,
1273
1329
  status
1274
1330
  });
1331
+ const tracedLearning = options.tracedLearning ?? buildTracedLearningStatusSurface(status.host.activationRoot);
1275
1332
  return [
1276
1333
  `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1277
1334
  `hook install=${hookLoad.installState} loadability=${hookLoad.loadability} loadProof=${hookLoad.loadProof} layout=${status.hook.installLayout ?? "unverified"} additional=${status.hook.additionalInstallCount ?? 0} detail=${hookLoad.detail}`,
@@ -1286,6 +1343,7 @@ function buildCompactStatusHeader(status, report, options) {
1286
1343
  `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}`,
1287
1344
  `embedder model=${embedder.model} provisioned=${yesNo(embedder.provisioned)} live=${yesNo(embedder.live)} why=${embedder.detail}`,
1288
1345
  `routeFn available=${yesNo(routeFn.available)} freshness=${routeFn.freshness} trained=${routeFn.trainedAt ?? "none"} updated=${routeFn.updatedAt ?? "none"} used=${routeFn.usedAt ?? "none"} why=${routeFn.detail}`,
1346
+ `traced ${formatTracedLearningSurface(tracedLearning)}`,
1289
1347
  `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
1290
1348
  `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
1291
1349
  `alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
@@ -1302,6 +1360,7 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
1302
1360
  openclawHome: options.openclawHome,
1303
1361
  status
1304
1362
  });
1363
+ const tracedLearning = options.tracedLearning ?? buildTracedLearningStatusSurface(status.host.activationRoot);
1305
1364
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
1306
1365
  const targetLine = targetInspection === null
1307
1366
  ? `target activation=${status.host.activationRoot} source=activation_root_only`
@@ -1335,6 +1394,7 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
1335
1394
  `graph source=${report.graph.runtimePlasticitySource ?? "none"} blocks=${report.graph.blockCount ?? "none"} strongest=${report.graph.strongestBlockId ?? "none"} ops=${formatStructuralOps(report)} latest=${report.graph.latestMaterialization.packId ?? "none"} latestChanged=${yesNo(report.graph.latestMaterialization.changed)} connect=${formatGraphConnectDiagnostics(report.graph.latestMaterialization.connectDiagnostics ?? report.graph.connectDiagnostics)} summary=${formatGraphSummary(report)}`,
1336
1395
  `path ${formatLearningPathSummary(report.learningPath)}`,
1337
1396
  `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}`,
1397
+ `traced ${formatTracedLearningSurface(tracedLearning)}`,
1338
1398
  `teacherProof ${formatTeacherLoopSummary(report)}`,
1339
1399
  `watch cadence=${report.teacherLoop.learningCadence} scan=${report.teacherLoop.scanPolicy} heartbeat=${report.teacherLoop.lastHeartbeatAt ?? "none"} interval=${report.teacherLoop.pollIntervalSeconds ?? "none"} replayed=${report.teacherLoop.replayedBundleCount ?? "none"}/${report.teacherLoop.replayedEventCount ?? "none"} exported=${report.teacherLoop.exportedBundleCount ?? "none"}/${report.teacherLoop.exportedEventCount ?? "none"} tail=${report.teacherLoop.sessionTailSessionsTracked ?? "none"}/${report.teacherLoop.sessionTailBridgedEventCount ?? "none"} tailState=${report.teacherLoop.localSessionTailNoopReason ?? "none"} lastJob=${report.teacherLoop.lastAppliedMaterializationJobId ?? "none"} lastPack=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
1340
1400
  `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
@@ -3294,6 +3354,13 @@ function runProfileHookAttachCommand(parsed) {
3294
3354
  steps.push(pluginConfigRepair.detail);
3295
3355
  const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
3296
3356
  steps.push(learnerService.detail);
3357
+ const attachmentPolicyDeclaration = writeAttachmentPolicyDeclaration({
3358
+ activationRoot: parsed.activationRoot,
3359
+ policy: parsed.shared ? "shared" : "dedicated",
3360
+ source: parsed.command,
3361
+ openclawHome: parsed.openclawHome
3362
+ });
3363
+ steps.push(`Recorded attachment policy declaration: ${attachmentPolicyDeclaration.declaration.policy} at ${shortenPath(attachmentPolicyDeclaration.path)}`);
3297
3364
  const brainFeedback = buildInstallBrainFeedbackSummary({
3298
3365
  parsed,
3299
3366
  targetInspection,
@@ -4244,6 +4311,31 @@ function runLearnCommand(parsed) {
4244
4311
  lastAppliedMaterializationJobId: lastMaterialization?.jobId ?? null
4245
4312
  }
4246
4313
  });
4314
+ const tracedLearningBridge = mergeTracedLearningBridgePayload({
4315
+ updatedAt: now,
4316
+ routeTraceCount: lastMaterialization?.candidate.summary.learnedRouter.routeTraceCount ?? serveTimeLearning.decisionLogCount,
4317
+ supervisionCount,
4318
+ routerUpdateCount,
4319
+ teacherArtifactCount: teacherArtifacts.length,
4320
+ pgVersionRequested: learnPathReport.pgVersionRequested,
4321
+ pgVersionUsed: learnPathReport.pgVersionUsed,
4322
+ decisionLogCount: learnPathReport.decisionLogCount,
4323
+ fallbackReason: learnPathReport.fallbackReason,
4324
+ routerNoOpReason,
4325
+ materializedPackId,
4326
+ promoted,
4327
+ baselinePersisted,
4328
+ source: {
4329
+ command: "learn",
4330
+ exportDigest: learningExport.provenance.exportDigest,
4331
+ teacherSnapshotPath
4332
+ }
4333
+ }, loadBrainStoreTracedLearningBridge());
4334
+ const surfacedSupervisionCount = tracedLearningBridge.supervisionCount;
4335
+ const surfacedRouterUpdateCount = tracedLearningBridge.routerUpdateCount;
4336
+ const surfacedRouterNoOpReason = tracedLearningBridge.routerNoOpReason;
4337
+ persistBrainStoreTracedLearningBridge(tracedLearningBridge);
4338
+ writeTracedLearningBridge(activationRoot, tracedLearningBridge);
4247
4339
  const summaryMessage = materializedPackId === null
4248
4340
  ? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no candidate materialized, no promotion.`
4249
4341
  : `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary}`;
@@ -4266,9 +4358,9 @@ function runLearnCommand(parsed) {
4266
4358
  teacherBudget: learnerResult.state.sparseFeedback.teacherBudget,
4267
4359
  eligibleFeedbackCount: learnerResult.state.sparseFeedback.eligibleFeedbackCount,
4268
4360
  budgetedOutFeedbackCount: learnerResult.state.sparseFeedback.budgetedOutFeedbackCount,
4269
- supervisionCount,
4270
- routerUpdateCount,
4271
- routerNoOpReason,
4361
+ supervisionCount: surfacedSupervisionCount,
4362
+ routerUpdateCount: surfacedRouterUpdateCount,
4363
+ routerNoOpReason: surfacedRouterNoOpReason,
4272
4364
  pending: plan.pending,
4273
4365
  learnedRange: plan.learnedRange
4274
4366
  },
@@ -4288,8 +4380,8 @@ function runLearnCommand(parsed) {
4288
4380
  }
4289
4381
  else {
4290
4382
  const text = materializedPackId === null
4291
- ? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no promotion. cycles=${learnerResult.cycles.length} stop=${learnerResult.stopReason} supervision=${supervisionCount}.`
4292
- : `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary} cycles=${learnerResult.cycles.length} supervision=${supervisionCount}.`;
4383
+ ? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no promotion. cycles=${learnerResult.cycles.length} stop=${learnerResult.stopReason} supervision=${surfacedSupervisionCount}.`
4384
+ : `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary} cycles=${learnerResult.cycles.length} supervision=${surfacedSupervisionCount}.`;
4293
4385
  console.log(text);
4294
4386
  console.log(`labels: source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`);
4295
4387
  console.log(`path: source=${learningPath.source} pg=${learningPath.policyGradientVersion} method=${learningPath.policyGradientMethod ?? "none"} target=${learningPath.targetConstruction ?? "none"} connect=${learningPath.connectOpsFired ?? "none"} trajectories=${learningPath.reconstructedTrajectoryCount ?? "none"}`);
@@ -5546,25 +5638,32 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
5546
5638
  teacherSnapshotPath: resolveOperatorTeacherSnapshotPath(activationRoot, statusOrRollback.input.teacherSnapshotPath)
5547
5639
  };
5548
5640
  const status = describeCurrentProfileBrainStatus(operatorInput);
5641
+ const tracedLearning = buildTracedLearningStatusSurface(activationRoot);
5642
+ const normalizedStatusAndReport = applyAttachmentPolicyTruth(status, statusOrRollback.json ? null : buildOperatorSurfaceReport(operatorInput));
5549
5643
  if (statusOrRollback.json) {
5550
- console.log(JSON.stringify(status, null, 2));
5644
+ console.log(JSON.stringify({
5645
+ ...normalizedStatusAndReport.status,
5646
+ tracedLearning
5647
+ }, null, 2));
5551
5648
  }
5552
5649
  else {
5553
- const report = buildOperatorSurfaceReport(operatorInput);
5650
+ const report = normalizedStatusAndReport.report;
5554
5651
  const providerConfig = readOpenClawBrainProviderConfigFromSources({
5555
5652
  env: process.env,
5556
5653
  activationRoot
5557
5654
  });
5558
5655
  if (statusOrRollback.detailed) {
5559
- console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
5656
+ console.log(formatCurrentProfileStatusSummary(normalizedStatusAndReport.status, report, targetInspection, {
5560
5657
  openclawHome: statusOrRollback.openclawHome,
5561
- providerConfig
5658
+ providerConfig,
5659
+ tracedLearning
5562
5660
  }));
5563
5661
  }
5564
5662
  else {
5565
- console.log(formatHumanFriendlyStatus(status, report, targetInspection, {
5663
+ console.log(formatHumanFriendlyStatus(normalizedStatusAndReport.status, report, targetInspection, {
5566
5664
  openclawHome: statusOrRollback.openclawHome,
5567
- providerConfig
5665
+ providerConfig,
5666
+ tracedLearning
5568
5667
  }));
5569
5668
  }
5570
5669
  }
@@ -258,11 +258,16 @@ export function resolveOpenClawBrainInstallTarget(openclawHome) {
258
258
  }
259
259
  export function pinInstalledOpenClawBrainPluginActivationRoot(loaderEntryPath, activationRoot) {
260
260
  const loaderSource = readFileSync(loaderEntryPath, "utf8");
261
+ const activationRootPattern = /const\s+ACTIVATION_ROOT\s*=\s*["'`][^"'`]*["'`];/;
261
262
  const pinnedActivationRoot = `const ACTIVATION_ROOT = ${JSON.stringify(activationRoot)};`;
262
- const nextLoaderSource = loaderSource.replace(/const\s+ACTIVATION_ROOT\s*=\s*["'`][^"'`]*["'`];/, pinnedActivationRoot);
263
- if (nextLoaderSource === loaderSource) {
263
+ const matchedActivationRoot = loaderSource.match(activationRootPattern)?.[0] ?? null;
264
+ if (matchedActivationRoot === null) {
264
265
  throw new Error(`Installed loader entry ${loaderEntryPath} does not expose a patchable ACTIVATION_ROOT constant`);
265
266
  }
267
+ if (matchedActivationRoot === pinnedActivationRoot) {
268
+ return;
269
+ }
270
+ const nextLoaderSource = loaderSource.replace(activationRootPattern, pinnedActivationRoot);
266
271
  writeFileSync(loaderEntryPath, nextLoaderSource, "utf8");
267
272
  }
268
273
  export function resolveOpenClawHomeFromExtensionEntryPath(extensionEntryPath) {
@@ -0,0 +1,554 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+
5
+ const TRACED_LEARNING_BRIDGE_CONTRACT = "openclawbrain.traced-learning-bridge.v1";
6
+ const TRACED_LEARNING_BRIDGE_FILENAME = "traced-learning-state.json";
7
+ // Canonical split-package learn/status summary persisted under brain_training_state.
8
+ const TRACED_LEARNING_STATUS_SURFACE_STATE_KEY = "traced_learning_status_surface_json";
9
+ const TRACED_LEARNING_STATUS_SURFACE_CONTRACT = "openclawbrain.traced-learning-status-surface.v1";
10
+ const TRACED_LEARNING_STATUS_SURFACE_BRIDGE = "brain_store_traced_learning_status_surface";
11
+
12
+ function normalizeCount(value) {
13
+ return Number.isFinite(value) && value >= 0 ? Math.trunc(value) : 0;
14
+ }
15
+ function normalizeOptionalString(value) {
16
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
17
+ }
18
+ function normalizeSource(value) {
19
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
20
+ }
21
+ function normalizeBridgePayload(payload) {
22
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
23
+ throw new Error("expected traced-learning bridge payload object");
24
+ }
25
+ return {
26
+ contract: TRACED_LEARNING_BRIDGE_CONTRACT,
27
+ updatedAt: normalizeOptionalString(payload.updatedAt) ?? new Date().toISOString(),
28
+ routeTraceCount: normalizeCount(payload.routeTraceCount),
29
+ supervisionCount: normalizeCount(payload.supervisionCount),
30
+ routerUpdateCount: normalizeCount(payload.routerUpdateCount),
31
+ teacherArtifactCount: normalizeCount(payload.teacherArtifactCount),
32
+ pgVersionRequested: normalizeOptionalString(payload.pgVersionRequested),
33
+ pgVersionUsed: normalizeOptionalString(payload.pgVersionUsed),
34
+ decisionLogCount: normalizeCount(payload.decisionLogCount),
35
+ fallbackReason: normalizeOptionalString(payload.fallbackReason),
36
+ routerNoOpReason: normalizeOptionalString(payload.routerNoOpReason),
37
+ materializedPackId: normalizeOptionalString(payload.materializedPackId),
38
+ promoted: payload.promoted === true,
39
+ baselinePersisted: payload.baselinePersisted === true,
40
+ source: normalizeSource(payload.source)
41
+ };
42
+ }
43
+ function normalizePersistedStatusSurface(payload) {
44
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
45
+ throw new Error("expected traced-learning status surface payload object");
46
+ }
47
+ const source = normalizeSource(payload.source);
48
+ if (source === null) {
49
+ throw new Error("expected traced-learning status surface source");
50
+ }
51
+ return {
52
+ contract: TRACED_LEARNING_STATUS_SURFACE_CONTRACT,
53
+ updatedAt: normalizeOptionalString(payload.updatedAt) ?? new Date().toISOString(),
54
+ routeTraceCount: normalizeCount(payload.routeTraceCount),
55
+ supervisionCount: normalizeCount(payload.supervisionCount),
56
+ routerUpdateCount: normalizeCount(payload.routerUpdateCount),
57
+ teacherArtifactCount: normalizeCount(payload.teacherArtifactCount),
58
+ pgVersionRequested: normalizeOptionalString(payload.pgVersionRequested),
59
+ pgVersionUsed: normalizeOptionalString(payload.pgVersionUsed),
60
+ decisionLogCount: normalizeCount(payload.decisionLogCount),
61
+ fallbackReason: normalizeOptionalString(payload.fallbackReason),
62
+ routerNoOpReason: normalizeOptionalString(payload.routerNoOpReason),
63
+ materializedPackId: normalizeOptionalString(payload.materializedPackId),
64
+ promoted: payload.promoted === true,
65
+ baselinePersisted: payload.baselinePersisted === true,
66
+ source
67
+ };
68
+ }
69
+ function defaultSurface(pathname, detail, error = null) {
70
+ return {
71
+ path: pathname,
72
+ present: false,
73
+ updatedAt: null,
74
+ routeTraceCount: 0,
75
+ supervisionCount: 0,
76
+ routerUpdateCount: 0,
77
+ teacherArtifactCount: 0,
78
+ pgVersionRequested: null,
79
+ pgVersionUsed: null,
80
+ decisionLogCount: 0,
81
+ materializedPackId: null,
82
+ promoted: false,
83
+ baselinePersisted: false,
84
+ source: null,
85
+ detail,
86
+ error
87
+ };
88
+ }
89
+ function resolveBrainRoot(env = process.env) {
90
+ const explicit = normalizeOptionalString(env.OPENCLAWBRAIN_ROOT);
91
+ if (explicit !== null) {
92
+ return path.resolve(explicit);
93
+ }
94
+ const lcmDatabasePath = normalizeOptionalString(env.LCM_DATABASE_PATH);
95
+ if (lcmDatabasePath !== null) {
96
+ return path.join(path.dirname(path.resolve(lcmDatabasePath)), "openclawbrain");
97
+ }
98
+ return path.join(homedir(), ".openclaw", "openclawbrain");
99
+ }
100
+ function loadTrainingStateValue(db, key) {
101
+ const row = db.prepare(`SELECT value FROM brain_training_state WHERE key = ?`).get(key);
102
+ return row !== undefined && typeof row.value === "string" ? row.value : null;
103
+ }
104
+ function loadTrainingStateJson(db, key) {
105
+ const raw = loadTrainingStateValue(db, key);
106
+ if (typeof raw !== "string") {
107
+ return {
108
+ value: null,
109
+ error: null
110
+ };
111
+ }
112
+ const trimmed = raw.trim();
113
+ if (trimmed.length === 0) {
114
+ return {
115
+ value: null,
116
+ error: null
117
+ };
118
+ }
119
+ try {
120
+ return {
121
+ value: JSON.parse(trimmed),
122
+ error: null
123
+ };
124
+ }
125
+ catch (error) {
126
+ return {
127
+ value: null,
128
+ error: error instanceof Error ? error.message : String(error)
129
+ };
130
+ }
131
+ }
132
+ function writeTrainingStateJson(db, key, value) {
133
+ db.prepare(`INSERT OR REPLACE INTO brain_training_state (key, value) VALUES (?, ?)`).run(key, JSON.stringify(value));
134
+ }
135
+ function countRows(db, tableName) {
136
+ const row = db.prepare(`SELECT COUNT(*) as count FROM ${tableName}`).get();
137
+ return normalizeCount(row?.count);
138
+ }
139
+ function toIsoTimestamp(value) {
140
+ return Number.isFinite(value) && value > 0 ? new Date(value).toISOString() : null;
141
+ }
142
+ function buildPersistedStatusSurfaceBridge(summary, context) {
143
+ return normalizeBridgePayload({
144
+ updatedAt: summary.updatedAt,
145
+ routeTraceCount: summary.routeTraceCount,
146
+ supervisionCount: summary.supervisionCount,
147
+ routerUpdateCount: summary.routerUpdateCount,
148
+ teacherArtifactCount: summary.teacherArtifactCount,
149
+ pgVersionRequested: summary.pgVersionRequested,
150
+ pgVersionUsed: summary.pgVersionUsed,
151
+ decisionLogCount: summary.decisionLogCount,
152
+ fallbackReason: summary.fallbackReason,
153
+ routerNoOpReason: summary.routerNoOpReason,
154
+ materializedPackId: summary.materializedPackId,
155
+ promoted: summary.promoted,
156
+ baselinePersisted: summary.baselinePersisted,
157
+ source: {
158
+ command: "brain-store",
159
+ bridge: TRACED_LEARNING_STATUS_SURFACE_BRIDGE,
160
+ brainRoot: context.brainRoot,
161
+ stateDbPath: context.dbPath,
162
+ persistedKey: TRACED_LEARNING_STATUS_SURFACE_STATE_KEY,
163
+ surfacedFrom: summary.source
164
+ }
165
+ });
166
+ }
167
+ function loadPersistedStatusSurface(db, context) {
168
+ const loaded = loadTrainingStateJson(db, TRACED_LEARNING_STATUS_SURFACE_STATE_KEY);
169
+ if (loaded.value === null) {
170
+ return {
171
+ bridge: null,
172
+ error: loaded.error
173
+ };
174
+ }
175
+ try {
176
+ if (normalizeOptionalString(loaded.value.contract) !== TRACED_LEARNING_STATUS_SURFACE_CONTRACT) {
177
+ throw new Error("unexpected traced-learning status surface contract");
178
+ }
179
+ return {
180
+ bridge: buildPersistedStatusSurfaceBridge(normalizePersistedStatusSurface(loaded.value), context),
181
+ error: null
182
+ };
183
+ }
184
+ catch (error) {
185
+ return {
186
+ bridge: null,
187
+ error: error instanceof Error ? error.message : String(error)
188
+ };
189
+ }
190
+ }
191
+ function buildDerivedBrainStoreBridge(db, context) {
192
+ const routeTraceCount = countRows(db, "brain_traces");
193
+ const supervisionCount = countRows(db, "brain_trace_supervision");
194
+ const candidateUpdateRaw = loadTrainingStateValue(db, "last_pg_candidate_update_json");
195
+ const candidatePackVersionRaw = loadTrainingStateValue(db, "last_pg_candidate_pack_version");
196
+ const candidateUpdate = candidateUpdateRaw === null || candidateUpdateRaw.trim().length === 0
197
+ ? null
198
+ : JSON.parse(candidateUpdateRaw);
199
+ const candidatePackVersion = Number.parseInt(candidatePackVersionRaw ?? "", 10);
200
+ return normalizeBridgePayload({
201
+ updatedAt: toIsoTimestamp(candidateUpdate?.generatedAt),
202
+ routeTraceCount,
203
+ supervisionCount,
204
+ routerUpdateCount: candidateUpdate?.routeUpdateCount,
205
+ teacherArtifactCount: candidateUpdate?.teacherLabelCount,
206
+ pgVersionRequested: null,
207
+ pgVersionUsed: null,
208
+ decisionLogCount: 0,
209
+ fallbackReason: null,
210
+ routerNoOpReason: null,
211
+ materializedPackId: null,
212
+ promoted: false,
213
+ baselinePersisted: false,
214
+ source: {
215
+ command: "brain-store",
216
+ bridge: "brain_store_state",
217
+ brainRoot: context.brainRoot,
218
+ stateDbPath: context.dbPath,
219
+ candidatePackVersion: Number.isFinite(candidatePackVersion) ? candidatePackVersion : null,
220
+ candidateUpdateCount: normalizeCount(candidateUpdate?.updateCount)
221
+ }
222
+ });
223
+ }
224
+ function hasMeaningfulTracedLearningSignal(bridge) {
225
+ return bridge.routeTraceCount > 0 ||
226
+ bridge.supervisionCount > 0 ||
227
+ bridge.routerUpdateCount > 0 ||
228
+ bridge.teacherArtifactCount > 0 ||
229
+ bridge.decisionLogCount > 0 ||
230
+ bridge.materializedPackId !== null ||
231
+ bridge.promoted ||
232
+ bridge.baselinePersisted ||
233
+ bridge.pgVersionRequested !== null ||
234
+ bridge.pgVersionUsed !== null ||
235
+ bridge.fallbackReason !== null ||
236
+ bridge.routerNoOpReason !== null ||
237
+ Number.isFinite(bridge.source?.candidatePackVersion) ||
238
+ normalizeCount(bridge.source?.candidateUpdateCount) > 0;
239
+ }
240
+ export function resolveTracedLearningBridgePath(activationRoot) {
241
+ return path.join(path.resolve(activationRoot), "watch", TRACED_LEARNING_BRIDGE_FILENAME);
242
+ }
243
+ export function writeTracedLearningBridge(activationRoot, payload) {
244
+ const bridgePath = resolveTracedLearningBridgePath(activationRoot);
245
+ const bridge = normalizeBridgePayload(payload);
246
+ mkdirSync(path.dirname(bridgePath), { recursive: true });
247
+ writeFileSync(bridgePath, `${JSON.stringify(bridge, null, 2)}\n`, "utf8");
248
+ return bridgePath;
249
+ }
250
+ export function loadTracedLearningBridge(activationRoot) {
251
+ const bridgePath = resolveTracedLearningBridgePath(activationRoot);
252
+ if (!existsSync(bridgePath)) {
253
+ return {
254
+ path: bridgePath,
255
+ bridge: null,
256
+ error: null
257
+ };
258
+ }
259
+ try {
260
+ const parsed = JSON.parse(readFileSync(bridgePath, "utf8"));
261
+ return {
262
+ path: bridgePath,
263
+ bridge: normalizeBridgePayload(parsed),
264
+ error: null
265
+ };
266
+ }
267
+ catch (error) {
268
+ return {
269
+ path: bridgePath,
270
+ bridge: null,
271
+ error: error instanceof Error ? error.message : String(error)
272
+ };
273
+ }
274
+ }
275
+ export function persistBrainStoreTracedLearningBridge(payload, options = {}) {
276
+ const brainRoot = resolveBrainRoot(options.env ?? process.env);
277
+ const dbPath = path.join(brainRoot, "state.db");
278
+ if (!existsSync(dbPath)) {
279
+ return {
280
+ path: dbPath,
281
+ bridge: null,
282
+ persisted: false,
283
+ error: null
284
+ };
285
+ }
286
+ const sqlite = typeof process.getBuiltinModule === "function"
287
+ ? process.getBuiltinModule("node:sqlite")
288
+ : null;
289
+ if (sqlite === null || typeof sqlite.DatabaseSync !== "function") {
290
+ return {
291
+ path: dbPath,
292
+ bridge: null,
293
+ persisted: false,
294
+ error: null
295
+ };
296
+ }
297
+ let db;
298
+ try {
299
+ db = new sqlite.DatabaseSync(dbPath);
300
+ const summary = normalizePersistedStatusSurface(payload);
301
+ writeTrainingStateJson(db, TRACED_LEARNING_STATUS_SURFACE_STATE_KEY, summary);
302
+ return {
303
+ path: dbPath,
304
+ bridge: buildPersistedStatusSurfaceBridge(summary, {
305
+ brainRoot,
306
+ dbPath
307
+ }),
308
+ persisted: true,
309
+ error: null
310
+ };
311
+ }
312
+ catch (error) {
313
+ return {
314
+ path: dbPath,
315
+ bridge: null,
316
+ persisted: false,
317
+ error: error instanceof Error ? error.message : String(error)
318
+ };
319
+ }
320
+ finally {
321
+ if (db && typeof db.close === "function") {
322
+ db.close();
323
+ }
324
+ }
325
+ }
326
+ export function loadBrainStoreTracedLearningBridge(options = {}) {
327
+ const brainRoot = resolveBrainRoot(options.env ?? process.env);
328
+ const dbPath = path.join(brainRoot, "state.db");
329
+ if (!existsSync(dbPath)) {
330
+ return {
331
+ path: dbPath,
332
+ bridge: null,
333
+ error: null
334
+ };
335
+ }
336
+ const sqlite = typeof process.getBuiltinModule === "function"
337
+ ? process.getBuiltinModule("node:sqlite")
338
+ : null;
339
+ if (sqlite === null || typeof sqlite.DatabaseSync !== "function") {
340
+ return {
341
+ path: dbPath,
342
+ bridge: null,
343
+ error: null
344
+ };
345
+ }
346
+ let db;
347
+ try {
348
+ db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
349
+ const persisted = loadPersistedStatusSurface(db, {
350
+ brainRoot,
351
+ dbPath
352
+ });
353
+ if (persisted.bridge !== null) {
354
+ return {
355
+ path: dbPath,
356
+ bridge: persisted.bridge,
357
+ error: null
358
+ };
359
+ }
360
+ const bridge = buildDerivedBrainStoreBridge(db, {
361
+ brainRoot,
362
+ dbPath
363
+ });
364
+ if (!hasMeaningfulTracedLearningSignal(bridge)) {
365
+ return {
366
+ path: dbPath,
367
+ bridge: null,
368
+ error: persisted.error
369
+ };
370
+ }
371
+ return {
372
+ path: dbPath,
373
+ bridge,
374
+ error: null
375
+ };
376
+ }
377
+ catch (error) {
378
+ return {
379
+ path: dbPath,
380
+ bridge: null,
381
+ error: error instanceof Error ? error.message : String(error)
382
+ };
383
+ }
384
+ finally {
385
+ if (db && typeof db.close === "function") {
386
+ db.close();
387
+ }
388
+ }
389
+ }
390
+ function describeBridgeRuntimeState(loaded) {
391
+ return loaded.bridge === null ? (loaded.error === null ? "missing" : "unreadable") : "present";
392
+ }
393
+ function buildStatusSurface(pathname, bridge, options = {}) {
394
+ const detailParts = [
395
+ `source=${bridge.source?.command === undefined ? "learn" : String(bridge.source.command)}`,
396
+ `promoted=${bridge.promoted ? "yes" : "no"}`
397
+ ];
398
+ if (typeof bridge.source?.bridge === "string") {
399
+ detailParts.push(`bridge=${bridge.source.bridge}`);
400
+ }
401
+ if (options.runtimeState !== undefined) {
402
+ detailParts.push(`runtime=${options.runtimeState}`);
403
+ }
404
+ if (bridge.fallbackReason !== null) {
405
+ detailParts.push(`fallback=${bridge.fallbackReason}`);
406
+ }
407
+ if (bridge.routerNoOpReason !== null) {
408
+ detailParts.push(`noOp=${bridge.routerNoOpReason}`);
409
+ }
410
+ return {
411
+ path: pathname,
412
+ present: true,
413
+ updatedAt: bridge.updatedAt,
414
+ routeTraceCount: bridge.routeTraceCount,
415
+ supervisionCount: bridge.supervisionCount,
416
+ routerUpdateCount: bridge.routerUpdateCount,
417
+ teacherArtifactCount: bridge.teacherArtifactCount,
418
+ pgVersionRequested: bridge.pgVersionRequested,
419
+ pgVersionUsed: bridge.pgVersionUsed,
420
+ decisionLogCount: bridge.decisionLogCount,
421
+ materializedPackId: bridge.materializedPackId,
422
+ promoted: bridge.promoted,
423
+ baselinePersisted: bridge.baselinePersisted,
424
+ source: bridge.source,
425
+ detail: detailParts.join(" "),
426
+ error: options.error ?? null
427
+ };
428
+ }
429
+ function buildRuntimeMaterializationMetadata(loaded) {
430
+ if (loaded.bridge === null) {
431
+ return null;
432
+ }
433
+ return {
434
+ path: loaded.path,
435
+ updatedAt: loaded.bridge.updatedAt,
436
+ routeTraceCount: loaded.bridge.routeTraceCount,
437
+ supervisionCount: loaded.bridge.supervisionCount,
438
+ routerUpdateCount: loaded.bridge.routerUpdateCount,
439
+ teacherArtifactCount: loaded.bridge.teacherArtifactCount,
440
+ pgVersionRequested: loaded.bridge.pgVersionRequested,
441
+ pgVersionUsed: loaded.bridge.pgVersionUsed,
442
+ decisionLogCount: loaded.bridge.decisionLogCount,
443
+ materializedPackId: loaded.bridge.materializedPackId,
444
+ promoted: loaded.bridge.promoted,
445
+ baselinePersisted: loaded.bridge.baselinePersisted,
446
+ fallbackReason: loaded.bridge.fallbackReason,
447
+ routerNoOpReason: loaded.bridge.routerNoOpReason,
448
+ source: loaded.bridge.source
449
+ };
450
+ }
451
+ function mergeCanonicalStatusBridge(canonicalBridge, runtimeLoaded) {
452
+ const runtimeBridge = runtimeLoaded.bridge;
453
+ const runtimeMaterialized = buildRuntimeMaterializationMetadata(runtimeLoaded);
454
+ const hasPersistedSurface = canonicalBridge.source?.bridge === TRACED_LEARNING_STATUS_SURFACE_BRIDGE;
455
+ if (hasPersistedSurface) {
456
+ return {
457
+ updatedAt: canonicalBridge.updatedAt,
458
+ routeTraceCount: canonicalBridge.routeTraceCount,
459
+ supervisionCount: canonicalBridge.supervisionCount,
460
+ routerUpdateCount: canonicalBridge.routerUpdateCount,
461
+ teacherArtifactCount: canonicalBridge.teacherArtifactCount,
462
+ pgVersionRequested: canonicalBridge.pgVersionRequested,
463
+ pgVersionUsed: canonicalBridge.pgVersionUsed,
464
+ decisionLogCount: canonicalBridge.decisionLogCount,
465
+ materializedPackId: canonicalBridge.materializedPackId,
466
+ promoted: canonicalBridge.promoted,
467
+ baselinePersisted: canonicalBridge.baselinePersisted,
468
+ fallbackReason: canonicalBridge.fallbackReason,
469
+ routerNoOpReason: canonicalBridge.routerNoOpReason,
470
+ source: runtimeMaterialized === null
471
+ ? canonicalBridge.source
472
+ : {
473
+ ...(canonicalBridge.source ?? {}),
474
+ runtimeMaterialized
475
+ }
476
+ };
477
+ }
478
+ return {
479
+ updatedAt: canonicalBridge.updatedAt ?? runtimeBridge?.updatedAt ?? null,
480
+ routeTraceCount: canonicalBridge.routeTraceCount,
481
+ supervisionCount: canonicalBridge.supervisionCount,
482
+ routerUpdateCount: canonicalBridge.routerUpdateCount,
483
+ teacherArtifactCount: canonicalBridge.teacherArtifactCount,
484
+ pgVersionRequested: runtimeBridge?.pgVersionRequested ?? canonicalBridge.pgVersionRequested ?? null,
485
+ pgVersionUsed: runtimeBridge?.pgVersionUsed ?? canonicalBridge.pgVersionUsed ?? null,
486
+ decisionLogCount: runtimeBridge?.decisionLogCount ?? canonicalBridge.decisionLogCount ?? 0,
487
+ materializedPackId: runtimeBridge?.materializedPackId ?? canonicalBridge.materializedPackId ?? null,
488
+ promoted: runtimeBridge?.promoted ?? canonicalBridge.promoted,
489
+ baselinePersisted: runtimeBridge?.baselinePersisted ?? canonicalBridge.baselinePersisted,
490
+ fallbackReason: runtimeBridge?.fallbackReason ?? canonicalBridge.fallbackReason ?? null,
491
+ routerNoOpReason: runtimeBridge?.routerNoOpReason ?? canonicalBridge.routerNoOpReason ?? null,
492
+ source: runtimeMaterialized === null
493
+ ? canonicalBridge.source
494
+ : {
495
+ ...(canonicalBridge.source ?? {}),
496
+ runtimeMaterialized
497
+ }
498
+ };
499
+ }
500
+ export function mergeTracedLearningBridgePayload(payload, persisted) {
501
+ const current = normalizeBridgePayload(payload);
502
+ const persistedBridge = persisted?.bridge ?? null;
503
+ if (persistedBridge === null) {
504
+ return current;
505
+ }
506
+ const routeTraceCount = Math.max(current.routeTraceCount, persistedBridge.routeTraceCount);
507
+ const supervisionCount = Math.max(current.supervisionCount, persistedBridge.supervisionCount);
508
+ const routerUpdateCount = Math.max(current.routerUpdateCount, persistedBridge.routerUpdateCount);
509
+ const teacherArtifactCount = Math.max(current.teacherArtifactCount, persistedBridge.teacherArtifactCount);
510
+ const usedBridge = routeTraceCount !== current.routeTraceCount ||
511
+ supervisionCount !== current.supervisionCount ||
512
+ routerUpdateCount !== current.routerUpdateCount ||
513
+ teacherArtifactCount !== current.teacherArtifactCount;
514
+ if (!usedBridge) {
515
+ return current;
516
+ }
517
+ return normalizeBridgePayload({
518
+ ...current,
519
+ routeTraceCount,
520
+ supervisionCount,
521
+ routerUpdateCount,
522
+ teacherArtifactCount,
523
+ routerNoOpReason: supervisionCount > 0 || routerUpdateCount > 0 ? null : current.routerNoOpReason,
524
+ source: {
525
+ ...(current.source ?? {}),
526
+ bridge: normalizeOptionalString(persistedBridge.source?.bridge) ?? "brain_store_state",
527
+ bridgedRuntime: {
528
+ path: persisted?.path ?? null,
529
+ updatedAt: persistedBridge.updatedAt,
530
+ routeTraceCount: persistedBridge.routeTraceCount,
531
+ supervisionCount: persistedBridge.supervisionCount,
532
+ routerUpdateCount: persistedBridge.routerUpdateCount,
533
+ teacherArtifactCount: persistedBridge.teacherArtifactCount,
534
+ source: persistedBridge.source
535
+ }
536
+ }
537
+ });
538
+ }
539
+ export function buildTracedLearningStatusSurface(activationRoot, options = {}) {
540
+ const persisted = loadBrainStoreTracedLearningBridge(options);
541
+ const runtime = loadTracedLearningBridge(activationRoot);
542
+ if (persisted.bridge !== null) {
543
+ return buildStatusSurface(persisted.path, mergeCanonicalStatusBridge(persisted.bridge, runtime), {
544
+ runtimeState: describeBridgeRuntimeState(runtime)
545
+ });
546
+ }
547
+ if (runtime.bridge !== null) {
548
+ return buildStatusSurface(runtime.path, runtime.bridge);
549
+ }
550
+ if (persisted.error !== null) {
551
+ return defaultSurface(persisted.path, "brain_store_unreadable", persisted.error);
552
+ }
553
+ return defaultSurface(runtime.path, runtime.error === null ? "bridge_missing" : "bridge_unreadable", runtime.error);
554
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@openclawbrain/cli",
3
- "version": "0.4.0",
4
- "description": "Staged OpenClawBrain operator package with the openclawbrain CLI, daemon, install/status helpers, and import/export tooling.",
3
+ "version": "0.4.2",
4
+ "description": "OpenClawBrain operator CLI package with install/status helpers, daemon controls, and import/export tooling.",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
7
7
  "types": "./dist/src/index.d.ts",