@openclawbrain/cli 0.4.13 → 0.4.14

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,240 @@
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 buildDecisionTimestamps(decision) {
84
+ const timestamps = [];
85
+ const turnCreatedAt = toTimestamp(decision.turnCreatedAt);
86
+ const recordedAt = toTimestamp(decision.recordedAt);
87
+ if (turnCreatedAt !== null) {
88
+ timestamps.push(turnCreatedAt);
89
+ }
90
+ if (recordedAt !== null && !timestamps.includes(recordedAt)) {
91
+ timestamps.push(recordedAt);
92
+ }
93
+ return timestamps;
94
+ }
95
+
96
+ function isOperationalDecision(decision) {
97
+ const userMessage = normalizeOptionalString(decision.userMessage);
98
+ if (userMessage === undefined) {
99
+ return true;
100
+ }
101
+ return OPERATIONAL_DECISION_PATTERNS.some((pattern) => pattern.test(userMessage));
102
+ }
103
+
104
+ function selectNearestDecision(entries, interactionAt, maxTimeDeltaMs) {
105
+ const candidates = entries
106
+ .map((entry) => {
107
+ const deltas = entry.timestamps.map((timestamp) => Math.abs(timestamp - interactionAt));
108
+ const bestDelta = deltas.length === 0 ? null : Math.min(...deltas);
109
+ return bestDelta === null || bestDelta > maxTimeDeltaMs
110
+ ? null
111
+ : {
112
+ decision: entry.decision,
113
+ deltaMs: bestDelta,
114
+ recordedAt: toTimestamp(entry.decision.recordedAt) ?? 0,
115
+ };
116
+ })
117
+ .filter((entry) => entry !== null)
118
+ .sort((left, right) => {
119
+ if (left.deltaMs !== right.deltaMs) {
120
+ return left.deltaMs - right.deltaMs;
121
+ }
122
+ return right.recordedAt - left.recordedAt;
123
+ });
124
+ const best = candidates[0] ?? null;
125
+ const runnerUp = candidates[1] ?? null;
126
+ if (best === null) {
127
+ return null;
128
+ }
129
+ if (runnerUp !== null && runnerUp.deltaMs === best.deltaMs && runnerUp.decision !== best.decision) {
130
+ return null;
131
+ }
132
+ return best.decision;
133
+ }
134
+
135
+ export function createServeTimeDecisionMatcher(decisions, options = {}) {
136
+ const maxTimeDeltaMs = Number.isInteger(options.maxTimeDeltaMs) && options.maxTimeDeltaMs >= 0
137
+ ? options.maxTimeDeltaMs
138
+ : DEFAULT_MATCH_WINDOW_MS;
139
+ const decisionsByRecordId = new Map();
140
+ const decisionsBySelectionDigest = new Map();
141
+ const ambiguousSelectionDigests = new Set();
142
+ const exactDecisions = new Map();
143
+ const fallbackDecisions = new Map();
144
+ const decisionsBySessionChannel = new Map();
145
+ const globalFallbackDecisions = [];
146
+
147
+ for (const decision of [...decisions].sort((left, right) => Date.parse(right.recordedAt) - Date.parse(left.recordedAt))) {
148
+ const userMessage = normalizeOptionalString(decision.userMessage);
149
+ if (userMessage === undefined) {
150
+ continue;
151
+ }
152
+ const decisionRecordId = normalizeOptionalString(decision.recordId);
153
+ if (decisionRecordId !== undefined && !decisionsByRecordId.has(decisionRecordId)) {
154
+ decisionsByRecordId.set(decisionRecordId, decision);
155
+ }
156
+ const selectionDigestKey = buildSelectionDigestKey(decision.selectionDigest, decision.activePackGraphChecksum);
157
+ if (selectionDigestKey !== null) {
158
+ if (decisionsBySelectionDigest.has(selectionDigestKey)) {
159
+ decisionsBySelectionDigest.delete(selectionDigestKey);
160
+ ambiguousSelectionDigests.add(selectionDigestKey);
161
+ }
162
+ else if (!ambiguousSelectionDigests.has(selectionDigestKey)) {
163
+ decisionsBySelectionDigest.set(selectionDigestKey, decision);
164
+ }
165
+ }
166
+ const turnCompileEventId = normalizeOptionalString(decision.turnCompileEventId);
167
+ if (turnCompileEventId !== undefined && !exactDecisions.has(turnCompileEventId)) {
168
+ exactDecisions.set(turnCompileEventId, decision);
169
+ }
170
+ for (const candidateKey of [
171
+ buildCandidateKey(decision.sessionId, decision.channel, decision.turnCreatedAt),
172
+ buildCandidateKey(decision.sessionId, decision.channel, decision.recordedAt),
173
+ ]) {
174
+ if (candidateKey !== null && !fallbackDecisions.has(candidateKey)) {
175
+ fallbackDecisions.set(candidateKey, decision);
176
+ }
177
+ }
178
+ const sessionChannelKey = buildSessionChannelKey(decision.sessionId, decision.channel);
179
+ if (sessionChannelKey === null) {
180
+ if (!isOperationalDecision(decision)) {
181
+ globalFallbackDecisions.push({
182
+ decision,
183
+ timestamps: buildDecisionTimestamps(decision),
184
+ });
185
+ }
186
+ continue;
187
+ }
188
+ const indexedEntry = {
189
+ decision,
190
+ timestamps: buildDecisionTimestamps(decision),
191
+ operational: isOperationalDecision(decision),
192
+ };
193
+ const indexed = decisionsBySessionChannel.get(sessionChannelKey) ?? [];
194
+ indexed.push(indexedEntry);
195
+ decisionsBySessionChannel.set(sessionChannelKey, indexed);
196
+ if (!indexedEntry.operational) {
197
+ globalFallbackDecisions.push(indexedEntry);
198
+ }
199
+ }
200
+
201
+ return (interaction) => {
202
+ const decisionRecordId = readInteractionExactDecisionRecordId(interaction);
203
+ if (decisionRecordId !== undefined) {
204
+ return decisionsByRecordId.get(decisionRecordId) ?? null;
205
+ }
206
+ const selectionDigestKey = buildSelectionDigestKey(
207
+ readInteractionSelectionDigest(interaction),
208
+ readInteractionActivePackGraphChecksum(interaction),
209
+ );
210
+ if (selectionDigestKey !== null) {
211
+ if (ambiguousSelectionDigests.has(selectionDigestKey)) {
212
+ return null;
213
+ }
214
+ return decisionsBySelectionDigest.get(selectionDigestKey) ?? null;
215
+ }
216
+ const exact = exactDecisions.get(interaction.eventId);
217
+ if (exact !== undefined) {
218
+ return exact;
219
+ }
220
+ const exactFallbackKey = buildCandidateKey(interaction.sessionId, interaction.channel, interaction.createdAt);
221
+ if (exactFallbackKey !== null) {
222
+ const fallback = fallbackDecisions.get(exactFallbackKey);
223
+ if (fallback !== undefined) {
224
+ return fallback;
225
+ }
226
+ }
227
+ const interactionAt = toTimestamp(interaction.createdAt);
228
+ const sessionChannelKey = buildSessionChannelKey(interaction.sessionId, interaction.channel);
229
+ if (interactionAt === null) {
230
+ return null;
231
+ }
232
+ if (sessionChannelKey !== null) {
233
+ const sessionMatch = selectNearestDecision((decisionsBySessionChannel.get(sessionChannelKey) ?? []).filter((entry) => entry.operational !== true), interactionAt, maxTimeDeltaMs);
234
+ if (sessionMatch !== null) {
235
+ return sessionMatch;
236
+ }
237
+ }
238
+ return selectNearestDecision(globalFallbackDecisions, interactionAt, maxTimeDeltaMs);
239
+ };
240
+ }
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclawbrain/cli",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
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
-