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,16 +1,34 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
import { evictExpiredRecords, tupleKey as tupleKeyFromCorrelation } from './correlation.js';
|
|
3
|
+
import { evictExpiredRecords, tupleKey as tupleKeyFromCorrelation, } from './correlation.js';
|
|
4
|
+
import { toJsonRecord } from './marks.js';
|
|
5
|
+
/** Merge lineage/root-context defaults without letting undefined hook fields erase them. */
|
|
6
|
+
function mergeEnsureSessionInput(input, context) {
|
|
7
|
+
if (!context) {
|
|
8
|
+
return input;
|
|
9
|
+
}
|
|
10
|
+
const merged = { ...context, ...input };
|
|
11
|
+
for (const [key, value] of Object.entries(context)) {
|
|
12
|
+
if (merged[key] === undefined && value !== undefined) {
|
|
13
|
+
merged[key] = value;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return merged;
|
|
17
|
+
}
|
|
18
|
+
/** Return the session key that belongs to the session itself, not a requester alias. */
|
|
19
|
+
function ownSessionKey(input) {
|
|
20
|
+
return input.sessionKey ?? input.childSessionKey;
|
|
21
|
+
}
|
|
4
22
|
/** Return all keys that may identify an existing OpenClaw session. */
|
|
5
23
|
export function lookupSessionKeys(input) {
|
|
6
24
|
return [input.sessionId, input.sessionKey, input.requesterSessionKey, input.childSessionKey, input.runId].filter((value) => typeof value === 'string' && value.length > 0);
|
|
7
25
|
}
|
|
8
|
-
/** Return keys that should alias to
|
|
26
|
+
/** Return keys that should alias to one stable internal session owner. */
|
|
9
27
|
export function aliasSessionKeys(input) {
|
|
10
|
-
return [input.sessionId, input.sessionKey, input.requesterSessionKey, input.runId].filter((value) => typeof value === 'string' && value.length > 0);
|
|
28
|
+
return [input.sessionId, input.sessionKey, input.childSessionKey, input.requesterSessionKey, input.runId].filter((value) => typeof value === 'string' && value.length > 0);
|
|
11
29
|
}
|
|
12
|
-
/** Resolve a hook's session identity to the
|
|
13
|
-
export function
|
|
30
|
+
/** Resolve a hook's session identity to the stable owner key used in replay state. */
|
|
31
|
+
export function resolveSessionOwnerKey(state, input) {
|
|
14
32
|
for (const key of lookupSessionKeys(input)) {
|
|
15
33
|
const canonical = state.sessionAliases.get(key);
|
|
16
34
|
if (canonical) {
|
|
@@ -19,10 +37,10 @@ export function resolveSessionKey(state, input) {
|
|
|
19
37
|
}
|
|
20
38
|
return input.sessionId ?? input.sessionKey ?? input.childSessionKey ?? input.runId;
|
|
21
39
|
}
|
|
22
|
-
/** Remember equivalent hook identifiers so later events attach to the same root span. */
|
|
40
|
+
/** Remember equivalent hook identifiers so later events attach to the same owner and root span. */
|
|
23
41
|
export function rememberSessionAliases(state, session, input) {
|
|
24
42
|
for (const alias of aliasSessionKeys(input)) {
|
|
25
|
-
state.sessionAliases.set(alias, session.
|
|
43
|
+
state.sessionAliases.set(alias, session.ownerKey);
|
|
26
44
|
}
|
|
27
45
|
}
|
|
28
46
|
/** Create the mutable in-memory state used by the hook replay backend. */
|
|
@@ -45,42 +63,96 @@ export function createHookReplayState() {
|
|
|
45
63
|
}
|
|
46
64
|
/** Return an existing session or lazily create a root session scope for replay. */
|
|
47
65
|
export function ensureSession(manager, input) {
|
|
48
|
-
const
|
|
49
|
-
|
|
66
|
+
const resolvedInput = mergeEnsureSessionInput(input, manager.resolveSessionRootContext?.(input));
|
|
67
|
+
const ownerKey = resolveSessionOwnerKey(manager.state, resolvedInput);
|
|
68
|
+
if (!ownerKey) {
|
|
50
69
|
manager.state.counters.skippedEvents += 1;
|
|
51
70
|
manager.logBoundedWarn('missing-session-key', 'nemo-relay skipped replay because no session/run key was available');
|
|
52
71
|
return undefined;
|
|
53
72
|
}
|
|
54
|
-
const existing = manager.state.sessions.get(
|
|
73
|
+
const existing = manager.state.sessions.get(ownerKey);
|
|
55
74
|
if (existing) {
|
|
56
|
-
|
|
75
|
+
enrichSession(existing, resolvedInput);
|
|
76
|
+
rememberSessionAliases(manager.state, existing, resolvedInput);
|
|
77
|
+
if (!existing.rootHandle && resolvedInput.deferRootOpen !== true) {
|
|
78
|
+
materializeSessionRoot(manager, existing, resolvedInput);
|
|
79
|
+
}
|
|
57
80
|
return existing;
|
|
58
81
|
}
|
|
59
|
-
const canonicalSessionId = input.sessionId ?? key;
|
|
60
|
-
const aliased = manager.state.sessions.get(canonicalSessionId);
|
|
61
|
-
if (aliased) {
|
|
62
|
-
rememberSessionAliases(manager.state, aliased, input);
|
|
63
|
-
return aliased;
|
|
64
|
-
}
|
|
65
82
|
const stack = manager.nf.createScopeStack();
|
|
66
83
|
const session = {
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
ownerKey,
|
|
85
|
+
sessionId: resolvedInput.sessionId ?? ownerKey,
|
|
86
|
+
source: resolvedInput.source,
|
|
69
87
|
stack,
|
|
70
88
|
};
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
const sessionKey = ownSessionKey(resolvedInput);
|
|
90
|
+
if (sessionKey !== undefined) {
|
|
91
|
+
session.sessionKey = sessionKey;
|
|
92
|
+
}
|
|
93
|
+
if (resolvedInput.agentId !== undefined) {
|
|
94
|
+
session.agentId = resolvedInput.agentId;
|
|
95
|
+
}
|
|
96
|
+
if (resolvedInput.resumedFrom !== undefined) {
|
|
97
|
+
session.resumedFrom = resolvedInput.resumedFrom;
|
|
98
|
+
}
|
|
99
|
+
if (resolvedInput.scopeRole !== undefined) {
|
|
100
|
+
session.scopeRole = resolvedInput.scopeRole;
|
|
101
|
+
}
|
|
102
|
+
if (resolvedInput.deferRootOpen === true) {
|
|
103
|
+
session.pendingRootOpen = true;
|
|
104
|
+
if (resolvedInput.timestamp !== undefined) {
|
|
105
|
+
session.pendingRootTimestampMicros = resolvedInput.timestamp;
|
|
106
|
+
}
|
|
107
|
+
if (resolvedInput.runId !== undefined) {
|
|
108
|
+
session.pendingRootRunId = resolvedInput.runId;
|
|
109
|
+
}
|
|
110
|
+
session.pendingCapturedEmits = [];
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
materializeSessionRoot(manager, session, resolvedInput);
|
|
114
|
+
}
|
|
115
|
+
manager.state.sessions.set(session.ownerKey, session);
|
|
116
|
+
rememberSessionAliases(manager.state, session, resolvedInput);
|
|
117
|
+
return session;
|
|
118
|
+
}
|
|
119
|
+
/** Fill stable session identifiers from later hooks without clobbering established values. */
|
|
120
|
+
function enrichSession(session, input) {
|
|
121
|
+
const sessionKey = ownSessionKey(input);
|
|
122
|
+
if (session.sessionKey === undefined && sessionKey !== undefined) {
|
|
123
|
+
session.sessionKey = sessionKey;
|
|
73
124
|
}
|
|
74
|
-
if (input.agentId !== undefined) {
|
|
125
|
+
if (session.agentId === undefined && input.agentId !== undefined) {
|
|
75
126
|
session.agentId = input.agentId;
|
|
76
127
|
}
|
|
77
|
-
if (input.resumedFrom !== undefined) {
|
|
128
|
+
if (session.resumedFrom === undefined && input.resumedFrom !== undefined) {
|
|
78
129
|
session.resumedFrom = input.resumedFrom;
|
|
79
130
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
if (session.scopeRole === undefined && input.scopeRole !== undefined) {
|
|
132
|
+
session.scopeRole = input.scopeRole;
|
|
133
|
+
}
|
|
134
|
+
if (!session.rootHandle && input.sessionId !== undefined) {
|
|
135
|
+
session.sessionId = input.sessionId;
|
|
136
|
+
}
|
|
137
|
+
if (!session.rootHandle && session.source === 'lazy_session' && input.source === 'session_start') {
|
|
138
|
+
session.source = 'session_start';
|
|
139
|
+
}
|
|
140
|
+
if (!session.rootHandle && input.timestamp !== undefined) {
|
|
141
|
+
if (input.source === 'session_start' || session.pendingRootTimestampMicros === undefined) {
|
|
142
|
+
session.pendingRootTimestampMicros = input.timestamp;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!session.rootHandle && session.pendingRootRunId === undefined && input.runId !== undefined) {
|
|
146
|
+
session.pendingRootRunId = input.runId;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** Queue an emit callback until a deferred session root can open with honest lineage. */
|
|
150
|
+
export function queueCapturedEmit(session, label, emit) {
|
|
151
|
+
if (session.rootHandle || session.pendingRootOpen !== true) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
(session.pendingCapturedEmits ??= []).push({ label, emit });
|
|
155
|
+
return true;
|
|
84
156
|
}
|
|
85
157
|
/** Flush pending LLM output/timing state before the root session closes. */
|
|
86
158
|
export function drainSession(manager, session) {
|
|
@@ -90,12 +162,12 @@ export function drainSession(manager, session) {
|
|
|
90
162
|
evictSessionCorrelationRecords(manager.state, session);
|
|
91
163
|
}
|
|
92
164
|
/** Close the root session scope with separate lifecycle summary and user-visible output. */
|
|
93
|
-
export function closeSessionRoot(manager, session, summary, rootOutput = summary, timestamp) {
|
|
165
|
+
export function closeSessionRoot(manager, session, summary, rootOutput = summary, metadata, timestamp) {
|
|
94
166
|
manager.emitCapturedUnderSession('session_end', session, () => {
|
|
95
167
|
if (!session.rootHandle) {
|
|
96
168
|
return;
|
|
97
169
|
}
|
|
98
|
-
manager.nf.event('openclaw.session_end', session.rootHandle, summary, null, timestamp ?? null);
|
|
170
|
+
manager.nf.event('openclaw.session_end', session.rootHandle, summary, metadata ?? null, timestamp ?? null);
|
|
99
171
|
manager.state.counters.marksEmitted += 1;
|
|
100
172
|
manager.nf.popScope(session.rootHandle, rootOutput, timestamp ?? null);
|
|
101
173
|
delete session.rootHandle;
|
|
@@ -103,7 +175,7 @@ export function closeSessionRoot(manager, session, summary, rootOutput = summary
|
|
|
103
175
|
}
|
|
104
176
|
/** Remove a closed session from active replay state. */
|
|
105
177
|
export function deleteSession(state, session) {
|
|
106
|
-
state.sessions.delete(session.
|
|
178
|
+
state.sessions.delete(session.ownerKey);
|
|
107
179
|
}
|
|
108
180
|
/** Insert a correlation record while bounding retained entries per key. */
|
|
109
181
|
export function insertBoundedRecord(map, key, record, maxRecordsPerKey) {
|
|
@@ -125,27 +197,55 @@ export function evictExpiredCorrelationRecords(state, nowMs, ttlMs) {
|
|
|
125
197
|
evictExpiredRecords(state.modelCallsByCallId, nowMs, ttlMs);
|
|
126
198
|
evictExpiredRecords(state.modelTimingsByLlmKey, nowMs, ttlMs);
|
|
127
199
|
}
|
|
128
|
-
/** Open
|
|
129
|
-
function
|
|
200
|
+
/** Open a deferred or new root session scope and flush queued child emissions. */
|
|
201
|
+
export function materializeSessionRoot(manager, session, input) {
|
|
202
|
+
if (session.rootHandle) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
enrichSession(session, input);
|
|
206
|
+
const timestampMicros = input.timestamp ?? session.pendingRootTimestampMicros ?? null;
|
|
207
|
+
const rootRunId = input.runId ?? session.pendingRootRunId;
|
|
130
208
|
const data = {
|
|
131
209
|
sessionId: session.sessionId,
|
|
132
210
|
source: session.source,
|
|
133
211
|
...(session.sessionKey === undefined ? {} : { sessionKey: session.sessionKey }),
|
|
134
212
|
...(session.agentId === undefined ? {} : { agentId: session.agentId }),
|
|
135
|
-
...(
|
|
213
|
+
...(rootRunId === undefined ? {} : { runId: rootRunId }),
|
|
136
214
|
...(session.resumedFrom === undefined ? {} : { resumedFrom: session.resumedFrom }),
|
|
137
215
|
};
|
|
138
|
-
|
|
139
|
-
session.
|
|
140
|
-
|
|
141
|
-
|
|
216
|
+
const metadata = toJsonRecord({
|
|
217
|
+
source: session.source === 'session_start' ? 'openclaw.session_start' : 'openclaw.lazy_session',
|
|
218
|
+
hook_event_name: session.source === 'session_start' ? 'session_start' : undefined,
|
|
219
|
+
sessionId: session.sessionId,
|
|
220
|
+
sessionKey: session.sessionKey,
|
|
221
|
+
agentId: session.agentId,
|
|
222
|
+
runId: rootRunId,
|
|
223
|
+
nemo_relay_scope_role: session.scopeRole,
|
|
142
224
|
});
|
|
225
|
+
const previousStack = manager.nf.currentScopeStack();
|
|
226
|
+
try {
|
|
227
|
+
manager.nf.setThreadScopeStack(session.stack);
|
|
228
|
+
session.rootHandle = manager.nf.pushScope('openclaw.session', agentScopeType(manager.nf), input.parentHandle ?? null, null, data, metadata, data, timestampMicros);
|
|
229
|
+
manager.nf.event('openclaw.session_start', session.rootHandle, data, metadata, timestampMicros);
|
|
230
|
+
manager.state.counters.marksEmitted += 1;
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
manager.nf.setThreadScopeStack(previousStack);
|
|
234
|
+
}
|
|
235
|
+
delete session.pendingRootOpen;
|
|
236
|
+
delete session.pendingRootTimestampMicros;
|
|
237
|
+
delete session.pendingRootRunId;
|
|
238
|
+
const pendingEmits = session.pendingCapturedEmits ?? [];
|
|
239
|
+
delete session.pendingCapturedEmits;
|
|
240
|
+
for (const pending of pendingEmits) {
|
|
241
|
+
manager.emitCapturedUnderSession(pending.label, session, pending.emit);
|
|
242
|
+
}
|
|
143
243
|
}
|
|
144
244
|
/** Cancel timers that would otherwise replay late LLM outputs after session close. */
|
|
145
245
|
function cancelPendingLlmOutputTimers(state, session) {
|
|
146
246
|
for (const records of state.llmOutputsPendingInput.values()) {
|
|
147
247
|
for (const record of records) {
|
|
148
|
-
if (record.
|
|
248
|
+
if (record.sessionOwnerKey === session.ownerKey && record.timer) {
|
|
149
249
|
clearTimeout(record.timer);
|
|
150
250
|
record.timer = undefined;
|
|
151
251
|
}
|
|
@@ -154,20 +254,20 @@ function cancelPendingLlmOutputTimers(state, session) {
|
|
|
154
254
|
}
|
|
155
255
|
/** Remove all correlation records and aliases owned by a closed session. */
|
|
156
256
|
function evictSessionCorrelationRecords(state, session) {
|
|
157
|
-
evictFromRecordMap(state.llmInputs, session.
|
|
158
|
-
evictFromRecordMap(state.llmOutputsPendingInput, session.
|
|
159
|
-
evictFromRecordMap(state.modelCallsByCallId, session.
|
|
160
|
-
evictFromRecordMap(state.modelTimingsByLlmKey, session.
|
|
161
|
-
for (const [alias,
|
|
162
|
-
if (
|
|
257
|
+
evictFromRecordMap(state.llmInputs, session.ownerKey);
|
|
258
|
+
evictFromRecordMap(state.llmOutputsPendingInput, session.ownerKey);
|
|
259
|
+
evictFromRecordMap(state.modelCallsByCallId, session.ownerKey);
|
|
260
|
+
evictFromRecordMap(state.modelTimingsByLlmKey, session.ownerKey);
|
|
261
|
+
for (const [alias, ownerKey] of state.sessionAliases) {
|
|
262
|
+
if (ownerKey === session.ownerKey) {
|
|
163
263
|
state.sessionAliases.delete(alias);
|
|
164
264
|
}
|
|
165
265
|
}
|
|
166
266
|
}
|
|
167
267
|
/** Drop records for one session from a single keyed correlation map. */
|
|
168
|
-
function evictFromRecordMap(map,
|
|
268
|
+
function evictFromRecordMap(map, ownerKey) {
|
|
169
269
|
for (const [key, records] of map) {
|
|
170
|
-
const retained = records.filter((record) => record.
|
|
270
|
+
const retained = records.filter((record) => record.sessionOwnerKey !== ownerKey);
|
|
171
271
|
if (retained.length === 0) {
|
|
172
272
|
map.delete(key);
|
|
173
273
|
}
|
|
@@ -5,12 +5,14 @@ import { ensureSession } from './session.js';
|
|
|
5
5
|
import { nowMicros, startMicrosFromDuration } from './correlation.js';
|
|
6
6
|
/** Run NeMo Relay tool conditional-execution guardrails before OpenClaw executes a tool. */
|
|
7
7
|
export async function guardBeforeToolCall(manager, event, ctx) {
|
|
8
|
+
const observedAtMicros = nowMicros();
|
|
8
9
|
const session = ensureSession(manager, {
|
|
9
10
|
sessionId: ctx.sessionId,
|
|
10
11
|
sessionKey: ctx.sessionKey,
|
|
11
12
|
runId: event.runId ?? ctx.runId,
|
|
12
13
|
agentId: ctx.agentId,
|
|
13
14
|
source: 'lazy_session',
|
|
15
|
+
timestamp: observedAtMicros,
|
|
14
16
|
});
|
|
15
17
|
const args = toJsonValue(event.params ?? {});
|
|
16
18
|
if (session) {
|
|
@@ -29,12 +31,15 @@ export async function guardBeforeToolCall(manager, event, ctx) {
|
|
|
29
31
|
}
|
|
30
32
|
/** Convert one OpenClaw after_tool_call event into a NeMo Relay tool span or blocked-tool mark. */
|
|
31
33
|
export function replayAfterToolCall(manager, event, ctx) {
|
|
34
|
+
const endMicros = nowMicros();
|
|
35
|
+
const sessionTimestamp = startMicrosFromDuration(endMicros, event.durationMs) ?? endMicros;
|
|
32
36
|
const session = ensureSession(manager, {
|
|
33
37
|
sessionId: ctx.sessionId,
|
|
34
38
|
sessionKey: ctx.sessionKey,
|
|
35
39
|
runId: event.runId ?? ctx.runId,
|
|
36
40
|
agentId: ctx.agentId,
|
|
37
41
|
source: 'lazy_session',
|
|
42
|
+
timestamp: sessionTimestamp,
|
|
38
43
|
});
|
|
39
44
|
const blockedDetails = blockedToolDetails(event, { runId: event.runId ?? ctx.runId });
|
|
40
45
|
if (session && blockedDetails) {
|
|
@@ -45,6 +50,16 @@ export function replayAfterToolCall(manager, event, ctx) {
|
|
|
45
50
|
session,
|
|
46
51
|
name: 'openclaw.tool_blocked',
|
|
47
52
|
data: blockedDetails,
|
|
53
|
+
metadata: toJsonRecord({
|
|
54
|
+
source: 'openclaw.after_tool_call',
|
|
55
|
+
hook_event_name: 'after_tool_call',
|
|
56
|
+
sessionId: session.sessionId,
|
|
57
|
+
sessionKey: session.sessionKey,
|
|
58
|
+
agentId: session.agentId,
|
|
59
|
+
runId: event.runId ?? ctx.runId,
|
|
60
|
+
toolCallId: event.toolCallId ?? ctx.toolCallId,
|
|
61
|
+
}),
|
|
62
|
+
timestamp: endMicros,
|
|
48
63
|
});
|
|
49
64
|
});
|
|
50
65
|
return;
|
|
@@ -52,12 +67,12 @@ export function replayAfterToolCall(manager, event, ctx) {
|
|
|
52
67
|
if (!session) {
|
|
53
68
|
return;
|
|
54
69
|
}
|
|
55
|
-
const endMicros = nowMicros();
|
|
56
70
|
const metadata = toJsonRecord({
|
|
57
71
|
source: 'openclaw.after_tool_call',
|
|
58
72
|
runId: event.runId ?? ctx.runId,
|
|
59
|
-
sessionId:
|
|
60
|
-
sessionKey:
|
|
73
|
+
sessionId: session.sessionId,
|
|
74
|
+
sessionKey: session.sessionKey,
|
|
75
|
+
agentId: session.agentId,
|
|
61
76
|
toolCallId: event.toolCallId ?? ctx.toolCallId,
|
|
62
77
|
durationMs: event.durationMs,
|
|
63
78
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { NemoRelayHookBackendConfig } from './config.js';
|
|
9
9
|
import { llmKey } from './hook-replay/correlation.js';
|
|
10
|
-
import {
|
|
10
|
+
import { resolveSessionOwnerKey, type HookReplayBackendState, type SessionLookupInput, type SessionState } from './hook-replay/session.js';
|
|
11
11
|
import type { NemoRelayRuntimeModule } from './modules.js';
|
|
12
12
|
import type { PluginHookAfterToolCallEvent, PluginHookAgentContext, PluginHookAgentEndEvent, PluginHookBeforeAgentFinalizeEvent, PluginHookBeforeMessageWriteContext, PluginHookBeforeMessageWriteEvent, PluginHookBeforeToolCallEvent, PluginHookGatewayContext, PluginHookGatewayStartEvent, PluginHookLlmInputEvent, PluginHookLlmOutputEvent, PluginHookModelCallEndedEvent, PluginHookModelCallStartedEvent, PluginHookSessionContext, PluginHookSessionEndEvent, PluginHookSessionStartEvent, PluginHookSubagentContext, PluginHookSubagentEndedEvent, PluginHookSubagentSpawnedEvent, PluginHookToolContext } from './openclaw-hook-types.js';
|
|
13
13
|
import type { PluginLogger } from 'openclaw/plugin-sdk/plugin-entry';
|
|
@@ -25,6 +25,8 @@ export declare class HookReplayBackend {
|
|
|
25
25
|
private readonly agentVersion;
|
|
26
26
|
private readonly stateValue;
|
|
27
27
|
private readonly warningCounts;
|
|
28
|
+
private readonly pendingSubagentLineageByChildSessionKey;
|
|
29
|
+
private readonly pendingSubagentChildKeyByRunId;
|
|
28
30
|
constructor(options: HookReplayBackendOptions);
|
|
29
31
|
/** Return mutable replay state for tests and health snapshots. */
|
|
30
32
|
state(): HookReplayBackendState;
|
|
@@ -88,9 +90,31 @@ export declare class HookReplayBackend {
|
|
|
88
90
|
private flushSubscriberDelivery;
|
|
89
91
|
/** Build the narrow manager interface consumed by focused replay modules. */
|
|
90
92
|
private sessionManager;
|
|
93
|
+
/** Prefer nested child scopes only when the hook surface provides real subagent lineage. */
|
|
94
|
+
private resolveSessionRootContext;
|
|
95
|
+
/** Track stable parent/child lineage from subagent hooks until child session hooks can use it. */
|
|
96
|
+
private trackPendingSubagentLineage;
|
|
97
|
+
/** Resolve the requester session if it exists, or seed a deferred lazy root placeholder for later promotion. */
|
|
98
|
+
private ensureRequesterSessionAnchor;
|
|
99
|
+
/** Open a deferred child session root once the requester scope is known. */
|
|
100
|
+
private promoteDeferredSubagentSession;
|
|
101
|
+
/** Promote any deferred child sessions waiting on a requester root once it exists. */
|
|
102
|
+
private promoteDeferredSubagentSessionsForRequester;
|
|
103
|
+
/** Materialize one deferred session root with nested lineage when available. */
|
|
104
|
+
private materializeDeferredSessionRoot;
|
|
105
|
+
/** Resolve stable subagent lineage from child session key first, then run id as a fallback. */
|
|
106
|
+
private resolvePendingSubagentLineage;
|
|
107
|
+
/** Drop stale pending lineage entries so abandoned subagent spawns do not accumulate indefinitely. */
|
|
108
|
+
private pruneExpiredPendingSubagentLineage;
|
|
109
|
+
/** Resolve one session through the same alias map used by the replay state. */
|
|
110
|
+
private resolveTrackedSession;
|
|
111
|
+
/** Free lineage bookkeeping once the child session is closed. */
|
|
112
|
+
private forgetPendingSubagentLineage;
|
|
113
|
+
/** Match the documented native subagent session key shape without depending on private OpenClaw internals. */
|
|
114
|
+
private isDocumentedSubagentSessionKey;
|
|
91
115
|
/** Log one warning per key to avoid noisy repeated hook failures. */
|
|
92
116
|
private logBoundedWarn;
|
|
93
117
|
}
|
|
94
118
|
export { llmKey };
|
|
95
|
-
/** Expose
|
|
96
|
-
export declare function
|
|
119
|
+
/** Expose owner-key resolution for tests without exporting the full session module. */
|
|
120
|
+
export declare function resolveBackendSessionOwnerKey(state: HookReplayBackendState, input: Parameters<typeof resolveSessionOwnerKey>[1]): string | undefined;
|