@openclawbrain/cli 0.4.16 → 0.4.18

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.
@@ -0,0 +1,65 @@
1
+ import { existsSync, openSync, readSync, closeSync, statSync, readFileSync } from "node:fs";
2
+
3
+ const DEFAULT_TAIL_BYTES = 4 * 1024 * 1024;
4
+ const DEFAULT_MAX_ENTRIES = 512;
5
+ const DEFAULT_MAX_LINE_BYTES = 128 * 1024;
6
+
7
+ export function readBoundedJsonlTail(filePath, options = {}) {
8
+ const tailBytes = options.tailBytes ?? DEFAULT_TAIL_BYTES;
9
+ const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
10
+ const maxLineBytes = options.maxLineBytes ?? DEFAULT_MAX_LINE_BYTES;
11
+ if (!existsSync(filePath)) {
12
+ return { entries: [], fallbackReason: null };
13
+ }
14
+ let fallbackReason = null;
15
+ let raw;
16
+ try {
17
+ const fileSize = statSync(filePath).size;
18
+ if (fileSize <= tailBytes) {
19
+ raw = readFileSync(filePath, "utf8");
20
+ }
21
+ else {
22
+ fallbackReason = "tail_truncated";
23
+ const offset = fileSize - tailBytes;
24
+ const buf = Buffer.alloc(tailBytes);
25
+ const fd = openSync(filePath, "r");
26
+ try {
27
+ readSync(fd, buf, 0, tailBytes, offset);
28
+ }
29
+ finally {
30
+ closeSync(fd);
31
+ }
32
+ raw = buf.toString("utf8");
33
+ const firstNewline = raw.indexOf("\n");
34
+ if (firstNewline >= 0) {
35
+ raw = raw.slice(firstNewline + 1);
36
+ }
37
+ }
38
+ }
39
+ catch {
40
+ return { entries: [], fallbackReason: "read_error" };
41
+ }
42
+ const lines = raw.split(/\r?\n/u).map((l) => l.trim()).filter((l) => l.length > 0);
43
+ const entries = [];
44
+ let skippedOversized = 0;
45
+ for (const line of lines) {
46
+ if (Buffer.byteLength(line, "utf8") > maxLineBytes) {
47
+ skippedOversized++;
48
+ continue;
49
+ }
50
+ try {
51
+ entries.push(JSON.parse(line));
52
+ }
53
+ catch {
54
+ // skip malformed lines
55
+ }
56
+ }
57
+ if (skippedOversized > 0) {
58
+ fallbackReason = fallbackReason ? `${fallbackReason}+oversized_lines_skipped` : "oversized_lines_skipped";
59
+ }
60
+ const trimmed = entries.length > maxEntries ? entries.slice(-maxEntries) : entries;
61
+ if (entries.length > maxEntries) {
62
+ fallbackReason = fallbackReason ? `${fallbackReason}+entry_count_capped` : "entry_count_capped";
63
+ }
64
+ return { entries: trimmed, fallbackReason };
65
+ }
package/dist/src/cli.js CHANGED
@@ -10,7 +10,7 @@ import { ensureManagedLearnerServiceForActivationRoot, inspectManagedLearnerServ
10
10
  import { exportBrain, importBrain } from "./import-export.js";
11
11
  import { buildNormalizedEventExport } from "@openclawbrain/contracts";
12
12
  import { buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, drainAlwaysOnLearningRuntime, loadOrInitBaseline, materializeAlwaysOnLearningCandidatePack, persistBaseline } from "./local-learner.js";
13
- import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, stageCandidatePack } from "@openclawbrain/pack-format";
13
+ import { inspectActivationState, loadPackFromActivation, promoteCandidatePack, resolveLearningSpineLogPath, stageCandidatePack } from "@openclawbrain/pack-format";
14
14
  import { resolveActivationRoot } from "./resolve-activation-root.js";
15
15
  import { describeOpenClawHomeInspection, discoverOpenClawHomes, formatOpenClawHomeLayout, formatOpenClawHomeProfileSource, inspectOpenClawHome } from "./openclaw-home-layout.js";
16
16
  import { inspectOpenClawBrainHookStatus, inspectOpenClawBrainPluginAllowlist } from "./openclaw-hook-truth.js";
