@openclawbrain/cli 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,17 @@
1
1
  # @openclawbrain/cli
2
2
 
3
- `@openclawbrain/cli@0.4.1` is the published operator CLI package for OpenClawBrain.
3
+ `@openclawbrain/cli@0.4.3` is the published operator CLI package for OpenClawBrain.
4
4
 
5
5
  Primary public flow:
6
6
 
7
7
  ```bash
8
8
  openclaw plugins install @openclawbrain/openclaw@0.4.0
9
- npx @openclawbrain/cli@0.4.1 openclawbrain install --openclaw-home ~/.openclaw
9
+ npx @openclawbrain/cli@0.4.3 install --openclaw-home ~/.openclaw
10
10
  openclaw gateway restart
11
- npx @openclawbrain/cli@0.4.1 openclawbrain status --openclaw-home ~/.openclaw --detailed
11
+ npx @openclawbrain/cli@0.4.3 status --openclaw-home ~/.openclaw --detailed
12
12
  ```
13
13
 
14
- Patch note for `0.4.1`: rerunning `openclawbrain install --shared` against a native package install that is already pinned to the requested activation root is now a successful no-op instead of a false repin failure.
14
+ Patch note for `0.4.3`: the CLI now reports active-pack numeric embeddings more truthfully across alternate vector shapes, and the repo-side serve-time decision matcher is more tolerant of event-id and timestamp drift when harvesting supervision candidates.
15
15
 
16
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
17
 
@@ -20,10 +20,10 @@ This package carries the `openclawbrain` CLI, daemon controls, import/export hel
20
20
  ## Commands
21
21
 
22
22
  ```bash
23
- npx @openclawbrain/cli@0.4.1 openclawbrain install --openclaw-home ~/.openclaw
24
- npx @openclawbrain/cli@0.4.1 openclawbrain status --openclaw-home ~/.openclaw --detailed
25
- npx @openclawbrain/cli@0.4.1 openclawbrain rollback --activation-root /var/openclawbrain/activation --dry-run
26
- npx @openclawbrain/cli@0.4.1 openclawbrain daemon status --activation-root /var/openclawbrain/activation
23
+ npx @openclawbrain/cli@0.4.3 install --openclaw-home ~/.openclaw
24
+ npx @openclawbrain/cli@0.4.3 status --openclaw-home ~/.openclaw --detailed
25
+ npx @openclawbrain/cli@0.4.3 rollback --activation-root /var/openclawbrain/activation --dry-run
26
+ npx @openclawbrain/cli@0.4.3 daemon status --activation-root /var/openclawbrain/activation
27
27
  ```
28
28
 
