@openclawbrain/openclaw 0.1.2 → 0.1.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/dist/src/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  import { createHash } from "node:crypto";
2
- import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { compileRuntimeFromActivation } from "@openclawbrain/compiler";
6
6
  import { CONTRACT_IDS, buildNormalizedEventExport, canonicalJson, checksumJsonPayload, createFeedbackEvent, createInteractionEvent, sortNormalizedEvents, validateKernelSurface, validateNormalizedEventExport } from "@openclawbrain/contracts";
7
7
  import { describeNormalizedEventExportObservability } from "@openclawbrain/event-export";
8
8
  import { DEFAULT_TEACHER_SUPERVISION_STALE_AFTER_MS, advanceAlwaysOnLearningRuntime, buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, materializeAlwaysOnLearningCandidatePack, materializeCandidatePackFromNormalizedEventExport } from "@openclawbrain/learner";
9
- import { activatePack, describeActivationObservability, describeActivationTarget, describePackCompileTarget, inspectActivationState, promoteCandidatePack, rollbackActivePack, stageCandidatePack } from "@openclawbrain/pack-format";
9
+ import { LEARNING_SPINE_LOG_LAYOUT, activatePack, describeActivationObservability, describeActivationTarget, describePackCompileTarget, inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, rollbackActivePack, stageCandidatePack } from "@openclawbrain/pack-format";
10
+ import { appendLearningUpdateLogs, appendServeTimeRouteDecisionLog } from "./learning-spine.js";
10
11
  const DEFAULT_AGENT_ID = "openclaw-runtime";
11
12
  const FEEDBACK_KINDS = new Set(["correction", "teaching", "approval", "suppression"]);
12
13
  export const DEFAULT_ASYNC_TEACHER_QUEUE_CAPACITY = 8;
@@ -20,6 +21,35 @@ export const RUNTIME_EVENT_EXPORT_BUNDLE_LAYOUT = {
20
21
  manifest: "manifest.json",
21
22
  payload: "normalized-event-export.json"
22
23
  };
