@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.
Files changed (120) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/src/agent/dispatch-bridge.d.ts +1 -1
  3. package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
  4. package/dist/src/agent/node-pairing-bridge.js +6 -2
  5. package/dist/src/agent/subagent-registry.js +0 -3
  6. package/dist/src/channel-actions.js +3 -1
  7. package/dist/src/channel.js +0 -2
  8. package/dist/src/collect-message-media-paths.js +10 -1
  9. package/dist/src/friday-session.js +34 -10
  10. package/dist/src/history/normalize-message.js +22 -8
  11. package/dist/src/http/handlers/agent-config.js +10 -4
  12. package/dist/src/http/handlers/cancel.js +4 -2
  13. package/dist/src/http/handlers/device-approve.js +3 -1
  14. package/dist/src/http/handlers/files-download.js +6 -8
  15. package/dist/src/http/handlers/files.js +1 -1
  16. package/dist/src/http/handlers/health.js +18 -4
  17. package/dist/src/http/handlers/history-messages.js +1 -1
  18. package/dist/src/http/handlers/history-sessions.js +5 -3
  19. package/dist/src/http/handlers/messages.js +25 -11
  20. package/dist/src/http/handlers/models-list.js +1 -1
  21. package/dist/src/http/handlers/nodes-approve.js +1 -6
  22. package/dist/src/http/handlers/plugin-info.js +1 -1
  23. package/dist/src/http/server.js +4 -2
  24. package/dist/src/link-preview/og-parse.js +3 -1
  25. package/dist/src/plugin-install-info.js +4 -1
  26. package/dist/src/session/session-manager.js +9 -3
  27. package/dist/src/session-usage-store.js +3 -1
  28. package/dist/src/skills-discovery.d.ts +5 -4
  29. package/dist/src/skills-discovery.js +27 -22
  30. package/dist/src/sse/offline-queue.js +4 -1
  31. package/dist/src/tool-catalog.js +2 -3
  32. package/dist/src/upgrade-runtime.d.ts +1 -1
  33. package/dist/src/version.js +3 -1
  34. package/index.ts +43 -35
  35. package/install.js +131 -43
  36. package/package.json +10 -1
  37. package/src/agent/abort-run.ts +2 -3
  38. package/src/agent/dispatch-bridge.ts +2 -1
  39. package/src/agent/media-bridge.ts +9 -2
  40. package/src/agent/node-pairing-bridge.ts +29 -15
  41. package/src/agent/run-usage-accumulator.ts +4 -2
  42. package/src/agent/subagent-registry.ts +0 -4
  43. package/src/agent-run-context-bridge.ts +3 -1
  44. package/src/channel-actions.test.ts +10 -4
  45. package/src/channel-actions.ts +3 -1
  46. package/src/channel.outbound.test.ts +18 -4
  47. package/src/channel.ts +121 -123
  48. package/src/collect-message-media-paths.ts +15 -6
  49. package/src/config.ts +1 -4
  50. package/src/e2e/agents-list.e2e.test.ts +9 -2
  51. package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
  52. package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
  53. package/src/e2e/auto-approve.integration.test.ts +13 -7
  54. package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
  55. package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
  56. package/src/e2e/offline-replay.e2e.test.ts +17 -3
  57. package/src/e2e/send-text.e2e.test.ts +11 -2
  58. package/src/e2e/slash-commands.e2e.test.ts +5 -1
  59. package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
  60. package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
  61. package/src/e2e/subagent.e2e.test.ts +136 -53
  62. package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
  63. package/src/friday-session.forward-agent.test.ts +44 -12
  64. package/src/friday-session.ts +44 -20
  65. package/src/history/normalize-message.test.ts +35 -8
  66. package/src/history/normalize-message.ts +24 -12
  67. package/src/history/read-transcript.ts +1 -4
  68. package/src/http/handlers/agent-config.test.ts +10 -3
  69. package/src/http/handlers/agent-config.ts +22 -8
  70. package/src/http/handlers/agents-list.test.ts +1 -5
  71. package/src/http/handlers/cancel.test.ts +12 -3
  72. package/src/http/handlers/cancel.ts +4 -2
  73. package/src/http/handlers/device-approve.test.ts +12 -3
  74. package/src/http/handlers/device-approve.ts +33 -21
  75. package/src/http/handlers/files-download.ts +17 -13
  76. package/src/http/handlers/files.test.ts +8 -2
  77. package/src/http/handlers/files.ts +21 -7
  78. package/src/http/handlers/health.test.ts +43 -11
  79. package/src/http/handlers/health.ts +22 -6
  80. package/src/http/handlers/history-messages.test.ts +51 -9
  81. package/src/http/handlers/history-messages.ts +4 -1
  82. package/src/http/handlers/history-sessions.test.ts +46 -9
  83. package/src/http/handlers/history-sessions.ts +5 -3
  84. package/src/http/handlers/history-set-title.test.ts +14 -5
  85. package/src/http/handlers/link-preview.test.ts +57 -16
  86. package/src/http/handlers/link-preview.ts +4 -1
  87. package/src/http/handlers/messages.test.ts +12 -8
  88. package/src/http/handlers/messages.ts +57 -19
  89. package/src/http/handlers/models-list.ts +14 -8
  90. package/src/http/handlers/nodes-approve.test.ts +15 -4
  91. package/src/http/handlers/nodes-approve.ts +38 -40
  92. package/src/http/handlers/plugin-info.ts +5 -6
  93. package/src/http/handlers/plugin-upgrade.ts +4 -1
  94. package/src/http/handlers/sse.ts +3 -1
  95. package/src/http/server.ts +9 -6
  96. package/src/link-preview/og-parse.test.ts +6 -2
  97. package/src/link-preview/og-parse.ts +10 -3
  98. package/src/link-preview/preview-service.ts +4 -1
  99. package/src/link-preview/ssrf-guard.test.ts +72 -15
  100. package/src/link-preview/ssrf-guard.ts +2 -1
  101. package/src/media-fetch.test.ts +7 -2
  102. package/src/media-fetch.ts +1 -2
  103. package/src/openclaw.d.ts +16 -9
  104. package/src/plugin-install-info.ts +20 -9
  105. package/src/run-metadata.ts +2 -1
  106. package/src/session/session-manager.ts +19 -11
  107. package/src/session-usage-snapshot.ts +3 -1
  108. package/src/session-usage-store.ts +3 -1
  109. package/src/skills-discovery.test.ts +14 -10
  110. package/src/skills-discovery.ts +43 -27
  111. package/src/sse/emitter.test.ts +1 -1
  112. package/src/sse/emitter.ts +9 -3
  113. package/src/sse/offline-queue.ts +17 -8
  114. package/src/test-support/app-simulator.ts +17 -3
  115. package/src/test-support/mock-dispatch.ts +17 -4
  116. package/src/thinking-levels.ts +3 -1
  117. package/src/tool-catalog.ts +16 -7
  118. package/src/upgrade-runtime.ts +4 -2
  119. package/src/version.ts +5 -1
  120. 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, seq: 1, stream: "lifecycle",
