@openclawbrain/cli 0.4.14 → 0.4.16

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.
@@ -80,6 +80,14 @@ function readInteractionActivePackGraphChecksum(interaction) {
80
80
  ?? undefined;
81
81
  }
82
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
+
83
91
  function buildDecisionTimestamps(decision) {
84
92
  const timestamps = [];
85
93
  const turnCreatedAt = toTimestamp(decision.turnCreatedAt);
@@ -139,8 +147,10 @@ export function createServeTimeDecisionMatcher(decisions, options = {}) {
139
147
  const decisionsByRecordId = new Map();
140
148
  const decisionsBySelectionDigest = new Map();
141
149
  const ambiguousSelectionDigests = new Set();
142
- const exactDecisions = new Map();
150
+ const decisionsByTurnCompileEventId = new Map();
151
+ const ambiguousTurnCompileEventIds = new Set();
143
152
  const fallbackDecisions = new Map();
153
+ const ambiguousFallbackDecisionKeys = new Set();
144
154
  const decisionsBySessionChannel = new Map();
145
155
  const globalFallbackDecisions = [];
146
156
 
@@ -164,15 +174,27 @@ export function createServeTimeDecisionMatcher(decisions, options = {}) {
164
174
  }
165
175
  }
166
176
  const turnCompileEventId = normalizeOptionalString(decision.turnCompileEventId);
167
- if (turnCompileEventId !== undefined && !exactDecisions.has(turnCompileEventId)) {
168
- exactDecisions.set(turnCompileEventId, decision);
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
+ }
169
185
  }
170
186
  for (const candidateKey of [
171
187
  buildCandidateKey(decision.sessionId, decision.channel, decision.turnCreatedAt),
172
188
  buildCandidateKey(decision.sessionId, decision.channel, decision.recordedAt),
173
189
  ]) {
174
- if (candidateKey !== null && !fallbackDecisions.has(candidateKey)) {
175
- fallbackDecisions.set(candidateKey, decision);
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
+ }
176
198
  }
177
199
  }
178
200
  const sessionChannelKey = buildSessionChannelKey(decision.sessionId, decision.channel);
@@ -203,22 +225,37 @@ export function createServeTimeDecisionMatcher(decisions, options = {}) {
203
225
  if (decisionRecordId !== undefined) {
204
226
  return decisionsByRecordId.get(decisionRecordId) ?? null;
205
227
  }
206
- const selectionDigestKey = buildSelectionDigestKey(
207
- readInteractionSelectionDigest(interaction),
208
- readInteractionActivePackGraphChecksum(interaction),
209
- );
228
+ const interactionSelectionDigest = readInteractionSelectionDigest(interaction);
229
+ const interactionGraphChecksum = readInteractionActivePackGraphChecksum(interaction);
230
+ const selectionDigestKey = buildSelectionDigestKey(interactionSelectionDigest, interactionGraphChecksum);
210
231
  if (selectionDigestKey !== null) {
211
232
  if (ambiguousSelectionDigests.has(selectionDigestKey)) {
212
233
  return null;
213
234
  }
214
235
  return decisionsBySelectionDigest.get(selectionDigestKey) ?? null;
215
236
  }
216
- const exact = exactDecisions.get(interaction.eventId);
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);
217
251
  if (exact !== undefined) {
218
252
  return exact;
219
253
  }
220
254
  const exactFallbackKey = buildCandidateKey(interaction.sessionId, interaction.channel, interaction.createdAt);
221
255
  if (exactFallbackKey !== null) {
256
+ if (ambiguousFallbackDecisionKeys.has(exactFallbackKey)) {
257
+ return null;
258
+ }
222
259
  const fallback = fallbackDecisions.get(exactFallbackKey);
223
260
  if (fallback !== undefined) {
224
261
  return fallback;
@@ -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) {
@@ -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.14",
3
+ "version": "0.4.16",
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",