@@ -37,6 +37,68 @@ const INSTALL_COMPATIBLE_LOCAL_TEACHER_MODEL_PREFIXES = [
37
37
  "qwen3:8b",
38
38
  "qwen2.5:7b"
39
39
  ];
40
+ const DEFAULT_BOUNDED_JSONL_TAIL_BYTES = 4 * 1024 * 1024;
41
+ const DEFAULT_BOUNDED_JSONL_MAX_ENTRIES = 512;
42
+ const DEFAULT_BOUNDED_JSONL_MAX_LINE_BYTES = 128 * 1024;
43
+ function readBoundedJsonlTail(filePath, options = {}) {
44
+ const tailBytes = options.tailBytes ?? DEFAULT_BOUNDED_JSONL_TAIL_BYTES;
45
+ const maxEntries = options.maxEntries ?? DEFAULT_BOUNDED_JSONL_MAX_ENTRIES;
46
+ const maxLineBytes = options.maxLineBytes ?? DEFAULT_BOUNDED_JSONL_MAX_LINE_BYTES;
47
+ if (!existsSync(filePath)) {
48
+ return { entries: [], fallbackReason: null };
49
+ }
50
+ let fallbackReason = null;
51
+ let raw;
52
+ try {
53
+ const fileSize = statSync(filePath).size;
54
+ if (fileSize <= tailBytes) {
55
+ raw = readFileSync(filePath, "utf8");
56
+ }
57
+ else {
58
+ fallbackReason = "tail_truncated";
59
+ const offset = fileSize - tailBytes;
60
+ const buffer = Buffer.alloc(tailBytes);
61
+ const fileDescriptor = openSync(filePath, "r");
62
+ try {
63
+ readSync(fileDescriptor, buffer, 0, tailBytes, offset);
64
+ }
65
+ finally {
66
+ closeSync(fileDescriptor);
67
+ }
68
+ raw = buffer.toString("utf8");
69
+ const firstNewline = raw.indexOf("\n");
70
+ if (firstNewline >= 0) {
71
+ raw = raw.slice(firstNewline + 1);
72
+ }
73
+ }
74
+ }
75
+ catch {
76
+ return { entries: [], fallbackReason: "read_error" };
77
+ }
78
+ const lines = raw.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
79
+ const entries = [];
80
+ let skippedOversized = 0;
81
+ for (const line of lines) {
82
+ if (Buffer.byteLength(line, "utf8") > maxLineBytes) {
83
+ skippedOversized++;
84
+ continue;
85
+ }
86
+ try {
87
+ entries.push(JSON.parse(line));
88
+ }
89
+ catch {
90
+ // skip malformed lines
91
+ }
92
+ }
93
+ if (skippedOversized > 0) {
94
+ fallbackReason = fallbackReason === null ? "oversized_lines_skipped" : `${fallbackReason}+oversized_lines_skipped`;
95
+ }
96
+ const trimmedEntries = entries.length > maxEntries ? entries.slice(-maxEntries) : entries;
97
+ if (entries.length > maxEntries) {
98
+ fallbackReason = fallbackReason === null ? "entry_count_capped" : `${fallbackReason}+entry_count_capped`;
99
+ }
100
+ return { entries: trimmedEntries, fallbackReason };
101
+ }
40
102
  function quoteShellArg(value) {
41
103
  return `'${value.replace(/'/g, `"'"'`)}'`;
42
104
  }
@@ -1171,6 +1233,7 @@ function summarizeStatusEmbedder(embeddings) {
1171
1233
  }
1172
1234
  function summarizeStatusRouteFn(status, report) {
1173
1235
  const freshness = report.servePath.refreshStatus ?? status.brain.routeFreshness;
1236
+ const fallbackReason = report.routeFn.fallbackReason ?? null;
1174
1237
  if (!report.routeFn.available) {
1175
1238
  return {
1176
1239
  available: false,
@@ -1178,6 +1241,7 @@ function summarizeStatusRouteFn(status, report) {
1178
1241
  trainedAt: report.routeFn.trainedAt,
1179
1242
  updatedAt: report.routeFn.updatedAt,
1180
1243
  usedAt: report.routeFn.usedAt,
1244
+ fallbackReason,
1181
1245
  detail: report.routeFn.detail
1182
1246
  };
1183
1247
  }
@@ -1191,12 +1255,16 @@ function summarizeStatusRouteFn(status, report) {
1191
1255
  else if (report.routeFn.updatedAt !== null) {
1192
1256
  detail = `active route_fn was last updated at ${report.routeFn.updatedAt}, but no learned serve use is visible yet for the current pack`;
1193
1257
  }
1258
+ if (fallbackReason !== null && !detail.includes(fallbackReason)) {
1259
+ detail = `${detail}; bounded_log_read=${fallbackReason}`;
1260
+ }
1194
1261
  return {
1195
1262
  available: true,
1196
1263
  freshness,
1197
1264
  trainedAt: report.routeFn.trainedAt,
1198
1265
  updatedAt: report.routeFn.updatedAt,
1199
1266
  usedAt: report.routeFn.usedAt,
1267
+ fallbackReason,
1200
1268
  detail
1201
1269
  };
1202
1270
  }
@@ -4453,14 +4521,8 @@ function runUninstallCommand(parsed) {
4453
4521
  return 0;
4454
4522
  }
4455
4523
  function resolveServeTimeLearningRuntimeInput(activationRoot) {
4456
- let serveTimeDecisions = [];
4457
- let fallbackReason = null;
4458
- try {
4459
- serveTimeDecisions = readLearningSpineLogEntries(activationRoot, "serveTimeRouteDecisions");
4460
- }
4461
- catch {
4462
- fallbackReason = "serve_time_decision_log_read_failed";
4463
- }
4524
+ const logPath = resolveLearningSpineLogPath(activationRoot, "serveTimeRouteDecisions");
4525
+ const { entries: serveTimeDecisions, fallbackReason } = readBoundedJsonlTail(logPath);
4464
4526
  const decisionLogCount = serveTimeDecisions.length;
4465
4527
  const pgVersion = decisionLogCount > 0 ? "v2" : "v1";
4466
4528
  return {
@@ -4468,7 +4530,7 @@ function resolveServeTimeLearningRuntimeInput(activationRoot) {
4468
4530
  serveTimeDecisions,
4469
4531
  decisionLogCount,
4470
4532
  baselineState: pgVersion === "v2" ? loadOrInitBaseline(activationRoot) : undefined,
4471
- fallbackReason
4533
+ fallbackReason: fallbackReason === null ? null : `serve_time_decision_log_${fallbackReason}`
4472
4534
  };
4473
4535
  }
4474
4536
  function resolveActivationInspectionPackId(inspection, slot) {
package/dist/src/index.js CHANGED
@@ -7,9 +7,10 @@ import { compileRuntimeFromActivation } from "@openclawbrain/compiler";
7
7
  import { CONTRACT_IDS, buildEventSemanticSurface, buildNormalizedEventExport, canonicalJson, checksumJsonPayload, createFeedbackEvent, createInteractionEvent, sortNormalizedEvents, validateKernelSurface, validateNormalizedEventExport } from "@openclawbrain/contracts";
8
8
  import { classifyFeedbackSignalContent, describeNormalizedEventExportObservability } from "@openclawbrain/event-export";
9
9
  import { DEFAULT_TEACHER_SUPERVISION_STALE_AFTER_MS, advanceAlwaysOnLearningRuntime, buildTeacherSupervisionArtifactsFromNormalizedEventExport, createAlwaysOnLearningRuntimeState, describeAlwaysOnLearningRuntimeState, materializeAlwaysOnLearningCandidatePack, materializeCandidatePackFromNormalizedEventExport } from "./local-learner.js";
10
- import { LEARNING_SPINE_LOG_LAYOUT, activatePack, describeActivationObservability, describeActivationTarget, describePackCompileTarget, inspectActivationState, loadPackFromActivation, promoteCandidatePack, readLearningSpineLogEntries, rollbackActivePack, stageCandidatePack } from "@openclawbrain/pack-format";
10
+ import { LEARNING_SPINE_LOG_LAYOUT, activatePack, describeActivationObservability, describeActivationTarget, describePackCompileTarget, inspectActivationState, loadPackFromActivation, promoteCandidatePack, resolveLearningSpineLogPath, rollbackActivePack, stageCandidatePack } from "@openclawbrain/pack-format";
11
11
  import { inspectOpenClawBrainHookStatus, summarizeOpenClawBrainHookLoad } from "./openclaw-hook-truth.js";
12
12
  import { appendLearningUpdateLogs, appendServeTimeRouteDecisionLog } from "./learning-spine.js";
13
+ import { readBoundedJsonlTail } from "./bounded-jsonl-reader.js";
13
14
  import { buildFeedbackSemanticMetadata, buildInteractionSemanticMetadata } from "./semantic-metadata.js";
14
15
  export { clearOpenClawProfileRuntimeLoadProof, listOpenClawProfileRuntimeLoadProofs, recordOpenClawProfileRuntimeLoadProof, resolveAttachmentRuntimeLoadProofsPath } from "./attachment-truth.js";
15
16
  import { createTeacherLabeler, summarizeTeacherLabelerOpportunity } from "./teacher-labeler.js";
@@ -6930,6 +6931,10 @@ function summarizeTeacherLoop(input) {
6930
6931
  : "raw async teacher snapshot loaded"
6931
6932
  };
6932
6933
  }
6934
+ function readBoundedLearningSpineLogEntries(activationRoot, stream) {
6935
+ const logPath = resolveLearningSpineLogPath(activationRoot, stream);
6936
+ return readBoundedJsonlTail(logPath).entries;
6937
+ }
6933
6938
  function matchesActiveRouteFnLog(input) {
6934
6939
  if (input.activePackId !== null && input.entryPackId === input.activePackId) {
6935
6940
  return true;
@@ -6970,8 +6975,8 @@ function summarizeRouteFnFreshness(input) {
6970
6975
  : `active pack ${input.activePackId} does not require a learned route_fn`
6971
6976
  };
6972
6977
  }
6973
- const updates = readLearningSpineLogEntries(input.activationRoot, "pgRouteUpdates");
6974
- const decisions = readLearningSpineLogEntries(input.activationRoot, "serveTimeRouteDecisions");
6978
+ const updates = readBoundedLearningSpineLogEntries(input.activationRoot, "pgRouteUpdates");
6979
+ const decisions = readBoundedLearningSpineLogEntries(input.activationRoot, "serveTimeRouteDecisions");
6975
6980
  const updated = [...updates].reverse().find((entry) => matchesActiveRouteFnLog({
6976
6981
  activePackId: input.activePackId,
6977
6982
  routerChecksum: input.learnedRouting.routerChecksum,
@@ -7458,7 +7463,7 @@ function summarizeCurrentProfileLogRoot(activationRoot) {
7458
7463
  return existsSync(logRoot) ? logRoot : null;
7459
7464
  }
7460
7465
  function summarizeCurrentProfileLastLearningUpdateAt(activationRoot, learning, teacherLoop) {
7461
- const updates = readLearningSpineLogEntries(activationRoot, "pgRouteUpdates");
7466
+ const updates = readBoundedLearningSpineLogEntries(activationRoot, "pgRouteUpdates");
7462
7467
  return updates.at(-1)?.recordedAt ?? teacherLoop.lastRunAt ?? learning.lastMaterializedAt ?? null;
7463
7468
  }
7464
7469
  function didCurrentProfileFirstExportOccur(report) {
@@ -15,9 +15,66 @@ function normalizeCount(value) {
15
15
  function normalizeOptionalString(value) {
16
16
  return typeof value === "string" && value.trim().length > 0 ? value : null;
17
17
  }
18
+ function normalizeUnitInterval(value) {
19
+ return Number.isFinite(value) ? Math.max(0, Math.min(1, Number(value))) : 0;
20
+ }
18
21
  function normalizeSource(value) {
19
22
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
20
23
  }
24
+ function normalizeLastInterruptionSummary(value) {
25
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
26
+ return null;
27
+ }
28
+ const normalized = {
29
+ reason: normalizeOptionalString(value.reason),
30
+ stage: normalizeOptionalString(value.stage),
31
+ servedPartial: value.servedPartial === true,
32
+ droppedFrontierCount: normalizeCount(value.droppedFrontierCount),
33
+ droppedProposalCount: normalizeCount(value.droppedProposalCount),
34
+ budgetUtilization: normalizeUnitInterval(value.budgetUtilization)
35
+ };
36
+ return normalized.reason !== null ||
37
+ normalized.stage !== null ||
38
+ normalized.servedPartial ||
39
+ normalized.droppedFrontierCount > 0 ||
40
+ normalized.droppedProposalCount > 0 ||
41
+ normalized.budgetUtilization > 0
42
+ ? normalized
43
+ : null;
44
+ }
45
+ function formatLastInterruptionDetail(value) {
46
+ const summary = normalizeLastInterruptionSummary(value);
47
+ if (summary === null) {
48
+ return null;
49
+ }
50
+ return [
51
+ `interrupt=${summary.reason ?? summary.stage ?? "unknown"}`,
52
+ `partial=${summary.servedPartial ? "yes" : "no"}`,
53
+ `frontier=${summary.droppedFrontierCount}`,
54
+ `proposals=${summary.droppedProposalCount}`,
55
+ `budget=${Math.round(summary.budgetUtilization * 100)}%`
56
+ ].join(" ");
57
+ }
58
+ function buildLastInterruptionSummaryFromAssemblyDecision(value) {
59
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
60
+ return null;
61
+ }
62
+ const accounting = value.interruptionAccounting !== null &&
63
+ typeof value.interruptionAccounting === "object" &&
64
+ !Array.isArray(value.interruptionAccounting)
65
+ ? value.interruptionAccounting
66
+ : null;
67
+ return normalizeLastInterruptionSummary({
68
+ reason: normalizeOptionalString(value.brainDropReason) ?? normalizeOptionalString(value.interruptionReason),
69
+ stage: normalizeOptionalString(value.interruptionStage),
70
+ servedPartial: value.servedPartial === true,
71
+ droppedFrontierCount: Array.isArray(accounting?.droppedFrontierNodeIds)
72
+ ? accounting.droppedFrontierNodeIds.filter((entry) => typeof entry === "string" && entry.trim().length > 0).length
73
+ : normalizeCount(accounting?.droppedFrontierCount),
74
+ droppedProposalCount: normalizeCount(accounting?.droppedProposalCount),
75
+ budgetUtilization: accounting?.budgetUtilization
76
+ });
77
+ }
21
78
  function summarizeBridgeSource(value) {
22
79
  const source = normalizeSource(value);
23
80
  if (source === null) {
@@ -53,6 +110,7 @@ function normalizeBridgePayload(payload) {
53
110
  materializedPackId: normalizeOptionalString(payload.materializedPackId),
54
111
  promoted: payload.promoted === true,
55
112
  baselinePersisted: payload.baselinePersisted === true,
113
+ lastInterruptionSummary: normalizeLastInterruptionSummary(payload.lastInterruptionSummary),
56
114
  source: normalizeSource(payload.source)
57
115
  };
58
116
  }
@@ -79,6 +137,7 @@ function normalizePersistedStatusSurface(payload) {
79
137
  materializedPackId: normalizeOptionalString(payload.materializedPackId),
80
138
  promoted: payload.promoted === true,
81
139
  baselinePersisted: payload.baselinePersisted === true,
140
+ lastInterruptionSummary: normalizeLastInterruptionSummary(payload.lastInterruptionSummary),
82
141
  source
83
142
  };
84
143
  }
@@ -97,6 +156,7 @@ function defaultSurface(pathname, detail, error = null) {
97
156
  materializedPackId: null,
98
157
  promoted: false,
99
158
  baselinePersisted: false,
159
+ lastInterruptionSummary: null,
100
160
  source: null,
101
161
  detail,
102
162
  error
@@ -145,6 +205,10 @@ function loadTrainingStateJson(db, key) {
145
205
  };
146
206
  }
147
207
  }
208
+ function loadLastAssemblyInterruptionSummary(db) {
209
+ const loaded = loadTrainingStateJson(db, "last_assembly_decision_json");
210
+ return loaded.value === null ? null : buildLastInterruptionSummaryFromAssemblyDecision(loaded.value);
211
+ }
148
212
  function writeTrainingStateJson(db, key, value) {
149
213
  db.prepare(`INSERT OR REPLACE INTO brain_training_state (key, value) VALUES (?, ?)`).run(key, JSON.stringify(value));
150
214
  }
@@ -187,6 +251,7 @@ export function buildTracedLearningBridgePayloadFromRuntime(input) {
187
251
  materializedPackId: input?.materializedPackId ?? lastMaterialization?.candidate?.summary?.packId ?? null,
188
252
  promoted: input?.promoted === true,
189
253
  baselinePersisted: input?.baselinePersisted === true,
254
+ lastInterruptionSummary: input?.lastInterruptionSummary ?? null,
190
255
  source: input?.source
191
256
  });
192
257
  }
@@ -212,6 +277,7 @@ function buildPersistedStatusSurfaceBridge(summary, context) {
212
277
  materializedPackId: summary.materializedPackId,
213
278
  promoted: summary.promoted,
214
279
  baselinePersisted: summary.baselinePersisted,
280
+ lastInterruptionSummary: summary.lastInterruptionSummary,
215
281
  source: {
216
282
  command: "brain-store",
217
283
  bridge: TRACED_LEARNING_STATUS_SURFACE_BRIDGE,
@@ -246,7 +312,7 @@ function loadPersistedStatusSurface(db, context) {
246
312
  };
247
313
  }
248
314
  }
249
- function buildDerivedBrainStoreBridge(db, context) {
315
+ function buildDerivedBrainStoreBridge(db, context, lastInterruptionSummary = null) {
250
316
  const routeTraceCount = countRows(db, "brain_traces");
251
317
  const supervisionCount = countRows(db, "brain_trace_supervision");
252
318
  const candidateUpdateRaw = loadTrainingStateValue(db, "last_pg_candidate_update_json");
@@ -269,6 +335,7 @@ function buildDerivedBrainStoreBridge(db, context) {
269
335
  materializedPackId: null,
270
336
  promoted: false,
271
337
  baselinePersisted: false,
338
+ lastInterruptionSummary,
272
339
  source: {
273
340
  command: "brain-store",
274
341
  bridge: "brain_store_state",
@@ -288,6 +355,7 @@ function hasMeaningfulTracedLearningSignal(bridge) {
288
355
  bridge.materializedPackId !== null ||
289
356
  bridge.promoted ||
290
357
  bridge.baselinePersisted ||
358
+ bridge.lastInterruptionSummary !== null ||
291
359
  bridge.pgVersionRequested !== null ||
292
360
  bridge.pgVersionUsed !== null ||
293
361
  bridge.fallbackReason !== null ||
@@ -410,21 +478,28 @@ export function loadBrainStoreTracedLearningBridge(options = {}) {
410
478
  let db;
411
479
  try {
412
480
  db = new sqlite.DatabaseSync(dbPath, { readOnly: true });
481
+ const lastInterruptionSummary = loadLastAssemblyInterruptionSummary(db);
413
482
  const persisted = loadPersistedStatusSurface(db, {
414
483
  brainRoot,
415
484
  dbPath
416
485
  });
417
486
  if (persisted.bridge !== null) {
487
+ const bridge = lastInterruptionSummary === null
488
+ ? persisted.bridge
489
+ : normalizeBridgePayload({
490
+ ...persisted.bridge,
491
+ lastInterruptionSummary
492
+ });
418
493
  return {
419
494
  path: dbPath,
420
- bridge: persisted.bridge,
495
+ bridge,
421
496
  error: null
422
497
  };
423
498
  }
424
499
  const bridge = buildDerivedBrainStoreBridge(db, {
425
500
  brainRoot,
426
501
  dbPath
427
- });
502
+ }, lastInterruptionSummary);
428
503
  if (!hasMeaningfulTracedLearningSignal(bridge)) {
429
504
  return {
430
505
  path: dbPath,
@@ -471,6 +546,10 @@ function buildStatusSurface(pathname, bridge, options = {}) {
471
546
  if (bridge.routerNoOpReason !== null) {
472
547
  detailParts.push(`noOp=${bridge.routerNoOpReason}`);
473
548
  }
549
+ const interruptionDetail = formatLastInterruptionDetail(bridge.lastInterruptionSummary);
550
+ if (interruptionDetail !== null) {
551
+ detailParts.push(interruptionDetail);
552
+ }
474
553
  return {
475
554
  path: pathname,
476
555
  present: true,
@@ -485,6 +564,7 @@ function buildStatusSurface(pathname, bridge, options = {}) {
485
564
  materializedPackId: bridge.materializedPackId,
486
565
  promoted: bridge.promoted,
487
566
  baselinePersisted: bridge.baselinePersisted,
567
+ lastInterruptionSummary: bridge.lastInterruptionSummary,
488
568
  source: bridge.source,
489
569
  detail: detailParts.join(" "),
490
570
  error: options.error ?? null
@@ -507,6 +587,7 @@ function buildRuntimeMaterializationMetadata(loaded) {
507
587
  materializedPackId: loaded.bridge.materializedPackId,
508
588
  promoted: loaded.bridge.promoted,
509
589
  baselinePersisted: loaded.bridge.baselinePersisted,
590
+ lastInterruptionSummary: loaded.bridge.lastInterruptionSummary,
510
591
  fallbackReason: loaded.bridge.fallbackReason,
511
592
  routerNoOpReason: loaded.bridge.routerNoOpReason,
512
593
  source: loaded.bridge.source
@@ -529,6 +610,7 @@ function mergeCanonicalStatusBridge(canonicalBridge, runtimeLoaded) {
529
610
  materializedPackId: canonicalBridge.materializedPackId,
530
611
  promoted: canonicalBridge.promoted,
531
612
  baselinePersisted: canonicalBridge.baselinePersisted,
613
+ lastInterruptionSummary: canonicalBridge.lastInterruptionSummary ?? runtimeBridge?.lastInterruptionSummary ?? null,
532
614
  fallbackReason: canonicalBridge.fallbackReason,
533
615
  routerNoOpReason: canonicalBridge.routerNoOpReason,
534
616
  source: runtimeMaterialized === null
@@ -551,6 +633,7 @@ function mergeCanonicalStatusBridge(canonicalBridge, runtimeLoaded) {
551
633
  materializedPackId: runtimeBridge?.materializedPackId ?? canonicalBridge.materializedPackId ?? null,
552
634
  promoted: runtimeBridge?.promoted ?? canonicalBridge.promoted,
553
635
  baselinePersisted: runtimeBridge?.baselinePersisted ?? canonicalBridge.baselinePersisted,
636
+ lastInterruptionSummary: canonicalBridge.lastInterruptionSummary ?? runtimeBridge?.lastInterruptionSummary ?? null,
554
637
  fallbackReason: runtimeBridge?.fallbackReason ?? canonicalBridge.fallbackReason ?? null,
555
638
  routerNoOpReason: runtimeBridge?.routerNoOpReason ?? canonicalBridge.routerNoOpReason ?? null,
556
639
  source: runtimeMaterialized === null
@@ -571,10 +654,12 @@ export function mergeTracedLearningBridgePayload(payload, persisted) {
571
654
  const supervisionCount = Math.max(current.supervisionCount, persistedBridge.supervisionCount);
572
655
  const routerUpdateCount = Math.max(current.routerUpdateCount, persistedBridge.routerUpdateCount);
573
656
  const teacherArtifactCount = Math.max(current.teacherArtifactCount, persistedBridge.teacherArtifactCount);
657
+ const lastInterruptionSummary = current.lastInterruptionSummary ?? persistedBridge.lastInterruptionSummary ?? null;
574
658
  const usedBridge = routeTraceCount !== current.routeTraceCount ||
575
659
  supervisionCount !== current.supervisionCount ||
576
660
  routerUpdateCount !== current.routerUpdateCount ||
577
- teacherArtifactCount !== current.teacherArtifactCount;
661
+ teacherArtifactCount !== current.teacherArtifactCount ||
662
+ lastInterruptionSummary !== current.lastInterruptionSummary;
578
663
  if (!usedBridge) {
579
664
  return current;
580
665
  }
@@ -584,6 +669,7 @@ export function mergeTracedLearningBridgePayload(payload, persisted) {
584
669
  supervisionCount,
585
670
  routerUpdateCount,
586
671
  teacherArtifactCount,
672
+ lastInterruptionSummary,
587
673
  routerNoOpReason: supervisionCount > 0 || routerUpdateCount > 0 ? null : current.routerNoOpReason,
588
674
  source: {
589
675
  ...(current.source ?? {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclawbrain/cli",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
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",