@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.
- package/.env.example +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +675 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/SECURITY.md +13 -0
- package/config.example.yaml +56 -0
- package/dist/acp/AcpJsonRpcConnection.js +155 -0
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
- package/dist/acp/AcpProcessManager.js +65 -0
- package/dist/acp/AcpProcessManager.js.map +1 -0
- package/dist/acp/AcpSessionManager.js +164 -0
- package/dist/acp/AcpSessionManager.js.map +1 -0
- package/dist/acp/acpTypes.js +10 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +349 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -0
- package/dist/cli/FeishuAppRegistration.js +160 -0
- package/dist/cli/FeishuAppRegistration.js.map +1 -0
- package/dist/cli/Initializer.js +112 -0
- package/dist/cli/Initializer.js.map +1 -0
- package/dist/cli/LocalControlClient.js +47 -0
- package/dist/cli/LocalControlClient.js.map +1 -0
- package/dist/cli/LocalControlServer.js +66 -0
- package/dist/cli/LocalControlServer.js.map +1 -0
- package/dist/cli/SkillRegistry.js +126 -0
- package/dist/cli/SkillRegistry.js.map +1 -0
- package/dist/cli/controlProtocol.js +10 -0
- package/dist/cli/controlProtocol.js.map +1 -0
- package/dist/cli/taskChatRoute.js +23 -0
- package/dist/cli/taskChatRoute.js.map +1 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/AppServerConnection.js +131 -0
- package/dist/codex/AppServerConnection.js.map +1 -0
- package/dist/codex/CodexEventMapper.js +296 -0
- package/dist/codex/CodexEventMapper.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +124 -0
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
- package/dist/codex/CodexProcessManager.js +64 -0
- package/dist/codex/CodexProcessManager.js.map +1 -0
- package/dist/codex/CodexRuntime.js +825 -0
- package/dist/codex/CodexRuntime.js.map +1 -0
- package/dist/codex/ProjectlessWorkspace.js +74 -0
- package/dist/codex/ProjectlessWorkspace.js.map +1 -0
- package/dist/codex/protocol.js +10 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/commands/CommandRouter.js +176 -0
- package/dist/commands/CommandRouter.js.map +1 -0
- package/dist/commands/commandTypes.js +2 -0
- package/dist/commands/commandTypes.js.map +1 -0
- package/dist/config/loadConfig.js +107 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/paths.js +32 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.js +38 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/console/ConsoleConnector.js +39 -0
- package/dist/console/ConsoleConnector.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +60 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -0
- package/dist/feishu/CardRenderer.js +972 -0
- package/dist/feishu/CardRenderer.js.map +1 -0
- package/dist/feishu/CardUpdateScheduler.js +148 -0
- package/dist/feishu/CardUpdateScheduler.js.map +1 -0
- package/dist/feishu/ConsoleFeishuClient.js +18 -0
- package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +257 -0
- package/dist/feishu/FeishuConnector.js.map +1 -0
- package/dist/feishu/FeishuMarkdown.js +37 -0
- package/dist/feishu/FeishuMarkdown.js.map +1 -0
- package/dist/feishu/FeishuMessageClient.js +459 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -0
- package/dist/feishu/FeishuTurnPresenter.js +425 -0
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
- package/dist/feishu/GroupAvatarGenerator.js +86 -0
- package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
- package/dist/feishu/LocalCardImage.js +13 -0
- package/dist/feishu/LocalCardImage.js.map +1 -0
- package/dist/feishu/LocalImageMarkdown.js +89 -0
- package/dist/feishu/LocalImageMarkdown.js.map +1 -0
- package/dist/feishu/contextKey.js +12 -0
- package/dist/feishu/contextKey.js.map +1 -0
- package/dist/feishu/transport.js +14 -0
- package/dist/feishu/transport.js.map +1 -0
- package/dist/feishu/types.js +2 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +22 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +127 -0
- package/dist/presentation/OutboundRouter.js.map +1 -0
- package/dist/presentation/TurnStateReducer.js +307 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -0
- package/dist/presentation/splitMarkdown.js +341 -0
- package/dist/presentation/splitMarkdown.js.map +1 -0
- package/dist/presentation/turnViewTypes.js +2 -0
- package/dist/presentation/turnViewTypes.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +2819 -0
- package/dist/proxy/ProxySessionController.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +271 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
- package/dist/runtime/AgentRuntimeRegistry.js +18 -0
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
- package/dist/runtime/agentEnvironment.js +11 -0
- package/dist/runtime/agentEnvironment.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +46 -0
- package/dist/startup/SessionMetadataHydrator.js.map +1 -0
- package/dist/startup/StartupNotifier.js +78 -0
- package/dist/startup/StartupNotifier.js.map +1 -0
- package/dist/startup/startFeishu.js +6 -0
- package/dist/startup/startFeishu.js.map +1 -0
- package/dist/state/StateStore.js +893 -0
- package/dist/state/StateStore.js.map +1 -0
- package/dist/state/migrations.js +129 -0
- package/dist/state/migrations.js.map +1 -0
- package/dist/supervision/SafeRestartNotifier.js +116 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -0
- package/dist/supervision/SafeRestartScheduler.js +123 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -0
- package/dist/supervision/replacementSupervisor.js +8 -0
- package/dist/supervision/replacementSupervisor.js.map +1 -0
- package/dist/supervision/restartPolicy.js +16 -0
- package/dist/supervision/restartPolicy.js.map +1 -0
- package/dist/supervisor.js +94 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/utils/executeShellCommand.js +76 -0
- package/dist/utils/executeShellCommand.js.map +1 -0
- package/dist/utils/id.js +5 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/markdown.js +29 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/spawnCommand.js +21 -0
- package/dist/utils/spawnCommand.js.map +1 -0
- package/dist/utils/taskTitle.js +10 -0
- package/dist/utils/taskTitle.js.map +1 -0
- package/docs/technical-reference.md +294 -0
- package/docs/technical-reference.zh.md +294 -0
- package/npm-shrinkwrap.json +4525 -0
- package/package.json +87 -0
- package/skills/agent-bot/SKILL.md +141 -0
- package/skills/agent-bot/agents/openai.yaml +4 -0
- package/src/acp/AcpJsonRpcConnection.ts +212 -0
- package/src/acp/AcpProcessManager.ts +87 -0
- package/src/acp/AcpSessionManager.ts +227 -0
- package/src/acp/acpTypes.ts +99 -0
- package/src/cli/FeishuAppConfiguration.ts +473 -0
- package/src/cli/FeishuAppRegistration.ts +207 -0
- package/src/cli/Initializer.ts +159 -0
- package/src/cli/LocalControlClient.ts +48 -0
- package/src/cli/LocalControlServer.ts +67 -0
- package/src/cli/SkillRegistry.ts +151 -0
- package/src/cli/controlProtocol.ts +34 -0
- package/src/cli/taskChatRoute.ts +31 -0
- package/src/cli.ts +630 -0
- package/src/codex/AppServerConnection.ts +159 -0
- package/src/codex/CodexEventMapper.ts +351 -0
- package/src/codex/CodexLocalActivityDetector.ts +148 -0
- package/src/codex/CodexProcessManager.ts +63 -0
- package/src/codex/CodexRuntime.ts +1025 -0
- package/src/codex/ProjectlessWorkspace.ts +90 -0
- package/src/codex/protocol.ts +37 -0
- package/src/commands/CommandRouter.ts +179 -0
- package/src/commands/commandTypes.ts +28 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/paths.ts +37 -0
- package/src/config/schema.ts +46 -0
- package/src/console/ConsoleConnector.ts +40 -0
- package/src/console/ConsoleTurnPresenter.ts +64 -0
- package/src/feishu/CardRenderer.ts +1157 -0
- package/src/feishu/CardUpdateScheduler.ts +165 -0
- package/src/feishu/ConsoleFeishuClient.ts +22 -0
- package/src/feishu/FeishuConnector.ts +277 -0
- package/src/feishu/FeishuMarkdown.ts +47 -0
- package/src/feishu/FeishuMessageClient.ts +649 -0
- package/src/feishu/FeishuTurnPresenter.ts +539 -0
- package/src/feishu/GroupAvatarGenerator.ts +102 -0
- package/src/feishu/LocalCardImage.ts +13 -0
- package/src/feishu/LocalImageMarkdown.ts +92 -0
- package/src/feishu/contextKey.ts +14 -0
- package/src/feishu/transport.ts +24 -0
- package/src/feishu/types.ts +83 -0
- package/src/index.ts +228 -0
- package/src/logging/logger.ts +28 -0
- package/src/presentation/OutboundRouter.ts +205 -0
- package/src/presentation/TurnStateReducer.ts +341 -0
- package/src/presentation/splitMarkdown.ts +358 -0
- package/src/presentation/turnViewTypes.ts +54 -0
- package/src/proxy/ProxySessionController.ts +3311 -0
- package/src/runtime/AcpRuntimeAdapter.ts +324 -0
- package/src/runtime/AgentRuntimeRegistry.ts +20 -0
- package/src/runtime/agentEnvironment.ts +14 -0
- package/src/runtime/types.ts +215 -0
- package/src/startup/SessionMetadataHydrator.ts +46 -0
- package/src/startup/StartupNotifier.ts +92 -0
- package/src/startup/startFeishu.ts +19 -0
- package/src/state/StateStore.ts +1249 -0
- package/src/state/migrations.ts +128 -0
- package/src/supervision/SafeRestartNotifier.ts +122 -0
- package/src/supervision/SafeRestartScheduler.ts +149 -0
- package/src/supervision/replacementSupervisor.ts +10 -0
- package/src/supervision/restartPolicy.ts +19 -0
- package/src/supervisor.ts +106 -0
- package/src/utils/executeShellCommand.ts +97 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/spawnCommand.ts +35 -0
- package/src/utils/taskTitle.ts +9 -0
- 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
|