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
|
@@ -1,10 +1,10 @@
|
|
|
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 } from './hook-replay/marks.js';
|
|
4
|
-
import { llmKey } from './hook-replay/correlation.js';
|
|
4
|
+
import { llmKey, nowMicros } from './hook-replay/correlation.js';
|
|
5
5
|
import { emitUnpairedModelCallTimingMarks, recordBeforeMessageWrite, recordLlmInput, recordLlmOutput, recordModelCallEnded, recordModelCallStarted, replayAgentEndMessages, replayPendingLlmOutputsForSession, } from './hook-replay/llm.js';
|
|
6
6
|
import { guardBeforeToolCall, replayAfterToolCall } from './hook-replay/tool.js';
|
|
7
|
-
import { createHookReplayState, drainSession, closeSessionRoot, deleteSession, ensureSession,
|
|
7
|
+
import { createHookReplayState, drainSession, closeSessionRoot, deleteSession, ensureSession, materializeSessionRoot, queueCapturedEmit, resolveSessionOwnerKey, } from './hook-replay/session.js';
|
|
8
8
|
/** Replays OpenClaw public hook events into NeMo Relay scopes, spans, and marks. */
|
|
9
9
|
export class HookReplayBackend {
|
|
10
10
|
nf;
|
|
@@ -13,6 +13,8 @@ export class HookReplayBackend {
|
|
|
13
13
|
agentVersion;
|
|
14
14
|
stateValue = createHookReplayState();
|
|
15
15
|
warningCounts = new Map();
|
|
16
|
+
pendingSubagentLineageByChildSessionKey = new Map();
|
|
17
|
+
pendingSubagentChildKeyByRunId = new Map();
|
|
16
18
|
constructor(options) {
|
|
17
19
|
this.nf = options.nf;
|
|
18
20
|
this.config = options.config;
|
|
@@ -30,13 +32,16 @@ export class HookReplayBackend {
|
|
|
30
32
|
}
|
|
31
33
|
/** Open or alias an explicit OpenClaw session root. */
|
|
32
34
|
onSessionStart(event, ctx) {
|
|
33
|
-
|
|
35
|
+
const observedAtMicros = nowMicros();
|
|
36
|
+
const session = this.ensureSession({
|
|
34
37
|
sessionId: event.sessionId,
|
|
35
38
|
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
36
39
|
agentId: ctx.agentId,
|
|
37
40
|
source: 'session_start',
|
|
38
41
|
resumedFrom: event.resumedFrom,
|
|
42
|
+
timestamp: observedAtMicros,
|
|
39
43
|
});
|
|
44
|
+
this.promoteDeferredSubagentSessionsForRequester(session?.sessionKey ?? event.sessionKey ?? ctx.sessionKey);
|
|
40
45
|
// ensureSession opens the root scope and emits openclaw.session_start for both explicit and lazy sessions.
|
|
41
46
|
}
|
|
42
47
|
/** Close one explicit OpenClaw session and export its ATIF artifact. */
|
|
@@ -50,7 +55,13 @@ export class HookReplayBackend {
|
|
|
50
55
|
if (!session) {
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
53
|
-
await this.closeSession(session, sessionEndSummary(event)
|
|
58
|
+
await this.closeSession(session, sessionEndSummary(event), toJsonRecord({
|
|
59
|
+
source: 'openclaw.session_end',
|
|
60
|
+
hook_event_name: 'session_end',
|
|
61
|
+
sessionId: session.sessionId,
|
|
62
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
63
|
+
agentId: ctx.agentId,
|
|
64
|
+
}));
|
|
54
65
|
}
|
|
55
66
|
/** Buffer an LLM request snapshot until a matching response or trajectory replay arrives. */
|
|
56
67
|
onLlmInput(event, ctx) {
|
|
@@ -82,12 +93,14 @@ export class HookReplayBackend {
|
|
|
82
93
|
}
|
|
83
94
|
/** Finalize one agent run, replaying message-write trajectory when needed. */
|
|
84
95
|
onAgentEnd(event, ctx) {
|
|
96
|
+
const observedAtMicros = nowMicros();
|
|
85
97
|
const session = this.ensureSession({
|
|
86
98
|
sessionId: ctx.sessionId,
|
|
87
99
|
sessionKey: ctx.sessionKey,
|
|
88
100
|
runId: event.runId ?? ctx.runId,
|
|
89
101
|
agentId: ctx.agentId,
|
|
90
102
|
source: 'lazy_session',
|
|
103
|
+
timestamp: observedAtMicros,
|
|
91
104
|
});
|
|
92
105
|
if (!session) {
|
|
93
106
|
return;
|
|
@@ -102,16 +115,18 @@ export class HookReplayBackend {
|
|
|
102
115
|
error: event.error,
|
|
103
116
|
durationMs: event.durationMs,
|
|
104
117
|
messageCount: event.messages.length,
|
|
105
|
-
}));
|
|
118
|
+
}), observedAtMicros);
|
|
106
119
|
}
|
|
107
120
|
/** Remember the last assistant text before OpenClaw finalizes the response. */
|
|
108
121
|
onBeforeAgentFinalize(event, ctx) {
|
|
122
|
+
const observedAtMicros = nowMicros();
|
|
109
123
|
const session = this.ensureSession({
|
|
110
124
|
sessionId: event.sessionId,
|
|
111
125
|
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
112
126
|
runId: event.runId ?? ctx.runId,
|
|
113
127
|
agentId: ctx.agentId,
|
|
114
128
|
source: 'lazy_session',
|
|
129
|
+
timestamp: observedAtMicros,
|
|
115
130
|
});
|
|
116
131
|
if (!session) {
|
|
117
132
|
return;
|
|
@@ -132,19 +147,21 @@ export class HookReplayBackend {
|
|
|
132
147
|
transcriptPath: event.transcriptPath,
|
|
133
148
|
stopHookActive: event.stopHookActive,
|
|
134
149
|
messageCount: event.messages?.length,
|
|
135
|
-
}));
|
|
150
|
+
}), observedAtMicros);
|
|
136
151
|
}
|
|
137
152
|
/** Attach subagent spawn metadata to the requester session when possible. */
|
|
138
153
|
onSubagentSpawned(event, ctx) {
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
154
|
+
const observedAtMicros = nowMicros();
|
|
155
|
+
this.trackPendingSubagentLineage(event, ctx, Math.trunc(observedAtMicros / 1000));
|
|
156
|
+
const requesterSession = this.ensureRequesterSessionAnchor(ctx.requesterSessionKey, observedAtMicros);
|
|
157
|
+
const childSessionKey = ctx.childSessionKey ?? event.childSessionKey;
|
|
158
|
+
const session = requesterSession ??
|
|
143
159
|
this.ensureSession({
|
|
144
|
-
childSessionKey
|
|
160
|
+
childSessionKey,
|
|
145
161
|
runId: ctx.runId ?? event.runId,
|
|
146
162
|
agentId: event.agentId,
|
|
147
163
|
source: 'lazy_session',
|
|
164
|
+
timestamp: observedAtMicros,
|
|
148
165
|
});
|
|
149
166
|
if (!session) {
|
|
150
167
|
return;
|
|
@@ -156,22 +173,29 @@ export class HookReplayBackend {
|
|
|
156
173
|
label: event.label,
|
|
157
174
|
mode: event.mode,
|
|
158
175
|
threadRequested: event.threadRequested,
|
|
159
|
-
}));
|
|
176
|
+
}), observedAtMicros);
|
|
177
|
+
if (!ctx.requesterSessionKey || requesterSession?.rootHandle) {
|
|
178
|
+
this.promoteDeferredSubagentSession(event.childSessionKey);
|
|
179
|
+
}
|
|
160
180
|
}
|
|
161
181
|
/** Attach subagent completion metadata to the requester or child session. */
|
|
162
182
|
onSubagentEnded(event, ctx) {
|
|
183
|
+
const observedAtMicros = nowMicros();
|
|
163
184
|
const session = this.ensureSession({
|
|
164
185
|
requesterSessionKey: ctx.requesterSessionKey,
|
|
165
186
|
source: 'lazy_session',
|
|
187
|
+
timestamp: observedAtMicros,
|
|
166
188
|
}) ??
|
|
167
189
|
this.ensureSession({
|
|
168
190
|
childSessionKey: ctx.childSessionKey ?? event.targetSessionKey,
|
|
169
191
|
runId: ctx.runId ?? event.runId,
|
|
170
192
|
source: 'lazy_session',
|
|
193
|
+
timestamp: observedAtMicros,
|
|
171
194
|
});
|
|
172
195
|
if (!session) {
|
|
173
196
|
return;
|
|
174
197
|
}
|
|
198
|
+
this.materializeDeferredSessionRoot(session);
|
|
175
199
|
this.emitSessionMark('openclaw.subagent_ended', session, toJsonRecord({
|
|
176
200
|
runId: event.runId ?? ctx.runId,
|
|
177
201
|
targetSessionKey: event.targetSessionKey,
|
|
@@ -182,7 +206,7 @@ export class HookReplayBackend {
|
|
|
182
206
|
endedAt: event.endedAt,
|
|
183
207
|
sendFarewell: event.sendFarewell,
|
|
184
208
|
accountId: event.accountId,
|
|
185
|
-
}));
|
|
209
|
+
}), observedAtMicros);
|
|
186
210
|
}
|
|
187
211
|
/** Drain all active sessions when the OpenClaw gateway is stopping. */
|
|
188
212
|
async drainForGatewayStop(reason) {
|
|
@@ -190,11 +214,11 @@ export class HookReplayBackend {
|
|
|
190
214
|
}
|
|
191
215
|
/** Close one session selected by a runtime lifecycle cleanup hook. */
|
|
192
216
|
async cleanupSession(input) {
|
|
193
|
-
const
|
|
194
|
-
if (!
|
|
217
|
+
const ownerKey = resolveSessionOwnerKey(this.stateValue, input);
|
|
218
|
+
if (!ownerKey) {
|
|
195
219
|
return;
|
|
196
220
|
}
|
|
197
|
-
const session = this.stateValue.sessions.get(
|
|
221
|
+
const session = this.stateValue.sessions.get(ownerKey);
|
|
198
222
|
if (!session) {
|
|
199
223
|
return;
|
|
200
224
|
}
|
|
@@ -226,6 +250,9 @@ export class HookReplayBackend {
|
|
|
226
250
|
}
|
|
227
251
|
/** Emit spans/marks under the stored session scope stack and ATIF capture window. */
|
|
228
252
|
emitCapturedUnderSession(label, session, emit) {
|
|
253
|
+
if (queueCapturedEmit(session, label, emit)) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
229
256
|
this.safeReplay(label, session, () => {
|
|
230
257
|
const previousStack = this.nf.currentScopeStack();
|
|
231
258
|
try {
|
|
@@ -250,22 +277,36 @@ export class HookReplayBackend {
|
|
|
250
277
|
return ensureSession(this.sessionManager(), input);
|
|
251
278
|
}
|
|
252
279
|
/** Drain, close, export, and delete one session. */
|
|
253
|
-
async closeSession(session, summary) {
|
|
280
|
+
async closeSession(session, summary, metadata) {
|
|
281
|
+
this.materializeDeferredSessionRoot(session);
|
|
254
282
|
drainSession(this.sessionManager(), session);
|
|
255
|
-
closeSessionRoot(this.sessionManager(), session, summary, session.finalOutput ?? summary);
|
|
283
|
+
closeSessionRoot(this.sessionManager(), session, summary, session.finalOutput ?? summary, metadata);
|
|
256
284
|
this.flushSubscriberDelivery('session_close');
|
|
285
|
+
this.forgetPendingSubagentLineage(session);
|
|
257
286
|
deleteSession(this.stateValue, session);
|
|
258
287
|
}
|
|
259
288
|
/** Emit a session-level OpenClaw lifecycle mark. */
|
|
260
|
-
emitSessionMark(name, session, data) {
|
|
289
|
+
emitSessionMark(name, session, data, timestampMicros) {
|
|
261
290
|
this.emitCapturedUnderSession(name, session, () => {
|
|
262
|
-
|
|
291
|
+
const params = {
|
|
263
292
|
nf: this.nf,
|
|
264
293
|
state: this.stateValue,
|
|
265
294
|
session,
|
|
266
295
|
name,
|
|
267
296
|
data,
|
|
268
|
-
|
|
297
|
+
metadata: toJsonRecord({
|
|
298
|
+
source: name,
|
|
299
|
+
hook_event_name: name.startsWith('openclaw.') ? name.slice('openclaw.'.length) : undefined,
|
|
300
|
+
sessionId: session.sessionId,
|
|
301
|
+
sessionKey: session.sessionKey,
|
|
302
|
+
agentId: session.agentId,
|
|
303
|
+
runId: typeof data.runId === 'string' ? data.runId : undefined,
|
|
304
|
+
}),
|
|
305
|
+
};
|
|
306
|
+
if (timestampMicros !== undefined) {
|
|
307
|
+
params.timestamp = timestampMicros;
|
|
308
|
+
}
|
|
309
|
+
emitMark(params);
|
|
269
310
|
});
|
|
270
311
|
}
|
|
271
312
|
/** Close every active session with the same lifecycle summary. */
|
|
@@ -295,8 +336,170 @@ export class HookReplayBackend {
|
|
|
295
336
|
replayPendingLlmOutputsForSession: (session, options) => this.replayPendingLlmOutputsForSession(session, options),
|
|
296
337
|
emitUnpairedModelCallTimingMarks: (session) => this.emitUnpairedModelCallTimingMarks(session),
|
|
297
338
|
logBoundedWarn: (key, message) => this.logBoundedWarn(key, message),
|
|
339
|
+
resolveSessionRootContext: (input) => this.resolveSessionRootContext(input),
|
|
298
340
|
};
|
|
299
341
|
}
|
|
342
|
+
/** Prefer nested child scopes only when the hook surface provides real subagent lineage. */
|
|
343
|
+
resolveSessionRootContext(input) {
|
|
344
|
+
this.pruneExpiredPendingSubagentLineage(microsToMs(input.timestamp) ?? Date.now());
|
|
345
|
+
const lineage = this.resolvePendingSubagentLineage(input);
|
|
346
|
+
if (lineage) {
|
|
347
|
+
const parentSession = this.resolveTrackedSession({ requesterSessionKey: lineage.requesterSessionKey });
|
|
348
|
+
return {
|
|
349
|
+
childSessionKey: input.childSessionKey ?? lineage.childSessionKey,
|
|
350
|
+
runId: input.runId ?? lineage.runId,
|
|
351
|
+
agentId: input.agentId ?? lineage.agentId,
|
|
352
|
+
scopeRole: 'subagent',
|
|
353
|
+
parentHandle: parentSession?.rootHandle,
|
|
354
|
+
deferRootOpen: input.deferRootOpen ?? (parentSession?.rootHandle ? false : true),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
if (this.isDocumentedSubagentSessionKey(input.sessionKey ?? input.childSessionKey)) {
|
|
358
|
+
return {
|
|
359
|
+
scopeRole: 'subagent',
|
|
360
|
+
deferRootOpen: input.deferRootOpen ?? true,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
/** Track stable parent/child lineage from subagent hooks until child session hooks can use it. */
|
|
366
|
+
trackPendingSubagentLineage(event, ctx, observedAtMs) {
|
|
367
|
+
this.pruneExpiredPendingSubagentLineage(observedAtMs);
|
|
368
|
+
const requesterSessionKey = ctx.requesterSessionKey?.trim();
|
|
369
|
+
const childSessionKey = (ctx.childSessionKey ?? event.childSessionKey)?.trim();
|
|
370
|
+
if (!requesterSessionKey || !childSessionKey) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.pendingSubagentLineageByChildSessionKey.set(childSessionKey, {
|
|
374
|
+
childSessionKey,
|
|
375
|
+
requesterSessionKey,
|
|
376
|
+
runId: ctx.runId ?? event.runId,
|
|
377
|
+
agentId: event.agentId,
|
|
378
|
+
observedAtMs,
|
|
379
|
+
});
|
|
380
|
+
if (ctx.runId ?? event.runId) {
|
|
381
|
+
this.pendingSubagentChildKeyByRunId.set(ctx.runId ?? event.runId, childSessionKey);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** Resolve the requester session if it exists, or seed a deferred lazy root placeholder for later promotion. */
|
|
385
|
+
ensureRequesterSessionAnchor(requesterSessionKey, timestampMicros) {
|
|
386
|
+
const trimmedRequesterSessionKey = requesterSessionKey?.trim();
|
|
387
|
+
if (!trimmedRequesterSessionKey) {
|
|
388
|
+
return undefined;
|
|
389
|
+
}
|
|
390
|
+
return (this.resolveTrackedSession({ requesterSessionKey: trimmedRequesterSessionKey }) ??
|
|
391
|
+
this.ensureSession({
|
|
392
|
+
sessionKey: trimmedRequesterSessionKey,
|
|
393
|
+
requesterSessionKey: trimmedRequesterSessionKey,
|
|
394
|
+
source: 'lazy_session',
|
|
395
|
+
timestamp: timestampMicros,
|
|
396
|
+
deferRootOpen: true,
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
/** Open a deferred child session root once the requester scope is known. */
|
|
400
|
+
promoteDeferredSubagentSession(childSessionKey) {
|
|
401
|
+
const session = this.resolveTrackedSession({ sessionKey: childSessionKey, childSessionKey });
|
|
402
|
+
if (!session) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
this.materializeDeferredSessionRoot(session);
|
|
406
|
+
}
|
|
407
|
+
/** Promote any deferred child sessions waiting on a requester root once it exists. */
|
|
408
|
+
promoteDeferredSubagentSessionsForRequester(requesterSessionKey) {
|
|
409
|
+
const trimmedRequesterSessionKey = requesterSessionKey?.trim();
|
|
410
|
+
if (!trimmedRequesterSessionKey) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const requesterSession = this.resolveTrackedSession({ requesterSessionKey: trimmedRequesterSessionKey });
|
|
414
|
+
if (!requesterSession?.rootHandle) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
for (const lineage of this.pendingSubagentLineageByChildSessionKey.values()) {
|
|
418
|
+
if (lineage.requesterSessionKey === trimmedRequesterSessionKey) {
|
|
419
|
+
this.promoteDeferredSubagentSession(lineage.childSessionKey);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/** Materialize one deferred session root with nested lineage when available. */
|
|
424
|
+
materializeDeferredSessionRoot(session) {
|
|
425
|
+
if (session.rootHandle) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
this.pruneExpiredPendingSubagentLineage(Date.now());
|
|
429
|
+
const lineage = this.resolvePendingSubagentLineage({
|
|
430
|
+
sessionId: session.sessionId,
|
|
431
|
+
sessionKey: session.sessionKey,
|
|
432
|
+
});
|
|
433
|
+
const parentSession = lineage === undefined
|
|
434
|
+
? undefined
|
|
435
|
+
: this.ensureRequesterSessionAnchor(lineage.requesterSessionKey, session.pendingRootTimestampMicros);
|
|
436
|
+
if (parentSession && !parentSession.rootHandle) {
|
|
437
|
+
materializeSessionRoot(this.sessionManager(), parentSession, {
|
|
438
|
+
sessionId: parentSession.sessionId,
|
|
439
|
+
sessionKey: parentSession.sessionKey ?? lineage?.requesterSessionKey,
|
|
440
|
+
source: parentSession.source,
|
|
441
|
+
resumedFrom: parentSession.resumedFrom,
|
|
442
|
+
timestamp: parentSession.pendingRootTimestampMicros,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
const parentHandle = parentSession?.rootHandle;
|
|
446
|
+
materializeSessionRoot(this.sessionManager(), session, {
|
|
447
|
+
sessionId: session.sessionId,
|
|
448
|
+
sessionKey: session.sessionKey,
|
|
449
|
+
runId: lineage?.runId,
|
|
450
|
+
agentId: session.agentId ?? lineage?.agentId,
|
|
451
|
+
source: session.source,
|
|
452
|
+
resumedFrom: session.resumedFrom,
|
|
453
|
+
scopeRole: session.scopeRole,
|
|
454
|
+
parentHandle,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/** Resolve stable subagent lineage from child session key first, then run id as a fallback. */
|
|
458
|
+
resolvePendingSubagentLineage(input) {
|
|
459
|
+
const childSessionKey = [input.sessionKey, input.childSessionKey]
|
|
460
|
+
.find((value) => this.isDocumentedSubagentSessionKey(value))
|
|
461
|
+
?.trim();
|
|
462
|
+
if (childSessionKey) {
|
|
463
|
+
return this.pendingSubagentLineageByChildSessionKey.get(childSessionKey);
|
|
464
|
+
}
|
|
465
|
+
const runChildSessionKey = typeof input.runId === 'string' && input.runId.length > 0 ? this.pendingSubagentChildKeyByRunId.get(input.runId) : undefined;
|
|
466
|
+
return runChildSessionKey === undefined ? undefined : this.pendingSubagentLineageByChildSessionKey.get(runChildSessionKey);
|
|
467
|
+
}
|
|
468
|
+
/** Drop stale pending lineage entries so abandoned subagent spawns do not accumulate indefinitely. */
|
|
469
|
+
pruneExpiredPendingSubagentLineage(nowMs) {
|
|
470
|
+
const ttlMs = this.config.correlation.recordTtlMs;
|
|
471
|
+
for (const [childSessionKey, lineage] of this.pendingSubagentLineageByChildSessionKey) {
|
|
472
|
+
if (nowMs - lineage.observedAtMs > ttlMs) {
|
|
473
|
+
this.pendingSubagentLineageByChildSessionKey.delete(childSessionKey);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
for (const [runId, childSessionKey] of this.pendingSubagentChildKeyByRunId) {
|
|
477
|
+
if (!this.pendingSubagentLineageByChildSessionKey.has(childSessionKey)) {
|
|
478
|
+
this.pendingSubagentChildKeyByRunId.delete(runId);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/** Resolve one session through the same alias map used by the replay state. */
|
|
483
|
+
resolveTrackedSession(input) {
|
|
484
|
+
const ownerKey = resolveSessionOwnerKey(this.stateValue, input);
|
|
485
|
+
return ownerKey === undefined ? undefined : this.stateValue.sessions.get(ownerKey);
|
|
486
|
+
}
|
|
487
|
+
/** Free lineage bookkeeping once the child session is closed. */
|
|
488
|
+
forgetPendingSubagentLineage(session) {
|
|
489
|
+
const childSessionKey = session.sessionKey;
|
|
490
|
+
if (childSessionKey) {
|
|
491
|
+
this.pendingSubagentLineageByChildSessionKey.delete(childSessionKey);
|
|
492
|
+
}
|
|
493
|
+
for (const [runId, trackedChildSessionKey] of this.pendingSubagentChildKeyByRunId) {
|
|
494
|
+
if (trackedChildSessionKey === childSessionKey) {
|
|
495
|
+
this.pendingSubagentChildKeyByRunId.delete(runId);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
/** Match the documented native subagent session key shape without depending on private OpenClaw internals. */
|
|
500
|
+
isDocumentedSubagentSessionKey(value) {
|
|
501
|
+
return typeof value === 'string' && /^agent:[^:]+:subagent:/.test(value);
|
|
502
|
+
}
|
|
300
503
|
/** Log one warning per key to avoid noisy repeated hook failures. */
|
|
301
504
|
logBoundedWarn(key, message) {
|
|
302
505
|
const count = this.warningCounts.get(key) ?? 0;
|
|
@@ -307,9 +510,12 @@ export class HookReplayBackend {
|
|
|
307
510
|
}
|
|
308
511
|
}
|
|
309
512
|
export { llmKey };
|
|
310
|
-
/** Expose
|
|
311
|
-
export function
|
|
312
|
-
return
|
|
513
|
+
/** Expose owner-key resolution for tests without exporting the full session module. */
|
|
514
|
+
export function resolveBackendSessionOwnerKey(state, input) {
|
|
515
|
+
return resolveSessionOwnerKey(state, input);
|
|
516
|
+
}
|
|
517
|
+
function microsToMs(timestampMicros) {
|
|
518
|
+
return timestampMicros === undefined ? undefined : Math.trunc(timestampMicros / 1000);
|
|
313
519
|
}
|
|
314
520
|
/** Build the lifecycle summary stored as the session_end mark payload. */
|
|
315
521
|
function sessionEndSummary(event) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nemo-relay-openclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "NeMo Relay-authored observability plugin for OpenClaw.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"openclaw": "^2026.5.26"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"nemo-relay-node": "0.
|
|
66
|
+
"nemo-relay-node": "0.4.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/node": "^24.0.0",
|