207
- sessionKey: mainSessionKey, data: { phase: "start" },
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, seq: 2, stream: "tool",
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, seq: 1, stream: "lifecycle",
247
- sessionKey: mainSessionKey, data: { phase: "start" },
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, seq: 2, stream: "tool",
260
+ runId: mainRunId,
261
+ seq: 2,
262
+ stream: "tool",
253
263
  sessionKey: mainSessionKey,
254
264
  data: {
255
- phase: "result", name: "sessions_spawn", toolCallId: "c1",
256
- result: { details: makeSpawnToolResult({ childSessionKey: childKey, runId: bareRunId, taskName: "cr" }) },
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, seq: 1, stream: "thinking",
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, seq: 1, stream: "lifecycle",
286
- sessionKey: mainSessionKey, data: { phase: "start" },
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, seq: 2, stream: "assistant",
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, seq: 1, stream: "lifecycle",
309
- sessionKey: mainSessionKey, data: { phase: "start" },
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, seq: 2, stream: "tool",
337
+ runId: mainRunId,
338
+ seq: 2,
339
+ stream: "tool",
314
340
  sessionKey: mainSessionKey,
315
341
  data: {
316
- phase: "result", name: "sessions_spawn", toolCallId: "c1",
317
- result: { details: makeSpawnToolResult({ childSessionKey: childKey, runId: bareRunId, taskName: "cr" }) },
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, seq: 5, stream: "lifecycle",
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, seq: 1, stream: "lifecycle",
346
- sessionKey: mainSessionKey, data: { phase: "start" },
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, seq: 2, stream: "tool",
389
+ runId: mainRunId,
390
+ seq: 2,
391
+ stream: "tool",
351
392
  sessionKey: mainSessionKey,
352
393
  data: {
353
- phase: "result", name: "sessions_spawn", toolCallId: "c1",
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, seq: 5, stream: "lifecycle",
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, seq: 1, stream: "lifecycle",
383
- sessionKey: mainSessionKey, data: { phase: "start" },
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, seq: 2, stream: "tool",
435
+ runId: mainRunId,
436
+ seq: 2,
437
+ stream: "tool",
388
438
  sessionKey: mainSessionKey,
389
439
  data: {
390
- phase: "result", name: "sessions_spawn", toolCallId: "c1",
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, seq: 5, stream: "lifecycle",
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, seq: 6, stream: "lifecycle",
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, seq: 1, stream: "lifecycle",
430
- sessionKey: mainSessionKey, data: { phase: "start" },
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, seq: 2, stream: "tool",
494
+ runId: mainRunId,
495
+ seq: 2,
496
+ stream: "tool",
436
497
  sessionKey: mainSessionKey,
437
498
  data: {
438
- phase: "result", name: "sessions_spawn", toolCallId: "c1",
439
- result: { details: makeSpawnToolResult({ childSessionKey: childKeyA, runId: bareA, taskName: "reviewer" }) },
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, seq: 1, stream: "thinking",
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, seq: 2, stream: "tool",
533
+ runId: compoundA,
534
+ seq: 2,
535
+ stream: "tool",
465
536
  sessionKey: mainSessionKey,
466
537
  data: {
467
- phase: "result", name: "sessions_spawn", toolCallId: "c2",
468
- result: { details: makeSpawnToolResult({ childSessionKey: childKeyB, runId: bareB, taskName: "lint" }) },
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, seq: 1, stream: "assistant",
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, seq: 5, stream: "lifecycle",
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, seq: 6, stream: "lifecycle",
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 { createTempHistoryDir, removeTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
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 { accumulateRunUsage, resetRunUsageAccumulatorForTest } from "./agent/run-usage-accumulator.js";
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.data;
60
- const second = (sseEmitter.broadcastToRun as ReturnType<typeof vi.fn>).mock.calls[1][1].data.data;
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.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.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].data;
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(runId, { input: 100, output: 50, cacheRead: 10, total: 150 }, "my-model", "openai");
218
- accumulateRunUsage(runId, { input: 30, output: 10, cacheRead: 0, total: 40 }, "my-model", "openai");
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<string, unknown>;
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(runId, { input: 500, output: 100, cacheRead: 200, total: 800 }, "llm-model", "llm-provider");
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<string, unknown>;
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<string, unknown>;
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");
@@ -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) || /^agent:main:friday-next:direct:/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 (typeof meta.totalTokens === "number" && Number.isFinite(meta.totalTokens) && meta.totalTokens > 0) {
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(to: string | undefined, rawCtx?: Record<string, unknown>): string {
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 (evt.data as Record<string, unknown>).meta === "string"
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
- ? { label: subagentEntry.label, parentRunId: subagentEntry.parentRunId, depth: subagentEntry.depth }
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 = evt.stream === "lifecycle" && (lifecyclePhase === "end" || lifecyclePhase === "error");
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 = lifecyclePhase === "error" ? String(evt.data.error ?? "unknown") : undefined;
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(