29
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`.
@@ -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,12 @@ 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 { summarizePackVectorEmbeddingState } from "./embedding-status.js";
23
+ import { buildTracedLearningStatusSurface, loadBrainStoreTracedLearningBridge, mergeTracedLearningBridgePayload, persistBrainStoreTracedLearningBridge, writeTracedLearningBridge } from "./traced-learning-bridge.js";
21
24
  import { discoverOpenClawSessionStores, loadOpenClawSessionIndex, readOpenClawSessionFile } from "./session-store.js";
22
25
  import { readOpenClawBrainProviderDefaults, readOpenClawBrainProviderConfig, readOpenClawBrainProviderConfigFromSources, resolveOpenClawBrainProviderDefaultsPath } from "./provider-config.js";
23
26
  const OPENCLAWBRAIN_EMBEDDER_BASE_URL_ENV = "OPENCLAWBRAIN_EMBEDDER_BASE_URL";
@@ -861,6 +864,56 @@ function summarizeStatusAttachmentTruth(input) {
861
864
  })
862
865
  };
863
866
  }
867
+ function normalizeAttachmentPolicyMode(value) {
868
+ return value === "undeclared" || value === "dedicated" || value === "shared"
869
+ ? value
870
+ : null;
871
+ }
872
+ function applyAttachmentPolicyTruth(status, report) {
873
+ const referenceCount = findInstalledHookReferencesForActivationRoot({
874
+ activationRoot: status.host.activationRoot
875
+ }).length;
876
+ const declaration = loadAttachmentPolicyDeclaration(status.host.activationRoot);
877
+ const resolvedPolicy = resolveEffectiveAttachmentPolicyTruth({
878
+ statusPolicy: normalizeAttachmentPolicyMode(status.attachment.policyMode),
879
+ reportPolicy: report === null
880
+ ? null
881
+ : normalizeAttachmentPolicyMode(report.manyProfile.declaredAttachmentPolicy),
882
+ declaredPolicy: declaration.declaration?.policy ?? null,
883
+ referenceCount
884
+ });
885
+ const effectivePolicy = resolvedPolicy.effectivePolicy;
886
+ if (effectivePolicy === null) {
887
+ return {
888
+ status,
889
+ report
890
+ };
891
+ }
892
+ const nextStatusPolicy = resolvedPolicy.statusPolicy;
893
+ const nextReportPolicy = report === null
894
+ ? null
895
+ : resolvedPolicy.reportPolicy;
896
+ return {
897
+ status: nextStatusPolicy === status.attachment.policyMode
898
+ ? status
899
+ : {
900
+ ...status,
901
+ attachment: {
902
+ ...status.attachment,
903
+ policyMode: nextStatusPolicy
904
+ }
905
+ },
906
+ report: report === null || nextReportPolicy === report.manyProfile.declaredAttachmentPolicy
907
+ ? report
908
+ : {
909
+ ...report,
910
+ manyProfile: {
911
+ ...report.manyProfile,
912
+ declaredAttachmentPolicy: nextReportPolicy
913
+ }
914
+ }
915
+ };
916
+ }
864
917
  function runOllamaProbe(args, baseUrl) {
865
918
  try {
866
919
  execFileSync("ollama", [...args], {
@@ -901,11 +954,14 @@ function summarizeStatusEmbeddings(report, providerConfig) {
901
954
  requireActivationReady: true
902
955
  });
903
956
  if (activePack !== null) {
904
- totalEntryCount = activePack.vectors.entries.length;
905
- embeddedEntryCount = activePack.vectors.entries.filter((entry) => entry.embedding !== undefined).length;
906
- models = [...new Set(activePack.vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
907
- liveState = embeddedEntryCount > 0 ? "yes" : "no";
908
- liveDetail = `active pack stores ${embeddedEntryCount}/${totalEntryCount} numeric embeddings`;
957
+ const summary = summarizePackVectorEmbeddingState(activePack.vectors);
958
+ totalEntryCount = summary.vectorEntryCount;
959
+ embeddedEntryCount = summary.numericEmbeddingEntryCount;
960
+ models = summary.embeddingModels;
961
+ liveState = embeddedEntryCount === null ? "unknown" : embeddedEntryCount > 0 ? "yes" : "no";
962
+ liveDetail = embeddedEntryCount === null || totalEntryCount === null
963
+ ? "active pack vector entries were unreadable during embedding inspection"
964
+ : `active pack stores ${embeddedEntryCount}/${totalEntryCount} numeric embeddings`;
909
965
  }
910
966
  }
911
967
  catch (error) {
@@ -1257,6 +1313,10 @@ function summarizeDisplayedStatus(status, installHook) {
1257
1313
  ? "fail"
1258
1314
  : status.brainStatus.status;
1259
1315
  }
1316
+ function formatTracedLearningSurface(surface) {
1317
+ const detail = surface.error === null ? surface.detail : `${surface.detail}: ${surface.error}`;
1318
+ 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}`;
1319
+ }
1260
1320
  function buildCompactStatusHeader(status, report, options) {
1261
1321
  const installHook = summarizeStatusInstallHook(options.openclawHome);
1262
1322
  const hookLoad = summarizeStatusHookLoad(installHook, status);
@@ -1272,6 +1332,7 @@ function buildCompactStatusHeader(status, report, options) {
1272
1332
  openclawHome: options.openclawHome,
1273
1333
  status
1274
1334
  });
