nemo-relay-openclaw 0.3.0-rc.1 → 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/README.md +8 -8
- package/dist/src/hook-replay/llm.js +88 -33
- 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 +99 -33
- 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/README.md
CHANGED
|
@@ -12,10 +12,11 @@ observability component can export as ATIF JSON, OpenTelemetry spans, and
|
|
|
12
12
|
OpenInference/Phoenix spans. The same generic plugin config path can initialize
|
|
13
13
|
Adaptive components for hook-backed telemetry learning.
|
|
14
14
|
|
|
15
|
-
This public OpenClaw plugin package uses OpenClaw public hooks. It
|
|
16
|
-
|
|
17
|
-
requests. For middleware-backed
|
|
18
|
-
patch-based OpenClaw integration from
|
|
15
|
+
This public OpenClaw plugin package uses OpenClaw public hooks. It can run
|
|
16
|
+
pre-tool conditional guardrails when OpenClaw invokes the before-tool hook, but
|
|
17
|
+
it does not rewrite provider routing or model requests. For middleware-backed
|
|
18
|
+
behavior that changes execution, use the patch-based OpenClaw integration from
|
|
19
|
+
the NeMo Relay repository.
|
|
19
20
|
|
|
20
21
|
## Why Use It?
|
|
21
22
|
|
|
@@ -204,11 +205,10 @@ openclaw gateway call nemoRelay.status --json
|
|
|
204
205
|
|
|
205
206
|
## Current Limits
|
|
206
207
|
|
|
207
|
-
The plugin maps supported OpenClaw hook events into NeMo Relay telemetry
|
|
208
|
-
|
|
208
|
+
The plugin maps supported OpenClaw hook events into NeMo Relay telemetry and can
|
|
209
|
+
run pre-tool conditional guardrail checks.
|
|
209
210
|
|
|
210
|
-
It does not
|
|
211
|
-
or provider request payloads.
|
|
211
|
+
It does not rewrite provider routing or provider request payloads.
|
|
212
212
|
|
|
213
213
|
Current OpenClaw public hooks expose request, response, message-write, and
|
|
214
214
|
provider timing details through separate event streams. The plugin correlates
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
import { emitMark, toJsonRecord, toJsonValue } from './marks.js';
|
|
4
|
-
import { evictExpiredCorrelationRecords, ensureSession, insertBoundedRecord,
|
|
4
|
+
import { evictExpiredCorrelationRecords, ensureSession, insertBoundedRecord, resolveSessionOwnerKey, } from './session.js';
|
|
5
5
|
import { llmKey, modelTimingKey, modelTimingLlmKey, nowMicros, startMicrosFromDuration } from './correlation.js';
|
|
6
6
|
/**
|
|
7
7
|
* Store one OpenClaw llm_input snapshot and replay it immediately if the matching
|
|
@@ -9,12 +9,14 @@ import { llmKey, modelTimingKey, modelTimingLlmKey, nowMicros, startMicrosFromDu
|
|
|
9
9
|
*/
|
|
10
10
|
export function recordLlmInput(manager, event, ctx) {
|
|
11
11
|
evictExpiredReplayRecords(manager);
|
|
12
|
+
const observedAtMicros = nowMicros();
|
|
12
13
|
const session = ensureSession(manager, {
|
|
13
14
|
sessionId: event.sessionId,
|
|
14
15
|
sessionKey: ctx.sessionKey,
|
|
15
16
|
runId: event.runId,
|
|
16
17
|
agentId: ctx.agentId,
|
|
17
18
|
source: 'lazy_session',
|
|
19
|
+
timestamp: observedAtMicros,
|
|
18
20
|
});
|
|
19
21
|
if (!session) {
|
|
20
22
|
return;
|
|
@@ -26,7 +28,7 @@ export function recordLlmInput(manager, event, ctx) {
|
|
|
26
28
|
const key = llmKey(event);
|
|
27
29
|
const input = createInputRecord(session, event);
|
|
28
30
|
insertBoundedRecord(manager.state.llmInputs, key, input, manager.config.correlation.maxRecordsPerKey);
|
|
29
|
-
const pending = shiftOldest(manager.state.llmOutputsPendingInput, key, (record) => record.
|
|
31
|
+
const pending = shiftOldest(manager.state.llmOutputsPendingInput, key, (record) => record.sessionOwnerKey === session.ownerKey);
|
|
30
32
|
if (!pending) {
|
|
31
33
|
return;
|
|
32
34
|
}
|
|
@@ -48,22 +50,24 @@ export function recordLlmInput(manager, event, ctx) {
|
|
|
48
50
|
*/
|
|
49
51
|
export function recordLlmOutput(manager, event, ctx) {
|
|
50
52
|
evictExpiredReplayRecords(manager);
|
|
53
|
+
const observedAtMicros = nowMicros();
|
|
51
54
|
const session = ensureSession(manager, {
|
|
52
55
|
sessionId: event.sessionId,
|
|
53
56
|
sessionKey: ctx.sessionKey,
|
|
54
57
|
runId: event.runId,
|
|
55
58
|
agentId: ctx.agentId,
|
|
56
59
|
source: 'lazy_session',
|
|
60
|
+
timestamp: observedAtMicros,
|
|
57
61
|
});
|
|
58
62
|
if (!session) {
|
|
59
63
|
return;
|
|
60
64
|
}
|
|
61
65
|
const key = llmKey(event);
|
|
62
66
|
if (hasTrajectoryReplay(session, event.runId)) {
|
|
63
|
-
shiftOldest(manager.state.llmInputs, key, (record) => record.
|
|
67
|
+
shiftOldest(manager.state.llmInputs, key, (record) => record.sessionOwnerKey === session.ownerKey);
|
|
64
68
|
return;
|
|
65
69
|
}
|
|
66
|
-
const input = shiftOldest(manager.state.llmInputs, key, (record) => record.
|
|
70
|
+
const input = shiftOldest(manager.state.llmInputs, key, (record) => record.sessionOwnerKey === session.ownerKey);
|
|
67
71
|
if (input) {
|
|
68
72
|
replayLlmOutput({
|
|
69
73
|
manager,
|
|
@@ -75,7 +79,7 @@ export function recordLlmOutput(manager, event, ctx) {
|
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
81
|
const pending = {
|
|
78
|
-
|
|
82
|
+
sessionOwnerKey: session.ownerKey,
|
|
79
83
|
sessionId: event.sessionId,
|
|
80
84
|
runId: event.runId,
|
|
81
85
|
provider: event.provider,
|
|
@@ -120,7 +124,7 @@ export function recordBeforeMessageWrite(manager, event, ctx) {
|
|
|
120
124
|
if (provider && model && (assistantTexts.length > 0 || assistantToolCalls.length > 0 || usage !== undefined)) {
|
|
121
125
|
session.assistantMessageWrites ??= [];
|
|
122
126
|
session.assistantMessageWrites.push({
|
|
123
|
-
|
|
127
|
+
sessionOwnerKey: session.ownerKey,
|
|
124
128
|
provider,
|
|
125
129
|
model,
|
|
126
130
|
assistantTexts,
|
|
@@ -158,7 +162,7 @@ export function recordModelCallStarted(manager, event, ctx) {
|
|
|
158
162
|
return;
|
|
159
163
|
}
|
|
160
164
|
insertBoundedRecord(manager.state.modelCallsByCallId, modelTimingKey(event), {
|
|
161
|
-
|
|
165
|
+
sessionOwnerKey: session.ownerKey,
|
|
162
166
|
sessionId: session.sessionId,
|
|
163
167
|
runId: event.runId,
|
|
164
168
|
callId: event.callId,
|
|
@@ -191,7 +195,7 @@ export function recordModelCallEnded(manager, event, ctx) {
|
|
|
191
195
|
const existing = latestUnendedRecord(manager.state.modelCallsByCallId.get(byCallKey), session);
|
|
192
196
|
const record = existing ??
|
|
193
197
|
{
|
|
194
|
-
|
|
198
|
+
sessionOwnerKey: session.ownerKey,
|
|
195
199
|
sessionId: session.sessionId,
|
|
196
200
|
runId: event.runId,
|
|
197
201
|
callId: event.callId,
|
|
@@ -205,7 +209,7 @@ export function recordModelCallEnded(manager, event, ctx) {
|
|
|
205
209
|
insertBoundedRecord(manager.state.modelCallsByCallId, byCallKey, record, manager.config.correlation.maxRecordsPerKey);
|
|
206
210
|
}
|
|
207
211
|
insertBoundedRecord(manager.state.modelTimingsByLlmKey, modelTimingLlmKey({
|
|
208
|
-
sessionId: session.
|
|
212
|
+
sessionId: session.ownerKey,
|
|
209
213
|
runId: event.runId,
|
|
210
214
|
provider: event.provider,
|
|
211
215
|
model: event.model,
|
|
@@ -219,7 +223,7 @@ export function replayPendingLlmOutputsForSession(manager, session, options) {
|
|
|
219
223
|
for (const [key, records] of [...manager.state.llmOutputsPendingInput]) {
|
|
220
224
|
const remaining = [];
|
|
221
225
|
for (const record of records) {
|
|
222
|
-
if (record.
|
|
226
|
+
if (record.sessionOwnerKey !== session.ownerKey) {
|
|
223
227
|
remaining.push(record);
|
|
224
228
|
continue;
|
|
225
229
|
}
|
|
@@ -274,7 +278,7 @@ export function replayAgentEndMessages(manager, event, ctx, session) {
|
|
|
274
278
|
export function emitUnpairedModelCallTimingMarks(manager, session) {
|
|
275
279
|
for (const records of manager.state.modelCallsByCallId.values()) {
|
|
276
280
|
for (const record of records) {
|
|
277
|
-
if (record.
|
|
281
|
+
if (record.sessionOwnerKey !== session.ownerKey || record.consumed || record.endedAtMs !== undefined) {
|
|
278
282
|
continue;
|
|
279
283
|
}
|
|
280
284
|
emitModelTimingMark(manager, session, 'openclaw.model_call_timing_unpaired', record);
|
|
@@ -284,7 +288,7 @@ export function emitUnpairedModelCallTimingMarks(manager, session) {
|
|
|
284
288
|
const unpairedEnded = [];
|
|
285
289
|
for (const records of manager.state.modelTimingsByLlmKey.values()) {
|
|
286
290
|
for (const record of records) {
|
|
287
|
-
if (record.
|
|
291
|
+
if (record.sessionOwnerKey !== session.ownerKey || record.consumed) {
|
|
288
292
|
continue;
|
|
289
293
|
}
|
|
290
294
|
unpairedEnded.push(record);
|
|
@@ -324,12 +328,14 @@ export function buildReplayLlmRequest(input, output, config, source = 'openclaw.
|
|
|
324
328
|
/** Build the response payload passed to NeMo Relay for a replayed LLM span. */
|
|
325
329
|
export function buildReplayLlmResponse(event, timing, config) {
|
|
326
330
|
const usage = mapUsage(event.usage);
|
|
327
|
-
const
|
|
331
|
+
const assistantToolCalls = assistantToolCallsForOutput(event, config);
|
|
332
|
+
const assistantToolCallNames = toolCallNames(assistantToolCalls);
|
|
328
333
|
return toJsonValue({
|
|
329
334
|
role: 'assistant',
|
|
330
335
|
content: config.capture.includeResponses
|
|
331
336
|
? responseContent(event.assistantTexts, assistantToolCallNames)
|
|
332
337
|
: undefined,
|
|
338
|
+
tool_calls: assistantToolCalls.length > 0 ? assistantToolCalls : undefined,
|
|
333
339
|
assistant_texts_count: event.assistantTexts.length,
|
|
334
340
|
resolved_ref: event.resolvedRef,
|
|
335
341
|
harness_id: event.harnessId,
|
|
@@ -347,13 +353,20 @@ export function buildReplayLlmResponse(event, timing, config) {
|
|
|
347
353
|
},
|
|
348
354
|
});
|
|
349
355
|
}
|
|
356
|
+
/** Return assistant tool calls from the current OpenClaw llm_output contract. */
|
|
357
|
+
function assistantToolCallsForOutput(event, config) {
|
|
358
|
+
const explicit = Array.isArray(event.assistantToolCalls) ? event.assistantToolCalls : [];
|
|
359
|
+
const inferred = explicit.length > 0 || !isRecord(event.lastAssistant) ? explicit : extractToolCalls(event.lastAssistant);
|
|
360
|
+
const snapshot = snapshotMessages(inferred);
|
|
361
|
+
return config.capture.stripToolArgs ? snapshot.map(stripToolCallArgs) : snapshot;
|
|
362
|
+
}
|
|
350
363
|
/** Replay an output whose matching input never arrived before the grace timeout. */
|
|
351
364
|
function replayExpiredPendingOutput(manager, key, record) {
|
|
352
365
|
try {
|
|
353
366
|
if (!removeRecord(manager.state.llmOutputsPendingInput, key, record)) {
|
|
354
367
|
return;
|
|
355
368
|
}
|
|
356
|
-
const session = manager.state.sessions.get(record.
|
|
369
|
+
const session = manager.state.sessions.get(record.sessionOwnerKey);
|
|
357
370
|
if (!session) {
|
|
358
371
|
manager.state.counters.skippedEvents += 1;
|
|
359
372
|
return;
|
|
@@ -399,7 +412,9 @@ function replayLlmOutput(params) {
|
|
|
399
412
|
const metadata = toJsonRecord({
|
|
400
413
|
source,
|
|
401
414
|
runId: event.runId,
|
|
402
|
-
sessionId:
|
|
415
|
+
sessionId: session.sessionId,
|
|
416
|
+
sessionKey: session.sessionKey,
|
|
417
|
+
agentId: session.agentId,
|
|
403
418
|
provider: event.provider,
|
|
404
419
|
model: event.model,
|
|
405
420
|
callId: timing?.callId,
|
|
@@ -443,7 +458,7 @@ function replayAssistantMessageWrites(manager, session, event, ctx) {
|
|
|
443
458
|
},
|
|
444
459
|
ctx,
|
|
445
460
|
input: {
|
|
446
|
-
|
|
461
|
+
sessionOwnerKey: session.ownerKey,
|
|
447
462
|
sessionId: session.sessionId,
|
|
448
463
|
runId,
|
|
449
464
|
provider: record.provider,
|
|
@@ -466,13 +481,13 @@ function replayAssistantMessageWrites(manager, session, event, ctx) {
|
|
|
466
481
|
/** Consume the next unpaired timing record for message-write trajectory replay. */
|
|
467
482
|
function consumeNextTimingCandidate(manager, session, input) {
|
|
468
483
|
const key = modelTimingLlmKey({
|
|
469
|
-
sessionId: session.
|
|
484
|
+
sessionId: session.ownerKey,
|
|
470
485
|
runId: input.runId,
|
|
471
486
|
provider: input.provider,
|
|
472
487
|
model: input.model,
|
|
473
488
|
});
|
|
474
489
|
const records = manager.state.modelTimingsByLlmKey.get(key) ?? [];
|
|
475
|
-
const candidate = records.find((record) => record.
|
|
490
|
+
const candidate = records.find((record) => record.sessionOwnerKey === session.ownerKey && !record.consumed);
|
|
476
491
|
if (!candidate) {
|
|
477
492
|
return undefined;
|
|
478
493
|
}
|
|
@@ -482,12 +497,12 @@ function consumeNextTimingCandidate(manager, session, input) {
|
|
|
482
497
|
/** Consume a timing candidate only when the public hook data makes it unambiguous. */
|
|
483
498
|
function consumeTimingCandidate(manager, session, event) {
|
|
484
499
|
const key = modelTimingLlmKey({
|
|
485
|
-
sessionId: session.
|
|
500
|
+
sessionId: session.ownerKey,
|
|
486
501
|
runId: event.runId,
|
|
487
502
|
provider: event.provider,
|
|
488
503
|
model: event.model,
|
|
489
504
|
});
|
|
490
|
-
const candidates = (manager.state.modelTimingsByLlmKey.get(key) ?? []).filter((record) => record.
|
|
505
|
+
const candidates = (manager.state.modelTimingsByLlmKey.get(key) ?? []).filter((record) => record.sessionOwnerKey === session.ownerKey && !record.consumed);
|
|
491
506
|
if (candidates.length === 1) {
|
|
492
507
|
const candidate = candidates[0];
|
|
493
508
|
if (!candidate) {
|
|
@@ -571,7 +586,7 @@ function emitModelTimingSummaryMark(manager, session, records) {
|
|
|
571
586
|
/** Convert an OpenClaw llm_input event into the buffered request record. */
|
|
572
587
|
function createInputRecord(session, event) {
|
|
573
588
|
return {
|
|
574
|
-
|
|
589
|
+
sessionOwnerKey: session.ownerKey,
|
|
575
590
|
sessionId: event.sessionId,
|
|
576
591
|
runId: event.runId,
|
|
577
592
|
provider: event.provider,
|
|
@@ -585,15 +600,23 @@ function createInputRecord(session, event) {
|
|
|
585
600
|
}
|
|
586
601
|
/** Resolve an existing session for before_message_write without creating a fake one. */
|
|
587
602
|
function existingSessionForMessageWrite(manager, event, ctx) {
|
|
588
|
-
const key =
|
|
603
|
+
const key = resolveSessionOwnerKey(manager.state, {
|
|
589
604
|
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
590
605
|
});
|
|
591
|
-
|
|
606
|
+
if (key === undefined) {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
const session = manager.state.sessions.get(key);
|
|
610
|
+
const sessionKey = event.sessionKey ?? ctx.sessionKey;
|
|
611
|
+
if (session && session.sessionKey === undefined && sessionKey !== undefined) {
|
|
612
|
+
session.sessionKey = sessionKey;
|
|
613
|
+
}
|
|
614
|
+
return session;
|
|
592
615
|
}
|
|
593
616
|
/** Build a minimal request placeholder when only an llm_output hook is available. */
|
|
594
617
|
function placeholderInputRecord(record) {
|
|
595
618
|
return {
|
|
596
|
-
|
|
619
|
+
sessionOwnerKey: record.sessionOwnerKey,
|
|
597
620
|
sessionId: record.sessionId,
|
|
598
621
|
runId: record.runId,
|
|
599
622
|
provider: record.provider,
|
|
@@ -658,6 +681,9 @@ function stripToolCallArgs(value) {
|
|
|
658
681
|
stripped[key] = { stripped: true };
|
|
659
682
|
}
|
|
660
683
|
}
|
|
684
|
+
if (isRecord(stripped.function) && stripped.function.arguments !== undefined) {
|
|
685
|
+
stripped.function = { ...stripped.function, arguments: { stripped: true } };
|
|
686
|
+
}
|
|
661
687
|
return stripped;
|
|
662
688
|
}
|
|
663
689
|
/** Strip provider thinking/signature payloads that are noisy in trace UIs. */
|
|
@@ -761,6 +787,8 @@ function extractToolCalls(message) {
|
|
|
761
787
|
function isToolCallLike(value) {
|
|
762
788
|
return (isRecord(value) &&
|
|
763
789
|
(value.type === 'toolCall' ||
|
|
790
|
+
value.type === 'toolcall' ||
|
|
791
|
+
value.type === 'function_call' ||
|
|
764
792
|
value.type === 'tool_use' ||
|
|
765
793
|
value.type === 'tool-call' ||
|
|
766
794
|
value.toolName !== undefined ||
|
|
@@ -776,13 +804,21 @@ function toolCallNames(toolCalls) {
|
|
|
776
804
|
if (!isRecord(toolCall)) {
|
|
777
805
|
continue;
|
|
778
806
|
}
|
|
779
|
-
const name = stringField(toolCall, 'name') ??
|
|
807
|
+
const name = stringField(toolCall, 'name') ??
|
|
808
|
+
stringField(toolCall, 'toolName') ??
|
|
809
|
+
stringField(toolCall, 'functionName') ??
|
|
810
|
+
functionToolCallName(toolCall);
|
|
780
811
|
if (name) {
|
|
781
812
|
names.push(name);
|
|
782
813
|
}
|
|
783
814
|
}
|
|
784
815
|
return names;
|
|
785
816
|
}
|
|
817
|
+
/** Read OpenAI-style nested function tool-call names. */
|
|
818
|
+
function functionToolCallName(toolCall) {
|
|
819
|
+
const fn = toolCall.function;
|
|
820
|
+
return isRecord(fn) ? stringField(fn, 'name') : undefined;
|
|
821
|
+
}
|
|
786
822
|
/** Convert stored message-write usage back into the llm_output usage contract. */
|
|
787
823
|
function mapHookUsage(usage) {
|
|
788
824
|
const mapped = mapUsage(usage);
|
|
@@ -919,19 +955,33 @@ function incrementHookLlmOutputReplayCount(session, runId, maxRuns) {
|
|
|
919
955
|
}
|
|
920
956
|
/** Build the per-session run key used for trajectory de-duplication. */
|
|
921
957
|
function trajectoryRunKey(session, runId) {
|
|
922
|
-
return runId ?? session.
|
|
958
|
+
return runId ?? session.ownerKey;
|
|
923
959
|
}
|
|
924
|
-
/** Normalize provider usage into
|
|
960
|
+
/** Normalize provider usage into NeMo Relay token and cost fields. */
|
|
925
961
|
function mapUsage(usage) {
|
|
926
962
|
if (!isRecord(usage)) {
|
|
927
963
|
return undefined;
|
|
928
964
|
}
|
|
929
965
|
const mapped = {};
|
|
930
|
-
const input = numberField(usage, 'input') ??
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
const
|
|
966
|
+
const input = numberField(usage, 'input') ??
|
|
967
|
+
numberField(usage, 'prompt_tokens') ??
|
|
968
|
+
numberField(usage, 'input_tokens') ??
|
|
969
|
+
numberField(usage, 'inputTokens');
|
|
970
|
+
const output = numberField(usage, 'output') ??
|
|
971
|
+
numberField(usage, 'completion_tokens') ??
|
|
972
|
+
numberField(usage, 'output_tokens') ??
|
|
973
|
+
numberField(usage, 'outputTokens');
|
|
974
|
+
const cacheRead = numberField(usage, 'cacheRead') ??
|
|
975
|
+
numberField(usage, 'cache_read_tokens') ??
|
|
976
|
+
numberField(usage, 'cache_read_input_tokens') ??
|
|
977
|
+
nestedNumberField(usage, 'input_tokens_details', 'cached_tokens') ??
|
|
978
|
+
nestedNumberField(usage, 'prompt_tokens_details', 'cached_tokens');
|
|
979
|
+
const cacheWrite = numberField(usage, 'cacheWrite') ??
|
|
980
|
+
numberField(usage, 'cache_write_tokens') ??
|
|
981
|
+
numberField(usage, 'cache_creation_input_tokens');
|
|
982
|
+
const total = numberField(usage, 'total') ??
|
|
983
|
+
numberField(usage, 'totalTokens') ??
|
|
984
|
+
numberField(usage, 'total_tokens');
|
|
935
985
|
const totalCanIncludeCompletion = total === undefined || output === undefined || total >= output;
|
|
936
986
|
const prompt = total !== undefined && output !== undefined && totalCanIncludeCompletion ? total - output : input;
|
|
937
987
|
const totalCanIncludePrompt = total === undefined || prompt === undefined || total >= prompt;
|
|
@@ -971,6 +1021,11 @@ function numberField(record, key) {
|
|
|
971
1021
|
const value = record[key];
|
|
972
1022
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
973
1023
|
}
|
|
1024
|
+
/** Read a finite numeric field nested one object deep from a generic hook record. */
|
|
1025
|
+
function nestedNumberField(record, objectKey, fieldKey) {
|
|
1026
|
+
const value = record[objectKey];
|
|
1027
|
+
return isRecord(value) ? numberField(value, fieldKey) : undefined;
|
|
1028
|
+
}
|
|
974
1029
|
/** Copy model_call_ended details into a retained timing record. */
|
|
975
1030
|
function applyModelCallEnd(record, event, nowMs) {
|
|
976
1031
|
record.observedAtMs = nowMs;
|
|
@@ -993,7 +1048,7 @@ function latestUnendedRecord(records, session) {
|
|
|
993
1048
|
}
|
|
994
1049
|
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
995
1050
|
const record = records[index];
|
|
996
|
-
if (record?.
|
|
1051
|
+
if (record?.sessionOwnerKey === session.ownerKey && record.endedAtMs === undefined) {
|
|
997
1052
|
return record;
|
|
998
1053
|
}
|
|
999
1054
|
}
|
|
@@ -6,7 +6,7 @@ export function emitMark(params) {
|
|
|
6
6
|
params.state.counters.skippedEvents += 1;
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
|
-
params.nf.event(params.name, params.session.rootHandle, params.data, null, params.timestamp ?? null);
|
|
9
|
+
params.nf.event(params.name, params.session.rootHandle, params.data, params.metadata ?? null, params.timestamp ?? null);
|
|
10
10
|
params.state.counters.marksEmitted += 1;
|
|
11
11
|
}
|
|
12
12
|
/** Return a blocked-tool event payload when OpenClaw denied the tool call. */
|
|
@@ -19,11 +19,16 @@ export type SessionLookupInput = {
|
|
|
19
19
|
};
|
|
20
20
|
export type EnsureSessionInput = SessionLookupInput & {
|
|
21
21
|
agentId?: string | undefined;
|
|
22
|
+
parentHandle?: ReturnType<NemoRelayRuntimeModule['pushScope']> | undefined;
|
|
23
|
+
scopeRole?: 'subagent' | undefined;
|
|
22
24
|
source: 'session_start' | 'lazy_session';
|
|
23
25
|
resumedFrom?: string | undefined;
|
|
24
26
|
timestamp?: number | undefined;
|
|
27
|
+
deferRootOpen?: boolean | undefined;
|
|
25
28
|
};
|
|
26
29
|
export type SessionState = {
|
|
30
|
+
/** Immutable internal owner key used for replay buffers and alias lookups. */
|
|
31
|
+
ownerKey: string;
|
|
27
32
|
sessionId: string;
|
|
28
33
|
sessionKey?: string;
|
|
29
34
|
agentId?: string;
|
|
@@ -42,9 +47,17 @@ export type SessionState = {
|
|
|
42
47
|
assistantMessageWrites?: AssistantMessageRecord[];
|
|
43
48
|
stack: ReturnType<NemoRelayRuntimeModule['createScopeStack']>;
|
|
44
49
|
rootHandle?: ReturnType<NemoRelayRuntimeModule['pushScope']>;
|
|
50
|
+
scopeRole?: 'subagent';
|
|
51
|
+
pendingRootOpen?: boolean;
|
|
52
|
+
pendingRootTimestampMicros?: number;
|
|
53
|
+
pendingRootRunId?: string;
|
|
54
|
+
pendingCapturedEmits?: Array<{
|
|
55
|
+
label: string;
|
|
56
|
+
emit: () => void;
|
|
57
|
+
}>;
|
|
45
58
|
};
|
|
46
59
|
export type PendingLlmOutputRecord = {
|
|
47
|
-
|
|
60
|
+
sessionOwnerKey: string;
|
|
48
61
|
sessionId: string;
|
|
49
62
|
runId: string;
|
|
50
63
|
provider: string;
|
|
@@ -55,7 +68,7 @@ export type PendingLlmOutputRecord = {
|
|
|
55
68
|
timer?: ReturnType<typeof setTimeout> | undefined;
|
|
56
69
|
};
|
|
57
70
|
export type LlmInputRecord = {
|
|
58
|
-
|
|
71
|
+
sessionOwnerKey: string;
|
|
59
72
|
sessionId: string;
|
|
60
73
|
runId: string;
|
|
61
74
|
provider: string;
|
|
@@ -68,7 +81,7 @@ export type LlmInputRecord = {
|
|
|
68
81
|
placeholderRequest?: boolean | undefined;
|
|
69
82
|
};
|
|
70
83
|
export type AssistantMessageRecord = {
|
|
71
|
-
|
|
84
|
+
sessionOwnerKey: string;
|
|
72
85
|
provider: string;
|
|
73
86
|
model: string;
|
|
74
87
|
assistantTexts: string[];
|
|
@@ -80,7 +93,7 @@ export type AssistantMessageRecord = {
|
|
|
80
93
|
usage?: unknown;
|
|
81
94
|
};
|
|
82
95
|
export type ModelCallRecord = {
|
|
83
|
-
|
|
96
|
+
sessionOwnerKey: string;
|
|
84
97
|
runId: string;
|
|
85
98
|
callId: string;
|
|
86
99
|
provider: string;
|
|
@@ -130,23 +143,26 @@ export type SessionManager = {
|
|
|
130
143
|
}) => void;
|
|
131
144
|
emitUnpairedModelCallTimingMarks: (session: SessionState) => void;
|
|
132
145
|
logBoundedWarn: (key: string, message: string) => void;
|
|
146
|
+
resolveSessionRootContext?: (input: EnsureSessionInput) => Partial<EnsureSessionInput> | undefined;
|
|
133
147
|
};
|
|
134
148
|
/** Return all keys that may identify an existing OpenClaw session. */
|
|
135
149
|
export declare function lookupSessionKeys(input: SessionLookupInput): string[];
|
|
136
|
-
/** Return keys that should alias to
|
|
150
|
+
/** Return keys that should alias to one stable internal session owner. */
|
|
137
151
|
export declare function aliasSessionKeys(input: SessionLookupInput): string[];
|
|
138
|
-
/** Resolve a hook's session identity to the
|
|
139
|
-
export declare function
|
|
140
|
-
/** Remember equivalent hook identifiers so later events attach to the same root span. */
|
|
152
|
+
/** Resolve a hook's session identity to the stable owner key used in replay state. */
|
|
153
|
+
export declare function resolveSessionOwnerKey(state: HookReplayBackendState, input: SessionLookupInput): string | undefined;
|
|
154
|
+
/** Remember equivalent hook identifiers so later events attach to the same owner and root span. */
|
|
141
155
|
export declare function rememberSessionAliases(state: HookReplayBackendState, session: SessionState, input: SessionLookupInput): void;
|
|
142
156
|
/** Create the mutable in-memory state used by the hook replay backend. */
|
|
143
157
|
export declare function createHookReplayState(): HookReplayBackendState;
|
|
144
158
|
/** Return an existing session or lazily create a root session scope for replay. */
|
|
145
159
|
export declare function ensureSession(manager: SessionManager, input: EnsureSessionInput): SessionState | undefined;
|
|
160
|
+
/** Queue an emit callback until a deferred session root can open with honest lineage. */
|
|
161
|
+
export declare function queueCapturedEmit(session: SessionState, label: string, emit: () => void): boolean;
|
|
146
162
|
/** Flush pending LLM output/timing state before the root session closes. */
|
|
147
163
|
export declare function drainSession(manager: SessionManager, session: SessionState): void;
|
|
148
164
|
/** Close the root session scope with separate lifecycle summary and user-visible output. */
|
|
149
|
-
export declare function closeSessionRoot(manager: SessionManager, session: SessionState, summary: JsonRecord, rootOutput?: JsonRecord, timestamp?: number): void;
|
|
165
|
+
export declare function closeSessionRoot(manager: SessionManager, session: SessionState, summary: JsonRecord, rootOutput?: JsonRecord, metadata?: JsonRecord | null, timestamp?: number): void;
|
|
150
166
|
/** Remove a closed session from active replay state. */
|
|
151
167
|
export declare function deleteSession(state: HookReplayBackendState, session: SessionState): void;
|
|
152
168
|
/** Insert a correlation record while bounding retained entries per key. */
|
|
@@ -155,3 +171,5 @@ export declare function insertBoundedRecord<T>(map: Map<string, T[]>, key: strin
|
|
|
155
171
|
export declare function tupleKey(parts: Array<string | undefined>): string;
|
|
156
172
|
/** Evict stale cross-hook correlation records across all replay maps. */
|
|
157
173
|
export declare function evictExpiredCorrelationRecords(state: HookReplayBackendState, nowMs: number, ttlMs: number): void;
|
|
174
|
+
/** Open a deferred or new root session scope and flush queued child emissions. */
|
|
175
|
+
export declare function materializeSessionRoot(manager: SessionManager, session: SessionState, input: EnsureSessionInput): void;
|