@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
@@ -23,7 +23,11 @@ class MockRes extends EventEmitter {
23
23
  }
24
24
  }
25
25
 
26
- function mockReq(method: string, url: string, headers: Record<string, string> = {}): IncomingMessage {
26
+ function mockReq(
27
+ method: string,
28
+ url: string,
29
+ headers: Record<string, string> = {},
30
+ ): IncomingMessage {
27
31
  return { method, url, headers } as IncomingMessage;
28
32
  }
29
33
 
@@ -82,7 +86,17 @@ describe("handleHealth", () => {
82
86
  {
83
87
  nodeId: NODE_ID,
84
88
  caps: ["location", "canvas"],
85
- commands: ["location.get", "canvas.present", "canvas.hide", "canvas.navigate", "canvas.eval", "canvas.snapshot", "canvas.a2ui.push", "canvas.a2ui.pushJSONL", "canvas.a2ui.reset"],
89
+ commands: [
90
+ "location.get",
91
+ "canvas.present",
92
+ "canvas.hide",
93
+ "canvas.navigate",
94
+ "canvas.eval",
95
+ "canvas.snapshot",
96
+ "canvas.a2ui.push",
97
+ "canvas.a2ui.pushJSONL",
98
+ "canvas.a2ui.reset",
99
+ ],
86
100
  },
87
101
  ],
88
102
  });