1335
+ const tracedLearning = options.tracedLearning ?? buildTracedLearningStatusSurface(status.host.activationRoot);
1275
1336
  return [
1276
1337
  `lifecycle attach=${status.attachment.state} learner=${yesNo(status.passiveLearning.learnerRunning)} watch=${summarizeStatusWatchState(status)} export=${status.passiveLearning.exportState} promote=${summarizeStatusPromotionState(status)} serve=${summarizeStatusServeReality(status)}`,
1277
1338
  `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 +1347,7 @@ function buildCompactStatusHeader(status, report, options) {
1286
1347
  `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
1348
  `embedder model=${embedder.model} provisioned=${yesNo(embedder.provisioned)} live=${yesNo(embedder.live)} why=${embedder.detail}`,
1288
1349
  `routeFn available=${yesNo(routeFn.available)} freshness=${routeFn.freshness} trained=${routeFn.trainedAt ?? "none"} updated=${routeFn.updatedAt ?? "none"} used=${routeFn.usedAt ?? "none"} why=${routeFn.detail}`,
1350
+ `traced ${formatTracedLearningSurface(tracedLearning)}`,
1289
1351
  `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
1290
1352
  `localLLM detected=${yesNo(localLlm.detected)} enabled=${yesNo(localLlm.enabled)} provider=${localLlm.provider} model=${localLlm.model}`,
1291
1353
  `alerts service_risk=${formatStatusAlertLine(alerts.serviceRisk)} degraded_brain=${formatStatusAlertLine(alerts.degradedBrain)} cosmetic_noise=${formatStatusAlertLine(alerts.cosmeticNoise)}`
@@ -1302,6 +1364,7 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
1302
1364
  openclawHome: options.openclawHome,
1303
1365
  status
1304
1366
  });
1367
+ const tracedLearning = options.tracedLearning ?? buildTracedLearningStatusSurface(status.host.activationRoot);
1305
1368
  const profileIdSuffix = status.profile.profileId === null ? "" : ` id=${status.profile.profileId}`;
1306
1369
  const targetLine = targetInspection === null
1307
1370
  ? `target activation=${status.host.activationRoot} source=activation_root_only`
@@ -1335,6 +1398,7 @@ function formatCurrentProfileStatusSummary(status, report, targetInspection, opt
1335
1398
  `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
1399
  `path ${formatLearningPathSummary(report.learningPath)}`,
1337
1400
  `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}`,
1401
+ `traced ${formatTracedLearningSurface(tracedLearning)}`,
1338
1402
  `teacherProof ${formatTeacherLoopSummary(report)}`,
1339
1403
  `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
1404
  `embeddings provider=${embeddings.provider} provisioned=${embeddings.provisionedState} live=${embeddings.liveState} stored=${embeddings.embeddedEntryCount ?? "none"}/${embeddings.totalEntryCount ?? "none"} models=${liveModels}`,
@@ -3294,6 +3358,13 @@ function runProfileHookAttachCommand(parsed) {
3294
3358
  steps.push(pluginConfigRepair.detail);
3295
3359
  const learnerService = ensureLifecycleLearnerService(parsed.activationRoot);
3296
3360
  steps.push(learnerService.detail);
3361
+ const attachmentPolicyDeclaration = writeAttachmentPolicyDeclaration({
3362
+ activationRoot: parsed.activationRoot,
3363
+ policy: parsed.shared ? "shared" : "dedicated",
3364
+ source: parsed.command,
3365
+ openclawHome: parsed.openclawHome
3366
+ });
3367
+ steps.push(`Recorded attachment policy declaration: ${attachmentPolicyDeclaration.declaration.policy} at ${shortenPath(attachmentPolicyDeclaration.path)}`);
3297
3368
  const brainFeedback = buildInstallBrainFeedbackSummary({
3298
3369
  parsed,
3299
3370
  targetInspection,
@@ -4244,6 +4315,31 @@ function runLearnCommand(parsed) {
4244
4315
  lastAppliedMaterializationJobId: lastMaterialization?.jobId ?? null
4245
4316
  }
4246
4317
  });
4318
+ const tracedLearningBridge = mergeTracedLearningBridgePayload({
4319
+ updatedAt: now,
4320
+ routeTraceCount: lastMaterialization?.candidate.summary.learnedRouter.routeTraceCount ?? serveTimeLearning.decisionLogCount,
4321
+ supervisionCount,
4322
+ routerUpdateCount,
4323
+ teacherArtifactCount: teacherArtifacts.length,
4324
+ pgVersionRequested: learnPathReport.pgVersionRequested,
4325
+ pgVersionUsed: learnPathReport.pgVersionUsed,
4326
+ decisionLogCount: learnPathReport.decisionLogCount,
4327
+ fallbackReason: learnPathReport.fallbackReason,
4328
+ routerNoOpReason,
4329
+ materializedPackId,
4330
+ promoted,
4331
+ baselinePersisted,
4332
+ source: {
4333
+ command: "learn",
4334
+ exportDigest: learningExport.provenance.exportDigest,
4335
+ teacherSnapshotPath
4336
+ }
4337
+ }, loadBrainStoreTracedLearningBridge());
4338
+ const surfacedSupervisionCount = tracedLearningBridge.supervisionCount;
4339
+ const surfacedRouterUpdateCount = tracedLearningBridge.routerUpdateCount;
4340
+ const surfacedRouterNoOpReason = tracedLearningBridge.routerNoOpReason;
4341
+ persistBrainStoreTracedLearningBridge(tracedLearningBridge);
4342
+ writeTracedLearningBridge(activationRoot, tracedLearningBridge);
4247
4343
  const summaryMessage = materializedPackId === null
4248
4344
  ? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no candidate materialized, no promotion.`
4249
4345
  : `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary}`;
@@ -4266,9 +4362,9 @@ function runLearnCommand(parsed) {
4266
4362
  teacherBudget: learnerResult.state.sparseFeedback.teacherBudget,
4267
4363
  eligibleFeedbackCount: learnerResult.state.sparseFeedback.eligibleFeedbackCount,
4268
4364
  budgetedOutFeedbackCount: learnerResult.state.sparseFeedback.budgetedOutFeedbackCount,
4269
- supervisionCount,
4270
- routerUpdateCount,
4271
- routerNoOpReason,
4365
+ supervisionCount: surfacedSupervisionCount,
4366
+ routerUpdateCount: surfacedRouterUpdateCount,
4367
+ routerNoOpReason: surfacedRouterNoOpReason,
4272
4368
  pending: plan.pending,
4273
4369
  learnedRange: plan.learnedRange
4274
4370
  },
@@ -4288,8 +4384,8 @@ function runLearnCommand(parsed) {
4288
4384
  }
4289
4385
  else {
4290
4386
  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}.`;
4387
+ ? `Scanned ${totalSessions} sessions, ${totalEvents} new events, no promotion. cycles=${learnerResult.cycles.length} stop=${learnerResult.stopReason} supervision=${surfacedSupervisionCount}.`
4388
+ : `Scanned ${totalSessions} sessions, ${totalEvents} new events, materialized ${materializedPackId}, promoted.${connectSummary} cycles=${learnerResult.cycles.length} supervision=${surfacedSupervisionCount}.`;
4293
4389
  console.log(text);
4294
4390
  console.log(`labels: source=${labelFlow.source} human=${labelFlow.humanLabelCount ?? "none"} self=${labelFlow.selfLabelCount ?? "none"} implicitPositive=${labelFlow.implicitPositiveCount ?? "none"} teacherArtifacts=${labelFlow.asyncTeacherArtifactCount ?? "none"}`);
4295
4391
  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"}`);
