@syengup/friday-channel-next 0.0.34 → 0.0.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 (186) hide show
  1. package/dist/attachments/0768c9b1-53b0-44df-83e8-be15c4ea188f.jpg +0 -0
  2. package/dist/attachments/0a379d01-116b-4da1-bf15-77cb2cbb0093.jpg +0 -0
  3. package/dist/attachments/181caab2-64a7-4004-a057-225a144f949e.mp3 +0 -0
  4. package/dist/attachments/19662331-e527-47d2-bc0e-0e19a7a91419.jpg +0 -0
  5. package/dist/attachments/26a23b2b-52df-4572-a5e1-15b34fb87e44.jpg +0 -0
  6. package/dist/attachments/2f9282c5-8db4-4c4a-a060-e65104f6f9ff.jpg +0 -0
  7. package/dist/attachments/3929ec3d-ea15-4de6-96bc-97e8b0b658a7.jpg +0 -0
  8. package/dist/attachments/403c0cbc-4e3c-4146-a3be-ff3746ee7cda.jpg +0 -0
  9. package/dist/attachments/441977f5-0f7b-4aa2-841a-1d63e787ea53.jpg +0 -0
  10. package/dist/attachments/453e8aa2-76e3-498d-8d6f-d7b96d6bf45b.jpg +0 -0
  11. package/dist/attachments/538cde71-d26e-4d3d-b901-e8dd905e668c.mp3 +0 -0
  12. package/dist/attachments/55c7f628-4ba2-4252-aa4b-4f3eb6045a8a.mp3 +0 -0
  13. package/dist/attachments/5f7683f5-8194-4698-b077-31d209525379.jpg +0 -0
  14. package/dist/attachments/60614a35-8f44-4197-b783-2f58f5a72ac8.jpeg +0 -0
  15. package/dist/attachments/62830489-8814-48b1-851c-3845e514f35e.mp3 +0 -0
  16. package/dist/attachments/66f4a62d-1531-4f38-a531-7456f9edf221.png +0 -0
  17. package/dist/attachments/6735d749-769e-483a-9b84-43b9338a720b.png +0 -0
  18. package/dist/attachments/6d1766b1-05e4-4b04-b3c8-1c25e9d182a1.png +0 -0
  19. package/dist/attachments/782b077b-06e3-484b-baf5-33e7160234ed.png +0 -0
  20. package/dist/attachments/7ad638b2-1f56-4d93-9ad8-b40346e0650f.jpg +0 -0
  21. package/dist/attachments/89f6fb15-e652-4111-a60c-baa414659052.png +0 -0
  22. package/dist/attachments/8a88b14f-442f-45fb-b01d-e51bab8f800d.mp3 +0 -0
  23. package/dist/attachments/92292034-9cf6-4f26-8d77-fddca3deb638.png +0 -0
  24. package/dist/attachments/92c2b414-d33d-4d93-bcb6-013da7bec9a4.jpg +0 -0
  25. package/dist/attachments/9664f69e-3c05-45ca-9a52-f2d0b9f9bf7e.jpg +0 -0
  26. package/dist/attachments/977d28c1-43c0-40e0-95e3-defe0f41afe8.jpg +0 -0
  27. package/dist/attachments/9df40f1a-c6e1-4177-8a03-06757a30b19e.png +0 -0
  28. package/dist/attachments/a68e6815-6163-4421-a70f-34493aa9a217.jpg +0 -0
  29. package/dist/attachments/aab32fea-6d99-47ec-ab1f-2340f31312eb.jpg +0 -0
  30. package/dist/attachments/ab403224-2fb1-49c1-8738-ea194ab65d44.png +0 -0
  31. package/dist/attachments/ac3da190-d6ee-4038-a673-8b893035a687.png +0 -0
  32. package/dist/attachments/af02be9c-87f7-4c5a-9969-7db32039bb58.png +0 -0
  33. package/dist/attachments/b011d42a-00e5-4f77-86bc-08da6112e6e1.mp3 +0 -0
  34. package/dist/attachments/b7d7df40-c627-4b1f-9b09-167b88545c25.mp3 +0 -0
  35. package/dist/attachments/c5e9bf09-a718-422c-bcb3-94c173e3755b.mp3 +0 -0
  36. package/dist/attachments/d5449e13-1995-44ba-9392-ecbfe5f9876f.jpg +0 -0
  37. package/dist/attachments/ea0069f5-01cf-4ea1-985e-3a1e426399c3.png +0 -0
  38. package/dist/attachments/f3989ff2-7b70-4a80-a896-74a6b197f7d8.png +0 -0
  39. package/dist/attachments/f64a4a14-e3aa-4eed-a8d9-1603f04baa5b.jpg +0 -0
  40. package/dist/index.d.ts +4 -0
  41. package/dist/index.js +176 -0
  42. package/dist/src/agent/abort-run.d.ts +1 -0
  43. package/dist/src/agent/abort-run.js +11 -0
  44. package/dist/src/agent/active-runs.d.ts +9 -0
  45. package/dist/src/agent/active-runs.js +20 -0
  46. package/dist/src/agent/dispatch-bridge.d.ts +5 -0
  47. package/dist/src/agent/dispatch-bridge.js +12 -0
  48. package/dist/src/agent/media-bridge.d.ts +4 -0
  49. package/dist/src/agent/media-bridge.js +21 -0
  50. package/dist/src/agent/subagent-registry.d.ts +68 -0
  51. package/dist/src/agent/subagent-registry.js +142 -0
  52. package/dist/src/agent-forward-runtime.d.ts +17 -0
  53. package/dist/src/agent-forward-runtime.js +16 -0
  54. package/dist/src/agent-run-context-bridge.d.ts +13 -0
  55. package/dist/src/agent-run-context-bridge.js +23 -0
  56. package/dist/src/channel-actions.d.ts +13 -0
  57. package/dist/src/channel-actions.js +101 -0
  58. package/dist/src/channel.d.ts +6 -0
  59. package/dist/src/channel.js +248 -0
  60. package/dist/src/collect-message-media-paths.d.ts +11 -0
  61. package/dist/src/collect-message-media-paths.js +143 -0
  62. package/dist/src/config.d.ts +15 -0
  63. package/dist/src/config.js +39 -0
  64. package/dist/src/friday-inbound-stats.d.ts +2 -0
  65. package/dist/src/friday-inbound-stats.js +8 -0
  66. package/dist/src/friday-session.d.ts +40 -0
  67. package/dist/src/friday-session.js +395 -0
  68. package/dist/src/host-config.d.ts +1 -0
  69. package/dist/src/host-config.js +15 -0
  70. package/dist/src/http/handlers/cancel.d.ts +2 -0
  71. package/dist/src/http/handlers/cancel.js +33 -0
  72. package/dist/src/http/handlers/device-approve.d.ts +2 -0
  73. package/dist/src/http/handlers/device-approve.js +125 -0
  74. package/dist/src/http/handlers/device-token.d.ts +2 -0
  75. package/dist/src/http/handlers/device-token.js +43 -0
  76. package/dist/src/http/handlers/files-download.d.ts +10 -0
  77. package/dist/src/http/handlers/files-download.js +210 -0
  78. package/dist/src/http/handlers/files-upload.d.ts +8 -0
  79. package/dist/src/http/handlers/files-upload.js +136 -0
  80. package/dist/src/http/handlers/files.d.ts +75 -0
  81. package/dist/src/http/handlers/files.js +305 -0
  82. package/dist/src/http/handlers/import.d.ts +7 -0
  83. package/dist/src/http/handlers/import.js +69 -0
  84. package/dist/src/http/handlers/info.d.ts +2 -0
  85. package/dist/src/http/handlers/info.js +13 -0
  86. package/dist/src/http/handlers/messages-list.d.ts +7 -0
  87. package/dist/src/http/handlers/messages-list.js +44 -0
  88. package/dist/src/http/handlers/messages.d.ts +34 -0
  89. package/dist/src/http/handlers/messages.js +476 -0
  90. package/dist/src/http/handlers/models-list.d.ts +10 -0
  91. package/dist/src/http/handlers/models-list.js +113 -0
  92. package/dist/src/http/handlers/nodes-approve.d.ts +2 -0
  93. package/dist/src/http/handlers/nodes-approve.js +146 -0
  94. package/dist/src/http/handlers/pair.d.ts +2 -0
  95. package/dist/src/http/handlers/pair.js +39 -0
  96. package/dist/src/http/handlers/sessions-delete.d.ts +2 -0
  97. package/dist/src/http/handlers/sessions-delete.js +49 -0
  98. package/dist/src/http/handlers/sessions-list.d.ts +8 -0
  99. package/dist/src/http/handlers/sessions-list.js +24 -0
  100. package/dist/src/http/handlers/sessions-messages-get.d.ts +2 -0
  101. package/dist/src/http/handlers/sessions-messages-get.js +55 -0
  102. package/dist/src/http/handlers/sessions-messages-post.d.ts +2 -0
  103. package/dist/src/http/handlers/sessions-messages-post.js +92 -0
  104. package/dist/src/http/handlers/sessions-messages.d.ts +2 -0
  105. package/dist/src/http/handlers/sessions-messages.js +135 -0
  106. package/dist/src/http/handlers/sessions-settings.d.ts +2 -0
  107. package/dist/src/http/handlers/sessions-settings.js +71 -0
  108. package/dist/src/http/handlers/sse.d.ts +2 -0
  109. package/dist/src/http/handlers/sse.js +70 -0
  110. package/dist/src/http/handlers/status.d.ts +2 -0
  111. package/dist/src/http/handlers/status.js +29 -0
  112. package/dist/src/http/handlers/sync.d.ts +7 -0
  113. package/dist/src/http/handlers/sync.js +56 -0
  114. package/dist/src/http/middleware/auth.d.ts +13 -0
  115. package/dist/src/http/middleware/auth.js +29 -0
  116. package/dist/src/http/middleware/body.d.ts +2 -0
  117. package/dist/src/http/middleware/body.js +24 -0
  118. package/dist/src/http/middleware/cors.d.ts +2 -0
  119. package/dist/src/http/middleware/cors.js +11 -0
  120. package/dist/src/http/server.d.ts +19 -0
  121. package/dist/src/http/server.js +87 -0
  122. package/dist/src/logging.d.ts +7 -0
  123. package/dist/src/logging.js +28 -0
  124. package/dist/src/push/apns.d.ts +15 -0
  125. package/dist/src/push/apns.js +56 -0
  126. package/dist/src/push/device-tokens.d.ts +3 -0
  127. package/dist/src/push/device-tokens.js +39 -0
  128. package/dist/src/run-metadata.d.ts +25 -0
  129. package/dist/src/run-metadata.js +139 -0
  130. package/dist/src/runtime.d.ts +13 -0
  131. package/dist/src/runtime.js +5 -0
  132. package/dist/src/session/session-manager.d.ts +22 -0
  133. package/dist/src/session/session-manager.js +190 -0
  134. package/dist/src/session-usage-snapshot.d.ts +23 -0
  135. package/dist/src/session-usage-snapshot.js +65 -0
  136. package/dist/src/sse/emitter.d.ts +59 -0
  137. package/dist/src/sse/emitter.js +219 -0
  138. package/dist/src/sse/offline-queue.d.ts +26 -0
  139. package/dist/src/sse/offline-queue.js +134 -0
  140. package/dist/src/sync/account-identity.d.ts +14 -0
  141. package/dist/src/sync/account-identity.js +101 -0
  142. package/dist/src/sync/archive.d.ts +9 -0
  143. package/dist/src/sync/archive.js +25 -0
  144. package/dist/src/sync/database.d.ts +66 -0
  145. package/dist/src/sync/database.js +364 -0
  146. package/dist/src/sync/init.d.ts +3 -0
  147. package/dist/src/sync/init.js +14 -0
  148. package/dist/src/sync/installation-id.d.ts +1 -0
  149. package/dist/src/sync/installation-id.js +41 -0
  150. package/dist/src/sync/message-accumulator.d.ts +29 -0
  151. package/dist/src/sync/message-accumulator.js +188 -0
  152. package/dist/src/sync/message-store.d.ts +68 -0
  153. package/dist/src/sync/message-store.js +262 -0
  154. package/dist/src/sync/push-store.d.ts +5 -0
  155. package/dist/src/sync/push-store.js +54 -0
  156. package/dist/src/sync/session-key.d.ts +12 -0
  157. package/dist/src/sync/session-key.js +47 -0
  158. package/dist/src/sync/sync-state.d.ts +5 -0
  159. package/dist/src/sync/sync-state.js +54 -0
  160. package/dist/src/sync/transcript-archive.d.ts +13 -0
  161. package/dist/src/sync/transcript-archive.js +37 -0
  162. package/dist/src/sync/transcript-store.d.ts +35 -0
  163. package/dist/src/sync/transcript-store.js +221 -0
  164. package/dist/src/sync/translate.d.ts +42 -0
  165. package/dist/src/sync/translate.js +171 -0
  166. package/dist/src/vendor/runtime-store.d.ts +26 -0
  167. package/dist/src/vendor/runtime-store.js +60 -0
  168. package/install.js +2 -2
  169. package/package.json +11 -10
  170. package/src/agent/subagent-registry.ts +195 -0
  171. package/src/channel.ts +6 -4
  172. package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
  173. package/src/e2e/subagent.e2e.test.ts +502 -0
  174. package/src/friday-session.ts +140 -1
  175. package/src/http/handlers/device-approve.test.ts +0 -1
  176. package/src/http/handlers/device-approve.ts +0 -2
  177. package/src/http/handlers/files-download.ts +4 -1
  178. package/src/http/handlers/files.ts +7 -4
  179. package/src/http/handlers/messages.ts +54 -4
  180. package/src/http/handlers/models-list.ts +24 -2
  181. package/src/http/handlers/nodes-approve.test.ts +288 -0
  182. package/src/http/handlers/nodes-approve.ts +189 -0
  183. package/src/http/server.ts +5 -0
  184. package/src/openclaw.d.ts +5 -0
  185. package/src/sse/emitter.ts +1 -1
  186. package/src/test-support/mock-runtime.ts +2 -0
