@keyou007/agent-bot 0.1.0

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 (212) hide show
  1. package/.env.example +8 -0
  2. package/CHANGELOG.md +11 -0
  3. package/LICENSE +675 -0
  4. package/README.md +187 -0
  5. package/README.zh.md +187 -0
  6. package/SECURITY.md +13 -0
  7. package/config.example.yaml +56 -0
  8. package/dist/acp/AcpJsonRpcConnection.js +155 -0
  9. package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
  10. package/dist/acp/AcpProcessManager.js +65 -0
  11. package/dist/acp/AcpProcessManager.js.map +1 -0
  12. package/dist/acp/AcpSessionManager.js +164 -0
  13. package/dist/acp/AcpSessionManager.js.map +1 -0
  14. package/dist/acp/acpTypes.js +10 -0
  15. package/dist/acp/acpTypes.js.map +1 -0
  16. package/dist/cli/FeishuAppConfiguration.js +349 -0
  17. package/dist/cli/FeishuAppConfiguration.js.map +1 -0
  18. package/dist/cli/FeishuAppRegistration.js +160 -0
  19. package/dist/cli/FeishuAppRegistration.js.map +1 -0
  20. package/dist/cli/Initializer.js +112 -0
  21. package/dist/cli/Initializer.js.map +1 -0
  22. package/dist/cli/LocalControlClient.js +47 -0
  23. package/dist/cli/LocalControlClient.js.map +1 -0
  24. package/dist/cli/LocalControlServer.js +66 -0
  25. package/dist/cli/LocalControlServer.js.map +1 -0
  26. package/dist/cli/SkillRegistry.js +126 -0
  27. package/dist/cli/SkillRegistry.js.map +1 -0
  28. package/dist/cli/controlProtocol.js +10 -0
  29. package/dist/cli/controlProtocol.js.map +1 -0
  30. package/dist/cli/taskChatRoute.js +23 -0
  31. package/dist/cli/taskChatRoute.js.map +1 -0
  32. package/dist/cli.js +616 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/codex/AppServerConnection.js +131 -0
  35. package/dist/codex/AppServerConnection.js.map +1 -0
  36. package/dist/codex/CodexEventMapper.js +296 -0
  37. package/dist/codex/CodexEventMapper.js.map +1 -0
  38. package/dist/codex/CodexLocalActivityDetector.js +124 -0
  39. package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
  40. package/dist/codex/CodexProcessManager.js +64 -0
  41. package/dist/codex/CodexProcessManager.js.map +1 -0
  42. package/dist/codex/CodexRuntime.js +825 -0
  43. package/dist/codex/CodexRuntime.js.map +1 -0
  44. package/dist/codex/ProjectlessWorkspace.js +74 -0
  45. package/dist/codex/ProjectlessWorkspace.js.map +1 -0
  46. package/dist/codex/protocol.js +10 -0
  47. package/dist/codex/protocol.js.map +1 -0
  48. package/dist/commands/CommandRouter.js +176 -0
  49. package/dist/commands/CommandRouter.js.map +1 -0
  50. package/dist/commands/commandTypes.js +2 -0
  51. package/dist/commands/commandTypes.js.map +1 -0
  52. package/dist/config/loadConfig.js +107 -0
  53. package/dist/config/loadConfig.js.map +1 -0
  54. package/dist/config/paths.js +32 -0
  55. package/dist/config/paths.js.map +1 -0
  56. package/dist/config/schema.js +38 -0
  57. package/dist/config/schema.js.map +1 -0
  58. package/dist/console/ConsoleConnector.js +39 -0
  59. package/dist/console/ConsoleConnector.js.map +1 -0
  60. package/dist/console/ConsoleTurnPresenter.js +60 -0
  61. package/dist/console/ConsoleTurnPresenter.js.map +1 -0
  62. package/dist/feishu/CardRenderer.js +972 -0
  63. package/dist/feishu/CardRenderer.js.map +1 -0
  64. package/dist/feishu/CardUpdateScheduler.js +148 -0
  65. package/dist/feishu/CardUpdateScheduler.js.map +1 -0
  66. package/dist/feishu/ConsoleFeishuClient.js +18 -0
  67. package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
  68. package/dist/feishu/FeishuConnector.js +257 -0
  69. package/dist/feishu/FeishuConnector.js.map +1 -0
  70. package/dist/feishu/FeishuMarkdown.js +37 -0
  71. package/dist/feishu/FeishuMarkdown.js.map +1 -0
  72. package/dist/feishu/FeishuMessageClient.js +459 -0
  73. package/dist/feishu/FeishuMessageClient.js.map +1 -0
  74. package/dist/feishu/FeishuTurnPresenter.js +425 -0
  75. package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
  76. package/dist/feishu/GroupAvatarGenerator.js +86 -0
  77. package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
  78. package/dist/feishu/LocalCardImage.js +13 -0
  79. package/dist/feishu/LocalCardImage.js.map +1 -0
  80. package/dist/feishu/LocalImageMarkdown.js +89 -0
  81. package/dist/feishu/LocalImageMarkdown.js.map +1 -0
  82. package/dist/feishu/contextKey.js +12 -0
  83. package/dist/feishu/contextKey.js.map +1 -0
  84. package/dist/feishu/transport.js +14 -0
  85. package/dist/feishu/transport.js.map +1 -0
  86. package/dist/feishu/types.js +2 -0
  87. package/dist/feishu/types.js.map +1 -0
  88. package/dist/index.js +213 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logging/logger.js +22 -0
  91. package/dist/logging/logger.js.map +1 -0
  92. package/dist/presentation/OutboundRouter.js +127 -0
  93. package/dist/presentation/OutboundRouter.js.map +1 -0
  94. package/dist/presentation/TurnStateReducer.js +307 -0
  95. package/dist/presentation/TurnStateReducer.js.map +1 -0
  96. package/dist/presentation/splitMarkdown.js +341 -0
  97. package/dist/presentation/splitMarkdown.js.map +1 -0
  98. package/dist/presentation/turnViewTypes.js +2 -0
  99. package/dist/presentation/turnViewTypes.js.map +1 -0
  100. package/dist/proxy/ProxySessionController.js +2819 -0
  101. package/dist/proxy/ProxySessionController.js.map +1 -0
  102. package/dist/runtime/AcpRuntimeAdapter.js +271 -0
  103. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
  104. package/dist/runtime/AgentRuntimeRegistry.js +18 -0
  105. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
  106. package/dist/runtime/agentEnvironment.js +11 -0
  107. package/dist/runtime/agentEnvironment.js.map +1 -0
  108. package/dist/runtime/types.js +2 -0
  109. package/dist/runtime/types.js.map +1 -0
  110. package/dist/startup/SessionMetadataHydrator.js +46 -0
  111. package/dist/startup/SessionMetadataHydrator.js.map +1 -0
  112. package/dist/startup/StartupNotifier.js +78 -0
  113. package/dist/startup/StartupNotifier.js.map +1 -0
  114. package/dist/startup/startFeishu.js +6 -0
  115. package/dist/startup/startFeishu.js.map +1 -0
  116. package/dist/state/StateStore.js +893 -0
  117. package/dist/state/StateStore.js.map +1 -0
  118. package/dist/state/migrations.js +129 -0
  119. package/dist/state/migrations.js.map +1 -0
  120. package/dist/supervision/SafeRestartNotifier.js +116 -0
  121. package/dist/supervision/SafeRestartNotifier.js.map +1 -0
  122. package/dist/supervision/SafeRestartScheduler.js +123 -0
  123. package/dist/supervision/SafeRestartScheduler.js.map +1 -0
  124. package/dist/supervision/replacementSupervisor.js +8 -0
  125. package/dist/supervision/replacementSupervisor.js.map +1 -0
  126. package/dist/supervision/restartPolicy.js +16 -0
  127. package/dist/supervision/restartPolicy.js.map +1 -0
  128. package/dist/supervisor.js +94 -0
  129. package/dist/supervisor.js.map +1 -0
  130. package/dist/utils/executeShellCommand.js +76 -0
  131. package/dist/utils/executeShellCommand.js.map +1 -0
  132. package/dist/utils/id.js +5 -0
  133. package/dist/utils/id.js.map +1 -0
  134. package/dist/utils/markdown.js +29 -0
  135. package/dist/utils/markdown.js.map +1 -0
  136. package/dist/utils/spawnCommand.js +21 -0
  137. package/dist/utils/spawnCommand.js.map +1 -0
  138. package/dist/utils/taskTitle.js +10 -0
  139. package/dist/utils/taskTitle.js.map +1 -0
  140. package/docs/technical-reference.md +294 -0
  141. package/docs/technical-reference.zh.md +294 -0
  142. package/npm-shrinkwrap.json +4525 -0
  143. package/package.json +87 -0
  144. package/skills/agent-bot/SKILL.md +141 -0
  145. package/skills/agent-bot/agents/openai.yaml +4 -0
  146. package/src/acp/AcpJsonRpcConnection.ts +212 -0
  147. package/src/acp/AcpProcessManager.ts +87 -0
  148. package/src/acp/AcpSessionManager.ts +227 -0
  149. package/src/acp/acpTypes.ts +99 -0
  150. package/src/cli/FeishuAppConfiguration.ts +473 -0
  151. package/src/cli/FeishuAppRegistration.ts +207 -0
  152. package/src/cli/Initializer.ts +159 -0
  153. package/src/cli/LocalControlClient.ts +48 -0
  154. package/src/cli/LocalControlServer.ts +67 -0
  155. package/src/cli/SkillRegistry.ts +151 -0
  156. package/src/cli/controlProtocol.ts +34 -0
  157. package/src/cli/taskChatRoute.ts +31 -0
  158. package/src/cli.ts +630 -0
  159. package/src/codex/AppServerConnection.ts +159 -0
  160. package/src/codex/CodexEventMapper.ts +351 -0
  161. package/src/codex/CodexLocalActivityDetector.ts +148 -0
  162. package/src/codex/CodexProcessManager.ts +63 -0
  163. package/src/codex/CodexRuntime.ts +1025 -0
  164. package/src/codex/ProjectlessWorkspace.ts +90 -0
  165. package/src/codex/protocol.ts +37 -0
  166. package/src/commands/CommandRouter.ts +179 -0
  167. package/src/commands/commandTypes.ts +28 -0
  168. package/src/config/loadConfig.ts +115 -0
  169. package/src/config/paths.ts +37 -0
  170. package/src/config/schema.ts +46 -0
  171. package/src/console/ConsoleConnector.ts +40 -0
  172. package/src/console/ConsoleTurnPresenter.ts +64 -0
  173. package/src/feishu/CardRenderer.ts +1157 -0
  174. package/src/feishu/CardUpdateScheduler.ts +165 -0
  175. package/src/feishu/ConsoleFeishuClient.ts +22 -0
  176. package/src/feishu/FeishuConnector.ts +277 -0
  177. package/src/feishu/FeishuMarkdown.ts +47 -0
  178. package/src/feishu/FeishuMessageClient.ts +649 -0
  179. package/src/feishu/FeishuTurnPresenter.ts +539 -0
  180. package/src/feishu/GroupAvatarGenerator.ts +102 -0
  181. package/src/feishu/LocalCardImage.ts +13 -0
  182. package/src/feishu/LocalImageMarkdown.ts +92 -0
  183. package/src/feishu/contextKey.ts +14 -0
  184. package/src/feishu/transport.ts +24 -0
  185. package/src/feishu/types.ts +83 -0
  186. package/src/index.ts +228 -0
  187. package/src/logging/logger.ts +28 -0
  188. package/src/presentation/OutboundRouter.ts +205 -0
  189. package/src/presentation/TurnStateReducer.ts +341 -0
  190. package/src/presentation/splitMarkdown.ts +358 -0
  191. package/src/presentation/turnViewTypes.ts +54 -0
  192. package/src/proxy/ProxySessionController.ts +3311 -0
  193. package/src/runtime/AcpRuntimeAdapter.ts +324 -0
  194. package/src/runtime/AgentRuntimeRegistry.ts +20 -0
  195. package/src/runtime/agentEnvironment.ts +14 -0
  196. package/src/runtime/types.ts +215 -0
  197. package/src/startup/SessionMetadataHydrator.ts +46 -0
  198. package/src/startup/StartupNotifier.ts +92 -0
  199. package/src/startup/startFeishu.ts +19 -0
  200. package/src/state/StateStore.ts +1249 -0
  201. package/src/state/migrations.ts +128 -0
  202. package/src/supervision/SafeRestartNotifier.ts +122 -0
  203. package/src/supervision/SafeRestartScheduler.ts +149 -0
  204. package/src/supervision/replacementSupervisor.ts +10 -0
  205. package/src/supervision/restartPolicy.ts +19 -0
  206. package/src/supervisor.ts +106 -0
  207. package/src/utils/executeShellCommand.ts +97 -0
  208. package/src/utils/id.ts +5 -0
  209. package/src/utils/markdown.ts +25 -0
  210. package/src/utils/spawnCommand.ts +35 -0
  211. package/src/utils/taskTitle.ts +9 -0
  212. package/tsconfig.json +19 -0
