@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
|
@@ -3,6 +3,7 @@ import { promises as fs } from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createProjectlessWorkspace, detectProjectlessWorkspace } from "../codex/ProjectlessWorkspace.js";
|
|
6
|
+
import { SystemThreadWriterProcessController, } from "../codex/ThreadWriterProcess.js";
|
|
6
7
|
import { resolveUserPath } from "../config/paths.js";
|
|
7
8
|
import { DEFAULT_GROUP_NAME_FORMAT, } from "../config/schema.js";
|
|
8
9
|
import { CommandRouter } from "../commands/CommandRouter.js";
|
|
@@ -14,12 +15,21 @@ import { formatGroupNameDate, formatNewGroupName, parseTaskNameFromGroupName, }
|
|
|
14
15
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
15
16
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
16
17
|
import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
|
|
18
|
+
import { errorLogValue } from "../logging/errorLogValue.js";
|
|
17
19
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
18
20
|
import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
|
|
19
21
|
import { createId } from "../utils/id.js";
|
|
20
22
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
21
23
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
22
24
|
import { executeShellCommand, } from "../utils/executeShellCommand.js";
|
|
25
|
+
class PresentedRequestError extends Error {
|
|
26
|
+
originalError;
|
|
27
|
+
constructor(originalError) {
|
|
28
|
+
super(originalError instanceof Error ? originalError.message : String(originalError), { cause: originalError });
|
|
29
|
+
this.originalError = originalError;
|
|
30
|
+
this.name = "PresentedRequestError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
23
33
|
const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
24
34
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
25
35
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
@@ -28,12 +38,15 @@ const SESSION_PAGE_SIZE = 10;
|
|
|
28
38
|
const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
29
39
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
30
40
|
const RESET_HISTORY_PAGE_SIZE = 10;
|
|
41
|
+
const REMOTE_TURN_HISTORY_REFRESH_INTERVAL_MS = 30_000;
|
|
31
42
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
32
43
|
const QUEUED_PROMPT_RETRY_DELAY_MS = 5_000;
|
|
33
44
|
const RECOVERY_ACTIVITY_WINDOW_MS = 10 * 60 * 1_000;
|
|
34
45
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
35
46
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
36
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;
|
|
37
50
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
38
51
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
39
52
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -156,6 +169,10 @@ const HELP_COMMAND_SECTIONS = [
|
|
|
156
169
|
usage: "[--force]",
|
|
157
170
|
description: "默认安全重启;--force 立即重启",
|
|
158
171
|
},
|
|
172
|
+
{
|
|
173
|
+
command: "/release",
|
|
174
|
+
description: "释放 Agent Bot 占用的 App Server 任务",
|
|
175
|
+
},
|
|
159
176
|
{
|
|
160
177
|
command: "/mute",
|
|
161
178
|
usage: "[on|off]",
|
|
@@ -178,6 +195,7 @@ export class ProxySessionController {
|
|
|
178
195
|
shellCommandExecutor;
|
|
179
196
|
windowsDriveLister;
|
|
180
197
|
shellCommandJobs;
|
|
198
|
+
threadWriterProcesses;
|
|
181
199
|
router = new CommandRouter();
|
|
182
200
|
cardRenderer = new CardRenderer();
|
|
183
201
|
messageQueues = new Map();
|
|
@@ -187,6 +205,7 @@ export class ProxySessionController {
|
|
|
187
205
|
queuedPromptCardWrites = new Map();
|
|
188
206
|
sessionResets = new Map();
|
|
189
207
|
resetHistoryOperations = new Map();
|
|
208
|
+
remoteTurnHistoryRefreshes = new Map();
|
|
190
209
|
lastSessionListings = new Map();
|
|
191
210
|
threadInitializations = new Map();
|
|
192
211
|
llmRetryingSessions = new Set();
|
|
@@ -197,11 +216,13 @@ export class ProxySessionController {
|
|
|
197
216
|
pendingResourceForwards = new Map();
|
|
198
217
|
relatedReactionMessageIds = new Map();
|
|
199
218
|
shellCommandJobMonitors = new Map();
|
|
219
|
+
appServerReleaseSchedules = new Map();
|
|
220
|
+
nextAppServerReleaseScheduleId = 1;
|
|
200
221
|
unsubscribe = [];
|
|
201
222
|
recoveryActivityHeartbeat;
|
|
202
223
|
startupRecovery;
|
|
203
224
|
queuedPromptsRestored = false;
|
|
204
|
-
constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots, shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs"))) {
|
|
225
|
+
constructor(config, store, runtimes, outbound, logger, lifecycle, shellCommandExecutor = executeShellCommand, windowsDriveLister = listWindowsDriveRoots, shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs")), threadWriterProcesses = new SystemThreadWriterProcessController()) {
|
|
205
226
|
this.config = config;
|
|
206
227
|
this.store = store;
|
|
207
228
|
this.runtimes = runtimes;
|
|
@@ -211,6 +232,7 @@ export class ProxySessionController {
|
|
|
211
232
|
this.shellCommandExecutor = shellCommandExecutor;
|
|
212
233
|
this.windowsDriveLister = windowsDriveLister;
|
|
213
234
|
this.shellCommandJobs = shellCommandJobs;
|
|
235
|
+
this.threadWriterProcesses = threadWriterProcesses;
|
|
214
236
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
215
237
|
this.unsubscribe.push(runtime.onEvent((event) => {
|
|
216
238
|
void this.handleRuntimeEvent(event).catch((error) => {
|
|
@@ -328,7 +350,11 @@ export class ProxySessionController {
|
|
|
328
350
|
: undefined;
|
|
329
351
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
330
352
|
const detail = result.error instanceof Error ? result.error.message : String(result.error);
|
|
331
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)
|
|
353
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`), {
|
|
354
|
+
requestId: message.messageId,
|
|
355
|
+
messageId: message.messageId,
|
|
356
|
+
phase: "merged_forward_read",
|
|
357
|
+
}));
|
|
332
358
|
return;
|
|
333
359
|
}
|
|
334
360
|
const instruction = message.displayText ?? DEFAULT_MERGED_FORWARD_INSTRUCTION;
|
|
@@ -360,7 +386,11 @@ export class ProxySessionController {
|
|
|
360
386
|
: undefined;
|
|
361
387
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
362
388
|
const detail = error instanceof Error ? error.message : String(error);
|
|
363
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)
|
|
389
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`), {
|
|
390
|
+
requestId: message.messageId,
|
|
391
|
+
messageId: message.messageId,
|
|
392
|
+
phase: "referenced_message_read",
|
|
393
|
+
}));
|
|
364
394
|
return;
|
|
365
395
|
}
|
|
366
396
|
}
|
|
@@ -394,7 +424,11 @@ export class ProxySessionController {
|
|
|
394
424
|
}
|
|
395
425
|
catch (error) {
|
|
396
426
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
397
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error
|
|
427
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
|
|
428
|
+
requestId: message.messageId,
|
|
429
|
+
messageId: message.messageId,
|
|
430
|
+
phase: "image_download",
|
|
431
|
+
}));
|
|
398
432
|
return;
|
|
399
433
|
}
|
|
400
434
|
}
|
|
@@ -413,7 +447,11 @@ export class ProxySessionController {
|
|
|
413
447
|
}
|
|
414
448
|
catch (error) {
|
|
415
449
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
416
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error
|
|
450
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
|
|
451
|
+
requestId: message.messageId,
|
|
452
|
+
messageId: message.messageId,
|
|
453
|
+
phase: "file_download",
|
|
454
|
+
}));
|
|
417
455
|
return;
|
|
418
456
|
}
|
|
419
457
|
}
|
|
@@ -426,9 +464,21 @@ export class ProxySessionController {
|
|
|
426
464
|
}
|
|
427
465
|
catch (error) {
|
|
428
466
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
429
|
-
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error
|
|
467
|
+
await this.outbound.withReplyTarget(message.contextKey, replyTarget, () => this.sendError(message.contextKey, error, {
|
|
468
|
+
requestId: message.messageId,
|
|
469
|
+
messageId: message.messageId,
|
|
470
|
+
phase: "command_parse",
|
|
471
|
+
}));
|
|
430
472
|
return;
|
|
431
473
|
}
|
|
474
|
+
if (command.type !== "prompt") {
|
|
475
|
+
this.logger.info({
|
|
476
|
+
requestId: message.messageId,
|
|
477
|
+
messageId: message.messageId,
|
|
478
|
+
contextKey: message.contextKey,
|
|
479
|
+
command: command.type,
|
|
480
|
+
}, "Processing Agent Bot command.");
|
|
481
|
+
}
|
|
432
482
|
// Operational and read-only commands must remain available even if a prompt operation is slow.
|
|
433
483
|
if (isQueueIndependentCommand(command)) {
|
|
434
484
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
@@ -442,7 +492,12 @@ export class ProxySessionController {
|
|
|
442
492
|
}
|
|
443
493
|
catch (error) {
|
|
444
494
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
445
|
-
await this.sendError(message.contextKey, error
|
|
495
|
+
await this.sendError(message.contextKey, error, {
|
|
496
|
+
requestId: message.messageId,
|
|
497
|
+
messageId: message.messageId,
|
|
498
|
+
command: command.type,
|
|
499
|
+
phase: "command_execute",
|
|
500
|
+
});
|
|
446
501
|
}
|
|
447
502
|
});
|
|
448
503
|
return;
|
|
@@ -450,16 +505,41 @@ export class ProxySessionController {
|
|
|
450
505
|
const previous = this.messageQueues.get(message.contextKey) ?? Promise.resolve();
|
|
451
506
|
const next = previous.catch(() => undefined).then(() => this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
452
507
|
try {
|
|
453
|
-
|
|
508
|
+
let executableCommand = command;
|
|
509
|
+
let executableImagePaths = localImagePaths;
|
|
510
|
+
let executableMessage = message;
|
|
511
|
+
let injectedRootMessageId;
|
|
512
|
+
if (command.type === "prompt") {
|
|
454
513
|
await this.ensureThreadFork(message);
|
|
455
|
-
|
|
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
|
+
}
|
|
456
531
|
if (!commandDefersReactionFinalization(command)) {
|
|
457
532
|
await this.finalizeStandaloneMessageReaction(message.messageId, "completed");
|
|
458
533
|
}
|
|
459
534
|
}
|
|
460
535
|
catch (error) {
|
|
461
536
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
462
|
-
await this.sendError(message.contextKey, error
|
|
537
|
+
await this.sendError(message.contextKey, error, {
|
|
538
|
+
requestId: message.messageId,
|
|
539
|
+
messageId: message.messageId,
|
|
540
|
+
command: command.type,
|
|
541
|
+
phase: "command_execute",
|
|
542
|
+
});
|
|
463
543
|
}
|
|
464
544
|
}));
|
|
465
545
|
this.messageQueues.set(message.contextKey, next);
|
|
@@ -479,7 +559,11 @@ export class ProxySessionController {
|
|
|
479
559
|
resolvedAction = this.resolveCardActionBinding(action);
|
|
480
560
|
}
|
|
481
561
|
catch (error) {
|
|
482
|
-
await this.sendError(action.contextKey, error
|
|
562
|
+
await this.sendError(action.contextKey, error, {
|
|
563
|
+
requestId: action.actionId,
|
|
564
|
+
actionId: action.actionId,
|
|
565
|
+
phase: "card_action_resolve",
|
|
566
|
+
});
|
|
483
567
|
return;
|
|
484
568
|
}
|
|
485
569
|
const contextKey = this.cardActionContextKey(resolvedAction);
|
|
@@ -510,6 +594,24 @@ export class ProxySessionController {
|
|
|
510
594
|
else if (kind === "shell_command_cancel") {
|
|
511
595
|
await this.cancelShellCommandJob(contextKey, String(scopedAction.value.jobId ?? ""));
|
|
512
596
|
}
|
|
597
|
+
else if (kind === "thread_writer_close") {
|
|
598
|
+
await this.closeThreadWriter(scopedAction, contextKey);
|
|
599
|
+
}
|
|
600
|
+
else if (kind === "thread_writer_fork") {
|
|
601
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
602
|
+
const threadId = String(scopedAction.value.threadId ?? "");
|
|
603
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
604
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
605
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
606
|
+
}
|
|
607
|
+
await this.forkSessionReference(contextKey, sessionId);
|
|
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
|
+
}
|
|
513
615
|
else if (kind === "turn_reset") {
|
|
514
616
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
515
617
|
const turnId = String(scopedAction.value.turnId ?? "");
|
|
@@ -734,7 +836,12 @@ export class ProxySessionController {
|
|
|
734
836
|
}
|
|
735
837
|
}
|
|
736
838
|
catch (error) {
|
|
737
|
-
await this.sendError(contextKey, error
|
|
839
|
+
await this.sendError(contextKey, error, {
|
|
840
|
+
requestId: scopedAction.actionId,
|
|
841
|
+
actionId: scopedAction.actionId,
|
|
842
|
+
command: String(scopedAction.value.action ?? "unknown"),
|
|
843
|
+
phase: "card_action_execute",
|
|
844
|
+
});
|
|
738
845
|
}
|
|
739
846
|
});
|
|
740
847
|
}
|
|
@@ -760,6 +867,7 @@ export class ProxySessionController {
|
|
|
760
867
|
this.queuedPromptCardWrites.clear();
|
|
761
868
|
this.sessionResets.clear();
|
|
762
869
|
this.resetHistoryOperations.clear();
|
|
870
|
+
this.remoteTurnHistoryRefreshes.clear();
|
|
763
871
|
for (const pending of this.pendingMergedForwards.values())
|
|
764
872
|
pending.resolveAttachment(undefined);
|
|
765
873
|
this.pendingMergedForwards.clear();
|
|
@@ -773,6 +881,11 @@ export class ProxySessionController {
|
|
|
773
881
|
for (const timer of this.recoveryRetryTimers.values())
|
|
774
882
|
clearTimeout(timer);
|
|
775
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();
|
|
776
889
|
for (const [jobId] of this.shellCommandJobMonitors)
|
|
777
890
|
this.stopShellCommandJobMonitor(jobId);
|
|
778
891
|
clearInterval(this.recoveryActivityHeartbeat);
|
|
@@ -784,6 +897,19 @@ export class ProxySessionController {
|
|
|
784
897
|
await this.cancelSession(record);
|
|
785
898
|
return `Task stop requested: ${record.title ?? record.remoteSessionId ?? record.localSessionId}`;
|
|
786
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
|
+
}
|
|
787
913
|
async controlArchiveTask(localSessionId) {
|
|
788
914
|
const record = this.requireControlSession(localSessionId);
|
|
789
915
|
return this.archiveSessionReference(this.controlSessionContextKey(record), localSessionId, {
|
|
@@ -1377,6 +1503,9 @@ export class ProxySessionController {
|
|
|
1377
1503
|
throw new Error("当前运行方式不支持自动重启。");
|
|
1378
1504
|
await this.lifecycle.restart(contextKey, command.force === true, replyTarget);
|
|
1379
1505
|
return;
|
|
1506
|
+
case "release":
|
|
1507
|
+
await this.requestAppServerRelease(contextKey);
|
|
1508
|
+
return;
|
|
1380
1509
|
case "mute":
|
|
1381
1510
|
await this.setGroupMute(contextKey, command.enabled);
|
|
1382
1511
|
return;
|
|
@@ -1809,6 +1938,9 @@ export class ProxySessionController {
|
|
|
1809
1938
|
loaded = await this.loadSession(record);
|
|
1810
1939
|
}
|
|
1811
1940
|
catch (error) {
|
|
1941
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
1942
|
+
throw new PresentedRequestError(error);
|
|
1943
|
+
}
|
|
1812
1944
|
await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
|
|
1813
1945
|
throw error;
|
|
1814
1946
|
}
|
|
@@ -2021,6 +2153,49 @@ export class ProxySessionController {
|
|
|
2021
2153
|
}
|
|
2022
2154
|
}
|
|
2023
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
|
+
}
|
|
2024
2199
|
async forkThreadSession(message) {
|
|
2025
2200
|
const { anchor, source, snapshot } = this.resolveThreadForkAnchor(message);
|
|
2026
2201
|
const agent = this.ensureAgent(source.agentName);
|
|
@@ -2515,6 +2690,38 @@ export class ProxySessionController {
|
|
|
2515
2690
|
this.logger.warn({ error, localSessionId: current.localSessionId, sourceRemoteSessionId: source.sourceRemoteSessionId }, "Failed to hydrate legacy Fork Turn history; rendering the locally available history.");
|
|
2516
2691
|
}
|
|
2517
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
|
+
}
|
|
2518
2725
|
async createProjectSessionFromReference(contextKey, reference) {
|
|
2519
2726
|
const resolved = await this.resolveProjectSessionReference(contextKey, reference);
|
|
2520
2727
|
const created = await this.createSession(contextKey, resolved.agentName, resolved.cwd, true, false, undefined, undefined, undefined, resolved.executionSettings);
|
|
@@ -2900,6 +3107,8 @@ export class ProxySessionController {
|
|
|
2900
3107
|
});
|
|
2901
3108
|
}
|
|
2902
3109
|
catch (error) {
|
|
3110
|
+
if (activeWriterThreadId(error))
|
|
3111
|
+
throw error;
|
|
2903
3112
|
if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error)))
|
|
2904
3113
|
throw error;
|
|
2905
3114
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
@@ -4197,6 +4406,7 @@ export class ProxySessionController {
|
|
|
4197
4406
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4198
4407
|
}
|
|
4199
4408
|
await this.hydrateLegacyForkTurnHistory(contextKey, current);
|
|
4409
|
+
await this.hydrateRemoteTurnHistory(contextKey, current);
|
|
4200
4410
|
const completedTurns = this.store.listTaskTurnGraph(current.localSessionId);
|
|
4201
4411
|
const completedTurnIds = new Set(completedTurns.map((turn) => turn.turnId));
|
|
4202
4412
|
const runningTurnId = current.lastTurnStatus === "running" && current.lastTurnId
|
|
@@ -4514,7 +4724,7 @@ export class ProxySessionController {
|
|
|
4514
4724
|
if (!runtime.listRemoteSessions)
|
|
4515
4725
|
return;
|
|
4516
4726
|
try {
|
|
4517
|
-
const result = await
|
|
4727
|
+
const result = await this.listRemoteSessionsThroughCursor(runtime, searchTerm, fetchLimit);
|
|
4518
4728
|
remoteSessions.push(...result.sessions.map((session) => ({ agentName, session })));
|
|
4519
4729
|
remoteHasMore ||= Boolean(result.nextCursor);
|
|
4520
4730
|
}
|
|
@@ -4712,6 +4922,37 @@ export class ProxySessionController {
|
|
|
4712
4922
|
}
|
|
4713
4923
|
}
|
|
4714
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
|
+
}
|
|
4715
4956
|
async refreshSessionsCardFromAction(action, options = {}) {
|
|
4716
4957
|
if (!action.messageId)
|
|
4717
4958
|
return;
|
|
@@ -5262,14 +5503,26 @@ export class ProxySessionController {
|
|
|
5262
5503
|
description: command.description,
|
|
5263
5504
|
})),
|
|
5264
5505
|
}));
|
|
5265
|
-
|
|
5506
|
+
const introLines = [
|
|
5266
5507
|
"直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
|
|
5267
5508
|
"点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
|
|
5268
5509
|
"> 命令前缀示例:/sess 等同于 /sessions。",
|
|
5269
5510
|
"> 命令缩写示例:/fg 等同于 /forkgroup。",
|
|
5270
5511
|
"前缀或缩写匹配多个命令时,需要输入更长的形式。",
|
|
5271
5512
|
"**[参数]** 可选 **<参数>** 必填",
|
|
5272
|
-
]
|
|
5513
|
+
];
|
|
5514
|
+
try {
|
|
5515
|
+
await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard("Agent Bot 使用帮助", introLines, sections));
|
|
5516
|
+
}
|
|
5517
|
+
catch (cardError) {
|
|
5518
|
+
this.logger.warn({ error: cardError, contextKey, command: "help", phase: "help_card_send" }, "Failed to send the help card; falling back to plain text.");
|
|
5519
|
+
try {
|
|
5520
|
+
await this.outbound.sendText(contextKey, renderPlainHelp(introLines, sections));
|
|
5521
|
+
}
|
|
5522
|
+
catch (textError) {
|
|
5523
|
+
throw new AggregateError([cardError, textError], "帮助卡片和纯文本帮助均发送失败。");
|
|
5524
|
+
}
|
|
5525
|
+
}
|
|
5273
5526
|
}
|
|
5274
5527
|
async setGroupMute(contextKey, enabled) {
|
|
5275
5528
|
const chatContextKey = baseChatContextKey(contextKey);
|
|
@@ -5375,11 +5628,506 @@ export class ProxySessionController {
|
|
|
5375
5628
|
throw new Error(`找不到任务:${sessionId}`);
|
|
5376
5629
|
return record;
|
|
5377
5630
|
}
|
|
5378
|
-
async
|
|
5379
|
-
|
|
5380
|
-
|
|
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
|
+
}
|
|
5900
|
+
async presentThreadWriterConflict(record, contextKey, error, replacePendingTurn) {
|
|
5901
|
+
if (contextKey.startsWith("console:"))
|
|
5902
|
+
return false;
|
|
5903
|
+
const threadId = activeWriterThreadId(error);
|
|
5904
|
+
if (!threadId || record.remoteSessionId !== threadId)
|
|
5905
|
+
return false;
|
|
5906
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5907
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
5908
|
+
let owners = [];
|
|
5909
|
+
if (lockPath) {
|
|
5910
|
+
try {
|
|
5911
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5912
|
+
}
|
|
5913
|
+
catch (inspectionError) {
|
|
5914
|
+
this.logger.warn({ error: inspectionError, threadId, lockPath }, "Failed to inspect the process holding a thread writer lock.");
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
5918
|
+
const applicationIdle = owner && lockPath
|
|
5919
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner)
|
|
5920
|
+
: undefined;
|
|
5921
|
+
const card = this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, "occupied", lockPath
|
|
5922
|
+
? undefined
|
|
5923
|
+
: "无法识别占用进程。", applicationIdle));
|
|
5924
|
+
if (replacePendingTurn) {
|
|
5925
|
+
const replaced = await this.outbound.failPendingTurn(record.localSessionId, "任务被占用。", card);
|
|
5926
|
+
if (!replaced)
|
|
5927
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
5928
|
+
}
|
|
5929
|
+
else {
|
|
5930
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
5931
|
+
}
|
|
5932
|
+
return true;
|
|
5933
|
+
}
|
|
5934
|
+
threadWriterConflictView(record, contextKey, threadId, owners, status, notice, applicationIdle) {
|
|
5935
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
5936
|
+
return {
|
|
5937
|
+
status,
|
|
5938
|
+
contextKey,
|
|
5939
|
+
sessionId: record.localSessionId,
|
|
5940
|
+
threadId,
|
|
5941
|
+
taskTitle: record.title,
|
|
5942
|
+
notice: notice ?? (owners.length > 1
|
|
5943
|
+
? `检测到 ${owners.length} 个相关进程,无法安全地自动关闭。`
|
|
5944
|
+
: undefined),
|
|
5945
|
+
applicationIdle,
|
|
5946
|
+
...(owner ? {
|
|
5947
|
+
owner: {
|
|
5948
|
+
displayName: owner.displayName,
|
|
5949
|
+
writerPid: owner.writerPid,
|
|
5950
|
+
writerProcessName: owner.writerProcessName,
|
|
5951
|
+
writerStartedAt: owner.writerStartedAt,
|
|
5952
|
+
applicationPid: owner.applicationPid,
|
|
5953
|
+
applicationProcessName: owner.applicationProcessName,
|
|
5954
|
+
applicationStartedAt: owner.applicationStartedAt,
|
|
5955
|
+
canClose: owner.canClose,
|
|
5956
|
+
},
|
|
5957
|
+
} : {}),
|
|
5958
|
+
};
|
|
5959
|
+
}
|
|
5960
|
+
async closeThreadWriter(action, contextKey) {
|
|
5961
|
+
const messageId = requiredCardMessageId(action.messageId);
|
|
5962
|
+
const sessionId = String(action.value.sessionId ?? "");
|
|
5963
|
+
const threadId = String(action.value.threadId ?? "");
|
|
5964
|
+
const expectedWriterPid = positiveActionPid(action.value.writerPid);
|
|
5965
|
+
const expectedApplicationPid = positiveActionPid(action.value.applicationPid);
|
|
5966
|
+
const expectedWriterStartedAt = String(action.value.writerStartedAt ?? "");
|
|
5967
|
+
const expectedApplicationStartedAt = String(action.value.applicationStartedAt ?? "");
|
|
5968
|
+
const force = action.value.force === true || action.value.force === "true";
|
|
5969
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
5970
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
5971
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
5972
|
+
}
|
|
5973
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5974
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
5975
|
+
if (!lockPath)
|
|
5976
|
+
throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
5977
|
+
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5978
|
+
const expectedOwner = owners.length === 1 && threadWriterMatches(owners[0], expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt) ? owners[0] : undefined;
|
|
5979
|
+
if (!expectedOwner) {
|
|
5980
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
5981
|
+
const applicationIdle = currentOwner
|
|
5982
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
5983
|
+
: undefined;
|
|
5984
|
+
await this.outbound.updateInteractiveCard(contextKey, messageId, this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, owners.length === 0 ? "released" : "occupied", owners.length === 0
|
|
5985
|
+
? "请重新发送消息。"
|
|
5986
|
+
: "占用进程已变化,请重试。", applicationIdle)));
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
if (!expectedOwner.canClose)
|
|
5990
|
+
throw new Error("无法确认占用者是 Agent 程序,已拒绝关闭。");
|
|
5991
|
+
try {
|
|
5992
|
+
await this.threadWriterProcesses.close(expectedOwner, force);
|
|
5993
|
+
}
|
|
5994
|
+
catch (error) {
|
|
5995
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
5996
|
+
if (owners.some((owner) => threadWriterMatches(owner, expectedWriterPid, expectedWriterStartedAt, expectedApplicationPid, expectedApplicationStartedAt)))
|
|
5997
|
+
throw error;
|
|
5998
|
+
}
|
|
5999
|
+
owners = await this.waitForThreadWriterChange(lockPath, expectedOwner);
|
|
6000
|
+
const sameOwnerRemains = owners.some((owner) => threadWriterMatches(owner, expectedOwner.writerPid, expectedOwner.writerStartedAt ?? "", expectedOwner.applicationPid, expectedOwner.applicationStartedAt ?? ""));
|
|
6001
|
+
if (sameOwnerRemains && force) {
|
|
6002
|
+
throw new Error(`无法关闭 ${expectedOwner.displayName}(PID ${expectedOwner.applicationPid})。`);
|
|
6003
|
+
}
|
|
6004
|
+
const status = owners.length === 0
|
|
6005
|
+
? "released"
|
|
6006
|
+
: sameOwnerRemains ? "force_required" : "occupied";
|
|
6007
|
+
const notice = owners.length === 0
|
|
6008
|
+
? "请重新发送消息。"
|
|
6009
|
+
: sameOwnerRemains
|
|
6010
|
+
? "普通关闭失败,可强制关闭。"
|
|
6011
|
+
: "占用进程已变化,请重试。";
|
|
6012
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
6013
|
+
const applicationIdle = currentOwner
|
|
6014
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
6015
|
+
: undefined;
|
|
6016
|
+
await this.outbound.updateInteractiveCard(contextKey, messageId, this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(record, contextKey, threadId, owners, status, notice, applicationIdle)));
|
|
6017
|
+
}
|
|
6018
|
+
async inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner) {
|
|
6019
|
+
if (!owner.applicationStartedAt
|
|
6020
|
+
|| !this.threadWriterProcesses.inspectApplicationThreadIds
|
|
6021
|
+
|| !runtime.inspectRemoteSessionActivity)
|
|
6022
|
+
return undefined;
|
|
6023
|
+
try {
|
|
6024
|
+
const ownedThreadIds = await this.threadWriterProcesses.inspectApplicationThreadIds(lockPath, owner);
|
|
6025
|
+
if (!ownedThreadIds.includes(threadId))
|
|
6026
|
+
return undefined;
|
|
6027
|
+
const activities = await Promise.all(ownedThreadIds.map((ownedThreadId) => runtime.inspectRemoteSessionActivity(ownedThreadId)));
|
|
6028
|
+
return activities.every((activity) => !activity.active);
|
|
6029
|
+
}
|
|
6030
|
+
catch (error) {
|
|
6031
|
+
this.logger.warn({ error, threadId, applicationPid: owner.applicationPid }, "Failed to inspect thread-writer application activity.");
|
|
6032
|
+
return undefined;
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
async waitForThreadWriterChange(lockPath, expectedOwner) {
|
|
6036
|
+
const deadline = Date.now() + 5_000;
|
|
6037
|
+
let owners = [];
|
|
6038
|
+
do {
|
|
6039
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
6040
|
+
if (!owners.some((owner) => threadWriterMatches(owner, expectedOwner.writerPid, expectedOwner.writerStartedAt ?? "", expectedOwner.applicationPid, expectedOwner.applicationStartedAt ?? "")))
|
|
6041
|
+
return owners;
|
|
6042
|
+
await delay(250);
|
|
6043
|
+
} while (Date.now() < deadline);
|
|
6044
|
+
return owners;
|
|
6045
|
+
}
|
|
6046
|
+
async sendError(contextKey, error, details = {}) {
|
|
6047
|
+
if (error instanceof PresentedRequestError) {
|
|
6048
|
+
this.logger.warn({ error: errorLogValue(error.originalError), contextKey, ...details }, "Request failed after a dedicated error card was presented.");
|
|
6049
|
+
return;
|
|
6050
|
+
}
|
|
6051
|
+
const record = this.currentSession(contextKey);
|
|
6052
|
+
if (record) {
|
|
6053
|
+
try {
|
|
6054
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, false))
|
|
6055
|
+
return;
|
|
6056
|
+
}
|
|
6057
|
+
catch (presentationError) {
|
|
6058
|
+
this.logger.warn({ error: presentationError, originalError: errorLogValue(error), contextKey, ...details }, "Failed to present the thread writer conflict card.");
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
this.logger.warn({ error, contextKey, ...details }, "Request failed.");
|
|
6062
|
+
try {
|
|
6063
|
+
await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
|
|
6064
|
+
}
|
|
6065
|
+
catch (deliveryError) {
|
|
6066
|
+
this.logger.error({
|
|
6067
|
+
error: deliveryError,
|
|
6068
|
+
originalError: errorLogValue(error),
|
|
6069
|
+
contextKey,
|
|
6070
|
+
...details,
|
|
6071
|
+
phase: "error_response_send",
|
|
6072
|
+
}, "Failed to deliver the request failure to the user.");
|
|
6073
|
+
}
|
|
5381
6074
|
}
|
|
5382
6075
|
}
|
|
6076
|
+
function activeWriterThreadId(error) {
|
|
6077
|
+
const visited = new Set();
|
|
6078
|
+
let current = error;
|
|
6079
|
+
while (current !== undefined && current !== null && !visited.has(current)) {
|
|
6080
|
+
visited.add(current);
|
|
6081
|
+
const message = current instanceof Error
|
|
6082
|
+
? current.message
|
|
6083
|
+
: typeof current === "string" ? current : undefined;
|
|
6084
|
+
const match = message?.match(/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);
|
|
6085
|
+
if (match?.[1])
|
|
6086
|
+
return match[1];
|
|
6087
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
6088
|
+
}
|
|
6089
|
+
return undefined;
|
|
6090
|
+
}
|
|
6091
|
+
function positiveActionPid(value) {
|
|
6092
|
+
const pid = typeof value === "number" ? value : Number(value);
|
|
6093
|
+
if (!Number.isSafeInteger(pid) || pid <= 4) {
|
|
6094
|
+
throw new Error("任务占用卡片中的进程信息无效,请重新发送消息获取最新状态。");
|
|
6095
|
+
}
|
|
6096
|
+
return pid;
|
|
6097
|
+
}
|
|
6098
|
+
function threadWriterMatches(owner, writerPid, writerStartedAt, applicationPid, applicationStartedAt) {
|
|
6099
|
+
return Boolean(writerStartedAt)
|
|
6100
|
+
&& Boolean(applicationStartedAt)
|
|
6101
|
+
&& owner.writerPid === writerPid
|
|
6102
|
+
&& owner.writerStartedAt === writerStartedAt
|
|
6103
|
+
&& owner.applicationPid === applicationPid
|
|
6104
|
+
&& owner.applicationStartedAt === applicationStartedAt;
|
|
6105
|
+
}
|
|
6106
|
+
function delay(milliseconds) {
|
|
6107
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
6108
|
+
}
|
|
6109
|
+
function renderPlainHelp(introLines, sections) {
|
|
6110
|
+
return [
|
|
6111
|
+
"Agent Bot 使用帮助",
|
|
6112
|
+
"",
|
|
6113
|
+
...introLines.map((line) => decodeHelpEntities(line.replace(/^>\s*/u, "").replaceAll("**", ""))),
|
|
6114
|
+
...sections.flatMap((section) => [
|
|
6115
|
+
"",
|
|
6116
|
+
section.title,
|
|
6117
|
+
...section.commands.map((command) => [
|
|
6118
|
+
command.text,
|
|
6119
|
+
command.usage ? decodeHelpEntities(command.usage) : "",
|
|
6120
|
+
`- ${command.description}`,
|
|
6121
|
+
].filter(Boolean).join(" ")),
|
|
6122
|
+
]),
|
|
6123
|
+
].join("\n");
|
|
6124
|
+
}
|
|
6125
|
+
function decodeHelpEntities(value) {
|
|
6126
|
+
return value
|
|
6127
|
+
.replaceAll("<", "<")
|
|
6128
|
+
.replaceAll(">", ">")
|
|
6129
|
+
.replaceAll("&", "&");
|
|
6130
|
+
}
|
|
5383
6131
|
function threadForkAnchorMessageIds(message) {
|
|
5384
6132
|
return [...new Set([message.rootMessageId, message.parentMessageId]
|
|
5385
6133
|
.filter((messageId) => Boolean(messageId && messageId !== message.messageId)))];
|
|
@@ -5865,7 +6613,7 @@ function isBotOwnedActiveTurn(record, remote) {
|
|
|
5865
6613
|
&& record.lastTurnId === remote.lastTurnId;
|
|
5866
6614
|
}
|
|
5867
6615
|
function isQueueIndependentCommand(command) {
|
|
5868
|
-
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))
|
|
5869
6617
|
return true;
|
|
5870
6618
|
if (command.type === "agent")
|
|
5871
6619
|
return command.agent === undefined;
|
|
@@ -6077,6 +6825,17 @@ function mergedForwardPrompt(instruction, transcript) {
|
|
|
6077
6825
|
function referencedMessagePrompt(instruction, referencedMessage) {
|
|
6078
6826
|
return `${instruction}\n\n引用消息:\n${referencedMessage}`;
|
|
6079
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
|
+
}
|
|
6080
6839
|
function quotedMessageId(message) {
|
|
6081
6840
|
const messageId = message.parentMessageId?.trim();
|
|
6082
6841
|
if (!messageId || message.contextKey.startsWith("console:"))
|