@openclawbrain/cli 0.4.15 → 0.4.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -17,7 +17,7 @@ import { inspectOpenClawBrainHookStatus, inspectOpenClawBrainPluginAllowlist } f
17
17
  import { describeOpenClawBrainInstallIdentity, describeOpenClawBrainInstallLayout, findInstalledOpenClawBrainPlugin, getOpenClawBrainKnownPluginIds, normalizeOpenClawBrainPluginsConfig, pinInstalledOpenClawBrainPluginActivationRoot, resolveOpenClawBrainInstallTarget } from "./openclaw-plugin-install.js";
18
18
  import { buildOpenClawBrainConvergeRestartPlan, classifyOpenClawBrainConvergeVerification, describeOpenClawBrainConvergeChangeReasons, diffOpenClawBrainConvergeRuntimeFingerprint, finalizeOpenClawBrainConvergeResult, planOpenClawBrainConvergePluginAction } from "./install-converge.js";
19
19
  import { loadAttachmentPolicyDeclaration, resolveEffectiveAttachmentPolicyTruth, writeAttachmentPolicyDeclaration } from "./attachment-policy-truth.js";
20
- import { DEFAULT_WATCH_POLL_INTERVAL_SECONDS, buildNormalizedEventExportFromScannedEvents, bootstrapRuntimeAttach, buildOperatorSurfaceReport, clearOpenClawProfileRuntimeLoadProof, compileRuntimeContext, createAsyncTeacherLiveLoop, createOpenClawLocalSessionTail, createRuntimeEventExportScanner, describeCurrentProfileBrainStatus, formatOperatorRollbackReport, listOpenClawProfileRuntimeLoadProofs, loadRuntimeEventExportBundle, loadWatchTeacherSnapshotState, persistWatchTeacherSnapshot, rollbackRuntimeAttach, resolveAttachmentRuntimeLoadProofsPath, resolveOperatorTeacherSnapshotPath, resolveAsyncTeacherLiveLoopSnapshotPath, resolveWatchSessionTailCursorPath, resolveWatchStateRoot, resolveWatchTeacherSnapshotPath, scanLiveEventExport, scanRecordedSession, summarizeLearningPathFromMaterialization, summarizeNormalizedEventExportLabelFlow, writeScannedEventExportBundle } from "./index.js";
20
+ 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, summarizeTeacherNoArtifactCycle, writeScannedEventExportBundle } from "./index.js";
21
21
  import { appendLearningUpdateLogs } from "./learning-spine.js";
22
22
  import { buildPassiveLearningSessionExportFromOpenClawSessionStore } from "./local-session-passive-learning.js";
23
23
  import { reindexMaterializationCandidateWithEmbedder } from "./materialization-embedder.js";
@@ -682,14 +682,14 @@ const LEARNING_WARNING_MESSAGES = {
682
682
  passive_backfill_pending: "passive backfill remains queued",
683
683
  teacher_queue_full: "teacher queue is full",
684
684
  teacher_labels_stale: "teacher labels are stale",
685
- teacher_no_artifacts: "teacher produced no artifacts",
685
+ teacher_no_artifacts: "latest no-op cycle had teachable material but no new teacher artifact",
686
686
  teacher_snapshot_unavailable: "teacher snapshot is unavailable"
687
687
  };
688
688
  const TEACHER_NO_OP_MESSAGES = {
689
689
  none: "the latest processed export produced teacher artifacts",
690
690
  duplicate_export: "the latest cycle was a no-op because the export was already seen",
691
691
  queue_full: "the latest cycle was a no-op because the teacher queue was full",
692
- no_teacher_artifacts: "the latest cycle was a no-op because no teacher artifacts were produced",
692
+ no_teacher_artifacts: "the latest cycle produced no new teacher artifacts",
693
693
  empty_scan: "the latest cycle was a no-op because the scanner did not produce any events",
694
694
  unavailable: "the latest cycle is not visible from the current operator snapshot"
695
695
  };
