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.
- package/README.md +2 -2
- package/dist/index.d.ts +10 -8
- package/dist/index.js +2 -1
- 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/index.ts +5 -1
- 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
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* those identifiers and owns the root `openclaw.session` scope lifecycle.
|
|
10
10
|
*/
|
|
11
11
|
import type { NemoRelayHookBackendConfig } from '../config.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
evictExpiredRecords,
|
|
14
|
+
tupleKey as tupleKeyFromCorrelation,
|
|
15
|
+
} from './correlation.js';
|
|
13
16
|
import type {
|
|
14
17
|
PluginHookAgentContext,
|
|
15
18
|
PluginHookLlmOutputEvent,
|
|
@@ -18,6 +21,7 @@ import type {
|
|
|
18
21
|
import type { PluginLogger } from 'openclaw/plugin-sdk/plugin-entry';
|
|
19
22
|
import type { JsonObject as JsonRecord } from 'nemo-relay-node/typed';
|
|
20
23
|
import type { NemoRelayRuntimeModule } from '../modules.js';
|
|
24
|
+
import { toJsonRecord } from './marks.js';
|
|
21
25
|
|
|
22
26
|
export type SessionLookupInput = {
|
|
23
27
|
sessionId?: string | undefined;
|
|
@@ -29,12 +33,17 @@ export type SessionLookupInput = {
|
|
|
29
33
|
|
|
30
34
|
export type EnsureSessionInput = SessionLookupInput & {
|
|
31
35
|
agentId?: string | undefined;
|
|
36
|
+
parentHandle?: ReturnType<NemoRelayRuntimeModule['pushScope']> | undefined;
|
|
37
|
+
scopeRole?: 'subagent' | undefined;
|
|
32
38
|
source: 'session_start' | 'lazy_session';
|
|
33
39
|
resumedFrom?: string | undefined;
|
|
34
40
|
timestamp?: number | undefined;
|
|
41
|
+
deferRootOpen?: boolean | undefined;
|
|
35
42
|
};
|
|
36
43
|
|
|
37
44
|
export type SessionState = {
|
|
45
|
+
/** Immutable internal owner key used for replay buffers and alias lookups. */
|
|
46
|
+
ownerKey: string;
|
|
38
47
|
sessionId: string;
|
|
39
48
|
sessionKey?: string;
|
|
40
49
|
agentId?: string;
|
|
@@ -51,10 +60,15 @@ export type SessionState = {
|
|
|
51
60
|
assistantMessageWrites?: AssistantMessageRecord[];
|
|
52
61
|
stack: ReturnType<NemoRelayRuntimeModule['createScopeStack']>;
|
|
53
62
|
rootHandle?: ReturnType<NemoRelayRuntimeModule['pushScope']>;
|
|
63
|
+
scopeRole?: 'subagent';
|
|
64
|
+
pendingRootOpen?: boolean;
|
|
65
|
+
pendingRootTimestampMicros?: number;
|
|
66
|
+
pendingRootRunId?: string;
|
|
67
|
+
pendingCapturedEmits?: Array<{ label: string; emit: () => void }>;
|
|
54
68
|
};
|
|
55
69
|
|
|
56
70
|
export type PendingLlmOutputRecord = {
|
|
57
|
-
|
|
71
|
+
sessionOwnerKey: string;
|
|
58
72
|
sessionId: string;
|
|
59
73
|
runId: string;
|
|
60
74
|
provider: string;
|
|
@@ -66,7 +80,7 @@ export type PendingLlmOutputRecord = {
|
|
|
66
80
|
};
|
|
67
81
|
|
|
68
82
|
export type LlmInputRecord = {
|
|
69
|
-
|
|
83
|
+
sessionOwnerKey: string;
|
|
70
84
|
sessionId: string;
|
|
71
85
|
runId: string;
|
|
72
86
|
provider: string;
|
|
@@ -80,7 +94,7 @@ export type LlmInputRecord = {
|
|
|
80
94
|
};
|
|
81
95
|
|
|
82
96
|
export type AssistantMessageRecord = {
|
|
83
|
-
|
|
97
|
+
sessionOwnerKey: string;
|
|
84
98
|
provider: string;
|
|
85
99
|
model: string;
|
|
86
100
|
assistantTexts: string[];
|
|
@@ -93,7 +107,7 @@ export type AssistantMessageRecord = {
|
|
|
93
107
|
};
|
|
94
108
|
|
|
95
109
|
export type ModelCallRecord = {
|
|
96
|
-
|
|
110
|
+
sessionOwnerKey: string;
|
|
97
111
|
runId: string;
|
|
98
112
|
callId: string;
|
|
99
113
|
provider: string;
|
|
@@ -144,8 +158,32 @@ export type SessionManager = {
|
|
|
144
158
|
replayPendingLlmOutputsForSession: (session: SessionState, options: { allowPlaceholderRequest: boolean }) => void;
|
|
145
159
|
emitUnpairedModelCallTimingMarks: (session: SessionState) => void;
|
|
146
160
|
logBoundedWarn: (key: string, message: string) => void;
|
|
161
|
+
resolveSessionRootContext?: (input: EnsureSessionInput) => Partial<EnsureSessionInput> | undefined;
|
|
147
162
|
};
|
|
148
163
|
|
|
164
|
+
/** Merge lineage/root-context defaults without letting undefined hook fields erase them. */
|
|
165
|
+
function mergeEnsureSessionInput(
|
|
166
|
+
input: EnsureSessionInput,
|
|
167
|
+
context: Partial<EnsureSessionInput> | undefined,
|
|
168
|
+
): EnsureSessionInput {
|
|
169
|
+
if (!context) {
|
|
170
|
+
return input;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const merged: Record<string, unknown> = { ...context, ...input };
|
|
174
|
+
for (const [key, value] of Object.entries(context)) {
|
|
175
|
+
if (merged[key] === undefined && value !== undefined) {
|
|
176
|
+
merged[key] = value;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return merged as EnsureSessionInput;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Return the session key that belongs to the session itself, not a requester alias. */
|
|
183
|
+
function ownSessionKey(input: SessionLookupInput): string | undefined {
|
|
184
|
+
return input.sessionKey ?? input.childSessionKey;
|
|
185
|
+
}
|
|
186
|
+
|
|
149
187
|
/** Return all keys that may identify an existing OpenClaw session. */
|
|
150
188
|
export function lookupSessionKeys(input: SessionLookupInput): string[] {
|
|
151
189
|
return [input.sessionId, input.sessionKey, input.requesterSessionKey, input.childSessionKey, input.runId].filter(
|
|
@@ -153,15 +191,15 @@ export function lookupSessionKeys(input: SessionLookupInput): string[] {
|
|
|
153
191
|
);
|
|
154
192
|
}
|
|
155
193
|
|
|
156
|
-
/** Return keys that should alias to
|
|
194
|
+
/** Return keys that should alias to one stable internal session owner. */
|
|
157
195
|
export function aliasSessionKeys(input: SessionLookupInput): string[] {
|
|
158
|
-
return [input.sessionId, input.sessionKey, input.requesterSessionKey, input.runId].filter(
|
|
196
|
+
return [input.sessionId, input.sessionKey, input.childSessionKey, input.requesterSessionKey, input.runId].filter(
|
|
159
197
|
(value): value is string => typeof value === 'string' && value.length > 0,
|
|
160
198
|
);
|
|
161
199
|
}
|
|
162
200
|
|
|
163
|
-
/** Resolve a hook's session identity to the
|
|
164
|
-
export function
|
|
201
|
+
/** Resolve a hook's session identity to the stable owner key used in replay state. */
|
|
202
|
+
export function resolveSessionOwnerKey(state: HookReplayBackendState, input: SessionLookupInput): string | undefined {
|
|
165
203
|
for (const key of lookupSessionKeys(input)) {
|
|
166
204
|
const canonical = state.sessionAliases.get(key);
|
|
167
205
|
if (canonical) {
|
|
@@ -172,14 +210,14 @@ export function resolveSessionKey(state: HookReplayBackendState, input: SessionL
|
|
|
172
210
|
return input.sessionId ?? input.sessionKey ?? input.childSessionKey ?? input.runId;
|
|
173
211
|
}
|
|
174
212
|
|
|
175
|
-
/** Remember equivalent hook identifiers so later events attach to the same root span. */
|
|
213
|
+
/** Remember equivalent hook identifiers so later events attach to the same owner and root span. */
|
|
176
214
|
export function rememberSessionAliases(
|
|
177
215
|
state: HookReplayBackendState,
|
|
178
216
|
session: SessionState,
|
|
179
217
|
input: SessionLookupInput,
|
|
180
218
|
): void {
|
|
181
219
|
for (const alias of aliasSessionKeys(input)) {
|
|
182
|
-
state.sessionAliases.set(alias, session.
|
|
220
|
+
state.sessionAliases.set(alias, session.ownerKey);
|
|
183
221
|
}
|
|
184
222
|
}
|
|
185
223
|
|
|
@@ -204,47 +242,102 @@ export function createHookReplayState(): HookReplayBackendState {
|
|
|
204
242
|
|
|
205
243
|
/** Return an existing session or lazily create a root session scope for replay. */
|
|
206
244
|
export function ensureSession(manager: SessionManager, input: EnsureSessionInput): SessionState | undefined {
|
|
207
|
-
const
|
|
208
|
-
|
|
245
|
+
const resolvedInput = mergeEnsureSessionInput(input, manager.resolveSessionRootContext?.(input));
|
|
246
|
+
const ownerKey = resolveSessionOwnerKey(manager.state, resolvedInput);
|
|
247
|
+
if (!ownerKey) {
|
|
209
248
|
manager.state.counters.skippedEvents += 1;
|
|
210
249
|
manager.logBoundedWarn('missing-session-key', 'nemo-relay skipped replay because no session/run key was available');
|
|
211
250
|
return undefined;
|
|
212
251
|
}
|
|
213
252
|
|
|
214
|
-
const existing = manager.state.sessions.get(
|
|
253
|
+
const existing = manager.state.sessions.get(ownerKey);
|
|
215
254
|
if (existing) {
|
|
216
|
-
|
|
255
|
+
enrichSession(existing, resolvedInput);
|
|
256
|
+
rememberSessionAliases(manager.state, existing, resolvedInput);
|
|
257
|
+
if (!existing.rootHandle && resolvedInput.deferRootOpen !== true) {
|
|
258
|
+
materializeSessionRoot(manager, existing, resolvedInput);
|
|
259
|
+
}
|
|
217
260
|
return existing;
|
|
218
261
|
}
|
|
219
262
|
|
|
220
|
-
const canonicalSessionId = input.sessionId ?? key;
|
|
221
|
-
const aliased = manager.state.sessions.get(canonicalSessionId);
|
|
222
|
-
if (aliased) {
|
|
223
|
-
rememberSessionAliases(manager.state, aliased, input);
|
|
224
|
-
return aliased;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
263
|
const stack = manager.nf.createScopeStack();
|
|
228
264
|
const session: SessionState = {
|
|
229
|
-
|
|
230
|
-
|
|
265
|
+
ownerKey,
|
|
266
|
+
sessionId: resolvedInput.sessionId ?? ownerKey,
|
|
267
|
+
source: resolvedInput.source,
|
|
231
268
|
stack,
|
|
232
269
|
};
|
|
233
270
|
|
|
234
|
-
|
|
235
|
-
|
|
271
|
+
const sessionKey = ownSessionKey(resolvedInput);
|
|
272
|
+
if (sessionKey !== undefined) {
|
|
273
|
+
session.sessionKey = sessionKey;
|
|
274
|
+
}
|
|
275
|
+
if (resolvedInput.agentId !== undefined) {
|
|
276
|
+
session.agentId = resolvedInput.agentId;
|
|
236
277
|
}
|
|
237
|
-
if (
|
|
278
|
+
if (resolvedInput.resumedFrom !== undefined) {
|
|
279
|
+
session.resumedFrom = resolvedInput.resumedFrom;
|
|
280
|
+
}
|
|
281
|
+
if (resolvedInput.scopeRole !== undefined) {
|
|
282
|
+
session.scopeRole = resolvedInput.scopeRole;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (resolvedInput.deferRootOpen === true) {
|
|
286
|
+
session.pendingRootOpen = true;
|
|
287
|
+
if (resolvedInput.timestamp !== undefined) {
|
|
288
|
+
session.pendingRootTimestampMicros = resolvedInput.timestamp;
|
|
289
|
+
}
|
|
290
|
+
if (resolvedInput.runId !== undefined) {
|
|
291
|
+
session.pendingRootRunId = resolvedInput.runId;
|
|
292
|
+
}
|
|
293
|
+
session.pendingCapturedEmits = [];
|
|
294
|
+
} else {
|
|
295
|
+
materializeSessionRoot(manager, session, resolvedInput);
|
|
296
|
+
}
|
|
297
|
+
manager.state.sessions.set(session.ownerKey, session);
|
|
298
|
+
rememberSessionAliases(manager.state, session, resolvedInput);
|
|
299
|
+
return session;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Fill stable session identifiers from later hooks without clobbering established values. */
|
|
303
|
+
function enrichSession(session: SessionState, input: EnsureSessionInput): void {
|
|
304
|
+
const sessionKey = ownSessionKey(input);
|
|
305
|
+
if (session.sessionKey === undefined && sessionKey !== undefined) {
|
|
306
|
+
session.sessionKey = sessionKey;
|
|
307
|
+
}
|
|
308
|
+
if (session.agentId === undefined && input.agentId !== undefined) {
|
|
238
309
|
session.agentId = input.agentId;
|
|
239
310
|
}
|
|
240
|
-
if (input.resumedFrom !== undefined) {
|
|
311
|
+
if (session.resumedFrom === undefined && input.resumedFrom !== undefined) {
|
|
241
312
|
session.resumedFrom = input.resumedFrom;
|
|
242
313
|
}
|
|
314
|
+
if (session.scopeRole === undefined && input.scopeRole !== undefined) {
|
|
315
|
+
session.scopeRole = input.scopeRole;
|
|
316
|
+
}
|
|
317
|
+
if (!session.rootHandle && input.sessionId !== undefined) {
|
|
318
|
+
session.sessionId = input.sessionId;
|
|
319
|
+
}
|
|
320
|
+
if (!session.rootHandle && session.source === 'lazy_session' && input.source === 'session_start') {
|
|
321
|
+
session.source = 'session_start';
|
|
322
|
+
}
|
|
323
|
+
if (!session.rootHandle && input.timestamp !== undefined) {
|
|
324
|
+
if (input.source === 'session_start' || session.pendingRootTimestampMicros === undefined) {
|
|
325
|
+
session.pendingRootTimestampMicros = input.timestamp;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (!session.rootHandle && session.pendingRootRunId === undefined && input.runId !== undefined) {
|
|
329
|
+
session.pendingRootRunId = input.runId;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
243
332
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
333
|
+
/** Queue an emit callback until a deferred session root can open with honest lineage. */
|
|
334
|
+
export function queueCapturedEmit(session: SessionState, label: string, emit: () => void): boolean {
|
|
335
|
+
if (session.rootHandle || session.pendingRootOpen !== true) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
(session.pendingCapturedEmits ??= []).push({ label, emit });
|
|
340
|
+
return true;
|
|
248
341
|
}
|
|
249
342
|
|
|
250
343
|
/** Flush pending LLM output/timing state before the root session closes. */
|
|
@@ -261,6 +354,7 @@ export function closeSessionRoot(
|
|
|
261
354
|
session: SessionState,
|
|
262
355
|
summary: JsonRecord,
|
|
263
356
|
rootOutput: JsonRecord = summary,
|
|
357
|
+
metadata?: JsonRecord | null,
|
|
264
358
|
timestamp?: number,
|
|
265
359
|
): void {
|
|
266
360
|
manager.emitCapturedUnderSession('session_end', session, () => {
|
|
@@ -268,7 +362,7 @@ export function closeSessionRoot(
|
|
|
268
362
|
return;
|
|
269
363
|
}
|
|
270
364
|
|
|
271
|
-
manager.nf.event('openclaw.session_end', session.rootHandle, summary, null, timestamp ?? null);
|
|
365
|
+
manager.nf.event('openclaw.session_end', session.rootHandle, summary, metadata ?? null, timestamp ?? null);
|
|
272
366
|
manager.state.counters.marksEmitted += 1;
|
|
273
367
|
manager.nf.popScope(session.rootHandle, rootOutput, timestamp ?? null);
|
|
274
368
|
delete session.rootHandle;
|
|
@@ -277,7 +371,7 @@ export function closeSessionRoot(
|
|
|
277
371
|
|
|
278
372
|
/** Remove a closed session from active replay state. */
|
|
279
373
|
export function deleteSession(state: HookReplayBackendState, session: SessionState): void {
|
|
280
|
-
state.sessions.delete(session.
|
|
374
|
+
state.sessions.delete(session.ownerKey);
|
|
281
375
|
}
|
|
282
376
|
|
|
283
377
|
/** Insert a correlation record while bounding retained entries per key. */
|
|
@@ -303,38 +397,69 @@ export function evictExpiredCorrelationRecords(state: HookReplayBackendState, no
|
|
|
303
397
|
evictExpiredRecords(state.modelTimingsByLlmKey, nowMs, ttlMs);
|
|
304
398
|
}
|
|
305
399
|
|
|
306
|
-
/** Open
|
|
307
|
-
function
|
|
400
|
+
/** Open a deferred or new root session scope and flush queued child emissions. */
|
|
401
|
+
export function materializeSessionRoot(manager: SessionManager, session: SessionState, input: EnsureSessionInput): void {
|
|
402
|
+
if (session.rootHandle) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
enrichSession(session, input);
|
|
407
|
+
|
|
408
|
+
const timestampMicros = input.timestamp ?? session.pendingRootTimestampMicros ?? null;
|
|
409
|
+
const rootRunId = input.runId ?? session.pendingRootRunId;
|
|
410
|
+
|
|
308
411
|
const data: JsonRecord = {
|
|
309
412
|
sessionId: session.sessionId,
|
|
310
413
|
source: session.source,
|
|
311
414
|
...(session.sessionKey === undefined ? {} : { sessionKey: session.sessionKey }),
|
|
312
415
|
...(session.agentId === undefined ? {} : { agentId: session.agentId }),
|
|
313
|
-
...(
|
|
416
|
+
...(rootRunId === undefined ? {} : { runId: rootRunId }),
|
|
314
417
|
...(session.resumedFrom === undefined ? {} : { resumedFrom: session.resumedFrom }),
|
|
315
418
|
};
|
|
419
|
+
const metadata = toJsonRecord({
|
|
420
|
+
source: session.source === 'session_start' ? 'openclaw.session_start' : 'openclaw.lazy_session',
|
|
421
|
+
hook_event_name: session.source === 'session_start' ? 'session_start' : undefined,
|
|
422
|
+
sessionId: session.sessionId,
|
|
423
|
+
sessionKey: session.sessionKey,
|
|
424
|
+
agentId: session.agentId,
|
|
425
|
+
runId: rootRunId,
|
|
426
|
+
nemo_relay_scope_role: session.scopeRole,
|
|
427
|
+
});
|
|
316
428
|
|
|
317
|
-
manager.
|
|
429
|
+
const previousStack = manager.nf.currentScopeStack();
|
|
430
|
+
try {
|
|
431
|
+
manager.nf.setThreadScopeStack(session.stack);
|
|
318
432
|
session.rootHandle = manager.nf.pushScope(
|
|
319
433
|
'openclaw.session',
|
|
320
434
|
agentScopeType(manager.nf),
|
|
321
|
-
null,
|
|
435
|
+
input.parentHandle ?? null,
|
|
322
436
|
null,
|
|
323
437
|
data,
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
438
|
+
metadata,
|
|
439
|
+
data,
|
|
440
|
+
timestampMicros,
|
|
327
441
|
);
|
|
328
|
-
manager.nf.event('openclaw.session_start', session.rootHandle, data,
|
|
442
|
+
manager.nf.event('openclaw.session_start', session.rootHandle, data, metadata, timestampMicros);
|
|
329
443
|
manager.state.counters.marksEmitted += 1;
|
|
330
|
-
}
|
|
444
|
+
} finally {
|
|
445
|
+
manager.nf.setThreadScopeStack(previousStack);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
delete session.pendingRootOpen;
|
|
449
|
+
delete session.pendingRootTimestampMicros;
|
|
450
|
+
delete session.pendingRootRunId;
|
|
451
|
+
const pendingEmits = session.pendingCapturedEmits ?? [];
|
|
452
|
+
delete session.pendingCapturedEmits;
|
|
453
|
+
for (const pending of pendingEmits) {
|
|
454
|
+
manager.emitCapturedUnderSession(pending.label, session, pending.emit);
|
|
455
|
+
}
|
|
331
456
|
}
|
|
332
457
|
|
|
333
458
|
/** Cancel timers that would otherwise replay late LLM outputs after session close. */
|
|
334
459
|
function cancelPendingLlmOutputTimers(state: HookReplayBackendState, session: SessionState): void {
|
|
335
460
|
for (const records of state.llmOutputsPendingInput.values()) {
|
|
336
461
|
for (const record of records) {
|
|
337
|
-
if (record.
|
|
462
|
+
if (record.sessionOwnerKey === session.ownerKey && record.timer) {
|
|
338
463
|
clearTimeout(record.timer);
|
|
339
464
|
record.timer = undefined;
|
|
340
465
|
}
|
|
@@ -344,22 +469,22 @@ function cancelPendingLlmOutputTimers(state: HookReplayBackendState, session: Se
|
|
|
344
469
|
|
|
345
470
|
/** Remove all correlation records and aliases owned by a closed session. */
|
|
346
471
|
function evictSessionCorrelationRecords(state: HookReplayBackendState, session: SessionState): void {
|
|
347
|
-
evictFromRecordMap(state.llmInputs, session.
|
|
348
|
-
evictFromRecordMap(state.llmOutputsPendingInput, session.
|
|
349
|
-
evictFromRecordMap(state.modelCallsByCallId, session.
|
|
350
|
-
evictFromRecordMap(state.modelTimingsByLlmKey, session.
|
|
472
|
+
evictFromRecordMap(state.llmInputs, session.ownerKey);
|
|
473
|
+
evictFromRecordMap(state.llmOutputsPendingInput, session.ownerKey);
|
|
474
|
+
evictFromRecordMap(state.modelCallsByCallId, session.ownerKey);
|
|
475
|
+
evictFromRecordMap(state.modelTimingsByLlmKey, session.ownerKey);
|
|
351
476
|
|
|
352
|
-
for (const [alias,
|
|
353
|
-
if (
|
|
477
|
+
for (const [alias, ownerKey] of state.sessionAliases) {
|
|
478
|
+
if (ownerKey === session.ownerKey) {
|
|
354
479
|
state.sessionAliases.delete(alias);
|
|
355
480
|
}
|
|
356
481
|
}
|
|
357
482
|
}
|
|
358
483
|
|
|
359
484
|
/** Drop records for one session from a single keyed correlation map. */
|
|
360
|
-
function evictFromRecordMap<T extends {
|
|
485
|
+
function evictFromRecordMap<T extends { sessionOwnerKey: string }>(map: Map<string, T[]>, ownerKey: string): void {
|
|
361
486
|
for (const [key, records] of map) {
|
|
362
|
-
const retained = records.filter((record) => record.
|
|
487
|
+
const retained = records.filter((record) => record.sessionOwnerKey !== ownerKey);
|
|
363
488
|
if (retained.length === 0) {
|
|
364
489
|
map.delete(key);
|
|
365
490
|
} else {
|
package/src/hook-replay/tool.ts
CHANGED
|
@@ -22,12 +22,14 @@ export async function guardBeforeToolCall(
|
|
|
22
22
|
event: PluginHookBeforeToolCallEvent,
|
|
23
23
|
ctx: PluginHookToolContext,
|
|
24
24
|
): Promise<void> {
|
|
25
|
+
const observedAtMicros = nowMicros();
|
|
25
26
|
const session = ensureSession(manager, {
|
|
26
27
|
sessionId: ctx.sessionId,
|
|
27
28
|
sessionKey: ctx.sessionKey,
|
|
28
29
|
runId: event.runId ?? ctx.runId,
|
|
29
30
|
agentId: ctx.agentId,
|
|
30
31
|
source: 'lazy_session',
|
|
32
|
+
timestamp: observedAtMicros,
|
|
31
33
|
});
|
|
32
34
|
const args = toJsonValue(event.params ?? {});
|
|
33
35
|
|
|
@@ -50,12 +52,15 @@ export function replayAfterToolCall(
|
|
|
50
52
|
event: PluginHookAfterToolCallEvent,
|
|
51
53
|
ctx: PluginHookToolContext,
|
|
52
54
|
): void {
|
|
55
|
+
const endMicros = nowMicros();
|
|
56
|
+
const sessionTimestamp = startMicrosFromDuration(endMicros, event.durationMs) ?? endMicros;
|
|
53
57
|
const session = ensureSession(manager, {
|
|
54
58
|
sessionId: ctx.sessionId,
|
|
55
59
|
sessionKey: ctx.sessionKey,
|
|
56
60
|
runId: event.runId ?? ctx.runId,
|
|
57
61
|
agentId: ctx.agentId,
|
|
58
62
|
source: 'lazy_session',
|
|
63
|
+
timestamp: sessionTimestamp,
|
|
59
64
|
});
|
|
60
65
|
|
|
61
66
|
const blockedDetails = blockedToolDetails(event, { runId: event.runId ?? ctx.runId });
|
|
@@ -67,6 +72,16 @@ export function replayAfterToolCall(
|
|
|
67
72
|
session,
|
|
68
73
|
name: 'openclaw.tool_blocked',
|
|
69
74
|
data: blockedDetails,
|
|
75
|
+
metadata: toJsonRecord({
|
|
76
|
+
source: 'openclaw.after_tool_call',
|
|
77
|
+
hook_event_name: 'after_tool_call',
|
|
78
|
+
sessionId: session.sessionId,
|
|
79
|
+
sessionKey: session.sessionKey,
|
|
80
|
+
agentId: session.agentId,
|
|
81
|
+
runId: event.runId ?? ctx.runId,
|
|
82
|
+
toolCallId: event.toolCallId ?? ctx.toolCallId,
|
|
83
|
+
}),
|
|
84
|
+
timestamp: endMicros,
|
|
70
85
|
});
|
|
71
86
|
});
|
|
72
87
|
return;
|
|
@@ -76,12 +91,12 @@ export function replayAfterToolCall(
|
|
|
76
91
|
return;
|
|
77
92
|
}
|
|
78
93
|
|
|
79
|
-
const endMicros = nowMicros();
|
|
80
94
|
const metadata = toJsonRecord({
|
|
81
95
|
source: 'openclaw.after_tool_call',
|
|
82
96
|
runId: event.runId ?? ctx.runId,
|
|
83
|
-
sessionId:
|
|
84
|
-
sessionKey:
|
|
97
|
+
sessionId: session.sessionId,
|
|
98
|
+
sessionKey: session.sessionKey,
|
|
99
|
+
agentId: session.agentId,
|
|
85
100
|
toolCallId: event.toolCallId ?? ctx.toolCallId,
|
|
86
101
|
durationMs: event.durationMs,
|
|
87
102
|
});
|