24
+ function normalizeBrainAttachmentPolicy(value) {
25
+ if (value === "dedicated" || value === "shared") {
26
+ return value;
27
+ }
28
+ return "undeclared";
29
+ }
30
+ function buildCurrentProfileAttachmentPolicy(policyMode) {
31
+ if (policyMode === "dedicated") {
32
+ return {
33
+ mode: "dedicated",
34
+ readScope: "current_profile_only",
35
+ writeScope: "current_profile_only",
36
+ currentProfileExclusive: true,
37
+ requiresProfileAttribution: true,
38
+ detail: "dedicated brains are exclusive to the current profile and must keep profile attribution explicit on every served turn"
39
+ };
40
+ }
41
+ if (policyMode === "shared") {
42
+ return {
43
+ mode: "shared",
44
+ readScope: "attached_profiles",
45
+ writeScope: "attached_profiles",
46
+ currentProfileExclusive: false,
47
+ requiresProfileAttribution: true,
48
+ detail: "shared brains may serve multiple attached profiles, so status and per-turn attribution must stay profile-explicit"
49
+ };
50
+ }
51
+ return null;
52
+ }
23
53
  const OPENCLAW_LANDING_BOUNDARIES_V1 = {
24
54
  compileBoundary: {
25
55
  contract: CONTRACT_IDS.runtimeCompile,
@@ -994,6 +1024,9 @@ function buildAttachSuccessSignals(input) {
994
1024
  }
995
1025
  if (activeTarget !== null) {
996
1026
  signals.push(`active_workspace_snapshot:${activeTarget.workspaceSnapshot}`);
1027
+ if (activeTarget.eventRange.count === 0) {
1028
+ signals.push("awaiting_first_export");
1029
+ }
997
1030
  }
998
1031
  if (input.compile?.ok) {
999
1032
  signals.push(`compile_ok:${input.compile.activePackId ?? "unknown"}`);
@@ -1079,7 +1112,10 @@ export function rollbackRuntimeAttach(input) {
1079
1112
  }
1080
1113
  const after = dryRun
1081
1114
  ? before.rollback.nextPointers
1082
- : rollbackActivePack(activationRoot, updatedAt).pointers;
1115
+ : rollbackActivePack(activationRoot, {
1116
+ updatedAt,
1117
+ reason: "runtime_attach_rollback"
1118
+ }).pointers;
1083
1119
  return {
1084
1120
  runtimeOwner: "openclaw",
1085
1121
  activationRoot,
@@ -1108,17 +1144,54 @@ function resolveBootstrapNormalizedEventExport(input) {
1108
1144
  throw new Error("Provide normalizedEventExport or interactionEvents/feedbackEvents, not both");
1109
1145
  }
1110
1146
  const normalizedEventExport = input.normalizedEventExport !== undefined
1111
- ? structuredClone(input.normalizedEventExport)
1147
+ ? canonicalizeBootstrapNormalizedEventExport(input.normalizedEventExport)
1112
1148
  : buildNormalizedEventExport({
1113
1149
  interactionEvents,
1114
1150
  feedbackEvents
1115
1151
  });
1116
1152
  const validationErrors = validateNormalizedEventExport(normalizedEventExport);
1117
1153
  if (validationErrors.length > 0) {
1118
- throw new Error(`normalized event export is invalid: ${validationErrors.join("; ")}`);
1154
+ throw new Error(formatBootstrapNormalizedEventExportValidationError(normalizedEventExport, validationErrors));
1119
1155
  }
1120
1156
  return normalizedEventExport;
1121
1157
  }
1158
+ function canonicalizeBootstrapNormalizedEventExport(normalizedEventExport) {
1159
+ const interactionEvents = cloneBootstrapNormalizedEventArray(normalizedEventExport.interactionEvents, "interactionEvents");
1160
+ const feedbackEvents = cloneBootstrapNormalizedEventArray(normalizedEventExport.feedbackEvents, "feedbackEvents");
1161
+ try {
1162
+ return buildNormalizedEventExport({
1163
+ interactionEvents,
1164
+ feedbackEvents
1165
+ });
1166
+ }
1167
+ catch (error) {
1168
+ const detail = error instanceof Error ? error.message : String(error);
1169
+ throw new Error("bootstrapRuntimeAttach could not reconstruct a safe normalized event export from the provided event arrays. " +
1170
+ "Repair the event payload or, for a first attach with no live events yet, pass empty arrays so bootstrap can self-boot. " +
1171
+ `Details: ${detail}`);
1172
+ }
1173
+ }
1174
+ function cloneBootstrapNormalizedEventArray(value, fieldName) {
1175
+ if (!Array.isArray(value)) {
1176
+ throw new Error(`bootstrapRuntimeAttach expected normalizedEventExport.${fieldName} to be an array. ` +
1177
+ "For a first attach with no live events yet, pass empty arrays or omit normalizedEventExport and use interactionEvents: [] / feedbackEvents: [].");
1178
+ }
1179
+ return structuredClone(value);
1180
+ }
1181
+ function formatBootstrapNormalizedEventExportValidationError(normalizedEventExport, validationErrors) {
1182
+ const details = validationErrors.join("; ");
1183
+ const zeroEventBootstrap = normalizedEventExport.interactionEvents.length === 0 &&
1184
+ normalizedEventExport.feedbackEvents.length === 0 &&
1185
+ normalizedEventExport.range.count === 0;
1186
+ if (zeroEventBootstrap) {
1187
+ return ("bootstrapRuntimeAttach could not derive a safe zero-event bootstrap export: " +
1188
+ `${details}. ` +
1189
+ "For a first attach with no live events yet, pass empty interaction/feedback arrays or an empty normalized export payload and bootstrap will self-boot.");
1190
+ }
1191
+ return ("bootstrapRuntimeAttach could not use the provided normalized event export: " +
1192
+ `${details}. ` +
1193
+ "Repair the event payload or pass raw interactionEvents/feedbackEvents so bootstrap can derive range and provenance itself.");
1194
+ }
1122
1195
  export function bootstrapRuntimeAttach(input) {
1123
1196
  const activationRoot = path.resolve(normalizeNonEmptyString(input.activationRoot, "activationRoot"));
1124
1197
  const packRoot = path.resolve(normalizeNonEmptyString(input.packRoot, "packRoot"));
@@ -1141,7 +1214,10 @@ export function bootstrapRuntimeAttach(input) {
1141
1214
  ...(input.structuralOps !== undefined ? { structuralOps: input.structuralOps } : {}),
1142
1215
  ...(input.sparseFeedback !== undefined ? { sparseFeedback: input.sparseFeedback } : {})
1143
1216
  });
1144
- activatePack(activationRoot, packRoot, activatedAt);
1217
+ activatePack(activationRoot, packRoot, {
1218
+ updatedAt: activatedAt,
1219
+ reason: "attach_bootstrap"
1220
+ });
1145
1221
  return {
1146
1222
  runtimeOwner: "openclaw",
1147
1223
  activationRoot,
@@ -1154,6 +1230,48 @@ export function bootstrapRuntimeAttach(input) {
1154
1230
  })
1155
1231
  };
1156
1232
  }
1233
+ function buildRuntimeTurnAttribution(input) {
1234
+ const brainAttachmentPolicy = normalizeBrainAttachmentPolicy(input.turn.brainAttachmentPolicy);
1235
+ if (input.compileResult.ok) {
1236
+ const notes = input.compileResult.compileResponse.diagnostics.notes;
1237
+ const contextAttribution = buildContextAttributionSummary({
1238
+ fallbackToStaticContext: false,
1239
+ hardRequirementViolated: false,
1240
+ usedLearnedRouteFn: input.compileResult.compileResponse.diagnostics.usedLearnedRouteFn,
1241
+ selectedContext: input.compileResult.compileResponse.selectedContext,
1242
+ notes
1243
+ });
1244
+ return {
1245
+ hostRuntimeOwner: "openclaw",
1246
+ profileSelector: "current_profile",
1247
+ brainAttachmentPolicy,
1248
+ brainStatus: "serving_active_pack",
1249
+ activePackId: input.compileResult.activePackId,
1250
+ usedLearnedRouteFn: input.compileResult.compileResponse.diagnostics.usedLearnedRouteFn,
1251
+ routerIdentity: input.compileResult.compileResponse.diagnostics.routerIdentity,
1252
+ selectionDigest: input.compileResult.compileResponse.diagnostics.selectionDigest,
1253
+ selectionTiers: readDiagnosticNoteValue(notes, "selection_tiers="),
1254
+ contextEvidence: contextAttribution.evidence === "unprobed" ? null : contextAttribution.evidence
1255
+ };
1256
+ }
1257
+ const contextAttribution = buildContextAttributionSummary({
1258
+ fallbackToStaticContext: input.compileResult.fallbackToStaticContext,
1259
+ hardRequirementViolated: input.compileResult.hardRequirementViolated,
1260
+ usedLearnedRouteFn: null
1261
+ });
1262
+ return {
1263
+ hostRuntimeOwner: "openclaw",
1264
+ profileSelector: "current_profile",
1265
+ brainAttachmentPolicy,
1266
+ brainStatus: input.compileResult.hardRequirementViolated ? "hard_fail" : "fail_open_static_context",
1267
+ activePackId: null,
1268
+ usedLearnedRouteFn: null,
1269
+ routerIdentity: null,
1270
+ selectionDigest: null,
1271
+ selectionTiers: null,
1272
+ contextEvidence: contextAttribution.evidence === "unprobed" ? null : contextAttribution.evidence
1273
+ };
1274
+ }
1157
1275
  function buildCompileInteractionEvent(input) {
1158
1276
  if (!input.compileResult.ok) {
1159
1277
  return null;
@@ -1168,6 +1286,10 @@ function buildCompileInteractionEvent(input) {
1168
1286
  sessionId: input.turn.sessionId,
1169
1287
  source: input.sourceStream
1170
1288
  });
1289
+ const attribution = buildRuntimeTurnAttribution({
1290
+ turn: input.turn,
1291
+ compileResult: input.compileResult
1292
+ });
1171
1293
  return createInteractionEvent({
1172
1294
  eventId,
1173
1295
  agentId: input.agentId,
@@ -1180,7 +1302,8 @@ function buildCompileInteractionEvent(input) {
1180
1302
  runtimeOwner: "openclaw",
1181
1303
  stream: input.sourceStream
1182
1304
  },
1183
- packId: input.compileResult.compileResponse.packId
1305
+ packId: input.compileResult.compileResponse.packId,
1306
+ attribution
1184
1307
  });
1185
1308
  }
1186
1309
  function buildDeliveryInteractionEvent(input) {
@@ -1201,6 +1324,10 @@ function buildDeliveryInteractionEvent(input) {
1201
1324
  sessionId: input.turn.sessionId,
1202
1325
  source: input.sourceStream
1203
1326
  });
1327
+ const attribution = buildRuntimeTurnAttribution({
1328
+ turn: input.turn,
1329
+ compileResult: input.compileResult
1330
+ });
1204
1331
  return createInteractionEvent({
1205
1332
  eventId,
1206
1333
  agentId: input.agentId,
@@ -1213,12 +1340,17 @@ function buildDeliveryInteractionEvent(input) {
1213
1340
  runtimeOwner: "openclaw",
1214
1341
  stream: input.sourceStream
1215
1342
  },
1343
+ attribution,
1216
1344
  ...(input.compileResult.ok ? { packId: input.compileResult.compileResponse.packId } : {}),
1217
1345
  ...(messageId !== undefined ? { messageId } : {})
1218
1346
  });
1219
1347
  }
