@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,68 @@
1
+ /** Shape of a ChatMessage as received from / stored for the Friday app. */
2
+ export type StoredChatMessage = Record<string, unknown>;
3
+ export interface StoreUserMessageParams {
4
+ sessionKey: string;
5
+ deviceId: string;
6
+ text: string;
7
+ attachments: string[];
8
+ localId?: string;
9
+ timestampMs?: number;
10
+ seq?: number;
11
+ runId: string;
12
+ }
13
+ export interface StoreAssistantMessageParams {
14
+ sessionKey: string;
15
+ deviceId: string;
16
+ message: StoredChatMessage;
17
+ runId: string;
18
+ }
19
+ export interface SyncResult {
20
+ idMappings: Record<string, string>;
21
+ remoteMessages: StoredChatMessage[];
22
+ latestServerId: string | null;
23
+ hasMore: boolean;
24
+ syncSeq: number;
25
+ }
26
+ /** Store a user message and return its serverId. */
27
+ export declare function storeUserMessage(params: StoreUserMessageParams): string;
28
+ /** Store an assistant message assembled by the accumulator. */
29
+ export declare function storeAssistantMessage(params: StoreAssistantMessageParams): string;
30
+ /** Paginated history query. Returns messages BEFORE the given cursor (for loading older messages). */
31
+ export declare function getMessagesBefore(sessionKey: string, beforeServerId: string | undefined, limit: number): {
32
+ messages: StoredChatMessage[];
33
+ hasMore: boolean;
34
+ };
35
+ /** Get latest serverId for a session (used as next cursor). */
36
+ export declare function getLatestServerIdForSession(sessionKey: string): string | null;
37
+ /** Process a sync request from a device. Idempotent by (deviceId, syncSeq). */
38
+ export declare function processSync(params: {
39
+ sessionKey: string;
40
+ deviceId: string;
41
+ syncSeq: number;
42
+ lastServerId: string | undefined;
43
+ messages: StoredChatMessage[];
44
+ }): SyncResult;
45
+ export interface ImportResult {
46
+ idMappings: Record<string, string>;
47
+ imported: number;
48
+ }
49
+ /** Bulk import for first-time migration. Rejects if session already has data. */
50
+ export declare function importMessages(params: {
51
+ sessionKey: string;
52
+ deviceId: string;
53
+ messages: StoredChatMessage[];
54
+ }): ImportResult;
55
+ export interface SessionListItem {
56
+ sessionKey: string;
57
+ deviceId: string;
58
+ title: string;
59
+ createdAt: string;
60
+ lastActiveAt: string;
61
+ messageCount: number;
62
+ latestServerId: string | null;
63
+ }
64
+ export declare function listSessions(deviceId: string): SessionListItem[];
65
+ export declare function listAllSessionsList(): SessionListItem[];
66
+ export declare class ConflictError extends Error {
67
+ constructor(message: string);
68
+ }
@@ -0,0 +1,262 @@
1
+ import { generateServerId, insertMessage, getMessagesAfterCursor, getMessagesBeforeCursor, getLatestServerId, getLastSyncSeq, setLastSyncSeq, getSessionMessageCount, listSessionsByDevice, listAllSessions, findServerIdByRunAndSender, } from "./database.js";
2
+ function buildUserMessageJson(params) {
3
+ const ts = params.timestampMs ?? Date.now();
4
+ return {
5
+ id: params.localId ?? "",
6
+ content: params.text,
7
+ sender: { user: { name: "You" } },
8
+ timestamp: new Date(ts).toISOString(),
9
+ seq: params.seq ?? 0,
10
+ status: "sent",
11
+ isFromCurrentUser: true,
12
+ toolCalls: null,
13
+ attachment: null,
14
+ extraAttachments: params.attachments.length > 0 ? params.attachments.map((url) => ({
15
+ id: "",
16
+ filename: "",
17
+ url,
18
+ isImage: false,
19
+ localData: null,
20
+ })) : null,
21
+ thoughtEvents: null,
22
+ ttsAttachment: null,
23
+ commandTerminals: null,
24
+ isInternalTrace: false,
25
+ modelName: null,
26
+ totalTokens: null,
27
+ contextWindowMax: null,
28
+ contextTokensUsed: null,
29
+ runId: params.runId,
30
+ };
31
+ }
32
+ /** Store a user message and return its serverId. */
33
+ export function storeUserMessage(params) {
34
+ const serverId = generateServerId();
35
+ const msg = buildUserMessageJson(params);
36
+ msg.serverId = serverId;
37
+ // App-side syncStatus is not stored server-side; the server is always "synced"
38
+ insertMessage({
39
+ serverId,
40
+ sessionKey: params.sessionKey,
41
+ deviceId: params.deviceId,
42
+ localId: params.localId ?? null,
43
+ senderType: "user",
44
+ senderName: "You",
45
+ content: params.text,
46
+ timestampMs: params.timestampMs ?? Date.now(),
47
+ seq: params.seq ?? 0,
48
+ status: "sent",
49
+ isFromCurrentUser: true,
50
+ messageJson: JSON.stringify(msg),
51
+ runId: params.runId,
52
+ });
53
+ return serverId;
54
+ }
55
+ /** Store an assistant message assembled by the accumulator. */
56
+ export function storeAssistantMessage(params) {
57
+ const serverId = generateServerId();
58
+ const msg = { ...params.message };
59
+ msg.serverId = serverId;
60
+ msg.isFromCurrentUser = false;
61
+ const content = typeof msg.content === "string" ? msg.content : "";
62
+ const sender = msg.sender;
63
+ const senderName = sender?.assistant?.name ?? "Claude";
64
+ const ts = typeof msg.timestamp === "string" ? Date.parse(msg.timestamp) : Date.now();
65
+ const seq = typeof msg.seq === "number" ? msg.seq : 0;
66
+ insertMessage({
67
+ serverId,
68
+ sessionKey: params.sessionKey,
69
+ deviceId: params.deviceId,
70
+ localId: null,
71
+ senderType: "assistant",
72
+ senderName,
73
+ content,
74
+ timestampMs: Number.isFinite(ts) ? ts : Date.now(),
75
+ seq,
76
+ status: "sent",
77
+ isFromCurrentUser: false,
78
+ messageJson: JSON.stringify(msg),
79
+ runId: params.runId,
80
+ });
81
+ return serverId;
82
+ }
83
+ /** Paginated history query. Returns messages BEFORE the given cursor (for loading older messages). */
84
+ export function getMessagesBefore(sessionKey, beforeServerId, limit) {
85
+ const effectiveLimit = Math.min(Math.max(limit || 25, 1), 50);
86
+ const before = beforeServerId ?? "";
87
+ const jsons = getMessagesBeforeCursor(sessionKey, before, effectiveLimit + 1);
88
+ const hasMore = jsons.length > effectiveLimit;
89
+ const slice = jsons.slice(0, effectiveLimit);
90
+ return {
91
+ messages: slice.map((j) => JSON.parse(j)),
92
+ hasMore,
93
+ };
94
+ }
95
+ /** Get latest serverId for a session (used as next cursor). */
96
+ export function getLatestServerIdForSession(sessionKey) {
97
+ return getLatestServerId(sessionKey);
98
+ }
99
+ /** Process a sync request from a device. Idempotent by (deviceId, syncSeq). */
100
+ export function processSync(params) {
101
+ const { sessionKey, deviceId, syncSeq, lastServerId, messages } = params;
102
+ const did = deviceId.trim().toUpperCase();
103
+ const idMappings = {};
104
+ // Idempotency check
105
+ const lastSeq = getLastSyncSeq(did);
106
+ if (syncSeq <= lastSeq) {
107
+ // Duplicate sync request — still return remote messages
108
+ const after = lastServerId ?? "";
109
+ const jsons = getMessagesAfterCursor(sessionKey, after, 50);
110
+ const remoteMessages = jsons.map((j) => JSON.parse(j));
111
+ return {
112
+ idMappings: {},
113
+ remoteMessages,
114
+ latestServerId: getLatestServerId(sessionKey),
115
+ hasMore: remoteMessages.length >= 50,
116
+ syncSeq,
117
+ };
118
+ }
119
+ // Store pushed messages (app's offline messages)
120
+ for (const msg of messages) {
121
+ const localId = typeof msg.id === "string" ? msg.id : "";
122
+ const fromCurrentUser = msg.isFromCurrentUser === true;
123
+ const msgRunId = typeof msg.runId === "string" ? msg.runId : null;
124
+ // Dedup: if user message with same run_id already exists (stored by POST /messages handler),
125
+ // reuse its serverId instead of creating a duplicate.
126
+ let serverId;
127
+ let skipInsert = false;
128
+ if (msgRunId && fromCurrentUser) {
129
+ const existing = findServerIdByRunAndSender(msgRunId, "user");
130
+ if (existing) {
131
+ serverId = existing;
132
+ skipInsert = true;
133
+ }
134
+ else {
135
+ serverId = generateServerId();
136
+ }
137
+ }
138
+ else {
139
+ serverId = generateServerId();
140
+ }
141
+ idMappings[localId] = serverId;
142
+ if (!skipInsert) {
143
+ const content = typeof msg.content === "string" ? msg.content : "";
144
+ const senderObj = msg.sender;
145
+ const senderName = fromCurrentUser
146
+ ? senderObj?.user?.name ?? "You"
147
+ : senderObj?.assistant?.name ?? "Claude";
148
+ const ts = typeof msg.timestamp === "string"
149
+ ? Date.parse(msg.timestamp)
150
+ : typeof msg.timestamp === "number"
151
+ ? msg.timestamp
152
+ : Date.now();
153
+ const seq = typeof msg.seq === "number" ? msg.seq : 0;
154
+ const status = typeof msg.status === "string" ? msg.status : "sent";
155
+ const enriched = { ...msg };
156
+ enriched.serverId = serverId;
157
+ insertMessage({
158
+ serverId,
159
+ sessionKey,
160
+ deviceId: did,
161
+ localId: localId || null,
162
+ senderType: fromCurrentUser ? "user" : "assistant",
163
+ senderName,
164
+ content,
165
+ timestampMs: Number.isFinite(ts) ? ts : Date.now(),
166
+ seq,
167
+ status,
168
+ isFromCurrentUser: fromCurrentUser,
169
+ messageJson: JSON.stringify(enriched),
170
+ runId: msgRunId,
171
+ });
172
+ }
173
+ }
174
+ // Update sync state
175
+ setLastSyncSeq(did, syncSeq);
176
+ // Pull remote messages
177
+ const after = lastServerId ?? "";
178
+ const jsons = getMessagesAfterCursor(sessionKey, after, 50);
179
+ const remoteMessages = jsons.map((j) => JSON.parse(j));
180
+ return {
181
+ idMappings,
182
+ remoteMessages,
183
+ latestServerId: getLatestServerId(sessionKey),
184
+ hasMore: remoteMessages.length >= 50,
185
+ syncSeq,
186
+ };
187
+ }
188
+ /** Bulk import for first-time migration. Rejects if session already has data. */
189
+ export function importMessages(params) {
190
+ const { sessionKey, deviceId, messages } = params;
191
+ if (sessionExistsData(sessionKey)) {
192
+ throw new ConflictError("Session already has data — use POST /sync instead");
193
+ }
194
+ const idMappings = {};
195
+ let imported = 0;
196
+ for (const msg of messages) {
197
+ const localId = typeof msg.id === "string" ? msg.id : "";
198
+ const serverId = generateServerId();
199
+ idMappings[localId] = serverId;
200
+ const content = typeof msg.content === "string" ? msg.content : "";
201
+ const senderObj = msg.sender;
202
+ const isFromCurrentUser = msg.isFromCurrentUser === true;
203
+ const senderName = isFromCurrentUser
204
+ ? senderObj?.user?.name ?? "You"
205
+ : senderObj?.assistant?.name ?? "Claude";
206
+ const ts = typeof msg.timestamp === "string"
207
+ ? Date.parse(msg.timestamp)
208
+ : typeof msg.timestamp === "number"
209
+ ? msg.timestamp
210
+ : Date.now();
211
+ const seq = typeof msg.seq === "number" ? msg.seq : 0;
212
+ const status = typeof msg.status === "string" ? msg.status : "sent";
213
+ const enriched = { ...msg };
214
+ enriched.serverId = serverId;
215
+ insertMessage({
216
+ serverId,
217
+ sessionKey,
218
+ deviceId: deviceId.trim().toUpperCase(),
219
+ localId: localId || null,
220
+ senderType: isFromCurrentUser ? "user" : "assistant",
221
+ senderName,
222
+ content,
223
+ timestampMs: Number.isFinite(ts) ? ts : Date.now(),
224
+ seq,
225
+ status,
226
+ isFromCurrentUser,
227
+ messageJson: JSON.stringify(enriched),
228
+ runId: typeof msg.runId === "string" ? msg.runId : null,
229
+ });
230
+ imported += 1;
231
+ }
232
+ return { idMappings, imported };
233
+ }
234
+ /** Check if session has any messages in the DB (used by import guard). */
235
+ function sessionExistsData(sessionKey) {
236
+ return getSessionMessageCount(sessionKey) > 0;
237
+ }
238
+ export function listSessions(deviceId) {
239
+ const rows = listSessionsByDevice(deviceId);
240
+ return rows.map(toSessionItem);
241
+ }
242
+ export function listAllSessionsList() {
243
+ const rows = listAllSessions();
244
+ return rows.map(toSessionItem);
245
+ }
246
+ function toSessionItem(r) {
247
+ return {
248
+ sessionKey: r.session_key,
249
+ deviceId: r.device_id,
250
+ title: r.title,
251
+ createdAt: r.created_at,
252
+ lastActiveAt: r.last_active_at,
253
+ messageCount: r.message_count,
254
+ latestServerId: r.latest_msg_id,
255
+ };
256
+ }
257
+ export class ConflictError extends Error {
258
+ constructor(message) {
259
+ super(message);
260
+ this.name = "ConflictError";
261
+ }
262
+ }
@@ -0,0 +1,5 @@
1
+ import type { StoredChatMessage } from "./translate.js";
2
+ export declare function setPushStoreBaseDir(dir: string | null): void;
3
+ export declare function appendPushMessage(sessionKey: string, msg: StoredChatMessage): string;
4
+ export declare function readPushMessages(sessionKey: string): StoredChatMessage[];
5
+ export declare function deletePushMessages(sessionKey: string): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Minimal JSONL store for offline push messages (app-sent when server was unreachable).
3
+ * These are merged into sync pull responses alongside OpenClaw transcript messages.
4
+ */
5
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join, dirname } from "node:path";
8
+ import { generateStandaloneServerId } from "./translate.js";
9
+ let baseDir = null;
10
+ export function setPushStoreBaseDir(dir) {
11
+ baseDir = dir;
12
+ }
13
+ function resolveBaseDir() {
14
+ if (baseDir)
15
+ return baseDir;
16
+ return join(homedir(), ".openclaw", "friday-next", "sync-push");
17
+ }
18
+ function pushFilePath(sessionKey) {
19
+ const safe = sessionKey.replace(/[^a-zA-Z0-9._-]/g, "_");
20
+ return join(resolveBaseDir(), `${safe}.jsonl`);
21
+ }
22
+ export function appendPushMessage(sessionKey, msg) {
23
+ const serverId = generateStandaloneServerId();
24
+ const enriched = { ...msg, serverId };
25
+ const file = pushFilePath(sessionKey);
26
+ mkdirSync(dirname(file), { recursive: true });
27
+ appendFileSync(file, JSON.stringify(enriched) + "\n", "utf-8");
28
+ return serverId;
29
+ }
30
+ export function readPushMessages(sessionKey) {
31
+ const file = pushFilePath(sessionKey);
32
+ if (!existsSync(file))
33
+ return [];
34
+ const msgs = [];
35
+ const raw = readFileSync(file, "utf-8");
36
+ for (const line of raw.split("\n")) {
37
+ if (!line.trim())
38
+ continue;
39
+ try {
40
+ msgs.push(JSON.parse(line));
41
+ }
42
+ catch {
43
+ // skip corrupt
44
+ }
45
+ }
46
+ return msgs;
47
+ }
48
+ export function deletePushMessages(sessionKey) {
49
+ const file = pushFilePath(sessionKey);
50
+ try {
51
+ unlinkSync(file);
52
+ }
53
+ catch { /* gone */ }
54
+ }
@@ -0,0 +1,12 @@
1
+ /** Parse Friday app session keys for transcript storage. */
2
+ export type ParsedFridaySession = {
3
+ /** Raw app session key (verbatim from client). */
4
+ sessionKey: string;
5
+ /** Account id when present in key, else undefined (caller supplies from header). */
6
+ accountIdFromKey?: string;
7
+ /** Stable file id for JSONL (session uuid or legacy tail). */
8
+ sessionFileId: string;
9
+ };
10
+ export declare function sessionFileIdFromKey(sessionKey: string): string;
11
+ export declare function parseFridaySessionKey(sessionKey: string): ParsedFridaySession;
12
+ export declare function sessionBelongsToAccount(sessionKey: string, accountId: string): boolean;
@@ -0,0 +1,47 @@
1
+ /** Parse Friday app session keys for transcript storage. */
2
+ const ACCOUNT_SESSION_RE = /^friday:account:([^:]+):([^:]+)$/i;
3
+ const DIRECT_SESSION_RE = /^friday:direct:[^:]+:([^:]+)$/i;
4
+ export function sessionFileIdFromKey(sessionKey) {
5
+ const raw = sessionKey.trim();
6
+ const mAccount = raw.match(ACCOUNT_SESSION_RE);
7
+ if (mAccount?.[2])
8
+ return sanitizeSessionFileId(mAccount[2]);
9
+ const mDirect = raw.match(DIRECT_SESSION_RE);
10
+ if (mDirect?.[1])
11
+ return sanitizeSessionFileId(mDirect[1]);
12
+ return sanitizeSessionFileId(raw);
13
+ }
14
+ export function parseFridaySessionKey(sessionKey) {
15
+ const raw = sessionKey.trim();
16
+ const mAccount = raw.match(ACCOUNT_SESSION_RE);
17
+ if (mAccount?.[1] && mAccount[2]) {
18
+ return {
19
+ sessionKey: raw,
20
+ accountIdFromKey: mAccount[1],
21
+ sessionFileId: sanitizeSessionFileId(mAccount[2]),
22
+ };
23
+ }
24
+ const mDirect = raw.match(DIRECT_SESSION_RE);
25
+ if (mDirect?.[1]) {
26
+ return {
27
+ sessionKey: raw,
28
+ sessionFileId: sanitizeSessionFileId(mDirect[1]),
29
+ };
30
+ }
31
+ return {
32
+ sessionKey: raw,
33
+ sessionFileId: sanitizeSessionFileId(raw),
34
+ };
35
+ }
36
+ function sanitizeSessionFileId(id) {
37
+ const s = id.trim();
38
+ if (!s)
39
+ return "session";
40
+ return s.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 128);
41
+ }
42
+ export function sessionBelongsToAccount(sessionKey, accountId) {
43
+ const parsed = parseFridaySessionKey(sessionKey);
44
+ if (!parsed.accountIdFromKey)
45
+ return true;
46
+ return parsed.accountIdFromKey.toLowerCase() === accountId.trim().toLowerCase();
47
+ }
@@ -0,0 +1,5 @@
1
+ export declare function setSyncStatePath(path: string): void;
2
+ export declare function getLastSyncSeq(deviceId: string): number;
3
+ export declare function setLastSyncSeq(deviceId: string, seq: number): void;
4
+ /** Vitest-only: reset in-memory cache and path. */
5
+ export declare function resetSyncStateForTest(): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Per-device sync sequence tracking.
3
+ * Stored in ~/.openclaw/friday-next/sync-state.json
4
+ */
5
+ import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname } from "node:path";
8
+ let statePath = null;
9
+ let stateCache = null;
10
+ export function setSyncStatePath(path) {
11
+ statePath = path;
12
+ stateCache = null;
13
+ }
14
+ function resolvePath() {
15
+ if (statePath)
16
+ return statePath;
17
+ statePath = `${homedir()}/.openclaw/friday-next/sync-state.json`;
18
+ return statePath;
19
+ }
20
+ function loadState() {
21
+ if (stateCache)
22
+ return stateCache;
23
+ const path = resolvePath();
24
+ try {
25
+ if (existsSync(path)) {
26
+ stateCache = JSON.parse(readFileSync(path, "utf-8"));
27
+ return stateCache;
28
+ }
29
+ }
30
+ catch {
31
+ // corrupted — reset
32
+ }
33
+ stateCache = {};
34
+ return stateCache;
35
+ }
36
+ function saveState() {
37
+ const path = resolvePath();
38
+ mkdirSync(dirname(path), { recursive: true });
39
+ writeFileSync(path, JSON.stringify(stateCache ?? {}, null, 2), "utf-8");
40
+ }
41
+ export function getLastSyncSeq(deviceId) {
42
+ const state = loadState();
43
+ return state[deviceId.trim().toUpperCase()] ?? 0;
44
+ }
45
+ export function setLastSyncSeq(deviceId, seq) {
46
+ const state = loadState();
47
+ state[deviceId.trim().toUpperCase()] = seq;
48
+ saveState();
49
+ }
50
+ /** Vitest-only: reset in-memory cache and path. */
51
+ export function resetSyncStateForTest() {
52
+ statePath = null;
53
+ stateCache = null;
54
+ }
@@ -0,0 +1,13 @@
1
+ import { type TranscriptEventKind } from "./transcript-store.js";
2
+ export declare function resolveAccountIdForArchive(deviceId: string, headerAccountId?: string | null): string | null;
3
+ export declare function archiveTranscriptEvent(params: {
4
+ deviceId: string;
5
+ sessionKey: string;
6
+ accountId?: string | null;
7
+ kind: TranscriptEventKind;
8
+ clientId?: string;
9
+ payload: Record<string, unknown>;
10
+ messageId?: string;
11
+ lamport?: number;
12
+ titleHint?: string;
13
+ }): void;
@@ -0,0 +1,37 @@
1
+ import { resolveFridayNextConfig } from "../config.js";
2
+ import { getHostOpenClawConfigSnapshot } from "../host-config.js";
3
+ import { getFridayNextRuntime } from "../runtime.js";
4
+ import { pairAccount, resolveAccountIdForDevice } from "./account-identity.js";
5
+ import { appendEvent } from "./transcript-store.js";
6
+ export function resolveAccountIdForArchive(deviceId, headerAccountId) {
7
+ const cfg = resolveFridayNextConfig(getHostOpenClawConfigSnapshot(getFridayNextRuntime().config));
8
+ const header = headerAccountId?.trim();
9
+ if (header)
10
+ return header;
11
+ const mapped = resolveAccountIdForDevice(cfg.historyDir, deviceId);
12
+ if (mapped)
13
+ return mapped;
14
+ const paired = pairAccount({ historyDir: cfg.historyDir, deviceId });
15
+ return paired.accountId;
16
+ }
17
+ export function archiveTranscriptEvent(params) {
18
+ try {
19
+ const accountId = params.accountId?.trim() ||
20
+ resolveAccountIdForArchive(params.deviceId.trim().toUpperCase(), null);
21
+ if (!accountId)
22
+ return;
23
+ appendEvent({
24
+ accountId,
25
+ sessionKey: params.sessionKey,
26
+ kind: params.kind,
27
+ clientId: params.clientId ?? "server",
28
+ payload: params.payload,
29
+ messageId: params.messageId,
30
+ lamport: params.lamport,
31
+ titleHint: params.titleHint,
32
+ });
33
+ }
34
+ catch {
35
+ // best-effort archive
36
+ }
37
+ }
@@ -0,0 +1,35 @@
1
+ export type TranscriptEventKind = "user_message" | "assistant_message" | "thought_event" | "tool_event" | "outbound_text" | "outbound_media" | "sync_upsert" | "summary";
2
+ export interface TranscriptEventInput {
3
+ eventId?: string;
4
+ clientId: string;
5
+ kind: TranscriptEventKind;
6
+ payload: Record<string, unknown>;
7
+ ts?: number;
8
+ lamportTs?: number;
9
+ }
10
+ export interface StoredTranscriptEvent extends TranscriptEventInput {
11
+ v: 1;
12
+ seq: number;
13
+ ts: number;
14
+ sessionKey: string;
15
+ }
16
+ export interface TranscriptSessionSummary {
17
+ sessionKey: string;
18
+ sessionId: string;
19
+ title?: string;
20
+ createdAt: number;
21
+ lastActiveAt: number;
22
+ lastSeq: number;
23
+ messageCount: number;
24
+ }
25
+ export declare function sessionIdFromSessionKey(sessionKey: string, accountId?: string): string;
26
+ export declare class TranscriptStore {
27
+ private readonly historyDir;
28
+ constructor(historyDir?: string);
29
+ appendEvent(accountId: string, sessionKey: string, input: TranscriptEventInput): StoredTranscriptEvent;
30
+ appendEvents(accountId: string, sessionKey: string, inputs: TranscriptEventInput[]): StoredTranscriptEvent[];
31
+ readSince(accountId: string, sessionKey: string, sinceSeq?: number, limit?: number): StoredTranscriptEvent[];
32
+ listSessions(accountId: string): TranscriptSessionSummary[];
33
+ compact(accountId: string, sessionKey: string, keepLast: number): void;
34
+ }
35
+ export declare function getTranscriptStore(): TranscriptStore;