@keyou007/agent-bot 0.1.14-alpha.0 → 0.1.14-alpha.2
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/CHANGELOG.md +26 -0
- package/README.md +82 -55
- package/README.zh.md +82 -55
- package/config.example.yaml +5 -1
- package/dist/cli/AutostartManager.js +431 -0
- package/dist/cli/AutostartManager.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +17 -13
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/Initializer.js +35 -0
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/SelfUpdateRunner.js +552 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -0
- package/dist/cli/SelfUpdater.js +431 -0
- package/dist/cli/SelfUpdater.js.map +1 -0
- package/dist/cli/autostartOutput.js +63 -0
- package/dist/cli/autostartOutput.js.map +1 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +92 -18
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/taskGroupOptions.js +14 -11
- package/dist/cli/taskGroupOptions.js.map +1 -1
- package/dist/cli/taskListOutput.js +3 -0
- package/dist/cli/taskListOutput.js.map +1 -1
- package/dist/cli/taskTarget.js +99 -0
- package/dist/cli/taskTarget.js.map +1 -0
- package/dist/cli.js +643 -58
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +16 -5
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +20 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/loadConfig.js +2 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +2 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +584 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +29 -8
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +25 -1
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +43 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +29 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/ownerAccess.js +7 -0
- package/dist/feishu/ownerAccess.js.map +1 -0
- package/dist/index.js +148 -19
- package/dist/index.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +19 -1
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +1444 -66
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/startup/StartupNotifier.js +65 -16
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -2
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/state/StateStore.js +268 -67
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +26 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +17 -16
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +12 -7
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +45 -4
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +30 -18
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +33 -10
- package/docs/technical-reference.zh.md +33 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -2
- package/skills/agent-bot/SKILL.md +107 -182
- package/src/cli/AutostartManager.ts +579 -0
- package/src/cli/FeishuAppConfiguration.ts +22 -7
- package/src/cli/Initializer.ts +57 -0
- package/src/cli/SelfUpdateRunner.ts +817 -0
- package/src/cli/SelfUpdater.ts +688 -0
- package/src/cli/autostartOutput.ts +75 -0
- package/src/cli/controlProtocol.ts +111 -1
- package/src/cli/help.ts +92 -18
- package/src/cli/taskGroupOptions.ts +27 -17
- package/src/cli/taskListOutput.ts +10 -0
- package/src/cli/taskTarget.ts +168 -0
- package/src/cli.ts +781 -65
- package/src/codex/CodexRuntime.ts +17 -5
- package/src/commands/CommandRouter.ts +19 -1
- package/src/commands/commandTypes.ts +3 -0
- package/src/config/loadConfig.ts +2 -0
- package/src/config/schema.ts +2 -0
- package/src/console/ConsoleTurnPresenter.ts +4 -1
- package/src/feishu/CardRenderer.ts +716 -16
- package/src/feishu/FeishuConnector.ts +44 -10
- package/src/feishu/FeishuMarkdown.ts +27 -1
- package/src/feishu/FeishuMessageClient.ts +58 -0
- package/src/feishu/FeishuTurnPresenter.ts +35 -2
- package/src/feishu/ownerAccess.ts +7 -0
- package/src/feishu/types.ts +7 -0
- package/src/index.ts +192 -22
- package/src/presentation/OutboundRouter.ts +28 -3
- package/src/proxy/ProxySessionController.ts +1707 -70
- package/src/runtime/types.ts +1 -0
- package/src/startup/StartupNotifier.ts +67 -17
- package/src/startup/startFeishu.ts +5 -3
- package/src/state/StateStore.ts +413 -65
- package/src/state/migrations.ts +26 -0
- package/src/supervision/SafeRestartNotifier.ts +17 -17
- package/src/supervision/SafeRestartScheduler.ts +13 -6
- package/src/supervision/replacementSupervisor.ts +50 -4
- package/src/supervisor.ts +35 -16
package/src/state/StateStore.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface UserContextRecord {
|
|
|
18
18
|
export interface ChatContextRecord {
|
|
19
19
|
contextKey: string;
|
|
20
20
|
chatType: "p2p" | "group";
|
|
21
|
+
requiresMention: boolean;
|
|
21
22
|
lastActivityAt?: string;
|
|
22
23
|
createdAt: string;
|
|
23
24
|
updatedAt: string;
|
|
@@ -89,6 +90,41 @@ export interface QueuedPromptRecord {
|
|
|
89
90
|
createdAt: string;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
export type TurnAttemptStatus =
|
|
94
|
+
| "accepted"
|
|
95
|
+
| "running"
|
|
96
|
+
| "recovering"
|
|
97
|
+
| "completed"
|
|
98
|
+
| "failed"
|
|
99
|
+
| "cancelled"
|
|
100
|
+
| "interrupted";
|
|
101
|
+
|
|
102
|
+
export interface TurnAttemptRecord {
|
|
103
|
+
attemptId: string;
|
|
104
|
+
localSessionId: string;
|
|
105
|
+
contextKey: string;
|
|
106
|
+
promptText: string;
|
|
107
|
+
localImagePaths?: string[];
|
|
108
|
+
messageId?: string;
|
|
109
|
+
replyMessageId?: string;
|
|
110
|
+
pendingTurnId?: string;
|
|
111
|
+
turnId?: string;
|
|
112
|
+
recoveredFromTurnId?: string;
|
|
113
|
+
recoveryCount: number;
|
|
114
|
+
retryCount: number;
|
|
115
|
+
status: TurnAttemptStatus;
|
|
116
|
+
createdAt: string;
|
|
117
|
+
updatedAt: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface PersistedTurnSnapshotRecord {
|
|
121
|
+
turnId: string;
|
|
122
|
+
localSessionId: string;
|
|
123
|
+
contextKey?: string;
|
|
124
|
+
snapshot: unknown;
|
|
125
|
+
updatedAt: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
92
128
|
interface UserContextRow {
|
|
93
129
|
context_key: string;
|
|
94
130
|
default_agent: string;
|
|
@@ -102,6 +138,7 @@ interface UserContextRow {
|
|
|
102
138
|
interface ChatContextRow {
|
|
103
139
|
context_key: string;
|
|
104
140
|
chat_type: "p2p" | "group";
|
|
141
|
+
require_mention: number;
|
|
105
142
|
last_activity_at: string | null;
|
|
106
143
|
created_at: string;
|
|
107
144
|
updated_at: string;
|
|
@@ -150,6 +187,24 @@ interface QueuedPromptRow {
|
|
|
150
187
|
created_at: string;
|
|
151
188
|
}
|
|
152
189
|
|
|
190
|
+
interface TurnAttemptRow {
|
|
191
|
+
attempt_id: string;
|
|
192
|
+
local_session_id: string;
|
|
193
|
+
context_key: string;
|
|
194
|
+
prompt_text: string;
|
|
195
|
+
local_image_paths_json: string;
|
|
196
|
+
message_id: string | null;
|
|
197
|
+
reply_message_id: string | null;
|
|
198
|
+
pending_turn_id: string | null;
|
|
199
|
+
turn_id: string | null;
|
|
200
|
+
recovered_from_turn_id: string | null;
|
|
201
|
+
recovery_count: number;
|
|
202
|
+
retry_count: number;
|
|
203
|
+
status: TurnAttemptStatus;
|
|
204
|
+
created_at: string;
|
|
205
|
+
updated_at: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
153
208
|
export class StateStore {
|
|
154
209
|
private readonly db: Database.Database;
|
|
155
210
|
|
|
@@ -165,6 +220,7 @@ export class StateStore {
|
|
|
165
220
|
this.ensureSessionColumns();
|
|
166
221
|
this.ensureTurnSnapshotColumns();
|
|
167
222
|
this.ensureChatContextColumns();
|
|
223
|
+
this.ensureTurnAttemptColumns();
|
|
168
224
|
this.initializeContextSessionMappings();
|
|
169
225
|
this.db.exec("DROP INDEX IF EXISTS idx_sessions_remote_session_id_unique");
|
|
170
226
|
this.reconcileDuplicateRemoteSessions();
|
|
@@ -235,6 +291,30 @@ export class StateStore {
|
|
|
235
291
|
`).run(contextKey, chatType, now, now);
|
|
236
292
|
}
|
|
237
293
|
|
|
294
|
+
getChatContext(contextKey: string): ChatContextRecord | undefined {
|
|
295
|
+
const row = this.db.prepare("SELECT * FROM chat_contexts WHERE context_key = ?")
|
|
296
|
+
.get(contextKey) as ChatContextRow | undefined;
|
|
297
|
+
return row ? mapChatContext(row) : undefined;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
setChatRequiresMention(contextKey: string, requiresMention: boolean): void {
|
|
301
|
+
const now = new Date().toISOString();
|
|
302
|
+
this.db.prepare(`
|
|
303
|
+
UPDATE chat_contexts
|
|
304
|
+
SET require_mention = ?, updated_at = ?
|
|
305
|
+
WHERE context_key = ? AND chat_type = 'group'
|
|
306
|
+
`).run(requiresMention ? 1 : 0, now, contextKey);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
chatRequiresMention(contextKey: string): boolean {
|
|
310
|
+
const row = this.db.prepare(`
|
|
311
|
+
SELECT require_mention
|
|
312
|
+
FROM chat_contexts
|
|
313
|
+
WHERE context_key = ? AND chat_type = 'group'
|
|
314
|
+
`).get(contextKey) as { require_mention: number } | undefined;
|
|
315
|
+
return row?.require_mention === 1;
|
|
316
|
+
}
|
|
317
|
+
|
|
238
318
|
listChatContexts(chatType?: "p2p" | "group"): ChatContextRecord[] {
|
|
239
319
|
const rows = chatType
|
|
240
320
|
? this.db.prepare(`
|
|
@@ -243,13 +323,7 @@ export class StateStore {
|
|
|
243
323
|
ORDER BY updated_at DESC
|
|
244
324
|
`).all(chatType) as ChatContextRow[]
|
|
245
325
|
: this.db.prepare("SELECT * FROM chat_contexts ORDER BY updated_at DESC").all() as ChatContextRow[];
|
|
246
|
-
return rows.map(
|
|
247
|
-
contextKey: row.context_key,
|
|
248
|
-
chatType: row.chat_type,
|
|
249
|
-
lastActivityAt: row.last_activity_at ?? undefined,
|
|
250
|
-
createdAt: row.created_at,
|
|
251
|
-
updatedAt: row.updated_at,
|
|
252
|
-
}));
|
|
326
|
+
return rows.map(mapChatContext);
|
|
253
327
|
}
|
|
254
328
|
|
|
255
329
|
markChatActive(contextKey: string, activeAt = new Date()): void {
|
|
@@ -267,13 +341,7 @@ export class StateStore {
|
|
|
267
341
|
WHERE last_activity_at >= ?
|
|
268
342
|
ORDER BY last_activity_at DESC
|
|
269
343
|
`).all(since.toISOString()) as ChatContextRow[];
|
|
270
|
-
return rows.map(
|
|
271
|
-
contextKey: row.context_key,
|
|
272
|
-
chatType: row.chat_type,
|
|
273
|
-
lastActivityAt: row.last_activity_at ?? undefined,
|
|
274
|
-
createdAt: row.created_at,
|
|
275
|
-
updatedAt: row.updated_at,
|
|
276
|
-
}));
|
|
344
|
+
return rows.map(mapChatContext);
|
|
277
345
|
}
|
|
278
346
|
|
|
279
347
|
enqueuePrompt(input: {
|
|
@@ -361,6 +429,199 @@ export class StateStore {
|
|
|
361
429
|
return cancel();
|
|
362
430
|
}
|
|
363
431
|
|
|
432
|
+
createTurnAttempt(input: {
|
|
433
|
+
attemptId: string;
|
|
434
|
+
localSessionId: string;
|
|
435
|
+
contextKey: string;
|
|
436
|
+
promptText: string;
|
|
437
|
+
localImagePaths?: string[];
|
|
438
|
+
messageId?: string;
|
|
439
|
+
replyMessageId?: string;
|
|
440
|
+
pendingTurnId?: string;
|
|
441
|
+
turnId?: string;
|
|
442
|
+
recoveredFromTurnId?: string;
|
|
443
|
+
recoveryCount?: number;
|
|
444
|
+
retryCount?: number;
|
|
445
|
+
status?: Extract<TurnAttemptStatus, "accepted" | "running" | "recovering">;
|
|
446
|
+
createdAt?: string;
|
|
447
|
+
updatedAt?: string;
|
|
448
|
+
}): TurnAttemptRecord {
|
|
449
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
450
|
+
const updatedAt = input.updatedAt ?? createdAt;
|
|
451
|
+
const status = input.status ?? (input.turnId ? "running" : "accepted");
|
|
452
|
+
this.db.prepare(`
|
|
453
|
+
INSERT INTO turn_attempts (
|
|
454
|
+
attempt_id, local_session_id, context_key, prompt_text, local_image_paths_json,
|
|
455
|
+
message_id, reply_message_id, pending_turn_id, turn_id, recovered_from_turn_id,
|
|
456
|
+
recovery_count, retry_count, status, created_at, updated_at
|
|
457
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
458
|
+
`).run(
|
|
459
|
+
input.attemptId,
|
|
460
|
+
input.localSessionId,
|
|
461
|
+
input.contextKey,
|
|
462
|
+
input.promptText,
|
|
463
|
+
JSON.stringify(input.localImagePaths ?? []),
|
|
464
|
+
input.messageId ?? null,
|
|
465
|
+
input.replyMessageId ?? null,
|
|
466
|
+
input.pendingTurnId ?? null,
|
|
467
|
+
input.turnId ?? null,
|
|
468
|
+
input.recoveredFromTurnId ?? null,
|
|
469
|
+
input.recoveryCount ?? 0,
|
|
470
|
+
input.retryCount ?? 0,
|
|
471
|
+
status,
|
|
472
|
+
createdAt,
|
|
473
|
+
updatedAt,
|
|
474
|
+
);
|
|
475
|
+
return this.getTurnAttempt(input.attemptId)!;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
getTurnAttempt(attemptId: string): TurnAttemptRecord | undefined {
|
|
479
|
+
const row = this.db.prepare("SELECT * FROM turn_attempts WHERE attempt_id = ?")
|
|
480
|
+
.get(attemptId) as TurnAttemptRow | undefined;
|
|
481
|
+
return row ? mapTurnAttempt(row) : undefined;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
findIncompleteTurnAttemptForSession(localSessionId: string): TurnAttemptRecord | undefined {
|
|
485
|
+
const row = this.db.prepare(`
|
|
486
|
+
SELECT * FROM turn_attempts
|
|
487
|
+
WHERE local_session_id = ?
|
|
488
|
+
AND status IN ('accepted', 'running', 'recovering')
|
|
489
|
+
ORDER BY updated_at DESC, rowid DESC
|
|
490
|
+
LIMIT 1
|
|
491
|
+
`).get(localSessionId) as TurnAttemptRow | undefined;
|
|
492
|
+
return row ? mapTurnAttempt(row) : undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
listIncompleteTurnAttempts(): TurnAttemptRecord[] {
|
|
496
|
+
const rows = this.db.prepare(`
|
|
497
|
+
SELECT * FROM turn_attempts
|
|
498
|
+
WHERE status IN ('accepted', 'running', 'recovering')
|
|
499
|
+
ORDER BY created_at ASC, rowid ASC
|
|
500
|
+
`).all() as TurnAttemptRow[];
|
|
501
|
+
return rows.map(mapTurnAttempt);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
updateTurnAttempt(
|
|
505
|
+
attemptId: string,
|
|
506
|
+
patch: Partial<{
|
|
507
|
+
messageId: string | null;
|
|
508
|
+
replyMessageId: string | null;
|
|
509
|
+
pendingTurnId: string | null;
|
|
510
|
+
turnId: string | null;
|
|
511
|
+
recoveredFromTurnId: string | null;
|
|
512
|
+
recoveryCount: number;
|
|
513
|
+
retryCount: number;
|
|
514
|
+
status: TurnAttemptStatus;
|
|
515
|
+
}>,
|
|
516
|
+
): void {
|
|
517
|
+
const existing = this.getTurnAttempt(attemptId);
|
|
518
|
+
if (!existing) return;
|
|
519
|
+
const value = <T>(candidate: T | null | undefined, fallback: T | undefined): T | null =>
|
|
520
|
+
candidate === undefined ? fallback ?? null : candidate;
|
|
521
|
+
this.db.prepare(`
|
|
522
|
+
UPDATE turn_attempts
|
|
523
|
+
SET message_id = ?, reply_message_id = ?, pending_turn_id = ?, turn_id = ?,
|
|
524
|
+
recovered_from_turn_id = ?, recovery_count = ?, retry_count = ?, status = ?, updated_at = ?
|
|
525
|
+
WHERE attempt_id = ?
|
|
526
|
+
`).run(
|
|
527
|
+
value(patch.messageId, existing.messageId),
|
|
528
|
+
value(patch.replyMessageId, existing.replyMessageId),
|
|
529
|
+
value(patch.pendingTurnId, existing.pendingTurnId),
|
|
530
|
+
value(patch.turnId, existing.turnId),
|
|
531
|
+
value(patch.recoveredFromTurnId, existing.recoveredFromTurnId),
|
|
532
|
+
patch.recoveryCount ?? existing.recoveryCount,
|
|
533
|
+
patch.retryCount ?? existing.retryCount,
|
|
534
|
+
patch.status ?? existing.status,
|
|
535
|
+
new Date().toISOString(),
|
|
536
|
+
attemptId,
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
bindTurnAttempt(localSessionId: string, turnId: string): TurnAttemptRecord | undefined {
|
|
541
|
+
const attempt = this.findIncompleteTurnAttemptForSession(localSessionId);
|
|
542
|
+
if (!attempt || (attempt.turnId && attempt.turnId !== turnId)) return undefined;
|
|
543
|
+
this.updateTurnAttempt(attempt.attemptId, { turnId, status: "running" });
|
|
544
|
+
return this.getTurnAttempt(attempt.attemptId);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
markTurnAttemptTerminal(
|
|
548
|
+
turnId: string,
|
|
549
|
+
status: Extract<TurnAttemptStatus, "completed" | "failed" | "cancelled">,
|
|
550
|
+
): void {
|
|
551
|
+
this.db.prepare(`
|
|
552
|
+
UPDATE turn_attempts
|
|
553
|
+
SET status = ?, updated_at = ?
|
|
554
|
+
WHERE turn_id = ?
|
|
555
|
+
`).run(status, new Date().toISOString(), turnId);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
findIncompleteTurnAttemptByTurnId(turnId: string): TurnAttemptRecord | undefined {
|
|
559
|
+
const row = this.db.prepare(`
|
|
560
|
+
SELECT * FROM turn_attempts
|
|
561
|
+
WHERE turn_id = ? AND status IN ('accepted', 'running', 'recovering')
|
|
562
|
+
ORDER BY updated_at DESC, rowid DESC
|
|
563
|
+
LIMIT 1
|
|
564
|
+
`).get(turnId) as TurnAttemptRow | undefined;
|
|
565
|
+
return row ? mapTurnAttempt(row) : undefined;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
touchTurnAttempt(turnId: string, activityAt = new Date()): void {
|
|
569
|
+
const timestamp = activityAt.toISOString();
|
|
570
|
+
const throttleCutoff = new Date(activityAt.getTime() - 15_000).toISOString();
|
|
571
|
+
this.db.prepare(`
|
|
572
|
+
UPDATE turn_attempts
|
|
573
|
+
SET updated_at = ?
|
|
574
|
+
WHERE turn_id = ?
|
|
575
|
+
AND status IN ('accepted', 'running', 'recovering')
|
|
576
|
+
AND updated_at < ?
|
|
577
|
+
`).run(timestamp, turnId, throttleCutoff);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
prepareTurnAttemptRecovery(attemptId: string, recoveredFromTurnId?: string): TurnAttemptRecord | undefined {
|
|
581
|
+
const existing = this.getTurnAttempt(attemptId);
|
|
582
|
+
if (!existing) return undefined;
|
|
583
|
+
this.updateTurnAttempt(attemptId, {
|
|
584
|
+
pendingTurnId: null,
|
|
585
|
+
turnId: null,
|
|
586
|
+
recoveredFromTurnId: recoveredFromTurnId ?? existing.turnId ?? existing.recoveredFromTurnId ?? null,
|
|
587
|
+
recoveryCount: existing.recoveryCount + 1,
|
|
588
|
+
status: "recovering",
|
|
589
|
+
});
|
|
590
|
+
return this.getTurnAttempt(attemptId);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
prepareTurnAttemptRetry(attemptId: string, failedTurnId: string): TurnAttemptRecord | undefined {
|
|
594
|
+
const existing = this.getTurnAttempt(attemptId);
|
|
595
|
+
if (!existing || existing.turnId !== failedTurnId || !isIncompleteTurnAttemptStatus(existing.status)) {
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
this.updateTurnAttempt(attemptId, {
|
|
599
|
+
pendingTurnId: null,
|
|
600
|
+
turnId: null,
|
|
601
|
+
recoveredFromTurnId: failedTurnId,
|
|
602
|
+
retryCount: existing.retryCount + 1,
|
|
603
|
+
status: "recovering",
|
|
604
|
+
});
|
|
605
|
+
return this.getTurnAttempt(attemptId);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
rebindPendingTurnMessages(localSessionId: string, sourceTurnId: string, targetTurnId: string): void {
|
|
609
|
+
const now = new Date().toISOString();
|
|
610
|
+
const rebind = this.db.transaction(() => {
|
|
611
|
+
this.db.prepare(`
|
|
612
|
+
UPDATE message_reactions
|
|
613
|
+
SET turn_id = ?, updated_at = ?
|
|
614
|
+
WHERE local_session_id = ? AND turn_id = ? AND status = 'pending'
|
|
615
|
+
`).run(targetTurnId, now, localSessionId, sourceTurnId);
|
|
616
|
+
this.db.prepare(`
|
|
617
|
+
UPDATE message_turn_bindings
|
|
618
|
+
SET turn_id = ?, updated_at = ?
|
|
619
|
+
WHERE local_session_id = ? AND turn_id = ?
|
|
620
|
+
`).run(targetTurnId, now, localSessionId, sourceTurnId);
|
|
621
|
+
});
|
|
622
|
+
rebind();
|
|
623
|
+
}
|
|
624
|
+
|
|
364
625
|
nextForkTitle(sourceTitle?: string): string {
|
|
365
626
|
const baseTitle = forkBaseTitle(sourceTitle);
|
|
366
627
|
const allocate = this.db.transaction(() => {
|
|
@@ -378,57 +639,6 @@ export class StateStore {
|
|
|
378
639
|
return formatForkTitle(baseTitle, allocate());
|
|
379
640
|
}
|
|
380
641
|
|
|
381
|
-
reconcileInterruptedAcpSessions(acpAgentNames: string[]): SessionRecord[] {
|
|
382
|
-
const agentClause = acpAgentNames.length
|
|
383
|
-
? ` OR (runtime_kind IS NULL AND agent_name IN (${acpAgentNames.map(() => "?").join(", ")}))`
|
|
384
|
-
: "";
|
|
385
|
-
const rows = this.db
|
|
386
|
-
.prepare(`SELECT * FROM sessions WHERE status = 'running' AND (runtime_kind = 'acp'${agentClause})`)
|
|
387
|
-
.all(...acpAgentNames) as SessionRow[];
|
|
388
|
-
if (rows.length === 0) return [];
|
|
389
|
-
|
|
390
|
-
const now = new Date().toISOString();
|
|
391
|
-
const completedAt = Date.now();
|
|
392
|
-
const reconcile = this.db.transaction(() => {
|
|
393
|
-
for (const row of rows) {
|
|
394
|
-
this.db.prepare(`
|
|
395
|
-
UPDATE sessions
|
|
396
|
-
SET status = 'failed',
|
|
397
|
-
last_turn_status = CASE WHEN last_turn_id IS NULL THEN last_turn_status ELSE 'failed' END,
|
|
398
|
-
updated_at = ?
|
|
399
|
-
WHERE local_session_id = ?
|
|
400
|
-
`).run(now, row.local_session_id);
|
|
401
|
-
|
|
402
|
-
if (!row.last_turn_id) continue;
|
|
403
|
-
const snapshotRow = this.db
|
|
404
|
-
.prepare("SELECT snapshot_json FROM turn_snapshots WHERE turn_id = ?")
|
|
405
|
-
.get(row.last_turn_id) as { snapshot_json: string } | undefined;
|
|
406
|
-
if (!snapshotRow) continue;
|
|
407
|
-
try {
|
|
408
|
-
const snapshot = JSON.parse(snapshotRow.snapshot_json) as Record<string, unknown>;
|
|
409
|
-
if (!["starting", "running", "tool_running", "waiting_for_approval"].includes(String(snapshot.status))) continue;
|
|
410
|
-
const startedAt = typeof snapshot.startedAt === "number" ? snapshot.startedAt : undefined;
|
|
411
|
-
const failedSnapshot: Record<string, unknown> = {
|
|
412
|
-
...snapshot,
|
|
413
|
-
status: "failed",
|
|
414
|
-
completedAt,
|
|
415
|
-
...(startedAt === undefined ? {} : { durationMs: Math.max(0, completedAt - startedAt) }),
|
|
416
|
-
error: "Agent Bot 已重启,原 ACP 进程中的执行无法继续。",
|
|
417
|
-
};
|
|
418
|
-
delete failedSnapshot.activeTool;
|
|
419
|
-
delete failedSnapshot.approval;
|
|
420
|
-
this.db.prepare(`
|
|
421
|
-
UPDATE turn_snapshots SET snapshot_json = ?, updated_at = ? WHERE turn_id = ?
|
|
422
|
-
`).run(JSON.stringify(failedSnapshot), now, row.last_turn_id);
|
|
423
|
-
} catch {
|
|
424
|
-
// Keep an unreadable historical snapshot intact while still correcting the session state.
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
reconcile();
|
|
429
|
-
return rows.map(mapSession);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
642
|
setDefaultAgent(contextKey: string, agentName: string): void {
|
|
433
643
|
const now = new Date().toISOString();
|
|
434
644
|
this.db
|
|
@@ -533,6 +743,25 @@ export class StateStore {
|
|
|
533
743
|
);
|
|
534
744
|
}
|
|
535
745
|
|
|
746
|
+
archiveSession(localSessionId: string): void {
|
|
747
|
+
const now = new Date().toISOString();
|
|
748
|
+
const archive = this.db.transaction(() => {
|
|
749
|
+
this.db.prepare(`
|
|
750
|
+
UPDATE sessions
|
|
751
|
+
SET status = 'closed', updated_at = ?
|
|
752
|
+
WHERE local_session_id = ?
|
|
753
|
+
`).run(now, localSessionId);
|
|
754
|
+
this.db.prepare(`
|
|
755
|
+
UPDATE user_contexts
|
|
756
|
+
SET current_session_id = CASE WHEN current_session_id = ? THEN NULL ELSE current_session_id END,
|
|
757
|
+
previous_session_id = CASE WHEN previous_session_id = ? THEN NULL ELSE previous_session_id END,
|
|
758
|
+
updated_at = ?
|
|
759
|
+
WHERE current_session_id = ? OR previous_session_id = ?
|
|
760
|
+
`).run(localSessionId, localSessionId, now, localSessionId, localSessionId);
|
|
761
|
+
});
|
|
762
|
+
archive();
|
|
763
|
+
}
|
|
764
|
+
|
|
536
765
|
updateRuntimeSession(
|
|
537
766
|
localSessionId: string,
|
|
538
767
|
patch: Partial<
|
|
@@ -620,6 +849,34 @@ export class StateStore {
|
|
|
620
849
|
return row ? JSON.parse(row.snapshot_json) : undefined;
|
|
621
850
|
}
|
|
622
851
|
|
|
852
|
+
findLatestTurnSnapshotForSession(localSessionId: string): PersistedTurnSnapshotRecord | undefined {
|
|
853
|
+
const row = this.db.prepare(`
|
|
854
|
+
SELECT turn_id, local_session_id, context_key, snapshot_json, updated_at
|
|
855
|
+
FROM turn_snapshots
|
|
856
|
+
WHERE local_session_id = ?
|
|
857
|
+
ORDER BY
|
|
858
|
+
coalesce(json_extract(snapshot_json, '$.startedAt'), 0) DESC,
|
|
859
|
+
updated_at DESC,
|
|
860
|
+
rowid DESC
|
|
861
|
+
LIMIT 1
|
|
862
|
+
`).get(localSessionId) as {
|
|
863
|
+
turn_id: string;
|
|
864
|
+
local_session_id: string;
|
|
865
|
+
context_key: string | null;
|
|
866
|
+
snapshot_json: string;
|
|
867
|
+
updated_at: string;
|
|
868
|
+
} | undefined;
|
|
869
|
+
return row
|
|
870
|
+
? {
|
|
871
|
+
turnId: row.turn_id,
|
|
872
|
+
localSessionId: row.local_session_id,
|
|
873
|
+
contextKey: row.context_key ?? undefined,
|
|
874
|
+
snapshot: JSON.parse(row.snapshot_json),
|
|
875
|
+
updatedAt: row.updated_at,
|
|
876
|
+
}
|
|
877
|
+
: undefined;
|
|
878
|
+
}
|
|
879
|
+
|
|
623
880
|
saveTurnRuntimeOrigin(
|
|
624
881
|
turnId: string,
|
|
625
882
|
localSessionId: string,
|
|
@@ -983,6 +1240,46 @@ export class StateStore {
|
|
|
983
1240
|
return rows.map(mapSession);
|
|
984
1241
|
}
|
|
985
1242
|
|
|
1243
|
+
listUndeliveredCompletedTurns(): TurnAnchorRecord[] {
|
|
1244
|
+
const rows = this.db.prepare(`
|
|
1245
|
+
SELECT snapshots.turn_id, snapshots.local_session_id, snapshots.context_key
|
|
1246
|
+
FROM turn_snapshots AS snapshots
|
|
1247
|
+
LEFT JOIN turn_deliveries AS deliveries ON deliveries.turn_id = snapshots.turn_id
|
|
1248
|
+
WHERE json_extract(snapshots.snapshot_json, '$.status') = 'completed'
|
|
1249
|
+
AND length(coalesce(json_extract(snapshots.snapshot_json, '$.finalResponse'), '')) > 0
|
|
1250
|
+
AND deliveries.final_delivered_at IS NULL
|
|
1251
|
+
ORDER BY snapshots.updated_at ASC
|
|
1252
|
+
`).all() as Array<{
|
|
1253
|
+
turn_id: string;
|
|
1254
|
+
local_session_id: string;
|
|
1255
|
+
context_key: string | null;
|
|
1256
|
+
}>;
|
|
1257
|
+
return rows.map((row) => ({
|
|
1258
|
+
turnId: row.turn_id,
|
|
1259
|
+
localSessionId: row.local_session_id,
|
|
1260
|
+
contextKey: row.context_key ?? undefined,
|
|
1261
|
+
}));
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
findMessageIdForTurn(turnId: string): string | undefined {
|
|
1265
|
+
const binding = this.db.prepare(`
|
|
1266
|
+
SELECT message_id
|
|
1267
|
+
FROM message_turn_bindings
|
|
1268
|
+
WHERE turn_id = ?
|
|
1269
|
+
ORDER BY updated_at DESC, rowid DESC
|
|
1270
|
+
LIMIT 1
|
|
1271
|
+
`).get(turnId) as { message_id: string } | undefined;
|
|
1272
|
+
if (binding) return binding.message_id;
|
|
1273
|
+
const reaction = this.db.prepare(`
|
|
1274
|
+
SELECT message_id
|
|
1275
|
+
FROM message_reactions
|
|
1276
|
+
WHERE turn_id = ?
|
|
1277
|
+
ORDER BY updated_at DESC, rowid DESC
|
|
1278
|
+
LIMIT 1
|
|
1279
|
+
`).get(turnId) as { message_id: string } | undefined;
|
|
1280
|
+
return reaction?.message_id;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
986
1283
|
findLatestMessageIdForSession(localSessionId: string, contextKey?: string): string | undefined {
|
|
987
1284
|
const row = this.db.prepare(`
|
|
988
1285
|
SELECT message_id
|
|
@@ -1402,6 +1699,18 @@ export class StateStore {
|
|
|
1402
1699
|
if (!existing.has("last_activity_at")) {
|
|
1403
1700
|
this.db.exec("ALTER TABLE chat_contexts ADD COLUMN last_activity_at TEXT");
|
|
1404
1701
|
}
|
|
1702
|
+
if (!existing.has("require_mention")) {
|
|
1703
|
+
this.db.exec("ALTER TABLE chat_contexts ADD COLUMN require_mention INTEGER NOT NULL DEFAULT 0");
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
private ensureTurnAttemptColumns(): void {
|
|
1708
|
+
const existing = new Set(
|
|
1709
|
+
(this.db.pragma("table_info(turn_attempts)") as Array<{ name: string }>).map((column) => column.name),
|
|
1710
|
+
);
|
|
1711
|
+
if (!existing.has("retry_count")) {
|
|
1712
|
+
this.db.exec("ALTER TABLE turn_attempts ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0");
|
|
1713
|
+
}
|
|
1405
1714
|
}
|
|
1406
1715
|
}
|
|
1407
1716
|
|
|
@@ -1433,6 +1742,17 @@ function mapUserContext(row: UserContextRow): UserContextRecord {
|
|
|
1433
1742
|
};
|
|
1434
1743
|
}
|
|
1435
1744
|
|
|
1745
|
+
function mapChatContext(row: ChatContextRow): ChatContextRecord {
|
|
1746
|
+
return {
|
|
1747
|
+
contextKey: row.context_key,
|
|
1748
|
+
chatType: row.chat_type,
|
|
1749
|
+
requiresMention: row.require_mention === 1,
|
|
1750
|
+
lastActivityAt: row.last_activity_at ?? undefined,
|
|
1751
|
+
createdAt: row.created_at,
|
|
1752
|
+
updatedAt: row.updated_at,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1436
1756
|
function mapSession(row: SessionRow): SessionRecord {
|
|
1437
1757
|
return {
|
|
1438
1758
|
localSessionId: row.local_session_id,
|
|
@@ -1485,3 +1805,31 @@ function mapQueuedPrompt(row: QueuedPromptRow): QueuedPromptRecord {
|
|
|
1485
1805
|
createdAt: row.created_at,
|
|
1486
1806
|
};
|
|
1487
1807
|
}
|
|
1808
|
+
|
|
1809
|
+
function mapTurnAttempt(row: TurnAttemptRow): TurnAttemptRecord {
|
|
1810
|
+
const parsedPaths = JSON.parse(row.local_image_paths_json) as unknown;
|
|
1811
|
+
const localImagePaths = Array.isArray(parsedPaths)
|
|
1812
|
+
? parsedPaths.filter((value): value is string => typeof value === "string")
|
|
1813
|
+
: [];
|
|
1814
|
+
return {
|
|
1815
|
+
attemptId: row.attempt_id,
|
|
1816
|
+
localSessionId: row.local_session_id,
|
|
1817
|
+
contextKey: row.context_key,
|
|
1818
|
+
promptText: row.prompt_text,
|
|
1819
|
+
localImagePaths: localImagePaths.length > 0 ? localImagePaths : undefined,
|
|
1820
|
+
messageId: row.message_id ?? undefined,
|
|
1821
|
+
replyMessageId: row.reply_message_id ?? undefined,
|
|
1822
|
+
pendingTurnId: row.pending_turn_id ?? undefined,
|
|
1823
|
+
turnId: row.turn_id ?? undefined,
|
|
1824
|
+
recoveredFromTurnId: row.recovered_from_turn_id ?? undefined,
|
|
1825
|
+
recoveryCount: row.recovery_count,
|
|
1826
|
+
retryCount: row.retry_count,
|
|
1827
|
+
status: row.status,
|
|
1828
|
+
createdAt: row.created_at,
|
|
1829
|
+
updatedAt: row.updated_at,
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function isIncompleteTurnAttemptStatus(status: TurnAttemptStatus): boolean {
|
|
1834
|
+
return status === "accepted" || status === "running" || status === "recovering";
|
|
1835
|
+
}
|
package/src/state/migrations.ts
CHANGED
|
@@ -104,6 +104,7 @@ export const migrations = [
|
|
|
104
104
|
CREATE TABLE IF NOT EXISTS chat_contexts (
|
|
105
105
|
context_key TEXT PRIMARY KEY,
|
|
106
106
|
chat_type TEXT NOT NULL,
|
|
107
|
+
require_mention INTEGER NOT NULL DEFAULT 0,
|
|
107
108
|
last_activity_at TEXT,
|
|
108
109
|
created_at TEXT NOT NULL,
|
|
109
110
|
updated_at TEXT NOT NULL
|
|
@@ -146,4 +147,29 @@ export const migrations = [
|
|
|
146
147
|
CREATE INDEX IF NOT EXISTS idx_turn_parent_links_session
|
|
147
148
|
ON turn_parent_links(local_session_id);
|
|
148
149
|
`,
|
|
150
|
+
`
|
|
151
|
+
CREATE TABLE IF NOT EXISTS turn_attempts (
|
|
152
|
+
attempt_id TEXT PRIMARY KEY,
|
|
153
|
+
local_session_id TEXT NOT NULL,
|
|
154
|
+
context_key TEXT NOT NULL,
|
|
155
|
+
prompt_text TEXT NOT NULL,
|
|
156
|
+
local_image_paths_json TEXT NOT NULL DEFAULT '[]',
|
|
157
|
+
message_id TEXT,
|
|
158
|
+
reply_message_id TEXT,
|
|
159
|
+
pending_turn_id TEXT,
|
|
160
|
+
turn_id TEXT,
|
|
161
|
+
recovered_from_turn_id TEXT,
|
|
162
|
+
recovery_count INTEGER NOT NULL DEFAULT 0,
|
|
163
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
164
|
+
status TEXT NOT NULL,
|
|
165
|
+
created_at TEXT NOT NULL,
|
|
166
|
+
updated_at TEXT NOT NULL
|
|
167
|
+
);
|
|
168
|
+
CREATE INDEX IF NOT EXISTS idx_turn_attempts_recovery
|
|
169
|
+
ON turn_attempts(status, created_at);
|
|
170
|
+
CREATE INDEX IF NOT EXISTS idx_turn_attempts_session
|
|
171
|
+
ON turn_attempts(local_session_id, updated_at);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_turn_attempts_turn
|
|
173
|
+
ON turn_attempts(turn_id);
|
|
174
|
+
`,
|
|
149
175
|
] as const;
|
|
@@ -109,17 +109,16 @@ export class SafeRestartNotifier {
|
|
|
109
109
|
id: session.remoteSessionId ?? session.localSessionId,
|
|
110
110
|
title: session.title,
|
|
111
111
|
}));
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
await Promise.all(targets.map(async ({ contextKey, replyMessageId }) => {
|
|
112
|
+
await Promise.all(targets.map(async ({ contextKey, replyMessageId, reason }) => {
|
|
113
|
+
const card = this.renderer.renderSafeRestartStatus({
|
|
114
|
+
scheduleId: status.scheduleId,
|
|
115
|
+
reason: reason ?? status.reason,
|
|
116
|
+
phase: status.phase,
|
|
117
|
+
remainingMs: status.remainingMs,
|
|
118
|
+
pendingFinalDeliveries: status.activity.pendingFinalDeliveries,
|
|
119
|
+
waitingTasks,
|
|
120
|
+
});
|
|
121
|
+
const cardHash = JSON.stringify(card);
|
|
123
122
|
const messageId = this.messageIds.get(contextKey);
|
|
124
123
|
if (messageId) {
|
|
125
124
|
if (this.cardHashes.get(contextKey) === cardHash) return;
|
|
@@ -165,12 +164,13 @@ export class SafeRestartNotifier {
|
|
|
165
164
|
const contextKey = target.contextKey.trim();
|
|
166
165
|
if (!contextKey) continue;
|
|
167
166
|
const existing = this.notificationTargets.get(contextKey);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
})
|
|
173
|
-
|
|
167
|
+
const replyMessageId = existing?.replyMessageId ?? target.replyMessageId?.trim();
|
|
168
|
+
const reason = target.reason?.trim() ?? existing?.reason;
|
|
169
|
+
this.notificationTargets.set(contextKey, {
|
|
170
|
+
contextKey,
|
|
171
|
+
...(replyMessageId ? { replyMessageId } : {}),
|
|
172
|
+
...(reason ? { reason } : {}),
|
|
173
|
+
});
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
}
|