nemo-relay-openclaw 0.3.0 → 0.5.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.
@@ -28,7 +28,7 @@ import {
28
28
  evictExpiredCorrelationRecords,
29
29
  ensureSession,
30
30
  insertBoundedRecord,
31
- resolveSessionKey,
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.sessionKey === session.sessionId,
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.sessionKey === session.sessionId);
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.sessionKey === session.sessionId);
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
- sessionKey: session.sessionId,
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
- sessionKey: session.sessionId,
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
- sessionKey: session.sessionId,
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
- sessionKey: session.sessionId,
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.sessionId,
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.sessionKey !== session.sessionId) {
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.sessionKey !== session.sessionId || record.consumed || record.endedAtMs !== undefined) {
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.sessionKey !== session.sessionId || record.consumed) {
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.sessionKey);
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: event.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
- sessionKey: session.sessionId,
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.sessionId,
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.sessionKey === session.sessionId && !record.consumed);
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.sessionId,
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.sessionKey === session.sessionId && !record.consumed,
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
- sessionKey: session.sessionId,
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 = resolveSessionKey(manager.state, {
797
+ const key = resolveSessionOwnerKey(manager.state, {
792
798
  sessionKey: event.sessionKey ?? ctx.sessionKey,
793
799
  });
794
- return key === undefined ? undefined : manager.state.sessions.get(key);
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
- sessionKey: record.sessionKey,
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.sessionId;
1192
+ return runId ?? session.ownerKey;
1179
1193
  }
1180
1194
 
1181
- /** Normalize provider usage into OpenInference-friendly token/cost fields. */
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 = numberField(usage, 'input') ?? numberField(usage, 'prompt_tokens');
1188
- const output = numberField(usage, 'output') ?? numberField(usage, 'completion_tokens');
1189
- const cacheRead = numberField(usage, 'cacheRead') ?? numberField(usage, 'cache_read_tokens');
1190
- const cacheWrite = numberField(usage, 'cacheWrite') ?? numberField(usage, 'cache_write_tokens');
1191
- const total = numberField(usage, 'total') ?? numberField(usage, 'totalTokens') ?? numberField(usage, 'total_tokens');
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?.sessionKey === session.sessionId && record.endedAtMs === undefined) {
1298
+ if (record?.sessionOwnerKey === session.ownerKey && record.endedAtMs === undefined) {
1260
1299
  return record;
1261
1300
  }
1262
1301
  }
@@ -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