@@ -0,0 +1,195 @@
1
+ export type SubagentStatus = "spawning" | "running" | "ended";
2
+
3
+ export type SubagentOutcome = "ok" | "error" | "timeout" | "killed" | "reset" | "deleted";
4
+
5
+ export type SubagentEntry = {
6
+ childSessionKey: string;
7
+ runId?: string;
8
+ parentRunId?: string;
9
+ deviceId: string;
10
+ label?: string;
11
+ depth: number;
12
+ status: SubagentStatus;
13
+ outcome?: SubagentOutcome;
14
+ error?: string;
15
+ };
16
+
17
+ export type SubagentMeta = {
18
+ label?: string;
19
+ parentRunId?: string;
20
+ depth: number;
21
+ };
22
+
23
+ type SpawnIntent = {
24
+ label?: string;
25
+ parentRunId: string;
26
+ deviceId: string;
27
+ depth: number;
28
+ requesterSessionKey?: string;
29
+ };
30
+
31
+ const byChildSessionKey = new Map<string, SubagentEntry>();
32
+ const byRunId = new Map<string, SubagentEntry>();
33
+ const byToolCallId = new Map<string, SpawnIntent>();
34
+ const sessionKeyToRunId = new Map<string, string>();
35
+
36
+ /** Called by forwardAgentEventRaw on lifecycle start so we can resolve parentRunId. */
37
+ export function registerSessionKeyForRun(sessionKey: string, runId: string): void {
38
+ if (!sessionKey || !runId) return;
39
+ sessionKeyToRunId.set(sessionKey, runId);
40
+ }
41
+
42
+ function resolveRunIdForSessionKey(sessionKey: string): string | undefined {
43
+ return sessionKeyToRunId.get(sessionKey);
44
+ }
45
+
46
+ /**
47
+ * Parse OpenClaw announce compound runId:
48
+ * announce:v<version>:<sessionKey>:<bareRunId>
49
+ * Example:
50
+ * announce:v1:agent:main:subagent:uuid:runId
51
+ * → { childSessionKey: "agent:main:subagent:uuid", bareRunId: "runId" }
52
+ */
53
+ const ANNOUNCE_RUN_ID_RE = /^announce:v\d+:(agent:.+?):([^:]+)$/;
54
+
55
+ function parseAnnounceRunId(runId: string): { childSessionKey: string; bareRunId: string } | null {
56
+ const m = runId.match(ANNOUNCE_RUN_ID_RE);
57
+ if (!m) return null;
58
+ return { childSessionKey: m[1] ?? "", bareRunId: m[2] ?? "" };
59
+ }
60
+
61
+ /** Look up subagent entry by any runId form (announce compound or bare). */
62
+ export function lookupByRunId(runId: string): SubagentEntry | undefined {
63
+ const direct = byRunId.get(runId);
64
+ if (direct) return direct;
65
+ const parsed = parseAnnounceRunId(runId);
66
+ if (parsed) {
67
+ const byChild = byChildSessionKey.get(parsed.childSessionKey);
68
+ if (byChild) {
69
+ // Also register the compound runId for future fast lookup
70
+ byRunId.set(runId, byChild);
71
+ return byChild;
72
+ }
73
+ return byRunId.get(parsed.bareRunId);
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ /** Look up subagent entry by childSessionKey. */
79
+ export function lookupByChildSessionKey(key: string): SubagentEntry | undefined {
80
+ return byChildSessionKey.get(key);
81
+ }
82
+
83
+ /**
84
+ * Called on sessions_spawn item.start (before tool execution).
85
+ * Stores the spawn intent so spawning SSE can be emitted immediately.
86
+ */
87
+ export function registerSpawnIntent(params: {
88
+ toolCallId: string;
89
+ label?: string;
90
+ deviceId: string;
91
+ parentRunId: string;
92
+ requesterSessionKey?: string;
93
+ }): SpawnIntent {
94
+ let depth = 1;
95
+ if (params.requesterSessionKey) {
96
+ const parent = byChildSessionKey.get(params.requesterSessionKey);
97
+ if (parent) depth = parent.depth + 1;
98
+ }
99
+ const intent: SpawnIntent = {
100
+ label: params.label,
101
+ parentRunId: params.parentRunId,
102
+ deviceId: params.deviceId.toUpperCase(),
103
+ depth,
104
+ requesterSessionKey: params.requesterSessionKey,
105
+ };
106
+ byToolCallId.set(params.toolCallId, intent);
107
+ return intent;
108
+ }
109
+
110
+ /** Consume a previously registered spawn intent. Returns undefined if none. */
111
+ export function consumeSpawnIntent(toolCallId: string): SpawnIntent | undefined {
112
+ const intent = byToolCallId.get(toolCallId);
113
+ byToolCallId.delete(toolCallId);
114
+ return intent;
115
+ }
116
+
117
+ /**
118
+ * Called when forwardAgentEventRaw sees a sessions_spawn tool result.
119
+ * Extracts childSessionKey, runId, taskName and registers the subagent.
120
+ */
121
+ export function ensureSubagentFromSpawnTool(params: {
122
+ childSessionKey: string;
123
+ bareRunId?: string;
124
+ label?: string;
125
+ deviceId: string;
126
+ parentRunId: string;
127
+ requesterSessionKey?: string;
128
+ depth?: number;
129
+ }): SubagentEntry {
130
+ const existing = byChildSessionKey.get(params.childSessionKey);
131
+ if (existing) {
132
+ if (params.bareRunId && !existing.runId) {
133
+ existing.runId = params.bareRunId;
134
+ byRunId.set(params.bareRunId, existing);
135
+ sessionKeyToRunId.set(params.childSessionKey, params.bareRunId);
136
+ }
137
+ existing.status = "running";
138
+ return existing;
139
+ }
140
+
141
+ let depth = params.depth ?? 1;
142
+ if (depth <= 1 && params.requesterSessionKey) {
143
+ const parent = byChildSessionKey.get(params.requesterSessionKey);
144
+ if (parent) depth = parent.depth + 1;
145
+ }
146
+
147
+ const entry: SubagentEntry = {
148
+ childSessionKey: params.childSessionKey,
149
+ runId: params.bareRunId,
150
+ parentRunId: params.parentRunId,
151
+ deviceId: params.deviceId.toUpperCase(),
152
+ label: params.label || undefined,
153
+ depth,
154
+ status: "running",
155
+ };
156
+ byChildSessionKey.set(params.childSessionKey, entry);
157
+ if (params.bareRunId) {
158
+ byRunId.set(params.bareRunId, entry);
159
+ sessionKeyToRunId.set(params.childSessionKey, params.bareRunId);
160
+ }
161
+ return entry;
162
+ }
163
+
164
+ /** Mark subagent as ended. Resolves by bare/compound runId or childSessionKey. */
165
+ export function registerEnded(params: {
166
+ runId?: string;
167
+ childSessionKey?: string;
168
+ outcome?: SubagentOutcome;
169
+ error?: string;
170
+ }): SubagentEntry | undefined {
171
+ let entry: SubagentEntry | undefined;
172
+ if (params.runId) entry = lookupByRunId(params.runId);
173
+ if (!entry && params.childSessionKey) entry = byChildSessionKey.get(params.childSessionKey);
174
+ if (!entry) return undefined;
175
+ entry.status = "ended";
176
+ if (params.outcome) entry.outcome = params.outcome;
177
+ if (params.error) entry.error = params.error;
178
+ return entry;
179
+ }
180
+
181
+ /** Extract subagent metadata for SSE annotation. */
182
+ export function subagentMeta(entry: SubagentEntry): SubagentMeta {
183
+ return {
184
+ label: entry.label,
185
+ parentRunId: entry.parentRunId,
186
+ depth: entry.depth,
187
+ };
188
+ }
189
+
190
+ export function resetForTest(): void {
191
+ byChildSessionKey.clear();
192
+ byRunId.clear();
193
+ byToolCallId.clear();
194
+ sessionKeyToRunId.clear();
195
+ }
package/src/channel.ts CHANGED
@@ -8,6 +8,7 @@ import crypto from "node:crypto";
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
11
+ import { createFridayNextLogger } from "./logging.js";
11
12
  import type { ChannelAccountSnapshot } from "openclaw/plugin-sdk/status-helpers";
12
13
  import { saveMediaBuffer } from "openclaw/plugin-sdk/media-store";
13
14
  import { sseEmitter } from "./sse/emitter.js";
@@ -19,6 +20,7 @@ import {
19
20
  } from "./friday-session.js";
20
21
  import { getLastFridayInboundAt } from "./friday-inbound-stats.js";
21
22
 
23
+ const logger = createFridayNextLogger("channel");
22
24
  const CHANNEL_ID = "friday-next" as const;
23
25
 
24
26
  function pickFirstString(source: Record<string, unknown>, keys: string[]): string | undefined {
@@ -156,8 +158,8 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
156
158
 
157
159
  const conn = sseEmitter.getConnection(deviceId);
158
160
  const ts = new Date().toISOString();
159
- console.error(
160
- `[Friday-OUT] [${ts}] [SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`,
161
+ logger.info(
162
+ `[SEND_TEXT] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} textLen=${text.length} online=${!!conn}`,
161
163
  );
162
164
 
163
165
  if (conn) {
@@ -243,8 +245,8 @@ export const fridayNextChannelPlugin = createChatChannelPlugin({
243
245
 
244
246
  const conn = sseEmitter.getConnection(deviceId);
245
247
  const ts = new Date().toISOString();
246
- console.error(
247
- `[Friday-OUT] [${ts}] [SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`,
248
+ logger.info(
249
+ `[SEND_MEDIA] to=${deviceId} runId=${runId ?? "(none)"} sessionKey=${sessionKey ?? "(none)"} audioAsVoice=${audioAsVoice} url=${publicUrl} online=${!!conn}`,
248
250
  );
249
251
 
250
252
  if (conn) {
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Subagent SSE smoke test — simulates the Friday iOS app connecting and
3
+ * receiving a complete subagent lifecycle via sessions_spawn tool events.
4
+ *
5
+ * Run with:
6
+ * pnpm vitest run --config vitest.e2e.config.ts src/e2e/subagent-smoke.e2e.test.ts --reporter verbose
7
+ */
8
+ import { beforeEach, describe, expect, it } from "vitest";
9
+ import { createAppSimulator } from "../test-support/app-simulator.js";
10
+ import { createTempHistoryDir, setMockRuntime } from "../test-support/mock-runtime.js";
11
+ import {
12
+ ensureSubagentFromSpawnTool,
13
+ resetForTest as resetSubagentRegistry,
14
+ } from "../agent/subagent-registry.js";
15
+ import {
16
+ forwardAgentEventRaw,
17
+ registerFridaySessionDeviceMapping,
18
+ } from "../friday-session.js";
19
+ import { sseEmitter } from "../sse/emitter.js";
20
+
21
+ const deviceId = "IOS-DEVICE-AAAA-BBBB-CCCC-DDDD";
22
+ const mainSessionKey = "agent:main:main";
23
+ const mainRunId = "main-run-001";
24
+
25
+ function spawnResult(childSessionKey: string, runId: string, taskName: string) {
26
+ return {
27
+ childSessionKey,
28
+ runId,
29
+ taskName,
30
+ };
31
+ }
32
+
33
+ function describeFrame(frame: { event?: string; data?: Record<string, unknown> }) {
34
+ const event = frame.event ?? "?";
35
+ const data = frame.data ?? {};
36
+ switch (event) {
37
+ case "connected":
38
+ return `connected deviceId=${data.deviceId} lastSeq=${data.lastSeq}`;
39
+ case "subagent": {
40
+ const extra = data.phase === "ended" ? ` outcome=${data.outcome} error=${data.error ?? "-"}` : "";
41
+ return `subagent phase=${data.phase} runId=${data.runId ?? "(pending)"} label=${data.label ?? "-"} parentRunId=${data.parentRunId ?? "-"} depth=${data.depth}${extra}`;
42
+ }
43
+ case "agent": {
44
+ const sub = data.subagent as Record<string, unknown> | undefined;
45
+ const subTag = sub ? ` SUB="agent:${sub.label ?? "?"}" depth=${sub.depth} parent=${sub.parentRunId ?? "-"}` : " (main)";
46
+ const inner = (data.data ?? {}) as Record<string, unknown>;
47
+ let detail = "";
48
+ if (data.stream === "thinking") detail = ` thinking: "${String(inner.text ?? "").slice(0, 50)}"`;
49
+ else if (data.stream === "tool") detail = ` tool=${inner.name ?? "?"} phase=${inner.phase}`;
50
+ else if (data.stream === "assistant") detail = ` text="${String(inner.text ?? inner.phase ?? "")}"`;
51
+ else if (data.stream === "lifecycle") detail = ` phase=${inner.phase}`;
52
+ return `agent stream=${data.stream} runId=${String(data.runId).slice(0, 30)}…${subTag}${detail}`;
53
+ }
54
+ default:
55
+ return `${event}`;
56
+ }
57
+ }
58
+
59
+ describe("subagent smoke", () => {
60
+ let historyDir = "";
61
+
62
+ beforeEach(() => {
63
+ historyDir = createTempHistoryDir();
64
+ setMockRuntime({ historyDir, authToken: "smoke-token" });
65
+ resetSubagentRegistry();
66
+ });
67
+
68
+ it("full nested subagent lifecycle via sessions_spawn tool events", async () => {
69
+ const app = createAppSimulator({ token: "smoke-token", deviceId });
70
+ await app.connectSSE();
71
+ registerFridaySessionDeviceMapping(mainSessionKey, deviceId);
72
+
73
+ // ── Main run lifecycle start ──────────────────────────
74
+ forwardAgentEventRaw({
75
+ runId: mainRunId, seq: 1, stream: "lifecycle",
76
+ sessionKey: mainSessionKey, data: { phase: "start" },
77
+ });
78
+
79
+ // ── Subagent A: tool.start (spawning) ─────────────────
80
+ const childKeyA = "agent:main:subagent:code-reviewer";
81
+ const bareA = "bare-run-reviewer";
82
+ const compoundA = `announce:v1:${childKeyA}:${bareA}`;
83
+
84
+ forwardAgentEventRaw({
85
+ runId: mainRunId, seq: 2, stream: "tool",
86
+ sessionKey: mainSessionKey,
87
+ data: {
88
+ phase: "start", name: "sessions_spawn",
89
+ toolCallId: "call_a",
90
+ args: { taskName: "code-reviewer" },
91
+ },
92
+ });
93
+
94
+ // ── Subagent A: tool.result (spawned) ─────────────────
95
+ forwardAgentEventRaw({
96
+ runId: mainRunId, seq: 3, stream: "tool",
97
+ sessionKey: mainSessionKey,
98
+ data: {
99
+ phase: "result", name: "sessions_spawn", toolCallId: "call_1",
100
+ meta: "code-reviewer",
101
+ result: { details: spawnResult(childKeyA, bareA, "code-reviewer") },
102
+ },
103
+ });
104
+ sseEmitter.trackDeviceForRun(deviceId, compoundA);
105
+
106
+ // ── Subagent A: agent events (subagent's own sessionKey) ─
107
+ forwardAgentEventRaw({
108
+ runId: compoundA, seq: 1, stream: "lifecycle",
109
+ data: { phase: "start" }, sessionKey: childKeyA,
110
+ });
111
+ forwardAgentEventRaw({
112
+ runId: compoundA, seq: 2, stream: "thinking",
113
+ data: { text: "Let me review the code for issues…", delta: "Let me review the code for issues…", reasoningPrefixChars: 0 },
114
+ sessionKey: childKeyA,
115
+ });
116
+ forwardAgentEventRaw({
117
+ runId: compoundA, seq: 3, stream: "tool",
118
+ data: { phase: "start", name: "read", args: { path: "src/app.ts" } },
119
+ sessionKey: childKeyA,
120
+ });
121
+ forwardAgentEventRaw({
122
+ runId: compoundA, seq: 4, stream: "tool",
123
+ data: { phase: "result", name: "read", result: "file contents…" },
124
+ sessionKey: childKeyA,
125
+ });
126
+ forwardAgentEventRaw({
127
+ runId: compoundA, seq: 5, stream: "assistant",
128
+ data: { phase: "delta", text: "Found 3 issues in the code." },
129
+ sessionKey: childKeyA,
130
+ });
131
+
132
+ // ── Nested: Subagent B spawning + spawned ────────────
133
+ const childKeyB = "agent:main:subagent:lint";
134
+ const bareB = "bare-run-lint";
135
+ const compoundB = `announce:v1:${childKeyB}:${bareB}`;
136
+
137
+ forwardAgentEventRaw({
138
+ runId: compoundA, seq: 7, stream: "tool",
139
+ sessionKey: mainSessionKey,
140
+ data: {
141
+ phase: "start", name: "sessions_spawn",
142
+ toolCallId: "call_b",
143
+ args: { taskName: "lint" },
144
+ },
145
+ });
146
+
147
+ forwardAgentEventRaw({
148
+ runId: compoundA, seq: 8, stream: "tool",
149
+ sessionKey: mainSessionKey,
150
+ data: {
151
+ phase: "result", name: "sessions_spawn", toolCallId: "call_2",
152
+ meta: "lint",
153
+ result: { details: spawnResult(childKeyB, bareB, "lint") },
154
+ },
155
+ });
156
+ sseEmitter.trackDeviceForRun(deviceId, compoundB);
157
+
158
+ // ── Subagent B: agent event (subagent's own sessionKey) ─
159
+ forwardAgentEventRaw({
160
+ runId: compoundB, seq: 1, stream: "assistant",
161
+ data: { phase: "delta", text: "No lint errors found." },
162
+ sessionKey: childKeyB,
163
+ });
164
+
165
+ // ── Subagent B ended (subagent's own sessionKey) ──────
166
+ forwardAgentEventRaw({
167
+ runId: compoundB, seq: 2, stream: "lifecycle",
168
+ data: { phase: "end" },
169
+ sessionKey: childKeyB,
170
+ });
171
+
172
+ // ── Subagent A ended (subagent's own sessionKey) ──────
173
+ forwardAgentEventRaw({
174
+ runId: compoundA, seq: 7, stream: "lifecycle",
175
+ data: { phase: "end" },
176
+ sessionKey: childKeyA,
177
+ });
178
+
179
+ // Wait for SSE flush
180
+ await new Promise((r) => setTimeout(r, 50));
181
+ const frames = app.getSseFrames();
182
+
183
+ // ── Verbose output ──────────────────────────────────
184
+ console.log("\n══════════════════════════════════════════════════════");
185
+ console.log(" SSE Frames (as received by the iOS app)");
186
+ console.log("══════════════════════════════════════════════════════\n");
187
+ for (const frame of frames) {
188
+ console.log(`[${String(frame.id ?? "-").padStart(3)}] ${describeFrame(frame)}`);
189
+ }
190
+ console.log("\n══════════════════════════════════════════════════════");
191
+ const subagentCount = frames.filter((f) => f.event === "subagent").length;
192
+ const annotatedCount = frames.filter(
193
+ (f) => f.event === "agent" && f.data?.subagent != null,
194
+ ).length;
195
+ console.log(` Totals: ${frames.length} frames`);
196
+ console.log(` subagent lifecycle events: ${subagentCount}`);
197
+ console.log(` agent events with subagent annotation: ${annotatedCount}`);
198
+ console.log("══════════════════════════════════════════════════════\n");
199
+
200
+ // ── Assertions ──────────────────────────────────────
201
+ expect(subagentCount).toBeGreaterThanOrEqual(6); // 2 spawning + 2 spawned + 2 ended
202
+ expect(annotatedCount).toBeGreaterThanOrEqual(5);
203
+
204
+ // Verify subagent phases
205
+ const subagentFrames = frames.filter((f) => f.event === "subagent");
206
+ const phases = subagentFrames.map((f) => f.data?.phase);
207
+ expect(phases.filter((p) => p === "spawning")).toHaveLength(2);
208
+ expect(phases.filter((p) => p === "spawned")).toHaveLength(2);
209
+ expect(phases.filter((p) => p === "ended")).toHaveLength(2);
210
+
211
+ // Verify annotation only on subagent events
212
+ for (const f of frames) {
213
+ if (f.event !== "agent") continue;
214
+ const hasSub = f.data?.subagent != null;
215
+ const runId = f.data?.runId as string | undefined;
216
+ if (runId === mainRunId) {
217
+ expect(hasSub, `main agent annotated: ${describeFrame(f)}`).toBe(false);
218
+ }
219
+ }
220
+
221
+ app.disconnectSSE();
222
+ });
223
+ });