@@ -4452,19 +4548,7 @@ function exportLocalSessionTailChangesToScanRoot(input) {
4452
4548
  };
4453
4549
  }
4454
4550
  function summarizeVectorEmbeddingState(vectors) {
4455
- if (vectors === null || vectors === undefined) {
4456
- return {
4457
- vectorEntryCount: null,
4458
- numericEmbeddingEntryCount: null,
4459
- embeddingModels: []
4460
- };
4461
- }
4462
- const embeddingModels = [...new Set(vectors.entries.flatMap((entry) => (entry.embedding === undefined ? [] : [entry.embedding.model])))].sort((left, right) => left.localeCompare(right));
4463
- return {
4464
- vectorEntryCount: vectors.entries.length,
4465
- numericEmbeddingEntryCount: vectors.entries.filter((entry) => entry.embedding !== undefined).length,
4466
- embeddingModels
4467
- };
4551
+ return summarizePackVectorEmbeddingState(vectors);
4468
4552
  }
4469
4553
  function buildWatchEmbedTracePoint(input) {
4470
4554
  const summary = summarizeVectorEmbeddingState(input.vectors);
@@ -5546,25 +5630,32 @@ export function runOperatorCli(argv = process.argv.slice(2)) {
5546
5630
  teacherSnapshotPath: resolveOperatorTeacherSnapshotPath(activationRoot, statusOrRollback.input.teacherSnapshotPath)
5547
5631
  };
5548
5632
  const status = describeCurrentProfileBrainStatus(operatorInput);
5633
+ const tracedLearning = buildTracedLearningStatusSurface(activationRoot);
5634
+ const normalizedStatusAndReport = applyAttachmentPolicyTruth(status, statusOrRollback.json ? null : buildOperatorSurfaceReport(operatorInput));
5549
5635
  if (statusOrRollback.json) {
5550
- console.log(JSON.stringify(status, null, 2));
5636
+ console.log(JSON.stringify({
5637
+ ...normalizedStatusAndReport.status,
5638
+ tracedLearning
5639
+ }, null, 2));
5551
5640
  }
5552
5641
  else {
5553
- const report = buildOperatorSurfaceReport(operatorInput);
5642
+ const report = normalizedStatusAndReport.report;
5554
5643
  const providerConfig = readOpenClawBrainProviderConfigFromSources({
5555
5644
  env: process.env,
5556
5645
  activationRoot
5557
5646
  });
5558
5647
  if (statusOrRollback.detailed) {
5559
- console.log(formatCurrentProfileStatusSummary(status, report, targetInspection, {
5648
+ console.log(formatCurrentProfileStatusSummary(normalizedStatusAndReport.status, report, targetInspection, {
5560
5649
  openclawHome: statusOrRollback.openclawHome,
5561
- providerConfig
5650
+ providerConfig,
5651
+ tracedLearning
5562
5652
  }));
5563
5653
  }
5564
5654
  else {
5565
- console.log(formatHumanFriendlyStatus(status, report, targetInspection, {
5655
+ console.log(formatHumanFriendlyStatus(normalizedStatusAndReport.status, report, targetInspection, {
5566
5656
  openclawHome: statusOrRollback.openclawHome,
5567
- providerConfig
5657
+ providerConfig,
5658
+ tracedLearning
5568
5659
  }));
5569
5660
  }
5570
5661
  }
@@ -0,0 +1,78 @@
1
+ function isNumericArray(value) {
2
+ return Array.isArray(value) &&
3
+ value.length > 0 &&
4
+ value.every((entry) => typeof entry === "number" && Number.isFinite(entry));
5
+ }
6
+
7
+ function isNumericTypedArray(value) {
8
+ return ArrayBuffer.isView(value) &&
9
+ !(value instanceof DataView) &&
10
+ typeof value.length === "number" &&
11
+ value.length > 0 &&
12
+ Array.from(value).every((entry) => typeof entry === "number" && Number.isFinite(entry));
13
+ }
14
+
15
+ function hasNumericValues(value) {
16
+ return isNumericArray(value) || isNumericTypedArray(value);
17
+ }
18
+
19
+ function normalizeOptionalString(value) {
20
+ if (typeof value !== "string") {
21
+ return null;
22
+ }
23
+ const trimmed = value.trim();
24
+ return trimmed.length > 0 ? trimmed : null;
25
+ }
26
+
27
+ function extractNumericEmbeddingShape(entry) {
28
+ const embedding = entry?.embedding;
29
+ const candidates = [
30
+ embedding,
31
+ embedding?.values,
32
+ embedding?.vector,
33
+ embedding?.embedding,
34
+ entry?.values,
35
+ entry?.vector,
36
+ entry?.numericEmbedding,
37
+ entry?.numericEmbeddingValues,
38
+ ];
39
+ return candidates.find((candidate) => hasNumericValues(candidate)) ?? null;
40
+ }
41
+
42
+ function extractEmbeddingModel(entry) {
43
+ const candidates = [
44
+ entry?.embedding?.model,
45
+ entry?.embeddingModel,
46
+ entry?.model,
47
+ ];
48
+ for (const candidate of candidates) {
49
+ const normalized = normalizeOptionalString(candidate);
50
+ if (normalized !== null) {
51
+ return normalized;
52
+ }
53
+ }
54
+ return null;
55
+ }
56
+
57
+ export function summarizePackVectorEmbeddingState(vectors) {
58
+ if (!vectors || !Array.isArray(vectors.entries)) {
59
+ return {
60
+ vectorEntryCount: null,
61
+ numericEmbeddingEntryCount: null,
62
+ embeddingModels: []
63
+ };
64
+ }
65
+ const embeddingModels = [...new Set(vectors.entries
66
+ .flatMap((entry) => {
67
+ if (extractNumericEmbeddingShape(entry) === null) {
68
+ return [];
69
+ }
70
+ const model = extractEmbeddingModel(entry);
71
+ return model === null ? [] : [model];
72
+ }))].sort((left, right) => left.localeCompare(right));
73
+ return {
74
+ vectorEntryCount: vectors.entries.length,
75
+ numericEmbeddingEntryCount: vectors.entries.filter((entry) => extractNumericEmbeddingShape(entry) !== null).length,
76
+ embeddingModels
77
+ };
78
+ }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclawbrain/cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
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",