@@ -1089,7 +1089,9 @@ function summarizeStatusTeacher(report, providerConfig, localLlm) {
1089
1089
  const healthy = report.teacherLoop.failureMode === "none" &&
1090
1090
  stale === false &&
1091
1091
  report.teacherLoop.watchState !== "not_visible";
1092
- const cycleDetail = TEACHER_NO_OP_MESSAGES[report.teacherLoop.lastNoOpReason] ?? "the latest teacher cycle detail is unavailable";
1092
+ const cycleDetail = report.teacherLoop.lastNoOpReason === "no_teacher_artifacts"
1093
+ ? summarizeTeacherNoArtifactCycle(report.teacherLoop.notes).detail
1094
+ : TEACHER_NO_OP_MESSAGES[report.teacherLoop.lastNoOpReason] ?? "the latest teacher cycle detail is unavailable";
1093
1095
  if (report.teacherLoop.failureMode !== "none" && report.teacherLoop.failureMode !== "unavailable") {
1094
1096
  return {
1095
1097
  model: providerConfig.teacher.model,
@@ -5,7 +5,7 @@ import { type AdvanceAlwaysOnLearningRuntimeInput, type AlwaysOnLearningCadenceV
5
5
  import { type ActivationInspection, type ActivationObservabilityReport, type GraphEvolutionLogV1, type LearningSpineServeRouteBreadcrumbsV1, type ActivationSlotInspection, type InitHandoffState, type LearningSpineServeRouteDecisionLogEntryV1 } from "@openclawbrain/pack-format";
6
6
  export { clearOpenClawProfileRuntimeLoadProof, listOpenClawProfileRuntimeLoadProofs, recordOpenClawProfileRuntimeLoadProof, resolveAttachmentRuntimeLoadProofsPath, type OpenClawProfileRuntimeLoadProofRecordV1, type OpenClawProfileRuntimeLoadProofSetV1, type OpenClawProfileRuntimeLoadProofsV1 } from "./attachment-truth.js";
7
7
  import { type AsyncTeacherLabelerConfigV1 } from "./teacher-labeler.js";
8
- export { createHttpOllamaTeacherLabelerClient, createOllamaTeacherLabeler, createTeacherLabeler, type AsyncTeacherLabelerConfigV1, type AsyncTeacherNoopLabelerConfigV1, type AsyncTeacherOllamaLabelerConfigV1, type OllamaTeacherLabelerClient, type TeacherLabeler, type TeacherLabelerResultV1, type TeacherLabelerRunInputV1 } from "./teacher-labeler.js";
8
+ export { createHttpOllamaTeacherLabelerClient, createOllamaTeacherLabeler, createTeacherLabeler, summarizeTeacherLabelerOpportunity, type AsyncTeacherLabelerConfigV1, type AsyncTeacherNoopLabelerConfigV1, type AsyncTeacherOllamaLabelerConfigV1, type OllamaTeacherLabelerClient, type TeacherLabeler, type TeacherLabelerOpportunityInputV1, type TeacherLabelerOpportunityV1, type TeacherLabelerResultV1, type TeacherLabelerRunInputV1 } from "./teacher-labeler.js";
9
9
  export declare const DEFAULT_ASYNC_TEACHER_QUEUE_CAPACITY = 8;
10
10
  declare const RECORDED_SESSION_TRACE_CONTRACT: "recorded_session_trace.v1";
11
11
  declare const RECORDED_SESSION_FIXTURE_CONTRACT: "recorded_session_replay_fixture.v1";
@@ -401,6 +401,10 @@ export interface AsyncTeacherLiveLoopInput extends Pick<AdvanceAlwaysOnLearningR
401
401
  persistUpdatedBaseline?: (state: BaselineStateV1) => void;
402
402
  teacherLabeler?: AsyncTeacherLabelerConfigV1 | null;
403
403
  }
404
+ export interface TeacherNoArtifactCycleSummaryV1 {
405
+ shouldWarn: boolean;
406
+ detail: string;
407
+ }
404
408
  export interface AsyncTeacherQueuedExportJobV1 {
405
409
  jobId: string;
406
410
  exportDigest: string;
@@ -681,6 +685,7 @@ export declare const WATCH_STATE_DIRNAME = "watch";
681
685
  export declare const WATCH_SESSION_TAIL_CURSOR_BASENAME = "session-tail-cursor.json";
682
686
  export declare const WATCH_TEACHER_SNAPSHOT_BASENAME = "teacher-snapshot.json";
683
687
  export declare const DEFAULT_WATCH_POLL_INTERVAL_SECONDS = 30;
688
+ export declare function summarizeTeacherNoArtifactCycle(notes: readonly string[] | null | undefined): TeacherNoArtifactCycleSummaryV1;
684
689
  export interface WatchTeacherSnapshotFailureV1 {
685
690
  mode: "materialization_failed" | "teacher_fail_open";
686
691
  detail: string;
package/dist/src/index.js CHANGED
@@ -12,8 +12,8 @@ import { inspectOpenClawBrainHookStatus, summarizeOpenClawBrainHookLoad } from "
12
12
  import { appendLearningUpdateLogs, appendServeTimeRouteDecisionLog } from "./learning-spine.js";
13
13
  import { buildFeedbackSemanticMetadata, buildInteractionSemanticMetadata } from "./semantic-metadata.js";
14
14
  export { clearOpenClawProfileRuntimeLoadProof, listOpenClawProfileRuntimeLoadProofs, recordOpenClawProfileRuntimeLoadProof, resolveAttachmentRuntimeLoadProofsPath } from "./attachment-truth.js";
15
- import { createTeacherLabeler } from "./teacher-labeler.js";
16
- export { createHttpOllamaTeacherLabelerClient, createOllamaTeacherLabeler, createTeacherLabeler } from "./teacher-labeler.js";
15
+ import { createTeacherLabeler, summarizeTeacherLabelerOpportunity } from "./teacher-labeler.js";
16
+ export { createHttpOllamaTeacherLabelerClient, createOllamaTeacherLabeler, createTeacherLabeler, summarizeTeacherLabelerOpportunity } from "./teacher-labeler.js";
17
17
  const DEFAULT_AGENT_ID = "openclaw-runtime";
18
18
  const FEEDBACK_KINDS = new Set(["correction", "teaching", "approval", "suppression"]);
19
19
  export const DEFAULT_ASYNC_TEACHER_QUEUE_CAPACITY = 8;
@@ -250,9 +250,45 @@ function buildAsyncTeacherLoopNotes(input) {
250
250
  `teacher_noop=${input.noOpReason}`,
251
251
  `teacher_labeler=${input.teacherLabeler?.status ?? "disabled"}`,
252
252
  `teacher_labeler_detail=${input.teacherLabeler?.detail ?? "disabled"}`,
253
+ `teacher_last_cycle_deterministic_artifacts=${input.lastCycle?.deterministicArtifactCount ?? "unknown"}`,
254
+ `teacher_last_cycle_new_deterministic_artifacts=${input.lastCycle?.newDeterministicArtifactCount ?? "unknown"}`,
255
+ `teacher_last_cycle_labeler_candidates=${input.lastCycle?.labelerCandidateCount ?? "unknown"}`,
256
+ `teacher_last_cycle_labeler_budgeted_candidates=${input.lastCycle?.labelerBudgetedCandidateCount ?? "unknown"}`,
257
+ `teacher_last_cycle_labeler_status=${input.lastCycle?.labelerStatus ?? "unknown"}`,
258
+ `teacher_last_cycle_labeler_detail=${input.lastCycle?.labelerDetail ?? "unknown"}`,
253
259
  input.materialization === null ? "teacher_materialization=noop" : `teacher_materialized_pack=${input.materialization.candidate.summary.packId}`
254
260
  ];
255
261
  }
262
+ function parseAsyncTeacherLastCycleNotes(notes) {
263
+ const values = new Map();
264
+ for (const note of notes) {
265
+ const separator = note.indexOf("=");
266
+ if (separator <= 0) {
267
+ continue;
268
+ }
269
+ values.set(note.slice(0, separator), note.slice(separator + 1));
270
+ }
271
+ const readNullableNumber = (key) => {
272
+ const raw = values.get(key);
273
+ if (raw === undefined || raw === "unknown") {
274
+ return null;
275
+ }
276
+ const parsed = Number.parseInt(raw, 10);
277
+ return Number.isFinite(parsed) ? parsed : null;
278
+ };
279
+ const readNullableString = (key) => {
280
+ const raw = values.get(key);
281
+ return raw === undefined || raw === "unknown" ? null : raw;
282
+ };
283
+ return {
284
+ deterministicArtifactCount: readNullableNumber("teacher_last_cycle_deterministic_artifacts"),
285
+ newDeterministicArtifactCount: readNullableNumber("teacher_last_cycle_new_deterministic_artifacts"),
286
+ labelerCandidateCount: readNullableNumber("teacher_last_cycle_labeler_candidates"),
287
+ labelerBudgetedCandidateCount: readNullableNumber("teacher_last_cycle_labeler_budgeted_candidates"),
288
+ labelerStatus: readNullableString("teacher_last_cycle_labeler_status"),
289
+ labelerDetail: readNullableString("teacher_last_cycle_labeler_detail")
290
+ };
291
+ }
256
292
  function cloneAlwaysOnLearningMaterializationJobOrNull(value) {
257
293
  return value === null ? null : structuredClone(value);
258
294
  }
@@ -565,6 +601,14 @@ export class AsyncTeacherLiveLoop {
565
601
  learnerState = createAlwaysOnLearningRuntimeState();
566
602
  lastMaterialization = null;
567
603
  lastTeacherLabelerResult = null;
604
+ lastCycle = {
605
+ deterministicArtifactCount: null,
606
+ newDeterministicArtifactCount: null,
607
+ labelerCandidateCount: null,
608
+ labelerBudgetedCandidateCount: null,
609
+ labelerStatus: null,
610
+ labelerDetail: null
611
+ };
568
612
  diagnostics = {
569
613
  acceptedExportCount: 0,
570
614
  processedExportCount: 0,
@@ -584,7 +628,8 @@ export class AsyncTeacherLiveLoop {
584
628
  sparseFeedback: this.learnerState.sparseFeedback,
585
629
  noOpReason: "none",
586
630
  materialization: null,
587
- teacherLabeler: null
631
+ teacherLabeler: null,
632
+ lastCycle: this.lastCycle
588
633
  })
589
634
  };
590
635
  constructor(input) {
@@ -612,6 +657,7 @@ export class AsyncTeacherLiveLoop {
612
657
  ...structuredClone(resumedSnapshot.diagnostics),
613
658
  notes: [...resumedSnapshot.diagnostics.notes]
614
659
  };
660
+ this.lastCycle = parseAsyncTeacherLastCycleNotes(this.diagnostics.notes);
615
661
  for (const exportDigest of resumedSnapshot.state?.seenExportDigests ?? []) {
616
662
  this.seenExportDigests.add(exportDigest);
617
663
  }
@@ -858,6 +904,19 @@ export class AsyncTeacherLiveLoop {
858
904
  feedbackEvents: this.feedbackEvents
859
905
  });
860
906
  const learnedRoutingState = this.input.resolveLearnedRoutingState?.() ?? {};
907
+ const currentDedupIds = new Set(this.teacherArtifacts.map((artifact) => artifact.dedupId));
908
+ const currentCycleBuiltArtifacts = buildTeacherSupervisionArtifactsFromNormalizedEventExport({
909
+ normalizedEventExport: job.normalizedEventExport,
910
+ observedAt: job.observedAt,
911
+ staleAfterMs: this.staleAfterMs,
912
+ ...(this.input.sparseFeedback !== undefined ? { sparseFeedback: this.input.sparseFeedback } : {})
913
+ });
914
+ const currentCycleOpportunity = summarizeTeacherLabelerOpportunity({
915
+ normalizedEventExport: job.normalizedEventExport,
916
+ ...(learnedRoutingState.serveTimeDecisions !== undefined
917
+ ? { serveTimeDecisions: learnedRoutingState.serveTimeDecisions }
918
+ : {})
919
+ }, this.input.teacherLabeler ?? null);
861
920
  const builtArtifacts = buildTeacherSupervisionArtifactsFromNormalizedEventExport({
862
921
  normalizedEventExport: mergedNormalizedEventExport,
863
922
  observedAt: job.observedAt,
@@ -887,10 +946,21 @@ export class AsyncTeacherLiveLoop {
887
946
  }
888
947
  }
889
948
  const nextBuiltArtifacts = mergeTeacherArtifacts([], [...builtArtifacts, ...generatedTeacherArtifacts]);
890
- const currentDedupIds = new Set(this.teacherArtifacts.map((artifact) => artifact.dedupId));
891
949
  const nextTeacherArtifacts = mergeTeacherArtifacts(this.teacherArtifacts, nextBuiltArtifacts);
892
950
  const emittedArtifactCount = nextBuiltArtifacts.filter((artifact) => !currentDedupIds.has(artifact.dedupId)).length;
893
951
  const dedupedArtifactCount = nextBuiltArtifacts.length - emittedArtifactCount;
952
+ this.lastCycle = {
953
+ deterministicArtifactCount: currentCycleBuiltArtifacts.length,
954
+ newDeterministicArtifactCount: currentCycleBuiltArtifacts.filter((artifact) => !currentDedupIds.has(artifact.dedupId)).length,
955
+ labelerCandidateCount: currentCycleOpportunity.candidateCount,
956
+ labelerBudgetedCandidateCount: currentCycleOpportunity.budgetedCandidateCount,
957
+ labelerStatus: currentCycleOpportunity.candidateCount === 0
958
+ ? currentCycleOpportunity.status
959
+ : this.lastTeacherLabelerResult?.status ?? (currentCycleOpportunity.enabled ? "unknown" : "disabled"),
960
+ labelerDetail: currentCycleOpportunity.candidateCount === 0
961
+ ? currentCycleOpportunity.detail
962
+ : this.lastTeacherLabelerResult?.detail ?? currentCycleOpportunity.detail
963
+ };
894
964
  this.teacherArtifacts = nextTeacherArtifacts;
895
965
  const learnerResult = advanceAlwaysOnLearningRuntime({
896
966
  packLabel: this.input.packLabel,
@@ -952,7 +1022,8 @@ export class AsyncTeacherLiveLoop {
952
1022
  sparseFeedback: this.learnerState.sparseFeedback,
953
1023
  noOpReason: this.diagnostics.lastNoOpReason,
954
1024
  materialization: this.lastMaterialization,
955
- teacherLabeler: this.lastTeacherLabelerResult
1025
+ teacherLabeler: this.lastTeacherLabelerResult,
1026
+ lastCycle: this.lastCycle
956
1027
  });
957
1028
  }
958
1029
  }
@@ -6981,11 +7052,104 @@ function summarizeLearningWarningStates(input) {
6981
7052
  if (input.teacherSnapshot.diagnostics.latestFreshness === "stale" && input.teacherSnapshot.diagnostics.lastNoOpReason !== "no_teacher_artifacts") {
6982
7053
  warnings.add("teacher_labels_stale");
6983
7054
  }
6984
- if (input.teacherSnapshot.diagnostics.lastNoOpReason === "no_teacher_artifacts") {
7055
+ if (input.teacherSnapshot.diagnostics.lastNoOpReason === "no_teacher_artifacts" &&
7056
+ summarizeTeacherNoArtifactCycle(input.teacherSnapshot.diagnostics.notes).shouldWarn) {
6985
7057
  warnings.add("teacher_no_artifacts");
6986
7058
  }
6987
7059
  return [...warnings];
6988
7060
  }
7061
+ export function summarizeTeacherNoArtifactCycle(notes) {
7062
+ const values = new Map();
7063
+ for (const note of notes ?? []) {
7064
+ const separator = note.indexOf("=");
7065
+ if (separator <= 0) {
7066
+ continue;
7067
+ }
7068
+ values.set(note.slice(0, separator), note.slice(separator + 1));
7069
+ }
7070
+ const readNullableNumber = (key) => {
7071
+ const raw = values.get(key);
7072
+ if (raw === undefined || raw === "unknown") {
7073
+ return null;
7074
+ }
7075
+ const parsed = Number.parseInt(raw, 10);
7076
+ return Number.isFinite(parsed) ? parsed : null;
7077
+ };
7078
+ const readNullableString = (key) => {
7079
+ const raw = values.get(key);
7080
+ return raw === undefined || raw === "unknown" ? null : raw;
7081
+ };
7082
+ const deterministicArtifactCount = readNullableNumber("teacher_last_cycle_deterministic_artifacts");
7083
+ const newDeterministicArtifactCount = readNullableNumber("teacher_last_cycle_new_deterministic_artifacts");
7084
+ const labelerCandidateCount = readNullableNumber("teacher_last_cycle_labeler_candidates");
7085
+ const labelerBudgetedCandidateCount = readNullableNumber("teacher_last_cycle_labeler_budgeted_candidates");
7086
+ const labelerStatus = readNullableString("teacher_last_cycle_labeler_status");
7087
+ const labelerDetail = readNullableString("teacher_last_cycle_labeler_detail");
7088
+ if (deterministicArtifactCount === null && labelerCandidateCount === null) {
7089
+ return {
7090
+ shouldWarn: true,
7091
+ detail: "the latest cycle produced no new teacher artifacts, and the snapshot did not say whether any teachable material was present"
7092
+ };
7093
+ }
7094
+ if ((deterministicArtifactCount ?? 0) === 0 && (labelerCandidateCount ?? 0) === 0) {
7095
+ return {
7096
+ shouldWarn: false,
7097
+ detail: "the latest cycle produced no new teacher artifacts because the current export had no eligible feedback, operator overrides, or matched interaction text"
7098
+ };
7099
+ }
7100
+ if ((deterministicArtifactCount ?? 0) > 0 &&
7101
+ (newDeterministicArtifactCount ?? 0) === 0 &&
7102
+ (labelerCandidateCount ?? 0) === 0) {
7103
+ return {
7104
+ shouldWarn: false,
7105
+ detail: "the latest cycle produced no new teacher artifacts because the current export only repeated supervision that was already captured"
7106
+ };
7107
+ }
7108
+ if ((labelerCandidateCount ?? 0) > 0) {
7109
+ if ((labelerBudgetedCandidateCount ?? labelerCandidateCount) === 0) {
7110
+ return {
7111
+ shouldWarn: true,
7112
+ detail: "the latest cycle produced no new teacher artifacts because candidate interactions exceeded the teacher labeler prompt budget"
7113
+ };
7114
+ }
7115
+ if (labelerStatus === "disabled") {
7116
+ return {
7117
+ shouldWarn: false,
7118
+ detail: "the latest cycle produced no new teacher artifacts because candidate interactions existed, but the background teacher labeler is disabled"
7119
+ };
7120
+ }
7121
+ if (labelerStatus === "ok") {
7122
+ return {
7123
+ shouldWarn: false,
7124
+ detail: "the latest cycle produced no new teacher artifacts because candidate interactions were evaluated but did not add a new reusable label"
7125
+ };
7126
+ }
7127
+ if (labelerStatus === "fail_open") {
7128
+ return {
7129
+ shouldWarn: true,
7130
+ detail: labelerDetail === null
7131
+ ? "the latest cycle produced no new teacher artifacts because the teacher labeler failed open while candidate interactions were present"
7132
+ : `the latest cycle produced no new teacher artifacts because the teacher labeler failed open while candidate interactions were present: ${labelerDetail}`
7133
+ };
7134
+ }
7135
+ if (labelerDetail === "no_labels_emitted") {
7136
+ return {
7137
+ shouldWarn: true,
7138
+ detail: "the latest cycle produced no new teacher artifacts even though candidate interactions were present; the teacher labeler emitted no reusable labels"
7139
+ };
7140
+ }
7141
+ return {
7142
+ shouldWarn: true,
7143
+ detail: labelerDetail === null
7144
+ ? "the latest cycle produced no new teacher artifacts even though candidate interactions were present"
7145
+ : `the latest cycle produced no new teacher artifacts even though candidate interactions were present: ${labelerDetail}`
7146
+ };
7147
+ }
7148
+ return {
7149
+ shouldWarn: true,
7150
+ detail: "the latest cycle produced no new teacher artifacts even though teachable material was present"
7151
+ };
7152
+ }
6989
7153
  function summarizeAlwaysOnLearning(input, active) {
6990
7154
  const unavailableLag = {
6991
7155
  activeEventRangeEnd: active?.eventRange.end ?? null,
@@ -142,9 +142,10 @@ export function classifyOpenClawBrainConvergeVerification(input) {
142
142
  if (displayedStatus === "fail") {
143
143
  blockingReasons.push("status still reports fail");
144
144
  }
145
- const runtimeTruthAlreadyProven = displayedStatus === "ok"
146
- && runtimeLoad === "proven"
147
- && loadProof === "status_probe_ready";
145
+ const runtimeTruthAlreadyProven = runtimeLoad === "proven"
146
+ && loadProof === "status_probe_ready"
147
+ && installState === "installed"
148
+ && loadability === "loadable";
148
149
  if (input.restartRequired === true && input.restartPerformed !== true && !runtimeTruthAlreadyProven) {
149
150
  blockingReasons.push("restart is still required before runtime load can be trusted");
150
151
  }
@@ -4854,8 +4854,10 @@ function isCarryForwardSeedBlock(block) {
4854
4854
  if (typeof block.text !== "string" || block.text.trim().length === 0) {
4855
4855
  return false;
4856
4856
  }
4857
- return block.initSeed !== undefined ||
4858
- block.semantic?.sourceKind === "recorded_session_seed" ||
4857
+ // Only true seed-session evidence should survive prefix-changing promotions.
4858
+ // Ordinary runtime-turn feedback is re-materialized from learnedEventExport;
4859
+ // carrying it forward under the old runtime-graph id duplicates evidence.
4860
+ return block.semantic?.sourceKind === "recorded_session_seed" ||
4859
4861
  block.source.includes("/seed:");
4860
4862
  }
4861
4863
  function carryForwardSeedBlockScore(block) {
@@ -398,8 +398,10 @@ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, brea
398
398
  const runtimeProofMatched = Array.isArray(runtimeLoadProofSnapshot?.value?.profiles)
399
399
  && runtimeLoadProofSnapshot.value.profiles.some((profile) => canonicalizeExistingProofPath(profile?.openclawHome ?? "") === canonicalizeExistingProofPath(openclawHome));
400
400
  const runtimeTruthGaps = [];
401
- if (!statusSignals.statusOk)
402
- runtimeTruthGaps.push("status_ok");
401
+ const strongRuntimeTruth = statusSignals.loadProofReady
402
+ && statusSignals.runtimeProven
403
+ && statusSignals.serveActivePack
404
+ && statusSignals.routeFnAvailable;
403
405
  if (!statusSignals.loadProofReady)
404
406
  runtimeTruthGaps.push("load_proof");
405
407
  if (!statusSignals.runtimeProven)
@@ -410,6 +412,15 @@ function buildVerdict({ steps, gatewayStatus, pluginInspect, statusSignals, brea
410
412
  runtimeTruthGaps.push("route_fn");
411
413
  const warningCodes = [];
412
414
  const warnings = [];
415
+ if (!statusSignals.statusOk) {
416
+ if (strongRuntimeTruth) {
417
+ warningCodes.push("status_warn");
418
+ warnings.push("detailed status did not return STATUS ok, but loadProof/runtime/serve/routeFn proofs stayed healthy");
419
+ }
420
+ else {
421
+ runtimeTruthGaps.push("status_ok");
422
+ }
423
+ }
413
424
  if (!gatewayHealthy) {
414
425
  warningCodes.push("gateway_health");
415
426
  warnings.push("gateway status did not confirm runtime running and RPC probe ok");
@@ -597,6 +608,17 @@ function buildGatewayArgs(action, profileName) {
597
608
  : ["gateway", action, "--profile", profileName];
598
609
  }
599
610
 
611
+ function buildGatewayStatusArgs(profileName, gatewayUrl, gatewayToken) {
612
+ const args = buildGatewayArgs("status", profileName);
613
+ if (gatewayUrl !== null) {
614
+ args.push("--url", gatewayUrl);
615
+ }
616
+ if (gatewayToken !== null) {
617
+ args.push("--token", gatewayToken);
618
+ }
619
+ return args;
620
+ }
621
+
600
622
  export function buildProofCommandForOpenClawHome(openclawHome) {
601
623
  return `openclawbrain proof --openclaw-home ${quoteShellArg(path.resolve(openclawHome))}`;
602
624
  }
@@ -609,6 +631,8 @@ export function buildProofCommandHelpSection() {
609
631
  " --skip-install Capture proof without rerunning install first (proof only).",
610
632
  " --skip-restart Capture proof without restarting OpenClaw first (proof only).",
611
633
  ` --plugin-id <id> Plugin id for \`openclaw plugins inspect\` (proof only; default: ${DEFAULT_OPERATOR_PROOF_PLUGIN_ID}).`,
634
+ " --gateway-url <url> Override the gateway-status probe target for proof capture (proof only).",
635
+ " --gateway-token <token> Gateway token to use with --gateway-url or other non-default proof probes.",
612
636
  ` --timeout-ms <ms> Per-step timeout in ms for proof capture (proof only; default: ${DEFAULT_OPERATOR_PROOF_TIMEOUT_MS}).`,
613
637
  ],
614
638
  lifecycle: " 5. proof openclawbrain proof --openclaw-home <path> - capture one durable operator proof bundle after install/restart/status",
@@ -624,6 +648,8 @@ export function parseProofCliArgs(argv, options = {}) {
624
648
  let skipInstall = false;
625
649
  let skipRestart = false;
626
650
  let pluginId = DEFAULT_OPERATOR_PROOF_PLUGIN_ID;
651
+ let gatewayUrl = null;
652
+ let gatewayToken = null;
627
653
  let timeoutMs = DEFAULT_OPERATOR_PROOF_TIMEOUT_MS;
628
654
  let json = false;
629
655
  let help = false;
@@ -681,6 +707,24 @@ export function parseProofCliArgs(argv, options = {}) {
681
707
  index += 1;
682
708
  continue;
683
709
  }
710
+ if (arg === "--gateway-url") {
711
+ const next = argv[index + 1];
712
+ if (next === undefined) {
713
+ throw new Error("--gateway-url requires a value");
714
+ }
715
+ gatewayUrl = next;
716
+ index += 1;
717
+ continue;
718
+ }
719
+ if (arg === "--gateway-token") {
720
+ const next = argv[index + 1];
721
+ if (next === undefined) {
722
+ throw new Error("--gateway-token requires a value");
723
+ }
724
+ gatewayToken = next;
725
+ index += 1;
726
+ continue;
727
+ }
684
728
  if (arg === "--timeout-ms") {
685
729
  const next = argv[index + 1];
686
730
  if (next === undefined) {
@@ -705,6 +749,8 @@ export function parseProofCliArgs(argv, options = {}) {
705
749
  skipInstall,
706
750
  skipRestart,
707
751
  pluginId,
752
+ gatewayUrl,
753
+ gatewayToken,
708
754
  timeoutMs,
709
755
  json,
710
756
  help
@@ -725,6 +771,8 @@ export function parseProofCliArgs(argv, options = {}) {
725
771
  skipInstall,
726
772
  skipRestart,
727
773
  pluginId,
774
+ gatewayUrl,
775
+ gatewayToken,
728
776
  timeoutMs,
729
777
  json,
730
778
  help
@@ -783,7 +831,11 @@ export function captureOperatorProofBundle(options) {
783
831
  }
784
832
  addStep("01-install", "install", cliInvocation.command, [...cliInvocation.args, "install", "--openclaw-home", options.openclawHome], { skipped: options.skipInstall === true });
785
833
  addStep("02-restart", "gateway restart", "openclaw", buildGatewayArgs("restart", gatewayProfile), { skipped: options.skipRestart === true });
786
- const gatewayStatusCapture = addStep("03-gateway-status", "gateway status", "openclaw", buildGatewayArgs("status", gatewayProfile));
834
+ const gatewayStatusCapture = addStep("03-gateway-status", "gateway status", "openclaw", buildGatewayStatusArgs(
835
+ gatewayProfile,
836
+ normalizeOptionalCliString(options.gatewayUrl ?? null),
837
+ normalizeOptionalCliString(options.gatewayToken ?? null),
838
+ ));
787
839
  const pluginInspectCapture = addStep("04-plugin-inspect", "plugin inspect", "openclaw", ["plugins", "inspect", options.pluginId]);
788
840
  const statusCapture = addStep("05-detailed-status", "detailed status", cliInvocation.command, [...cliInvocation.args, "status", "--openclaw-home", options.openclawHome, "--detailed"]);
789
841
  const gatewayLogPath = extractGatewayLogPath(gatewayStatusCapture.stdout);
@@ -15,6 +15,17 @@ export interface TeacherLabelerResultV1 {
15
15
  export interface TeacherLabeler {
16
16
  label(input: TeacherLabelerRunInputV1): Promise<TeacherLabelerResultV1>;
17
17
  }
18
+ export interface TeacherLabelerOpportunityInputV1 {
19
+ normalizedEventExport: NormalizedEventExportV1;
20
+ serveTimeDecisions?: readonly LearningSpineServeRouteDecisionLogEntryV1[];
21
+ }
22
+ export interface TeacherLabelerOpportunityV1 {
23
+ enabled: boolean;
24
+ candidateCount: number;
25
+ budgetedCandidateCount: number;
26
+ status: "disabled" | "ready" | "skipped";
27
+ detail: string;
28
+ }
18
29
  export interface OllamaTeacherLabelerGenerateInputV1 {
19
30
  model: string;
20
31
  prompt: string;
@@ -47,4 +58,5 @@ export interface AsyncTeacherNoopLabelerConfigV1 {
47
58
  export type AsyncTeacherLabelerConfigV1 = AsyncTeacherNoopLabelerConfigV1 | AsyncTeacherOllamaLabelerConfigV1;
48
59
  export declare function createHttpOllamaTeacherLabelerClient(baseUrl?: string): OllamaTeacherLabelerClient;
49
60
  export declare function createOllamaTeacherLabeler(config: AsyncTeacherOllamaLabelerConfigV1): TeacherLabeler;
61
+ export declare function summarizeTeacherLabelerOpportunity(input: TeacherLabelerOpportunityInputV1, config?: AsyncTeacherLabelerConfigV1 | null): TeacherLabelerOpportunityV1;
50
62
  export declare function createTeacherLabeler(config: AsyncTeacherLabelerConfigV1 | null | undefined): TeacherLabeler | null;
@@ -246,6 +246,48 @@ function normalizeOllamaTeacherLabelerConfig(config) {
246
246
  client: config.client ?? createHttpOllamaTeacherLabelerClient(normalizeBaseUrl(config.baseUrl))
247
247
  };
248
248
  }
249
+ export function summarizeTeacherLabelerOpportunity(input, config) {
250
+ const normalized = config === undefined || config === null || config.provider === "none"
251
+ ? {
252
+ enabled: false,
253
+ maxPromptChars: DEFAULT_OLLAMA_MAX_PROMPT_CHARS,
254
+ maxArtifactsPerExport: DEFAULT_OLLAMA_MAX_ARTIFACTS_PER_EXPORT,
255
+ maxInteractionsPerExport: DEFAULT_OLLAMA_MAX_INTERACTIONS,
256
+ maxUserMessageChars: DEFAULT_OLLAMA_MAX_USER_MESSAGE_CHARS,
257
+ maxContextIdsPerDecision: DEFAULT_OLLAMA_MAX_CONTEXT_IDS
258
+ }
259
+ : {
260
+ enabled: true,
261
+ ...normalizeOllamaTeacherLabelerConfig(config)
262
+ };
263
+ const candidates = collectCandidates(input, normalized);
264
+ if (candidates.length === 0) {
265
+ return {
266
+ enabled: normalized.enabled,
267
+ candidateCount: 0,
268
+ budgetedCandidateCount: 0,
269
+ status: normalized.enabled ? "skipped" : "disabled",
270
+ detail: "no_matching_interaction_text"
271
+ };
272
+ }
273
+ const budgetedCandidates = fitCandidatesToPromptBudget(candidates, normalized);
274
+ if (budgetedCandidates.length === 0) {
275
+ return {
276
+ enabled: normalized.enabled,
277
+ candidateCount: candidates.length,
278
+ budgetedCandidateCount: 0,
279
+ status: normalized.enabled ? "skipped" : "disabled",
280
+ detail: "prompt_budget_exhausted"
281
+ };
282
+ }
283
+ return {
284
+ enabled: normalized.enabled,
285
+ candidateCount: candidates.length,
286
+ budgetedCandidateCount: budgetedCandidates.length,
287
+ status: normalized.enabled ? "ready" : "disabled",
288
+ detail: `candidates=${budgetedCandidates.length}`
289
+ };
290
+ }
249
291
  class HttpOllamaTeacherLabelerClient {
250
292
  baseUrl;
251
293
  constructor(baseUrl) {
@@ -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.15",
3
+ "version": "0.4.17",
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",