nemo-relay-openclaw 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/hook-replay/llm.js +64 -31
- package/dist/src/hook-replay/marks.d.ts +1 -0
- package/dist/src/hook-replay/marks.js +1 -1
- package/dist/src/hook-replay/session.d.ts +27 -9
- package/dist/src/hook-replay/session.js +146 -46
- package/dist/src/hook-replay/tool.js +18 -3
- package/dist/src/hooks-backend.d.ts +27 -3
- package/dist/src/hooks-backend.js +230 -24
- package/package.json +2 -2
- package/src/hook-replay/llm.ts +70 -31
- package/src/hook-replay/marks.ts +2 -1
- package/src/hook-replay/session.ts +178 -53
- package/src/hook-replay/tool.ts +18 -3
- package/src/hooks-backend.ts +283 -21
package/src/hook-replay/llm.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
evictExpiredCorrelationRecords,
|
|
29
29
|
ensureSession,
|
|
30
30
|
insertBoundedRecord,
|
|
31
|
-
|
|
31
|
+
resolveSessionOwnerKey,
|
|
32
32
|
type LlmInputRecord,
|
|
33
33
|
type ModelCallRecord,
|
|
34
34
|
type PendingLlmOutputRecord,
|
|
@@ -47,12 +47,14 @@ export function recordLlmInput(
|
|
|
47
47
|
ctx: PluginHookAgentContext,
|
|
48
48
|
): void {
|
|
49
49
|
evictExpiredReplayRecords(manager);
|
|
50
|
+
const observedAtMicros = nowMicros();
|
|
50
51
|
const session = ensureSession(manager, {
|
|
51
52
|
sessionId: event.sessionId,
|
|
52
53
|
sessionKey: ctx.sessionKey,
|
|
53
54
|
runId: event.runId,
|
|
54
55
|
agentId: ctx.agentId,
|
|
55
56
|
source: 'lazy_session',
|
|
57
|
+
timestamp: observedAtMicros,
|
|
56
58
|
});
|
|
57
59
|
if (!session) {
|
|
58
60
|
return;
|
|
@@ -76,7 +78,7 @@ export function recordLlmInput(
|
|
|
76
78
|
const pending = shiftOldest(
|
|
77
79
|
manager.state.llmOutputsPendingInput,
|
|
78
80
|
key,
|
|
79
|
-
(record) => record.
|
|
81
|
+
(record) => record.sessionOwnerKey === session.ownerKey,
|
|
80
82
|
);
|
|
81
83
|
if (!pending) {
|
|
82
84
|
return;
|
|
@@ -105,12 +107,14 @@ export function recordLlmOutput(
|
|
|
105
107
|
ctx: PluginHookAgentContext,
|
|
106
108
|
): void {
|
|
107
109
|
evictExpiredReplayRecords(manager);
|
|
110
|
+
const observedAtMicros = nowMicros();
|
|
108
111
|
const session = ensureSession(manager, {
|
|
109
112
|
sessionId: event.sessionId,
|
|
110
113
|
sessionKey: ctx.sessionKey,
|
|
111
114
|
runId: event.runId,
|
|
112
115
|
agentId: ctx.agentId,
|
|
113
116
|
source: 'lazy_session',
|
|
117
|
+
timestamp: observedAtMicros,
|
|
114
118
|
});
|
|
115
119
|
if (!session) {
|
|
116
120
|
return;
|
|
@@ -118,11 +122,11 @@ export function recordLlmOutput(
|
|
|
118
122
|
|
|
119
123
|
const key = llmKey(event);
|
|
120
124
|
if (hasTrajectoryReplay(session, event.runId)) {
|
|
121
|
-
shiftOldest(manager.state.llmInputs, key, (record) => record.
|
|
125
|
+
shiftOldest(manager.state.llmInputs, key, (record) => record.sessionOwnerKey === session.ownerKey);
|
|
122
126
|
return;
|
|
123
127
|
}
|
|
124
128
|
|
|
125
|
-
const input = shiftOldest(manager.state.llmInputs, key, (record) => record.
|
|
129
|
+
const input = shiftOldest(manager.state.llmInputs, key, (record) => record.sessionOwnerKey === session.ownerKey);
|
|
126
130
|
if (input) {
|
|
127
131
|
replayLlmOutput({
|
|
128
132
|
manager,
|
|
@@ -135,7 +139,7 @@ export function recordLlmOutput(
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
const pending: PendingLlmOutputRecord = {
|
|
138
|
-
|
|
142
|
+
sessionOwnerKey: session.ownerKey,
|
|
139
143
|
sessionId: event.sessionId,
|
|
140
144
|
runId: event.runId,
|
|
141
145
|
provider: event.provider,
|
|
@@ -191,7 +195,7 @@ export function recordBeforeMessageWrite(
|
|
|
191
195
|
if (provider && model && (assistantTexts.length > 0 || assistantToolCalls.length > 0 || usage !== undefined)) {
|
|
192
196
|
session.assistantMessageWrites ??= [];
|
|
193
197
|
session.assistantMessageWrites.push({
|
|
194
|
-
|
|
198
|
+
sessionOwnerKey: session.ownerKey,
|
|
195
199
|
provider,
|
|
196
200
|
model,
|
|
197
201
|
assistantTexts,
|
|
@@ -239,7 +243,7 @@ export function recordModelCallStarted(
|
|
|
239
243
|
manager.state.modelCallsByCallId,
|
|
240
244
|
modelTimingKey(event),
|
|
241
245
|
{
|
|
242
|
-
|
|
246
|
+
sessionOwnerKey: session.ownerKey,
|
|
243
247
|
sessionId: session.sessionId,
|
|
244
248
|
runId: event.runId,
|
|
245
249
|
callId: event.callId,
|
|
@@ -281,7 +285,7 @@ export function recordModelCallEnded(
|
|
|
281
285
|
const record =
|
|
282
286
|
existing ??
|
|
283
287
|
({
|
|
284
|
-
|
|
288
|
+
sessionOwnerKey: session.ownerKey,
|
|
285
289
|
sessionId: session.sessionId,
|
|
286
290
|
runId: event.runId,
|
|
287
291
|
callId: event.callId,
|
|
@@ -303,7 +307,7 @@ export function recordModelCallEnded(
|
|
|
303
307
|
insertBoundedRecord(
|
|
304
308
|
manager.state.modelTimingsByLlmKey,
|
|
305
309
|
modelTimingLlmKey({
|
|
306
|
-
sessionId: session.
|
|
310
|
+
sessionId: session.ownerKey,
|
|
307
311
|
runId: event.runId,
|
|
308
312
|
provider: event.provider,
|
|
309
313
|
model: event.model,
|
|
@@ -325,7 +329,7 @@ export function replayPendingLlmOutputsForSession(
|
|
|
325
329
|
for (const [key, records] of [...manager.state.llmOutputsPendingInput]) {
|
|
326
330
|
const remaining: PendingLlmOutputRecord[] = [];
|
|
327
331
|
for (const record of records) {
|
|
328
|
-
if (record.
|
|
332
|
+
if (record.sessionOwnerKey !== session.ownerKey) {
|
|
329
333
|
remaining.push(record);
|
|
330
334
|
continue;
|
|
331
335
|
}
|
|
@@ -389,7 +393,7 @@ export function replayAgentEndMessages(
|
|
|
389
393
|
export function emitUnpairedModelCallTimingMarks(manager: SessionManager, session: SessionState): void {
|
|
390
394
|
for (const records of manager.state.modelCallsByCallId.values()) {
|
|
391
395
|
for (const record of records) {
|
|
392
|
-
if (record.
|
|
396
|
+
if (record.sessionOwnerKey !== session.ownerKey || record.consumed || record.endedAtMs !== undefined) {
|
|
393
397
|
continue;
|
|
394
398
|
}
|
|
395
399
|
emitModelTimingMark(manager, session, 'openclaw.model_call_timing_unpaired', record);
|
|
@@ -400,7 +404,7 @@ export function emitUnpairedModelCallTimingMarks(manager: SessionManager, sessio
|
|
|
400
404
|
const unpairedEnded: ModelCallRecord[] = [];
|
|
401
405
|
for (const records of manager.state.modelTimingsByLlmKey.values()) {
|
|
402
406
|
for (const record of records) {
|
|
403
|
-
if (record.
|
|
407
|
+
if (record.sessionOwnerKey !== session.ownerKey || record.consumed) {
|
|
404
408
|
continue;
|
|
405
409
|
}
|
|
406
410
|
unpairedEnded.push(record);
|
|
@@ -494,7 +498,7 @@ function replayExpiredPendingOutput(manager: SessionManager, key: string, record
|
|
|
494
498
|
if (!removeRecord(manager.state.llmOutputsPendingInput, key, record)) {
|
|
495
499
|
return;
|
|
496
500
|
}
|
|
497
|
-
const session = manager.state.sessions.get(record.
|
|
501
|
+
const session = manager.state.sessions.get(record.sessionOwnerKey);
|
|
498
502
|
if (!session) {
|
|
499
503
|
manager.state.counters.skippedEvents += 1;
|
|
500
504
|
return;
|
|
@@ -552,7 +556,9 @@ function replayLlmOutput(params: {
|
|
|
552
556
|
const metadata = toJsonRecord({
|
|
553
557
|
source,
|
|
554
558
|
runId: event.runId,
|
|
555
|
-
sessionId:
|
|
559
|
+
sessionId: session.sessionId,
|
|
560
|
+
sessionKey: session.sessionKey,
|
|
561
|
+
agentId: session.agentId,
|
|
556
562
|
provider: event.provider,
|
|
557
563
|
model: event.model,
|
|
558
564
|
callId: timing?.callId,
|
|
@@ -614,7 +620,7 @@ function replayAssistantMessageWrites(
|
|
|
614
620
|
},
|
|
615
621
|
ctx,
|
|
616
622
|
input: {
|
|
617
|
-
|
|
623
|
+
sessionOwnerKey: session.ownerKey,
|
|
618
624
|
sessionId: session.sessionId,
|
|
619
625
|
runId,
|
|
620
626
|
provider: record.provider,
|
|
@@ -643,13 +649,13 @@ function consumeNextTimingCandidate(
|
|
|
643
649
|
input: { runId?: string | undefined; provider: string; model: string },
|
|
644
650
|
): ModelCallRecord | undefined {
|
|
645
651
|
const key = modelTimingLlmKey({
|
|
646
|
-
sessionId: session.
|
|
652
|
+
sessionId: session.ownerKey,
|
|
647
653
|
runId: input.runId,
|
|
648
654
|
provider: input.provider,
|
|
649
655
|
model: input.model,
|
|
650
656
|
});
|
|
651
657
|
const records = manager.state.modelTimingsByLlmKey.get(key) ?? [];
|
|
652
|
-
const candidate = records.find((record) => record.
|
|
658
|
+
const candidate = records.find((record) => record.sessionOwnerKey === session.ownerKey && !record.consumed);
|
|
653
659
|
if (!candidate) {
|
|
654
660
|
return undefined;
|
|
655
661
|
}
|
|
@@ -664,13 +670,13 @@ function consumeTimingCandidate(
|
|
|
664
670
|
event: PluginHookLlmOutputEvent,
|
|
665
671
|
): ModelCallRecord | undefined {
|
|
666
672
|
const key = modelTimingLlmKey({
|
|
667
|
-
sessionId: session.
|
|
673
|
+
sessionId: session.ownerKey,
|
|
668
674
|
runId: event.runId,
|
|
669
675
|
provider: event.provider,
|
|
670
676
|
model: event.model,
|
|
671
677
|
});
|
|
672
678
|
const candidates = (manager.state.modelTimingsByLlmKey.get(key) ?? []).filter(
|
|
673
|
-
(record) => record.
|
|
679
|
+
(record) => record.sessionOwnerKey === session.ownerKey && !record.consumed,
|
|
674
680
|
);
|
|
675
681
|
if (candidates.length === 1) {
|
|
676
682
|
const candidate = candidates[0];
|
|
@@ -769,7 +775,7 @@ function emitModelTimingSummaryMark(manager: SessionManager, session: SessionSta
|
|
|
769
775
|
/** Convert an OpenClaw llm_input event into the buffered request record. */
|
|
770
776
|
function createInputRecord(session: SessionState, event: PluginHookLlmInputEvent): LlmInputRecord {
|
|
771
777
|
return {
|
|
772
|
-
|
|
778
|
+
sessionOwnerKey: session.ownerKey,
|
|
773
779
|
sessionId: event.sessionId,
|
|
774
780
|
runId: event.runId,
|
|
775
781
|
provider: event.provider,
|
|
@@ -788,16 +794,24 @@ function existingSessionForMessageWrite(
|
|
|
788
794
|
event: PluginHookBeforeMessageWriteEvent,
|
|
789
795
|
ctx: PluginHookBeforeMessageWriteContext,
|
|
790
796
|
): SessionState | undefined {
|
|
791
|
-
const key =
|
|
797
|
+
const key = resolveSessionOwnerKey(manager.state, {
|
|
792
798
|
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
793
799
|
});
|
|
794
|
-
|
|
800
|
+
if (key === undefined) {
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
const session = manager.state.sessions.get(key);
|
|
804
|
+
const sessionKey = event.sessionKey ?? ctx.sessionKey;
|
|
805
|
+
if (session && session.sessionKey === undefined && sessionKey !== undefined) {
|
|
806
|
+
session.sessionKey = sessionKey;
|
|
807
|
+
}
|
|
808
|
+
return session;
|
|
795
809
|
}
|
|
796
810
|
|
|
797
811
|
/** Build a minimal request placeholder when only an llm_output hook is available. */
|
|
798
812
|
function placeholderInputRecord(record: PendingLlmOutputRecord): LlmInputRecord {
|
|
799
813
|
return {
|
|
800
|
-
|
|
814
|
+
sessionOwnerKey: record.sessionOwnerKey,
|
|
801
815
|
sessionId: record.sessionId,
|
|
802
816
|
runId: record.runId,
|
|
803
817
|
provider: record.provider,
|
|
@@ -1175,20 +1189,39 @@ function incrementHookLlmOutputReplayCount(session: SessionState, runId: string
|
|
|
1175
1189
|
|
|
1176
1190
|
/** Build the per-session run key used for trajectory de-duplication. */
|
|
1177
1191
|
function trajectoryRunKey(session: SessionState, runId?: string): string {
|
|
1178
|
-
return runId ?? session.
|
|
1192
|
+
return runId ?? session.ownerKey;
|
|
1179
1193
|
}
|
|
1180
1194
|
|
|
1181
|
-
/** Normalize provider usage into
|
|
1195
|
+
/** Normalize provider usage into NeMo Relay token and cost fields. */
|
|
1182
1196
|
function mapUsage(usage: unknown): Record<string, number> | undefined {
|
|
1183
1197
|
if (!isRecord(usage)) {
|
|
1184
1198
|
return undefined;
|
|
1185
1199
|
}
|
|
1186
1200
|
const mapped: Record<string, number> = {};
|
|
1187
|
-
const input =
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1201
|
+
const input =
|
|
1202
|
+
numberField(usage, 'input') ??
|
|
1203
|
+
numberField(usage, 'prompt_tokens') ??
|
|
1204
|
+
numberField(usage, 'input_tokens') ??
|
|
1205
|
+
numberField(usage, 'inputTokens');
|
|
1206
|
+
const output =
|
|
1207
|
+
numberField(usage, 'output') ??
|
|
1208
|
+
numberField(usage, 'completion_tokens') ??
|
|
1209
|
+
numberField(usage, 'output_tokens') ??
|
|
1210
|
+
numberField(usage, 'outputTokens');
|
|
1211
|
+
const cacheRead =
|
|
1212
|
+
numberField(usage, 'cacheRead') ??
|
|
1213
|
+
numberField(usage, 'cache_read_tokens') ??
|
|
1214
|
+
numberField(usage, 'cache_read_input_tokens') ??
|
|
1215
|
+
nestedNumberField(usage, 'input_tokens_details', 'cached_tokens') ??
|
|
1216
|
+
nestedNumberField(usage, 'prompt_tokens_details', 'cached_tokens');
|
|
1217
|
+
const cacheWrite =
|
|
1218
|
+
numberField(usage, 'cacheWrite') ??
|
|
1219
|
+
numberField(usage, 'cache_write_tokens') ??
|
|
1220
|
+
numberField(usage, 'cache_creation_input_tokens');
|
|
1221
|
+
const total =
|
|
1222
|
+
numberField(usage, 'total') ??
|
|
1223
|
+
numberField(usage, 'totalTokens') ??
|
|
1224
|
+
numberField(usage, 'total_tokens');
|
|
1192
1225
|
const totalCanIncludeCompletion = total === undefined || output === undefined || total >= output;
|
|
1193
1226
|
const prompt = total !== undefined && output !== undefined && totalCanIncludeCompletion ? total - output : input;
|
|
1194
1227
|
const totalCanIncludePrompt = total === undefined || prompt === undefined || total >= prompt;
|
|
@@ -1230,6 +1263,12 @@ function numberField(record: Record<string, unknown>, key: string): number | und
|
|
|
1230
1263
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
1231
1264
|
}
|
|
1232
1265
|
|
|
1266
|
+
/** Read a finite numeric field nested one object deep from a generic hook record. */
|
|
1267
|
+
function nestedNumberField(record: Record<string, unknown>, objectKey: string, fieldKey: string): number | undefined {
|
|
1268
|
+
const value = record[objectKey];
|
|
1269
|
+
return isRecord(value) ? numberField(value, fieldKey) : undefined;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1233
1272
|
/** Copy model_call_ended details into a retained timing record. */
|
|
1234
1273
|
function applyModelCallEnd(record: ModelCallRecord, event: PluginHookModelCallEndedEvent, nowMs: number): void {
|
|
1235
1274
|
record.observedAtMs = nowMs;
|
|
@@ -1256,7 +1295,7 @@ function latestUnendedRecord(
|
|
|
1256
1295
|
}
|
|
1257
1296
|
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
1258
1297
|
const record = records[index];
|
|
1259
|
-
if (record?.
|
|
1298
|
+
if (record?.sessionOwnerKey === session.ownerKey && record.endedAtMs === undefined) {
|
|
1260
1299
|
return record;
|
|
1261
1300
|
}
|
|
1262
1301
|
}
|
package/src/hook-replay/marks.ts
CHANGED
|
@@ -20,6 +20,7 @@ export function emitMark(params: {
|
|
|
20
20
|
session: SessionState;
|
|
21
21
|
name: string;
|
|
22
22
|
data: JsonRecord;
|
|
23
|
+
metadata?: JsonRecord | null;
|
|
23
24
|
timestamp?: number;
|
|
24
25
|
}): void {
|
|
25
26
|
if (!params.session.rootHandle) {
|
|
@@ -27,7 +28,7 @@ export function emitMark(params: {
|
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
params.nf.event(params.name, params.session.rootHandle, params.data, null, params.timestamp ?? null);
|
|
31
|
+
params.nf.event(params.name, params.session.rootHandle, params.data, params.metadata ?? null, params.timestamp ?? null);
|
|
31
32
|
params.state.counters.marksEmitted += 1;
|
|
32
33
|
}
|
|
33
34
|
|