@keyou007/agent-bot 0.1.19-alpha.0 → 0.1.19-alpha.1
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 +5 -0
- package/README.md +3 -0
- package/README.zh.md +3 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +48 -0
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +35 -2
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +83 -0
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +6 -1
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +449 -4
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +10 -0
- package/dist/state/StateStore.js.map +1 -1
- package/docs/technical-reference.md +1 -1
- package/docs/technical-reference.zh.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +3 -0
- package/src/cli/controlProtocol.ts +8 -0
- package/src/cli/help.ts +2 -0
- package/src/cli.ts +33 -0
- package/src/codex/CodexProcessManager.ts +43 -0
- package/src/codex/CodexRuntime.ts +33 -2
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +96 -0
- package/src/feishu/MergedForwardMessage.ts +5 -1
- package/src/index.ts +2 -0
- package/src/proxy/ProxySessionController.ts +544 -6
- package/src/runtime/types.ts +6 -0
- package/src/state/StateStore.ts +11 -0
|
@@ -26,6 +26,7 @@ import type {
|
|
|
26
26
|
ReferencedMessageContent,
|
|
27
27
|
} from "../feishu/types.js";
|
|
28
28
|
import {
|
|
29
|
+
type AppServerReleaseCardView,
|
|
29
30
|
CardRenderer,
|
|
30
31
|
type CardSection,
|
|
31
32
|
type DirectoryBrowserCardEntry,
|
|
@@ -59,6 +60,7 @@ import type {
|
|
|
59
60
|
PermissionMode,
|
|
60
61
|
RemoteCompletedTurnSummary,
|
|
61
62
|
RemoteSessionActivity,
|
|
63
|
+
RemoteSessionPage,
|
|
62
64
|
RemoteSessionSummary,
|
|
63
65
|
RuntimeGoal,
|
|
64
66
|
RuntimeEvent,
|
|
@@ -117,12 +119,40 @@ interface ForwardAttachmentReservation {
|
|
|
117
119
|
registry: Map<string, PendingForwardAttachment>;
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
interface PreparedTopicRootPrompt {
|
|
123
|
+
text: string;
|
|
124
|
+
localImagePaths?: string[];
|
|
125
|
+
displayPrompt: string;
|
|
126
|
+
injectedRootMessageId?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
interface ShellCommandJobMonitor {
|
|
121
130
|
scheduler: CardUpdateScheduler<ShellCommandCardView>;
|
|
122
131
|
timer: NodeJS.Timeout;
|
|
123
132
|
refreshing: boolean;
|
|
124
133
|
}
|
|
125
134
|
|
|
135
|
+
interface AppServerReleaseCardTarget {
|
|
136
|
+
contextKey: string;
|
|
137
|
+
messageId: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface AppServerReleaseSchedule {
|
|
141
|
+
id: number;
|
|
142
|
+
agentName: string;
|
|
143
|
+
runtime: AgentRuntime;
|
|
144
|
+
status: AppServerReleaseCardView["status"];
|
|
145
|
+
blockingTaskCount: number;
|
|
146
|
+
targets: Map<string, AppServerReleaseCardTarget>;
|
|
147
|
+
forced: boolean;
|
|
148
|
+
releaseAt?: number;
|
|
149
|
+
releaseCountdownSeconds?: number;
|
|
150
|
+
releaseStarted?: boolean;
|
|
151
|
+
error?: string;
|
|
152
|
+
timer?: NodeJS.Timeout;
|
|
153
|
+
inFlight?: Promise<void>;
|
|
154
|
+
}
|
|
155
|
+
|
|
126
156
|
interface RequestFailureDetails {
|
|
127
157
|
requestId?: string;
|
|
128
158
|
messageId?: string;
|
|
@@ -146,12 +176,15 @@ const SESSION_PAGE_SIZE = 10;
|
|
|
146
176
|
const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
147
177
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
148
178
|
const RESET_HISTORY_PAGE_SIZE = 10;
|
|
179
|
+
const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
|
|
149
180
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
150
181
|
const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
|
|
151
182
|
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
152
183
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
153
184
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
154
185
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
186
|
+
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
187
|
+
const APP_SERVER_RELEASE_IDLE_DELAY_MS = 5_000;
|
|
155
188
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
156
189
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
157
190
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -285,6 +318,10 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
285
318
|
usage: "[--force]",
|
|
286
319
|
description: "默认安全重启;--force 立即重启",
|
|
287
320
|
},
|
|
321
|
+
{
|
|
322
|
+
command: "/release",
|
|
323
|
+
description: "释放 Agent Bot 占用的 App Server 任务",
|
|
324
|
+
},
|
|
288
325
|
{
|
|
289
326
|
command: "/mute",
|
|
290
327
|
usage: "[on|off]",
|
|
@@ -413,6 +450,13 @@ export interface ControlTaskGroupResult {
|
|
|
413
450
|
task: SessionRecord;
|
|
414
451
|
}
|
|
415
452
|
|
|
453
|
+
export interface ControlTaskReleaseResult {
|
|
454
|
+
agentName: string;
|
|
455
|
+
status: "waiting" | "released";
|
|
456
|
+
blockingTaskCount: number;
|
|
457
|
+
releaseInSeconds?: number;
|
|
458
|
+
}
|
|
459
|
+
|
|
416
460
|
export interface ProxyLifecycle {
|
|
417
461
|
supervised?: boolean;
|
|
418
462
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
@@ -447,6 +491,7 @@ export class ProxySessionController {
|
|
|
447
491
|
private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
|
|
448
492
|
private readonly sessionResets = new Map<string, Promise<void>>();
|
|
449
493
|
private readonly resetHistoryOperations = new Map<string, string>();
|
|
494
|
+
private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
|
|
450
495
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
451
496
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
452
497
|
private readonly llmRetryingSessions = new Set<string>();
|
|
@@ -457,6 +502,8 @@ export class ProxySessionController {
|
|
|
457
502
|
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
458
503
|
private readonly relatedReactionMessageIds = new Map<string, string[]>();
|
|
459
504
|
private readonly shellCommandJobMonitors = new Map<string, ShellCommandJobMonitor>();
|
|
505
|
+
private readonly appServerReleaseSchedules = new Map<string, AppServerReleaseSchedule>();
|
|
506
|
+
private nextAppServerReleaseScheduleId = 1;
|
|
460
507
|
private readonly unsubscribe: Array<() => void> = [];
|
|
461
508
|
private readonly recoveryActivityHeartbeat: NodeJS.Timeout;
|
|
462
509
|
private startupRecovery?: Promise<void>;
|
|
@@ -816,16 +863,37 @@ export class ProxySessionController {
|
|
|
816
863
|
const next = previous.catch(() => undefined).then(() =>
|
|
817
864
|
this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
818
865
|
try {
|
|
819
|
-
|
|
866
|
+
let executableCommand = command;
|
|
867
|
+
let executableImagePaths = localImagePaths;
|
|
868
|
+
let executableMessage = message;
|
|
869
|
+
let injectedRootMessageId: string | undefined;
|
|
870
|
+
if (command.type === "prompt") {
|
|
871
|
+
await this.ensureThreadFork(message);
|
|
872
|
+
const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
|
|
873
|
+
executableCommand = { ...command, text: prepared.text };
|
|
874
|
+
executableImagePaths = prepared.localImagePaths;
|
|
875
|
+
executableMessage = { ...message, displayText: prepared.displayPrompt };
|
|
876
|
+
injectedRootMessageId = prepared.injectedRootMessageId;
|
|
877
|
+
}
|
|
820
878
|
await this.execute(
|
|
821
879
|
message.contextKey,
|
|
822
|
-
|
|
880
|
+
executableCommand,
|
|
823
881
|
message.messageId,
|
|
824
882
|
replyTarget,
|
|
825
|
-
|
|
883
|
+
executableImagePaths,
|
|
826
884
|
message.userId,
|
|
827
|
-
|
|
885
|
+
executableMessage,
|
|
828
886
|
);
|
|
887
|
+
if (injectedRootMessageId) {
|
|
888
|
+
const current = this.currentSession(message.contextKey);
|
|
889
|
+
if (current) {
|
|
890
|
+
this.store.audit(message.contextKey, "topic_root_injected", {
|
|
891
|
+
localSessionId: current.localSessionId,
|
|
892
|
+
rootMessageId: injectedRootMessageId,
|
|
893
|
+
promptMessageId: message.messageId,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
829
897
|
if (!commandDefersReactionFinalization(command)) {
|
|
830
898
|
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
831
899
|
}
|
|
@@ -907,6 +975,10 @@ export class ProxySessionController {
|
|
|
907
975
|
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
908
976
|
}
|
|
909
977
|
await this.forkSessionReference(contextKey, sessionId);
|
|
978
|
+
} else if (kind === "app_server_release_now") {
|
|
979
|
+
await this.releaseAppServerNow(scopedAction);
|
|
980
|
+
} else if (kind === "app_server_release_cancel") {
|
|
981
|
+
await this.cancelAppServerRelease(scopedAction);
|
|
910
982
|
} else if (kind === "turn_reset") {
|
|
911
983
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
912
984
|
const turnId = String(scopedAction.value.turnId ?? "");
|
|
@@ -1177,6 +1249,7 @@ export class ProxySessionController {
|
|
|
1177
1249
|
this.queuedPromptCardWrites.clear();
|
|
1178
1250
|
this.sessionResets.clear();
|
|
1179
1251
|
this.resetHistoryOperations.clear();
|
|
1252
|
+
this.remoteTurnHistoryRefreshes.clear();
|
|
1180
1253
|
for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
|
|
1181
1254
|
this.pendingMergedForwards.clear();
|
|
1182
1255
|
for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
|
|
@@ -1186,6 +1259,10 @@ export class ProxySessionController {
|
|
|
1186
1259
|
this.queuedPromptRetryTimers.clear();
|
|
1187
1260
|
for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
|
|
1188
1261
|
this.recoveryRetryTimers.clear();
|
|
1262
|
+
for (const schedule of this.appServerReleaseSchedules.values()) {
|
|
1263
|
+
if (schedule.timer) clearTimeout(schedule.timer);
|
|
1264
|
+
}
|
|
1265
|
+
this.appServerReleaseSchedules.clear();
|
|
1189
1266
|
for (const [jobId] of this.shellCommandJobMonitors) this.stopShellCommandJobMonitor(jobId);
|
|
1190
1267
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
1191
1268
|
}
|
|
@@ -1197,6 +1274,23 @@ export class ProxySessionController {
|
|
|
1197
1274
|
return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
|
|
1198
1275
|
}
|
|
1199
1276
|
|
|
1277
|
+
async controlReleaseTask(localSessionId: string): Promise<ControlTaskReleaseResult> {
|
|
1278
|
+
const record = this.requireControlSession(localSessionId);
|
|
1279
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
1280
|
+
const result = await this.outbound.withReplyTarget(
|
|
1281
|
+
contextKey,
|
|
1282
|
+
this.controlSessionReplyTarget(record),
|
|
1283
|
+
() => this.requestAppServerRelease(contextKey, record),
|
|
1284
|
+
);
|
|
1285
|
+
if (result.status === "failed") throw new Error(result.error ?? "Failed to release App Server tasks.");
|
|
1286
|
+
return {
|
|
1287
|
+
agentName: result.agentName,
|
|
1288
|
+
status: result.status,
|
|
1289
|
+
blockingTaskCount: result.blockingTaskCount,
|
|
1290
|
+
...(result.releaseInSeconds !== undefined ? { releaseInSeconds: result.releaseInSeconds } : {}),
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1200
1294
|
async controlArchiveTask(localSessionId: string): Promise<{
|
|
1201
1295
|
localSessionId: string;
|
|
1202
1296
|
remoteSessionId: string;
|
|
@@ -1935,6 +2029,9 @@ export class ProxySessionController {
|
|
|
1935
2029
|
if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
|
|
1936
2030
|
await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
|
|
1937
2031
|
return;
|
|
2032
|
+
case "release":
|
|
2033
|
+
await this.requestAppServerRelease(contextKey);
|
|
2034
|
+
return;
|
|
1938
2035
|
case "mute":
|
|
1939
2036
|
await this.setGroupMute(contextKey, command.enabled);
|
|
1940
2037
|
return;
|
|
@@ -2740,6 +2837,61 @@ export class ProxySessionController {
|
|
|
2740
2837
|
}
|
|
2741
2838
|
}
|
|
2742
2839
|
|
|
2840
|
+
private async prepareTopicRootPrompt(
|
|
2841
|
+
message: IncomingMessage,
|
|
2842
|
+
text: string,
|
|
2843
|
+
localImagePaths?: string[],
|
|
2844
|
+
): Promise<PreparedTopicRootPrompt> {
|
|
2845
|
+
const displayPrompt = message.displayText ?? text;
|
|
2846
|
+
const rootMessageId = topicRootMessageId(message);
|
|
2847
|
+
if (!rootMessageId) return { text, localImagePaths, displayPrompt };
|
|
2848
|
+
|
|
2849
|
+
const current = this.currentSession(message.contextKey);
|
|
2850
|
+
if (current && (
|
|
2851
|
+
this.store.hasInjectedTopicRoot(current.localSessionId, rootMessageId)
|
|
2852
|
+
|| this.taskHistoryContainsMessage(current, rootMessageId)
|
|
2853
|
+
)) {
|
|
2854
|
+
return { text, localImagePaths, displayPrompt };
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
try {
|
|
2858
|
+
const root = await this.resolveReferencedMessage(message.contextKey, rootMessageId);
|
|
2859
|
+
const rootImagePaths = await Promise.all(root.images.map((image) =>
|
|
2860
|
+
this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
|
|
2861
|
+
let promptText = topicRootMessagePrompt(text, root.text);
|
|
2862
|
+
if (root.files.length > 0) {
|
|
2863
|
+
const downloadedFiles = await Promise.all(root.files.map(async (file) => ({
|
|
2864
|
+
fileName: file.fileName,
|
|
2865
|
+
filePath: await this.outbound.downloadFile(
|
|
2866
|
+
message.contextKey,
|
|
2867
|
+
file.messageId,
|
|
2868
|
+
file.fileKey,
|
|
2869
|
+
file.fileName,
|
|
2870
|
+
),
|
|
2871
|
+
})));
|
|
2872
|
+
promptText = appendDownloadedFiles(promptText, downloadedFiles);
|
|
2873
|
+
}
|
|
2874
|
+
const combinedImagePaths = [...new Set([...(localImagePaths ?? []), ...rootImagePaths])];
|
|
2875
|
+
return {
|
|
2876
|
+
text: promptText,
|
|
2877
|
+
...(combinedImagePaths.length > 0 ? { localImagePaths: combinedImagePaths } : {}),
|
|
2878
|
+
displayPrompt,
|
|
2879
|
+
injectedRootMessageId: rootMessageId,
|
|
2880
|
+
};
|
|
2881
|
+
} catch (error) {
|
|
2882
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2883
|
+
throw new Error(`无法读取话题根消息:${detail}`);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
private taskHistoryContainsMessage(record: SessionRecord, messageId: string): boolean {
|
|
2888
|
+
const anchor = this.store.findTurnAnchorByMessageId(messageId);
|
|
2889
|
+
if (!anchor) return false;
|
|
2890
|
+
if (record.lastTurnId === anchor.turnId) return true;
|
|
2891
|
+
return this.store.listTaskTurnGraph(record.localSessionId)
|
|
2892
|
+
.some((turn) => turn.turnId === anchor.turnId);
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2743
2895
|
private async forkThreadSession(message: IncomingMessage): Promise<void> {
|
|
2744
2896
|
const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
|
|
2745
2897
|
const agent = this.ensureAgent(source.agentName);
|
|
@@ -3335,6 +3487,39 @@ export class ProxySessionController {
|
|
|
3335
3487
|
}
|
|
3336
3488
|
}
|
|
3337
3489
|
|
|
3490
|
+
private async hydrateRemoteTurnHistory(
|
|
3491
|
+
contextKey: string,
|
|
3492
|
+
current: SessionRecord,
|
|
3493
|
+
): Promise<void> {
|
|
3494
|
+
if (!current.remoteSessionId) return;
|
|
3495
|
+
const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
|
|
3496
|
+
if (refreshedAt !== undefined && Date.now() - refreshedAt < REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS) return;
|
|
3497
|
+
const forkSource = this.store.getForkHistorySource(current.localSessionId);
|
|
3498
|
+
if (forkSource?.sourceTurnId === current.lastTurnId
|
|
3499
|
+
&& this.store.hasImportedForkTurnHistory(current.localSessionId)) return;
|
|
3500
|
+
const runtime = this.runtimes.forAgent(current.agentName);
|
|
3501
|
+
if (runtime.kind !== "codex" || !runtime.readRemoteSession) return;
|
|
3502
|
+
try {
|
|
3503
|
+
const remote = await runtime.readRemoteSession(current.remoteSessionId);
|
|
3504
|
+
this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
|
|
3505
|
+
const completedTurns = remote.completedTurns ?? [];
|
|
3506
|
+
if (completedTurns.length === 0) return;
|
|
3507
|
+
this.persistCompletedTurnHistory({
|
|
3508
|
+
localSessionId: current.localSessionId,
|
|
3509
|
+
contextKey,
|
|
3510
|
+
agentName: current.agentName,
|
|
3511
|
+
remoteSessionId: current.remoteSessionId,
|
|
3512
|
+
completedTurns,
|
|
3513
|
+
fallbackStart: parseIsoTimestamp(current.createdAt) ?? Date.now() - completedTurns.length,
|
|
3514
|
+
});
|
|
3515
|
+
} catch (error) {
|
|
3516
|
+
this.logger.warn(
|
|
3517
|
+
{ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId },
|
|
3518
|
+
"Failed to hydrate App Server Turn history; rendering the locally available history.",
|
|
3519
|
+
);
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3338
3523
|
private async createProjectSessionFromReference(contextKey: string, reference: string): Promise<void> {
|
|
3339
3524
|
const resolved = await this.resolveProjectSessionReference(contextKey, reference);
|
|
3340
3525
|
const created = await this.createSession(
|
|
@@ -5417,6 +5602,7 @@ export class ProxySessionController {
|
|
|
5417
5602
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
5418
5603
|
}
|
|
5419
5604
|
await this.hydrateLegacyForkTurnHistory(contextKey, current);
|
|
5605
|
+
await this.hydrateRemoteTurnHistory(contextKey, current);
|
|
5420
5606
|
const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
|
|
5421
5607
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
5422
5608
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
@@ -5751,7 +5937,7 @@ export class ProxySessionController {
|
|
|
5751
5937
|
await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
|
|
5752
5938
|
if (!runtime.listRemoteSessions) return;
|
|
5753
5939
|
try {
|
|
5754
|
-
const result = await
|
|
5940
|
+
const result = await this.listRemoteSessionsThroughCursor(runtime, searchTerm, fetchLimit);
|
|
5755
5941
|
remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
|
|
5756
5942
|
remoteHasMore ||= Boolean(result.nextCursor);
|
|
5757
5943
|
} catch (error) {
|
|
@@ -5963,6 +6149,42 @@ export class ProxySessionController {
|
|
|
5963
6149
|
}
|
|
5964
6150
|
}
|
|
5965
6151
|
|
|
6152
|
+
private async listRemoteSessionsThroughCursor(
|
|
6153
|
+
runtime: AgentRuntime,
|
|
6154
|
+
searchTerm: string | undefined,
|
|
6155
|
+
limit: number,
|
|
6156
|
+
): Promise<RemoteSessionPage> {
|
|
6157
|
+
const sessions: RemoteSessionSummary[] = [];
|
|
6158
|
+
const seenSessionIds = new Set<string>();
|
|
6159
|
+
const seenCursors = new Set<string>();
|
|
6160
|
+
let cursor: string | undefined;
|
|
6161
|
+
|
|
6162
|
+
while (sessions.length < limit) {
|
|
6163
|
+
const page = await runtime.listRemoteSessions!({
|
|
6164
|
+
searchTerm,
|
|
6165
|
+
...(cursor ? { cursor } : {}),
|
|
6166
|
+
limit: limit - sessions.length,
|
|
6167
|
+
});
|
|
6168
|
+
for (const session of page.sessions) {
|
|
6169
|
+
if (seenSessionIds.has(session.id)) continue;
|
|
6170
|
+
seenSessionIds.add(session.id);
|
|
6171
|
+
sessions.push(session);
|
|
6172
|
+
}
|
|
6173
|
+
|
|
6174
|
+
const nextCursor = page.nextCursor?.trim() || undefined;
|
|
6175
|
+
if (!nextCursor) return { sessions };
|
|
6176
|
+
if (sessions.length >= limit) return { sessions, nextCursor };
|
|
6177
|
+
if (seenCursors.has(nextCursor)) {
|
|
6178
|
+
this.logger.warn({ cursor: nextCursor }, "App Server repeated a thread/list cursor; stopping pagination.");
|
|
6179
|
+
return { sessions };
|
|
6180
|
+
}
|
|
6181
|
+
seenCursors.add(nextCursor);
|
|
6182
|
+
cursor = nextCursor;
|
|
6183
|
+
}
|
|
6184
|
+
|
|
6185
|
+
return { sessions };
|
|
6186
|
+
}
|
|
6187
|
+
|
|
5966
6188
|
private async refreshSessionsCardFromAction(
|
|
5967
6189
|
action: CardAction,
|
|
5968
6190
|
options: { forceSwitchTaskId?: string; page?: number } = {},
|
|
@@ -6739,6 +6961,311 @@ export class ProxySessionController {
|
|
|
6739
6961
|
return record;
|
|
6740
6962
|
}
|
|
6741
6963
|
|
|
6964
|
+
private async requestAppServerRelease(
|
|
6965
|
+
contextKey: string,
|
|
6966
|
+
requestedRecord?: SessionRecord,
|
|
6967
|
+
): Promise<{
|
|
6968
|
+
agentName: string;
|
|
6969
|
+
status: "waiting" | "released" | "failed";
|
|
6970
|
+
blockingTaskCount: number;
|
|
6971
|
+
releaseInSeconds?: number;
|
|
6972
|
+
error?: string;
|
|
6973
|
+
}> {
|
|
6974
|
+
const record = requestedRecord ?? this.requireCurrentSession(contextKey);
|
|
6975
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
6976
|
+
if (!this.isCodexSession(record) || runtime.kind !== "codex" || !runtime.release) {
|
|
6977
|
+
throw new Error("当前 Agent 不使用可释放的 App Server。");
|
|
6978
|
+
}
|
|
6979
|
+
|
|
6980
|
+
let schedule = this.appServerReleaseSchedules.get(record.agentName);
|
|
6981
|
+
const created = !schedule;
|
|
6982
|
+
if (!schedule) {
|
|
6983
|
+
const blockingTaskCount = this.appServerReleaseBlockers(record.agentName, runtime).length;
|
|
6984
|
+
schedule = {
|
|
6985
|
+
id: this.nextAppServerReleaseScheduleId++,
|
|
6986
|
+
agentName: record.agentName,
|
|
6987
|
+
runtime,
|
|
6988
|
+
status: "waiting",
|
|
6989
|
+
blockingTaskCount,
|
|
6990
|
+
targets: new Map(),
|
|
6991
|
+
forced: false,
|
|
6992
|
+
...(blockingTaskCount === 0
|
|
6993
|
+
? {
|
|
6994
|
+
releaseCountdownSeconds: Math.ceil(APP_SERVER_RELEASE_IDLE_DELAY_MS / 1_000),
|
|
6995
|
+
}
|
|
6996
|
+
: {}),
|
|
6997
|
+
};
|
|
6998
|
+
this.appServerReleaseSchedules.set(record.agentName, schedule);
|
|
6999
|
+
}
|
|
7000
|
+
|
|
7001
|
+
try {
|
|
7002
|
+
const messageId = await this.outbound.sendInteractiveCard(
|
|
7003
|
+
contextKey,
|
|
7004
|
+
this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, contextKey)),
|
|
7005
|
+
);
|
|
7006
|
+
if (messageId) {
|
|
7007
|
+
schedule.targets.set(`${contextKey}\u0000${messageId}`, { contextKey, messageId });
|
|
7008
|
+
}
|
|
7009
|
+
if (
|
|
7010
|
+
schedule.status === "waiting"
|
|
7011
|
+
&& schedule.blockingTaskCount === 0
|
|
7012
|
+
&& schedule.releaseCountdownSeconds !== undefined
|
|
7013
|
+
&& schedule.releaseAt === undefined
|
|
7014
|
+
) {
|
|
7015
|
+
schedule.releaseAt = Date.now() + APP_SERVER_RELEASE_IDLE_DELAY_MS;
|
|
7016
|
+
}
|
|
7017
|
+
} catch (error) {
|
|
7018
|
+
if (created && schedule.targets.size === 0) {
|
|
7019
|
+
this.clearAppServerReleaseSchedule(schedule);
|
|
7020
|
+
}
|
|
7021
|
+
throw error;
|
|
7022
|
+
}
|
|
7023
|
+
|
|
7024
|
+
this.armAppServerReleaseCheck(schedule);
|
|
7025
|
+
return {
|
|
7026
|
+
agentName: schedule.agentName,
|
|
7027
|
+
status: schedule.status === "failed" ? "failed" : schedule.status === "released" ? "released" : "waiting",
|
|
7028
|
+
blockingTaskCount: schedule.blockingTaskCount,
|
|
7029
|
+
...(schedule.releaseCountdownSeconds !== undefined
|
|
7030
|
+
? { releaseInSeconds: schedule.releaseCountdownSeconds }
|
|
7031
|
+
: {}),
|
|
7032
|
+
...(schedule.error ? { error: schedule.error } : {}),
|
|
7033
|
+
};
|
|
7034
|
+
}
|
|
7035
|
+
|
|
7036
|
+
private async releaseAppServerNow(action: CardAction): Promise<void> {
|
|
7037
|
+
const scheduleId = Number(action.value.scheduleId);
|
|
7038
|
+
const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
|
|
7039
|
+
const schedule = this.appServerReleaseSchedules.get(agentName);
|
|
7040
|
+
if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || schedule.status !== "waiting") {
|
|
7041
|
+
throw new Error("释放卡片已失效,请重新发送 /release。");
|
|
7042
|
+
}
|
|
7043
|
+
await this.performAppServerRelease(schedule, true);
|
|
7044
|
+
}
|
|
7045
|
+
|
|
7046
|
+
private async cancelAppServerRelease(action: CardAction): Promise<void> {
|
|
7047
|
+
const scheduleId = Number(action.value.scheduleId);
|
|
7048
|
+
const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
|
|
7049
|
+
const schedule = this.appServerReleaseSchedules.get(agentName);
|
|
7050
|
+
const cancellable = schedule?.status === "waiting"
|
|
7051
|
+
|| (schedule?.status === "releasing" && schedule.releaseStarted !== true);
|
|
7052
|
+
if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || !cancellable) {
|
|
7053
|
+
throw new Error("释放卡片已失效,请重新发送 /release。");
|
|
7054
|
+
}
|
|
7055
|
+
const pendingRelease = schedule.inFlight;
|
|
7056
|
+
schedule.status = "cancelled";
|
|
7057
|
+
if (schedule.timer) {
|
|
7058
|
+
clearTimeout(schedule.timer);
|
|
7059
|
+
schedule.timer = undefined;
|
|
7060
|
+
}
|
|
7061
|
+
if (pendingRelease) await pendingRelease;
|
|
7062
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7063
|
+
this.clearAppServerReleaseSchedule(schedule);
|
|
7064
|
+
}
|
|
7065
|
+
|
|
7066
|
+
private appServerReleaseBlockers(agentName: string, runtime: AgentRuntime): SessionRecord[] {
|
|
7067
|
+
return this.store.listAllSessions().filter((session) => {
|
|
7068
|
+
if (session.agentName !== agentName || session.status === "closed") return false;
|
|
7069
|
+
return session.status === "starting"
|
|
7070
|
+
|| session.status === "running"
|
|
7071
|
+
|| this.store.countQueuedPrompts(session.localSessionId) > 0
|
|
7072
|
+
|| this.queuedPromptStarts.has(session.localSessionId)
|
|
7073
|
+
|| this.sessionLoads.has(session.localSessionId)
|
|
7074
|
+
|| Boolean(runtime.getSession(session.localSessionId)?.activeTurnId);
|
|
7075
|
+
});
|
|
7076
|
+
}
|
|
7077
|
+
|
|
7078
|
+
private armAppServerReleaseCheck(schedule: AppServerReleaseSchedule): void {
|
|
7079
|
+
if (schedule.status !== "waiting" || schedule.timer || schedule.inFlight) return;
|
|
7080
|
+
schedule.timer = setTimeout(() => {
|
|
7081
|
+
schedule.timer = undefined;
|
|
7082
|
+
void this.refreshAppServerReleaseSchedule(schedule).catch((error: unknown) => {
|
|
7083
|
+
this.logger.warn({ error, agentName: schedule.agentName }, "Failed to refresh App Server release status.");
|
|
7084
|
+
this.armAppServerReleaseCheck(schedule);
|
|
7085
|
+
});
|
|
7086
|
+
}, APP_SERVER_RELEASE_POLL_INTERVAL_MS);
|
|
7087
|
+
schedule.timer.unref?.();
|
|
7088
|
+
}
|
|
7089
|
+
|
|
7090
|
+
private async refreshAppServerReleaseSchedule(schedule: AppServerReleaseSchedule): Promise<void> {
|
|
7091
|
+
if (this.appServerReleaseSchedules.get(schedule.agentName) !== schedule || schedule.status !== "waiting") return;
|
|
7092
|
+
const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
|
|
7093
|
+
if (blockingTaskCount > 0) {
|
|
7094
|
+
const changed = schedule.blockingTaskCount !== blockingTaskCount || schedule.releaseAt !== undefined;
|
|
7095
|
+
schedule.releaseAt = undefined;
|
|
7096
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
7097
|
+
if (changed) {
|
|
7098
|
+
schedule.blockingTaskCount = blockingTaskCount;
|
|
7099
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7100
|
+
}
|
|
7101
|
+
this.armAppServerReleaseCheck(schedule);
|
|
7102
|
+
return;
|
|
7103
|
+
}
|
|
7104
|
+
schedule.blockingTaskCount = 0;
|
|
7105
|
+
if (schedule.releaseAt !== undefined) {
|
|
7106
|
+
const releaseCountdownSeconds = Math.max(0, Math.ceil((schedule.releaseAt - Date.now()) / 1_000));
|
|
7107
|
+
if (releaseCountdownSeconds > 0) {
|
|
7108
|
+
if (schedule.releaseCountdownSeconds !== releaseCountdownSeconds) {
|
|
7109
|
+
schedule.releaseCountdownSeconds = releaseCountdownSeconds;
|
|
7110
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7111
|
+
}
|
|
7112
|
+
this.armAppServerReleaseCheck(schedule);
|
|
7113
|
+
return;
|
|
7114
|
+
}
|
|
7115
|
+
}
|
|
7116
|
+
await this.performAppServerRelease(schedule, false);
|
|
7117
|
+
}
|
|
7118
|
+
|
|
7119
|
+
private async performAppServerRelease(schedule: AppServerReleaseSchedule, force: boolean): Promise<void> {
|
|
7120
|
+
if (schedule.inFlight) return schedule.inFlight;
|
|
7121
|
+
const operation = this.performAppServerReleaseNow(schedule, force);
|
|
7122
|
+
schedule.inFlight = operation;
|
|
7123
|
+
try {
|
|
7124
|
+
await operation;
|
|
7125
|
+
} finally {
|
|
7126
|
+
if (schedule.inFlight === operation) schedule.inFlight = undefined;
|
|
7127
|
+
if (schedule.status === "waiting") this.armAppServerReleaseCheck(schedule);
|
|
7128
|
+
}
|
|
7129
|
+
}
|
|
7130
|
+
|
|
7131
|
+
private async performAppServerReleaseNow(schedule: AppServerReleaseSchedule, force: boolean): Promise<void> {
|
|
7132
|
+
if (schedule.timer) {
|
|
7133
|
+
clearTimeout(schedule.timer);
|
|
7134
|
+
schedule.timer = undefined;
|
|
7135
|
+
}
|
|
7136
|
+
const interruptedWork = force && this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length > 0;
|
|
7137
|
+
if (force) await this.cancelQueuedPromptsForAppServerRelease(schedule.agentName);
|
|
7138
|
+
else {
|
|
7139
|
+
const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
|
|
7140
|
+
if (blockingTaskCount > 0) {
|
|
7141
|
+
schedule.status = "waiting";
|
|
7142
|
+
schedule.blockingTaskCount = blockingTaskCount;
|
|
7143
|
+
schedule.releaseAt = undefined;
|
|
7144
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
7145
|
+
schedule.releaseStarted = false;
|
|
7146
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7147
|
+
this.armAppServerReleaseCheck(schedule);
|
|
7148
|
+
return;
|
|
7149
|
+
}
|
|
7150
|
+
}
|
|
7151
|
+
|
|
7152
|
+
schedule.status = "releasing";
|
|
7153
|
+
schedule.blockingTaskCount = 0;
|
|
7154
|
+
schedule.releaseAt = undefined;
|
|
7155
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
7156
|
+
schedule.releaseStarted = false;
|
|
7157
|
+
schedule.forced = interruptedWork;
|
|
7158
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7159
|
+
if (
|
|
7160
|
+
this.appServerReleaseWasCancelled(schedule)
|
|
7161
|
+
|| this.appServerReleaseSchedules.get(schedule.agentName) !== schedule
|
|
7162
|
+
) return;
|
|
7163
|
+
schedule.releaseStarted = true;
|
|
7164
|
+
try {
|
|
7165
|
+
const result = await schedule.runtime.release?.({ force });
|
|
7166
|
+
if (!result) throw new Error("当前 Agent 不支持释放 App Server。");
|
|
7167
|
+
if (result.status === "busy") {
|
|
7168
|
+
schedule.status = "waiting";
|
|
7169
|
+
schedule.blockingTaskCount = Math.max(
|
|
7170
|
+
result.activeSessionIds?.length ?? 0,
|
|
7171
|
+
this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length,
|
|
7172
|
+
1,
|
|
7173
|
+
);
|
|
7174
|
+
schedule.forced = false;
|
|
7175
|
+
schedule.releaseAt = undefined;
|
|
7176
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
7177
|
+
schedule.releaseStarted = false;
|
|
7178
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7179
|
+
this.armAppServerReleaseCheck(schedule);
|
|
7180
|
+
return;
|
|
7181
|
+
}
|
|
7182
|
+
schedule.status = "released";
|
|
7183
|
+
} catch (error) {
|
|
7184
|
+
schedule.status = "failed";
|
|
7185
|
+
schedule.error = error instanceof Error ? error.message : String(error);
|
|
7186
|
+
this.logger.warn({ error, agentName: schedule.agentName }, "Failed to release App Server tasks.");
|
|
7187
|
+
}
|
|
7188
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
7189
|
+
this.clearAppServerReleaseSchedule(schedule);
|
|
7190
|
+
}
|
|
7191
|
+
|
|
7192
|
+
private appServerReleaseWasCancelled(schedule: AppServerReleaseSchedule): boolean {
|
|
7193
|
+
return schedule.status === "cancelled";
|
|
7194
|
+
}
|
|
7195
|
+
|
|
7196
|
+
private async cancelQueuedPromptsForAppServerRelease(agentName: string): Promise<void> {
|
|
7197
|
+
for (const session of this.store.listAllSessions()) {
|
|
7198
|
+
if (session.agentName !== agentName) continue;
|
|
7199
|
+
const retryTimer = this.queuedPromptRetryTimers.get(session.localSessionId);
|
|
7200
|
+
if (retryTimer) {
|
|
7201
|
+
clearTimeout(retryTimer);
|
|
7202
|
+
this.queuedPromptRetryTimers.delete(session.localSessionId);
|
|
7203
|
+
}
|
|
7204
|
+
const queued = this.store.listQueuedPrompts(session.localSessionId);
|
|
7205
|
+
for (const prompt of queued) {
|
|
7206
|
+
const cancelled = this.store.cancelQueuedPrompt(prompt.promptId, session.localSessionId);
|
|
7207
|
+
if (!cancelled?.messageId) continue;
|
|
7208
|
+
await this.finalizeStandaloneMessageReaction(cancelled.messageId, "cancelled").catch((error: unknown) => {
|
|
7209
|
+
this.logger.warn(
|
|
7210
|
+
{ error, agentName, sessionId: session.localSessionId, promptId: cancelled.promptId },
|
|
7211
|
+
"Failed to finalize a queued Prompt reaction during App Server release.",
|
|
7212
|
+
);
|
|
7213
|
+
});
|
|
7214
|
+
}
|
|
7215
|
+
if (queued.length > 0) {
|
|
7216
|
+
await this.refreshPromptQueueCards(session.localSessionId).catch((error: unknown) => {
|
|
7217
|
+
this.logger.warn(
|
|
7218
|
+
{ error, agentName, sessionId: session.localSessionId },
|
|
7219
|
+
"Failed to refresh queued Prompt cards during App Server release.",
|
|
7220
|
+
);
|
|
7221
|
+
});
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
7225
|
+
|
|
7226
|
+
private appServerReleaseCardView(
|
|
7227
|
+
schedule: AppServerReleaseSchedule,
|
|
7228
|
+
contextKey: string,
|
|
7229
|
+
): AppServerReleaseCardView {
|
|
7230
|
+
return {
|
|
7231
|
+
status: schedule.status,
|
|
7232
|
+
contextKey,
|
|
7233
|
+
scheduleId: schedule.id,
|
|
7234
|
+
agentName: schedule.agentName,
|
|
7235
|
+
...(schedule.blockingTaskCount > 0 ? { blockingTaskCount: schedule.blockingTaskCount } : {}),
|
|
7236
|
+
...(schedule.releaseCountdownSeconds !== undefined
|
|
7237
|
+
? { releaseInSeconds: schedule.releaseCountdownSeconds }
|
|
7238
|
+
: {}),
|
|
7239
|
+
...(schedule.forced ? { forced: true } : {}),
|
|
7240
|
+
...(schedule.error ? { error: schedule.error } : {}),
|
|
7241
|
+
};
|
|
7242
|
+
}
|
|
7243
|
+
|
|
7244
|
+
private async publishAppServerReleaseSchedule(schedule: AppServerReleaseSchedule): Promise<void> {
|
|
7245
|
+
await Promise.all([...schedule.targets.values()].map(async (target) => {
|
|
7246
|
+
try {
|
|
7247
|
+
await this.outbound.updateInteractiveCard(
|
|
7248
|
+
target.contextKey,
|
|
7249
|
+
target.messageId,
|
|
7250
|
+
this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, target.contextKey)),
|
|
7251
|
+
);
|
|
7252
|
+
} catch (error) {
|
|
7253
|
+
this.logger.warn(
|
|
7254
|
+
{ error, agentName: schedule.agentName, messageId: target.messageId },
|
|
7255
|
+
"Failed to update App Server release card.",
|
|
7256
|
+
);
|
|
7257
|
+
}
|
|
7258
|
+
}));
|
|
7259
|
+
}
|
|
7260
|
+
|
|
7261
|
+
private clearAppServerReleaseSchedule(schedule: AppServerReleaseSchedule): void {
|
|
7262
|
+
if (schedule.timer) clearTimeout(schedule.timer);
|
|
7263
|
+
schedule.timer = undefined;
|
|
7264
|
+
if (this.appServerReleaseSchedules.get(schedule.agentName) === schedule) {
|
|
7265
|
+
this.appServerReleaseSchedules.delete(schedule.agentName);
|
|
7266
|
+
}
|
|
7267
|
+
}
|
|
7268
|
+
|
|
6742
7269
|
private async presentThreadWriterConflict(
|
|
6743
7270
|
record: SessionRecord,
|
|
6744
7271
|
contextKey: string,
|
|
@@ -7629,7 +8156,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
|
|
|
7629
8156
|
}
|
|
7630
8157
|
|
|
7631
8158
|
function isQueueIndependentCommand(command: Command): boolean {
|
|
7632
|
-
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
|
|
8159
|
+
if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type)) return true;
|
|
7633
8160
|
if (command.type === "agent") return command.agent === undefined;
|
|
7634
8161
|
if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
|
|
7635
8162
|
return false;
|
|
@@ -7858,6 +8385,17 @@ function referencedMessagePrompt(instruction: string, referencedMessage: string)
|
|
|
7858
8385
|
return `${instruction}\n\n引用消息:\n${referencedMessage}`;
|
|
7859
8386
|
}
|
|
7860
8387
|
|
|
8388
|
+
function topicRootMessagePrompt(instruction: string, rootMessage: string): string {
|
|
8389
|
+
return `${instruction}\n\n话题根消息:\n${rootMessage}`;
|
|
8390
|
+
}
|
|
8391
|
+
|
|
8392
|
+
function topicRootMessageId(message: IncomingMessage): string | undefined {
|
|
8393
|
+
if (!message.threadContext) return undefined;
|
|
8394
|
+
const rootMessageId = message.rootMessageId?.trim();
|
|
8395
|
+
if (!rootMessageId || rootMessageId === message.messageId) return undefined;
|
|
8396
|
+
return rootMessageId;
|
|
8397
|
+
}
|
|
8398
|
+
|
|
7861
8399
|
function quotedMessageId(message: IncomingMessage): string | undefined {
|
|
7862
8400
|
const messageId = message.parentMessageId?.trim();
|
|
7863
8401
|
if (!messageId || message.contextKey.startsWith("console:")) return undefined;
|