1220
1348
  function buildFeedbackEvents(input) {
1221
1349
  const feedbackItems = input.turn.feedback ?? [];
1350
+ const attribution = buildRuntimeTurnAttribution({
1351
+ turn: input.turn,
1352
+ compileResult: input.compileResult
1353
+ });
1222
1354
  return feedbackItems.map((item, index) => {
1223
1355
  if (item === null) {
1224
1356
  throw new Error(`feedback[${index}] must be an object`);
@@ -1255,6 +1387,7 @@ function buildFeedbackEvents(input) {
1255
1387
  stream: input.sourceStream
1256
1388
  },
1257
1389
  content,
1390
+ attribution,
1258
1391
  ...(messageId !== undefined ? { messageId } : {}),
1259
1392
  ...(relatedInteractionId !== undefined ? { relatedInteractionId } : {})
1260
1393
  });
@@ -1287,6 +1420,7 @@ export function buildNormalizedRuntimeEventExport(turn, compileResult) {
1287
1420
  nextSequence,
1288
1421
  defaultCreatedAt: compileCreatedAt,
1289
1422
  compileInteraction,
1423
+ compileResult,
1290
1424
  agentId
1291
1425
  });
1292
1426
  const deliveryInteraction = buildDeliveryInteractionEvent({
@@ -1357,12 +1491,36 @@ export function runRuntimeTurn(turn, options = {}) {
1357
1491
  ...(turn.runtimeHints !== undefined ? { runtimeHints: turn.runtimeHints } : {})
1358
1492
  };
1359
1493
  const compileResult = compileRuntimeContext(compileInput);
1494
+ const warnings = [];
1495
+ const serveLoggedAt = turn.compile?.createdAt ?? turn.createdAt ?? new Date().toISOString();
1360
1496
  if (!compileResult.ok && compileResult.hardRequirementViolated) {
1497
+ try {
1498
+ appendServeTimeRouteDecisionLog({
1499
+ activationRoot: compileInput.activationRoot,
1500
+ turn,
1501
+ compileResult,
1502
+ recordedAt: serveLoggedAt
1503
+ });
1504
+ }
1505
+ catch {
1506
+ }
1361
1507
  throw new Error(compileResult.error);
1362
1508
  }
1363
- const warnings = [];
1364
1509
  try {
1365
1510
  const normalizedEventExport = buildNormalizedRuntimeEventExport(turn, compileResult);
1511
+ try {
1512
+ const compileEvent = normalizedEventExport.interactionEvents.find((event) => event.kind === "memory_compiled");
1513
+ appendServeTimeRouteDecisionLog({
1514
+ activationRoot: compileInput.activationRoot,
1515
+ turn,
1516
+ compileResult,
1517
+ normalizedEventExport,
1518
+ recordedAt: compileEvent?.createdAt ?? serveLoggedAt
1519
+ });
1520
+ }
1521
+ catch (error) {
1522
+ warnings.push(`learning spine serve log failed: ${toErrorMessage(error)}`);
1523
+ }
1366
1524
  const eventExport = writeRuntimeEventExportBundle(turn, normalizedEventExport);
1367
1525
  return {
1368
1526
  ...compileResult,
@@ -1442,6 +1600,13 @@ export function runContinuousProductLoopTurn(input) {
1442
1600
  currentState.interactionEvents = mergedHistory.interactionEvents;
1443
1601
  currentState.feedbackEvents = mergedHistory.feedbackEvents;
1444
1602
  try {
1603
+ let activeBeforePack = null;
1604
+ try {
1605
+ activeBeforePack = loadPackFromActivation(activationRoot, "active", { requireActivationReady: true });
1606
+ }
1607
+ catch {
1608
+ activeBeforePack = null;
1609
+ }
1445
1610
  const learnerResult = advanceAlwaysOnLearningRuntime({
1446
1611
  packLabel: input.packLabel,
1447
1612
  workspace: input.workspace,
@@ -1474,13 +1639,30 @@ export function runContinuousProductLoopTurn(input) {
1474
1639
  learning.candidatePack = cloneContinuousProductLoopPackVersion(candidatePack);
1475
1640
  currentState.candidatePack = cloneContinuousProductLoopPackVersion(candidatePack);
1476
1641
  const stagedAt = normalizeIsoTimestamp(input.stageUpdatedAt, "stageUpdatedAt", descriptor.manifest.provenance.builtAt);
1477
- stageCandidatePack(activationRoot, candidateRootDir, stagedAt);
1642
+ try {
1643
+ appendLearningUpdateLogs({
1644
+ activationRoot,
1645
+ materialization: learnerResult.materialization,
1646
+ activeBeforePack,
1647
+ candidateDescriptor: descriptor
1648
+ });
1649
+ }
1650
+ catch (error) {
1651
+ learningWarnings.push(`learning spine update logs failed: ${toErrorMessage(error)}`);
1652
+ }
1653
+ stageCandidatePack(activationRoot, candidateRootDir, {
1654
+ updatedAt: stagedAt,
1655
+ reason: `stage_candidate:${learnerResult.materialization.reason}:${learnerResult.materialization.lane}`
1656
+ });
1478
1657
  const stagedInspection = inspectActivationState(activationRoot, stagedAt);
1479
1658
  learning.promotionAllowed = stagedInspection.promotion.allowed;
1480
1659
  learning.promotionFindings = [...stagedInspection.promotion.findings];
1481
1660
  if ((input.autoPromote ?? true) && stagedInspection.promotion.allowed) {
1482
1661
  const promotedAt = normalizeIsoTimestamp(input.promoteUpdatedAt, "promoteUpdatedAt", stagedAt);
1483
- promoteCandidatePack(activationRoot, promotedAt);
1662
+ promoteCandidatePack(activationRoot, {
1663
+ updatedAt: promotedAt,
1664
+ reason: `auto_promote:${learnerResult.materialization.reason}:${learnerResult.materialization.lane}`
1665
+ });
1484
1666
  currentState.promotionCount += 1;
1485
1667
  currentState.candidatePack = null;
1486
1668
  learning.promoted = true;
@@ -1896,7 +2078,10 @@ function prepareSeedActivation(rootDir, fixture) {
1896
2078
  connect: 1
1897
2079
  }
1898
2080
  });
1899
- activatePack(activationRoot, seedPackRoot, fixture.seedActivatedAt);
2081
+ activatePack(activationRoot, seedPackRoot, {
2082
+ updatedAt: fixture.seedActivatedAt,
2083
+ reason: "recorded_session_seed_activate"
2084
+ });
1900
2085
  return {
1901
2086
  activationRoot,
1902
2087
  seedPackId: seedPack.manifest.packId
@@ -2056,6 +2241,146 @@ export function verifyRecordedSessionReplayBundleHashes(bundle) {
2056
2241
  scoreHashMatches: rescored.scoreHash === bundle.scoreHash
2057
2242
  };
2058
2243
  }
2244
+ export const OPERATOR_API_CONTRACT_ID = "openclaw_operator_api.v1";
2245
+ export const SUPPORTED_OPERATOR_API_FAMILIES = [
2246
+ "bootstrap_attach",
2247
+ "status",
2248
+ "export",
2249
+ "refresh",
2250
+ "promote",
2251
+ "rollback",
2252
+ "proof_observability"
2253
+ ];
2254
+ export const OPERATOR_API_CONTRACT_V1 = {
2255
+ contract: OPERATOR_API_CONTRACT_ID,
2256
+ runtimeOwner: "openclaw",
2257
+ scope: "narrow_supported_operator_surface",
2258
+ families: SUPPORTED_OPERATOR_API_FAMILIES,
2259
+ routes: [
2260
+ {
2261
+ family: "bootstrap_attach",
2262
+ scope: "programmatic",
2263
+ packageName: "@openclawbrain/openclaw",
2264
+ entrypoints: ["bootstrapRuntimeAttach", "describeAttachStatus"],
2265
+ summary: "Bootstrap the first attach pack and prove the initial handoff state without pretending live learning has already run.",
2266
+ notes: [
2267
+ "Zero-event bootstrap is supported and stays explicit through awaiting_first_export.",
2268
+ "Attach serves only from activation's active slot after bootstrap completes."
2269
+ ]
2270
+ },
2271
+ {
2272
+ family: "status",
2273
+ scope: "cli",
2274
+ packageName: "@openclawbrain/openclaw",
2275
+ entrypoints: ["openclawbrain-ops status", "describeCurrentProfileBrainStatus"],
2276
+ summary: "Read the canonical current-profile brain-status object for the active Host/Profile/Brain/Attachment boundary.",
2277
+ notes: [
2278
+ "Status is the first operator read path.",
2279
+ "describeCurrentProfileBrainStatus() freezes the supported Host/Profile/Brain/Attachment answer shape for the current profile.",
2280
+ "Use activation and export observability proof helpers when you need candidate/previous or export-freshness detail."
2281
+ ]
2282
+ },
2283
+ {
2284
+ family: "export",
2285
+ scope: "programmatic",
2286
+ packageName: "@openclawbrain/openclaw",
2287
+ entrypoints: ["buildNormalizedRuntimeEventExport", "writeRuntimeEventExportBundle", "loadRuntimeEventExportBundle"],
2288
+ summary: "Emit the deterministic learner handoff artifact explicitly instead of folding export into a larger implicit runtime loop.",
2289
+ notes: [
2290
+ "Export is an off-hot-path operator handoff artifact, not proof of immediate active-pack mutation.",
2291
+ "Bundle roots and normalized payloads are both accepted downstream by observability surfaces."
2292
+ ]
2293
+ },
2294
+ {
2295
+ family: "refresh",
2296
+ scope: "programmatic",
2297
+ packageName: "@openclawbrain/learner",
2298
+ entrypoints: [
2299
+ "createAlwaysOnLearningRuntimeState",
2300
+ "advanceAlwaysOnLearningRuntime",
2301
+ "materializeAlwaysOnLearningCandidatePack"
2302
+ ],
2303
+ summary: "Refresh candidate learning state explicitly through the learner boundary before any activation-pointer move happens.",
2304
+ notes: [
2305
+ "Refresh is PG-only candidate-pack materialization in this repo.",
2306
+ "Refresh does not mutate the currently served active pack in place."
2307
+ ]
2308
+ },
2309
+ {
2310
+ family: "promote",
2311
+ scope: "programmatic",
2312
+ packageName: "@openclawbrain/pack-format",
2313
+ entrypoints: ["stageCandidatePack", "promoteCandidatePack"],
2314
+ summary: "Stage and promote activation-ready candidate packs through explicit pointer changes.",
2315
+ notes: [
2316
+ "Promotion is the only path that changes which pack is served.",
2317
+ "Candidate and previous remain inspectable around the pointer move."
2318
+ ]
2319
+ },
2320
+ {
2321
+ family: "rollback",
2322
+ scope: "cli",
2323
+ packageName: "@openclawbrain/openclaw",
2324
+ entrypoints: ["openclawbrain-ops rollback", "rollbackRuntimeAttach", "formatOperatorRollbackReport"],
2325
+ summary: "Preview and apply the explicit active<-previous / active->candidate rollback move.",
2326
+ notes: [
2327
+ "Rollback is blocked when the previous pointer is unavailable.",
2328
+ "Dry-run is the required first read path for safe operator rollback."
2329
+ ]
2330
+ },
2331
+ {
2332
+ family: "proof_observability",
2333
+ scope: "programmatic",
2334
+ packageName: "@openclawbrain/openclaw",
2335
+ entrypoints: ["describeAttachStatus", "describeKernelBrainBoundary"],
2336
+ summary: "Prove the local attach and kernel-vs-brain boundary from the shipped bridge surface.",
2337
+ notes: [
2338
+ "Use these for repo-local or installed-package operator proof reads.",
2339
+ "These surfaces report the promoted artifact boundary, not full live runtime plasticity."
2340
+ ]
2341
+ },
2342
+ {
2343
+ family: "proof_observability",
2344
+ scope: "programmatic",
2345
+ packageName: "@openclawbrain/pack-format",
2346
+ entrypoints: ["describeActivationObservability"],
2347
+ summary: "Inspect activation health, freshness, route artifacts, rollback lineage, and slot readiness.",
2348
+ notes: ["Activation observability is the ground truth for active/candidate/previous slot inspection."]
2349
+ },
2350
+ {
2351
+ family: "proof_observability",
2352
+ scope: "programmatic",
2353
+ packageName: "@openclawbrain/event-export",
2354
+ entrypoints: ["describeNormalizedEventExportObservability"],
2355
+ summary: "Inspect supervision freshness and teacher freshness from the exported learner handoff artifact.",
2356
+ notes: ["Export observability is local-to-export proof only."]
2357
+ },
2358
+ {
2359
+ family: "proof_observability",
2360
+ scope: "proof_lane",
2361
+ packageName: "workspace",
2362
+ entrypoints: ["pnpm current-profile-lifecycle:smoke", "pnpm observability:smoke"],
2363
+ summary: "Run the repo-local proof lanes that derive operator truth from the canonical current-profile status object plus activation observability.",
2364
+ notes: ["These lanes are proof machinery, not a second semver-stable API."]
2365
+ }
2366
+ ],
2367
+ quarantinedSurface: [
2368
+ "openclawbrain-ops doctor was deleted; use the canonical current-profile status object plus proof helpers instead of a parallel troubleshooting surface.",
2369
+ "buildOperatorSurfaceReport / formatOperatorStatusReport / formatOperatorDoctorReport were historical parallel status surfaces and are not the supported operator API.",
2370
+ "runContinuousProductLoopTurn collapses export/refresh/promote into one proof helper and is not the supported operator API.",
2371
+ "runRecordedSessionReplay and recorded-session fixtures are proof helpers, not operator API.",
2372
+ "release scripts, root smoke plumbing, and workspace layout are proof-and-build machinery, not operator API.",
2373
+ "runRuntimeTurn is a runtime convenience wrapper and not the narrow operator export contract.",
2374
+ "createAsyncTeacherLiveLoop is supporting internals for refresh/teacher snapshots, not the narrow operator contract."
2375
+ ]
2376
+ };
2377
+ export const OPENCLAW_OPERATOR_NOUNS_V1 = ["Host", "Profile", "Brain", "Attachment"];
2378
+ export const CURRENT_PROFILE_BRAIN_STATUS_CONTRACT = CONTRACT_IDS.currentProfileBrainStatus;
2379
+ export const BRAIN_ATTACHMENT_POLICY_SEMANTICS_V1 = {
2380
+ undeclared: "The Host has not declared whether the current Profile's Brain attachment policy is shared or dedicated; do not infer profile exclusivity from activation state alone.",
2381
+ dedicated: "The Host declares a dedicated Brain attachment policy: one Profile is intentionally attached to one Brain activation root, and operators may treat the served Brain state as profile-specific until the attachment changes.",
2382
+ shared: "The Host declares a shared Brain attachment policy: multiple Profiles may intentionally attach to the same Brain activation root, attribution must stay current-profile explicit, and operators must not treat later served context as profile-exclusive."
2383
+ };
2059
2384
  function summarizeOperatorSlot(slot, updatedAt) {
2060
2385
  if (slot === null) {
2061
2386
  return null;
@@ -2108,6 +2433,9 @@ function summarizeCandidateAheadBy(candidateAheadBy) {
2108
2433
  .map(([field]) => field)
2109
2434
  .sort();
2110
2435
  }
2436
+ function isAwaitingFirstExportSlot(slot) {
2437
+ return slot !== null && slot.eventRange.count === 0;
2438
+ }
2111
2439
  function summarizeBrainState(active, observability) {
2112
2440
  if (active === null) {
2113
2441
  return {
@@ -2202,7 +2530,8 @@ function loadOperatorEventExport(input) {
2202
2530
  return {
2203
2531
  sourcePath: resolvedPath,
2204
2532
  sourceKind: "bundle_root",
2205
- normalizedEventExport: bundle.normalizedEventExport
2533
+ normalizedEventExport: bundle.normalizedEventExport,
2534
+ exportedAt: bundle.manifest.exportedAt
2206
2535
  };
2207
2536
  }
2208
2537
  const normalizedEventExport = readJsonFile(resolvedPath);
@@ -2213,7 +2542,8 @@ function loadOperatorEventExport(input) {
2213
2542
  return {
2214
2543
  sourcePath: resolvedPath,
2215
2544
  sourceKind: "payload",
2216
- normalizedEventExport
2545
+ normalizedEventExport,
2546
+ exportedAt: null
2217
2547
  };
2218
2548
  }
2219
2549
  function summarizeSupervision(input) {
@@ -2224,6 +2554,7 @@ function summarizeSupervision(input) {
2224
2554
  sourcePath: null,
2225
2555
  sourceKind: "missing",
2226
2556
  exportDigest: null,
2557
+ exportedAt: null,
2227
2558
  flowing: null,
2228
2559
  sourceCount: 0,
2229
2560
  freshestSourceStream: null,
@@ -2242,6 +2573,7 @@ function summarizeSupervision(input) {
2242
2573
  sourcePath: loaded.sourcePath,
2243
2574
  sourceKind: loaded.sourceKind,
2244
2575
  exportDigest: observability.exportDigest,
2576
+ exportedAt: loaded.exportedAt,
2245
2577
  flowing,
2246
2578
  sourceCount: observability.supervisionFreshnessBySource.length,
2247
2579
  freshestSourceStream: observability.teacherFreshness.sourceStream ?? freshestSource?.sourceStream ?? null,
@@ -2390,13 +2722,16 @@ function buildOperatorFindings(report) {
2390
2722
  findings.push({ severity, code, summary, detail });
2391
2723
  };
2392
2724
  if (report.active === null) {
2393
- push("fail", "active_missing", "active slot is empty", "activate a healthy pack before serving or troubleshooting promotions");
2725
+ push("fail", "active_missing", "active slot is empty", "no active pack found; this is the pre-bootstrap state — call `bootstrapRuntimeAttach()` to activate an initial pack before compiling or serving");
2394
2726
  }
2395
2727
  else if (!report.active.activationReady) {
2396
2728
  push("fail", "active_unhealthy", `active slot is not activation-ready: ${report.active.packId}`, report.active.findings.join("; ") || "inspect the active pack payloads and activation pointers");
2397
2729
  }
2398
2730
  else {
2399
2731
  push("pass", "active_ready", `active slot is ready: ${report.active.packId}`, "serving can inspect the active pack without activation drift");
2732
+ if (isAwaitingFirstExportSlot(report.active)) {
2733
+ push("warn", "bootstrap_waiting_for_first_export", "active pack bootstrapped without live exports yet", "serving is up from seed-state defaults; this is expected on first install — learning activates after the first turn is captured via `runRuntimeTurn()`");
2734
+ }
2400
2735
  }
2401
2736
  if (report.learnedRouting.required) {
2402
2737
  if (report.learnedRouting.available) {
@@ -2506,7 +2841,139 @@ function formatCompactValue(value, empty = "none", maxLength = 24) {
2506
2841
  }
2507
2842
  return value.length <= maxLength ? value : `${value.slice(0, maxLength)}…`;
2508
2843
  }
2509
- export function buildOperatorSurfaceReport(input) {
2844
+ function summarizeCurrentProfileLogRoot(activationRoot) {
2845
+ const logRoot = path.join(path.resolve(activationRoot), LEARNING_SPINE_LOG_LAYOUT.dir);
2846
+ return existsSync(logRoot) ? logRoot : null;
2847
+ }
2848
+ function summarizeCurrentProfileLastLearningUpdateAt(activationRoot, learning) {
2849
+ const updates = readLearningSpineLogEntries(activationRoot, "pgRouteUpdates");
2850
+ return updates.at(-1)?.recordedAt ?? learning.lastMaterializedAt ?? null;
2851
+ }
2852
+ function summarizeCurrentProfileBrainSummary(input) {
2853
+ if (!input.attached) {
2854
+ return "Brain is not attached to the current Profile.";
2855
+ }
2856
+ if (input.serveState === "fail_open_static_context") {
2857
+ return "Brain is attached but would fail open to static context.";
2858
+ }
2859
+ if (input.serveState === "hard_fail") {
2860
+ return "Brain is attached but currently hard-fails learned routing.";
2861
+ }
2862
+ if (input.awaitingFirstExport) {
2863
+ return "Brain is serving the seed-state pack and awaiting the first export.";
2864
+ }
2865
+ if (input.brainState === "pg_promoted_pack_authoritative") {
2866
+ return "Brain is serving the promoted pack.";
2867
+ }
2868
+ if (input.serveState === "serving_active_pack") {
2869
+ return "Brain is serving the active pack with learned routing.";
2870
+ }
2871
+ return "Brain is attached but has not been compile-probed yet.";
2872
+ }
2873
+ function summarizeCurrentProfileBrainStatusLevel(input) {
2874
+ if (!input.attached) {
2875
+ return "fail";
2876
+ }
2877
+ if (input.serveState === "fail_open_static_context" || input.serveState === "hard_fail") {
2878
+ return "fail";
2879
+ }
2880
+ if (input.awaitingFirstExport || input.serveState === "unprobed") {
2881
+ return "warn";
2882
+ }
2883
+ return input.routeFreshness === "updated" ? "ok" : "warn";
2884
+ }
2885
+ function buildCurrentProfileBrainStatusFromReport(report, policyMode) {
2886
+ const attached = report.active !== null;
2887
+ const awaitingFirstExport = isAwaitingFirstExportSlot(report.active);
2888
+ const routerIdentity = report.servePath.routerIdentity ?? report.learnedRouting.routerIdentity ?? report.active?.routerIdentity ?? null;
2889
+ const routeFreshness = report.servePath.refreshStatus === "updated" || report.servePath.refreshStatus === "no_supervision"
2890
+ ? report.servePath.refreshStatus
2891
+ : "unknown";
2892
+ const activePackId = report.brain.activePackId ?? report.servePath.activePackId ?? report.active?.packId ?? null;
2893
+ const status = summarizeCurrentProfileBrainStatusLevel({
2894
+ attached,
2895
+ serveState: report.servePath.state,
2896
+ routeFreshness,
2897
+ awaitingFirstExport
2898
+ });
2899
+ return {
2900
+ contract: CURRENT_PROFILE_BRAIN_STATUS_CONTRACT,
2901
+ generatedAt: report.generatedAt,
2902
+ host: {
2903
+ noun: "Host",
2904
+ runtimeOwner: "openclaw",
2905
+ activationRoot: report.activationRoot
2906
+ },
2907
+ profile: {
2908
+ noun: "Profile",
2909
+ selector: "current_profile",
2910
+ detail: attached
2911
+ ? "The Host resolves the current Profile through the active Attachment boundary only."
2912
+ : "The current Profile has no active Brain attachment visible at the Host boundary."
2913
+ },
2914
+ brain: {
2915
+ noun: "Brain",
2916
+ activationRoot: attached ? report.activationRoot : null,
2917
+ logRoot: summarizeCurrentProfileLogRoot(report.activationRoot),
2918
+ activePackId,
2919
+ initMode: report.learnedRouting.initMode,
2920
+ state: report.brain.state,
2921
+ routeFreshness,
2922
+ routerIdentity,
2923
+ routerChecksum: report.learnedRouting.routerChecksum,
2924
+ lastExportAt: report.supervision.exportedAt,
2925
+ lastLearningUpdateAt: summarizeCurrentProfileLastLearningUpdateAt(report.activationRoot, report.learning),
2926
+ lastPromotionAt: report.promotion.lastPromotion.at,
2927
+ summary: summarizeCurrentProfileBrainSummary({
2928
+ attached,
2929
+ serveState: report.servePath.state,
2930
+ brainState: report.brain.state,
2931
+ awaitingFirstExport
2932
+ }),
2933
+ detail: report.brain.detail
2934
+ },
2935
+ attachment: attached
2936
+ ? {
2937
+ noun: "Attachment",
2938
+ state: "attached",
2939
+ activationRoot: report.activationRoot,
2940
+ servingSlot: "active",
2941
+ policyMode,
2942
+ policy: buildCurrentProfileAttachmentPolicy(policyMode),
2943
+ detail: policyMode === "shared"
2944
+ ? "current profile is attached to a shared OpenClawBrain activation boundary"
2945
+ : policyMode === "dedicated"
2946
+ ? "current profile is attached to a dedicated OpenClawBrain activation boundary"
2947
+ : "current profile is attached to an OpenClawBrain activation boundary, but shared-vs-dedicated policy has not been declared"
2948
+ }
2949
+ : {
2950
+ noun: "Attachment",
2951
+ state: "not_attached",
2952
+ activationRoot: null,
2953
+ servingSlot: "none",
2954
+ policyMode,
2955
+ policy: buildCurrentProfileAttachmentPolicy(policyMode),
2956
+ detail: "current profile is not attached to an OpenClawBrain activation boundary"
2957
+ },
2958
+ brainStatus: {
2959
+ status,
2960
+ brainState: report.brain.state,
2961
+ serveState: report.servePath.state,
2962
+ usedLearnedRouteFn: report.servePath.usedLearnedRouteFn,
2963
+ failOpen: report.servePath.fallbackToStaticContext,
2964
+ awaitingFirstExport,
2965
+ detail: report.servePath.state === "serving_active_pack"
2966
+ ? "current profile is serving compiled context from the active promoted pack"
2967
+ : report.servePath.state === "fail_open_static_context"
2968
+ ? "current profile would fail open to static context because no serving pack is available"
2969
+ : report.servePath.state === "hard_fail"
2970
+ ? "current profile cannot serve because the learned-route or activation requirement hard-failed"
2971
+ : "current profile serve state has not been compile-probed yet"
2972
+ },
2973
+ currentTurnAttribution: null
2974
+ };
2975
+ }
2976
+ function buildOperatorSurfaceReport(input) {
2510
2977
  const activationRoot = path.resolve(normalizeNonEmptyString(input.activationRoot, "activationRoot"));
2511
2978
  const updatedAt = normalizeIsoTimestamp(input.updatedAt, "updatedAt", new Date().toISOString());
2512
2979
  const inspection = inspectActivationState(activationRoot, updatedAt);
@@ -2535,6 +3002,7 @@ export function buildOperatorSurfaceReport(input) {
2535
3002
  routerTrainedAt: observability.learnedRouteFn.routerTrainedAt,
2536
3003
  objective: observability.learnedRouteFn.objective,
2537
3004
  pgProfile: observability.learnedRouteFn.pgProfile,
3005
+ routerChecksum: observability.learnedRouteFn.routerChecksum,
2538
3006
  objectiveChecksum: observability.learnedRouteFn.objectiveChecksum,
2539
3007
  updateMechanism: observability.learnedRouteFn.updateMechanism,
2540
3008
  updateVersion: observability.learnedRouteFn.updateVersion,
@@ -2572,11 +3040,9 @@ export function buildOperatorSurfaceReport(input) {
2572
3040
  findings
2573
3041
  };
2574
3042
  }
2575
- function formatSlot(label, slot) {
2576
- if (slot === null) {
2577
- return `${label.padEnd(11)}pack=none ready=no`;
2578
- }
2579
- return `${label.padEnd(11)}pack=${slot.packId} ready=${yesNo(slot.activationReady)} route=${slot.routePolicy} snapshot=${slot.workspaceSnapshot} export=${formatCompactValue(slot.eventExportDigest)} range=${slot.eventRange.start}-${slot.eventRange.end}/${slot.eventRange.count} built=${slot.builtAt} updated=${slot.updatedAt ?? "unknown"}`;
3043
+ export function describeCurrentProfileBrainStatus(input) {
3044
+ const report = buildOperatorSurfaceReport(input);
3045
+ return buildCurrentProfileBrainStatusFromReport(report, normalizeBrainAttachmentPolicy(input.brainAttachmentPolicy));
2580
3046
  }
2581
3047
  export function formatOperatorRollbackReport(result) {
2582
3048
  const header = result.allowed ? (result.dryRun ? "ROLLBACK ready" : "ROLLBACK ok") : "ROLLBACK blocked";
@@ -2589,94 +3055,6 @@ export function formatOperatorRollbackReport(result) {
2589
3055
  `findings ${formatList(result.findings)}`
2590
3056
  ].join("\n");
2591
3057
  }
2592
- export function formatOperatorStatusReport(report) {
2593
- return [
2594
- `STATUS ${report.status}`,
2595
- formatSlot("active", report.active),
2596
- formatSlot("candidate", report.candidate),
2597
- `freshness candidateAhead=${yesNo(report.freshness.activeBehindPromotionReadyCandidate)} delta=${formatList(report.freshness.candidateAheadBy)} lastPromotion=${report.promotion.lastPromotion.known ? report.promotion.lastPromotion.at : report.promotion.lastPromotion.confidence}`,
2598
- `brain state=${report.brain.state} init=${report.brain.initMode ?? "unknown"} plasticity=${report.brain.runtimePlasticitySource ?? "unknown"} seedVisible=${yesNo(report.brain.seedStateVisible)} seedBlocks=${report.brain.seedBlockCount}`,
2599
- `serve pack=${report.servePath.activePackId ?? report.active?.packId ?? "none"} state=${report.servePath.state} failOpen=${yesNo(report.servePath.fallbackToStaticContext)} hardFail=${yesNo(report.servePath.hardRequirementViolated)} usedRouteFn=${yesNo(report.servePath.usedLearnedRouteFn)} selection=${report.servePath.selectionMode ?? "unknown"} tiers=${report.servePath.contextAttribution.selectionTiers ?? "unknown"}`,
2600
- `context kernel=${report.servePath.contextAttribution.stableKernelBlockCount} brain=${report.servePath.contextAttribution.brainCompiledBlockCount} evidence=${report.servePath.contextAttribution.evidence} kernelSrc=${formatCompactList(report.servePath.contextAttribution.stableKernelSources)} brainSrc=${formatCompactList(report.servePath.contextAttribution.brainCompiledSources)}`,
2601
- `route router=${report.servePath.routerIdentity ?? report.learnedRouting.routerIdentity ?? "none"} refresh=${report.servePath.refreshStatus ?? "unknown"} freshness=${formatCompactValue(report.servePath.freshnessChecksum ?? report.learnedRouting.freshnessChecksum)} objective=${formatCompactValue(report.learnedRouting.objectiveChecksum)} labels=${report.learnedRouting.collectedLabelsTotal ?? 0} updates=${report.learnedRouting.updateCount ?? 0}`,
2602
- `supervision flowing=${yesNo(report.supervision.flowing)} source=${report.supervision.freshestSourceStream ?? "none"} freshest=${report.supervision.freshestCreatedAt ?? "unknown"} humanLabels=${report.supervision.humanLabelCount ?? 0} export=${report.supervision.exportDigest ?? "none"}`,
2603
- `learning mode=${report.learning.mode} next=${report.learning.nextPriorityLane} pendingLive=${report.learning.pendingLive ?? 0} pendingBackfill=${report.learning.pendingBackfill ?? 0} activeBuilt=${report.active?.builtAt ?? "unknown"} supervisionFreshest=${report.supervision.freshestCreatedAt ?? "unknown"} teacher=${report.teacherLoop.latestFreshness} lastNoOp=${report.teacherLoop.lastNoOpReason} lastProcessed=${report.teacherLoop.lastProcessedAt ?? "unknown"} materialized=${report.teacherLoop.lastMaterializedPackId ?? "none"}`,
2604
- `rollback ready=${yesNo(report.rollback.allowed)} previous=${report.rollback.previousPackId ?? "none"} findings=${formatList(report.rollback.findings)}`
2605
- ].join("\n");
2606
- }
2607
- function orderedDoctorFindings(findings) {
2608
- const severityOrder = {
2609
- fail: 0,
2610
- warn: 1,
2611
- pass: 2
2612
- };
2613
- return [...findings].sort((left, right) => {
2614
- if (severityOrder[left.severity] !== severityOrder[right.severity]) {
2615
- return severityOrder[left.severity] - severityOrder[right.severity];
2616
- }
2617
- return left.code.localeCompare(right.code);
2618
- });
2619
- }
2620
- function buildDoctorNextSteps(findings) {
2621
- const steps = new Map();
2622
- for (const finding of findings) {
2623
- switch (finding.code) {
2624
- case "active_missing":
2625
- steps.set(finding.code, "activate a healthy pack before serving or promotion checks");
2626
- break;
2627
- case "active_unhealthy":
2628
- case "learned_route_missing":
2629
- steps.set(finding.code, "repair the active pack or promote a healthy learned-routing candidate");
2630
- break;
2631
- case "candidate_missing":
2632
- case "promotion_blocked":
2633
- case "candidate_unhealthy":
2634
- steps.set(finding.code, "stage or repair a fresher candidate pack before attempting promotion");
2635
- break;
2636
- case "rollback_blocked":
2637
- steps.set(finding.code, "capture a promotion before expecting rollback readiness; previous pointer is the key guardrail");
2638
- break;
2639
- case "last_promotion_unknown":
2640
- steps.set(finding.code, "treat `active.updatedAt` as the last pointer move only; do not claim exact last-promotion time without previous-pointer lineage");
2641
- break;
2642
- case "supervision_unavailable":
2643
- case "supervision_not_flowing":
2644
- steps.set(finding.code, "pass `--event-export <bundle-root-or-payload>` to inspect local supervision freshness and teacher signals");
2645
- break;
2646
- case "serve_path_fail_open":
2647
- steps.set(finding.code, "decide whether fail-open static context is acceptable for this runtime and repair activation if it is not");
2648
- break;
2649
- case "serve_path_hard_fail":
2650
- case "serve_path_route_evidence_missing":
2651
- steps.set(finding.code, "repair the active learned-routing pack or promote a healthy candidate before serving again");
2652
- break;
2653
- case "brain_context_kernel_only":
2654
- steps.set(finding.code, "compare stable-kernel versus brain-compiled sources; if you expected live context, refresh exports or promote a fresher candidate pack");
2655
- break;
2656
- case "teacher_snapshot_unavailable":
2657
- steps.set(finding.code, "serialize `teacherLoop.snapshot()` or `await teacherLoop.flush()` and pass `--teacher-snapshot <snapshot.json>` when you need the last no-op reason");
2658
- break;
2659
- default:
2660
- break;
2661
- }
2662
- }
2663
- return [...steps.values()];
2664
- }
2665
- export function formatOperatorDoctorReport(report) {
2666
- const lines = [`DOCTOR ${report.status}`];
2667
- for (const finding of orderedDoctorFindings(report.findings)) {
2668
- lines.push(`${finding.severity.toUpperCase()} ${finding.summary}`);
2669
- lines.push(` ${finding.detail}`);
2670
- }
2671
- const nextSteps = buildDoctorNextSteps(report.findings.filter((finding) => finding.severity !== "pass"));
2672
- if (nextSteps.length > 0) {
2673
- lines.push("NEXT");
2674
- for (const step of nextSteps) {
2675
- lines.push(`- ${step}`);
2676
- }
2677
- }
2678
- return lines.join("\n");
2679
- }
2680
3058
  /**
2681
3059
  * Describes the kernel/brain boundary for a single compile response.
2682
3060
  *