@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.
- package/README.md +17 -11
- package/dist/extension/index.js +29 -3
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/runtime-guard.d.ts +8 -0
- package/dist/extension/runtime-guard.js +100 -12
- package/dist/extension/runtime-guard.js.map +1 -1
- package/dist/src/attachment-truth.d.ts +32 -22
- package/dist/src/attachment-truth.js +338 -186
- package/dist/src/cli.d.ts +13 -1
- package/dist/src/cli.js +595 -113
- package/dist/src/index.d.ts +242 -3
- package/dist/src/index.js +1029 -38
- package/dist/src/install-converge.js +217 -0
- package/dist/src/learning-spine.d.ts +2 -1
- package/dist/src/learning-spine.js +49 -19
- package/dist/src/local-learner.d.ts +30 -0
- package/dist/src/local-learner.js +298 -179
- package/dist/src/local-session-passive-learning.js +28 -2
- package/dist/src/materialization-embedder.js +11 -0
- package/dist/src/openclaw-hook-truth.d.ts +6 -0
- package/dist/src/openclaw-hook-truth.js +27 -0
- package/dist/src/proof-command.js +301 -42
- package/dist/src/runtime-core.js +658 -0
- package/dist/src/status-learning-path.js +32 -2
- package/dist/src/teacher-decision-match.js +277 -0
- package/dist/src/teacher-labeler.js +4 -30
- package/dist/src/traced-learning-bridge.js +17 -1
- package/extension/index.ts +35 -4
- package/extension/runtime-guard.ts +92 -14
- package/package.json +4 -3
|
@@ -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
|
|
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
|
|
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
|
});
|
package/extension/index.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
64
|
-
|
|
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:
|
|
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.
|
|
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/
|
|
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
|
-
|