@keyou007/agent-bot 0.1.18 → 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 +11 -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 +43 -5
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ThreadWriterProcess.js +344 -0
- package/dist/codex/ThreadWriterProcess.js.map +1 -0
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +1 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +166 -0
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +13 -3
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +6 -1
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/logging/errorLogValue.js +18 -0
- package/dist/logging/errorLogValue.js.map +1 -0
- package/dist/logging/logger.js +4 -0
- package/dist/logging/logger.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +2 -2
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +778 -19
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +10 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +1 -1
- package/dist/supervision/SafeRestartScheduler.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 +41 -4
- package/src/codex/ThreadWriterProcess.ts +385 -0
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/console/ConsoleTurnPresenter.ts +2 -1
- package/src/feishu/CardRenderer.ts +205 -0
- package/src/feishu/FeishuTurnPresenter.ts +16 -3
- package/src/feishu/MergedForwardMessage.ts +5 -1
- package/src/index.ts +4 -2
- package/src/logging/errorLogValue.ts +26 -0
- package/src/logging/logger.ts +4 -0
- package/src/presentation/OutboundRouter.ts +15 -3
- package/src/proxy/ProxySessionController.ts +1003 -30
- package/src/runtime/types.ts +7 -0
- package/src/state/StateStore.ts +11 -0
- package/src/supervision/SafeRestartScheduler.ts +1 -1
|
@@ -4,6 +4,11 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import type { Logger } from "pino";
|
|
6
6
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
7
|
+
import {
|
|
8
|
+
SystemThreadWriterProcessController,
|
|
9
|
+
type ThreadWriterProcess,
|
|
10
|
+
type ThreadWriterProcessController,
|
|
11
|
+
} from "../codex/ThreadWriterProcess.js";
|
|
7
12
|
import { resolveUserPath } from "../config/paths.js";
|
|
8
13
|
import {
|
|
9
14
|
DEFAULT_GROUP_NAME_FORMAT,
|
|
@@ -21,6 +26,7 @@ import type {
|
|
|
21
26
|
ReferencedMessageContent,
|
|
22
27
|
} from "../feishu/types.js";
|
|
23
28
|
import {
|
|
29
|
+
type AppServerReleaseCardView,
|
|
24
30
|
CardRenderer,
|
|
25
31
|
type CardSection,
|
|
26
32
|
type DirectoryBrowserCardEntry,
|
|
@@ -31,6 +37,7 @@ import {
|
|
|
31
37
|
type SessionTaskCardGroup,
|
|
32
38
|
type ShellCommandCardView,
|
|
33
39
|
type TaskListCardAction,
|
|
40
|
+
type ThreadWriterConflictCardView,
|
|
34
41
|
} from "../feishu/CardRenderer.js";
|
|
35
42
|
import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
|
|
36
43
|
import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
|
|
@@ -42,6 +49,7 @@ import {
|
|
|
42
49
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
43
50
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
44
51
|
import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
|
|
52
|
+
import { errorLogValue } from "../logging/errorLogValue.js";
|
|
45
53
|
import type { OutboundRouter } from "../presentation/OutboundRouter.js";
|
|
46
54
|
import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
|
|
47
55
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
@@ -52,6 +60,7 @@ import type {
|
|
|
52
60
|
PermissionMode,
|
|
53
61
|
RemoteCompletedTurnSummary,
|
|
54
62
|
RemoteSessionActivity,
|
|
63
|
+
RemoteSessionPage,
|
|
55
64
|
RemoteSessionSummary,
|
|
56
65
|
RuntimeGoal,
|
|
57
66
|
RuntimeEvent,
|
|
@@ -110,12 +119,55 @@ interface ForwardAttachmentReservation {
|
|
|
110
119
|
registry: Map<string, PendingForwardAttachment>;
|
|
111
120
|
}
|
|
112
121
|
|
|
122
|
+
interface PreparedTopicRootPrompt {
|
|
123
|
+
text: string;
|
|
124
|
+
localImagePaths?: string[];
|
|
125
|
+
displayPrompt: string;
|
|
126
|
+
injectedRootMessageId?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
113
129
|
interface ShellCommandJobMonitor {
|
|
114
130
|
scheduler: CardUpdateScheduler<ShellCommandCardView>;
|
|
115
131
|
timer: NodeJS.Timeout;
|
|
116
132
|
refreshing: boolean;
|
|
117
133
|
}
|
|
118
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
|
+
|
|
156
|
+
interface RequestFailureDetails {
|
|
157
|
+
requestId?: string;
|
|
158
|
+
messageId?: string;
|
|
159
|
+
actionId?: string;
|
|
160
|
+
command?: string;
|
|
161
|
+
phase?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
class PresentedRequestError extends Error {
|
|
165
|
+
constructor(public readonly originalError: unknown) {
|
|
166
|
+
super(originalError instanceof Error ? originalError.message : String(originalError), { cause: originalError });
|
|
167
|
+
this.name = "PresentedRequestError";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
119
171
|
const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
120
172
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
121
173
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
@@ -124,12 +176,15 @@ const SESSION_PAGE_SIZE = 10;
|
|
|
124
176
|
const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
125
177
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
126
178
|
const RESET_HISTORY_PAGE_SIZE = 10;
|
|
179
|
+
const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
|
|
127
180
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
128
181
|
const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
|
|
129
182
|
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
130
183
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
131
184
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
132
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;
|
|
133
188
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
134
189
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
135
190
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -263,6 +318,10 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
263
318
|
usage: "[--force]",
|
|
264
319
|
description: "默认安全重启;--force 立即重启",
|
|
265
320
|
},
|
|
321
|
+
{
|
|
322
|
+
command: "/release",
|
|
323
|
+
description: "释放 Agent Bot 占用的 App Server 任务",
|
|
324
|
+
},
|
|
266
325
|
{
|
|
267
326
|
command: "/mute",
|
|
268
327
|
usage: "[on|off]",
|
|
@@ -391,6 +450,13 @@ export interface ControlTaskGroupResult {
|
|
|
391
450
|
task: SessionRecord;
|
|
392
451
|
}
|
|
393
452
|
|
|
453
|
+
export interface ControlTaskReleaseResult {
|
|
454
|
+
agentName: string;
|
|
455
|
+
status: "waiting" | "released";
|
|
456
|
+
blockingTaskCount: number;
|
|
457
|
+
releaseInSeconds?: number;
|
|
458
|
+
}
|
|
459
|
+
|
|
394
460
|
export interface ProxyLifecycle {
|
|
395
461
|
supervised?: boolean;
|
|
396
462
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
@@ -425,6 +491,7 @@ export class ProxySessionController {
|
|
|
425
491
|
private readonly queuedPromptCardWrites = new Map<string, Promise<void>>();
|
|
426
492
|
private readonly sessionResets = new Map<string, Promise<void>>();
|
|
427
493
|
private readonly resetHistoryOperations = new Map<string, string>();
|
|
494
|
+
private readonly remoteTurnHistoryRefreshes = new Map<string, number>();
|
|
428
495
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
429
496
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
430
497
|
private readonly llmRetryingSessions = new Set<string>();
|
|
@@ -435,6 +502,8 @@ export class ProxySessionController {
|
|
|
435
502
|
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
436
503
|
private readonly relatedReactionMessageIds = new Map<string, string[]>();
|
|
437
504
|
private readonly shellCommandJobMonitors = new Map<string, ShellCommandJobMonitor>();
|
|
505
|
+
private readonly appServerReleaseSchedules = new Map<string, AppServerReleaseSchedule>();
|
|
506
|
+
private nextAppServerReleaseScheduleId = 1;
|
|
438
507
|
private readonly unsubscribe: Array<() => void> = [];
|
|
439
508
|
private readonly recoveryActivityHeartbeat: NodeJS.Timeout;
|
|
440
509
|
private startupRecovery?: Promise<void>;
|
|
@@ -452,6 +521,7 @@ export class ProxySessionController {
|
|
|
452
521
|
private readonly shellCommandJobs: ShellCommandJobManagerLike = new ShellCommandJobManager(
|
|
453
522
|
path.join(path.dirname(config.storage.sqlitePath), "command-jobs"),
|
|
454
523
|
),
|
|
524
|
+
private readonly threadWriterProcesses: ThreadWriterProcessController = new SystemThreadWriterProcessController(),
|
|
455
525
|
) {
|
|
456
526
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
457
527
|
this.unsubscribe.push(
|
|
@@ -608,7 +678,11 @@ export class ProxySessionController {
|
|
|
608
678
|
await this.outbound.withReplyTarget(
|
|
609
679
|
message.contextKey,
|
|
610
680
|
replyTarget,
|
|
611
|
-
() => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)
|
|
681
|
+
() => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`), {
|
|
682
|
+
requestId: message.messageId,
|
|
683
|
+
messageId: message.messageId,
|
|
684
|
+
phase: "merged_forward_read",
|
|
685
|
+
}),
|
|
612
686
|
);
|
|
613
687
|
return;
|
|
614
688
|
}
|
|
@@ -643,7 +717,11 @@ export class ProxySessionController {
|
|
|
643
717
|
await this.outbound.withReplyTarget(
|
|
644
718
|
message.contextKey,
|
|
645
719
|
replyTarget,
|
|
646
|
-
() => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)
|
|
720
|
+
() => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`), {
|
|
721
|
+
requestId: message.messageId,
|
|
722
|
+
messageId: message.messageId,
|
|
723
|
+
phase: "referenced_message_read",
|
|
724
|
+
}),
|
|
647
725
|
);
|
|
648
726
|
return;
|
|
649
727
|
}
|
|
@@ -681,7 +759,11 @@ export class ProxySessionController {
|
|
|
681
759
|
await this.outbound.withReplyTarget(
|
|
682
760
|
message.contextKey,
|
|
683
761
|
replyTarget,
|
|
684
|
-
() => this.sendError(message.contextKey, error
|
|
762
|
+
() => this.sendError(message.contextKey, error, {
|
|
763
|
+
requestId: message.messageId,
|
|
764
|
+
messageId: message.messageId,
|
|
765
|
+
phase: "image_download",
|
|
766
|
+
}),
|
|
685
767
|
);
|
|
686
768
|
return;
|
|
687
769
|
}
|
|
@@ -708,7 +790,11 @@ export class ProxySessionController {
|
|
|
708
790
|
await this.outbound.withReplyTarget(
|
|
709
791
|
message.contextKey,
|
|
710
792
|
replyTarget,
|
|
711
|
-
() => this.sendError(message.contextKey, error
|
|
793
|
+
() => this.sendError(message.contextKey, error, {
|
|
794
|
+
requestId: message.messageId,
|
|
795
|
+
messageId: message.messageId,
|
|
796
|
+
phase: "file_download",
|
|
797
|
+
}),
|
|
712
798
|
);
|
|
713
799
|
return;
|
|
714
800
|
}
|
|
@@ -724,10 +810,25 @@ export class ProxySessionController {
|
|
|
724
810
|
await this.outbound.withReplyTarget(
|
|
725
811
|
message.contextKey,
|
|
726
812
|
replyTarget,
|
|
727
|
-
() => this.sendError(message.contextKey, error
|
|
813
|
+
() => this.sendError(message.contextKey, error, {
|
|
814
|
+
requestId: message.messageId,
|
|
815
|
+
messageId: message.messageId,
|
|
816
|
+
phase: "command_parse",
|
|
817
|
+
}),
|
|
728
818
|
);
|
|
729
819
|
return;
|
|
730
820
|
}
|
|
821
|
+
if (command.type !== "prompt") {
|
|
822
|
+
this.logger.info(
|
|
823
|
+
{
|
|
824
|
+
requestId: message.messageId,
|
|
825
|
+
messageId: message.messageId,
|
|
826
|
+
contextKey: message.contextKey,
|
|
827
|
+
command: command.type,
|
|
828
|
+
},
|
|
829
|
+
"Processing Agent Bot command.",
|
|
830
|
+
);
|
|
831
|
+
}
|
|
731
832
|
// Operational and read-only commands must remain available even if a prompt operation is slow.
|
|
732
833
|
if (isQueueIndependentCommand(command)) {
|
|
733
834
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
@@ -747,7 +848,12 @@ export class ProxySessionController {
|
|
|
747
848
|
}
|
|
748
849
|
} catch (error) {
|
|
749
850
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
750
|
-
await this.sendError(message.contextKey, error
|
|
851
|
+
await this.sendError(message.contextKey, error, {
|
|
852
|
+
requestId: message.messageId,
|
|
853
|
+
messageId: message.messageId,
|
|
854
|
+
command: command.type,
|
|
855
|
+
phase: "command_execute",
|
|
856
|
+
});
|
|
751
857
|
}
|
|
752
858
|
});
|
|
753
859
|
return;
|
|
@@ -757,22 +863,48 @@ export class ProxySessionController {
|
|
|
757
863
|
const next = previous.catch(() => undefined).then(() =>
|
|
758
864
|
this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
759
865
|
try {
|
|
760
|
-
|
|
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
|
+
}
|
|
761
878
|
await this.execute(
|
|
762
879
|
message.contextKey,
|
|
763
|
-
|
|
880
|
+
executableCommand,
|
|
764
881
|
message.messageId,
|
|
765
882
|
replyTarget,
|
|
766
|
-
|
|
883
|
+
executableImagePaths,
|
|
767
884
|
message.userId,
|
|
768
|
-
|
|
885
|
+
executableMessage,
|
|
769
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
|
+
}
|
|
770
897
|
if (!commandDefersReactionFinalization(command)) {
|
|
771
898
|
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
772
899
|
}
|
|
773
900
|
} catch (error) {
|
|
774
901
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
775
|
-
await this.sendError(message.contextKey, error
|
|
902
|
+
await this.sendError(message.contextKey, error, {
|
|
903
|
+
requestId: message.messageId,
|
|
904
|
+
messageId: message.messageId,
|
|
905
|
+
command: command.type,
|
|
906
|
+
phase: "command_execute",
|
|
907
|
+
});
|
|
776
908
|
}
|
|
777
909
|
}),
|
|
778
910
|
);
|
|
@@ -794,7 +926,11 @@ export class ProxySessionController {
|
|
|
794
926
|
try {
|
|
795
927
|
resolvedAction = this.resolveCardActionBinding(action);
|
|
796
928
|
} catch (error) {
|
|
797
|
-
await this.sendError(action.contextKey, error
|
|
929
|
+
await this.sendError(action.contextKey, error, {
|
|
930
|
+
requestId: action.actionId,
|
|
931
|
+
actionId: action.actionId,
|
|
932
|
+
phase: "card_action_resolve",
|
|
933
|
+
});
|
|
798
934
|
return;
|
|
799
935
|
}
|
|
800
936
|
const contextKey = this.cardActionContextKey(resolvedAction);
|
|
@@ -829,6 +965,20 @@ export class ProxySessionController {
|
|
|
829
965
|
contextKey,
|
|
830
966
|
String(scopedAction.value.jobId ?? ""),
|
|
831
967
|
);
|
|
968
|
+
} else if (kind === "thread_writer_close") {
|
|
969
|
+
await this.closeThreadWriter(scopedAction, contextKey);
|
|
970
|
+
} else if (kind === "thread_writer_fork") {
|
|
971
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
972
|
+
const threadId = String(scopedAction.value.threadId ?? "");
|
|
973
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
974
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
975
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
976
|
+
}
|
|
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);
|
|
832
982
|
} else if (kind === "turn_reset") {
|
|
833
983
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
834
984
|
const turnId = String(scopedAction.value.turnId ?? "");
|
|
@@ -1069,7 +1219,12 @@ export class ProxySessionController {
|
|
|
1069
1219
|
await this.resolveApproval(scopedAction);
|
|
1070
1220
|
}
|
|
1071
1221
|
} catch (error) {
|
|
1072
|
-
await this.sendError(contextKey, error
|
|
1222
|
+
await this.sendError(contextKey, error, {
|
|
1223
|
+
requestId: scopedAction.actionId,
|
|
1224
|
+
actionId: scopedAction.actionId,
|
|
1225
|
+
command: String(scopedAction.value.action ?? "unknown"),
|
|
1226
|
+
phase: "card_action_execute",
|
|
1227
|
+
});
|
|
1073
1228
|
}
|
|
1074
1229
|
});
|
|
1075
1230
|
}
|
|
@@ -1094,6 +1249,7 @@ export class ProxySessionController {
|
|
|
1094
1249
|
this.queuedPromptCardWrites.clear();
|
|
1095
1250
|
this.sessionResets.clear();
|
|
1096
1251
|
this.resetHistoryOperations.clear();
|
|
1252
|
+
this.remoteTurnHistoryRefreshes.clear();
|
|
1097
1253
|
for (const pending of this.pendingMergedForwards.values()) pending.resolveAttachment(undefined);
|
|
1098
1254
|
this.pendingMergedForwards.clear();
|
|
1099
1255
|
for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
|
|
@@ -1103,6 +1259,10 @@ export class ProxySessionController {
|
|
|
1103
1259
|
this.queuedPromptRetryTimers.clear();
|
|
1104
1260
|
for (const timer of this.recoveryRetryTimers.values()) clearTimeout(timer);
|
|
1105
1261
|
this.recoveryRetryTimers.clear();
|
|
1262
|
+
for (const schedule of this.appServerReleaseSchedules.values()) {
|
|
1263
|
+
if (schedule.timer) clearTimeout(schedule.timer);
|
|
1264
|
+
}
|
|
1265
|
+
this.appServerReleaseSchedules.clear();
|
|
1106
1266
|
for (const [jobId] of this.shellCommandJobMonitors) this.stopShellCommandJobMonitor(jobId);
|
|
1107
1267
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
1108
1268
|
}
|
|
@@ -1114,6 +1274,23 @@ export class ProxySessionController {
|
|
|
1114
1274
|
return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
|
|
1115
1275
|
}
|
|
1116
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
|
+
|
|
1117
1294
|
async controlArchiveTask(localSessionId: string): Promise<{
|
|
1118
1295
|
localSessionId: string;
|
|
1119
1296
|
remoteSessionId: string;
|
|
@@ -1852,6 +2029,9 @@ export class ProxySessionController {
|
|
|
1852
2029
|
if (!this.lifecycle) throw new Error("当前运行方式不支持自动重启。");
|
|
1853
2030
|
await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
|
|
1854
2031
|
return;
|
|
2032
|
+
case "release":
|
|
2033
|
+
await this.requestAppServerRelease(contextKey);
|
|
2034
|
+
return;
|
|
1855
2035
|
case "mute":
|
|
1856
2036
|
await this.setGroupMute(contextKey, command.enabled);
|
|
1857
2037
|
return;
|
|
@@ -2401,6 +2581,9 @@ export class ProxySessionController {
|
|
|
2401
2581
|
try {
|
|
2402
2582
|
loaded = await this.loadSession(record);
|
|
2403
2583
|
} catch (error) {
|
|
2584
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
2585
|
+
throw new PresentedRequestError(error);
|
|
2586
|
+
}
|
|
2404
2587
|
await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
|
|
2405
2588
|
throw error;
|
|
2406
2589
|
}
|
|
@@ -2654,6 +2837,61 @@ export class ProxySessionController {
|
|
|
2654
2837
|
}
|
|
2655
2838
|
}
|
|
2656
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
|
+
|
|
2657
2895
|
private async forkThreadSession(message: IncomingMessage): Promise<void> {
|
|
2658
2896
|
const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
|
|
2659
2897
|
const agent = this.ensureAgent(source.agentName);
|
|
@@ -3249,6 +3487,39 @@ export class ProxySessionController {
|
|
|
3249
3487
|
}
|
|
3250
3488
|
}
|
|
3251
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
|
+
|
|
3252
3523
|
private async createProjectSessionFromReference(contextKey: string, reference: string): Promise<void> {
|
|
3253
3524
|
const resolved = await this.resolveProjectSessionReference(contextKey, reference);
|
|
3254
3525
|
const created = await this.createSession(
|
|
@@ -3809,6 +4080,7 @@ export class ProxySessionController {
|
|
|
3809
4080
|
lastTurnStatus: record.lastTurnStatus,
|
|
3810
4081
|
});
|
|
3811
4082
|
} catch (error) {
|
|
4083
|
+
if (activeWriterThreadId(error)) throw error;
|
|
3812
4084
|
if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error))) throw error;
|
|
3813
4085
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
3814
4086
|
session = await runtime.createSession({
|
|
@@ -5330,6 +5602,7 @@ export class ProxySessionController {
|
|
|
5330
5602
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
5331
5603
|
}
|
|
5332
5604
|
await this.hydrateLegacyForkTurnHistory(contextKey, current);
|
|
5605
|
+
await this.hydrateRemoteTurnHistory(contextKey, current);
|
|
5333
5606
|
const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
|
|
5334
5607
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
5335
5608
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
@@ -5664,7 +5937,7 @@ export class ProxySessionController {
|
|
|
5664
5937
|
await Promise.all(this.runtimes.entries("codex").map(async ([agentName, runtime]) => {
|
|
5665
5938
|
if (!runtime.listRemoteSessions) return;
|
|
5666
5939
|
try {
|
|
5667
|
-
const result = await
|
|
5940
|
+
const result = await this.listRemoteSessionsThroughCursor(runtime, searchTerm, fetchLimit);
|
|
5668
5941
|
remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
|
|
5669
5942
|
remoteHasMore ||= Boolean(result.nextCursor);
|
|
5670
5943
|
} catch (error) {
|
|
@@ -5876,6 +6149,42 @@ export class ProxySessionController {
|
|
|
5876
6149
|
}
|
|
5877
6150
|
}
|
|
5878
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
|
+
|
|
5879
6188
|
private async refreshSessionsCardFromAction(
|
|
5880
6189
|
action: CardAction,
|
|
5881
6190
|
options: { forceSwitchTaskId?: string; page?: number } = {},
|
|
@@ -6509,18 +6818,34 @@ export class ProxySessionController {
|
|
|
6509
6818
|
description: command.description,
|
|
6510
6819
|
})),
|
|
6511
6820
|
}));
|
|
6512
|
-
|
|
6513
|
-
"
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6821
|
+
const introLines = [
|
|
6822
|
+
"直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
|
|
6823
|
+
"点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
|
|
6824
|
+
"> 命令前缀示例:/sess 等同于 /sessions。",
|
|
6825
|
+
"> 命令缩写示例:/fg 等同于 /forkgroup。",
|
|
6826
|
+
"前缀或缩写匹配多个命令时,需要输入更长的形式。",
|
|
6827
|
+
"**[参数]** 可选 **<参数>** 必填",
|
|
6828
|
+
];
|
|
6829
|
+
try {
|
|
6830
|
+
await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard(
|
|
6831
|
+
"Agent Bot 使用帮助",
|
|
6832
|
+
introLines,
|
|
6833
|
+
sections,
|
|
6834
|
+
));
|
|
6835
|
+
} catch (cardError) {
|
|
6836
|
+
this.logger.warn(
|
|
6837
|
+
{ error: cardError, contextKey, command: "help", phase: "help_card_send" },
|
|
6838
|
+
"Failed to send the help card; falling back to plain text.",
|
|
6839
|
+
);
|
|
6840
|
+
try {
|
|
6841
|
+
await this.outbound.sendText(contextKey, renderPlainHelp(introLines, sections));
|
|
6842
|
+
} catch (textError) {
|
|
6843
|
+
throw new AggregateError(
|
|
6844
|
+
[cardError, textError],
|
|
6845
|
+
"帮助卡片和纯文本帮助均发送失败。",
|
|
6846
|
+
);
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6524
6849
|
}
|
|
6525
6850
|
|
|
6526
6851
|
private async setGroupMute(contextKey: string, enabled: boolean): Promise<void> {
|
|
@@ -6636,10 +6961,647 @@ export class ProxySessionController {
|
|
|
6636
6961
|
return record;
|
|
6637
6962
|
}
|
|
6638
6963
|
|
|
6639
|
-
private async
|
|
6640
|
-
|
|
6641
|
-
|
|
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
|
+
};
|
|
6642
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
|
+
|
|
7269
|
+
private async presentThreadWriterConflict(
|
|
7270
|
+
record: SessionRecord,
|
|
7271
|
+
contextKey: string,
|
|
7272
|
+
error: unknown,
|
|
7273
|
+
replacePendingTurn: boolean,
|
|
7274
|
+
): Promise<boolean> {
|
|
7275
|
+
if (contextKey.startsWith("console:")) return false;
|
|
7276
|
+
const threadId = activeWriterThreadId(error);
|
|
7277
|
+
if (!threadId || record.remoteSessionId !== threadId) return false;
|
|
7278
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
7279
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
7280
|
+
let owners: ThreadWriterProcess[] = [];
|
|
7281
|
+
if (lockPath) {
|
|
7282
|
+
try {
|
|
7283
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
7284
|
+
} catch (inspectionError) {
|
|
7285
|
+
this.logger.warn(
|
|
7286
|
+
{ error: inspectionError, threadId, lockPath },
|
|
7287
|
+
"Failed to inspect the process holding a thread writer lock.",
|
|
7288
|
+
);
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
7292
|
+
const applicationIdle = owner && lockPath
|
|
7293
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner)
|
|
7294
|
+
: undefined;
|
|
7295
|
+
const card = this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
|
|
7296
|
+
record,
|
|
7297
|
+
contextKey,
|
|
7298
|
+
threadId,
|
|
7299
|
+
owners,
|
|
7300
|
+
"occupied",
|
|
7301
|
+
lockPath
|
|
7302
|
+
? undefined
|
|
7303
|
+
: "无法识别占用进程。",
|
|
7304
|
+
applicationIdle,
|
|
7305
|
+
));
|
|
7306
|
+
if (replacePendingTurn) {
|
|
7307
|
+
const replaced = await this.outbound.failPendingTurn(
|
|
7308
|
+
record.localSessionId,
|
|
7309
|
+
"任务被占用。",
|
|
7310
|
+
card,
|
|
7311
|
+
);
|
|
7312
|
+
if (!replaced) await this.outbound.sendInteractiveCard(contextKey, card);
|
|
7313
|
+
} else {
|
|
7314
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
7315
|
+
}
|
|
7316
|
+
return true;
|
|
7317
|
+
}
|
|
7318
|
+
|
|
7319
|
+
private threadWriterConflictView(
|
|
7320
|
+
record: SessionRecord,
|
|
7321
|
+
contextKey: string,
|
|
7322
|
+
threadId: string,
|
|
7323
|
+
owners: ThreadWriterProcess[],
|
|
7324
|
+
status: ThreadWriterConflictCardView["status"],
|
|
7325
|
+
notice?: string,
|
|
7326
|
+
applicationIdle?: boolean,
|
|
7327
|
+
): ThreadWriterConflictCardView {
|
|
7328
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
7329
|
+
return {
|
|
7330
|
+
status,
|
|
7331
|
+
contextKey,
|
|
7332
|
+
sessionId: record.localSessionId,
|
|
7333
|
+
threadId,
|
|
7334
|
+
taskTitle: record.title,
|
|
7335
|
+
notice: notice ?? (owners.length > 1
|
|
7336
|
+
? `检测到 ${owners.length} 个相关进程,无法安全地自动关闭。`
|
|
7337
|
+
: undefined),
|
|
7338
|
+
applicationIdle,
|
|
7339
|
+
...(owner ? {
|
|
7340
|
+
owner: {
|
|
7341
|
+
displayName: owner.displayName,
|
|
7342
|
+
writerPid: owner.writerPid,
|
|
7343
|
+
writerProcessName: owner.writerProcessName,
|
|
7344
|
+
writerStartedAt: owner.writerStartedAt,
|
|
7345
|
+
applicationPid: owner.applicationPid,
|
|
7346
|
+
applicationProcessName: owner.applicationProcessName,
|
|
7347
|
+
applicationStartedAt: owner.applicationStartedAt,
|
|
7348
|
+
canClose: owner.canClose,
|
|
7349
|
+
},
|
|
7350
|
+
} : {}),
|
|
7351
|
+
};
|
|
7352
|
+
}
|
|
7353
|
+
|
|
7354
|
+
private async closeThreadWriter(action: CardAction, contextKey: string): Promise<void> {
|
|
7355
|
+
const messageId = requiredCardMessageId(action.messageId);
|
|
7356
|
+
const sessionId = String(action.value.sessionId ?? "");
|
|
7357
|
+
const threadId = String(action.value.threadId ?? "");
|
|
7358
|
+
const expectedWriterPid = positiveActionPid(action.value.writerPid);
|
|
7359
|
+
const expectedApplicationPid = positiveActionPid(action.value.applicationPid);
|
|
7360
|
+
const expectedWriterStartedAt = String(action.value.writerStartedAt ?? "");
|
|
7361
|
+
const expectedApplicationStartedAt = String(action.value.applicationStartedAt ?? "");
|
|
7362
|
+
const force = action.value.force === true || action.value.force === "true";
|
|
7363
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
7364
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
7365
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
7366
|
+
}
|
|
7367
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
7368
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
7369
|
+
if (!lockPath) throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
7370
|
+
|
|
7371
|
+
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
7372
|
+
const expectedOwner = owners.length === 1 && threadWriterMatches(
|
|
7373
|
+
owners[0],
|
|
7374
|
+
expectedWriterPid,
|
|
7375
|
+
expectedWriterStartedAt,
|
|
7376
|
+
expectedApplicationPid,
|
|
7377
|
+
expectedApplicationStartedAt,
|
|
7378
|
+
) ? owners[0] : undefined;
|
|
7379
|
+
if (!expectedOwner) {
|
|
7380
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
7381
|
+
const applicationIdle = currentOwner
|
|
7382
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
7383
|
+
: undefined;
|
|
7384
|
+
await this.outbound.updateInteractiveCard(
|
|
7385
|
+
contextKey,
|
|
7386
|
+
messageId,
|
|
7387
|
+
this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
|
|
7388
|
+
record,
|
|
7389
|
+
contextKey,
|
|
7390
|
+
threadId,
|
|
7391
|
+
owners,
|
|
7392
|
+
owners.length === 0 ? "released" : "occupied",
|
|
7393
|
+
owners.length === 0
|
|
7394
|
+
? "请重新发送消息。"
|
|
7395
|
+
: "占用进程已变化,请重试。",
|
|
7396
|
+
applicationIdle,
|
|
7397
|
+
)),
|
|
7398
|
+
);
|
|
7399
|
+
return;
|
|
7400
|
+
}
|
|
7401
|
+
if (!expectedOwner.canClose) throw new Error("无法确认占用者是 Agent 程序,已拒绝关闭。");
|
|
7402
|
+
|
|
7403
|
+
try {
|
|
7404
|
+
await this.threadWriterProcesses.close(expectedOwner, force);
|
|
7405
|
+
} catch (error) {
|
|
7406
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
7407
|
+
if (owners.some((owner) => threadWriterMatches(
|
|
7408
|
+
owner,
|
|
7409
|
+
expectedWriterPid,
|
|
7410
|
+
expectedWriterStartedAt,
|
|
7411
|
+
expectedApplicationPid,
|
|
7412
|
+
expectedApplicationStartedAt,
|
|
7413
|
+
))) throw error;
|
|
7414
|
+
}
|
|
7415
|
+
owners = await this.waitForThreadWriterChange(lockPath, expectedOwner);
|
|
7416
|
+
const sameOwnerRemains = owners.some((owner) => threadWriterMatches(
|
|
7417
|
+
owner,
|
|
7418
|
+
expectedOwner.writerPid,
|
|
7419
|
+
expectedOwner.writerStartedAt ?? "",
|
|
7420
|
+
expectedOwner.applicationPid,
|
|
7421
|
+
expectedOwner.applicationStartedAt ?? "",
|
|
7422
|
+
));
|
|
7423
|
+
if (sameOwnerRemains && force) {
|
|
7424
|
+
throw new Error(`无法关闭 ${expectedOwner.displayName}(PID ${expectedOwner.applicationPid})。`);
|
|
7425
|
+
}
|
|
7426
|
+
const status: ThreadWriterConflictCardView["status"] = owners.length === 0
|
|
7427
|
+
? "released"
|
|
7428
|
+
: sameOwnerRemains ? "force_required" : "occupied";
|
|
7429
|
+
const notice = owners.length === 0
|
|
7430
|
+
? "请重新发送消息。"
|
|
7431
|
+
: sameOwnerRemains
|
|
7432
|
+
? "普通关闭失败,可强制关闭。"
|
|
7433
|
+
: "占用进程已变化,请重试。";
|
|
7434
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
7435
|
+
const applicationIdle = currentOwner
|
|
7436
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
7437
|
+
: undefined;
|
|
7438
|
+
await this.outbound.updateInteractiveCard(
|
|
7439
|
+
contextKey,
|
|
7440
|
+
messageId,
|
|
7441
|
+
this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
|
|
7442
|
+
record,
|
|
7443
|
+
contextKey,
|
|
7444
|
+
threadId,
|
|
7445
|
+
owners,
|
|
7446
|
+
status,
|
|
7447
|
+
notice,
|
|
7448
|
+
applicationIdle,
|
|
7449
|
+
)),
|
|
7450
|
+
);
|
|
7451
|
+
}
|
|
7452
|
+
|
|
7453
|
+
private async inspectThreadWriterApplicationIdle(
|
|
7454
|
+
runtime: AgentRuntime,
|
|
7455
|
+
lockPath: string,
|
|
7456
|
+
threadId: string,
|
|
7457
|
+
owner: ThreadWriterProcess,
|
|
7458
|
+
): Promise<boolean | undefined> {
|
|
7459
|
+
if (!owner.applicationStartedAt
|
|
7460
|
+
|| !this.threadWriterProcesses.inspectApplicationThreadIds
|
|
7461
|
+
|| !runtime.inspectRemoteSessionActivity) return undefined;
|
|
7462
|
+
try {
|
|
7463
|
+
const ownedThreadIds = await this.threadWriterProcesses.inspectApplicationThreadIds(lockPath, owner);
|
|
7464
|
+
if (!ownedThreadIds.includes(threadId)) return undefined;
|
|
7465
|
+
const activities = await Promise.all(
|
|
7466
|
+
ownedThreadIds.map((ownedThreadId) => runtime.inspectRemoteSessionActivity!(ownedThreadId)),
|
|
7467
|
+
);
|
|
7468
|
+
return activities.every((activity) => !activity.active);
|
|
7469
|
+
} catch (error) {
|
|
7470
|
+
this.logger.warn(
|
|
7471
|
+
{ error, threadId, applicationPid: owner.applicationPid },
|
|
7472
|
+
"Failed to inspect thread-writer application activity.",
|
|
7473
|
+
);
|
|
7474
|
+
return undefined;
|
|
7475
|
+
}
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7478
|
+
private async waitForThreadWriterChange(
|
|
7479
|
+
lockPath: string,
|
|
7480
|
+
expectedOwner: ThreadWriterProcess,
|
|
7481
|
+
): Promise<ThreadWriterProcess[]> {
|
|
7482
|
+
const deadline = Date.now() + 5_000;
|
|
7483
|
+
let owners: ThreadWriterProcess[] = [];
|
|
7484
|
+
do {
|
|
7485
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
7486
|
+
if (!owners.some((owner) => threadWriterMatches(
|
|
7487
|
+
owner,
|
|
7488
|
+
expectedOwner.writerPid,
|
|
7489
|
+
expectedOwner.writerStartedAt ?? "",
|
|
7490
|
+
expectedOwner.applicationPid,
|
|
7491
|
+
expectedOwner.applicationStartedAt ?? "",
|
|
7492
|
+
))) return owners;
|
|
7493
|
+
await delay(250);
|
|
7494
|
+
} while (Date.now() < deadline);
|
|
7495
|
+
return owners;
|
|
7496
|
+
}
|
|
7497
|
+
|
|
7498
|
+
private async sendError(
|
|
7499
|
+
contextKey: string,
|
|
7500
|
+
error: unknown,
|
|
7501
|
+
details: RequestFailureDetails = {},
|
|
7502
|
+
): Promise<void> {
|
|
7503
|
+
if (error instanceof PresentedRequestError) {
|
|
7504
|
+
this.logger.warn(
|
|
7505
|
+
{ error: errorLogValue(error.originalError), contextKey, ...details },
|
|
7506
|
+
"Request failed after a dedicated error card was presented.",
|
|
7507
|
+
);
|
|
7508
|
+
return;
|
|
7509
|
+
}
|
|
7510
|
+
const record = this.currentSession(contextKey);
|
|
7511
|
+
if (record) {
|
|
7512
|
+
try {
|
|
7513
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, false)) return;
|
|
7514
|
+
} catch (presentationError) {
|
|
7515
|
+
this.logger.warn(
|
|
7516
|
+
{ error: presentationError, originalError: errorLogValue(error), contextKey, ...details },
|
|
7517
|
+
"Failed to present the thread writer conflict card.",
|
|
7518
|
+
);
|
|
7519
|
+
}
|
|
7520
|
+
}
|
|
7521
|
+
this.logger.warn({ error, contextKey, ...details }, "Request failed.");
|
|
7522
|
+
try {
|
|
7523
|
+
await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
|
|
7524
|
+
} catch (deliveryError) {
|
|
7525
|
+
this.logger.error(
|
|
7526
|
+
{
|
|
7527
|
+
error: deliveryError,
|
|
7528
|
+
originalError: errorLogValue(error),
|
|
7529
|
+
contextKey,
|
|
7530
|
+
...details,
|
|
7531
|
+
phase: "error_response_send",
|
|
7532
|
+
},
|
|
7533
|
+
"Failed to deliver the request failure to the user.",
|
|
7534
|
+
);
|
|
7535
|
+
}
|
|
7536
|
+
}
|
|
7537
|
+
}
|
|
7538
|
+
|
|
7539
|
+
function activeWriterThreadId(error: unknown): string | undefined {
|
|
7540
|
+
const visited = new Set<unknown>();
|
|
7541
|
+
let current = error;
|
|
7542
|
+
while (current !== undefined && current !== null && !visited.has(current)) {
|
|
7543
|
+
visited.add(current);
|
|
7544
|
+
const message = current instanceof Error
|
|
7545
|
+
? current.message
|
|
7546
|
+
: typeof current === "string" ? current : undefined;
|
|
7547
|
+
const match = message?.match(
|
|
7548
|
+
/thread(?:\/resume failed:\s*thread)?\s+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+already has an active writer/iu,
|
|
7549
|
+
);
|
|
7550
|
+
if (match?.[1]) return match[1];
|
|
7551
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
7552
|
+
}
|
|
7553
|
+
return undefined;
|
|
7554
|
+
}
|
|
7555
|
+
|
|
7556
|
+
function positiveActionPid(value: unknown): number {
|
|
7557
|
+
const pid = typeof value === "number" ? value : Number(value);
|
|
7558
|
+
if (!Number.isSafeInteger(pid) || pid <= 4) {
|
|
7559
|
+
throw new Error("任务占用卡片中的进程信息无效,请重新发送消息获取最新状态。");
|
|
7560
|
+
}
|
|
7561
|
+
return pid;
|
|
7562
|
+
}
|
|
7563
|
+
|
|
7564
|
+
function threadWriterMatches(
|
|
7565
|
+
owner: ThreadWriterProcess,
|
|
7566
|
+
writerPid: number,
|
|
7567
|
+
writerStartedAt: string,
|
|
7568
|
+
applicationPid: number,
|
|
7569
|
+
applicationStartedAt: string,
|
|
7570
|
+
): boolean {
|
|
7571
|
+
return Boolean(writerStartedAt)
|
|
7572
|
+
&& Boolean(applicationStartedAt)
|
|
7573
|
+
&& owner.writerPid === writerPid
|
|
7574
|
+
&& owner.writerStartedAt === writerStartedAt
|
|
7575
|
+
&& owner.applicationPid === applicationPid
|
|
7576
|
+
&& owner.applicationStartedAt === applicationStartedAt;
|
|
7577
|
+
}
|
|
7578
|
+
|
|
7579
|
+
function delay(milliseconds: number): Promise<void> {
|
|
7580
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
7581
|
+
}
|
|
7582
|
+
|
|
7583
|
+
function renderPlainHelp(introLines: string[], sections: HelpCardSection[]): string {
|
|
7584
|
+
return [
|
|
7585
|
+
"Agent Bot 使用帮助",
|
|
7586
|
+
"",
|
|
7587
|
+
...introLines.map((line) => decodeHelpEntities(line.replace(/^>\s*/u, "").replaceAll("**", ""))),
|
|
7588
|
+
...sections.flatMap((section) => [
|
|
7589
|
+
"",
|
|
7590
|
+
section.title,
|
|
7591
|
+
...section.commands.map((command) => [
|
|
7592
|
+
command.text,
|
|
7593
|
+
command.usage ? decodeHelpEntities(command.usage) : "",
|
|
7594
|
+
`- ${command.description}`,
|
|
7595
|
+
].filter(Boolean).join(" ")),
|
|
7596
|
+
]),
|
|
7597
|
+
].join("\n");
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
function decodeHelpEntities(value: string): string {
|
|
7601
|
+
return value
|
|
7602
|
+
.replaceAll("<", "<")
|
|
7603
|
+
.replaceAll(">", ">")
|
|
7604
|
+
.replaceAll("&", "&");
|
|
6643
7605
|
}
|
|
6644
7606
|
|
|
6645
7607
|
function threadForkAnchorMessageIds(message: IncomingMessage): string[] {
|
|
@@ -7194,7 +8156,7 @@ function isBotOwnedActiveTurn(record: SessionRecord, remote: RemoteSessionSummar
|
|
|
7194
8156
|
}
|
|
7195
8157
|
|
|
7196
8158
|
function isQueueIndependentCommand(command: Command): boolean {
|
|
7197
|
-
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;
|
|
7198
8160
|
if (command.type === "agent") return command.agent === undefined;
|
|
7199
8161
|
if (["model", "provider", "thinking", "permissions"].includes(command.type)) return true;
|
|
7200
8162
|
return false;
|
|
@@ -7423,6 +8385,17 @@ function referencedMessagePrompt(instruction: string, referencedMessage: string)
|
|
|
7423
8385
|
return `${instruction}\n\n引用消息:\n${referencedMessage}`;
|
|
7424
8386
|
}
|
|
7425
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
|
+
|
|
7426
8399
|
function quotedMessageId(message: IncomingMessage): string | undefined {
|
|
7427
8400
|
const messageId = message.parentMessageId?.trim();
|
|
7428
8401
|
if (!messageId || message.contextKey.startsWith("console:")) return undefined;
|