@openclawbrain/cli 0.4.13 → 0.4.15

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,277 @@
1
+ const DEFAULT_MATCH_WINDOW_MS = 60_000;
2
+ const OPERATIONAL_DECISION_PATTERNS = [
3
+ /^NO_REPLY$/i,
4
+ /^HEARTBEAT_OK$/i,
5
+ /^read heartbeat\.md if it exists\b/i,
6
+ /^a new session was started via \/new or \/reset\./i,
7
+ /\[cron:[a-f0-9-]+\s/i,
8
+ /\[system message\]\s*\[sessionid:/i,
9
+ ];
10
+
11
+ function normalizeOptionalString(value) {
12
+ if (typeof value !== "string") {
13
+ return undefined;
14
+ }
15
+ const trimmed = value.trim();
16
+ return trimmed.length > 0 ? trimmed : undefined;
17
+ }
18
+
19
+ function toTimestamp(value) {
20
+ const normalized = normalizeOptionalString(value);
21
+ if (normalized === undefined) {
22
+ return null;
23
+ }
24
+ const parsed = Date.parse(normalized);
25
+ return Number.isFinite(parsed) ? parsed : null;
26
+ }
27
+
28
+ function buildSessionChannelKey(sessionId, channel) {
29
+ const normalizedSessionId = normalizeOptionalString(sessionId);
30
+ const normalizedChannel = normalizeOptionalString(channel);
31
+ if (normalizedSessionId === undefined || normalizedChannel === undefined) {
32
+ return null;
33
+ }
34
+ return `${normalizedSessionId}|${normalizedChannel}`;
35
+ }
36
+
37
+ function buildCandidateKey(sessionId, channel, createdAt) {
38
+ const sessionChannelKey = buildSessionChannelKey(sessionId, channel);
39
+ const normalizedCreatedAt = normalizeOptionalString(createdAt);
40
+ if (sessionChannelKey === null || normalizedCreatedAt === undefined) {
41
+ return null;
42
+ }
43
+ return `${sessionChannelKey}|${normalizedCreatedAt}`;
44
+ }
45
+
46
+ function buildSelectionDigestKey(selectionDigest, activePackGraphChecksum) {
47
+ const normalizedSelectionDigest = normalizeOptionalString(selectionDigest);
48
+ const normalizedGraphChecksum = normalizeOptionalString(activePackGraphChecksum);
49
+ if (normalizedSelectionDigest === undefined || normalizedGraphChecksum === undefined) {
50
+ return null;
51
+ }
52
+ return `${normalizedGraphChecksum}|${normalizedSelectionDigest}`;
53
+ }
54
+
55
+ function toRecord(value) {
56
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
57
+ }
58
+
59
+ function readInteractionExactDecisionRecordId(interaction) {
60
+ return normalizeOptionalString(interaction?.serveDecisionRecordId)
61
+ ?? normalizeOptionalString(toRecord(interaction?.routeMetadata)?.serveDecisionRecordId)
62
+ ?? normalizeOptionalString(toRecord(interaction?.decisionProvenance)?.serveDecisionRecordId)
63
+ ?? normalizeOptionalString(toRecord(interaction?.metadata)?.serveDecisionRecordId)
64
+ ?? undefined;
65
+ }
66
+
67
+ function readInteractionSelectionDigest(interaction) {
68
+ return normalizeOptionalString(interaction?.selectionDigest)
69
+ ?? normalizeOptionalString(toRecord(interaction?.routeMetadata)?.selectionDigest)
70
+ ?? normalizeOptionalString(toRecord(interaction?.decisionProvenance)?.selectionDigest)
71
+ ?? normalizeOptionalString(toRecord(interaction?.metadata)?.selectionDigest)
72
+ ?? undefined;
73
+ }
74
+
75
+ function readInteractionActivePackGraphChecksum(interaction) {
76
+ return normalizeOptionalString(interaction?.activePackGraphChecksum)
77
+ ?? normalizeOptionalString(toRecord(interaction?.routeMetadata)?.activePackGraphChecksum)
78
+ ?? normalizeOptionalString(toRecord(interaction?.decisionProvenance)?.activePackGraphChecksum)
79
+ ?? normalizeOptionalString(toRecord(interaction?.metadata)?.activePackGraphChecksum)
80
+ ?? undefined;
81
+ }
82
+
83
+ function readInteractionExplicitTurnCompileEventId(interaction) {
84
+ return normalizeOptionalString(interaction?.turnCompileEventId)
85
+ ?? normalizeOptionalString(toRecord(interaction?.routeMetadata)?.turnCompileEventId)
86
+ ?? normalizeOptionalString(toRecord(interaction?.decisionProvenance)?.turnCompileEventId)
87
+ ?? normalizeOptionalString(toRecord(interaction?.metadata)?.turnCompileEventId)
88
+ ?? undefined;
89
+ }
90
+
91
+ function buildDecisionTimestamps(decision) {
92
+ const timestamps = [];
93
+ const turnCreatedAt = toTimestamp(decision.turnCreatedAt);
94
+ const recordedAt = toTimestamp(decision.recordedAt);
95
+ if (turnCreatedAt !== null) {
96
+ timestamps.push(turnCreatedAt);
97
+ }
98
+ if (recordedAt !== null && !timestamps.includes(recordedAt)) {
99
+ timestamps.push(recordedAt);
100
+ }
101
+ return timestamps;
102
+ }
103
+
104
+ function isOperationalDecision(decision) {
105
+ const userMessage = normalizeOptionalString(decision.userMessage);
106
+ if (userMessage === undefined) {
107
+ return true;
108
+ }
109
+ return OPERATIONAL_DECISION_PATTERNS.some((pattern) => pattern.test(userMessage));
110
+ }
111
+
112
+ function selectNearestDecision(entries, interactionAt, maxTimeDeltaMs) {
113
+ const candidates = entries
114
+ .map((entry) => {
115
+ const deltas = entry.timestamps.map((timestamp) => Math.abs(timestamp - interactionAt));
116
+ const bestDelta = deltas.length === 0 ? null : Math.min(...deltas);
117
+ return bestDelta === null || bestDelta > maxTimeDeltaMs
118
+ ? null
119
+ : {
120
+ decision: entry.decision,
121
+ deltaMs: bestDelta,
122
+ recordedAt: toTimestamp(entry.decision.recordedAt) ?? 0,
123
+ };
124
+ })
125
+ .filter((entry) => entry !== null)
126
+ .sort((left, right) => {
127
+ if (left.deltaMs !== right.deltaMs) {
128
+ return left.deltaMs - right.deltaMs;
129
+ }
130
+ return right.recordedAt - left.recordedAt;
131
+ });
132
+ const best = candidates[0] ?? null;
133
+ const runnerUp = candidates[1] ?? null;
134
+ if (best === null) {
135
+ return null;
136
+ }
137
+ if (runnerUp !== null && runnerUp.deltaMs === best.deltaMs && runnerUp.decision !== best.decision) {
138
+ return null;
139
+ }
140
+ return best.decision;
141
+ }
142
+
143
+ export function createServeTimeDecisionMatcher(decisions, options = {}) {
144
+ const maxTimeDeltaMs = Number.isInteger(options.maxTimeDeltaMs) && options.maxTimeDeltaMs >= 0
145
+ ? options.maxTimeDeltaMs
146
+ : DEFAULT_MATCH_WINDOW_MS;
147
+ const decisionsByRecordId = new Map();
148
+ const decisionsBySelectionDigest = new Map();
149
+ const ambiguousSelectionDigests = new Set();
150
+ const decisionsByTurnCompileEventId = new Map();
151
+ const ambiguousTurnCompileEventIds = new Set();
152
+ const fallbackDecisions = new Map();
153
+ const ambiguousFallbackDecisionKeys = new Set();
154
+ const decisionsBySessionChannel = new Map();
155
+ const globalFallbackDecisions = [];
156
+
157
+ for (const decision of [...decisions].sort((left, right) => Date.parse(right.recordedAt) - Date.parse(left.recordedAt))) {
158
+ const userMessage = normalizeOptionalString(decision.userMessage);
159
+ if (userMessage === undefined) {
160
+ continue;
161
+ }
162
+ const decisionRecordId = normalizeOptionalString(decision.recordId);
163
+ if (decisionRecordId !== undefined && !decisionsByRecordId.has(decisionRecordId)) {
164
+ decisionsByRecordId.set(decisionRecordId, decision);
165
+ }
166
+ const selectionDigestKey = buildSelectionDigestKey(decision.selectionDigest, decision.activePackGraphChecksum);
167
+ if (selectionDigestKey !== null) {
168
+ if (decisionsBySelectionDigest.has(selectionDigestKey)) {
169
+ decisionsBySelectionDigest.delete(selectionDigestKey);
170
+ ambiguousSelectionDigests.add(selectionDigestKey);
171
+ }
172
+ else if (!ambiguousSelectionDigests.has(selectionDigestKey)) {
173
+ decisionsBySelectionDigest.set(selectionDigestKey, decision);
174
+ }
175
+ }
176
+ const turnCompileEventId = normalizeOptionalString(decision.turnCompileEventId);
177
+ if (turnCompileEventId !== undefined) {
178
+ if (decisionsByTurnCompileEventId.has(turnCompileEventId)) {
179
+ decisionsByTurnCompileEventId.delete(turnCompileEventId);
180
+ ambiguousTurnCompileEventIds.add(turnCompileEventId);
181
+ }
182
+ else if (!ambiguousTurnCompileEventIds.has(turnCompileEventId)) {
183
+ decisionsByTurnCompileEventId.set(turnCompileEventId, decision);
184
+ }
185
+ }
186
+ for (const candidateKey of [
187
+ buildCandidateKey(decision.sessionId, decision.channel, decision.turnCreatedAt),
188
+ buildCandidateKey(decision.sessionId, decision.channel, decision.recordedAt),
189
+ ]) {
190
+ if (candidateKey !== null) {
191
+ if (fallbackDecisions.has(candidateKey)) {
192
+ fallbackDecisions.delete(candidateKey);
193
+ ambiguousFallbackDecisionKeys.add(candidateKey);
194
+ }
195
+ else if (!ambiguousFallbackDecisionKeys.has(candidateKey)) {
196
+ fallbackDecisions.set(candidateKey, decision);
197
+ }
198
+ }
199
+ }
200
+ const sessionChannelKey = buildSessionChannelKey(decision.sessionId, decision.channel);
201
+ if (sessionChannelKey === null) {
202
+ if (!isOperationalDecision(decision)) {
203
+ globalFallbackDecisions.push({
204
+ decision,
205
+ timestamps: buildDecisionTimestamps(decision),
206
+ });
207
+ }
208
+ continue;
209
+ }
210
+ const indexedEntry = {
211
+ decision,
212
+ timestamps: buildDecisionTimestamps(decision),
213
+ operational: isOperationalDecision(decision),
214
+ };
215
+ const indexed = decisionsBySessionChannel.get(sessionChannelKey) ?? [];
216
+ indexed.push(indexedEntry);
217
+ decisionsBySessionChannel.set(sessionChannelKey, indexed);
218
+ if (!indexedEntry.operational) {
219
+ globalFallbackDecisions.push(indexedEntry);
220
+ }
221
+ }
222
+
223
+ return (interaction) => {
224
+ const decisionRecordId = readInteractionExactDecisionRecordId(interaction);
225
+ if (decisionRecordId !== undefined) {
226
+ return decisionsByRecordId.get(decisionRecordId) ?? null;
227
+ }
228
+ const interactionSelectionDigest = readInteractionSelectionDigest(interaction);
229
+ const interactionGraphChecksum = readInteractionActivePackGraphChecksum(interaction);
230
+ const selectionDigestKey = buildSelectionDigestKey(interactionSelectionDigest, interactionGraphChecksum);
231
+ if (selectionDigestKey !== null) {
232
+ if (ambiguousSelectionDigests.has(selectionDigestKey)) {
233
+ return null;
234
+ }
235
+ return decisionsBySelectionDigest.get(selectionDigestKey) ?? null;
236
+ }
237
+ if (interactionSelectionDigest !== undefined || interactionGraphChecksum !== undefined) {
238
+ return null;
239
+ }
240
+ const explicitTurnCompileEventId = readInteractionExplicitTurnCompileEventId(interaction);
241
+ if (explicitTurnCompileEventId !== undefined) {
242
+ if (ambiguousTurnCompileEventIds.has(explicitTurnCompileEventId)) {
243
+ return null;
244
+ }
245
+ return decisionsByTurnCompileEventId.get(explicitTurnCompileEventId) ?? null;
246
+ }
247
+ const softTurnCompileEventId = normalizeOptionalString(interaction.eventId);
248
+ const exact = softTurnCompileEventId === undefined || ambiguousTurnCompileEventIds.has(softTurnCompileEventId)
249
+ ? undefined
250
+ : decisionsByTurnCompileEventId.get(softTurnCompileEventId);
251
+ if (exact !== undefined) {
252
+ return exact;
253
+ }
254
+ const exactFallbackKey = buildCandidateKey(interaction.sessionId, interaction.channel, interaction.createdAt);
255
+ if (exactFallbackKey !== null) {
256
+ if (ambiguousFallbackDecisionKeys.has(exactFallbackKey)) {
257
+ return null;
258
+ }
259
+ const fallback = fallbackDecisions.get(exactFallbackKey);
260
+ if (fallback !== undefined) {
261
+ return fallback;
262
+ }
263
+ }
264
+ const interactionAt = toTimestamp(interaction.createdAt);
265
+ const sessionChannelKey = buildSessionChannelKey(interaction.sessionId, interaction.channel);
266
+ if (interactionAt === null) {
267
+ return null;
268
+ }
269
+ if (sessionChannelKey !== null) {
270
+ const sessionMatch = selectNearestDecision((decisionsBySessionChannel.get(sessionChannelKey) ?? []).filter((entry) => entry.operational !== true), interactionAt, maxTimeDeltaMs);
271
+ if (sessionMatch !== null) {
272
+ return sessionMatch;
273
+ }
274
+ }
275
+ return selectNearestDecision(globalFallbackDecisions, interactionAt, maxTimeDeltaMs);
276
+ };
277
+ }
@@ -1,5 +1,6 @@
1
1
  import { CONTRACT_IDS, checksumJsonPayload, validateTeacherSupervisionArtifact } from "@openclawbrain/contracts";
2
2
  import { buildNormalizedEventDedupId } from "@openclawbrain/event-export";
3
+ import { createServeTimeDecisionMatcher } from "./teacher-decision-match.js";
3
4
  const FEEDBACK_KINDS = new Set(["correction", "teaching", "approval", "suppression"]);
4
5
  const OLLAMA_ROUTE_DECISION_STREAM = "openclaw/learning-spine/serve-time-route-decisions";
5
6
  const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
@@ -140,41 +141,14 @@ function buildPrompt(candidates, config) {
140
141
  payload
141
142
  ].join("\n");
142
143
  }
143
- function buildCandidateKey(interaction) {
144
- return `${interaction.sessionId}|${interaction.channel}|${interaction.createdAt}`;
145
- }
146
- function matchServeTimeDecision(interaction, exactDecisions, fallbackDecisions) {
147
- const exact = exactDecisions.get(interaction.eventId);
148
- if (exact !== undefined) {
149
- return exact;
150
- }
151
- return fallbackDecisions.get(buildCandidateKey(interaction)) ?? null;
152
- }
153
144
  function collectCandidates(input, config) {
154
145
  const decisions = [...(input.serveTimeDecisions ?? [])].sort((left, right) => Date.parse(right.recordedAt) - Date.parse(left.recordedAt));
155
- const exactDecisions = new Map();
156
- const fallbackDecisions = new Map();
157
- for (const decision of decisions) {
158
- const userMessage = normalizeOptionalString(decision.userMessage);
159
- if (userMessage === undefined) {
160
- continue;
161
- }
162
- if (decision.turnCompileEventId !== null && !exactDecisions.has(decision.turnCompileEventId)) {
163
- exactDecisions.set(decision.turnCompileEventId, decision);
164
- }
165
- const turnCreatedAt = normalizeOptionalString(decision.turnCreatedAt);
166
- if (turnCreatedAt !== undefined && decision.sessionId !== null && decision.channel !== null) {
167
- const key = `${decision.sessionId}|${decision.channel}|${turnCreatedAt}`;
168
- if (!fallbackDecisions.has(key)) {
169
- fallbackDecisions.set(key, decision);
170
- }
171
- }
172
- }
146
+ const matchServeTimeDecision = createServeTimeDecisionMatcher(decisions);
173
147
  return input.normalizedEventExport.interactionEvents
174
148
  .filter((interaction) => interaction.kind === "memory_compiled")
175
149
  .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
176
150
  .map((interaction) => {
177
- const decision = matchServeTimeDecision(interaction, exactDecisions, fallbackDecisions);
151
+ const decision = matchServeTimeDecision(interaction);
178
152
  const userMessage = normalizeOptionalString(decision?.userMessage);
179
153
  if (decision === null || userMessage === undefined) {
180
154
  return null;
@@ -421,4 +395,4 @@ export function createTeacherLabeler(config) {
421
395
  }
422
396
  return createOllamaTeacherLabeler(config);
423
397
  }
424
- //# sourceMappingURL=teacher-labeler.js.map
398
+ //# sourceMappingURL=teacher-labeler.js.map
@@ -18,6 +18,22 @@ function normalizeOptionalString(value) {
18
18
  function normalizeSource(value) {
19
19
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
20
20
  }
21
+ function summarizeBridgeSource(value) {
22
+ const source = normalizeSource(value);
23
+ if (source === null) {
24
+ return null;
25
+ }
26
+ const summarized = {
27
+ command: normalizeOptionalString(source.command),
28
+ bridge: normalizeOptionalString(source.bridge),
29
+ brainRoot: normalizeOptionalString(source.brainRoot),
30
+ stateDbPath: normalizeOptionalString(source.stateDbPath),
31
+ persistedKey: normalizeOptionalString(source.persistedKey),
32
+ candidatePackVersion: Number.isFinite(source.candidatePackVersion) ? Math.trunc(source.candidatePackVersion) : undefined,
33
+ candidateUpdateCount: normalizeCount(source.candidateUpdateCount)
34
+ };
35
+ return Object.fromEntries(Object.entries(summarized).filter(([, candidate]) => candidate !== null && candidate !== undefined));
36
+ }
21
37
  function normalizeBridgePayload(payload) {
22
38
  if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
23
39
  throw new Error("expected traced-learning bridge payload object");
@@ -579,7 +595,7 @@ export function mergeTracedLearningBridgePayload(payload, persisted) {
579
595
  supervisionCount: persistedBridge.supervisionCount,
580
596
  routerUpdateCount: persistedBridge.routerUpdateCount,
581
597
  teacherArtifactCount: persistedBridge.teacherArtifactCount,
582
- source: persistedBridge.source
598
+ source: summarizeBridgeSource(persistedBridge.source)
583
599
  }
584
600
  }
585
601
  });
@@ -18,6 +18,7 @@ import {
18
18
  } from "@openclawbrain/openclaw";
19
19
  import {
20
20
  createBeforePromptBuildHandler,
21
+ type ExtensionDiagnostic,
21
22
  isActivationRootPlaceholder,
22
23
  validateExtensionRegistrationApi
23
24
  } from "./runtime-guard.js";
@@ -52,7 +53,7 @@ async function appendLocalDiagnosticLog(message: string): Promise<void> {
52
53
  }
53
54
  }
54
55
 
55
- async function reportDiagnostic(input: { key: string; message: string; once?: boolean }): Promise<void> {
56
+ async function reportDiagnostic(input: ExtensionDiagnostic): Promise<void> {
56
57
  if (input.once) {
57
58
  if (warnedDiagnostics.has(input.key)) {
58
59
  return;
@@ -60,8 +61,30 @@ async function reportDiagnostic(input: { key: string; message: string; once?: bo
60
61
  warnedDiagnostics.add(input.key);
61
62
  }
62
63
 
63
- console.warn(input.message);
64
- await appendLocalDiagnosticLog(input.message);
64
+ const formatted = formatDiagnosticMessage(input);
65
+ console.warn(formatted);
66
+ await appendLocalDiagnosticLog(formatted);
67
+ }
68
+
69
+ function formatDiagnosticMessage(input: ExtensionDiagnostic): string {
70
+ if (
71
+ input.severity === undefined ||
72
+ input.actionability === undefined ||
73
+ input.summary === undefined ||
74
+ input.action === undefined
75
+ ) {
76
+ return input.message;
77
+ }
78
+
79
+ const detail = input.message.replace(/^\[openclawbrain\]\s*/, "");
80
+ return [
81
+ "[openclawbrain]",
82
+ `severity=${input.severity}`,
83
+ `actionability=${input.actionability}`,
84
+ `summary=${JSON.stringify(input.summary)}`,
85
+ `action=${JSON.stringify(input.action)}`,
86
+ `detail=${JSON.stringify(detail)}`
87
+ ].join(" ");
65
88
  }
66
89
 
67
90
  function announceStartupBreadcrumb(): void {
@@ -107,8 +130,12 @@ export default function register(api: unknown) {
107
130
  } catch (error) {
108
131
  const detail = error instanceof Error ? error.message : String(error);
109
132
  void reportDiagnostic({
110
- key: `runtime-load-proof:${detail}`,
133
+ key: "runtime-load-proof-failed",
111
134
  once: true,
135
+ severity: "degraded",
136
+ actionability: "inspect_local_proof_write",
137
+ summary: "runtime-load proof write failed after hook registration",
138
+ action: "Inspect local filesystem permissions and the activation-root proof path if proof capture is expected.",
112
139
  message: `[openclawbrain] runtime load proof failed: ${detail}`
113
140
  });
114
141
  }
@@ -119,6 +146,10 @@ export default function register(api: unknown) {
119
146
  void reportDiagnostic({
120
147
  key: "registration-failed",
121
148
  once: true,
149
+ severity: "blocking",
150
+ actionability: "rerun_install",
151
+ summary: "extension registration threw before the runtime hook was fully attached",
152
+ action: "Rerun openclawbrain install --openclaw-home <path>; if it still fails, inspect the extension loader/runtime.",
122
153
  message: `[openclawbrain] extension registration failed: ${detail}`
123
154
  });
124
155
  }
@@ -27,10 +27,23 @@ export type ExtensionCompileResult = ExtensionCompileSuccess | ExtensionCompileF
27
27
 
28
28
  export type ExtensionCompileRuntimeContext = (input: ExtensionCompileInput) => ExtensionCompileResult;
29
29
 
30
+ export type ExtensionDiagnosticSeverity = "degraded" | "blocking";
31
+
32
+ export type ExtensionDiagnosticActionability =
33
+ | "inspect_host_event_shape"
34
+ | "inspect_host_registration_api"
35
+ | "inspect_local_proof_write"
36
+ | "inspect_runtime_compile"
37
+ | "rerun_install";
38
+
30
39
  export interface ExtensionDiagnostic {
31
40
  key: string;
32
41
  message: string;
33
42
  once?: boolean;
43
+ severity?: ExtensionDiagnosticSeverity;
44
+ actionability?: ExtensionDiagnosticActionability;
45
+ summary?: string;
46
+ action?: string;
34
47
  }
35
48
 
36
49
  export interface ExtensionRegistrationApi {
@@ -53,13 +66,13 @@ export function validateExtensionRegistrationApi(api: unknown): { ok: true; api:
53
66
  if (!isRecord(api) || typeof api.on !== "function") {
54
67
  return {
55
68
  ok: false,
56
- diagnostic: {
69
+ diagnostic: shapeDiagnostic({
57
70
  key: "registration-api-invalid",
58
71
  once: true,
59
72
  message:
60
73
  `[openclawbrain] extension inactive: host registration API is missing api.on(event, handler, options) ` +
61
74
  `(received=${describeValue(api)})`
62
- }
75
+ })
63
76
  };
64
77
  }
65
78
 
@@ -145,12 +158,12 @@ export function createBeforePromptBuildHandler(input: {
145
158
  }): (event: unknown, ctx: unknown) => Promise<Record<string, unknown>> {
146
159
  return async (event: unknown, _ctx: unknown) => {
147
160
  if (isActivationRootPlaceholder(input.activationRoot)) {
148
- await input.reportDiagnostic({
161
+ await input.reportDiagnostic(shapeDiagnostic({
149
162
  key: "activation-root-placeholder",
150
163
  once: true,
151
164
  message:
152
165
  "[openclawbrain] BRAIN NOT YET LOADED: ACTIVATION_ROOT is still a placeholder. Install @openclawbrain/cli, then run: openclawbrain install --openclaw-home <path>"
153
- });
166
+ }));
154
167
  return {};
155
168
  }
156
169
 
@@ -190,12 +203,12 @@ export function createBeforePromptBuildHandler(input: {
190
203
 
191
204
  if (!result.ok) {
192
205
  const mode = result.hardRequirementViolated ? "hard-fail" : "fail-open";
193
- await input.reportDiagnostic({
206
+ await input.reportDiagnostic(shapeDiagnostic({
194
207
  key: `compile-${mode}`,
195
208
  message:
196
209
  `[openclawbrain] ${mode}: ${result.error} ` +
197
210
  `(activationRoot=${input.activationRoot}, sessionId=${normalized.event.sessionId ?? "unknown"}, channel=${normalized.event.channel ?? "unknown"})`
198
- });
211
+ }));
199
212
  return {};
200
213
  }
201
214
 
@@ -207,12 +220,12 @@ export function createBeforePromptBuildHandler(input: {
207
220
  }
208
221
  } catch (error) {
209
222
  const detail = error instanceof Error ? error.stack ?? error.message : String(error);
210
- await input.reportDiagnostic({
223
+ await input.reportDiagnostic(shapeDiagnostic({
211
224
  key: "compile-threw",
212
225
  message:
213
226
  `[openclawbrain] compile threw: ${detail} ` +
214
227
  `(activationRoot=${input.activationRoot}, sessionId=${normalized.event.sessionId ?? "unknown"}, channel=${normalized.event.channel ?? "unknown"})`
215
- });
228
+ }));
216
229
  }
217
230
 
218
231
  return {};
@@ -220,10 +233,10 @@ export function createBeforePromptBuildHandler(input: {
220
233
  }
221
234
 
222
235
  function failOpenDiagnostic(key: string, reason: string, detail: string): ExtensionDiagnostic {
223
- return {
236
+ return shapeDiagnostic({
224
237
  key,
225
238
  message: `[openclawbrain] fail-open: ${reason} (${detail})`
226
- };
239
+ });
227
240
  }
228
241
 
229
242
  function normalizeOptionalScalarField(
@@ -244,12 +257,12 @@ function normalizeOptionalScalarField(
244
257
  return String(value);
245
258
  }
246
259
 
247
- warnings.push({
260
+ warnings.push(shapeDiagnostic({
248
261
  key: `runtime-${fieldName}-ignored`,
249
262
  message:
250
263
  `[openclawbrain] fail-open: ignored unsupported before_prompt_build ${fieldName} ` +
251
264
  `(${fieldName}=${describeValue(value)})`
252
- });
265
+ }));
253
266
 
254
267
  return undefined;
255
268
  }
@@ -284,12 +297,12 @@ function normalizeOptionalNonNegativeIntegerField(
284
297
  }
285
298
  }
286
299
 
287
- warnings.push({
300
+ warnings.push(shapeDiagnostic({
288
301
  key: `runtime-${fieldName}-ignored`,
289
302
  message:
290
303
  `[openclawbrain] fail-open: ignored unsupported before_prompt_build ${fieldName} ` +
291
304
  `(${fieldName}=${describeValue(value)})`
292
- });
305
+ }));
293
306
 
294
307
  return undefined;
295
308
  }
@@ -402,6 +415,71 @@ function describeValue(value: unknown): string {
402
415
  return `${typeof value}(${String(value)})`;
403
416
  }
404
417
 
418
+ function shapeDiagnostic(diagnostic: ExtensionDiagnostic): ExtensionDiagnostic {
419
+ if (
420
+ diagnostic.severity !== undefined &&
421
+ diagnostic.actionability !== undefined &&
422
+ diagnostic.summary !== undefined &&
423
+ diagnostic.action !== undefined
424
+ ) {
425
+ return diagnostic;
426
+ }
427
+
428
+ if (diagnostic.key === "activation-root-placeholder") {
429
+ return {
430
+ ...diagnostic,
431
+ severity: "blocking",
432
+ actionability: "rerun_install",
433
+ summary: "extension hook is installed but ACTIVATION_ROOT is still unpinned",
434
+ action: "Run openclawbrain install --openclaw-home <path> to pin the runtime hook."
435
+ };
436
+ }
437
+
438
+ if (diagnostic.key === "registration-api-invalid") {
439
+ return {
440
+ ...diagnostic,
441
+ severity: "blocking",
442
+ actionability: "inspect_host_registration_api",
443
+ summary: "extension host registration API is missing or incompatible",
444
+ action: "Repair or upgrade the host extension API so api.on(event, handler, options) is available."
445
+ };
446
+ }
447
+
448
+ if (diagnostic.key === "compile-hard-fail") {
449
+ return {
450
+ ...diagnostic,
451
+ severity: "blocking",
452
+ actionability: "inspect_runtime_compile",
453
+ summary: "brain context compile hit a hard requirement",
454
+ action: "Inspect the activation root and compile error; rerun install if the pinned hook may be stale."
455
+ };
456
+ }
457
+
458
+ if (diagnostic.key === "compile-fail-open" || diagnostic.key === "compile-threw") {
459
+ return {
460
+ ...diagnostic,
461
+ severity: "degraded",
462
+ actionability: "inspect_runtime_compile",
463
+ summary: diagnostic.key === "compile-threw"
464
+ ? "brain context compile threw during before_prompt_build"
465
+ : "brain context compile failed open during before_prompt_build",
466
+ action: "Inspect the activation root and compile error if brain context is unexpectedly empty."
467
+ };
468
+ }
469
+
470
+ if (diagnostic.key.startsWith("runtime-")) {
471
+ return {
472
+ ...diagnostic,
473
+ severity: "degraded",
474
+ actionability: "inspect_host_event_shape",
475
+ summary: "before_prompt_build payload was partial or malformed",
476
+ action: "Inspect the host before_prompt_build event shape; OpenClawBrain fail-opened safely."
477
+ };
478
+ }
479
+
480
+ return diagnostic;
481
+ }
482
+
405
483
  function isRecord(value: unknown): value is Record<string, unknown> {
406
484
  return typeof value === "object" && value !== null;
407
485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclawbrain/cli",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
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",
@@ -48,9 +48,11 @@
48
48
  "@openclawbrain/compiler": "0.3.5",
49
49
  "@openclawbrain/contracts": "^0.3.5",
50
50
  "@openclawbrain/events": "^0.3.4",
51
+ "@openclawbrain/event-export": "^0.3.4",
51
52
  "@openclawbrain/learner": "^0.3.4",
52
53
  "@openclawbrain/pack-format": "^0.3.4",
53
- "@openclawbrain/event-export": "^0.3.4"
54
+ "@openclawbrain/provenance": "^0.3.4",
55
+ "@openclawbrain/workspace-metadata": "^0.3.4"
54
56
  },
55
57
  "bin": {
56
58
  "openclawbrain": "dist/src/cli.js",
@@ -62,4 +64,3 @@
62
64
  "test": "node --test dist/test/*.test.js"
63
65
  }
64
66
  }
65
-