@syengup/friday-channel-next 0.1.36 → 0.1.37
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/index.js +1 -1
- package/dist/src/agent/dispatch-bridge.d.ts +1 -1
- package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
- package/dist/src/agent/node-pairing-bridge.js +6 -2
- package/dist/src/agent/subagent-registry.js +0 -3
- package/dist/src/channel-actions.js +3 -1
- package/dist/src/channel.js +0 -2
- package/dist/src/collect-message-media-paths.js +10 -1
- package/dist/src/friday-session.js +34 -10
- package/dist/src/history/normalize-message.js +22 -8
- package/dist/src/http/handlers/agent-config.js +10 -4
- package/dist/src/http/handlers/cancel.js +4 -2
- package/dist/src/http/handlers/device-approve.js +3 -1
- package/dist/src/http/handlers/files-download.js +6 -8
- package/dist/src/http/handlers/files.js +1 -1
- package/dist/src/http/handlers/health.js +18 -4
- package/dist/src/http/handlers/history-messages.js +1 -1
- package/dist/src/http/handlers/history-sessions.js +5 -3
- package/dist/src/http/handlers/messages.js +25 -11
- package/dist/src/http/handlers/models-list.js +1 -1
- package/dist/src/http/handlers/nodes-approve.js +1 -6
- package/dist/src/http/handlers/plugin-info.js +1 -1
- package/dist/src/http/server.js +4 -2
- package/dist/src/link-preview/og-parse.js +3 -1
- package/dist/src/plugin-install-info.js +4 -1
- package/dist/src/session/session-manager.js +9 -3
- package/dist/src/session-usage-store.js +3 -1
- package/dist/src/skills-discovery.d.ts +5 -4
- package/dist/src/skills-discovery.js +27 -22
- package/dist/src/sse/offline-queue.js +4 -1
- package/dist/src/tool-catalog.js +2 -3
- package/dist/src/upgrade-runtime.d.ts +1 -1
- package/dist/src/version.js +3 -1
- package/index.ts +43 -35
- package/install.js +131 -43
- package/package.json +10 -1
- package/src/agent/abort-run.ts +2 -3
- package/src/agent/dispatch-bridge.ts +2 -1
- package/src/agent/media-bridge.ts +9 -2
- package/src/agent/node-pairing-bridge.ts +29 -15
- package/src/agent/run-usage-accumulator.ts +4 -2
- package/src/agent/subagent-registry.ts +0 -4
- package/src/agent-run-context-bridge.ts +3 -1
- package/src/channel-actions.test.ts +10 -4
- package/src/channel-actions.ts +3 -1
- package/src/channel.outbound.test.ts +18 -4
- package/src/channel.ts +121 -123
- package/src/collect-message-media-paths.ts +15 -6
- package/src/config.ts +1 -4
- package/src/e2e/agents-list.e2e.test.ts +9 -2
- package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
- package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
- package/src/e2e/auto-approve.integration.test.ts +13 -7
- package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
- package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
- package/src/e2e/offline-replay.e2e.test.ts +17 -3
- package/src/e2e/send-text.e2e.test.ts +11 -2
- package/src/e2e/slash-commands.e2e.test.ts +5 -1
- package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
- package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
- package/src/e2e/subagent.e2e.test.ts +136 -53
- package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
- package/src/friday-session.forward-agent.test.ts +44 -12
- package/src/friday-session.ts +44 -20
- package/src/history/normalize-message.test.ts +35 -8
- package/src/history/normalize-message.ts +24 -12
- package/src/history/read-transcript.ts +1 -4
- package/src/http/handlers/agent-config.test.ts +10 -3
- package/src/http/handlers/agent-config.ts +22 -8
- package/src/http/handlers/agents-list.test.ts +1 -5
- package/src/http/handlers/cancel.test.ts +12 -3
- package/src/http/handlers/cancel.ts +4 -2
- package/src/http/handlers/device-approve.test.ts +12 -3
- package/src/http/handlers/device-approve.ts +33 -21
- package/src/http/handlers/files-download.ts +17 -13
- package/src/http/handlers/files.test.ts +8 -2
- package/src/http/handlers/files.ts +21 -7
- package/src/http/handlers/health.test.ts +43 -11
- package/src/http/handlers/health.ts +22 -6
- package/src/http/handlers/history-messages.test.ts +51 -9
- package/src/http/handlers/history-messages.ts +4 -1
- package/src/http/handlers/history-sessions.test.ts +46 -9
- package/src/http/handlers/history-sessions.ts +5 -3
- package/src/http/handlers/history-set-title.test.ts +14 -5
- package/src/http/handlers/link-preview.test.ts +57 -16
- package/src/http/handlers/link-preview.ts +4 -1
- package/src/http/handlers/messages.test.ts +12 -8
- package/src/http/handlers/messages.ts +57 -19
- package/src/http/handlers/models-list.ts +14 -8
- package/src/http/handlers/nodes-approve.test.ts +15 -4
- package/src/http/handlers/nodes-approve.ts +38 -40
- package/src/http/handlers/plugin-info.ts +5 -6
- package/src/http/handlers/plugin-upgrade.ts +4 -1
- package/src/http/handlers/sse.ts +3 -1
- package/src/http/server.ts +9 -6
- package/src/link-preview/og-parse.test.ts +6 -2
- package/src/link-preview/og-parse.ts +10 -3
- package/src/link-preview/preview-service.ts +4 -1
- package/src/link-preview/ssrf-guard.test.ts +72 -15
- package/src/link-preview/ssrf-guard.ts +2 -1
- package/src/media-fetch.test.ts +7 -2
- package/src/media-fetch.ts +1 -2
- package/src/openclaw.d.ts +16 -9
- package/src/plugin-install-info.ts +20 -9
- package/src/run-metadata.ts +2 -1
- package/src/session/session-manager.ts +19 -11
- package/src/session-usage-snapshot.ts +3 -1
- package/src/session-usage-store.ts +3 -1
- package/src/skills-discovery.test.ts +14 -10
- package/src/skills-discovery.ts +43 -27
- package/src/sse/emitter.test.ts +1 -1
- package/src/sse/emitter.ts +9 -3
- package/src/sse/offline-queue.ts +17 -8
- package/src/test-support/app-simulator.ts +17 -3
- package/src/test-support/mock-dispatch.ts +17 -4
- package/src/thinking-levels.ts +3 -1
- package/src/tool-catalog.ts +16 -7
- package/src/upgrade-runtime.ts +4 -2
- package/src/version.ts +5 -1
- package/tsconfig.json +1 -1
|
@@ -203,15 +203,20 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
203
203
|
|
|
204
204
|
// Main run start
|
|
205
205
|
forwardAgentEventRaw({
|
|
206
|
-
runId: mainRunId,
|
|
207
|
-
|
|
206
|
+
runId: mainRunId,
|
|
207
|
+
seq: 1,
|
|
208
|
+
stream: "lifecycle",
|
|
209
|
+
sessionKey: mainSessionKey,
|
|
210
|
+
data: { phase: "start" },
|
|
208
211
|
});
|
|
209
212
|
|
|
210
213
|
const broadcastCalls = captureBroadcastCalls();
|
|
211
214
|
|
|
212
215
|
// Simulate sessions_spawn tool result
|
|
213
216
|
forwardAgentEventRaw({
|
|
214
|
-
runId: mainRunId,
|
|
217
|
+
runId: mainRunId,
|
|
218
|
+
seq: 2,
|
|
219
|
+
stream: "tool",
|
|
215
220
|
sessionKey: mainSessionKey,
|
|
216
221
|
data: {
|
|
217
222
|
phase: "result",
|
|
@@ -243,17 +248,30 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
243
248
|
|
|
244
249
|
// Main run
|
|
245
250
|
forwardAgentEventRaw({
|
|
246
|
-
runId: mainRunId,
|
|
247
|
-
|
|
251
|
+
runId: mainRunId,
|
|
252
|
+
seq: 1,
|
|
253
|
+
stream: "lifecycle",
|
|
254
|
+
sessionKey: mainSessionKey,
|
|
255
|
+
data: { phase: "start" },
|
|
248
256
|
});
|
|
249
257
|
|
|
250
258
|
// Spawn subagent
|
|
251
259
|
forwardAgentEventRaw({
|
|
252
|
-
runId: mainRunId,
|
|
260
|
+
runId: mainRunId,
|
|
261
|
+
seq: 2,
|
|
262
|
+
stream: "tool",
|
|
253
263
|
sessionKey: mainSessionKey,
|
|
254
264
|
data: {
|
|
255
|
-
phase: "result",
|
|
256
|
-
|
|
265
|
+
phase: "result",
|
|
266
|
+
name: "sessions_spawn",
|
|
267
|
+
toolCallId: "c1",
|
|
268
|
+
result: {
|
|
269
|
+
details: makeSpawnToolResult({
|
|
270
|
+
childSessionKey: childKey,
|
|
271
|
+
runId: bareRunId,
|
|
272
|
+
taskName: "cr",
|
|
273
|
+
}),
|
|
274
|
+
},
|
|
257
275
|
},
|
|
258
276
|
});
|
|
259
277
|
|
|
@@ -264,14 +282,14 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
264
282
|
|
|
265
283
|
// Agent event for subagent (announce runId, subagent's own sessionKey)
|
|
266
284
|
forwardAgentEventRaw({
|
|
267
|
-
runId: compoundRunId,
|
|
285
|
+
runId: compoundRunId,
|
|
286
|
+
seq: 1,
|
|
287
|
+
stream: "thinking",
|
|
268
288
|
data: { text: "reviewing..." },
|
|
269
289
|
sessionKey: childKey,
|
|
270
290
|
});
|
|
271
291
|
|
|
272
|
-
const agentCall = calls.find(
|
|
273
|
-
([, e]) => e.type === "agent" && e.data.stream === "thinking",
|
|
274
|
-
);
|
|
292
|
+
const agentCall = calls.find(([, e]) => e.type === "agent" && e.data.stream === "thinking");
|
|
275
293
|
expect(agentCall).toBeTruthy();
|
|
276
294
|
expect(agentCall![1].data.subagent).toEqual({
|
|
277
295
|
label: "cr",
|
|
@@ -282,21 +300,24 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
282
300
|
|
|
283
301
|
it("main agent events are NOT annotated", () => {
|
|
284
302
|
forwardAgentEventRaw({
|
|
285
|
-
runId: mainRunId,
|
|
286
|
-
|
|
303
|
+
runId: mainRunId,
|
|
304
|
+
seq: 1,
|
|
305
|
+
stream: "lifecycle",
|
|
306
|
+
sessionKey: mainSessionKey,
|
|
307
|
+
data: { phase: "start" },
|
|
287
308
|
});
|
|
288
309
|
|
|
289
310
|
const calls = captureBroadcastToRunCalls();
|
|
290
311
|
|
|
291
312
|
forwardAgentEventRaw({
|
|
292
|
-
runId: mainRunId,
|
|
313
|
+
runId: mainRunId,
|
|
314
|
+
seq: 2,
|
|
315
|
+
stream: "assistant",
|
|
293
316
|
sessionKey: mainSessionKey,
|
|
294
317
|
data: { text: "main reply" },
|
|
295
318
|
});
|
|
296
319
|
|
|
297
|
-
const agentCall = calls.find(
|
|
298
|
-
([, e]) => e.type === "agent" && e.data.stream === "assistant",
|
|
299
|
-
);
|
|
320
|
+
const agentCall = calls.find(([, e]) => e.type === "agent" && e.data.stream === "assistant");
|
|
300
321
|
expect(agentCall![1].data.subagent).toBeUndefined();
|
|
301
322
|
});
|
|
302
323
|
|
|
@@ -305,16 +326,29 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
305
326
|
const bareRunId = "bare-cr";
|
|
306
327
|
|
|
307
328
|
forwardAgentEventRaw({
|
|
308
|
-
runId: mainRunId,
|
|
309
|
-
|
|
329
|
+
runId: mainRunId,
|
|
330
|
+
seq: 1,
|
|
331
|
+
stream: "lifecycle",
|
|
332
|
+
sessionKey: mainSessionKey,
|
|
333
|
+
data: { phase: "start" },
|
|
310
334
|
});
|
|
311
335
|
|
|
312
336
|
forwardAgentEventRaw({
|
|
313
|
-
runId: mainRunId,
|
|
337
|
+
runId: mainRunId,
|
|
338
|
+
seq: 2,
|
|
339
|
+
stream: "tool",
|
|
314
340
|
sessionKey: mainSessionKey,
|
|
315
341
|
data: {
|
|
316
|
-
phase: "result",
|
|
317
|
-
|
|
342
|
+
phase: "result",
|
|
343
|
+
name: "sessions_spawn",
|
|
344
|
+
toolCallId: "c1",
|
|
345
|
+
result: {
|
|
346
|
+
details: makeSpawnToolResult({
|
|
347
|
+
childSessionKey: childKey,
|
|
348
|
+
runId: bareRunId,
|
|
349
|
+
taskName: "cr",
|
|
350
|
+
}),
|
|
351
|
+
},
|
|
318
352
|
},
|
|
319
353
|
});
|
|
320
354
|
|
|
@@ -325,7 +359,9 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
325
359
|
|
|
326
360
|
// Lifecycle end for subagent (subagent's own sessionKey)
|
|
327
361
|
forwardAgentEventRaw({
|
|
328
|
-
runId: compoundRunId,
|
|
362
|
+
runId: compoundRunId,
|
|
363
|
+
seq: 5,
|
|
364
|
+
stream: "lifecycle",
|
|
329
365
|
data: { phase: "end" },
|
|
330
366
|
sessionKey: childKey,
|
|
331
367
|
});
|
|
@@ -342,15 +378,22 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
342
378
|
const bareRunId = "bare-cr";
|
|
343
379
|
|
|
344
380
|
forwardAgentEventRaw({
|
|
345
|
-
runId: mainRunId,
|
|
346
|
-
|
|
381
|
+
runId: mainRunId,
|
|
382
|
+
seq: 1,
|
|
383
|
+
stream: "lifecycle",
|
|
384
|
+
sessionKey: mainSessionKey,
|
|
385
|
+
data: { phase: "start" },
|
|
347
386
|
});
|
|
348
387
|
|
|
349
388
|
forwardAgentEventRaw({
|
|
350
|
-
runId: mainRunId,
|
|
389
|
+
runId: mainRunId,
|
|
390
|
+
seq: 2,
|
|
391
|
+
stream: "tool",
|
|
351
392
|
sessionKey: mainSessionKey,
|
|
352
393
|
data: {
|
|
353
|
-
phase: "result",
|
|
394
|
+
phase: "result",
|
|
395
|
+
name: "sessions_spawn",
|
|
396
|
+
toolCallId: "c1",
|
|
354
397
|
result: { details: makeSpawnToolResult({ childSessionKey: childKey, runId: bareRunId }) },
|
|
355
398
|
},
|
|
356
399
|
});
|
|
@@ -361,7 +404,9 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
361
404
|
const broadcastCalls = captureBroadcastCalls();
|
|
362
405
|
|
|
363
406
|
forwardAgentEventRaw({
|
|
364
|
-
runId: compoundRunId,
|
|
407
|
+
runId: compoundRunId,
|
|
408
|
+
seq: 5,
|
|
409
|
+
stream: "lifecycle",
|
|
365
410
|
data: { phase: "error", error: "timeout" },
|
|
366
411
|
sessionKey: childKey,
|
|
367
412
|
});
|
|
@@ -379,15 +424,22 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
379
424
|
const bareRunId = "bare-cr";
|
|
380
425
|
|
|
381
426
|
forwardAgentEventRaw({
|
|
382
|
-
runId: mainRunId,
|
|
383
|
-
|
|
427
|
+
runId: mainRunId,
|
|
428
|
+
seq: 1,
|
|
429
|
+
stream: "lifecycle",
|
|
430
|
+
sessionKey: mainSessionKey,
|
|
431
|
+
data: { phase: "start" },
|
|
384
432
|
});
|
|
385
433
|
|
|
386
434
|
forwardAgentEventRaw({
|
|
387
|
-
runId: mainRunId,
|
|
435
|
+
runId: mainRunId,
|
|
436
|
+
seq: 2,
|
|
437
|
+
stream: "tool",
|
|
388
438
|
sessionKey: mainSessionKey,
|
|
389
439
|
data: {
|
|
390
|
-
phase: "result",
|
|
440
|
+
phase: "result",
|
|
441
|
+
name: "sessions_spawn",
|
|
442
|
+
toolCallId: "c1",
|
|
391
443
|
result: { details: makeSpawnToolResult({ childSessionKey: childKey, runId: bareRunId }) },
|
|
392
444
|
},
|
|
393
445
|
});
|
|
@@ -398,12 +450,16 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
398
450
|
const broadcastCalls = captureBroadcastCalls();
|
|
399
451
|
|
|
400
452
|
forwardAgentEventRaw({
|
|
401
|
-
runId: compoundRunId,
|
|
453
|
+
runId: compoundRunId,
|
|
454
|
+
seq: 5,
|
|
455
|
+
stream: "lifecycle",
|
|
402
456
|
data: { phase: "end" },
|
|
403
457
|
sessionKey: childKey,
|
|
404
458
|
});
|
|
405
459
|
forwardAgentEventRaw({
|
|
406
|
-
runId: compoundRunId,
|
|
460
|
+
runId: compoundRunId,
|
|
461
|
+
seq: 6,
|
|
462
|
+
stream: "lifecycle",
|
|
407
463
|
data: { phase: "end" },
|
|
408
464
|
sessionKey: childKey,
|
|
409
465
|
});
|
|
@@ -426,17 +482,30 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
426
482
|
|
|
427
483
|
// Main run start
|
|
428
484
|
forwardAgentEventRaw({
|
|
429
|
-
runId: mainRunId,
|
|
430
|
-
|
|
485
|
+
runId: mainRunId,
|
|
486
|
+
seq: 1,
|
|
487
|
+
stream: "lifecycle",
|
|
488
|
+
sessionKey: mainSessionKey,
|
|
489
|
+
data: { phase: "start" },
|
|
431
490
|
});
|
|
432
491
|
|
|
433
492
|
// sessions_spawn for A
|
|
434
493
|
forwardAgentEventRaw({
|
|
435
|
-
runId: mainRunId,
|
|
494
|
+
runId: mainRunId,
|
|
495
|
+
seq: 2,
|
|
496
|
+
stream: "tool",
|
|
436
497
|
sessionKey: mainSessionKey,
|
|
437
498
|
data: {
|
|
438
|
-
phase: "result",
|
|
439
|
-
|
|
499
|
+
phase: "result",
|
|
500
|
+
name: "sessions_spawn",
|
|
501
|
+
toolCallId: "c1",
|
|
502
|
+
result: {
|
|
503
|
+
details: makeSpawnToolResult({
|
|
504
|
+
childSessionKey: childKeyA,
|
|
505
|
+
runId: bareA,
|
|
506
|
+
taskName: "reviewer",
|
|
507
|
+
}),
|
|
508
|
+
},
|
|
440
509
|
},
|
|
441
510
|
});
|
|
442
511
|
sseEmitter.trackDeviceForRun(deviceId, compoundA);
|
|
@@ -444,13 +513,13 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
444
513
|
// Subagent A thinking (subagent's own sessionKey)
|
|
445
514
|
const calls = captureBroadcastToRunCalls();
|
|
446
515
|
forwardAgentEventRaw({
|
|
447
|
-
runId: compoundA,
|
|
516
|
+
runId: compoundA,
|
|
517
|
+
seq: 1,
|
|
518
|
+
stream: "thinking",
|
|
448
519
|
data: { text: "reviewing code..." },
|
|
449
520
|
sessionKey: childKeyA,
|
|
450
521
|
});
|
|
451
|
-
const thinkingA = calls.find(
|
|
452
|
-
([, e]) => e.type === "agent" && e.data.stream === "thinking",
|
|
453
|
-
);
|
|
522
|
+
const thinkingA = calls.find(([, e]) => e.type === "agent" && e.data.stream === "thinking");
|
|
454
523
|
expect(thinkingA![1].data.subagent).toEqual({
|
|
455
524
|
label: "reviewer",
|
|
456
525
|
parentRunId: mainRunId,
|
|
@@ -461,36 +530,50 @@ describe("subagent via sessions_spawn tool", () => {
|
|
|
461
530
|
// In reality, B is spawned from a tool call inside A's run, but here we simulate it from the main run
|
|
462
531
|
// The registry handles nesting via requesterSessionKey chain
|
|
463
532
|
forwardAgentEventRaw({
|
|
464
|
-
runId: compoundA,
|
|
533
|
+
runId: compoundA,
|
|
534
|
+
seq: 2,
|
|
535
|
+
stream: "tool",
|
|
465
536
|
sessionKey: mainSessionKey,
|
|
466
537
|
data: {
|
|
467
|
-
phase: "result",
|
|
468
|
-
|
|
538
|
+
phase: "result",
|
|
539
|
+
name: "sessions_spawn",
|
|
540
|
+
toolCallId: "c2",
|
|
541
|
+
result: {
|
|
542
|
+
details: makeSpawnToolResult({
|
|
543
|
+
childSessionKey: childKeyB,
|
|
544
|
+
runId: bareB,
|
|
545
|
+
taskName: "lint",
|
|
546
|
+
}),
|
|
547
|
+
},
|
|
469
548
|
},
|
|
470
549
|
});
|
|
471
550
|
sseEmitter.trackDeviceForRun(deviceId, compoundB);
|
|
472
551
|
|
|
473
552
|
// Subagent B thinking (subagent's own sessionKey)
|
|
474
553
|
forwardAgentEventRaw({
|
|
475
|
-
runId: compoundB,
|
|
554
|
+
runId: compoundB,
|
|
555
|
+
seq: 1,
|
|
556
|
+
stream: "assistant",
|
|
476
557
|
data: { text: "no lint errors" },
|
|
477
558
|
sessionKey: childKeyB,
|
|
478
559
|
});
|
|
479
|
-
const assistantB = calls.find(
|
|
480
|
-
([, e]) => e.type === "agent" && e.data.stream === "assistant",
|
|
481
|
-
);
|
|
560
|
+
const assistantB = calls.find(([, e]) => e.type === "agent" && e.data.stream === "assistant");
|
|
482
561
|
expect(assistantB![1].data.subagent).toBeDefined();
|
|
483
562
|
|
|
484
563
|
// B ends (subagent's own sessionKey)
|
|
485
564
|
forwardAgentEventRaw({
|
|
486
|
-
runId: compoundB,
|
|
565
|
+
runId: compoundB,
|
|
566
|
+
seq: 5,
|
|
567
|
+
stream: "lifecycle",
|
|
487
568
|
data: { phase: "end" },
|
|
488
569
|
sessionKey: childKeyB,
|
|
489
570
|
});
|
|
490
571
|
|
|
491
572
|
// A ends (subagent's own sessionKey)
|
|
492
573
|
forwardAgentEventRaw({
|
|
493
|
-
runId: compoundA,
|
|
574
|
+
runId: compoundA,
|
|
575
|
+
seq: 6,
|
|
576
|
+
stream: "lifecycle",
|
|
494
577
|
data: { phase: "end" },
|
|
495
578
|
sessionKey: childKeyA,
|
|
496
579
|
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
2
|
import { createAppSimulator } from "../test-support/app-simulator.js";
|
|
3
3
|
import { mockDispatchScript, resetMockDispatch } from "../test-support/mock-dispatch.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createTempHistoryDir,
|
|
6
|
+
removeTempHistoryDir,
|
|
7
|
+
setMockRuntime,
|
|
8
|
+
} from "../test-support/mock-runtime.js";
|
|
5
9
|
|
|
6
10
|
describe("e2e tool lifecycle", () => {
|
|
7
11
|
let historyDir = "";
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
resetThinkingStreamAccumStateForTest,
|
|
11
11
|
} from "./friday-session.js";
|
|
12
12
|
import { resetRunMetadataForTest } from "./run-metadata.js";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
accumulateRunUsage,
|
|
15
|
+
resetRunUsageAccumulatorForTest,
|
|
16
|
+
} from "./agent/run-usage-accumulator.js";
|
|
14
17
|
import { sseEmitter } from "./sse/emitter.js";
|
|
15
18
|
import { toSessionStoreKey } from "./session/session-manager.js";
|
|
16
19
|
|
|
@@ -56,8 +59,10 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
56
59
|
});
|
|
57
60
|
|
|
58
61
|
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(2);
|
|
59
|
-
const first = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data
|
|
60
|
-
|
|
62
|
+
const first = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data
|
|
63
|
+
.data;
|
|
64
|
+
const second = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[1][1].data
|
|
65
|
+
.data;
|
|
61
66
|
|
|
62
67
|
expect(first.text).toBe(t1);
|
|
63
68
|
expect(first.delta).toBe(t1);
|
|
@@ -94,7 +99,8 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
94
99
|
});
|
|
95
100
|
|
|
96
101
|
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(3);
|
|
97
|
-
const third = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[2][1].data
|
|
102
|
+
const third = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[2][1].data
|
|
103
|
+
.data;
|
|
98
104
|
expect(third.delta).toBe(t1);
|
|
99
105
|
expect(third.reasoningPrefixChars).toBe(0);
|
|
100
106
|
});
|
|
@@ -123,7 +129,8 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
123
129
|
data: { text: t1, delta: t1 },
|
|
124
130
|
});
|
|
125
131
|
|
|
126
|
-
const third = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[2][1].data
|
|
132
|
+
const third = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[2][1].data
|
|
133
|
+
.data;
|
|
127
134
|
expect(third.reasoningPrefixChars).toBe(0);
|
|
128
135
|
expect(third.delta).toBe(t1);
|
|
129
136
|
});
|
|
@@ -179,7 +186,8 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
179
186
|
});
|
|
180
187
|
|
|
181
188
|
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(2);
|
|
182
|
-
const endPayload = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[1][1]
|
|
189
|
+
const endPayload = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[1][1]
|
|
190
|
+
.data;
|
|
183
191
|
expect(endPayload.stream).toBe("lifecycle");
|
|
184
192
|
expect((endPayload.data as { phase?: string }).phase).toBe("end");
|
|
185
193
|
expect(endPayload.sessionKey).toBe(sessionKey);
|
|
@@ -214,8 +222,18 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
214
222
|
},
|
|
215
223
|
} as never);
|
|
216
224
|
|
|
217
|
-
accumulateRunUsage(
|
|
218
|
-
|
|
225
|
+
accumulateRunUsage(
|
|
226
|
+
runId,
|
|
227
|
+
{ input: 100, output: 50, cacheRead: 10, total: 150 },
|
|
228
|
+
"my-model",
|
|
229
|
+
"openai",
|
|
230
|
+
);
|
|
231
|
+
accumulateRunUsage(
|
|
232
|
+
runId,
|
|
233
|
+
{ input: 30, output: 10, cacheRead: 0, total: 40 },
|
|
234
|
+
"my-model",
|
|
235
|
+
"openai",
|
|
236
|
+
);
|
|
219
237
|
|
|
220
238
|
forwardAgentEventRaw({
|
|
221
239
|
runId,
|
|
@@ -232,7 +250,10 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
232
250
|
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
|
|
233
251
|
const forwarded = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data;
|
|
234
252
|
expect(forwarded.stream).toBe("lifecycle");
|
|
235
|
-
const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<
|
|
253
|
+
const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<
|
|
254
|
+
string,
|
|
255
|
+
unknown
|
|
256
|
+
>;
|
|
236
257
|
expect(sessionUsage).toBeDefined();
|
|
237
258
|
// llm_output fallback — per-run totals.
|
|
238
259
|
expect(sessionUsage.modelId).toBe("my-model");
|
|
@@ -272,7 +293,12 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
272
293
|
} as never);
|
|
273
294
|
|
|
274
295
|
// llm_output has fresher model/provider but per-run (smaller) tokens.
|
|
275
|
-
accumulateRunUsage(
|
|
296
|
+
accumulateRunUsage(
|
|
297
|
+
runId,
|
|
298
|
+
{ input: 500, output: 100, cacheRead: 200, total: 800 },
|
|
299
|
+
"llm-model",
|
|
300
|
+
"llm-provider",
|
|
301
|
+
);
|
|
276
302
|
|
|
277
303
|
forwardAgentEventRaw({
|
|
278
304
|
runId,
|
|
@@ -287,7 +313,10 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
287
313
|
|
|
288
314
|
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
|
|
289
315
|
const forwarded = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data;
|
|
290
|
-
const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<
|
|
316
|
+
const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<
|
|
317
|
+
string,
|
|
318
|
+
unknown
|
|
319
|
+
>;
|
|
291
320
|
expect(sessionUsage).toBeDefined();
|
|
292
321
|
// Store cumulative totals win.
|
|
293
322
|
expect((sessionUsage.tokens as Record<string, unknown>).input).toBe(5000);
|
|
@@ -344,7 +373,10 @@ describe("forwardAgentEventRaw (thinking delta rewrite)", () => {
|
|
|
344
373
|
expect(sseEmitter.broadcastToRun).toHaveBeenCalledTimes(1);
|
|
345
374
|
const forwarded = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[0][1].data;
|
|
346
375
|
expect(forwarded.stream).toBe("lifecycle");
|
|
347
|
-
const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<
|
|
376
|
+
const sessionUsage = (forwarded.data as Record<string, unknown>).sessionUsage as Record<
|
|
377
|
+
string,
|
|
378
|
+
unknown
|
|
379
|
+
>;
|
|
348
380
|
expect(sessionUsage).toBeDefined();
|
|
349
381
|
expect(sessionUsage.modelId).toBe("store-model");
|
|
350
382
|
expect(sessionUsage.modelProvider).toBe("store-provider");
|
package/src/friday-session.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function deviceIdFromSessionKey(sessionKey: string): string | null {
|
|
|
49
49
|
const m1 = sessionKey.match(/^friday-next-(.+)$/i);
|
|
50
50
|
if (m1) return m1[1] ?? null;
|
|
51
51
|
const m2 = sessionKey.match(/^agent:main:friday-next-(.+)$/i);
|
|
52
|
-
return m2 ? m2[1] ?? null : null;
|
|
52
|
+
return m2 ? (m2[1] ?? null) : null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
@@ -66,7 +66,8 @@ const deviceIdToLatestHistorySessionKey = new Map<string, string>();
|
|
|
66
66
|
let lastRegisteredFridayDeviceId: string | undefined;
|
|
67
67
|
|
|
68
68
|
function normalizeFridaySessionKeyCase(sk: string): string {
|
|
69
|
-
return /^friday-next-|^agent:main:friday-next-/i.test(sk) ||
|
|
69
|
+
return /^friday-next-|^agent:main:friday-next-/i.test(sk) ||
|
|
70
|
+
/^agent:main:friday-next:direct:/i.test(sk)
|
|
70
71
|
? sk.toLowerCase()
|
|
71
72
|
: sk;
|
|
72
73
|
}
|
|
@@ -157,7 +158,11 @@ function mergeRunMetadataIntoLifecycleEnd(
|
|
|
157
158
|
if (typeof meta.modelName === "string" && meta.modelName.trim()) {
|
|
158
159
|
extra.modelName = meta.modelName.trim();
|
|
159
160
|
}
|
|
160
|
-
if (
|
|
161
|
+
if (
|
|
162
|
+
typeof meta.totalTokens === "number" &&
|
|
163
|
+
Number.isFinite(meta.totalTokens) &&
|
|
164
|
+
meta.totalTokens > 0
|
|
165
|
+
) {
|
|
161
166
|
extra.totalTokens = Math.floor(meta.totalTokens);
|
|
162
167
|
}
|
|
163
168
|
if (
|
|
@@ -260,7 +265,10 @@ function completeAgentEventForward(params: {
|
|
|
260
265
|
/**
|
|
261
266
|
* Resolve the real device UUID for Friday outbound (`sendText` / `sendMedia`).
|
|
262
267
|
*/
|
|
263
|
-
export function resolveFridayDeviceIdForOutbound(
|
|
268
|
+
export function resolveFridayDeviceIdForOutbound(
|
|
269
|
+
to: string | undefined,
|
|
270
|
+
rawCtx?: Record<string, unknown>,
|
|
271
|
+
): string {
|
|
264
272
|
const trimmed = (to ?? "").trim();
|
|
265
273
|
if (trimmed && trimmed.toLowerCase() !== "friday-next") {
|
|
266
274
|
return trimmed;
|
|
@@ -279,6 +287,23 @@ export function resolveFridayDeviceIdForOutbound(to: string | undefined, rawCtx?
|
|
|
279
287
|
return trimmed || "friday-next";
|
|
280
288
|
}
|
|
281
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Stringify a subagent error payload for the wire. `evt.data.error` comes from an
|
|
292
|
+
* in-process SDK callback, so it can be a real Error, a plain object, or a value
|
|
293
|
+
* that JSON.stringify would throw on (circular/BigInt) — keep this total and
|
|
294
|
+
* never-throwing, since it runs on the error path that emits `subagent ended`.
|
|
295
|
+
*/
|
|
296
|
+
function stringifySubagentError(raw: unknown): string {
|
|
297
|
+
if (raw == null) return "unknown";
|
|
298
|
+
if (typeof raw === "string") return raw;
|
|
299
|
+
if (raw instanceof Error) return raw.message || raw.name || "error";
|
|
300
|
+
try {
|
|
301
|
+
return JSON.stringify(raw) ?? "unknown";
|
|
302
|
+
} catch {
|
|
303
|
+
return "unstringifiable error";
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
282
307
|
/**
|
|
283
308
|
* Forward global OpenClaw agent events to the Friday SSE connection (transparent).
|
|
284
309
|
*
|
|
@@ -308,8 +333,7 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
308
333
|
if (!deviceIdRaw) return;
|
|
309
334
|
|
|
310
335
|
if (!sk) {
|
|
311
|
-
sk =
|
|
312
|
-
latestHistorySessionKeyForDeviceId(deviceIdRaw) ?? `friday-next-${deviceIdRaw}`;
|
|
336
|
+
sk = latestHistorySessionKeyForDeviceId(deviceIdRaw) ?? `friday-next-${deviceIdRaw}`;
|
|
313
337
|
}
|
|
314
338
|
|
|
315
339
|
openClawRunIdToDeviceId.set(evt.runId, deviceIdRaw.toUpperCase());
|
|
@@ -325,11 +349,9 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
325
349
|
|
|
326
350
|
// Phase 1: spawning — tool.start with taskName in args
|
|
327
351
|
if (isSpawnTool && evt.data.phase === "start") {
|
|
328
|
-
const toolCallId =
|
|
329
|
-
typeof evt.data.toolCallId === "string" ? evt.data.toolCallId : "";
|
|
352
|
+
const toolCallId = typeof evt.data.toolCallId === "string" ? evt.data.toolCallId : "";
|
|
330
353
|
const args = evt.data.args as Record<string, unknown> | undefined;
|
|
331
|
-
const label =
|
|
332
|
-
typeof args?.taskName === "string" ? args.taskName : undefined;
|
|
354
|
+
const label = typeof args?.taskName === "string" ? args.taskName : undefined;
|
|
333
355
|
if (toolCallId) {
|
|
334
356
|
const intent = registerSpawnIntent({
|
|
335
357
|
toolCallId,
|
|
@@ -362,15 +384,12 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
362
384
|
| { childSessionKey?: string; runId?: string; taskName?: string }
|
|
363
385
|
| undefined;
|
|
364
386
|
if (details?.childSessionKey) {
|
|
365
|
-
const toolCallId =
|
|
366
|
-
typeof evt.data.toolCallId === "string" ? evt.data.toolCallId : "";
|
|
387
|
+
const toolCallId = typeof evt.data.toolCallId === "string" ? evt.data.toolCallId : "";
|
|
367
388
|
const intent = toolCallId ? consumeSpawnIntent(toolCallId) : undefined;
|
|
368
389
|
const label =
|
|
369
390
|
details.taskName ||
|
|
370
391
|
intent?.label ||
|
|
371
|
-
(typeof
|
|
372
|
-
? ((evt.data as Record<string, unknown>).meta as string)
|
|
373
|
-
: undefined);
|
|
392
|
+
(typeof evt.data.meta === "string" ? evt.data.meta : undefined);
|
|
374
393
|
const entry = ensureSubagentFromSpawnTool({
|
|
375
394
|
childSessionKey: details.childSessionKey,
|
|
376
395
|
bareRunId: details.runId,
|
|
@@ -404,10 +423,13 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
404
423
|
// Only annotate events that originate from the subagent itself
|
|
405
424
|
// (sessionKey matches childSessionKey). Main-agent delivery events
|
|
406
425
|
// share the announce runId but have a different sessionKey.
|
|
407
|
-
const isSubagentOwnEvent =
|
|
408
|
-
subagentEntry && sk && subagentEntry.childSessionKey === sk;
|
|
426
|
+
const isSubagentOwnEvent = subagentEntry && sk && subagentEntry.childSessionKey === sk;
|
|
409
427
|
const subagentMeta = isSubagentOwnEvent
|
|
410
|
-
? {
|
|
428
|
+
? {
|
|
429
|
+
label: subagentEntry.label,
|
|
430
|
+
parentRunId: subagentEntry.parentRunId,
|
|
431
|
+
depth: subagentEntry.depth,
|
|
432
|
+
}
|
|
411
433
|
: undefined;
|
|
412
434
|
|
|
413
435
|
let outgoingData: Record<string, unknown> = { ...evt.data };
|
|
@@ -436,12 +458,14 @@ export function forwardAgentEventRaw(evt: ForwardAgentEventArgs): void {
|
|
|
436
458
|
|
|
437
459
|
const lifecyclePhase =
|
|
438
460
|
evt.stream === "lifecycle" && typeof evt.data.phase === "string" ? evt.data.phase : "";
|
|
439
|
-
const isTerminalLifecycle =
|
|
461
|
+
const isTerminalLifecycle =
|
|
462
|
+
evt.stream === "lifecycle" && (lifecyclePhase === "end" || lifecyclePhase === "error");
|
|
440
463
|
|
|
441
464
|
// Emit subagent ended SSE when a subagent run terminates
|
|
442
465
|
if (isTerminalLifecycle && isSubagentOwnEvent && subagentEntry.status !== "ended") {
|
|
443
466
|
const outcome = lifecyclePhase === "error" ? "error" : "ok";
|
|
444
|
-
const errorStr =
|
|
467
|
+
const errorStr =
|
|
468
|
+
lifecyclePhase === "error" ? stringifySubagentError(evt.data.error) : undefined;
|
|
445
469
|
const ended = registerSubagentEnded({ runId: evt.runId, outcome, error: errorStr });
|
|
446
470
|
if (ended) {
|
|
447
471
|
sseEmitter.broadcast(
|