@syengup/friday-channel-next 0.0.35 → 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 (185) 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/package.json +11 -10
  169. package/src/agent/subagent-registry.ts +195 -0
  170. package/src/channel.ts +6 -4
  171. package/src/e2e/subagent-smoke.e2e.test.ts +223 -0
  172. package/src/e2e/subagent.e2e.test.ts +502 -0
  173. package/src/friday-session.ts +140 -1
  174. package/src/http/handlers/device-approve.test.ts +0 -1
  175. package/src/http/handlers/device-approve.ts +0 -2
  176. package/src/http/handlers/files-download.ts +4 -1
  177. package/src/http/handlers/files.ts +7 -4
  178. package/src/http/handlers/messages.ts +54 -4
  179. package/src/http/handlers/models-list.ts +24 -2
  180. package/src/http/handlers/nodes-approve.test.ts +288 -0
  181. package/src/http/handlers/nodes-approve.ts +189 -0
  182. package/src/http/server.ts +5 -0
  183. package/src/openclaw.d.ts +5 -0
  184. package/src/sse/emitter.ts +1 -1
  185. package/src/test-support/mock-runtime.ts +2 -0
@@ -0,0 +1,219 @@
1
+ import { createFridayNextLogger } from "../logging.js";
2
+ import { fridaySseOfflineQueue } from "./offline-queue.js";
3
+ const logger = createFridayNextLogger("sse", "info");
4
+ export class SseConnection {
5
+ deviceId;
6
+ res;
7
+ closed = false;
8
+ pending = [];
9
+ flushTimer = null;
10
+ waitingDrain = false;
11
+ constructor(deviceId, res) {
12
+ this.deviceId = deviceId;
13
+ this.res = res;
14
+ this.res.on("drain", () => {
15
+ this.waitingDrain = false;
16
+ this.scheduleFlush();
17
+ });
18
+ this.res.on("error", () => this.close());
19
+ }
20
+ send(entry, flushNow) {
21
+ if (this.closed)
22
+ return;
23
+ const normalized = "id" in entry && "event" in entry ? entry : { id: Date.now(), event: entry };
24
+ const payload = JSON.stringify(normalized.event.data);
25
+ this.pending.push(`id: ${normalized.id}\nevent: ${normalized.event.type}\ndata: ${payload}\n\n`);
26
+ if (flushNow) {
27
+ if (this.flushTimer)
28
+ clearTimeout(this.flushTimer);
29
+ this.flushTimer = null;
30
+ this.flush();
31
+ return;
32
+ }
33
+ this.scheduleFlush();
34
+ }
35
+ sendRaw(line) {
36
+ if (this.closed)
37
+ return;
38
+ const ok = this.res.write(line);
39
+ if (ok === false)
40
+ this.waitingDrain = true;
41
+ }
42
+ scheduleFlush() {
43
+ if (this.waitingDrain || this.flushTimer)
44
+ return;
45
+ this.flushTimer = setTimeout(() => {
46
+ this.flushTimer = null;
47
+ this.flush();
48
+ }, 16);
49
+ }
50
+ flush() {
51
+ if (this.closed || this.waitingDrain || this.pending.length === 0)
52
+ return;
53
+ const data = this.pending.join("");
54
+ this.pending = [];
55
+ const ok = this.res.write(data);
56
+ if (ok === false)
57
+ this.waitingDrain = true;
58
+ }
59
+ close() {
60
+ if (this.closed)
61
+ return;
62
+ this.closed = true;
63
+ if (this.flushTimer)
64
+ clearTimeout(this.flushTimer);
65
+ this.flushTimer = null;
66
+ this.pending = [];
67
+ try {
68
+ this.res.end();
69
+ }
70
+ catch {
71
+ // ignore
72
+ }
73
+ }
74
+ get isClosed() {
75
+ return this.closed;
76
+ }
77
+ }
78
+ class SseEmitterRegistry {
79
+ connections = new Map();
80
+ runEmitter = new Map();
81
+ lastRunIdByDevice = new Map();
82
+ eventSeqByDevice = new Map();
83
+ backlogLimit = 200;
84
+ getConnectionCount() {
85
+ return this.connections.size;
86
+ }
87
+ setBacklogLimit(limit) {
88
+ this.backlogLimit = Math.max(0, Math.floor(limit));
89
+ }
90
+ getBacklogLimit() {
91
+ return this.backlogLimit;
92
+ }
93
+ /** Last persisted / assigned SSE id for device (for `connected.lastSeq`). */
94
+ latestSeqForDevice(deviceId) {
95
+ const key = deviceId.trim().toUpperCase();
96
+ const disk = fridaySseOfflineQueue.latestId(key);
97
+ const mem = this.eventSeqByDevice.get(key) ?? 0;
98
+ return Math.max(disk, mem);
99
+ }
100
+ addConnection(deviceId, res) {
101
+ const normalized = deviceId.trim().toUpperCase();
102
+ const existing = this.connections.get(normalized);
103
+ if (existing && !existing.isClosed) {
104
+ existing.close();
105
+ for (const set of this.runEmitter.values()) {
106
+ set.delete(normalized);
107
+ }
108
+ }
109
+ const conn = new SseConnection(normalized, res);
110
+ this.connections.set(normalized, conn);
111
+ logger.info(`connect ${normalized} total=${this.connections.size}`);
112
+ return conn;
113
+ }
114
+ /**
115
+ * @param expectedConn When provided, only removes if this connection is still the active one
116
+ * (avoids stale `req.close` after a reconnect replaced the map entry).
117
+ */
118
+ removeConnection(deviceId, expectedConn) {
119
+ const normalized = deviceId.trim().toUpperCase();
120
+ const current = this.connections.get(normalized);
121
+ if (expectedConn !== undefined && current !== expectedConn) {
122
+ return;
123
+ }
124
+ current?.close();
125
+ this.connections.delete(normalized);
126
+ for (const set of this.runEmitter.values())
127
+ set.delete(normalized);
128
+ logger.info(`disconnect ${normalized} total=${this.connections.size}`);
129
+ }
130
+ getConnection(deviceId) {
131
+ return this.connections.get(deviceId.trim().toUpperCase());
132
+ }
133
+ nextEntry(deviceId, event) {
134
+ const key = deviceId.trim().toUpperCase();
135
+ const diskMax = fridaySseOfflineQueue.latestId(key);
136
+ const memMax = this.eventSeqByDevice.get(key) ?? 0;
137
+ const last = Math.max(memMax, diskMax);
138
+ const id = last + 1;
139
+ this.eventSeqByDevice.set(key, id);
140
+ fridaySseOfflineQueue.append(key, id, event.type, event.data, this.backlogLimit);
141
+ return { id, event };
142
+ }
143
+ replayBacklog(deviceId, afterEventId) {
144
+ const key = deviceId.trim().toUpperCase();
145
+ const conn = this.connections.get(key);
146
+ if (!conn)
147
+ return 0;
148
+ const entries = fridaySseOfflineQueue.readAfter(key, afterEventId);
149
+ let count = 0;
150
+ for (const e of entries) {
151
+ conn.send({ id: e.id, event: { type: e.event, data: e.data } }, true);
152
+ count += 1;
153
+ }
154
+ return count;
155
+ }
156
+ broadcast(event, deviceId, flushNow) {
157
+ if (deviceId) {
158
+ const key = deviceId.trim().toUpperCase();
159
+ const entry = this.nextEntry(key, event);
160
+ this.connections.get(key)?.send(entry, flushNow);
161
+ return;
162
+ }
163
+ for (const conn of this.connections.values()) {
164
+ const entry = this.nextEntry(conn.deviceId, event);
165
+ conn.send(entry, flushNow);
166
+ }
167
+ }
168
+ trackDeviceForRun(deviceId, runId) {
169
+ const key = deviceId.trim().toUpperCase();
170
+ const set = this.runEmitter.get(runId) ?? new Set();
171
+ set.add(key);
172
+ this.runEmitter.set(runId, set);
173
+ this.lastRunIdByDevice.set(key, runId);
174
+ }
175
+ untrackRun(runId) {
176
+ this.runEmitter.delete(runId);
177
+ }
178
+ hasTrackedDevices(runId) {
179
+ return (this.runEmitter.get(runId)?.size ?? 0) > 0;
180
+ }
181
+ getDeviceIdByRunId(runId) {
182
+ const first = this.runEmitter.get(runId)?.values().next().value;
183
+ return typeof first === "string" ? first : null;
184
+ }
185
+ getSoleConnectedDeviceId() {
186
+ if (this.connections.size !== 1)
187
+ return null;
188
+ return this.connections.keys().next().value ?? null;
189
+ }
190
+ getLastRunIdForDevice(deviceId) {
191
+ return this.lastRunIdByDevice.get(deviceId.trim().toUpperCase()) ?? null;
192
+ }
193
+ broadcastToRun(runId, event, flushNow) {
194
+ const direct = typeof event.data.deviceId === "string" ? event.data.deviceId : "";
195
+ if (direct.trim()) {
196
+ this.broadcast(event, direct, flushNow);
197
+ return;
198
+ }
199
+ const set = this.runEmitter.get(runId);
200
+ if (!set || set.size === 0)
201
+ return;
202
+ for (const deviceId of set)
203
+ this.broadcast(event, deviceId, flushNow);
204
+ }
205
+ broadcastToolEvent(deviceId, runId, event, flushNow) {
206
+ this.trackDeviceForRun(deviceId, runId);
207
+ this.broadcastToRun(runId, event, flushNow ?? true);
208
+ }
209
+ /** Vitest / e2e: drop connections and in-memory seq maps (does not delete disk queue files). */
210
+ resetForTest() {
211
+ for (const c of this.connections.values())
212
+ c.close();
213
+ this.connections.clear();
214
+ this.runEmitter.clear();
215
+ this.lastRunIdByDevice.clear();
216
+ this.eventSeqByDevice.clear();
217
+ }
218
+ }
219
+ export const sseEmitter = new SseEmitterRegistry();
@@ -0,0 +1,26 @@
1
+ export type PersistedSseEntry = {
2
+ id: number;
3
+ event: string;
4
+ data: Record<string, unknown>;
5
+ };
6
+ export declare function setOfflineQueueBaseDirForTest(dir: string | null): void;
7
+ export declare function resolveFridayNextEventsQueueDir(): string;
8
+ /**
9
+ * Per-device JSONL persistence for SSE replay.
10
+ * `overrideBaseDir` is for tests; production uses `resolveFridayNextEventsQueueDir()`.
11
+ */
12
+ export declare class FridaySseOfflineQueue {
13
+ private readonly overrideBaseDir;
14
+ constructor(overrideBaseDir?: string | null);
15
+ private baseDir;
16
+ private devicePath;
17
+ private ensureDir;
18
+ /** Highest id in file (full scan; ok for bounded backlog). */
19
+ scanMaxId(deviceId: string): number;
20
+ latestId(deviceId: string): number;
21
+ append(deviceId: string, id: number, event: string, data: Record<string, unknown>, backlogLimit: number): void;
22
+ readAfter(deviceId: string, afterId: number): PersistedSseEntry[];
23
+ truncateKeepLastN(deviceId: string, keep: number): void;
24
+ }
25
+ /** Shared queue: base directory follows `setOfflineQueueBaseDirForTest` / config. */
26
+ export declare const fridaySseOfflineQueue: FridaySseOfflineQueue;
@@ -0,0 +1,134 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { resolveFridayNextConfig } from "../config.js";
5
+ import { getHostOpenClawConfigSnapshot } from "../host-config.js";
6
+ import { getFridayNextRuntime } from "../runtime.js";
7
+ /** Test-only override for queue base directory. */
8
+ let testQueueBaseDir = null;
9
+ export function setOfflineQueueBaseDirForTest(dir) {
10
+ testQueueBaseDir = dir;
11
+ }
12
+ export function resolveFridayNextEventsQueueDir() {
13
+ if (testQueueBaseDir)
14
+ return testQueueBaseDir;
15
+ try {
16
+ const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
17
+ return path.join(path.dirname(cfg.historyDir), "events-queue");
18
+ }
19
+ catch {
20
+ return path.join(os.homedir(), ".openclaw", "friday-next", "events-queue");
21
+ }
22
+ }
23
+ /**
24
+ * Per-device JSONL persistence for SSE replay.
25
+ * `overrideBaseDir` is for tests; production uses `resolveFridayNextEventsQueueDir()`.
26
+ */
27
+ export class FridaySseOfflineQueue {
28
+ overrideBaseDir;
29
+ constructor(overrideBaseDir = null) {
30
+ this.overrideBaseDir = overrideBaseDir;
31
+ }
32
+ baseDir() {
33
+ if (this.overrideBaseDir)
34
+ return this.overrideBaseDir;
35
+ return resolveFridayNextEventsQueueDir();
36
+ }
37
+ devicePath(deviceId) {
38
+ const key = deviceId.trim().toUpperCase();
39
+ return path.join(this.baseDir(), `${key}.jsonl`);
40
+ }
41
+ ensureDir() {
42
+ fs.mkdirSync(this.baseDir(), { recursive: true });
43
+ }
44
+ /** Highest id in file (full scan; ok for bounded backlog). */
45
+ scanMaxId(deviceId) {
46
+ const file = this.devicePath(deviceId);
47
+ if (!fs.existsSync(file))
48
+ return 0;
49
+ let max = 0;
50
+ const content = fs.readFileSync(file, "utf8");
51
+ for (const line of content.split("\n")) {
52
+ if (!line.trim())
53
+ continue;
54
+ try {
55
+ const o = JSON.parse(line);
56
+ if (typeof o.id === "number" && o.id > max)
57
+ max = o.id;
58
+ }
59
+ catch {
60
+ /* skip corrupt line */
61
+ }
62
+ }
63
+ return max;
64
+ }
65
+ latestId(deviceId) {
66
+ return this.scanMaxId(deviceId.trim().toUpperCase());
67
+ }
68
+ append(deviceId, id, event, data, backlogLimit) {
69
+ if (event === "connected")
70
+ return;
71
+ this.ensureDir();
72
+ const file = this.devicePath(deviceId);
73
+ const line = JSON.stringify({ id, event, data }) + "\n";
74
+ fs.appendFileSync(file, line, "utf8");
75
+ if (backlogLimit > 0) {
76
+ this.truncateKeepLastN(deviceId, backlogLimit);
77
+ }
78
+ }
79
+ readAfter(deviceId, afterId) {
80
+ const file = this.devicePath(deviceId);
81
+ if (!fs.existsSync(file))
82
+ return [];
83
+ const out = [];
84
+ const content = fs.readFileSync(file, "utf8");
85
+ for (const line of content.split("\n")) {
86
+ if (!line.trim())
87
+ continue;
88
+ try {
89
+ const o = JSON.parse(line);
90
+ if (typeof o.id === "number" &&
91
+ o.id > afterId &&
92
+ typeof o.event === "string" &&
93
+ o.data &&
94
+ typeof o.data === "object" &&
95
+ !Array.isArray(o.data)) {
96
+ out.push(o);
97
+ }
98
+ }
99
+ catch {
100
+ /* skip */
101
+ }
102
+ }
103
+ out.sort((a, b) => a.id - b.id);
104
+ return out;
105
+ }
106
+ truncateKeepLastN(deviceId, keep) {
107
+ if (keep <= 0)
108
+ return;
109
+ const file = this.devicePath(deviceId);
110
+ if (!fs.existsSync(file))
111
+ return;
112
+ const all = [];
113
+ const content = fs.readFileSync(file, "utf8");
114
+ for (const line of content.split("\n")) {
115
+ if (!line.trim())
116
+ continue;
117
+ try {
118
+ const o = JSON.parse(line);
119
+ if (typeof o.id === "number" && typeof o.event === "string" && o.data && typeof o.data === "object") {
120
+ all.push(o);
121
+ }
122
+ }
123
+ catch {
124
+ /* skip */
125
+ }
126
+ }
127
+ if (all.length <= keep)
128
+ return;
129
+ const slice = all.slice(-keep);
130
+ fs.writeFileSync(file, slice.map((e) => JSON.stringify(e) + "\n").join(""), "utf8");
131
+ }
132
+ }
133
+ /** Shared queue: base directory follows `setOfflineQueueBaseDirForTest` / config. */
134
+ export const fridaySseOfflineQueue = new FridaySseOfflineQueue(null);
@@ -0,0 +1,14 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ export interface PairAccountResult {
3
+ accountId: string;
4
+ status: "new" | "existing" | "unknown";
5
+ suggestedAccountId?: string;
6
+ }
7
+ export declare function getOrCreateAccountId(params: {
8
+ deviceId: string;
9
+ claimAccountId?: string;
10
+ claimConfirmed?: boolean;
11
+ historyDir?: string;
12
+ }): PairAccountResult;
13
+ export declare function isKnownAccountId(accountId: string, historyDir?: string): boolean;
14
+ export declare function resolveAccountIdFromRequest(req: IncomingMessage): string | null;
@@ -0,0 +1,101 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { resolveFridayNextConfig } from "../config.js";
5
+ import { getHostOpenClawConfigSnapshot } from "../host-config.js";
6
+ import { getFridayNextRuntime } from "../runtime.js";
7
+ function currentHistoryDir() {
8
+ const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
9
+ return cfg.historyDir;
10
+ }
11
+ function indexPath(historyDir = currentHistoryDir()) {
12
+ return path.join(historyDir, "accounts", "index.json");
13
+ }
14
+ function normalizeDeviceId(deviceId) {
15
+ return deviceId.trim().toUpperCase();
16
+ }
17
+ function readIndex(historyDir = currentHistoryDir()) {
18
+ try {
19
+ const parsed = JSON.parse(fs.readFileSync(indexPath(historyDir), "utf8"));
20
+ return {
21
+ accounts: parsed.accounts && typeof parsed.accounts === "object" ? parsed.accounts : {},
22
+ deviceToAccount: parsed.deviceToAccount && typeof parsed.deviceToAccount === "object" ? parsed.deviceToAccount : {},
23
+ };
24
+ }
25
+ catch {
26
+ return { accounts: {}, deviceToAccount: {} };
27
+ }
28
+ }
29
+ function writeIndex(index, historyDir = currentHistoryDir()) {
30
+ const file = indexPath(historyDir);
31
+ fs.mkdirSync(path.dirname(file), { recursive: true });
32
+ fs.writeFileSync(file, JSON.stringify(index, null, 2), "utf8");
33
+ }
34
+ function touchAccount(index, accountId, deviceId) {
35
+ const now = Date.now();
36
+ const existing = index.accounts[accountId];
37
+ const entry = existing ?? {
38
+ accountId,
39
+ createdAt: now,
40
+ lastSeenAt: now,
41
+ deviceIds: [],
42
+ };
43
+ entry.lastSeenAt = now;
44
+ if (deviceId) {
45
+ const normalized = normalizeDeviceId(deviceId);
46
+ if (normalized && !entry.deviceIds.includes(normalized)) {
47
+ entry.deviceIds.push(normalized);
48
+ }
49
+ if (normalized)
50
+ index.deviceToAccount[normalized] = accountId;
51
+ }
52
+ index.accounts[accountId] = entry;
53
+ return entry;
54
+ }
55
+ export function getOrCreateAccountId(params) {
56
+ const historyDir = params.historyDir ?? currentHistoryDir();
57
+ const index = readIndex(historyDir);
58
+ const deviceId = normalizeDeviceId(params.deviceId);
59
+ const claimed = params.claimAccountId?.trim();
60
+ if (claimed) {
61
+ const existed = Boolean(index.accounts[claimed]);
62
+ if (existed || params.claimConfirmed === true) {
63
+ touchAccount(index, claimed, deviceId);
64
+ writeIndex(index, historyDir);
65
+ return { accountId: claimed, status: existed ? "existing" : "new" };
66
+ }
67
+ const suggested = crypto.randomUUID();
68
+ return { accountId: suggested, status: "unknown", suggestedAccountId: suggested };
69
+ }
70
+ const mapped = deviceId ? index.deviceToAccount[deviceId] : undefined;
71
+ if (mapped && index.accounts[mapped]) {
72
+ touchAccount(index, mapped, deviceId);
73
+ writeIndex(index, historyDir);
74
+ return { accountId: mapped, status: "existing" };
75
+ }
76
+ const existingAccounts = Object.values(index.accounts);
77
+ if (existingAccounts.length === 1) {
78
+ const accountId = existingAccounts[0].accountId;
79
+ touchAccount(index, accountId, deviceId);
80
+ writeIndex(index, historyDir);
81
+ return { accountId, status: "existing" };
82
+ }
83
+ const accountId = crypto.randomUUID();
84
+ touchAccount(index, accountId, deviceId);
85
+ writeIndex(index, historyDir);
86
+ return { accountId, status: "new" };
87
+ }
88
+ export function isKnownAccountId(accountId, historyDir = currentHistoryDir()) {
89
+ const id = accountId.trim();
90
+ if (!id)
91
+ return false;
92
+ return Boolean(readIndex(historyDir).accounts[id]);
93
+ }
94
+ export function resolveAccountIdFromRequest(req) {
95
+ const raw = req.headers["x-account-id"];
96
+ const accountId = Array.isArray(raw) ? raw[0] : raw;
97
+ if (typeof accountId !== "string" || !accountId.trim())
98
+ return null;
99
+ const trimmed = accountId.trim();
100
+ return isKnownAccountId(trimmed) ? trimmed : null;
101
+ }
@@ -0,0 +1,9 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import { type TranscriptEventInput } from "./transcript-store.js";
3
+ export declare function accountIdFromSessionKey(sessionKey: string): string | null;
4
+ export declare function resolveArchiveAccountId(req: IncomingMessage | null, sessionKey: string): string | null;
5
+ export declare function archiveTranscriptEvent(params: {
6
+ accountId: string | null | undefined;
7
+ sessionKey: string | null | undefined;
8
+ event: TranscriptEventInput;
9
+ }): void;
@@ -0,0 +1,25 @@
1
+ import { resolveAccountIdFromRequest } from "./account-identity.js";
2
+ import { getTranscriptStore } from "./transcript-store.js";
3
+ export function accountIdFromSessionKey(sessionKey) {
4
+ const trimmed = sessionKey.trim();
5
+ const rawMatch = trimmed.match(/^friday:account:([^:]+):/i);
6
+ if (rawMatch?.[1])
7
+ return rawMatch[1];
8
+ const storeMatch = trimmed.match(/^agent:main:friday:account:([^:]+):/i);
9
+ return storeMatch?.[1] ?? null;
10
+ }
11
+ export function resolveArchiveAccountId(req, sessionKey) {
12
+ return (req ? resolveAccountIdFromRequest(req) : null) ?? accountIdFromSessionKey(sessionKey);
13
+ }
14
+ export function archiveTranscriptEvent(params) {
15
+ const accountId = params.accountId?.trim();
16
+ const sessionKey = params.sessionKey?.trim();
17
+ if (!accountId || !sessionKey)
18
+ return;
19
+ try {
20
+ getTranscriptStore().appendEvent(accountId, sessionKey, params.event);
21
+ }
22
+ catch (err) {
23
+ console.error(`[Friday-SYNC] archive failed: ${String(err)}`);
24
+ }
25
+ }
@@ -0,0 +1,66 @@
1
+ import Database from "better-sqlite3";
2
+ /** ULID-like server ID: time-sortable (monotonic within same ms), globally unique. */
3
+ export declare function generateServerId(): string;
4
+ export type SyncMessageRecord = {
5
+ server_id: string;
6
+ session_key: string;
7
+ local_id: string | null;
8
+ sender_type: "user" | "assistant";
9
+ sender_name: string;
10
+ content: string;
11
+ timestamp_ms: number;
12
+ seq: number;
13
+ status: string;
14
+ is_from_current_user: number;
15
+ message_json: string;
16
+ run_id: string | null;
17
+ created_at: string;
18
+ };
19
+ export type SyncSessionRecord = {
20
+ session_key: string;
21
+ device_id: string;
22
+ title: string;
23
+ created_at: string;
24
+ last_active_at: string;
25
+ message_count: number;
26
+ latest_msg_id: string | null;
27
+ };
28
+ export type SyncStateRecord = {
29
+ device_id: string;
30
+ last_sync_seq: number;
31
+ updated_at: string;
32
+ };
33
+ export declare function resolveSyncDbPath(): string;
34
+ export declare function initDatabase(dbPath?: string): void;
35
+ /** Test-only: initialize with an in-memory database. */
36
+ export declare function initMemoryDatabase(): void;
37
+ export declare function getDatabase(): Database.Database;
38
+ export declare function closeDatabase(): void;
39
+ export interface InsertMessageParams {
40
+ serverId: string;
41
+ sessionKey: string;
42
+ deviceId?: string;
43
+ localId?: string | null;
44
+ senderType: "user" | "assistant";
45
+ senderName: string;
46
+ content: string;
47
+ timestampMs: number;
48
+ seq: number;
49
+ status: string;
50
+ isFromCurrentUser: boolean;
51
+ messageJson: string;
52
+ runId?: string | null;
53
+ }
54
+ export declare function insertMessage(params: InsertMessageParams): void;
55
+ export declare function getMessagesAfterCursor(sessionKey: string, afterServerId: string, limit: number): string[];
56
+ export declare function getLatestServerId(sessionKey: string): string | null;
57
+ export declare function getMessagesBeforeCursor(sessionKey: string, beforeServerId: string, limit: number): string[];
58
+ export declare function getLastMessageJson(sessionKey: string): string | null;
59
+ export declare function getLastSyncSeq(deviceId: string): number;
60
+ export declare function setLastSyncSeq(deviceId: string, seq: number): void;
61
+ export declare function sessionExists(sessionKey: string): boolean;
62
+ export declare function getSessionMessageCount(sessionKey: string): number;
63
+ export declare function listSessionsByDevice(deviceId: string): SyncSessionRecord[];
64
+ export declare function listAllSessions(): SyncSessionRecord[];
65
+ export declare function findServerIdByRunAndSender(runId: string, senderType: string): string | null;
66
+ export declare function deleteSessionFromDb(sessionKey: string): void;