@@ -104,9 +118,7 @@ describe("handleHealth", () => {
104
118
  it("returns degraded when node is missing required caps", async () => {
105
119
  mockListNodePairing.mockResolvedValueOnce({
106
120
  pending: [],
107
- paired: [
108
- { nodeId: NODE_ID, caps: ["canvas"], commands: ["canvas.present"] },
109
- ],
121
+ paired: [{ nodeId: NODE_ID, caps: ["canvas"], commands: ["canvas.present"] }],
110
122
  });
111
123
 
112
124
  const req = mockReq("GET", `/friday-next/health?nodeDeviceId=${NODE_ID}`, {
@@ -127,7 +139,10 @@ describe("handleHealth", () => {
127
139
  pending: [{ requestId: REQUEST_ID, nodeId: NODE_ID }],
128
140
  paired: [],
129
141
  });
130
- mockApproveNodePairing.mockResolvedValueOnce({ requestId: REQUEST_ID, node: { nodeId: NODE_ID } });
142
+ mockApproveNodePairing.mockResolvedValueOnce({
143
+ requestId: REQUEST_ID,
144
+ node: { nodeId: NODE_ID },
145
+ });
131
146
 
132
147
  const req = mockReq("GET", `/friday-next/health?nodeDeviceId=${NODE_ID}&selfHeal=true`, {
133
148
  authorization: "Bearer test-token",
@@ -186,7 +201,10 @@ describe("handleHealth", () => {
186
201
  pending: [{ requestId: REQUEST_ID, nodeId: NODE_ID }],
187
202
  paired: [],
188
203
  });
189
- mockApproveNodePairing.mockResolvedValueOnce({ status: "forbidden", missingScope: "operator.admin" });
204
+ mockApproveNodePairing.mockResolvedValueOnce({
205
+ status: "forbidden",
206
+ missingScope: "operator.admin",
207
+ });
190
208
 
191
209
  const req = mockReq("GET", `/friday-next/health?nodeDeviceId=${NODE_ID}&selfHeal=true`, {
192
210
  authorization: "Bearer test-token",
@@ -236,14 +254,28 @@ describe("handleHealth", () => {
236
254
  {
237
255
  nodeId: NODE_ID,
238
256
  caps: ["location", "canvas"],
239
- commands: ["location.get", "canvas.present", "canvas.hide", "canvas.navigate", "canvas.eval", "canvas.snapshot", "canvas.a2ui.push", "canvas.a2ui.pushJSONL", "canvas.a2ui.reset"],
257
+ commands: [
258
+ "location.get",
259
+ "canvas.present",
260
+ "canvas.hide",
261
+ "canvas.navigate",
262
+ "canvas.eval",
263
+ "canvas.snapshot",
264
+ "canvas.a2ui.push",
265
+ "canvas.a2ui.pushJSONL",
266
+ "canvas.a2ui.reset",
267
+ ],
240
268
  },
241
269
  ],
242
270
  });
243
271
 
244
- const req = mockReq("GET", `/friday-next/health?deviceId=${DEVICE_ID}&nodeDeviceId=${NODE_ID}`, {
245
- authorization: "Bearer test-token",
246
- });
272
+ const req = mockReq(
273
+ "GET",
274
+ `/friday-next/health?deviceId=${DEVICE_ID}&nodeDeviceId=${NODE_ID}`,
275
+ {
276
+ authorization: "Bearer test-token",
277
+ },
278
+ );
247
279
  const res = new MockRes() as unknown as ServerResponse;
248
280
  await handleHealth(req, res);
249
281
  const body = JSON.parse((res as unknown as MockRes).body);
@@ -75,7 +75,10 @@ export async function handleHealth(req: IncomingMessage, res: ServerResponse): P
75
75
  result.nodePairing = await checkNodePairing(nodeDeviceId, selfHeal, result, log);
76
76
  }
77
77
 
78
- result.ok = !result.nodePairing || (result.nodePairing.status === "ok" || result.nodePairing.status === "pending");
78
+ result.ok =
79
+ !result.nodePairing ||
80
+ result.nodePairing.status === "ok" ||
81
+ result.nodePairing.status === "pending";
79
82
 
80
83
  res.statusCode = 200;
81
84
  res.setHeader("Content-Type", "application/json");
@@ -113,7 +116,8 @@ async function checkNodePairing(
113
116
  };
114
117
  }
115
118
 
116
- const pairedNodes: Array<{ nodeId: string; caps?: string[]; commands?: string[] }> = listData?.paired ?? [];
119
+ const pairedNodes: Array<{ nodeId: string; caps?: string[]; commands?: string[] }> =
120
+ listData?.paired ?? [];
117
121
  const pairedMatch = pairedNodes.find(
118
122
  (entry) => entry.nodeId?.trim().toUpperCase() === normalizedNodeId,
119
123
  );
@@ -156,16 +160,24 @@ async function checkNodePairing(
156
160
 
157
161
  if (pendingMatch && selfHeal) {
158
162
  try {
159
- const callerScopes = ["operator.admin", "operator.pairing", "operator.read", "operator.write"];
163
+ const callerScopes = [
164
+ "operator.admin",
165
+ "operator.pairing",
166
+ "operator.read",
167
+ "operator.write",
168
+ ];
160
169
  const approved = await approveNodePairing(pendingMatch.requestId, { callerScopes });
161
- const succeeded = approved != null && !("status" in approved && (approved as any).status === "forbidden") && "requestId" in approved;
170
+ const succeeded =
171
+ approved != null &&
172
+ !("status" in approved && approved.status === "forbidden") &&
173
+ "requestId" in approved;
162
174
  (result.repairActions ??= []).push({
163
175
  component: "nodePairing",
164
176
  action: "approveNodePairing",
165
177
  result: succeeded ? "ok" : "failed",
166
178
  detail: succeeded
167
179
  ? `Auto-approved node ${normalizedNodeId}`
168
- : `approveNodePairing returned status=${(approved as any)?.status ?? "null"}`,
180
+ : `approveNodePairing returned status=${approved?.status ?? "null"}`,
169
181
  });
170
182
  if (succeeded) {
171
183
  log.info(`Auto-approved node ${normalizedNodeId}`);
@@ -191,5 +203,9 @@ async function checkNodePairing(
191
203
  return { status: "pending", detail: "Node is pending approval", nodePaired: false };
192
204
  }
193
205
 
194
- return { status: "not_found", detail: `Node ${normalizedNodeId} not registered`, nodePaired: false };
206
+ return {
207
+ status: "not_found",
208
+ detail: `Node ${normalizedNodeId} not registered`,
209
+ nodePaired: false,
210
+ };
195
211
  }
@@ -35,7 +35,12 @@ const CFG = {
35
35
  let tmpDir = "";
36
36
 
37
37
  /** Auth config + optional subagent fallback. */
38
- function setRuntime(getSessionMessages?: (params: { sessionKey: string; limit?: number }) => Promise<{ messages?: unknown[] }>): void {
38
+ function setRuntime(
39
+ getSessionMessages?: (params: {
40
+ sessionKey: string;
41
+ limit?: number;
42
+ }) => Promise<{ messages?: unknown[] }>,
43
+ ): void {
39
44
  setFridayNextRuntime({
40
45
  config: { loadConfig: () => CFG },
41
46
  logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} },
@@ -100,13 +105,30 @@ describe("handleHistoryMessages", () => {
100
105
  it("reads the transcript file from disk including user + assistant messages", async () => {
101
106
  const file = writeTranscript("sess.jsonl", [
102
107
  { type: "session", version: 1, sessionId: "s" },
103
- { type: "message", id: "u1", timestamp: "2026-01-01T00:00:00.000Z", message: { role: "user", content: "hi there" } },
104
- { type: "message", id: "a1", timestamp: "2026-01-01T00:00:01.000Z", message: { role: "assistant", content: [{ type: "text", text: "hello" }], model: "openai/gpt-4" } },
108
+ {
109
+ type: "message",
110
+ id: "u1",
111
+ timestamp: "2026-01-01T00:00:00.000Z",
112
+ message: { role: "user", content: "hi there" },
113
+ },
114
+ {
115
+ type: "message",
116
+ id: "a1",
117
+ timestamp: "2026-01-01T00:00:01.000Z",
118
+ message: {
119
+ role: "assistant",
120
+ content: [{ type: "text", text: "hello" }],
121
+ model: "openai/gpt-4",
122
+ },
123
+ },
105
124
  ]);
106
125
  setForward({ "agent:main:main": { sessionId: "s", sessionFile: file } });
107
126
 
108
127
  const res = new MockRes();
109
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
128
+ await handleHistoryMessages(
129
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
130
+ res as any,
131
+ );
110
132
  expect(res.statusCode).toBe(200);
111
133
  const body = JSON.parse(res.body);
112
134
  expect(body.messages.map((m: any) => m.role)).toEqual(["user", "assistant"]);
@@ -117,7 +139,15 @@ describe("handleHistoryMessages", () => {
117
139
  it("returns the cumulative sessionUsage snapshot from the store", async () => {
118
140
  const file = writeTranscript("usage.jsonl", [
119
141
  { type: "message", id: "u1", message: { role: "user", content: "hi" } },
120
- { type: "message", id: "a1", message: { role: "assistant", content: [{ type: "text", text: "yo" }], model: "openai/gpt-4" } },
142
+ {
143
+ type: "message",
144
+ id: "a1",
145
+ message: {
146
+ role: "assistant",
147
+ content: [{ type: "text", text: "yo" }],
148
+ model: "openai/gpt-4",
149
+ },
150
+ },
121
151
  ]);
122
152
  setForward({
123
153
  "agent:main:main": {
@@ -132,7 +162,10 @@ describe("handleHistoryMessages", () => {
132
162
  });
133
163
 
134
164
  const res = new MockRes();
135
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
165
+ await handleHistoryMessages(
166
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
167
+ res as any,
168
+ );
136
169
  expect(res.statusCode).toBe(200);
137
170
  const body = JSON.parse(res.body);
138
171
  expect(body.sessionUsage).toBeDefined();
@@ -148,7 +181,10 @@ describe("handleHistoryMessages", () => {
148
181
  setForward({ "agent:main:main": { sessionId: "s", sessionFile: file } });
149
182
 
150
183
  const res = new MockRes();
151
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
184
+ await handleHistoryMessages(
185
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
186
+ res as any,
187
+ );
152
188
  const body = JSON.parse(res.body);
153
189
  expect(body.sessionUsage).toBeUndefined();
154
190
  });
@@ -162,7 +198,10 @@ describe("handleHistoryMessages", () => {
162
198
 
163
199
  const res = new MockRes();
164
200
  await handleHistoryMessages(
165
- makeReq("/friday-next/history/messages?sessionKey=agent:main:friday:direct:ABCD-1234:9", AUTH),
201
+ makeReq(
202
+ "/friday-next/history/messages?sessionKey=agent:main:friday:direct:ABCD-1234:9",
203
+ AUTH,
204
+ ),
166
205
  res as any,
167
206
  );
168
207
  const body = JSON.parse(res.body);
@@ -209,7 +248,10 @@ describe("handleHistoryMessages", () => {
209
248
  messages: [{ role: "assistant", content: "fallback", __openclaw: { id: "a1", seq: 1 } }],
210
249
  }));
211
250
  const res = new MockRes();
212
- await handleHistoryMessages(makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH), res as any);
251
+ await handleHistoryMessages(
252
+ makeReq("/friday-next/history/messages?sessionKey=agent:main:main", AUTH),
253
+ res as any,
254
+ );
213
255
  const body = JSON.parse(res.body);
214
256
  expect(body.messages.map((m: any) => m.id)).toEqual(["a1"]);
215
257
  });
@@ -15,7 +15,10 @@ import { fileURLToPath } from "node:url";
15
15
  import { getFridayNextRuntime } from "../../runtime.js";
16
16
  import { extractBearerToken } from "../middleware/auth.js";
17
17
  import { normalizeHistoryMessages } from "../../history/normalize-message.js";
18
- import { readSessionTranscriptRawMessages, resolveSessionId } from "../../history/read-transcript.js";
18
+ import {
19
+ readSessionTranscriptRawMessages,
20
+ resolveSessionId,
21
+ } from "../../history/read-transcript.js";
19
22
  import { resolveMediaAttachment } from "./files.js";
20
23
  import { readSessionUsageSnapshotFromStore } from "../../session-usage-store.js";
21
24
 
@@ -33,7 +33,11 @@ let tmpDir = "";
33
33
  /** Write a non-empty transcript file and return its absolute path. */
34
34
  function transcript(name: string): string {
35
35
  const file = path.join(tmpDir, name);
36
- fs.writeFileSync(file, `${JSON.stringify({ type: "message", id: "m", message: { role: "user", content: "hi" } })}\n`, "utf-8");
36
+ fs.writeFileSync(
37
+ file,
38
+ `${JSON.stringify({ type: "message", id: "m", message: { role: "user", content: "hi" } })}\n`,
39
+ "utf-8",
40
+ );
37
41
  return file;
38
42
  }
39
43
 
@@ -86,7 +90,11 @@ describe("handleHistorySessions", () => {
86
90
  { agents: { list: [{ id: "main" }] } },
87
91
  {
88
92
  main: {
89
- "agent:main:main": { sessionId: "s-main", updatedAt: 100, sessionFile: transcript("main.jsonl") },
93
+ "agent:main:main": {
94
+ sessionId: "s-main",
95
+ updatedAt: 100,
96
+ sessionFile: transcript("main.jsonl"),
97
+ },
90
98
  "agent:main:friday:direct:dev:1": {
91
99
  sessionId: "s-fd",
92
100
  updatedAt: 300,
@@ -112,8 +120,16 @@ describe("handleHistorySessions", () => {
112
120
  { agents: { list: [{ id: "main" }] } },
113
121
  {
114
122
  main: {
115
- "agent:main:live": { sessionId: "a", updatedAt: 1, sessionFile: transcript("live.jsonl") },
116
- "agent:main:archived": { sessionId: "b", updatedAt: 2, sessionFile: path.join(tmpDir, "gone.jsonl") },
123
+ "agent:main:live": {
124
+ sessionId: "a",
125
+ updatedAt: 1,
126
+ sessionFile: transcript("live.jsonl"),
127
+ },
128
+ "agent:main:archived": {
129
+ sessionId: "b",
130
+ updatedAt: 2,
131
+ sessionFile: path.join(tmpDir, "gone.jsonl"),
132
+ },
117
133
  },
118
134
  },
119
135
  );
@@ -129,11 +145,32 @@ describe("handleHistorySessions", () => {
129
145
  {
130
146
  main: {
131
147
  "agent:main:main": { sessionId: "ok", updatedAt: 5, sessionFile: transcript("ok.jsonl") },
132
- "agent:main:main:heartbeat": { sessionId: "hb", updatedAt: 4, sessionFile: transcript("hb.jsonl") },
133
- "agent:main:cron:abc": { sessionId: "c", updatedAt: 3, sessionFile: transcript("c.jsonl") },
134
- "agent:main:subagent:xyz": { sessionId: "sa", updatedAt: 2, sessionFile: transcript("sa.jsonl") },
135
- "agent:main:dreaming-narrative-rem-1": { sessionId: "d", updatedAt: 1, sessionFile: transcript("d.jsonl") },
136
- "agent:main:child": { sessionId: "ch", updatedAt: 6, spawnedBy: "agent:main:main", sessionFile: transcript("ch.jsonl") },
148
+ "agent:main:main:heartbeat": {
149
+ sessionId: "hb",
150
+ updatedAt: 4,
151
+ sessionFile: transcript("hb.jsonl"),
152
+ },
153
+ "agent:main:cron:abc": {
154
+ sessionId: "c",
155
+ updatedAt: 3,
156
+ sessionFile: transcript("c.jsonl"),
157
+ },
158
+ "agent:main:subagent:xyz": {
159
+ sessionId: "sa",
160
+ updatedAt: 2,
161
+ sessionFile: transcript("sa.jsonl"),
162
+ },
163
+ "agent:main:dreaming-narrative-rem-1": {
164
+ sessionId: "d",
165
+ updatedAt: 1,
166
+ sessionFile: transcript("d.jsonl"),
167
+ },
168
+ "agent:main:child": {
169
+ sessionId: "ch",
170
+ updatedAt: 6,
171
+ spawnedBy: "agent:main:main",
172
+ sessionFile: transcript("ch.jsonl"),
173
+ },
137
174
  global: { sessionId: "g", updatedAt: 7, sessionFile: transcript("g.jsonl") },
138
175
  },
139
176
  },
@@ -139,12 +139,14 @@ function readAgentSessions(agentId: string): FridayHistorySessionSummary[] {
139
139
  sessionKey: canonicalKey,
140
140
  agentId,
141
141
  ...(readString(entry.sessionId) ? { sessionId: readString(entry.sessionId) } : {}),
142
- ...(readNumber(entry.updatedAt) !== undefined ? { updatedAt: readNumber(entry.updatedAt) } : {}),
143
- ...(readString(entry.model) ?? readString(entry.modelOverride)
142
+ ...(readNumber(entry.updatedAt) !== undefined
143
+ ? { updatedAt: readNumber(entry.updatedAt) }
144
+ : {}),
145
+ ...((readString(entry.model) ?? readString(entry.modelOverride))
144
146
  ? { model: readString(entry.model) ?? readString(entry.modelOverride) }
145
147
  : {}),
146
148
  // Server-side session display name (matches OpenClaw's resolution order).
147
- ...(readString(entry.displayName) ?? readString(entry.label)
149
+ ...((readString(entry.displayName) ?? readString(entry.label))
148
150
  ? { title: readString(entry.displayName) ?? readString(entry.label) }
149
151
  : {}),
150
152
  });
@@ -42,7 +42,8 @@ function setForward(store: Record<string, unknown>, withWriter = true): void {
42
42
  runtime: {
43
43
  agent: {
44
44
  session: {
45
- resolveStorePath: (_s?: string, opts?: { agentId?: string }) => `/store/${opts?.agentId ?? "main"}.json`,
45
+ resolveStorePath: (_s?: string, opts?: { agentId?: string }) =>
46
+ `/store/${opts?.agentId ?? "main"}.json`,
46
47
  loadSessionStore: () => store,
47
48
  ...(withWriter
48
49
  ? {
@@ -61,7 +62,9 @@ function setForward(store: Record<string, unknown>, withWriter = true): void {
61
62
  }
62
63
 
63
64
  describe("handleHistorySetTitle", () => {
64
- beforeEach(() => setFridayNextRuntime({ config: { loadConfig: () => CFG }, logger: {} } as never));
65
+ beforeEach(() =>
66
+ setFridayNextRuntime({ config: { loadConfig: () => CFG }, logger: {} } as never),
67
+ );
65
68
  afterEach(() => resetFridayAgentForwardRuntimeForTest());
66
69
 
67
70
  it("rejects GET with 405", async () => {
@@ -102,14 +105,20 @@ describe("handleHistorySetTitle", () => {
102
105
  it("404s when the session is unknown", async () => {
103
106
  setForward({});
104
107
  const res = new MockRes();
105
- await handleHistorySetTitle(makeReq({ sessionKey: "agent:main:nope", title: "x" }, AUTH), res as any);
108
+ await handleHistorySetTitle(
109
+ makeReq({ sessionKey: "agent:main:nope", title: "x" }, AUTH),
110
+ res as any,
111
+ );
106
112
  expect(res.statusCode).toBe(404);
107
113
  });
108
114
 
109
115
  it("503s when the store writer is unavailable", async () => {
110
116
  setForward({ "agent:main:main": { sessionId: "s" } }, false);
111
117
  const res = new MockRes();
112
- await handleHistorySetTitle(makeReq({ sessionKey: "agent:main:main", title: "x" }, AUTH), res as any);
118
+ await handleHistorySetTitle(
119
+ makeReq({ sessionKey: "agent:main:main", title: "x" }, AUTH),
120
+ res as any,
121
+ );
113
122
  expect(res.statusCode).toBe(503);
114
123
  });
115
- })
124
+ });
@@ -11,7 +11,10 @@ vi.mock("node:dns/promises", () => ({
11
11
 
12
12
  import dns from "node:dns/promises";
13
13
  import { handleLinkPreview } from "./link-preview.js";
14
- import { resetLinkPreviewCacheForTest, type LinkPreviewPayload } from "../../link-preview/preview-service.js";
14
+ import {
15
+ resetLinkPreviewCacheForTest,
16
+ type LinkPreviewPayload,
17
+ } from "../../link-preview/preview-service.js";
15
18
  import { setAttachmentsDirForTest } from "./files.js";
16
19
  import { clearFridayNextRuntime, setFridayNextRuntime } from "../../runtime.js";
17
20
 
@@ -33,7 +36,10 @@ class MockRes extends EventEmitter {
33
36
  function makeReq(query: string | null, token: string | null = "tok"): IncomingMessage {
34
37
  return {
35
38
  method: "GET",
36
- url: query == null ? "/friday-next/link-preview" : `/friday-next/link-preview?url=${encodeURIComponent(query)}`,
39
+ url:
40
+ query == null
41
+ ? "/friday-next/link-preview"
42
+ : `/friday-next/link-preview?url=${encodeURIComponent(query)}`,
37
43
  headers: token ? { authorization: `Bearer ${token}` } : {},
38
44
  } as unknown as IncomingMessage;
39
45
  }
@@ -76,7 +82,10 @@ afterEach(() => {
76
82
  describe("handleLinkPreview", () => {
77
83
  it("405 on non-GET", async () => {
78
84
  const res = new MockRes();
79
- await handleLinkPreview({ method: "POST", url: "/friday-next/link-preview", headers: {} } as never, res as never);
85
+ await handleLinkPreview(
86
+ { method: "POST", url: "/friday-next/link-preview", headers: {} } as never,
87
+ res as never,
88
+ );
80
89
  expect(res.statusCode).toBe(405);
81
90
  });
82
91
 
@@ -105,7 +114,10 @@ describe("handleLinkPreview", () => {
105
114
  if (url.includes("cover.png")) {
106
115
  return new Response(PNG_BYTES, { status: 200, headers: { "content-type": "image/png" } });
107
116
  }
108
- return new Response(PAGE_HTML, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } });
117
+ return new Response(PAGE_HTML, {
118
+ status: 200,
119
+ headers: { "content-type": "text/html; charset=utf-8" },
120
+ });
109
121
  }),
110
122
  );
111
123
  const res = await invoke(makeReq("https://example.com/article"));
@@ -142,7 +154,9 @@ describe("handleLinkPreview", () => {
142
154
  const html = `<meta property="og:title" content="T">`;
143
155
  vi.stubGlobal(
144
156
  "fetch",
145
- vi.fn(async () => new Response(html, { status: 200, headers: { "content-type": "text/html" } })),
157
+ vi.fn(
158
+ async () => new Response(html, { status: 200, headers: { "content-type": "text/html" } }),
159
+ ),
146
160
  );
147
161
  const res = await invoke(makeReq("https://example.com/x"));
148
162
  expect(JSON.parse(res.body).preview.siteName).toBe("example.com");
@@ -152,7 +166,13 @@ describe("handleLinkPreview", () => {
152
166
  // 可达但无 OG/title → 退到 hostname 卡片(不再折叠)。
153
167
  vi.stubGlobal(
154
168
  "fetch",
155
- vi.fn(async () => new Response("<html><body>plain</body></html>", { status: 200, headers: { "content-type": "text/html" } })),
169
+ vi.fn(
170
+ async () =>
171
+ new Response("<html><body>plain</body></html>", {
172
+ status: 200,
173
+ headers: { "content-type": "text/html" },
174
+ }),
175
+ ),
156
176
  );
157
177
  const res = await invoke(makeReq("https://example.com/bare"));
158
178
  expect(res.statusCode).toBe(200);
@@ -166,9 +186,15 @@ describe("handleLinkPreview", () => {
166
186
  vi.fn(async (input: URL | string) => {
167
187
  const url = String(input);
168
188
  if (url.includes("favicon")) {
169
- return new Response(ICO_BYTES, { status: 200, headers: { "content-type": "image/x-icon" } });
189
+ return new Response(ICO_BYTES, {
190
+ status: 200,
191
+ headers: { "content-type": "image/x-icon" },
192
+ });
170
193
  }
171
- return new Response(`<meta property="og:title" content="Titled">`, { status: 200, headers: { "content-type": "text/html" } });
194
+ return new Response(`<meta property="og:title" content="Titled">`, {
195
+ status: 200,
196
+ headers: { "content-type": "text/html" },
197
+ });
172
198
  }),
173
199
  );
174
200
  const res = await invoke(makeReq("https://example.com/p"));
@@ -185,7 +211,10 @@ describe("handleLinkPreview", () => {
185
211
  vi.fn(async (input: URL | string) => {
186
212
  const url = String(input);
187
213
  if (url.endsWith("/favicon.ico")) {
188
- return new Response(ICO_BYTES, { status: 200, headers: { "content-type": "image/vnd.microsoft.icon" } });
214
+ return new Response(ICO_BYTES, {
215
+ status: 200,
216
+ headers: { "content-type": "image/vnd.microsoft.icon" },
217
+ });
189
218
  }
190
219
  return new Response("blocked", { status: 403, headers: { "content-type": "text/html" } }); // page blocks bots
191
220
  }),
@@ -199,29 +228,41 @@ describe("handleLinkPreview", () => {
199
228
  });
200
229
 
201
230
  it("502 fetch_failed for a dead domain (page and favicon both fail)", async () => {
202
- vi.stubGlobal("fetch", vi.fn(async () => new Response("nope", { status: 404 })));
231
+ vi.stubGlobal(
232
+ "fetch",
233
+ vi.fn(async () => new Response("nope", { status: 404 })),
234
+ );
203
235
  const res = await invoke(makeReq("https://dead.example.com/x"));
204
236
  expect(res.statusCode).toBe(502);
205
237
  expect(JSON.parse(res.body).error).toBe("fetch_failed");
206
238
  });
207
239
 
208
240
  it("502 fetch_failed on non-2xx and non-HTML responses", async () => {
209
- vi.stubGlobal("fetch", vi.fn(async () => new Response("nope", { status: 500 })));
241
+ vi.stubGlobal(
242
+ "fetch",
243
+ vi.fn(async () => new Response("nope", { status: 500 })),
244
+ );
210
245
  expect((await invoke(makeReq("https://example.com/down"))).statusCode).toBe(502);
211
246
 
212
247
  resetLinkPreviewCacheForTest();
213
248
  vi.stubGlobal(
214
249
  "fetch",
215
- vi.fn(async () => new Response("{}", { status: 200, headers: { "content-type": "application/json" } })),
250
+ vi.fn(
251
+ async () =>
252
+ new Response("{}", { status: 200, headers: { "content-type": "application/json" } }),
253
+ ),
216
254
  );
217
255
  expect((await invoke(makeReq("https://example.com/api"))).statusCode).toBe(502);
218
256
  });
219
257
 
220
258
  it("serves the second request from cache without refetching", async () => {
221
- const fetchMock = vi.fn(async () => new Response(`<meta property="og:title" content="Cached">`, {
222
- status: 200,
223
- headers: { "content-type": "text/html" },
224
- }));
259
+ const fetchMock = vi.fn(
260
+ async () =>
261
+ new Response(`<meta property="og:title" content="Cached">`, {
262
+ status: 200,
263
+ headers: { "content-type": "text/html" },
264
+ }),
265
+ );
225
266
  vi.stubGlobal("fetch", fetchMock);
226
267
  await invoke(makeReq("https://example.com/cached"));
227
268
  const afterFirst = fetchMock.mock.calls.length;
@@ -17,7 +17,10 @@ const ERROR_STATUS: Record<LinkPreviewError, number> = {
17
17
  fetch_failed: 502,
18
18
  };
19
19
 
20
- export async function handleLinkPreview(req: IncomingMessage, res: ServerResponse): Promise<boolean> {
20
+ export async function handleLinkPreview(
21
+ req: IncomingMessage,
22
+ res: ServerResponse,
23
+ ): Promise<boolean> {
21
24
  if (req.method !== "GET") {
22
25
  res.statusCode = 405;
23
26
  res.setHeader("Content-Type", "application/json");
@@ -35,9 +35,9 @@ describe("composeBodyWithMediaRefs", () => {
35
35
  });
36
36
 
37
37
  it("omits the leading blank line when text is empty (attachment-only)", () => {
38
- expect(composeBodyWithMediaRefs("", ["[media attached: file:///a]", "[media attached: file:///b]"])).toBe(
39
- "[media attached: file:///a]\n[media attached: file:///b]",
40
- );
38
+ expect(
39
+ composeBodyWithMediaRefs("", ["[media attached: file:///a]", "[media attached: file:///b]"]),
40
+ ).toBe("[media attached: file:///a]\n[media attached: file:///b]");
41
41
  });
42
42
  });
43
43
 
@@ -152,7 +152,9 @@ describe("handleMessages dispatch context (owner fields)", () => {
152
152
  const dispatchCalled = new Promise<void>((resolve) => {
153
153
  __setMockFridayDispatchForTests(async (args: unknown) => {
154
154
  const a = args as {
155
- dispatcherOptions?: { deliver?: (payload: unknown, info: { kind: string }) => Promise<void> };
155
+ dispatcherOptions?: {
156
+ deliver?: (payload: unknown, info: { kind: string }) => Promise<void>;
157
+ };
156
158
  };
157
159
  if (a.dispatcherOptions?.deliver) {
158
160
  await a.dispatcherOptions.deliver(
@@ -169,10 +171,12 @@ describe("handleMessages dispatch context (owner fields)", () => {
169
171
  req.headers = { authorization: "Bearer tok" };
170
172
  const res = new MockRes() as unknown as ServerResponse;
171
173
  let observedPayload: Record<string, unknown> | null = null;
172
- const broadcastSpy = vi.spyOn(sseEmitter, "broadcastToRun").mockImplementation((_: string, evt: unknown) => {
173
- const data = (evt as { data?: { payload?: Record<string, unknown> } })?.data;
174
- if (data?.payload) observedPayload = data.payload;
175
- });
174
+ const broadcastSpy = vi
175
+ .spyOn(sseEmitter, "broadcastToRun")
176
+ .mockImplementation((_: string, evt: unknown) => {
177
+ const data = (evt as { data?: { payload?: Record<string, unknown> } })?.data;
178
+ if (data?.payload) observedPayload = data.payload;
179
+ });
176
180
 
177
181
  const p = handleMessages(req, res);
178
182
  req.end(