@keyou007/agent-bot 0.1.18 → 0.1.19-alpha.0
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 +6 -0
- package/dist/codex/CodexRuntime.js +8 -3
- 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/console/ConsoleTurnPresenter.js +1 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +83 -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/index.js +2 -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 +329 -15
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +1 -1
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/codex/CodexRuntime.ts +8 -2
- package/src/codex/ThreadWriterProcess.ts +385 -0
- package/src/console/ConsoleTurnPresenter.ts +2 -1
- package/src/feishu/CardRenderer.ts +109 -0
- package/src/feishu/FeishuTurnPresenter.ts +16 -3
- package/src/index.ts +2 -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 +459 -24
- package/src/runtime/types.ts +1 -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,
|
|
@@ -31,6 +36,7 @@ import {
|
|
|
31
36
|
type SessionTaskCardGroup,
|
|
32
37
|
type ShellCommandCardView,
|
|
33
38
|
type TaskListCardAction,
|
|
39
|
+
type ThreadWriterConflictCardView,
|
|
34
40
|
} from "../feishu/CardRenderer.js";
|
|
35
41
|
import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
|
|
36
42
|
import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
|
|
@@ -42,6 +48,7 @@ import {
|
|
|
42
48
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
43
49
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
44
50
|
import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
|
|
51
|
+
import { errorLogValue } from "../logging/errorLogValue.js";
|
|
45
52
|
import type { OutboundRouter } from "../presentation/OutboundRouter.js";
|
|
46
53
|
import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.js";
|
|
47
54
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
@@ -116,6 +123,21 @@ interface ShellCommandJobMonitor {
|
|
|
116
123
|
refreshing: boolean;
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
interface RequestFailureDetails {
|
|
127
|
+
requestId?: string;
|
|
128
|
+
messageId?: string;
|
|
129
|
+
actionId?: string;
|
|
130
|
+
command?: string;
|
|
131
|
+
phase?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class PresentedRequestError extends Error {
|
|
135
|
+
constructor(public readonly originalError: unknown) {
|
|
136
|
+
super(originalError instanceof Error ? originalError.message : String(originalError), { cause: originalError });
|
|
137
|
+
this.name = "PresentedRequestError";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
119
141
|
const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
120
142
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
121
143
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
@@ -452,6 +474,7 @@ export class ProxySessionController {
|
|
|
452
474
|
private readonly shellCommandJobs: ShellCommandJobManagerLike = new ShellCommandJobManager(
|
|
453
475
|
path.join(path.dirname(config.storage.sqlitePath), "command-jobs"),
|
|
454
476
|
),
|
|
477
|
+
private readonly threadWriterProcesses: ThreadWriterProcessController = new SystemThreadWriterProcessController(),
|
|
455
478
|
) {
|
|
456
479
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
457
480
|
this.unsubscribe.push(
|
|
@@ -608,7 +631,11 @@ export class ProxySessionController {
|
|
|
608
631
|
await this.outbound.withReplyTarget(
|
|
609
632
|
message.contextKey,
|
|
610
633
|
replyTarget,
|
|
611
|
-
() => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`)
|
|
634
|
+
() => this.sendError(message.contextKey, new Error(`无法读取合并转发消息:${detail}`), {
|
|
635
|
+
requestId: message.messageId,
|
|
636
|
+
messageId: message.messageId,
|
|
637
|
+
phase: "merged_forward_read",
|
|
638
|
+
}),
|
|
612
639
|
);
|
|
613
640
|
return;
|
|
614
641
|
}
|
|
@@ -643,7 +670,11 @@ export class ProxySessionController {
|
|
|
643
670
|
await this.outbound.withReplyTarget(
|
|
644
671
|
message.contextKey,
|
|
645
672
|
replyTarget,
|
|
646
|
-
() => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`)
|
|
673
|
+
() => this.sendError(message.contextKey, new Error(`无法读取引用消息:${detail}`), {
|
|
674
|
+
requestId: message.messageId,
|
|
675
|
+
messageId: message.messageId,
|
|
676
|
+
phase: "referenced_message_read",
|
|
677
|
+
}),
|
|
647
678
|
);
|
|
648
679
|
return;
|
|
649
680
|
}
|
|
@@ -681,7 +712,11 @@ export class ProxySessionController {
|
|
|
681
712
|
await this.outbound.withReplyTarget(
|
|
682
713
|
message.contextKey,
|
|
683
714
|
replyTarget,
|
|
684
|
-
() => this.sendError(message.contextKey, error
|
|
715
|
+
() => this.sendError(message.contextKey, error, {
|
|
716
|
+
requestId: message.messageId,
|
|
717
|
+
messageId: message.messageId,
|
|
718
|
+
phase: "image_download",
|
|
719
|
+
}),
|
|
685
720
|
);
|
|
686
721
|
return;
|
|
687
722
|
}
|
|
@@ -708,7 +743,11 @@ export class ProxySessionController {
|
|
|
708
743
|
await this.outbound.withReplyTarget(
|
|
709
744
|
message.contextKey,
|
|
710
745
|
replyTarget,
|
|
711
|
-
() => this.sendError(message.contextKey, error
|
|
746
|
+
() => this.sendError(message.contextKey, error, {
|
|
747
|
+
requestId: message.messageId,
|
|
748
|
+
messageId: message.messageId,
|
|
749
|
+
phase: "file_download",
|
|
750
|
+
}),
|
|
712
751
|
);
|
|
713
752
|
return;
|
|
714
753
|
}
|
|
@@ -724,10 +763,25 @@ export class ProxySessionController {
|
|
|
724
763
|
await this.outbound.withReplyTarget(
|
|
725
764
|
message.contextKey,
|
|
726
765
|
replyTarget,
|
|
727
|
-
() => this.sendError(message.contextKey, error
|
|
766
|
+
() => this.sendError(message.contextKey, error, {
|
|
767
|
+
requestId: message.messageId,
|
|
768
|
+
messageId: message.messageId,
|
|
769
|
+
phase: "command_parse",
|
|
770
|
+
}),
|
|
728
771
|
);
|
|
729
772
|
return;
|
|
730
773
|
}
|
|
774
|
+
if (command.type !== "prompt") {
|
|
775
|
+
this.logger.info(
|
|
776
|
+
{
|
|
777
|
+
requestId: message.messageId,
|
|
778
|
+
messageId: message.messageId,
|
|
779
|
+
contextKey: message.contextKey,
|
|
780
|
+
command: command.type,
|
|
781
|
+
},
|
|
782
|
+
"Processing Agent Bot command.",
|
|
783
|
+
);
|
|
784
|
+
}
|
|
731
785
|
// Operational and read-only commands must remain available even if a prompt operation is slow.
|
|
732
786
|
if (isQueueIndependentCommand(command)) {
|
|
733
787
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
@@ -747,7 +801,12 @@ export class ProxySessionController {
|
|
|
747
801
|
}
|
|
748
802
|
} catch (error) {
|
|
749
803
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
750
|
-
await this.sendError(message.contextKey, error
|
|
804
|
+
await this.sendError(message.contextKey, error, {
|
|
805
|
+
requestId: message.messageId,
|
|
806
|
+
messageId: message.messageId,
|
|
807
|
+
command: command.type,
|
|
808
|
+
phase: "command_execute",
|
|
809
|
+
});
|
|
751
810
|
}
|
|
752
811
|
});
|
|
753
812
|
return;
|
|
@@ -772,7 +831,12 @@ export class ProxySessionController {
|
|
|
772
831
|
}
|
|
773
832
|
} catch (error) {
|
|
774
833
|
await this.finalizeStandaloneMessageReaction(message.messageId, "failed");
|
|
775
|
-
await this.sendError(message.contextKey, error
|
|
834
|
+
await this.sendError(message.contextKey, error, {
|
|
835
|
+
requestId: message.messageId,
|
|
836
|
+
messageId: message.messageId,
|
|
837
|
+
command: command.type,
|
|
838
|
+
phase: "command_execute",
|
|
839
|
+
});
|
|
776
840
|
}
|
|
777
841
|
}),
|
|
778
842
|
);
|
|
@@ -794,7 +858,11 @@ export class ProxySessionController {
|
|
|
794
858
|
try {
|
|
795
859
|
resolvedAction = this.resolveCardActionBinding(action);
|
|
796
860
|
} catch (error) {
|
|
797
|
-
await this.sendError(action.contextKey, error
|
|
861
|
+
await this.sendError(action.contextKey, error, {
|
|
862
|
+
requestId: action.actionId,
|
|
863
|
+
actionId: action.actionId,
|
|
864
|
+
phase: "card_action_resolve",
|
|
865
|
+
});
|
|
798
866
|
return;
|
|
799
867
|
}
|
|
800
868
|
const contextKey = this.cardActionContextKey(resolvedAction);
|
|
@@ -829,6 +897,16 @@ export class ProxySessionController {
|
|
|
829
897
|
contextKey,
|
|
830
898
|
String(scopedAction.value.jobId ?? ""),
|
|
831
899
|
);
|
|
900
|
+
} else if (kind === "thread_writer_close") {
|
|
901
|
+
await this.closeThreadWriter(scopedAction, contextKey);
|
|
902
|
+
} else if (kind === "thread_writer_fork") {
|
|
903
|
+
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
904
|
+
const threadId = String(scopedAction.value.threadId ?? "");
|
|
905
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
906
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
907
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
908
|
+
}
|
|
909
|
+
await this.forkSessionReference(contextKey, sessionId);
|
|
832
910
|
} else if (kind === "turn_reset") {
|
|
833
911
|
const sessionId = String(scopedAction.value.sessionId ?? "");
|
|
834
912
|
const turnId = String(scopedAction.value.turnId ?? "");
|
|
@@ -1069,7 +1147,12 @@ export class ProxySessionController {
|
|
|
1069
1147
|
await this.resolveApproval(scopedAction);
|
|
1070
1148
|
}
|
|
1071
1149
|
} catch (error) {
|
|
1072
|
-
await this.sendError(contextKey, error
|
|
1150
|
+
await this.sendError(contextKey, error, {
|
|
1151
|
+
requestId: scopedAction.actionId,
|
|
1152
|
+
actionId: scopedAction.actionId,
|
|
1153
|
+
command: String(scopedAction.value.action ?? "unknown"),
|
|
1154
|
+
phase: "card_action_execute",
|
|
1155
|
+
});
|
|
1073
1156
|
}
|
|
1074
1157
|
});
|
|
1075
1158
|
}
|
|
@@ -2401,6 +2484,9 @@ export class ProxySessionController {
|
|
|
2401
2484
|
try {
|
|
2402
2485
|
loaded = await this.loadSession(record);
|
|
2403
2486
|
} catch (error) {
|
|
2487
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, true)) {
|
|
2488
|
+
throw new PresentedRequestError(error);
|
|
2489
|
+
}
|
|
2404
2490
|
await this.outbound.failPendingTurn(record.localSessionId, error instanceof Error ? error.message : String(error));
|
|
2405
2491
|
throw error;
|
|
2406
2492
|
}
|
|
@@ -3809,6 +3895,7 @@ export class ProxySessionController {
|
|
|
3809
3895
|
lastTurnStatus: record.lastTurnStatus,
|
|
3810
3896
|
});
|
|
3811
3897
|
} catch (error) {
|
|
3898
|
+
if (activeWriterThreadId(error)) throw error;
|
|
3812
3899
|
if (!(agent.kind === "app-server" && !record.lastTurnId && isMissingRolloutError(error))) throw error;
|
|
3813
3900
|
this.logger.warn({ error, sessionId: record.localSessionId }, "App Server task has no rollout; creating a replacement task.");
|
|
3814
3901
|
session = await runtime.createSession({
|
|
@@ -6509,18 +6596,34 @@ export class ProxySessionController {
|
|
|
6509
6596
|
description: command.description,
|
|
6510
6597
|
})),
|
|
6511
6598
|
}));
|
|
6512
|
-
|
|
6513
|
-
"
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6599
|
+
const introLines = [
|
|
6600
|
+
"直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
|
|
6601
|
+
"点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
|
|
6602
|
+
"> 命令前缀示例:/sess 等同于 /sessions。",
|
|
6603
|
+
"> 命令缩写示例:/fg 等同于 /forkgroup。",
|
|
6604
|
+
"前缀或缩写匹配多个命令时,需要输入更长的形式。",
|
|
6605
|
+
"**[参数]** 可选 **<参数>** 必填",
|
|
6606
|
+
];
|
|
6607
|
+
try {
|
|
6608
|
+
await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard(
|
|
6609
|
+
"Agent Bot 使用帮助",
|
|
6610
|
+
introLines,
|
|
6611
|
+
sections,
|
|
6612
|
+
));
|
|
6613
|
+
} catch (cardError) {
|
|
6614
|
+
this.logger.warn(
|
|
6615
|
+
{ error: cardError, contextKey, command: "help", phase: "help_card_send" },
|
|
6616
|
+
"Failed to send the help card; falling back to plain text.",
|
|
6617
|
+
);
|
|
6618
|
+
try {
|
|
6619
|
+
await this.outbound.sendText(contextKey, renderPlainHelp(introLines, sections));
|
|
6620
|
+
} catch (textError) {
|
|
6621
|
+
throw new AggregateError(
|
|
6622
|
+
[cardError, textError],
|
|
6623
|
+
"帮助卡片和纯文本帮助均发送失败。",
|
|
6624
|
+
);
|
|
6625
|
+
}
|
|
6626
|
+
}
|
|
6524
6627
|
}
|
|
6525
6628
|
|
|
6526
6629
|
private async setGroupMute(contextKey: string, enabled: boolean): Promise<void> {
|
|
@@ -6636,10 +6739,342 @@ export class ProxySessionController {
|
|
|
6636
6739
|
return record;
|
|
6637
6740
|
}
|
|
6638
6741
|
|
|
6639
|
-
private async
|
|
6640
|
-
|
|
6641
|
-
|
|
6742
|
+
private async presentThreadWriterConflict(
|
|
6743
|
+
record: SessionRecord,
|
|
6744
|
+
contextKey: string,
|
|
6745
|
+
error: unknown,
|
|
6746
|
+
replacePendingTurn: boolean,
|
|
6747
|
+
): Promise<boolean> {
|
|
6748
|
+
if (contextKey.startsWith("console:")) return false;
|
|
6749
|
+
const threadId = activeWriterThreadId(error);
|
|
6750
|
+
if (!threadId || record.remoteSessionId !== threadId) return false;
|
|
6751
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
6752
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
6753
|
+
let owners: ThreadWriterProcess[] = [];
|
|
6754
|
+
if (lockPath) {
|
|
6755
|
+
try {
|
|
6756
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
6757
|
+
} catch (inspectionError) {
|
|
6758
|
+
this.logger.warn(
|
|
6759
|
+
{ error: inspectionError, threadId, lockPath },
|
|
6760
|
+
"Failed to inspect the process holding a thread writer lock.",
|
|
6761
|
+
);
|
|
6762
|
+
}
|
|
6763
|
+
}
|
|
6764
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
6765
|
+
const applicationIdle = owner && lockPath
|
|
6766
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, owner)
|
|
6767
|
+
: undefined;
|
|
6768
|
+
const card = this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
|
|
6769
|
+
record,
|
|
6770
|
+
contextKey,
|
|
6771
|
+
threadId,
|
|
6772
|
+
owners,
|
|
6773
|
+
"occupied",
|
|
6774
|
+
lockPath
|
|
6775
|
+
? undefined
|
|
6776
|
+
: "无法识别占用进程。",
|
|
6777
|
+
applicationIdle,
|
|
6778
|
+
));
|
|
6779
|
+
if (replacePendingTurn) {
|
|
6780
|
+
const replaced = await this.outbound.failPendingTurn(
|
|
6781
|
+
record.localSessionId,
|
|
6782
|
+
"任务被占用。",
|
|
6783
|
+
card,
|
|
6784
|
+
);
|
|
6785
|
+
if (!replaced) await this.outbound.sendInteractiveCard(contextKey, card);
|
|
6786
|
+
} else {
|
|
6787
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
6788
|
+
}
|
|
6789
|
+
return true;
|
|
6790
|
+
}
|
|
6791
|
+
|
|
6792
|
+
private threadWriterConflictView(
|
|
6793
|
+
record: SessionRecord,
|
|
6794
|
+
contextKey: string,
|
|
6795
|
+
threadId: string,
|
|
6796
|
+
owners: ThreadWriterProcess[],
|
|
6797
|
+
status: ThreadWriterConflictCardView["status"],
|
|
6798
|
+
notice?: string,
|
|
6799
|
+
applicationIdle?: boolean,
|
|
6800
|
+
): ThreadWriterConflictCardView {
|
|
6801
|
+
const owner = owners.length === 1 ? owners[0] : undefined;
|
|
6802
|
+
return {
|
|
6803
|
+
status,
|
|
6804
|
+
contextKey,
|
|
6805
|
+
sessionId: record.localSessionId,
|
|
6806
|
+
threadId,
|
|
6807
|
+
taskTitle: record.title,
|
|
6808
|
+
notice: notice ?? (owners.length > 1
|
|
6809
|
+
? `检测到 ${owners.length} 个相关进程,无法安全地自动关闭。`
|
|
6810
|
+
: undefined),
|
|
6811
|
+
applicationIdle,
|
|
6812
|
+
...(owner ? {
|
|
6813
|
+
owner: {
|
|
6814
|
+
displayName: owner.displayName,
|
|
6815
|
+
writerPid: owner.writerPid,
|
|
6816
|
+
writerProcessName: owner.writerProcessName,
|
|
6817
|
+
writerStartedAt: owner.writerStartedAt,
|
|
6818
|
+
applicationPid: owner.applicationPid,
|
|
6819
|
+
applicationProcessName: owner.applicationProcessName,
|
|
6820
|
+
applicationStartedAt: owner.applicationStartedAt,
|
|
6821
|
+
canClose: owner.canClose,
|
|
6822
|
+
},
|
|
6823
|
+
} : {}),
|
|
6824
|
+
};
|
|
6642
6825
|
}
|
|
6826
|
+
|
|
6827
|
+
private async closeThreadWriter(action: CardAction, contextKey: string): Promise<void> {
|
|
6828
|
+
const messageId = requiredCardMessageId(action.messageId);
|
|
6829
|
+
const sessionId = String(action.value.sessionId ?? "");
|
|
6830
|
+
const threadId = String(action.value.threadId ?? "");
|
|
6831
|
+
const expectedWriterPid = positiveActionPid(action.value.writerPid);
|
|
6832
|
+
const expectedApplicationPid = positiveActionPid(action.value.applicationPid);
|
|
6833
|
+
const expectedWriterStartedAt = String(action.value.writerStartedAt ?? "");
|
|
6834
|
+
const expectedApplicationStartedAt = String(action.value.applicationStartedAt ?? "");
|
|
6835
|
+
const force = action.value.force === true || action.value.force === "true";
|
|
6836
|
+
const record = this.requireSession(contextKey, sessionId);
|
|
6837
|
+
if (!threadId || record.remoteSessionId !== threadId) {
|
|
6838
|
+
throw new Error("任务占用卡片已经失效,请重新发送消息获取最新状态。");
|
|
6839
|
+
}
|
|
6840
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
6841
|
+
const lockPath = runtime.getThreadWriterLockPath?.(threadId);
|
|
6842
|
+
if (!lockPath) throw new Error("当前 Agent 不支持自动关闭占用程序。");
|
|
6843
|
+
|
|
6844
|
+
let owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
6845
|
+
const expectedOwner = owners.length === 1 && threadWriterMatches(
|
|
6846
|
+
owners[0],
|
|
6847
|
+
expectedWriterPid,
|
|
6848
|
+
expectedWriterStartedAt,
|
|
6849
|
+
expectedApplicationPid,
|
|
6850
|
+
expectedApplicationStartedAt,
|
|
6851
|
+
) ? owners[0] : undefined;
|
|
6852
|
+
if (!expectedOwner) {
|
|
6853
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
6854
|
+
const applicationIdle = currentOwner
|
|
6855
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
6856
|
+
: undefined;
|
|
6857
|
+
await this.outbound.updateInteractiveCard(
|
|
6858
|
+
contextKey,
|
|
6859
|
+
messageId,
|
|
6860
|
+
this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
|
|
6861
|
+
record,
|
|
6862
|
+
contextKey,
|
|
6863
|
+
threadId,
|
|
6864
|
+
owners,
|
|
6865
|
+
owners.length === 0 ? "released" : "occupied",
|
|
6866
|
+
owners.length === 0
|
|
6867
|
+
? "请重新发送消息。"
|
|
6868
|
+
: "占用进程已变化,请重试。",
|
|
6869
|
+
applicationIdle,
|
|
6870
|
+
)),
|
|
6871
|
+
);
|
|
6872
|
+
return;
|
|
6873
|
+
}
|
|
6874
|
+
if (!expectedOwner.canClose) throw new Error("无法确认占用者是 Agent 程序,已拒绝关闭。");
|
|
6875
|
+
|
|
6876
|
+
try {
|
|
6877
|
+
await this.threadWriterProcesses.close(expectedOwner, force);
|
|
6878
|
+
} catch (error) {
|
|
6879
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
6880
|
+
if (owners.some((owner) => threadWriterMatches(
|
|
6881
|
+
owner,
|
|
6882
|
+
expectedWriterPid,
|
|
6883
|
+
expectedWriterStartedAt,
|
|
6884
|
+
expectedApplicationPid,
|
|
6885
|
+
expectedApplicationStartedAt,
|
|
6886
|
+
))) throw error;
|
|
6887
|
+
}
|
|
6888
|
+
owners = await this.waitForThreadWriterChange(lockPath, expectedOwner);
|
|
6889
|
+
const sameOwnerRemains = owners.some((owner) => threadWriterMatches(
|
|
6890
|
+
owner,
|
|
6891
|
+
expectedOwner.writerPid,
|
|
6892
|
+
expectedOwner.writerStartedAt ?? "",
|
|
6893
|
+
expectedOwner.applicationPid,
|
|
6894
|
+
expectedOwner.applicationStartedAt ?? "",
|
|
6895
|
+
));
|
|
6896
|
+
if (sameOwnerRemains && force) {
|
|
6897
|
+
throw new Error(`无法关闭 ${expectedOwner.displayName}(PID ${expectedOwner.applicationPid})。`);
|
|
6898
|
+
}
|
|
6899
|
+
const status: ThreadWriterConflictCardView["status"] = owners.length === 0
|
|
6900
|
+
? "released"
|
|
6901
|
+
: sameOwnerRemains ? "force_required" : "occupied";
|
|
6902
|
+
const notice = owners.length === 0
|
|
6903
|
+
? "请重新发送消息。"
|
|
6904
|
+
: sameOwnerRemains
|
|
6905
|
+
? "普通关闭失败,可强制关闭。"
|
|
6906
|
+
: "占用进程已变化,请重试。";
|
|
6907
|
+
const currentOwner = owners.length === 1 ? owners[0] : undefined;
|
|
6908
|
+
const applicationIdle = currentOwner
|
|
6909
|
+
? await this.inspectThreadWriterApplicationIdle(runtime, lockPath, threadId, currentOwner)
|
|
6910
|
+
: undefined;
|
|
6911
|
+
await this.outbound.updateInteractiveCard(
|
|
6912
|
+
contextKey,
|
|
6913
|
+
messageId,
|
|
6914
|
+
this.cardRenderer.renderThreadWriterConflict(this.threadWriterConflictView(
|
|
6915
|
+
record,
|
|
6916
|
+
contextKey,
|
|
6917
|
+
threadId,
|
|
6918
|
+
owners,
|
|
6919
|
+
status,
|
|
6920
|
+
notice,
|
|
6921
|
+
applicationIdle,
|
|
6922
|
+
)),
|
|
6923
|
+
);
|
|
6924
|
+
}
|
|
6925
|
+
|
|
6926
|
+
private async inspectThreadWriterApplicationIdle(
|
|
6927
|
+
runtime: AgentRuntime,
|
|
6928
|
+
lockPath: string,
|
|
6929
|
+
threadId: string,
|
|
6930
|
+
owner: ThreadWriterProcess,
|
|
6931
|
+
): Promise<boolean | undefined> {
|
|
6932
|
+
if (!owner.applicationStartedAt
|
|
6933
|
+
|| !this.threadWriterProcesses.inspectApplicationThreadIds
|
|
6934
|
+
|| !runtime.inspectRemoteSessionActivity) return undefined;
|
|
6935
|
+
try {
|
|
6936
|
+
const ownedThreadIds = await this.threadWriterProcesses.inspectApplicationThreadIds(lockPath, owner);
|
|
6937
|
+
if (!ownedThreadIds.includes(threadId)) return undefined;
|
|
6938
|
+
const activities = await Promise.all(
|
|
6939
|
+
ownedThreadIds.map((ownedThreadId) => runtime.inspectRemoteSessionActivity!(ownedThreadId)),
|
|
6940
|
+
);
|
|
6941
|
+
return activities.every((activity) => !activity.active);
|
|
6942
|
+
} catch (error) {
|
|
6943
|
+
this.logger.warn(
|
|
6944
|
+
{ error, threadId, applicationPid: owner.applicationPid },
|
|
6945
|
+
"Failed to inspect thread-writer application activity.",
|
|
6946
|
+
);
|
|
6947
|
+
return undefined;
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
|
|
6951
|
+
private async waitForThreadWriterChange(
|
|
6952
|
+
lockPath: string,
|
|
6953
|
+
expectedOwner: ThreadWriterProcess,
|
|
6954
|
+
): Promise<ThreadWriterProcess[]> {
|
|
6955
|
+
const deadline = Date.now() + 5_000;
|
|
6956
|
+
let owners: ThreadWriterProcess[] = [];
|
|
6957
|
+
do {
|
|
6958
|
+
owners = await this.threadWriterProcesses.inspect(lockPath);
|
|
6959
|
+
if (!owners.some((owner) => threadWriterMatches(
|
|
6960
|
+
owner,
|
|
6961
|
+
expectedOwner.writerPid,
|
|
6962
|
+
expectedOwner.writerStartedAt ?? "",
|
|
6963
|
+
expectedOwner.applicationPid,
|
|
6964
|
+
expectedOwner.applicationStartedAt ?? "",
|
|
6965
|
+
))) return owners;
|
|
6966
|
+
await delay(250);
|
|
6967
|
+
} while (Date.now() < deadline);
|
|
6968
|
+
return owners;
|
|
6969
|
+
}
|
|
6970
|
+
|
|
6971
|
+
private async sendError(
|
|
6972
|
+
contextKey: string,
|
|
6973
|
+
error: unknown,
|
|
6974
|
+
details: RequestFailureDetails = {},
|
|
6975
|
+
): Promise<void> {
|
|
6976
|
+
if (error instanceof PresentedRequestError) {
|
|
6977
|
+
this.logger.warn(
|
|
6978
|
+
{ error: errorLogValue(error.originalError), contextKey, ...details },
|
|
6979
|
+
"Request failed after a dedicated error card was presented.",
|
|
6980
|
+
);
|
|
6981
|
+
return;
|
|
6982
|
+
}
|
|
6983
|
+
const record = this.currentSession(contextKey);
|
|
6984
|
+
if (record) {
|
|
6985
|
+
try {
|
|
6986
|
+
if (await this.presentThreadWriterConflict(record, contextKey, error, false)) return;
|
|
6987
|
+
} catch (presentationError) {
|
|
6988
|
+
this.logger.warn(
|
|
6989
|
+
{ error: presentationError, originalError: errorLogValue(error), contextKey, ...details },
|
|
6990
|
+
"Failed to present the thread writer conflict card.",
|
|
6991
|
+
);
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
this.logger.warn({ error, contextKey, ...details }, "Request failed.");
|
|
6995
|
+
try {
|
|
6996
|
+
await this.outbound.sendText(contextKey, error instanceof Error ? error.message : String(error));
|
|
6997
|
+
} catch (deliveryError) {
|
|
6998
|
+
this.logger.error(
|
|
6999
|
+
{
|
|
7000
|
+
error: deliveryError,
|
|
7001
|
+
originalError: errorLogValue(error),
|
|
7002
|
+
contextKey,
|
|
7003
|
+
...details,
|
|
7004
|
+
phase: "error_response_send",
|
|
7005
|
+
},
|
|
7006
|
+
"Failed to deliver the request failure to the user.",
|
|
7007
|
+
);
|
|
7008
|
+
}
|
|
7009
|
+
}
|
|
7010
|
+
}
|
|
7011
|
+
|
|
7012
|
+
function activeWriterThreadId(error: unknown): string | undefined {
|
|
7013
|
+
const visited = new Set<unknown>();
|
|
7014
|
+
let current = error;
|
|
7015
|
+
while (current !== undefined && current !== null && !visited.has(current)) {
|
|
7016
|
+
visited.add(current);
|
|
7017
|
+
const message = current instanceof Error
|
|
7018
|
+
? current.message
|
|
7019
|
+
: typeof current === "string" ? current : undefined;
|
|
7020
|
+
const match = message?.match(
|
|
7021
|
+
/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,
|
|
7022
|
+
);
|
|
7023
|
+
if (match?.[1]) return match[1];
|
|
7024
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
7025
|
+
}
|
|
7026
|
+
return undefined;
|
|
7027
|
+
}
|
|
7028
|
+
|
|
7029
|
+
function positiveActionPid(value: unknown): number {
|
|
7030
|
+
const pid = typeof value === "number" ? value : Number(value);
|
|
7031
|
+
if (!Number.isSafeInteger(pid) || pid <= 4) {
|
|
7032
|
+
throw new Error("任务占用卡片中的进程信息无效,请重新发送消息获取最新状态。");
|
|
7033
|
+
}
|
|
7034
|
+
return pid;
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
function threadWriterMatches(
|
|
7038
|
+
owner: ThreadWriterProcess,
|
|
7039
|
+
writerPid: number,
|
|
7040
|
+
writerStartedAt: string,
|
|
7041
|
+
applicationPid: number,
|
|
7042
|
+
applicationStartedAt: string,
|
|
7043
|
+
): boolean {
|
|
7044
|
+
return Boolean(writerStartedAt)
|
|
7045
|
+
&& Boolean(applicationStartedAt)
|
|
7046
|
+
&& owner.writerPid === writerPid
|
|
7047
|
+
&& owner.writerStartedAt === writerStartedAt
|
|
7048
|
+
&& owner.applicationPid === applicationPid
|
|
7049
|
+
&& owner.applicationStartedAt === applicationStartedAt;
|
|
7050
|
+
}
|
|
7051
|
+
|
|
7052
|
+
function delay(milliseconds: number): Promise<void> {
|
|
7053
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
7054
|
+
}
|
|
7055
|
+
|
|
7056
|
+
function renderPlainHelp(introLines: string[], sections: HelpCardSection[]): string {
|
|
7057
|
+
return [
|
|
7058
|
+
"Agent Bot 使用帮助",
|
|
7059
|
+
"",
|
|
7060
|
+
...introLines.map((line) => decodeHelpEntities(line.replace(/^>\s*/u, "").replaceAll("**", ""))),
|
|
7061
|
+
...sections.flatMap((section) => [
|
|
7062
|
+
"",
|
|
7063
|
+
section.title,
|
|
7064
|
+
...section.commands.map((command) => [
|
|
7065
|
+
command.text,
|
|
7066
|
+
command.usage ? decodeHelpEntities(command.usage) : "",
|
|
7067
|
+
`- ${command.description}`,
|
|
7068
|
+
].filter(Boolean).join(" ")),
|
|
7069
|
+
]),
|
|
7070
|
+
].join("\n");
|
|
7071
|
+
}
|
|
7072
|
+
|
|
7073
|
+
function decodeHelpEntities(value: string): string {
|
|
7074
|
+
return value
|
|
7075
|
+
.replaceAll("<", "<")
|
|
7076
|
+
.replaceAll(">", ">")
|
|
7077
|
+
.replaceAll("&", "&");
|
|
6643
7078
|
}
|
|
6644
7079
|
|
|
6645
7080
|
function threadForkAnchorMessageIds(message: IncomingMessage): string[] {
|
package/src/runtime/types.ts
CHANGED
|
@@ -217,6 +217,7 @@ export interface ReasoningEffortOption {
|
|
|
217
217
|
export interface AgentRuntime {
|
|
218
218
|
readonly kind: RuntimeKind;
|
|
219
219
|
getProcessInfo?(): AgentProcessInfo;
|
|
220
|
+
getThreadWriterLockPath?(remoteSessionId: string): string | undefined;
|
|
220
221
|
createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession>;
|
|
221
222
|
resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession>;
|
|
222
223
|
forkSession?(input: ForkRuntimeSessionInput): Promise<RuntimeSession>;
|
|
@@ -140,7 +140,7 @@ export class SafeRestartScheduler {
|
|
|
140
140
|
await this.restart(reason, scheduleId, state);
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
|
-
const quietPeriodMs = this.options.quietPeriodMs ??
|
|
143
|
+
const quietPeriodMs = this.options.quietPeriodMs ?? 5_000;
|
|
144
144
|
const latestInboundAt = state.latestInboundAt ?? "";
|
|
145
145
|
if (this.idleSince === undefined || this.idleInboundAt !== latestInboundAt) {
|
|
146
146
|
this.idleSince = Date.now();
|