@@ -0,0 +1,893 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { migrations } from "./migrations.js";
5
+ export class StateStore {
6
+ db;
7
+ constructor(sqlitePath) {
8
+ fs.mkdirSync(path.dirname(sqlitePath), { recursive: true });
9
+ this.db = new Database(sqlitePath);
10
+ this.db.pragma("journal_mode = WAL");
11
+ for (const migration of migrations) {
12
+ this.db.exec(migration);
13
+ }
14
+ this.db.exec("UPDATE message_reactions SET status = 'pending' WHERE status = 'updating'");
15
+ this.ensureUserContextColumns();
16
+ this.ensureSessionColumns();
17
+ this.ensureTurnSnapshotColumns();
18
+ this.ensureChatContextColumns();
19
+ this.initializeContextSessionMappings();
20
+ this.reconcileDuplicateRemoteSessions();
21
+ this.db.exec(`
22
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_remote_session_id_unique
23
+ ON sessions(remote_session_id)
24
+ WHERE remote_session_id IS NOT NULL
25
+ `);
26
+ this.db.exec(`
27
+ CREATE INDEX IF NOT EXISTS idx_chat_contexts_activity
28
+ ON chat_contexts(last_activity_at)
29
+ `);
30
+ }
31
+ close() {
32
+ this.db.close();
33
+ }
34
+ getOrCreateUserContext(contextKey, defaultAgent) {
35
+ const existing = this.db
36
+ .prepare("SELECT * FROM user_contexts WHERE context_key = ?")
37
+ .get(contextKey);
38
+ if (existing) {
39
+ return mapUserContext(existing);
40
+ }
41
+ const now = new Date().toISOString();
42
+ this.db
43
+ .prepare(`
44
+ INSERT INTO user_contexts (context_key, default_agent, current_session_id, created_at, updated_at)
45
+ VALUES (?, ?, NULL, ?, ?)
46
+ `)
47
+ .run(contextKey, defaultAgent, now, now);
48
+ return {
49
+ contextKey,
50
+ defaultAgent,
51
+ createdAt: now,
52
+ updatedAt: now,
53
+ };
54
+ }
55
+ getUserContext(contextKey) {
56
+ const row = this.db
57
+ .prepare("SELECT * FROM user_contexts WHERE context_key = ?")
58
+ .get(contextKey);
59
+ return row ? mapUserContext(row) : undefined;
60
+ }
61
+ listUserContexts() {
62
+ const rows = this.db
63
+ .prepare("SELECT * FROM user_contexts ORDER BY created_at ASC")
64
+ .all();
65
+ return rows.map(mapUserContext);
66
+ }
67
+ recordChatContext(contextKey, chatType) {
68
+ const now = new Date().toISOString();
69
+ this.db.prepare(`
70
+ INSERT INTO chat_contexts (context_key, chat_type, created_at, updated_at)
71
+ VALUES (?, ?, ?, ?)
72
+ ON CONFLICT(context_key) DO UPDATE SET
73
+ chat_type = excluded.chat_type,
74
+ updated_at = excluded.updated_at
75
+ `).run(contextKey, chatType, now, now);
76
+ }
77
+ listChatContexts(chatType) {
78
+ const rows = chatType
79
+ ? this.db.prepare(`
80
+ SELECT * FROM chat_contexts
81
+ WHERE chat_type = ?
82
+ ORDER BY updated_at DESC
83
+ `).all(chatType)
84
+ : this.db.prepare("SELECT * FROM chat_contexts ORDER BY updated_at DESC").all();
85
+ return rows.map((row) => ({
86
+ contextKey: row.context_key,
87
+ chatType: row.chat_type,
88
+ lastActivityAt: row.last_activity_at ?? undefined,
89
+ createdAt: row.created_at,
90
+ updatedAt: row.updated_at,
91
+ }));
92
+ }
93
+ markChatActive(contextKey, activeAt = new Date()) {
94
+ const timestamp = activeAt.toISOString();
95
+ this.db.prepare(`
96
+ UPDATE chat_contexts
97
+ SET last_activity_at = ?, updated_at = ?
98
+ WHERE context_key = ?
99
+ `).run(timestamp, timestamp, contextKey);
100
+ }
101
+ listRecentlyActiveChatContexts(since) {
102
+ const rows = this.db.prepare(`
103
+ SELECT * FROM chat_contexts
104
+ WHERE last_activity_at >= ?
105
+ ORDER BY last_activity_at DESC
106
+ `).all(since.toISOString());
107
+ return rows.map((row) => ({
108
+ contextKey: row.context_key,
109
+ chatType: row.chat_type,
110
+ lastActivityAt: row.last_activity_at ?? undefined,
111
+ createdAt: row.created_at,
112
+ updatedAt: row.updated_at,
113
+ }));
114
+ }
115
+ enqueuePrompt(input) {
116
+ const createdAt = new Date().toISOString();
117
+ this.db.prepare(`
118
+ INSERT INTO queued_prompts (
119
+ prompt_id, local_session_id, context_key, prompt_text, local_image_paths_json,
120
+ message_id, reply_message_id, created_at
121
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
122
+ `).run(input.promptId, input.localSessionId, input.contextKey, input.text, JSON.stringify(input.localImagePaths ?? []), input.messageId ?? null, input.replyMessageId ?? null, createdAt);
123
+ return {
124
+ ...input,
125
+ localImagePaths: input.localImagePaths?.length ? [...input.localImagePaths] : undefined,
126
+ createdAt,
127
+ };
128
+ }
129
+ listQueuedPrompts(localSessionId) {
130
+ const rows = this.db.prepare(`
131
+ SELECT * FROM queued_prompts
132
+ WHERE local_session_id = ?
133
+ ORDER BY created_at ASC, rowid ASC
134
+ `).all(localSessionId);
135
+ return rows.map(mapQueuedPrompt);
136
+ }
137
+ listQueuedPromptSessionIds() {
138
+ const rows = this.db.prepare(`
139
+ SELECT local_session_id, min(created_at) AS first_created_at, min(rowid) AS first_rowid
140
+ FROM queued_prompts
141
+ GROUP BY local_session_id
142
+ ORDER BY first_created_at ASC, first_rowid ASC
143
+ `).all();
144
+ return rows.map((row) => row.local_session_id);
145
+ }
146
+ countQueuedPrompts(localSessionId) {
147
+ const row = this.db.prepare(`
148
+ SELECT count(*) AS count FROM queued_prompts WHERE local_session_id = ?
149
+ `).get(localSessionId);
150
+ return row.count;
151
+ }
152
+ takeNextQueuedPrompt(localSessionId) {
153
+ const take = this.db.transaction(() => {
154
+ const row = this.db.prepare(`
155
+ SELECT * FROM queued_prompts
156
+ WHERE local_session_id = ?
157
+ ORDER BY created_at ASC, rowid ASC
158
+ LIMIT 1
159
+ `).get(localSessionId);
160
+ if (!row)
161
+ return undefined;
162
+ this.db.prepare("DELETE FROM queued_prompts WHERE prompt_id = ?").run(row.prompt_id);
163
+ return mapQueuedPrompt(row);
164
+ });
165
+ return take();
166
+ }
167
+ cancelQueuedPrompt(promptId, localSessionId) {
168
+ const cancel = this.db.transaction(() => {
169
+ const row = this.db.prepare(`
170
+ SELECT * FROM queued_prompts WHERE prompt_id = ? AND local_session_id = ?
171
+ `).get(promptId, localSessionId);
172
+ if (!row)
173
+ return undefined;
174
+ this.db.prepare("DELETE FROM queued_prompts WHERE prompt_id = ?").run(promptId);
175
+ return mapQueuedPrompt(row);
176
+ });
177
+ return cancel();
178
+ }
179
+ nextForkTitle(sourceTitle) {
180
+ const baseTitle = forkBaseTitle(sourceTitle);
181
+ const allocate = this.db.transaction(() => {
182
+ const existing = this.db
183
+ .prepare("SELECT last_sequence FROM fork_title_sequences WHERE base_title = ?")
184
+ .get(baseTitle);
185
+ const nextSequence = (existing?.last_sequence ?? 0) + 1;
186
+ this.db.prepare(`
187
+ INSERT INTO fork_title_sequences (base_title, last_sequence)
188
+ VALUES (?, ?)
189
+ ON CONFLICT(base_title) DO UPDATE SET last_sequence = excluded.last_sequence
190
+ `).run(baseTitle, nextSequence);
191
+ return nextSequence;
192
+ });
193
+ return formatForkTitle(baseTitle, allocate());
194
+ }
195
+ reconcileInterruptedAcpSessions(acpAgentNames) {
196
+ const agentClause = acpAgentNames.length
197
+ ? ` OR (runtime_kind IS NULL AND agent_name IN (${acpAgentNames.map(() => "?").join(", ")}))`
198
+ : "";
199
+ const rows = this.db
200
+ .prepare(`SELECT * FROM sessions WHERE status = 'running' AND (runtime_kind = 'acp'${agentClause})`)
201
+ .all(...acpAgentNames);
202
+ if (rows.length === 0)
203
+ return [];
204
+ const now = new Date().toISOString();
205
+ const completedAt = Date.now();
206
+ const reconcile = this.db.transaction(() => {
207
+ for (const row of rows) {
208
+ this.db.prepare(`
209
+ UPDATE sessions
210
+ SET status = 'failed',
211
+ last_turn_status = CASE WHEN last_turn_id IS NULL THEN last_turn_status ELSE 'failed' END,
212
+ updated_at = ?
213
+ WHERE local_session_id = ?
214
+ `).run(now, row.local_session_id);
215
+ if (!row.last_turn_id)
216
+ continue;
217
+ const snapshotRow = this.db
218
+ .prepare("SELECT snapshot_json FROM turn_snapshots WHERE turn_id = ?")
219
+ .get(row.last_turn_id);
220
+ if (!snapshotRow)
221
+ continue;
222
+ try {
223
+ const snapshot = JSON.parse(snapshotRow.snapshot_json);
224
+ if (!["starting", "running", "tool_running", "waiting_for_approval"].includes(String(snapshot.status)))
225
+ continue;
226
+ const startedAt = typeof snapshot.startedAt === "number" ? snapshot.startedAt : undefined;
227
+ const failedSnapshot = {
228
+ ...snapshot,
229
+ status: "failed",
230
+ completedAt,
231
+ ...(startedAt === undefined ? {} : { durationMs: Math.max(0, completedAt - startedAt) }),
232
+ error: "Agent Bot 已重启,原 ACP 进程中的执行无法继续。",
233
+ };
234
+ delete failedSnapshot.activeTool;
235
+ delete failedSnapshot.approval;
236
+ this.db.prepare(`
237
+ UPDATE turn_snapshots SET snapshot_json = ?, updated_at = ? WHERE turn_id = ?
238
+ `).run(JSON.stringify(failedSnapshot), now, row.last_turn_id);
239
+ }
240
+ catch {
241
+ // Keep an unreadable historical snapshot intact while still correcting the session state.
242
+ }
243
+ }
244
+ });
245
+ reconcile();
246
+ return rows.map(mapSession);
247
+ }
248
+ setDefaultAgent(contextKey, agentName) {
249
+ const now = new Date().toISOString();
250
+ this.db
251
+ .prepare("UPDATE user_contexts SET default_agent = ?, updated_at = ? WHERE context_key = ?")
252
+ .run(agentName, now, contextKey);
253
+ }
254
+ setBoundProjectCwd(contextKey, cwd) {
255
+ const now = new Date().toISOString();
256
+ this.db
257
+ .prepare("UPDATE user_contexts SET bound_project_cwd = ?, updated_at = ? WHERE context_key = ?")
258
+ .run(cwd ? path.resolve(cwd) : null, now, contextKey);
259
+ }
260
+ setCurrentSession(contextKey, localSessionId) {
261
+ const now = new Date().toISOString();
262
+ if (localSessionId && this.getSession(localSessionId)) {
263
+ this.attachSessionToContext(contextKey, localSessionId);
264
+ }
265
+ this.db
266
+ .prepare(`
267
+ UPDATE user_contexts
268
+ SET previous_session_id = CASE
269
+ WHEN current_session_id IS NOT ? THEN current_session_id
270
+ ELSE previous_session_id
271
+ END,
272
+ current_session_id = ?,
273
+ updated_at = ?
274
+ WHERE context_key = ?
275
+ `)
276
+ .run(localSessionId ?? null, localSessionId ?? null, now, contextKey);
277
+ }
278
+ createSession(input) {
279
+ const now = new Date().toISOString();
280
+ const create = this.db.transaction(() => {
281
+ this.db.prepare(`
282
+ INSERT INTO sessions (
283
+ local_session_id,
284
+ context_key,
285
+ agent_name,
286
+ cwd,
287
+ acp_session_id,
288
+ status,
289
+ created_at,
290
+ updated_at
291
+ ) VALUES (?, ?, ?, ?, NULL, ?, ?, ?)
292
+ `).run(input.localSessionId, input.contextKey, input.agentName, input.cwd, input.status, now, now);
293
+ this.db.prepare(`
294
+ INSERT INTO context_sessions (context_key, local_session_id, created_at, updated_at)
295
+ VALUES (?, ?, ?, ?)
296
+ `).run(input.contextKey, input.localSessionId, now, now);
297
+ });
298
+ create();
299
+ return {
300
+ ...input,
301
+ createdAt: now,
302
+ updatedAt: now,
303
+ };
304
+ }
305
+ updateSession(localSessionId, patch) {
306
+ const existing = this.getSession(localSessionId);
307
+ if (!existing) {
308
+ return;
309
+ }
310
+ const now = new Date().toISOString();
311
+ this.db
312
+ .prepare(`
313
+ UPDATE sessions
314
+ SET acp_session_id = ?, status = ?, updated_at = ?
315
+ WHERE local_session_id = ?
316
+ `)
317
+ .run(patch.acpSessionId ?? existing.acpSessionId ?? null, patch.status ?? existing.status, now, localSessionId);
318
+ }
319
+ updateRuntimeSession(localSessionId, patch) {
320
+ const existing = this.getSession(localSessionId);
321
+ if (!existing) {
322
+ return;
323
+ }
324
+ this.db
325
+ .prepare(`
326
+ UPDATE sessions
327
+ SET runtime_kind = ?, remote_session_id = ?, title = ?, model = ?, reasoning_effort = ?, permission_mode = ?,
328
+ last_turn_id = ?, last_turn_status = ?, updated_at = ?
329
+ WHERE local_session_id = ?
330
+ `)
331
+ .run(patch.runtimeKind ?? existing.runtimeKind ?? null, patch.remoteSessionId ?? existing.remoteSessionId ?? existing.acpSessionId ?? null, patch.title ?? existing.title ?? null, patch.model ?? existing.model ?? null, patch.reasoningEffort ?? existing.reasoningEffort ?? null, patch.permissionMode ?? existing.permissionMode ?? null, patch.lastTurnId ?? existing.lastTurnId ?? null, patch.lastTurnStatus ?? existing.lastTurnStatus ?? null, new Date().toISOString(), localSessionId);
332
+ }
333
+ saveTurnSnapshot(turnId, localSessionId, snapshot, contextKey) {
334
+ const now = new Date().toISOString();
335
+ this.db
336
+ .prepare(`
337
+ INSERT INTO turn_snapshots (turn_id, local_session_id, context_key, snapshot_json, updated_at)
338
+ VALUES (?, ?, ?, ?, ?)
339
+ ON CONFLICT(turn_id) DO UPDATE SET
340
+ local_session_id = excluded.local_session_id,
341
+ context_key = coalesce(excluded.context_key, turn_snapshots.context_key),
342
+ snapshot_json = excluded.snapshot_json,
343
+ updated_at = excluded.updated_at
344
+ `)
345
+ .run(turnId, localSessionId, contextKey ?? null, JSON.stringify(snapshot), now);
346
+ }
347
+ promotePendingTurn(pendingTurnId, turnId, localSessionId, snapshot, contextKey) {
348
+ if (pendingTurnId === turnId) {
349
+ this.saveTurnSnapshot(turnId, localSessionId, snapshot, contextKey);
350
+ return;
351
+ }
352
+ this.db.transaction(() => {
353
+ const pendingDelivery = this.getTurnDelivery(pendingTurnId);
354
+ this.saveTurnSnapshot(turnId, localSessionId, snapshot, contextKey);
355
+ if (pendingDelivery) {
356
+ this.saveTurnDelivery(turnId, {
357
+ progressMessageId: pendingDelivery.progressMessageId,
358
+ lastCardHash: pendingDelivery.lastCardHash,
359
+ });
360
+ }
361
+ this.db.prepare("DELETE FROM turn_deliveries WHERE turn_id = ?").run(pendingTurnId);
362
+ this.db.prepare("DELETE FROM turn_snapshots WHERE turn_id = ?").run(pendingTurnId);
363
+ })();
364
+ }
365
+ getTurnSnapshot(turnId) {
366
+ const row = this.db
367
+ .prepare("SELECT snapshot_json FROM turn_snapshots WHERE turn_id = ?")
368
+ .get(turnId);
369
+ return row ? JSON.parse(row.snapshot_json) : undefined;
370
+ }
371
+ findLatestCompletedTurnId(localSessionId, contextKey) {
372
+ const row = this.db
373
+ .prepare(`
374
+ SELECT turn_id
375
+ FROM turn_snapshots
376
+ WHERE local_session_id = ?
377
+ AND context_key = ?
378
+ AND json_extract(snapshot_json, '$.status') = 'completed'
379
+ ORDER BY
380
+ coalesce(
381
+ json_extract(snapshot_json, '$.completedAt'),
382
+ json_extract(snapshot_json, '$.startedAt'),
383
+ 0
384
+ ) DESC,
385
+ updated_at DESC
386
+ LIMIT 1
387
+ `)
388
+ .get(localSessionId, contextKey);
389
+ return row?.turn_id;
390
+ }
391
+ saveTurnDelivery(turnId, patch) {
392
+ const existing = this.getTurnDelivery(turnId);
393
+ const now = new Date().toISOString();
394
+ this.db
395
+ .prepare(`
396
+ INSERT INTO turn_deliveries (
397
+ turn_id, progress_message_id, final_message_ids_json, final_delivered_at, last_card_hash, updated_at
398
+ ) VALUES (?, ?, ?, ?, ?, ?)
399
+ ON CONFLICT(turn_id) DO UPDATE SET
400
+ progress_message_id = excluded.progress_message_id,
401
+ last_card_hash = excluded.last_card_hash,
402
+ updated_at = excluded.updated_at
403
+ `)
404
+ .run(turnId, patch.progressMessageId ?? existing?.progressMessageId ?? null, JSON.stringify(existing?.finalMessageIds ?? []), existing?.finalDeliveredAt ?? null, patch.lastCardHash ?? existing?.lastCardHash ?? null, now);
405
+ }
406
+ markFinalDelivered(turnId, messageIds) {
407
+ const now = new Date().toISOString();
408
+ const existing = this.getTurnDelivery(turnId);
409
+ this.db
410
+ .prepare(`
411
+ INSERT INTO turn_deliveries (
412
+ turn_id, progress_message_id, final_message_ids_json, final_delivered_at, last_card_hash, updated_at
413
+ ) VALUES (?, ?, ?, ?, ?, ?)
414
+ ON CONFLICT(turn_id) DO UPDATE SET
415
+ final_message_ids_json = excluded.final_message_ids_json,
416
+ final_delivered_at = excluded.final_delivered_at,
417
+ updated_at = excluded.updated_at
418
+ `)
419
+ .run(turnId, existing?.progressMessageId ?? null, JSON.stringify(messageIds), now, existing?.lastCardHash ?? null, now);
420
+ }
421
+ saveFinalDeliveryProgress(turnId, messageIds) {
422
+ const now = new Date().toISOString();
423
+ const existing = this.getTurnDelivery(turnId);
424
+ this.db
425
+ .prepare(`
426
+ INSERT INTO turn_deliveries (
427
+ turn_id, progress_message_id, final_message_ids_json, final_delivered_at, last_card_hash, updated_at
428
+ ) VALUES (?, ?, ?, ?, ?, ?)
429
+ ON CONFLICT(turn_id) DO UPDATE SET
430
+ final_message_ids_json = excluded.final_message_ids_json,
431
+ updated_at = excluded.updated_at
432
+ `)
433
+ .run(turnId, existing?.progressMessageId ?? null, JSON.stringify(messageIds), existing?.finalDeliveredAt ?? null, existing?.lastCardHash ?? null, now);
434
+ }
435
+ getTurnDelivery(turnId) {
436
+ const row = this.db
437
+ .prepare("SELECT * FROM turn_deliveries WHERE turn_id = ?")
438
+ .get(turnId);
439
+ if (!row) {
440
+ return undefined;
441
+ }
442
+ return {
443
+ progressMessageId: row.progress_message_id ?? undefined,
444
+ finalMessageIds: JSON.parse(row.final_message_ids_json),
445
+ finalDelivered: Boolean(row.final_delivered_at),
446
+ finalDeliveredAt: row.final_delivered_at ?? undefined,
447
+ lastCardHash: row.last_card_hash ?? undefined,
448
+ };
449
+ }
450
+ getSession(localSessionId) {
451
+ const row = this.db
452
+ .prepare("SELECT * FROM sessions WHERE local_session_id = ?")
453
+ .get(localSessionId);
454
+ return row ? mapSession(row) : undefined;
455
+ }
456
+ getSessionForContext(localSessionId, contextKey) {
457
+ const linked = this.db.prepare(`
458
+ SELECT 1
459
+ FROM context_sessions
460
+ WHERE context_key = ? AND local_session_id = ?
461
+ `).get(contextKey, localSessionId);
462
+ if (!linked)
463
+ return undefined;
464
+ const session = this.getSession(localSessionId);
465
+ return session ? { ...session, contextKey } : undefined;
466
+ }
467
+ attachSessionToContext(contextKey, localSessionId) {
468
+ if (!this.getSession(localSessionId))
469
+ throw new Error(`找不到任务:${localSessionId}`);
470
+ const now = new Date().toISOString();
471
+ this.db.prepare(`
472
+ INSERT INTO context_sessions (context_key, local_session_id, created_at, updated_at)
473
+ VALUES (?, ?, ?, ?)
474
+ ON CONFLICT(context_key, local_session_id) DO UPDATE SET updated_at = excluded.updated_at
475
+ `).run(contextKey, localSessionId, now, now);
476
+ }
477
+ listSessions(contextKey) {
478
+ const rows = this.db
479
+ .prepare(`
480
+ SELECT sessions.*
481
+ FROM context_sessions
482
+ JOIN sessions ON sessions.local_session_id = context_sessions.local_session_id
483
+ WHERE context_sessions.context_key = ?
484
+ ORDER BY context_sessions.created_at DESC
485
+ `)
486
+ .all(contextKey);
487
+ return rows.map((row) => ({ ...mapSession(row), contextKey }));
488
+ }
489
+ listAllSessions() {
490
+ const rows = this.db
491
+ .prepare("SELECT * FROM sessions ORDER BY updated_at DESC, created_at DESC")
492
+ .all();
493
+ return rows.map(mapSession);
494
+ }
495
+ getServerActivityState() {
496
+ const running = this.db
497
+ .prepare(`
498
+ SELECT COUNT(*) AS count
499
+ FROM sessions
500
+ WHERE status = 'running'
501
+ OR EXISTS (
502
+ SELECT 1 FROM queued_prompts WHERE queued_prompts.local_session_id = sessions.local_session_id
503
+ )
504
+ `)
505
+ .get();
506
+ const pending = this.db.prepare(`
507
+ SELECT COUNT(*) AS count
508
+ FROM sessions x
509
+ JOIN turn_snapshots s ON s.turn_id = x.last_turn_id
510
+ LEFT JOIN turn_deliveries d ON d.turn_id = s.turn_id
511
+ WHERE json_extract(s.snapshot_json, '$.status') = 'completed'
512
+ AND length(coalesce(json_extract(s.snapshot_json, '$.finalResponse'), '')) > 0
513
+ AND d.final_delivered_at IS NULL
514
+ `).get();
515
+ const inbound = this.db
516
+ .prepare("SELECT max(created_at) AS latest FROM inbound_event_receipts")
517
+ .get();
518
+ return {
519
+ runningSessions: running.count,
520
+ pendingFinalDeliveries: pending.count,
521
+ latestInboundAt: inbound.latest ?? undefined,
522
+ };
523
+ }
524
+ findSessionByRemoteSessionId(remoteSessionId, contextKey) {
525
+ const row = contextKey
526
+ ? this.db
527
+ .prepare(`
528
+ SELECT sessions.*
529
+ FROM sessions
530
+ JOIN context_sessions ON context_sessions.local_session_id = sessions.local_session_id
531
+ WHERE sessions.remote_session_id = ?
532
+ AND context_sessions.context_key = ?
533
+ AND sessions.status != 'closed'
534
+ LIMIT 1
535
+ `)
536
+ .get(remoteSessionId, contextKey)
537
+ : this.db
538
+ .prepare(`
539
+ SELECT * FROM sessions
540
+ WHERE remote_session_id = ? AND status != 'closed'
541
+ ORDER BY created_at ASC LIMIT 1
542
+ `)
543
+ .get(remoteSessionId);
544
+ return row ? { ...mapSession(row), ...(contextKey ? { contextKey } : {}) } : undefined;
545
+ }
546
+ getTurnContextKey(turnId) {
547
+ const row = this.db.prepare("SELECT context_key FROM turn_snapshots WHERE turn_id = ?")
548
+ .get(turnId);
549
+ return row?.context_key ?? undefined;
550
+ }
551
+ audit(contextKey, eventType, payload) {
552
+ this.db
553
+ .prepare(`
554
+ INSERT INTO audit_events (context_key, event_type, payload_json, created_at)
555
+ VALUES (?, ?, ?, ?)
556
+ `)
557
+ .run(contextKey, eventType, JSON.stringify(payload), new Date().toISOString());
558
+ }
559
+ claimInboundEvent(eventId, eventKind) {
560
+ const result = this.db
561
+ .prepare(`
562
+ INSERT OR IGNORE INTO inbound_event_receipts (event_id, event_kind, created_at)
563
+ VALUES (?, ?, ?)
564
+ `)
565
+ .run(eventId, eventKind, new Date().toISOString());
566
+ return result.changes === 1;
567
+ }
568
+ saveMessageReaction(messageId, contextKey, reactionId, emojiType) {
569
+ const now = new Date().toISOString();
570
+ this.db.prepare(`
571
+ INSERT INTO message_reactions (
572
+ message_id, context_key, reaction_id, emoji_type, status, created_at, updated_at
573
+ ) VALUES (?, ?, ?, ?, 'pending', ?, ?)
574
+ ON CONFLICT(message_id) DO UPDATE SET
575
+ context_key = excluded.context_key,
576
+ reaction_id = excluded.reaction_id,
577
+ emoji_type = excluded.emoji_type,
578
+ status = 'pending',
579
+ updated_at = excluded.updated_at
580
+ `).run(messageId, contextKey, reactionId, emojiType, now, now);
581
+ }
582
+ bindMessageReaction(messageId, localSessionId, turnId) {
583
+ this.db.prepare(`
584
+ UPDATE message_reactions
585
+ SET local_session_id = ?, turn_id = ?, updated_at = ?
586
+ WHERE message_id = ? AND status = 'pending'
587
+ `).run(localSessionId, turnId, new Date().toISOString(), messageId);
588
+ }
589
+ bindMessageToTurn(messageId, localSessionId, turnId) {
590
+ const now = new Date().toISOString();
591
+ this.db.prepare(`
592
+ INSERT INTO message_turn_bindings (message_id, local_session_id, turn_id, created_at, updated_at)
593
+ VALUES (?, ?, ?, ?, ?)
594
+ ON CONFLICT(message_id) DO UPDATE SET
595
+ local_session_id = excluded.local_session_id,
596
+ turn_id = excluded.turn_id,
597
+ updated_at = excluded.updated_at
598
+ `).run(messageId, localSessionId, turnId, now, now);
599
+ }
600
+ claimMessageReactionsForTurn(turnId) {
601
+ const claim = this.db.transaction(() => {
602
+ const rows = this.db.prepare(`
603
+ SELECT * FROM message_reactions WHERE turn_id = ? AND status = 'pending' ORDER BY created_at ASC
604
+ `).all(turnId);
605
+ if (rows.length > 0) {
606
+ this.db.prepare(`
607
+ UPDATE message_reactions SET status = 'updating', updated_at = ?
608
+ WHERE turn_id = ? AND status = 'pending'
609
+ `).run(new Date().toISOString(), turnId);
610
+ }
611
+ return rows.map((row) => ({ ...mapMessageReaction(row), status: "updating" }));
612
+ });
613
+ return claim();
614
+ }
615
+ claimMessageReaction(messageId) {
616
+ const claim = this.db.transaction(() => {
617
+ const row = this.db.prepare(`
618
+ SELECT * FROM message_reactions WHERE message_id = ? AND status = 'pending'
619
+ `).get(messageId);
620
+ if (!row)
621
+ return undefined;
622
+ this.db.prepare(`
623
+ UPDATE message_reactions SET status = 'updating', updated_at = ? WHERE message_id = ? AND status = 'pending'
624
+ `).run(new Date().toISOString(), messageId);
625
+ return { ...mapMessageReaction(row), status: "updating" };
626
+ });
627
+ return claim();
628
+ }
629
+ listPendingMessageReactions() {
630
+ const rows = this.db.prepare(`
631
+ SELECT * FROM message_reactions WHERE status = 'pending' AND turn_id IS NOT NULL ORDER BY created_at ASC
632
+ `).all();
633
+ return rows.map(mapMessageReaction);
634
+ }
635
+ finishMessageReaction(messageId, reactionId, emojiType, status) {
636
+ this.db.prepare(`
637
+ UPDATE message_reactions
638
+ SET reaction_id = ?, emoji_type = ?, status = ?, updated_at = ?
639
+ WHERE message_id = ?
640
+ `).run(reactionId, emojiType, status, new Date().toISOString(), messageId);
641
+ }
642
+ releaseMessageReaction(messageId) {
643
+ this.db.prepare(`
644
+ UPDATE message_reactions SET status = 'pending', updated_at = ?
645
+ WHERE message_id = ? AND status = 'updating'
646
+ `).run(new Date().toISOString(), messageId);
647
+ }
648
+ getMessageReaction(messageId) {
649
+ const row = this.db.prepare("SELECT * FROM message_reactions WHERE message_id = ?")
650
+ .get(messageId);
651
+ return row ? mapMessageReaction(row) : undefined;
652
+ }
653
+ findTurnAnchorByMessageId(messageId) {
654
+ const binding = this.db.prepare(`
655
+ SELECT turn_id, local_session_id
656
+ FROM message_turn_bindings
657
+ WHERE message_id = ?
658
+ LIMIT 1
659
+ `).get(messageId);
660
+ if (binding) {
661
+ return {
662
+ turnId: binding.turn_id,
663
+ localSessionId: binding.local_session_id,
664
+ contextKey: this.getTurnContextKey(binding.turn_id),
665
+ };
666
+ }
667
+ const reaction = this.db.prepare(`
668
+ SELECT turn_id, local_session_id
669
+ FROM message_reactions
670
+ WHERE message_id = ? AND turn_id IS NOT NULL AND local_session_id IS NOT NULL
671
+ LIMIT 1
672
+ `).get(messageId);
673
+ if (reaction) {
674
+ const context = this.db.prepare("SELECT context_key FROM message_reactions WHERE message_id = ?")
675
+ .get(messageId);
676
+ return {
677
+ turnId: reaction.turn_id,
678
+ localSessionId: reaction.local_session_id,
679
+ contextKey: context?.context_key ?? this.getTurnContextKey(reaction.turn_id),
680
+ };
681
+ }
682
+ const delivery = this.db.prepare(`
683
+ SELECT deliveries.turn_id, snapshots.local_session_id
684
+ FROM turn_deliveries AS deliveries
685
+ JOIN turn_snapshots AS snapshots ON snapshots.turn_id = deliveries.turn_id
686
+ WHERE deliveries.progress_message_id = ?
687
+ OR EXISTS (
688
+ SELECT 1
689
+ FROM json_each(deliveries.final_message_ids_json)
690
+ WHERE json_each.value = ?
691
+ )
692
+ ORDER BY deliveries.updated_at DESC
693
+ LIMIT 1
694
+ `).get(messageId, messageId);
695
+ return delivery
696
+ ? {
697
+ turnId: delivery.turn_id,
698
+ localSessionId: delivery.local_session_id,
699
+ contextKey: this.getTurnContextKey(delivery.turn_id),
700
+ }
701
+ : undefined;
702
+ }
703
+ initializeContextSessionMappings() {
704
+ this.db.exec(`
705
+ INSERT OR IGNORE INTO context_sessions (context_key, local_session_id, created_at, updated_at)
706
+ SELECT context_key, local_session_id, created_at, updated_at FROM sessions;
707
+
708
+ UPDATE turn_snapshots
709
+ SET context_key = (
710
+ SELECT sessions.context_key
711
+ FROM sessions
712
+ WHERE sessions.local_session_id = turn_snapshots.local_session_id
713
+ )
714
+ WHERE context_key IS NULL;
715
+ `);
716
+ }
717
+ reconcileDuplicateRemoteSessions() {
718
+ const duplicateIds = this.db.prepare(`
719
+ SELECT remote_session_id
720
+ FROM sessions
721
+ WHERE remote_session_id IS NOT NULL
722
+ GROUP BY remote_session_id
723
+ HAVING count(*) > 1
724
+ `).all();
725
+ if (duplicateIds.length === 0)
726
+ return;
727
+ const reconcile = this.db.transaction(() => {
728
+ for (const { remote_session_id: remoteSessionId } of duplicateIds) {
729
+ const rows = this.db.prepare(`
730
+ SELECT * FROM sessions
731
+ WHERE remote_session_id = ?
732
+ ORDER BY created_at ASC
733
+ `).all(remoteSessionId);
734
+ const canonical = rows[0];
735
+ if (!canonical)
736
+ continue;
737
+ const latest = [...rows].sort((left, right) => right.updated_at.localeCompare(left.updated_at))[0] ?? canonical;
738
+ for (const duplicate of rows.slice(1)) {
739
+ this.db.prepare(`
740
+ INSERT OR IGNORE INTO context_sessions (context_key, local_session_id, created_at, updated_at)
741
+ SELECT context_key, ?, created_at, updated_at
742
+ FROM context_sessions
743
+ WHERE local_session_id = ?
744
+ `).run(canonical.local_session_id, duplicate.local_session_id);
745
+ this.db.prepare(`
746
+ UPDATE user_contexts
747
+ SET current_session_id = CASE WHEN current_session_id = ? THEN ? ELSE current_session_id END,
748
+ previous_session_id = CASE WHEN previous_session_id = ? THEN ? ELSE previous_session_id END
749
+ `).run(duplicate.local_session_id, canonical.local_session_id, duplicate.local_session_id, canonical.local_session_id);
750
+ this.db.prepare("UPDATE turn_snapshots SET local_session_id = ? WHERE local_session_id = ?")
751
+ .run(canonical.local_session_id, duplicate.local_session_id);
752
+ this.db.prepare("UPDATE message_reactions SET local_session_id = ? WHERE local_session_id = ?")
753
+ .run(canonical.local_session_id, duplicate.local_session_id);
754
+ this.db.prepare("UPDATE message_turn_bindings SET local_session_id = ? WHERE local_session_id = ?")
755
+ .run(canonical.local_session_id, duplicate.local_session_id);
756
+ this.db.prepare("UPDATE queued_prompts SET local_session_id = ? WHERE local_session_id = ?")
757
+ .run(canonical.local_session_id, duplicate.local_session_id);
758
+ this.db.prepare("DELETE FROM context_sessions WHERE local_session_id = ?")
759
+ .run(duplicate.local_session_id);
760
+ this.db.prepare("DELETE FROM sessions WHERE local_session_id = ?")
761
+ .run(duplicate.local_session_id);
762
+ }
763
+ this.db.prepare(`
764
+ UPDATE sessions
765
+ SET agent_name = ?, cwd = ?, acp_session_id = ?, runtime_kind = ?, title = ?, model = ?,
766
+ reasoning_effort = ?, permission_mode = ?, last_turn_id = ?, last_turn_status = ?,
767
+ status = ?, updated_at = ?
768
+ WHERE local_session_id = ?
769
+ `).run(latest.agent_name, latest.cwd, latest.acp_session_id, latest.runtime_kind, latest.title, latest.model, latest.reasoning_effort, latest.permission_mode, latest.last_turn_id, latest.last_turn_status, latest.status, latest.updated_at, canonical.local_session_id);
770
+ this.db.prepare(`
771
+ UPDATE turn_snapshots
772
+ SET snapshot_json = json_set(snapshot_json, '$.sessionId', ?)
773
+ WHERE local_session_id = ? AND json_valid(snapshot_json)
774
+ `).run(canonical.local_session_id, canonical.local_session_id);
775
+ }
776
+ });
777
+ reconcile();
778
+ }
779
+ ensureSessionColumns() {
780
+ const existing = new Set(this.db.pragma("table_info(sessions)").map((column) => column.name));
781
+ const columns = [
782
+ ["runtime_kind", "TEXT"],
783
+ ["remote_session_id", "TEXT"],
784
+ ["title", "TEXT"],
785
+ ["model", "TEXT"],
786
+ ["reasoning_effort", "TEXT"],
787
+ ["permission_mode", "TEXT"],
788
+ ["last_turn_id", "TEXT"],
789
+ ["last_turn_status", "TEXT"],
790
+ ];
791
+ for (const [name, type] of columns) {
792
+ if (!existing.has(name)) {
793
+ this.db.exec(`ALTER TABLE sessions ADD COLUMN ${name} ${type}`);
794
+ }
795
+ }
796
+ }
797
+ ensureUserContextColumns() {
798
+ const existing = new Set(this.db.pragma("table_info(user_contexts)").map((column) => column.name));
799
+ if (!existing.has("previous_session_id")) {
800
+ this.db.exec("ALTER TABLE user_contexts ADD COLUMN previous_session_id TEXT");
801
+ }
802
+ if (!existing.has("bound_project_cwd")) {
803
+ this.db.exec("ALTER TABLE user_contexts ADD COLUMN bound_project_cwd TEXT");
804
+ }
805
+ }
806
+ ensureTurnSnapshotColumns() {
807
+ const existing = new Set(this.db.pragma("table_info(turn_snapshots)").map((column) => column.name));
808
+ if (!existing.has("context_key")) {
809
+ this.db.exec("ALTER TABLE turn_snapshots ADD COLUMN context_key TEXT");
810
+ }
811
+ }
812
+ ensureChatContextColumns() {
813
+ const existing = new Set(this.db.pragma("table_info(chat_contexts)").map((column) => column.name));
814
+ if (!existing.has("last_activity_at")) {
815
+ this.db.exec("ALTER TABLE chat_contexts ADD COLUMN last_activity_at TEXT");
816
+ }
817
+ }
818
+ }
819
+ const MAX_TASK_TITLE_LENGTH = 120;
820
+ const FORK_TITLE_SUFFIX = /\s*(分支\s+(\d+))$/u;
821
+ function forkBaseTitle(sourceTitle) {
822
+ let title = sourceTitle?.replace(/\s+/g, " ").trim() || "未命名任务";
823
+ while (FORK_TITLE_SUFFIX.test(title))
824
+ title = title.replace(FORK_TITLE_SUFFIX, "").trim();
825
+ return title || "未命名任务";
826
+ }
827
+ function formatForkTitle(baseTitle, sequence) {
828
+ const suffix = `(分支 ${sequence})`;
829
+ const available = Math.max(1, MAX_TASK_TITLE_LENGTH - suffix.length);
830
+ const truncatedBase = baseTitle.length <= available ? baseTitle : baseTitle.slice(0, available).trimEnd();
831
+ return `${truncatedBase}${suffix}`;
832
+ }
833
+ function mapUserContext(row) {
834
+ return {
835
+ contextKey: row.context_key,
836
+ defaultAgent: row.default_agent,
837
+ currentSessionId: row.current_session_id ?? undefined,
838
+ previousSessionId: row.previous_session_id ?? undefined,
839
+ boundProjectCwd: row.bound_project_cwd ?? undefined,
840
+ createdAt: row.created_at,
841
+ updatedAt: row.updated_at,
842
+ };
843
+ }
844
+ function mapSession(row) {
845
+ return {
846
+ localSessionId: row.local_session_id,
847
+ contextKey: row.context_key,
848
+ agentName: row.agent_name,
849
+ cwd: row.cwd,
850
+ acpSessionId: row.acp_session_id ?? undefined,
851
+ runtimeKind: row.runtime_kind ?? (row.acp_session_id ? "acp" : undefined),
852
+ remoteSessionId: row.remote_session_id ?? row.acp_session_id ?? undefined,
853
+ title: row.title ?? undefined,
854
+ model: row.model ?? undefined,
855
+ reasoningEffort: row.reasoning_effort ?? undefined,
856
+ permissionMode: row.permission_mode ?? undefined,
857
+ lastTurnId: row.last_turn_id ?? undefined,
858
+ lastTurnStatus: row.last_turn_status ?? undefined,
859
+ status: row.status,
860
+ createdAt: row.created_at,
861
+ updatedAt: row.updated_at,
862
+ };
863
+ }
864
+ function mapMessageReaction(row) {
865
+ return {
866
+ messageId: row.message_id,
867
+ contextKey: row.context_key,
868
+ reactionId: row.reaction_id,
869
+ emojiType: row.emoji_type,
870
+ localSessionId: row.local_session_id ?? undefined,
871
+ turnId: row.turn_id ?? undefined,
872
+ status: row.status,
873
+ createdAt: row.created_at,
874
+ updatedAt: row.updated_at,
875
+ };
876
+ }
877
+ function mapQueuedPrompt(row) {
878
+ const parsedPaths = JSON.parse(row.local_image_paths_json);
879
+ const localImagePaths = Array.isArray(parsedPaths)
880
+ ? parsedPaths.filter((value) => typeof value === "string")
881
+ : [];
882
+ return {
883
+ promptId: row.prompt_id,
884
+ localSessionId: row.local_session_id,
885
+ contextKey: row.context_key,
886
+ text: row.prompt_text,
887
+ localImagePaths: localImagePaths.length > 0 ? localImagePaths : undefined,
888
+ messageId: row.message_id ?? undefined,
889
+ replyMessageId: row.reply_message_id ?? undefined,
890
+ createdAt: row.created_at,
891
+ };
892
+ }
893
+ //# sourceMappingURL=StateStore.js.map