@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
|
@@ -38,12 +38,15 @@ const SESSION_PAGE_SIZE = 10;
|
|
|
38
38
|
const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
39
39
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
40
40
|
const RESET_HISTORY_PAGE_SIZE = 10;
|
|
41
|
+
const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
|
|
41
42
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
42
43
|
const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
|
|
43
44
|
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
44
45
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
45
46
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
46
47
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
48
|
+
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
49
|
+
const APP_SERVER_RELEASE_IDLE_DELAY_MS = 5_000;
|
|
47
50
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
48
51
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
49
52
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -166,6 +169,10 @@ const HELP_COMMAND_SECTIONS = [
|
|
|
166
169
|
usage: "[--force]",
|
|
167
170
|
description: "默认安全重启;--force 立即重启",
|
|
168
171
|
},
|
|
172
|
+
{
|
|
173
|
+
command: "/release",
|
|
174
|
+
description: "释放 Agent Bot 占用的 App Server 任务",
|
|
175
|
+
},
|
|
169
176
|
{
|
|
170
177
|
command: "/mute",
|
|
171
178
|
usage: "[on|off]",
|
|
@@ -198,6 +205,7 @@ export class ProxySessionController {
|
|
|
198
205
|
queuedPromptCardWrites = new Map();
|
|
199
206
|
sessionResets = new Map();
|
|
200
207
|
resetHistoryOperations = new Map();
|
|
208
|
+
remoteTurnHistoryRefreshes = new Map();
|
|
201
209
|
lastSessionListings = new Map();
|
|
202
210
|
threadInitializations = new Map();
|
|
203
211
|
llmRetryingSessions = new Set();
|
|
@@ -208,6 +216,8 @@ export class ProxySessionController {
|
|
|
208
216
|
pendingResourceForwards = new Map();
|
|
209
217
|
relatedReactionMessageIds = new Map();
|
|
210
218
|
shellCommandJobMonitors = new Map();
|
|
219
|
+
appServerReleaseSchedules = new Map();
|
|
220
|
+
nextAppServerReleaseScheduleId = 1;
|
|
211
221
|
unsubscribe = [];
|
|
212
222
|
recoveryActivityHeartbeat;
|
|
213
223
|
startupRecovery;
|
|
@@ -495,9 +505,29 @@ export class ProxySessionController {
|
|
|
495
505
|
const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
|
|
496
506
|
const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
497
507
|
try {
|
|
498
|
-
|
|
508
|
+
let executableCommand = command;
|
|
509
|
+
let executableImagePaths = localImagePaths;
|
|
510
|
+
let executableMessage = message;
|
|
511
|
+
let injectedRootMessageId;
|
|
512
|
+
if (command.type === "prompt") {
|
|
499
513
|
await this.ensureThreadFork(message);
|
|
500
|
-
|
|
514
|
+
const prepared = await this.prepareTopicRootPrompt(message, command.text, localImagePaths);
|
|
515
|
+
executableCommand = { ...command, text: prepared.text };
|
|
516
|
+
executableImagePaths = prepared.localImagePaths;
|
|
517
|
+
executableMessage = { ...message, displayText: prepared.displayPrompt };
|
|
518
|
+
injectedRootMessageId = prepared.injectedRootMessageId;
|
|
519
|
+
}
|
|
520
|
+
await this.execute(message.contextKey, executableCommand, message.messageId, replyTarget, executableImagePaths, message.userId, executableMessage);
|
|
521
|
+
if (injectedRootMessageId) {
|
|
522
|
+
const current = this.currentSession(message.contextKey);
|
|
523
|
+
if (current) {
|
|
524
|
+
this.store.audit(message.contextKey, "topic_root_injected", {
|
|
525
|
+
localSessionId: current.localSessionId,
|
|
526
|
+
rootMessageId: injectedRootMessageId,
|
|
527
|
+
promptMessageId: message.messageId,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
501
531
|
if (!commandDefersReactionFinalization(command)) {
|
|
502
532
|
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
503
533
|
}
|
|
@@ -576,6 +606,12 @@ export class ProxySessionController {
|
|
|
576
606
|
}
|
|
577
607
|
await this.forkSessionReference(contextKey, sessionId);
|
|
578
608
|
}
|
|
609
|
+
else if (kind === "app_server_release_now") {
|
|
610
|
+
await this.releaseAppServerNow(scopedAction);
|
|
611
|
+
}
|
|
612
|
+
else if (kind === "app_server_release_cancel") {
|
|
613
|
+
await this.cancelAppServerRelease(scopedAction);
|
|
614
|
+
}
|
|
579
615
|
else if (kind === "turn_reset") {
|
|
580
616
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
581
617
|
const turnId = String(scopedAction.value.turnId ?? "");
|
|
@@ -831,6 +867,7 @@ export class ProxySessionController {
|
|
|
831
867
|
this.queuedPromptCardWrites.clear();
|
|
832
868
|
this.sessionResets.clear();
|
|
833
869
|
this.resetHistoryOperations.clear();
|
|
870
|
+
this.remoteTurnHistoryRefreshes.clear();
|
|
834
871
|
for (const pending of this.pendingMergedForwards.values())
|
|
835
872
|
pending.resolveAttachment(undefined);
|
|
836
873
|
this.pendingMergedForwards.clear();
|
|
@@ -844,6 +881,11 @@ export class ProxySessionController {
|
|
|
844
881
|
for (const timer of this.recoveryRetryTimers.values())
|
|
845
882
|
clearTimeout(timer);
|
|
846
883
|
this.recoveryRetryTimers.clear();
|
|
884
|
+
for (const schedule of this.appServerReleaseSchedules.values()) {
|
|
885
|
+
if (schedule.timer)
|
|
886
|
+
clearTimeout(schedule.timer);
|
|
887
|
+
}
|
|
888
|
+
this.appServerReleaseSchedules.clear();
|
|
847
889
|
for (const [jobId] of this.shellCommandJobMonitors)
|
|
848
890
|
this.stopShellCommandJobMonitor(jobId);
|
|
849
891
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
@@ -855,6 +897,19 @@ export class ProxySessionController {
|
|
|
855
897
|
await this.cancelSession(record);
|
|
856
898
|
return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
|
|
857
899
|
}
|
|
900
|
+
async controlReleaseTask(localSessionId) {
|
|
901
|
+
const record = this.requireControlSession(localSessionId);
|
|
902
|
+
const contextKey = this.controlSessionContextKey(record);
|
|
903
|
+
const result = await this.outbound.withReplyTarget(contextKey, this.controlSessionReplyTarget(record), () => this.requestAppServerRelease(contextKey, record));
|
|
904
|
+
if (result.status === "failed")
|
|
905
|
+
throw new Error(result.error ?? "Failed to release App Server tasks.");
|
|
906
|
+
return {
|
|
907
|
+
agentName: result.agentName,
|
|
908
|
+
status: result.status,
|
|
909
|
+
blockingTaskCount: result.blockingTaskCount,
|
|
910
|
+
...(result.releaseInSeconds !== undefined ? { releaseInSeconds: result.releaseInSeconds } : {}),
|
|
911
|
+
};
|
|
912
|
+
}
|
|
858
913
|
async controlArchiveTask(localSessionId) {
|
|
859
914
|
const record = this.requireControlSession(localSessionId);
|
|
860
915
|
return this.archiveSessionReference(this.controlSessionContextKey(record), localSessionId, {
|
|
@@ -1448,6 +1503,9 @@ export class ProxySessionController {
|
|
|
1448
1503
|
throw new Error("当前运行方式不支持自动重启。");
|
|
1449
1504
|
await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
|
|
1450
1505
|
return;
|
|
1506
|
+
case "release":
|
|
1507
|
+
await this.requestAppServerRelease(contextKey);
|
|
1508
|
+
return;
|
|
1451
1509
|
case "mute":
|
|
1452
1510
|
await this.setGroupMute(contextKey, command.enabled);
|
|
1453
1511
|
return;
|
|
@@ -2095,6 +2153,49 @@ export class ProxySessionController {
|
|
|
2095
2153
|
}
|
|
2096
2154
|
}
|
|
2097
2155
|
}
|
|
2156
|
+
async prepareTopicRootPrompt(message, text, localImagePaths) {
|
|
2157
|
+
const displayPrompt = message.displayText ?? text;
|
|
2158
|
+
const rootMessageId = topicRootMessageId(message);
|
|
2159
|
+
if (!rootMessageId)
|
|
2160
|
+
return { text, localImagePaths, displayPrompt };
|
|
2161
|
+
const current = this.currentSession(message.contextKey);
|
|
2162
|
+
if (current && (this.store.hasInjectedTopicRoot(current.localSessionId, rootMessageId)
|
|
2163
|
+
|| this.taskHistoryContainsMessage(current, rootMessageId))) {
|
|
2164
|
+
return { text, localImagePaths, displayPrompt };
|
|
2165
|
+
}
|
|
2166
|
+
try {
|
|
2167
|
+
const root = await this.resolveReferencedMessage(message.contextKey, rootMessageId);
|
|
2168
|
+
const rootImagePaths = await Promise.all(root.images.map((image) => this.outbound.downloadImage(message.contextKey, image.messageId, image.imageKey)));
|
|
2169
|
+
let promptText = topicRootMessagePrompt(text, root.text);
|
|
2170
|
+
if (root.files.length > 0) {
|
|
2171
|
+
const downloadedFiles = await Promise.all(root.files.map(async (file) => ({
|
|
2172
|
+
fileName: file.fileName,
|
|
2173
|
+
filePath: await this.outbound.downloadFile(message.contextKey, file.messageId, file.fileKey, file.fileName),
|
|
2174
|
+
})));
|
|
2175
|
+
promptText = appendDownloadedFiles(promptText, downloadedFiles);
|
|
2176
|
+
}
|
|
2177
|
+
const combinedImagePaths = [...new Set([...(localImagePaths ?? []), ...rootImagePaths])];
|
|
2178
|
+
return {
|
|
2179
|
+
text: promptText,
|
|
2180
|
+
...(combinedImagePaths.length > 0 ? { localImagePaths: combinedImagePaths } : {}),
|
|
2181
|
+
displayPrompt,
|
|
2182
|
+
injectedRootMessageId: rootMessageId,
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
catch (error) {
|
|
2186
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2187
|
+
throw new Error(`无法读取话题根消息:${detail}`);
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
taskHistoryContainsMessage(record, messageId) {
|
|
2191
|
+
const anchor = this.store.findTurnAnchorByMessageId(messageId);
|
|
2192
|
+
if (!anchor)
|
|
2193
|
+
return false;
|
|
2194
|
+
if (record.lastTurnId === anchor.turnId)
|
|
2195
|
+
return true;
|
|
2196
|
+
return this.store.listTaskTurnGraph(record.localSessionId)
|
|
2197
|
+
.some((turn) => turn.turnId === anchor.turnId);
|
|
2198
|
+
}
|
|
2098
2199
|
async forkThreadSession(message) {
|
|
2099
2200
|
const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
|
|
2100
2201
|
const agent = this.ensureAgent(source.agentName);
|
|
@@ -2589,6 +2690,38 @@ export class ProxySessionController {
|
|
|
2589
2690
|
this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to hydrate legacy Fork Turn history; rendering the locally available history.");
|
|
2590
2691
|
}
|
|
2591
2692
|
}
|
|
2693
|
+
async hydrateRemoteTurnHistory(contextKey, current) {
|
|
2694
|
+
if (!current.remoteSessionId)
|
|
2695
|
+
return;
|
|
2696
|
+
const refreshedAt = this.remoteTurnHistoryRefreshes.get(current.localSessionId);
|
|
2697
|
+
if (refreshedAt !== undefined && Date.now() - refreshedAt < REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS)
|
|
2698
|
+
return;
|
|
2699
|
+
const forkSource = this.store.getForkHistorySource(current.localSessionId);
|
|
2700
|
+
if (forkSource?.sourceTurnId === current.lastTurnId
|
|
2701
|
+
&& this.store.hasImportedForkTurnHistory(current.localSessionId))
|
|
2702
|
+
return;
|
|
2703
|
+
const runtime = this.runtimes.forAgent(current.agentName);
|
|
2704
|
+
if (runtime.kind !== "codex" || !runtime.readRemoteSession)
|
|
2705
|
+
return;
|
|
2706
|
+
try {
|
|
2707
|
+
const remote = await runtime.readRemoteSession(current.remoteSessionId);
|
|
2708
|
+
this.remoteTurnHistoryRefreshes.set(current.localSessionId, Date.now());
|
|
2709
|
+
const completedTurns = remote.completedTurns ?? [];
|
|
2710
|
+
if (completedTurns.length === 0)
|
|
2711
|
+
return;
|
|
2712
|
+
this.persistCompletedTurnHistory({
|
|
2713
|
+
localSessionId: current.localSessionId,
|
|
2714
|
+
contextKey,
|
|
2715
|
+
agentName: current.agentName,
|
|
2716
|
+
remoteSessionId: current.remoteSessionId,
|
|
2717
|
+
completedTurns,
|
|
2718
|
+
fallbackStart: parseIsoTimestamp(current.createdAt) ?? Date.now() - completedTurns.length,
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
catch (error) {
|
|
2722
|
+
this.logger.warn({ error, localSessionId: current.localSessionId, remoteSessionId: current.remoteSessionId }, "Failed to hydrate App Server Turn history; rendering the locally available history.");
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2592
2725
|
async createProjectSessionFromReference(contextKey, reference) {
|
|
2593
2726
|
const resolved = await this.resolveProjectSessionReference(contextKey, reference);
|
|
2594
2727
|
const created = await this.createSession(contextKey, resolved.agentName, resolved.cwd, true, false, undefined, undefined, undefined, resolved.executionSettings);
|
|
@@ -4273,6 +4406,7 @@ export class ProxySessionController {
|
|
|
4273
4406
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4274
4407
|
}
|
|
4275
4408
|
await this.hydrateLegacyForkTurnHistory(contextKey, current);
|
|
4409
|
+
await this.hydrateRemoteTurnHistory(contextKey, current);
|
|
4276
4410
|
const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
|
|
4277
4411
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
4278
4412
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
@@ -4590,7 +4724,7 @@ export class ProxySessionController {
|
|
|
4590
4724
|
if (!runtime.listRemoteSessions)
|
|
4591
4725
|
return;
|
|
4592
4726
|
try {
|
|
4593
|
-
const result = await
|
|
4727
|
+
const result = await this.listRemoteSessionsThroughCursor(runtime, searchTerm, fetchLimit);
|
|
4594
4728
|
remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
|
|
4595
4729
|
remoteHasMore ||= Boolean(result.nextCursor);
|
|
4596
4730
|
}
|
|
@@ -4788,6 +4922,37 @@ export class ProxySessionController {
|
|
|
4788
4922
|
}
|
|
4789
4923
|
}
|
|
4790
4924
|
}
|
|
4925
|
+
async listRemoteSessionsThroughCursor(runtime, searchTerm, limit) {
|
|
4926
|
+
const sessions = [];
|
|
4927
|
+
const seenSessionIds = new Set();
|
|
4928
|
+
const seenCursors = new Set();
|
|
4929
|
+
let cursor;
|
|
4930
|
+
while (sessions.length < limit) {
|
|
4931
|
+
const page = await runtime.listRemoteSessions({
|
|
4932
|
+
searchTerm,
|
|
4933
|
+
...(cursor ? { cursor } : {}),
|
|
4934
|
+
limit: limit - sessions.length,
|
|
4935
|
+
});
|
|
4936
|
+
for (const session of page.sessions) {
|
|
4937
|
+
if (seenSessionIds.has(session.id))
|
|
4938
|
+
continue;
|
|
4939
|
+
seenSessionIds.add(session.id);
|
|
4940
|
+
sessions.push(session);
|
|
4941
|
+
}
|
|
4942
|
+
const nextCursor = page.nextCursor?.trim() || undefined;
|
|
4943
|
+
if (!nextCursor)
|
|
4944
|
+
return { sessions };
|
|
4945
|
+
if (sessions.length >= limit)
|
|
4946
|
+
return { sessions, nextCursor };
|
|
4947
|
+
if (seenCursors.has(nextCursor)) {
|
|
4948
|
+
this.logger.warn({ cursor: nextCursor }, "App Server repeated a thread/list cursor; stopping pagination.");
|
|
4949
|
+
return { sessions };
|
|
4950
|
+
}
|
|
4951
|
+
seenCursors.add(nextCursor);
|
|
4952
|
+
cursor = nextCursor;
|
|
4953
|
+
}
|
|
4954
|
+
return { sessions };
|
|
4955
|
+
}
|
|
4791
4956
|
async refreshSessionsCardFromAction(action, options = {}) {
|
|
4792
4957
|
if (!action.messageId)
|
|
4793
4958
|
return;
|
|
@@ -5463,6 +5628,275 @@ export class ProxySessionController {
|
|
|
5463
5628
|
throw new Error(`找不到任务:${sessionId}`);
|
|
5464
5629
|
return record;
|
|
5465
5630
|
}
|
|
5631
|
+
async requestAppServerRelease(contextKey, requestedRecord) {
|
|
5632
|
+
const record = requestedRecord ?? this.requireCurrentSession(contextKey);
|
|
5633
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5634
|
+
if (!this.isCodexSession(record) || runtime.kind !== "codex" || !runtime.release) {
|
|
5635
|
+
throw new Error("当前 Agent 不使用可释放的 App Server。");
|
|
5636
|
+
}
|
|
5637
|
+
let schedule = this.appServerReleaseSchedules.get(record.agentName);
|
|
5638
|
+
const created = !schedule;
|
|
5639
|
+
if (!schedule) {
|
|
5640
|
+
const blockingTaskCount = this.appServerReleaseBlockers(record.agentName, runtime).length;
|
|
5641
|
+
schedule = {
|
|
5642
|
+
id: this.nextAppServerReleaseScheduleId++,
|
|
5643
|
+
agentName: record.agentName,
|
|
5644
|
+
runtime,
|
|
5645
|
+
status: "waiting",
|
|
5646
|
+
blockingTaskCount,
|
|
5647
|
+
targets: new Map(),
|
|
5648
|
+
forced: false,
|
|
5649
|
+
...(blockingTaskCount === 0
|
|
5650
|
+
? {
|
|
5651
|
+
releaseCountdownSeconds: Math.ceil(APP_SERVER_RELEASE_IDLE_DELAY_MS / 1_000),
|
|
5652
|
+
}
|
|
5653
|
+
: {}),
|
|
5654
|
+
};
|
|
5655
|
+
this.appServerReleaseSchedules.set(record.agentName, schedule);
|
|
5656
|
+
}
|
|
5657
|
+
try {
|
|
5658
|
+
const messageId = await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, contextKey)));
|
|
5659
|
+
if (messageId) {
|
|
5660
|
+
schedule.targets.set(`${contextKey}\u0000${messageId}`, { contextKey, messageId });
|
|
5661
|
+
}
|
|
5662
|
+
if (schedule.status === "waiting"
|
|
5663
|
+
&& schedule.blockingTaskCount === 0
|
|
5664
|
+
&& schedule.releaseCountdownSeconds !== undefined
|
|
5665
|
+
&& schedule.releaseAt === undefined) {
|
|
5666
|
+
schedule.releaseAt = Date.now() + APP_SERVER_RELEASE_IDLE_DELAY_MS;
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
catch (error) {
|
|
5670
|
+
if (created && schedule.targets.size === 0) {
|
|
5671
|
+
this.clearAppServerReleaseSchedule(schedule);
|
|
5672
|
+
}
|
|
5673
|
+
throw error;
|
|
5674
|
+
}
|
|
5675
|
+
this.armAppServerReleaseCheck(schedule);
|
|
5676
|
+
return {
|
|
5677
|
+
agentName: schedule.agentName,
|
|
5678
|
+
status: schedule.status === "failed" ? "failed" : schedule.status === "released" ? "released" : "waiting",
|
|
5679
|
+
blockingTaskCount: schedule.blockingTaskCount,
|
|
5680
|
+
...(schedule.releaseCountdownSeconds !== undefined
|
|
5681
|
+
? { releaseInSeconds: schedule.releaseCountdownSeconds }
|
|
5682
|
+
: {}),
|
|
5683
|
+
...(schedule.error ? { error: schedule.error } : {}),
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
async releaseAppServerNow(action) {
|
|
5687
|
+
const scheduleId = Number(action.value.scheduleId);
|
|
5688
|
+
const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
|
|
5689
|
+
const schedule = this.appServerReleaseSchedules.get(agentName);
|
|
5690
|
+
if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || schedule.status !== "waiting") {
|
|
5691
|
+
throw new Error("释放卡片已失效,请重新发送 /release。");
|
|
5692
|
+
}
|
|
5693
|
+
await this.performAppServerRelease(schedule, true);
|
|
5694
|
+
}
|
|
5695
|
+
async cancelAppServerRelease(action) {
|
|
5696
|
+
const scheduleId = Number(action.value.scheduleId);
|
|
5697
|
+
const agentName = typeof action.value.agentName === "string" ? action.value.agentName : "";
|
|
5698
|
+
const schedule = this.appServerReleaseSchedules.get(agentName);
|
|
5699
|
+
const cancellable = schedule?.status === "waiting"
|
|
5700
|
+
|| (schedule?.status === "releasing" && schedule.releaseStarted !== true);
|
|
5701
|
+
if (!Number.isSafeInteger(scheduleId) || !schedule || schedule.id !== scheduleId || !cancellable) {
|
|
5702
|
+
throw new Error("释放卡片已失效,请重新发送 /release。");
|
|
5703
|
+
}
|
|
5704
|
+
const pendingRelease = schedule.inFlight;
|
|
5705
|
+
schedule.status = "cancelled";
|
|
5706
|
+
if (schedule.timer) {
|
|
5707
|
+
clearTimeout(schedule.timer);
|
|
5708
|
+
schedule.timer = undefined;
|
|
5709
|
+
}
|
|
5710
|
+
if (pendingRelease)
|
|
5711
|
+
await pendingRelease;
|
|
5712
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
5713
|
+
this.clearAppServerReleaseSchedule(schedule);
|
|
5714
|
+
}
|
|
5715
|
+
appServerReleaseBlockers(agentName, runtime) {
|
|
5716
|
+
return this.store.listAllSessions().filter((session) => {
|
|
5717
|
+
if (session.agentName !== agentName || session.status === "closed")
|
|
5718
|
+
return false;
|
|
5719
|
+
return session.status === "starting"
|
|
5720
|
+
|| session.status === "running"
|
|
5721
|
+
|| this.store.countQueuedPrompts(session.localSessionId) > 0
|
|
5722
|
+
|| this.queuedPromptStarts.has(session.localSessionId)
|
|
5723
|
+
|| this.sessionLoads.has(session.localSessionId)
|
|
5724
|
+
|| Boolean(runtime.getSession(session.localSessionId)?.activeTurnId);
|
|
5725
|
+
});
|
|
5726
|
+
}
|
|
5727
|
+
armAppServerReleaseCheck(schedule) {
|
|
5728
|
+
if (schedule.status !== "waiting" || schedule.timer || schedule.inFlight)
|
|
5729
|
+
return;
|
|
5730
|
+
schedule.timer = setTimeout(() => {
|
|
5731
|
+
schedule.timer = undefined;
|
|
5732
|
+
void this.refreshAppServerReleaseSchedule(schedule).catch((error) => {
|
|
5733
|
+
this.logger.warn({ error, agentName: schedule.agentName }, "Failed to refresh App Server release status.");
|
|
5734
|
+
this.armAppServerReleaseCheck(schedule);
|
|
5735
|
+
});
|
|
5736
|
+
}, APP_SERVER_RELEASE_POLL_INTERVAL_MS);
|
|
5737
|
+
schedule.timer.unref?.();
|
|
5738
|
+
}
|
|
5739
|
+
async refreshAppServerReleaseSchedule(schedule) {
|
|
5740
|
+
if (this.appServerReleaseSchedules.get(schedule.agentName) !== schedule || schedule.status !== "waiting")
|
|
5741
|
+
return;
|
|
5742
|
+
const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
|
|
5743
|
+
if (blockingTaskCount > 0) {
|
|
5744
|
+
const changed = schedule.blockingTaskCount !== blockingTaskCount || schedule.releaseAt !== undefined;
|
|
5745
|
+
schedule.releaseAt = undefined;
|
|
5746
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
5747
|
+
if (changed) {
|
|
5748
|
+
schedule.blockingTaskCount = blockingTaskCount;
|
|
5749
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
5750
|
+
}
|
|
5751
|
+
this.armAppServerReleaseCheck(schedule);
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
5754
|
+
schedule.blockingTaskCount = 0;
|
|
5755
|
+
if (schedule.releaseAt !== undefined) {
|
|
5756
|
+
const releaseCountdownSeconds = Math.max(0, Math.ceil((schedule.releaseAt - Date.now()) / 1_000));
|
|
5757
|
+
if (releaseCountdownSeconds > 0) {
|
|
5758
|
+
if (schedule.releaseCountdownSeconds !== releaseCountdownSeconds) {
|
|
5759
|
+
schedule.releaseCountdownSeconds = releaseCountdownSeconds;
|
|
5760
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
5761
|
+
}
|
|
5762
|
+
this.armAppServerReleaseCheck(schedule);
|
|
5763
|
+
return;
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
await this.performAppServerRelease(schedule, false);
|
|
5767
|
+
}
|
|
5768
|
+
async performAppServerRelease(schedule, force) {
|
|
5769
|
+
if (schedule.inFlight)
|
|
5770
|
+
return schedule.inFlight;
|
|
5771
|
+
const operation = this.performAppServerReleaseNow(schedule, force);
|
|
5772
|
+
schedule.inFlight = operation;
|
|
5773
|
+
try {
|
|
5774
|
+
await operation;
|
|
5775
|
+
}
|
|
5776
|
+
finally {
|
|
5777
|
+
if (schedule.inFlight === operation)
|
|
5778
|
+
schedule.inFlight = undefined;
|
|
5779
|
+
if (schedule.status === "waiting")
|
|
5780
|
+
this.armAppServerReleaseCheck(schedule);
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5783
|
+
async performAppServerReleaseNow(schedule, force) {
|
|
5784
|
+
if (schedule.timer) {
|
|
5785
|
+
clearTimeout(schedule.timer);
|
|
5786
|
+
schedule.timer = undefined;
|
|
5787
|
+
}
|
|
5788
|
+
const interruptedWork = force && this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length > 0;
|
|
5789
|
+
if (force)
|
|
5790
|
+
await this.cancelQueuedPromptsForAppServerRelease(schedule.agentName);
|
|
5791
|
+
else {
|
|
5792
|
+
const blockingTaskCount = this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length;
|
|
5793
|
+
if (blockingTaskCount > 0) {
|
|
5794
|
+
schedule.status = "waiting";
|
|
5795
|
+
schedule.blockingTaskCount = blockingTaskCount;
|
|
5796
|
+
schedule.releaseAt = undefined;
|
|
5797
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
5798
|
+
schedule.releaseStarted = false;
|
|
5799
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
5800
|
+
this.armAppServerReleaseCheck(schedule);
|
|
5801
|
+
return;
|
|
5802
|
+
}
|
|
5803
|
+
}
|
|
5804
|
+
schedule.status = "releasing";
|
|
5805
|
+
schedule.blockingTaskCount = 0;
|
|
5806
|
+
schedule.releaseAt = undefined;
|
|
5807
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
5808
|
+
schedule.releaseStarted = false;
|
|
5809
|
+
schedule.forced = interruptedWork;
|
|
5810
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
5811
|
+
if (this.appServerReleaseWasCancelled(schedule)
|
|
5812
|
+
|| this.appServerReleaseSchedules.get(schedule.agentName) !== schedule)
|
|
5813
|
+
return;
|
|
5814
|
+
schedule.releaseStarted = true;
|
|
5815
|
+
try {
|
|
5816
|
+
const result = await schedule.runtime.release?.({ force });
|
|
5817
|
+
if (!result)
|
|
5818
|
+
throw new Error("当前 Agent 不支持释放 App Server。");
|
|
5819
|
+
if (result.status === "busy") {
|
|
5820
|
+
schedule.status = "waiting";
|
|
5821
|
+
schedule.blockingTaskCount = Math.max(result.activeSessionIds?.length ?? 0, this.appServerReleaseBlockers(schedule.agentName, schedule.runtime).length, 1);
|
|
5822
|
+
schedule.forced = false;
|
|
5823
|
+
schedule.releaseAt = undefined;
|
|
5824
|
+
schedule.releaseCountdownSeconds = undefined;
|
|
5825
|
+
schedule.releaseStarted = false;
|
|
5826
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
5827
|
+
this.armAppServerReleaseCheck(schedule);
|
|
5828
|
+
return;
|
|
5829
|
+
}
|
|
5830
|
+
schedule.status = "released";
|
|
5831
|
+
}
|
|
5832
|
+
catch (error) {
|
|
5833
|
+
schedule.status = "failed";
|
|
5834
|
+
schedule.error = error instanceof Error ? error.message : String(error);
|
|
5835
|
+
this.logger.warn({ error, agentName: schedule.agentName }, "Failed to release App Server tasks.");
|
|
5836
|
+
}
|
|
5837
|
+
await this.publishAppServerReleaseSchedule(schedule);
|
|
5838
|
+
this.clearAppServerReleaseSchedule(schedule);
|
|
5839
|
+
}
|
|
5840
|
+
appServerReleaseWasCancelled(schedule) {
|
|
5841
|
+
return schedule.status === "cancelled";
|
|
5842
|
+
}
|
|
5843
|
+
async cancelQueuedPromptsForAppServerRelease(agentName) {
|
|
5844
|
+
for (const session of this.store.listAllSessions()) {
|
|
5845
|
+
if (session.agentName !== agentName)
|
|
5846
|
+
continue;
|
|
5847
|
+
const retryTimer = this.queuedPromptRetryTimers.get(session.localSessionId);
|
|
5848
|
+
if (retryTimer) {
|
|
5849
|
+
clearTimeout(retryTimer);
|
|
5850
|
+
this.queuedPromptRetryTimers.delete(session.localSessionId);
|
|
5851
|
+
}
|
|
5852
|
+
const queued = this.store.listQueuedPrompts(session.localSessionId);
|
|
5853
|
+
for (const prompt of queued) {
|
|
5854
|
+
const cancelled = this.store.cancelQueuedPrompt(prompt.promptId, session.localSessionId);
|
|
5855
|
+
if (!cancelled?.messageId)
|
|
5856
|
+
continue;
|
|
5857
|
+
await this.finalizeStandaloneMessageReaction(cancelled.messageId, "cancelled").catch((error) => {
|
|
5858
|
+
this.logger.warn({ error, agentName, sessionId: session.localSessionId, promptId: cancelled.promptId }, "Failed to finalize a queued Prompt reaction during App Server release.");
|
|
5859
|
+
});
|
|
5860
|
+
}
|
|
5861
|
+
if (queued.length > 0) {
|
|
5862
|
+
await this.refreshPromptQueueCards(session.localSessionId).catch((error) => {
|
|
5863
|
+
this.logger.warn({ error, agentName, sessionId: session.localSessionId }, "Failed to refresh queued Prompt cards during App Server release.");
|
|
5864
|
+
});
|
|
5865
|
+
}
|
|
5866
|
+
}
|
|
5867
|
+
}
|
|
5868
|
+
appServerReleaseCardView(schedule, contextKey) {
|
|
5869
|
+
return {
|
|
5870
|
+
status: schedule.status,
|
|
5871
|
+
contextKey,
|
|
5872
|
+
scheduleId: schedule.id,
|
|
5873
|
+
agentName: schedule.agentName,
|
|
5874
|
+
...(schedule.blockingTaskCount > 0 ? { blockingTaskCount: schedule.blockingTaskCount } : {}),
|
|
5875
|
+
...(schedule.releaseCountdownSeconds !== undefined
|
|
5876
|
+
? { releaseInSeconds: schedule.releaseCountdownSeconds }
|
|
5877
|
+
: {}),
|
|
5878
|
+
...(schedule.forced ? { forced: true } : {}),
|
|
5879
|
+
...(schedule.error ? { error: schedule.error } : {}),
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
async publishAppServerReleaseSchedule(schedule) {
|
|
5883
|
+
await Promise.all([...schedule.targets.values()].map(async (target) => {
|
|
5884
|
+
try {
|
|
5885
|
+
await this.outbound.updateInteractiveCard(target.contextKey, target.messageId, this.cardRenderer.renderAppServerRelease(this.appServerReleaseCardView(schedule, target.contextKey)));
|
|
5886
|
+
}
|
|
5887
|
+
catch (error) {
|
|
5888
|
+
this.logger.warn({ error, agentName: schedule.agentName, messageId: target.messageId }, "Failed to update App Server release card.");
|
|
5889
|
+
}
|
|
5890
|
+
}));
|
|
5891
|
+
}
|
|
5892
|
+
clearAppServerReleaseSchedule(schedule) {
|
|
5893
|
+
if (schedule.timer)
|
|
5894
|
+
clearTimeout(schedule.timer);
|
|
5895
|
+
schedule.timer = undefined;
|
|
5896
|
+
if (this.appServerReleaseSchedules.get(schedule.agentName) === schedule) {
|
|
5897
|
+
this.appServerReleaseSchedules.delete(schedule.agentName);
|
|
5898
|
+
}
|
|
5899
|
+
}
|
|
5466
5900
|
async presentThreadWriterConflict(record, contextKey, error, replacePendingTurn) {
|
|
5467
5901
|
if (contextKey.startsWith("console:"))
|
|
5468
5902
|
return false;
|
|
@@ -6179,7 +6613,7 @@ function isBotOwnedActiveTurn(record, remote) {
|
|
|
6179
6613
|
&& record.lastTurnId === remote.lastTurnId;
|
|
6180
6614
|
}
|
|
6181
6615
|
function isQueueIndependentCommand(command) {
|
|
6182
|
-
if (["archive", "dismiss", "stop", "status", "restart", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
|
|
6616
|
+
if (["archive", "dismiss", "stop", "status", "restart", "release", "mute", "help", "sessions", "dir", "file", "goal", "nosteer", "shell"].includes(command.type))
|
|
6183
6617
|
return true;
|
|
6184
6618
|
if (command.type === "agent")
|
|
6185
6619
|
return command.agent === undefined;
|
|
@@ -6391,6 +6825,17 @@ function mergedForwardPrompt(instruction, transcript) {
|
|
|
6391
6825
|
function referencedMessagePrompt(instruction, referencedMessage) {
|
|
6392
6826
|
return `${instruction}\n\n引用消息:\n${referencedMessage}`;
|
|
6393
6827
|
}
|
|
6828
|
+
function topicRootMessagePrompt(instruction, rootMessage) {
|
|
6829
|
+
return `${instruction}\n\n话题根消息:\n${rootMessage}`;
|
|
6830
|
+
}
|
|
6831
|
+
function topicRootMessageId(message) {
|
|
6832
|
+
if (!message.threadContext)
|
|
6833
|
+
return undefined;
|
|
6834
|
+
const rootMessageId = message.rootMessageId?.trim();
|
|
6835
|
+
if (!rootMessageId || rootMessageId === message.messageId)
|
|
6836
|
+
return undefined;
|
|
6837
|
+
return rootMessageId;
|
|
6838
|
+
}
|
|
6394
6839
|
function quotedMessageId(message) {
|
|
6395
6840
|
const messageId = message.parentMessageId?.trim();
|
|
6396
6841
|
if (!messageId || message.contextKey.startsWith("console:"))
|