@keyou007/agent-bot 0.1.24-alpha.8 → 0.1.25-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 +28 -0
- package/README.md +12 -6
- package/README.zh.md +12 -6
- package/dist/codex/CodexRuntime.js +141 -21
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +1 -1
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/feishu/CardRenderer.js +10 -6
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +10 -3
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +14 -11
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +26 -10
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/visibleTurnActivities.js +13 -0
- package/dist/presentation/visibleTurnActivities.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +169 -92
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/responseErrors.js +60 -0
- package/dist/runtime/responseErrors.js.map +1 -0
- package/docs/technical-reference.md +12 -2
- package/docs/technical-reference.zh.md +12 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +5 -1
- package/src/codex/CodexRuntime.ts +139 -20
- package/src/codex/ProviderSwitch.ts +1 -1
- package/src/feishu/CardRenderer.ts +10 -5
- package/src/feishu/FeishuConnector.ts +10 -3
- package/src/feishu/MergedForwardMessage.ts +14 -11
- package/src/feishu/types.ts +3 -0
- package/src/local-files/TurnPreviewPage.ts +25 -10
- package/src/presentation/visibleTurnActivities.ts +12 -0
- package/src/proxy/ProxySessionController.ts +175 -105
- package/src/runtime/responseErrors.ts +62 -0
- package/src/runtime/types.ts +7 -1
|
@@ -59,6 +59,7 @@ import type { TurnActivity, TurnViewState } from "../presentation/turnViewTypes.
|
|
|
59
59
|
import { createTurnViewState, hydrateTurnViewState } from "../presentation/TurnStateReducer.js";
|
|
60
60
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
61
61
|
import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
|
|
62
|
+
import { displayRuntimeError, isResponseContinuationError } from "../runtime/responseErrors.js";
|
|
62
63
|
import type {
|
|
63
64
|
AgentRuntime,
|
|
64
65
|
ApprovalDecision,
|
|
@@ -76,6 +77,7 @@ import type {
|
|
|
76
77
|
RuntimeExecutionSettings,
|
|
77
78
|
RuntimePrompt,
|
|
78
79
|
RuntimeSession,
|
|
80
|
+
RuntimeSettingsReplacement,
|
|
79
81
|
} from "../runtime/types.js";
|
|
80
82
|
import {
|
|
81
83
|
isActiveShellCommandJob,
|
|
@@ -120,9 +122,10 @@ interface StartTurnOptions {
|
|
|
120
122
|
displayPrompt?: string;
|
|
121
123
|
preserveAttemptOnFailure?: boolean;
|
|
122
124
|
approvedPlanTurnId?: string;
|
|
125
|
+
responseContinuationTurnId?: string;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
|
-
interface
|
|
128
|
+
interface PendingMessageReference {
|
|
126
129
|
contextKey: string;
|
|
127
130
|
userId?: string;
|
|
128
131
|
attachmentMessageId?: string;
|
|
@@ -130,11 +133,9 @@ interface PendingForwardAttachment {
|
|
|
130
133
|
resolveAttachment: (message: IncomingMessage | undefined) => void;
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
interface
|
|
136
|
+
interface MessageReferenceReservation {
|
|
134
137
|
sourceMessageId: string;
|
|
135
|
-
|
|
136
|
-
pending: PendingForwardAttachment;
|
|
137
|
-
registry: Map<string, PendingForwardAttachment>;
|
|
138
|
+
pending: PendingMessageReference;
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
interface PreparedTopicRootPrompt {
|
|
@@ -189,6 +190,7 @@ const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
|
189
190
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
190
191
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
191
192
|
const MESSAGE_CANCELLED_REACTION = "CrossMark";
|
|
193
|
+
const INBOUND_MESSAGE_DELAY_WARNING_MS = 60_000;
|
|
192
194
|
const SESSION_PAGE_SIZE = 10;
|
|
193
195
|
const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
194
196
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
@@ -202,7 +204,7 @@ const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
|
202
204
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
203
205
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
204
206
|
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
205
|
-
const
|
|
207
|
+
const REFERENCE_INSTRUCTION_WINDOW_MS = 1_500;
|
|
206
208
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
207
209
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
208
210
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
@@ -566,6 +568,7 @@ export class ProxySessionController {
|
|
|
566
568
|
private readonly lastSessionListings = new Map<string, string[]>();
|
|
567
569
|
private readonly threadInitializations = new Map<string, Promise<void>>();
|
|
568
570
|
private readonly llmRetryingSessions = new Set<string>();
|
|
571
|
+
private readonly responseContinuations = new Map<string, { turnId: string; abort: AbortController }>();
|
|
569
572
|
private readonly planHandoffs = new Map<string, string>();
|
|
570
573
|
private readonly retriedFailureTurnIds = new Set<string>();
|
|
571
574
|
private readonly queuedPromptRetryTimers = new Map<string, NodeJS.Timeout>();
|
|
@@ -573,8 +576,7 @@ export class ProxySessionController {
|
|
|
573
576
|
private readonly recoveryRetryStartedAt = new Map<string, number>();
|
|
574
577
|
private readonly recoveredLiveAttempts = new Set<string>();
|
|
575
578
|
private recoveryClosed = false;
|
|
576
|
-
private readonly
|
|
577
|
-
private readonly pendingResourceForwards = new Map<string, PendingForwardAttachment>();
|
|
579
|
+
private readonly pendingMessageReferences = new Map<string, PendingMessageReference>();
|
|
578
580
|
private readonly relatedReactionMessageIds = new Map<string, string[]>();
|
|
579
581
|
private readonly shellCommandJobMonitors = new Map<string, ShellCommandJobMonitor>();
|
|
580
582
|
private readonly appServerReleaseSchedules = new Map<string, AppServerReleaseSchedule>();
|
|
@@ -597,6 +599,7 @@ export class ProxySessionController {
|
|
|
597
599
|
path.join(path.dirname(config.storage.sqlitePath), "command-jobs"),
|
|
598
600
|
),
|
|
599
601
|
private readonly threadWriterProcesses: ThreadWriterProcessController = new SystemThreadWriterProcessController(),
|
|
602
|
+
private readonly referenceInstructionWindowMs = REFERENCE_INSTRUCTION_WINDOW_MS,
|
|
600
603
|
) {
|
|
601
604
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
602
605
|
this.unsubscribe.push(
|
|
@@ -643,14 +646,29 @@ export class ProxySessionController {
|
|
|
643
646
|
// For accepted messages, claim durable deduplication before acknowledgement so event
|
|
644
647
|
// retries cannot add duplicate reactions.
|
|
645
648
|
if (!this.store.claimInboundEvent(message.messageId, "message")) return;
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
649
|
+
const acceptedAt = Date.now();
|
|
650
|
+
const receivedAt = message.receivedAt ?? acceptedAt;
|
|
651
|
+
const deliveryDelayMs = message.createdAt === undefined
|
|
652
|
+
? 0 : Math.max(0, receivedAt - message.createdAt);
|
|
653
|
+
const dispatchDelayMs = Math.max(0, acceptedAt - receivedAt);
|
|
654
|
+
if (deliveryDelayMs + dispatchDelayMs >= INBOUND_MESSAGE_DELAY_WARNING_MS) {
|
|
655
|
+
this.logger.warn(
|
|
656
|
+
{
|
|
657
|
+
messageId: message.messageId,
|
|
658
|
+
eventId: message.eventId,
|
|
659
|
+
contextKey: message.contextKey,
|
|
660
|
+
messageCreatedAt: message.createdAt,
|
|
661
|
+
receivedAt,
|
|
662
|
+
acceptedAt,
|
|
663
|
+
deliveryDelayMs,
|
|
664
|
+
dispatchDelayMs,
|
|
665
|
+
},
|
|
666
|
+
"Accepted a delayed Feishu message.",
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
const attachmentReservation = this.reserveReferenceInstruction(message);
|
|
670
|
+
const pendingReference = !attachmentReservation && isReferenceSource(message)
|
|
671
|
+
? this.registerPendingReference(message)
|
|
654
672
|
: undefined;
|
|
655
673
|
try {
|
|
656
674
|
const reactionId = await this.outbound.addReaction(
|
|
@@ -668,8 +686,9 @@ export class ProxySessionController {
|
|
|
668
686
|
);
|
|
669
687
|
}
|
|
670
688
|
await this.waitForCurrentSessionReset(message.contextKey);
|
|
671
|
-
if (
|
|
672
|
-
|
|
689
|
+
if (this.recoveryClosed) return;
|
|
690
|
+
if (attachmentReservation && this.completeReferenceInstruction(message, attachmentReservation)) {
|
|
691
|
+
this.store.audit(message.contextKey, "referenced_message_coalesced", {
|
|
673
692
|
messageId: message.messageId,
|
|
674
693
|
parentMessageId: message.parentMessageId,
|
|
675
694
|
});
|
|
@@ -690,24 +709,24 @@ export class ProxySessionController {
|
|
|
690
709
|
this.store.recordChatContext(chatContextKey, message.chatType);
|
|
691
710
|
this.store.markChatActive(chatContextKey);
|
|
692
711
|
}
|
|
693
|
-
if (
|
|
694
|
-
const
|
|
695
|
-
const attachment = await this.
|
|
712
|
+
if (pendingReference && !message.mergedForwardMessageId) {
|
|
713
|
+
const sourceMessage = message;
|
|
714
|
+
const attachment = await this.waitForReferenceInstruction(
|
|
696
715
|
message.messageId,
|
|
697
|
-
|
|
698
|
-
this.pendingResourceForwards,
|
|
716
|
+
pendingReference,
|
|
699
717
|
);
|
|
718
|
+
if (this.recoveryClosed) return;
|
|
700
719
|
if (attachment) {
|
|
701
720
|
coalescedForwardAttachment = true;
|
|
702
|
-
const sourceText =
|
|
703
|
-
const instruction = normalizeFeishuPostText(attachment.text)
|
|
704
|
-
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(
|
|
705
|
-
forwardedImages = (
|
|
706
|
-
messageId:
|
|
721
|
+
const sourceText = sourceMessage.text.trim();
|
|
722
|
+
const instruction = normalizeFeishuPostText(attachment.displayText ?? attachment.text)
|
|
723
|
+
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(sourceMessage));
|
|
724
|
+
forwardedImages = (sourceMessage.images ?? []).map((image) => ({
|
|
725
|
+
messageId: sourceMessage.messageId,
|
|
707
726
|
imageKey: image.imageKey,
|
|
708
727
|
}));
|
|
709
|
-
forwardedFiles = (
|
|
710
|
-
messageId:
|
|
728
|
+
forwardedFiles = (sourceMessage.files ?? []).map((file) => ({
|
|
729
|
+
messageId: sourceMessage.messageId,
|
|
711
730
|
fileKey: file.fileKey,
|
|
712
731
|
fileName: file.fileName,
|
|
713
732
|
}));
|
|
@@ -716,7 +735,7 @@ export class ProxySessionController {
|
|
|
716
735
|
text: sourceText ? referencedMessagePrompt(instruction, sourceText) : instruction,
|
|
717
736
|
displayText: instruction,
|
|
718
737
|
};
|
|
719
|
-
this.relatedReactionMessageIds.set(message.messageId, [
|
|
738
|
+
this.relatedReactionMessageIds.set(message.messageId, [sourceMessage.messageId]);
|
|
720
739
|
}
|
|
721
740
|
}
|
|
722
741
|
if (message.mergedForwardMessageId) {
|
|
@@ -728,16 +747,15 @@ export class ProxySessionController {
|
|
|
728
747
|
(content) => ({ content } as const),
|
|
729
748
|
(error: unknown) => ({ error } as const),
|
|
730
749
|
);
|
|
731
|
-
const attachment =
|
|
732
|
-
? await this.
|
|
750
|
+
const attachment = pendingReference
|
|
751
|
+
? await this.waitForReferenceInstruction(
|
|
733
752
|
message.messageId,
|
|
734
|
-
|
|
735
|
-
this.pendingMergedForwards,
|
|
753
|
+
pendingReference,
|
|
736
754
|
)
|
|
737
755
|
: undefined;
|
|
738
756
|
if (attachment) {
|
|
739
757
|
coalescedForwardAttachment = true;
|
|
740
|
-
const instruction = normalizeFeishuPostText(attachment.text)
|
|
758
|
+
const instruction = normalizeFeishuPostText(attachment.displayText ?? attachment.text)
|
|
741
759
|
|| ((attachment.images?.length ?? 0) > 0 ? "请查看附带的内容。" : "请结合参考聊天记录处理。");
|
|
742
760
|
message = {
|
|
743
761
|
...attachment,
|
|
@@ -747,6 +765,7 @@ export class ProxySessionController {
|
|
|
747
765
|
this.relatedReactionMessageIds.set(message.messageId, [mergedForwardMessage.messageId]);
|
|
748
766
|
}
|
|
749
767
|
const result = await mergedForwardResult;
|
|
768
|
+
if (this.recoveryClosed) return;
|
|
750
769
|
if ("error" in result) {
|
|
751
770
|
const replyTarget = message.replyInThread
|
|
752
771
|
? { messageId: message.messageId, replyInThread: true as const }
|
|
@@ -823,6 +842,9 @@ export class ProxySessionController {
|
|
|
823
842
|
const fileCount = fileReferences.length;
|
|
824
843
|
this.store.audit(message.contextKey, "incoming_message", {
|
|
825
844
|
messageId: message.messageId,
|
|
845
|
+
eventId: message.eventId,
|
|
846
|
+
messageCreatedAt: message.createdAt,
|
|
847
|
+
receivedAt: message.receivedAt,
|
|
826
848
|
text: message.text,
|
|
827
849
|
...(imageCount > 0 ? { imageCount } : {}),
|
|
828
850
|
...(fileCount > 0 ? { fileCount } : {}),
|
|
@@ -1400,10 +1422,8 @@ export class ProxySessionController {
|
|
|
1400
1422
|
this.remoteTurnHistoryRefreshes.clear();
|
|
1401
1423
|
this.forkHistoryCursors.clear();
|
|
1402
1424
|
this.turnHistoryHydrations.clear();
|
|
1403
|
-
for (const pending of this.
|
|
1404
|
-
this.
|
|
1405
|
-
for (const pending of this.pendingResourceForwards.values()) pending.resolveAttachment(undefined);
|
|
1406
|
-
this.pendingResourceForwards.clear();
|
|
1425
|
+
for (const pending of this.pendingMessageReferences.values()) pending.resolveAttachment(undefined);
|
|
1426
|
+
this.pendingMessageReferences.clear();
|
|
1407
1427
|
this.relatedReactionMessageIds.clear();
|
|
1408
1428
|
for (const timer of this.queuedPromptRetryTimers.values()) clearTimeout(timer);
|
|
1409
1429
|
this.queuedPromptRetryTimers.clear();
|
|
@@ -3976,6 +3996,16 @@ export class ProxySessionController {
|
|
|
3976
3996
|
localImagePaths?: string[], options: StartTurnOptions = {},
|
|
3977
3997
|
): Promise<string> {
|
|
3978
3998
|
return this.settingsOperations.run(loaded.record.localSessionId, async () => {
|
|
3999
|
+
if (options.responseContinuationTurnId) {
|
|
4000
|
+
const current = this.store.getSession(loaded.record.localSessionId);
|
|
4001
|
+
if (!current || current.status === "closed" || current.lastTurnId !== options.responseContinuationTurnId
|
|
4002
|
+
|| this.sessionResets.has(current.localSessionId)
|
|
4003
|
+
|| this.responseContinuations.get(current.localSessionId)?.turnId !== options.responseContinuationTurnId
|
|
4004
|
+
|| this.store.getPendingProviderSettings(current.localSessionId)) {
|
|
4005
|
+
throw new Error("任务或运行设置已变化,未自动恢复模型续接。请发送消息继续。");
|
|
4006
|
+
}
|
|
4007
|
+
return this.startTurnNow(loaded, text, replyTarget, localImagePaths, options);
|
|
4008
|
+
}
|
|
3979
4009
|
try {
|
|
3980
4010
|
await this.applyPendingProviderSettings(loaded);
|
|
3981
4011
|
} catch (error) {
|
|
@@ -4050,17 +4080,27 @@ export class ProxySessionController {
|
|
|
4050
4080
|
|| this.sessionResets.has(loaded.record.localSessionId))) {
|
|
4051
4081
|
throw new Error("方案执行已取消。");
|
|
4052
4082
|
}
|
|
4053
|
-
|
|
4054
|
-
loaded.record.localSessionId
|
|
4055
|
-
options.
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4083
|
+
if (options.responseContinuationTurnId) {
|
|
4084
|
+
const recovery = this.responseContinuations.get(loaded.record.localSessionId);
|
|
4085
|
+
if (recovery?.turnId !== options.responseContinuationTurnId
|
|
4086
|
+
|| this.sessionResets.has(loaded.record.localSessionId) || !loaded.runtime.recoverResponseContinuation) {
|
|
4087
|
+
throw new Error("模型续接恢复已取消。");
|
|
4088
|
+
}
|
|
4089
|
+
turnId = await loaded.runtime.recoverResponseContinuation(loaded.record.localSessionId, options.responseContinuationTurnId, recovery.abort.signal);
|
|
4090
|
+
} else {
|
|
4091
|
+
turnId = await loaded.runtime.startTurn(
|
|
4092
|
+
loaded.record.localSessionId,
|
|
4093
|
+
options.approvedPlanTurnId
|
|
4094
|
+
? { text: runtimeText, collaborationMode: "default" }
|
|
4095
|
+
: runtimePrompt(runtimeText, localImagePaths),
|
|
4096
|
+
);
|
|
4097
|
+
}
|
|
4059
4098
|
break;
|
|
4060
4099
|
} catch (error) {
|
|
4061
4100
|
const message = error instanceof Error ? error.message : String(error);
|
|
4062
4101
|
const attempt = this.store.getTurnAttempt(attemptId);
|
|
4063
4102
|
const retryAttempt = attempt
|
|
4103
|
+
&& !options.responseContinuationTurnId
|
|
4064
4104
|
&& isRetryableLlmTurnFailure(message)
|
|
4065
4105
|
&& attempt.retryCount < MAX_LLM_TURN_RETRIES
|
|
4066
4106
|
? this.store.prepareUnstartedTurnAttemptRetry(attemptId)
|
|
@@ -4794,7 +4834,23 @@ export class ProxySessionController {
|
|
|
4794
4834
|
: undefined;
|
|
4795
4835
|
if (event.type === "turn_failed") {
|
|
4796
4836
|
if (this.retriedFailureTurnIds.has(event.turnId)) return;
|
|
4797
|
-
|
|
4837
|
+
const source = this.store.getSession(event.sessionId);
|
|
4838
|
+
const responseContinuation = failedAttempt?.retryCount === 0 && failedAttempt.status !== "cancelling"
|
|
4839
|
+
&& source && source.status !== "closed" && source.lastTurnId === event.turnId
|
|
4840
|
+
&& !this.sessionResets.has(event.sessionId) && !this.store.getPendingProviderSettings(event.sessionId)
|
|
4841
|
+
&& Boolean(this.runtimes.forAgent(source.agentName).recoverResponseContinuation)
|
|
4842
|
+
&& isResponseContinuationError(event.message);
|
|
4843
|
+
if (failedAttempt && responseContinuation) {
|
|
4844
|
+
const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
|
|
4845
|
+
if (retryAttempt) {
|
|
4846
|
+
this.retriedFailureTurnIds.add(event.turnId);
|
|
4847
|
+
this.responseContinuations.set(event.sessionId, { turnId: event.turnId, abort: new AbortController() });
|
|
4848
|
+
await this.retryFailedLlmTurn(event, retryAttempt, true);
|
|
4849
|
+
return;
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4852
|
+
if (failedAttempt && !this.responseContinuations.has(event.sessionId)
|
|
4853
|
+
&& isRetryableLlmTurnFailure(event.message) && failedAttempt.retryCount < MAX_LLM_TURN_RETRIES) {
|
|
4798
4854
|
const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
|
|
4799
4855
|
if (retryAttempt) {
|
|
4800
4856
|
this.retriedFailureTurnIds.add(event.turnId);
|
|
@@ -4957,6 +5013,7 @@ export class ProxySessionController {
|
|
|
4957
5013
|
private async retryFailedLlmTurn(
|
|
4958
5014
|
event: Extract<RuntimeEvent, { type: "turn_failed" }>,
|
|
4959
5015
|
attempt: TurnAttemptRecord,
|
|
5016
|
+
responseContinuation = false,
|
|
4960
5017
|
): Promise<void> {
|
|
4961
5018
|
const retryNumber = attempt.retryCount;
|
|
4962
5019
|
const session = this.store.getSession(event.sessionId);
|
|
@@ -4974,14 +5031,17 @@ export class ProxySessionController {
|
|
|
4974
5031
|
attemptId: attempt.attemptId,
|
|
4975
5032
|
failedTurnId: event.turnId,
|
|
4976
5033
|
retryNumber,
|
|
4977
|
-
maxRetries: MAX_LLM_TURN_RETRIES,
|
|
5034
|
+
maxRetries: responseContinuation ? 1 : MAX_LLM_TURN_RETRIES,
|
|
5035
|
+
responseContinuation,
|
|
4978
5036
|
error: event.message,
|
|
4979
5037
|
});
|
|
4980
5038
|
|
|
4981
5039
|
try {
|
|
4982
5040
|
await this.outbound.onEvent({
|
|
4983
5041
|
...event,
|
|
4984
|
-
message:
|
|
5042
|
+
message: responseContinuation
|
|
5043
|
+
? `${event.message}\n\n模型续接状态失效,正在恢复连接并继续(1/1)。`
|
|
5044
|
+
: `${event.message}\n\n检测到临时模型服务错误,正在自动重试(${retryNumber}/${MAX_LLM_TURN_RETRIES})。`,
|
|
4985
5045
|
});
|
|
4986
5046
|
} catch (error) {
|
|
4987
5047
|
this.logger.warn(
|
|
@@ -4998,21 +5058,26 @@ export class ProxySessionController {
|
|
|
4998
5058
|
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
4999
5059
|
const retryTurnId = await this.startTurn(
|
|
5000
5060
|
loaded,
|
|
5001
|
-
llmRetryRuntimePrompt(attempt.promptText, retryNumber),
|
|
5061
|
+
responseContinuation ? "" : llmRetryRuntimePrompt(attempt.promptText, retryNumber),
|
|
5002
5062
|
replyTarget,
|
|
5003
|
-
attempt.localImagePaths,
|
|
5063
|
+
responseContinuation ? undefined : attempt.localImagePaths,
|
|
5004
5064
|
{
|
|
5005
5065
|
attemptId: attempt.attemptId,
|
|
5006
5066
|
messageId: attempt.messageId,
|
|
5007
|
-
displayPrompt: llmRetryCardPrompt(attempt.promptText, retryNumber),
|
|
5067
|
+
displayPrompt: responseContinuation ? `恢复模型续接:${attempt.promptText}` : llmRetryCardPrompt(attempt.promptText, retryNumber),
|
|
5008
5068
|
preserveAttemptOnFailure: true,
|
|
5069
|
+
...(responseContinuation ? { responseContinuationTurnId: event.turnId } : {}),
|
|
5009
5070
|
},
|
|
5010
5071
|
);
|
|
5011
5072
|
this.store.rebindPendingTurnMessages(session.localSessionId, event.turnId, retryTurnId);
|
|
5012
5073
|
} catch (error) {
|
|
5013
|
-
this.
|
|
5014
|
-
this.store.
|
|
5015
|
-
|
|
5074
|
+
const aborted = responseContinuation && !this.responseContinuations.has(session.localSessionId);
|
|
5075
|
+
this.store.updateTurnAttempt(attempt.attemptId, { status: aborted ? "cancelled" : "failed" });
|
|
5076
|
+
const latest = this.store.getSession(session.localSessionId);
|
|
5077
|
+
if (!responseContinuation || (latest?.lastTurnId === event.turnId && latest.status !== "closed")) {
|
|
5078
|
+
this.store.updateSession(session.localSessionId, { status: aborted ? "ready" : "failed" });
|
|
5079
|
+
}
|
|
5080
|
+
await this.finalizeTurnMessageReactions(event.turnId, aborted ? "cancelled" : "failed").catch((reactionError: unknown) => {
|
|
5016
5081
|
this.logger.warn(
|
|
5017
5082
|
{ error: reactionError, sessionId: session.localSessionId, turnId: event.turnId },
|
|
5018
5083
|
"Failed to finalize reactions after an LLM retry could not start.",
|
|
@@ -5022,9 +5087,16 @@ export class ProxySessionController {
|
|
|
5022
5087
|
{ error, sessionId: session.localSessionId, retryNumber },
|
|
5023
5088
|
"Failed to start an automatic LLM turn retry.",
|
|
5024
5089
|
);
|
|
5090
|
+
if (responseContinuation && !aborted) {
|
|
5091
|
+
await this.outbound.sendText(attempt.contextKey,
|
|
5092
|
+
`模型续接恢复未能启动:${displayRuntimeError(runtimeErrorMessage(error))}。请发送消息继续。`);
|
|
5093
|
+
}
|
|
5025
5094
|
await this.scheduleNextQueuedPrompt(session.localSessionId);
|
|
5026
5095
|
} finally {
|
|
5027
5096
|
this.llmRetryingSessions.delete(session.localSessionId);
|
|
5097
|
+
if (this.responseContinuations.get(session.localSessionId)?.turnId === event.turnId) {
|
|
5098
|
+
this.responseContinuations.delete(session.localSessionId);
|
|
5099
|
+
}
|
|
5028
5100
|
}
|
|
5029
5101
|
}
|
|
5030
5102
|
|
|
@@ -5146,10 +5218,7 @@ export class ProxySessionController {
|
|
|
5146
5218
|
}
|
|
5147
5219
|
}
|
|
5148
5220
|
|
|
5149
|
-
private
|
|
5150
|
-
message: IncomingMessage,
|
|
5151
|
-
registry: Map<string, PendingForwardAttachment>,
|
|
5152
|
-
): PendingForwardAttachment {
|
|
5221
|
+
private registerPendingReference(message: IncomingMessage): PendingMessageReference {
|
|
5153
5222
|
let resolveAttachment!: (message: IncomingMessage | undefined) => void;
|
|
5154
5223
|
const attachmentPromise = new Promise<IncomingMessage | undefined>((resolve) => {
|
|
5155
5224
|
resolveAttachment = resolve;
|
|
@@ -5160,61 +5229,51 @@ export class ProxySessionController {
|
|
|
5160
5229
|
attachmentPromise,
|
|
5161
5230
|
resolveAttachment,
|
|
5162
5231
|
};
|
|
5163
|
-
|
|
5232
|
+
this.pendingMessageReferences.set(message.messageId, pending);
|
|
5164
5233
|
return pending;
|
|
5165
5234
|
}
|
|
5166
5235
|
|
|
5167
|
-
private
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
if (!pending || pending.contextKey !== message.contextKey
|
|
5176
|
-
|| pending.userId !== message.userId || pending.attachmentMessageId) continue;
|
|
5177
|
-
pending.attachmentMessageId = message.messageId;
|
|
5178
|
-
return {
|
|
5179
|
-
sourceMessageId: message.parentMessageId,
|
|
5180
|
-
sourceKind: candidate.sourceKind,
|
|
5181
|
-
pending,
|
|
5182
|
-
registry: candidate.registry,
|
|
5183
|
-
};
|
|
5184
|
-
}
|
|
5185
|
-
return undefined;
|
|
5236
|
+
private reserveReferenceInstruction(message: IncomingMessage): MessageReferenceReservation | undefined {
|
|
5237
|
+
const parentMessageId = quotedMessageId(message);
|
|
5238
|
+
if (!parentMessageId || isCommandMessage(message)) return undefined;
|
|
5239
|
+
const pending = this.pendingMessageReferences.get(parentMessageId);
|
|
5240
|
+
if (!pending || pending.contextKey !== message.contextKey
|
|
5241
|
+
|| pending.userId !== message.userId || pending.attachmentMessageId) return undefined;
|
|
5242
|
+
pending.attachmentMessageId = message.messageId;
|
|
5243
|
+
return { sourceMessageId: parentMessageId, pending };
|
|
5186
5244
|
}
|
|
5187
5245
|
|
|
5188
|
-
private
|
|
5246
|
+
private completeReferenceInstruction(
|
|
5189
5247
|
message: IncomingMessage,
|
|
5190
|
-
reservation:
|
|
5248
|
+
reservation: MessageReferenceReservation,
|
|
5191
5249
|
): boolean {
|
|
5192
|
-
|
|
5193
|
-
if (reservation.sourceMessageId !==
|
|
5194
|
-
if (
|
|
5250
|
+
const parentMessageId = quotedMessageId(message);
|
|
5251
|
+
if (!parentMessageId || reservation.sourceMessageId !== parentMessageId) return false;
|
|
5252
|
+
if (this.pendingMessageReferences.get(parentMessageId) !== reservation.pending) return false;
|
|
5195
5253
|
if (reservation.pending.attachmentMessageId !== message.messageId) return false;
|
|
5196
5254
|
reservation.pending.resolveAttachment(message);
|
|
5197
5255
|
return true;
|
|
5198
5256
|
}
|
|
5199
5257
|
|
|
5200
|
-
private async
|
|
5258
|
+
private async waitForReferenceInstruction(
|
|
5201
5259
|
messageId: string,
|
|
5202
|
-
pending:
|
|
5203
|
-
registry: Map<string, PendingForwardAttachment>,
|
|
5260
|
+
pending: PendingMessageReference,
|
|
5204
5261
|
): Promise<IncomingMessage | undefined> {
|
|
5205
|
-
const
|
|
5262
|
+
const expire = (): void => {
|
|
5206
5263
|
// Arrival reserves the pair before acknowledgement. A slow Reaction must not split it.
|
|
5207
5264
|
if (pending.attachmentMessageId) return;
|
|
5208
|
-
if (
|
|
5265
|
+
if (this.pendingMessageReferences.get(messageId) === pending) this.pendingMessageReferences.delete(messageId);
|
|
5209
5266
|
pending.resolveAttachment(undefined);
|
|
5210
|
-
}
|
|
5211
|
-
timer.
|
|
5267
|
+
};
|
|
5268
|
+
const timer = this.referenceInstructionWindowMs > 0 ? setTimeout(expire, this.referenceInstructionWindowMs) : undefined;
|
|
5269
|
+
if (timer) timer.unref?.();
|
|
5270
|
+
else expire();
|
|
5212
5271
|
try {
|
|
5213
5272
|
return await pending.attachmentPromise;
|
|
5214
5273
|
} finally {
|
|
5215
|
-
clearTimeout(timer);
|
|
5216
|
-
if (
|
|
5217
|
-
|
|
5274
|
+
if (timer) clearTimeout(timer);
|
|
5275
|
+
if (this.pendingMessageReferences.get(messageId) === pending) {
|
|
5276
|
+
this.pendingMessageReferences.delete(messageId);
|
|
5218
5277
|
}
|
|
5219
5278
|
}
|
|
5220
5279
|
}
|
|
@@ -5291,6 +5350,8 @@ export class ProxySessionController {
|
|
|
5291
5350
|
}
|
|
5292
5351
|
|
|
5293
5352
|
private async cancelSession(record: SessionRecord): Promise<void> {
|
|
5353
|
+
this.responseContinuations.get(record.localSessionId)?.abort.abort();
|
|
5354
|
+
this.responseContinuations.delete(record.localSessionId);
|
|
5294
5355
|
this.planHandoffs.delete(record.localSessionId);
|
|
5295
5356
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
5296
5357
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
@@ -5370,6 +5431,8 @@ export class ProxySessionController {
|
|
|
5370
5431
|
}
|
|
5371
5432
|
|
|
5372
5433
|
private async runSessionReset(sessionId: string, operation: () => Promise<void>): Promise<void> {
|
|
5434
|
+
this.responseContinuations.get(sessionId)?.abort.abort();
|
|
5435
|
+
this.responseContinuations.delete(sessionId);
|
|
5373
5436
|
this.planHandoffs.delete(sessionId);
|
|
5374
5437
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
5375
5438
|
const reset = previous.catch(() => undefined).then(operation);
|
|
@@ -5844,9 +5907,16 @@ export class ProxySessionController {
|
|
|
5844
5907
|
if (!loaded.runtime.setExecutionSettings) throw new Error("当前运行时不支持 Provider 设置。");
|
|
5845
5908
|
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
5846
5909
|
const remoteSessionId = loaded.session.remoteSessionId;
|
|
5847
|
-
const persist = (candidate: RuntimeSession): Promise<void> => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
5910
|
+
const persist = (candidate: RuntimeSession, replacement?: RuntimeSettingsReplacement): Promise<void> => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
5848
5911
|
if (candidate.remoteSessionId !== remoteSessionId) {
|
|
5849
|
-
|
|
5912
|
+
const saved = this.store.getSession(loaded.record.localSessionId);
|
|
5913
|
+
if (replacement?.emptyRemoteSessionId !== remoteSessionId || !saved || saved.remoteSessionId !== remoteSessionId
|
|
5914
|
+
|| saved.status === "closed" || saved.status === "running" || saved.lastTurnId || loaded.session.activeTurnId || candidate.activeTurnId
|
|
5915
|
+
|| candidate.localSessionId !== saved.localSessionId || !candidate.remoteSessionId
|
|
5916
|
+
|| candidate.agentName !== saved.agentName || candidate.cwd !== saved.cwd || candidate.runtimeKind !== loaded.session.runtimeKind
|
|
5917
|
+
|| this.store.hasTurnAttemptsForSession(saved.localSessionId) || this.store.getForkHistorySource(saved.localSessionId)) {
|
|
5918
|
+
throw new Error("设置切换返回了不同的任务 ID,未保存设置;仅允许已验证的空会话更换底层任务。");
|
|
5919
|
+
}
|
|
5850
5920
|
}
|
|
5851
5921
|
assertProviderSettingsApplied(candidate, settings);
|
|
5852
5922
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
@@ -5854,6 +5924,7 @@ export class ProxySessionController {
|
|
|
5854
5924
|
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
5855
5925
|
try {
|
|
5856
5926
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
5927
|
+
remoteSessionId: candidate.remoteSessionId,
|
|
5857
5928
|
modelProvider: candidate.modelProvider,
|
|
5858
5929
|
model: candidate.model,
|
|
5859
5930
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -5866,6 +5937,7 @@ export class ProxySessionController {
|
|
|
5866
5937
|
}
|
|
5867
5938
|
});
|
|
5868
5939
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
5940
|
+
loaded.record = this.store.getSession(loaded.record.localSessionId) ?? loaded.record;
|
|
5869
5941
|
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
5870
5942
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
5871
5943
|
return session;
|
|
@@ -7107,6 +7179,7 @@ export class ProxySessionController {
|
|
|
7107
7179
|
}
|
|
7108
7180
|
|
|
7109
7181
|
private async createDirectoryFromCard(contextKey: string, action: CardAction): Promise<void> {
|
|
7182
|
+
const updateMessageId = requiredCardMessageId(action.messageId);
|
|
7110
7183
|
const directory = directoryActionPath(action.value.directory);
|
|
7111
7184
|
await this.assertBrowsableDirectory(directory);
|
|
7112
7185
|
const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
|
|
@@ -7120,9 +7193,9 @@ export class ProxySessionController {
|
|
|
7120
7193
|
if (code === "EEXIST") throw new Error(`目录已存在:${newDirectory}`);
|
|
7121
7194
|
throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
|
|
7122
7195
|
}
|
|
7123
|
-
await this.openDirectoryBrowser(contextKey,
|
|
7124
|
-
updateMessageId
|
|
7125
|
-
page:
|
|
7196
|
+
await this.openDirectoryBrowser(contextKey, newDirectory, {
|
|
7197
|
+
updateMessageId,
|
|
7198
|
+
page: 0,
|
|
7126
7199
|
});
|
|
7127
7200
|
await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
|
|
7128
7201
|
}
|
|
@@ -9758,13 +9831,10 @@ function isCommandMessage(message: IncomingMessage): boolean {
|
|
|
9758
9831
|
return text.startsWith("/") || text.startsWith("!");
|
|
9759
9832
|
}
|
|
9760
9833
|
|
|
9761
|
-
function
|
|
9762
|
-
return !message
|
|
9763
|
-
&& !message.parentMessageId
|
|
9834
|
+
function isReferenceSource(message: IncomingMessage): boolean {
|
|
9835
|
+
return !quotedMessageId(message)
|
|
9764
9836
|
&& !message.contextKey.startsWith("console:")
|
|
9765
|
-
&& !isCommandMessage(message)
|
|
9766
|
-
&& (message.messageType === "post"
|
|
9767
|
-
|| (message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
|
|
9837
|
+
&& !isCommandMessage(message);
|
|
9768
9838
|
}
|
|
9769
9839
|
|
|
9770
9840
|
function deduplicateImageReferences(
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const RUNTIME_WARNING_PREFIXES = ["运行请求失败,等待 Agent 返回最终状态:", "运行请求出错,Agent 正在重试:"];
|
|
4
|
+
|
|
5
|
+
const responseErrorSchema = z.object({
|
|
6
|
+
type: z.literal("error"),
|
|
7
|
+
status: z.number().optional(),
|
|
8
|
+
error: z.object({
|
|
9
|
+
message: z.string().min(1),
|
|
10
|
+
code: z.string().nullish(),
|
|
11
|
+
param: z.string().nullish(),
|
|
12
|
+
type: z.string().nullish(),
|
|
13
|
+
}).passthrough(),
|
|
14
|
+
}).passthrough();
|
|
15
|
+
|
|
16
|
+
function parseResponseError(message: string): { value: z.infer<typeof responseErrorSchema>; notice: string } | undefined {
|
|
17
|
+
// Agent Bot may append a recovery notice after the upstream JSON error.
|
|
18
|
+
const boundary = message.indexOf("\n\n");
|
|
19
|
+
for (const end of [message.length, ...(boundary >= 0 ? [boundary] : [])]) {
|
|
20
|
+
try {
|
|
21
|
+
const parsed = responseErrorSchema.safeParse(JSON.parse(message.slice(0, end)));
|
|
22
|
+
if (parsed.success) return { value: parsed.data, notice: message.slice(end) };
|
|
23
|
+
} catch {
|
|
24
|
+
// Non-JSON runtime errors remain unchanged.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isResponseContinuationError(message: string): boolean {
|
|
31
|
+
const parsed = parseResponseError(message);
|
|
32
|
+
return parsed?.value.error.code === "unsupported_persisted_item_context"
|
|
33
|
+
&& parsed.value.error.param === "previous_response_id"
|
|
34
|
+
&& parsed.value.error.type === "invalid_request_error"
|
|
35
|
+
&& parsed.value.status === 400
|
|
36
|
+
&& !parsed.notice;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function displayRuntimeError(message: string): string {
|
|
40
|
+
const prefix = RUNTIME_WARNING_PREFIXES.find((value) => message.startsWith(value));
|
|
41
|
+
const parsed = parseResponseError(prefix ? message.slice(prefix.length) : message);
|
|
42
|
+
if (!parsed) return message;
|
|
43
|
+
const { error, status } = parsed.value;
|
|
44
|
+
const details = [status === undefined ? undefined : `HTTP ${status}`, error.code, error.param];
|
|
45
|
+
return (prefix ?? "") + `${error.message}\n${details.filter(Boolean).join(" · ")}${parsed.notice}`.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function sameRuntimeError(warning: string, terminal: string): boolean {
|
|
49
|
+
if (warning.trim() === terminal.trim()) return true;
|
|
50
|
+
const parsed = parseResponseError(terminal);
|
|
51
|
+
if (parsed) {
|
|
52
|
+
const other = parseResponseError(warning);
|
|
53
|
+
return Boolean(other && !other.notice && (!parsed.notice || isRecoveryNotice(parsed.notice))
|
|
54
|
+
&& JSON.stringify(other.value) === JSON.stringify(parsed.value));
|
|
55
|
+
}
|
|
56
|
+
const suffix = terminal.slice(warning.trim().length);
|
|
57
|
+
return terminal.startsWith(warning.trim()) && isRecoveryNotice(suffix);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isRecoveryNotice(text: string): boolean {
|
|
61
|
+
return /^\n\n(?:检测到临时模型服务错误,正在自动重试|模型续接状态失效,正在恢复连接并继续|已自动重试)/u.test(text);
|
|
62
|
+
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -270,6 +270,10 @@ export interface RuntimeExecutionSettings {
|
|
|
270
270
|
permissionMode: PermissionMode;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
export interface RuntimeSettingsReplacement {
|
|
274
|
+
emptyRemoteSessionId: string;
|
|
275
|
+
}
|
|
276
|
+
|
|
273
277
|
export interface RuntimeReleaseResult {
|
|
274
278
|
status: "released" | "busy";
|
|
275
279
|
activeSessionIds?: string[];
|
|
@@ -304,6 +308,8 @@ export interface AgentRuntime {
|
|
|
304
308
|
inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
|
|
305
309
|
synchronizeSession(sessionId: string): Promise<RuntimeSession>;
|
|
306
310
|
startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
|
|
311
|
+
// Continue a verified Responses replay failure without resubmitting user input.
|
|
312
|
+
recoverResponseContinuation?(sessionId: string, failedTurnId: string, signal?: AbortSignal): Promise<string>;
|
|
307
313
|
steerTurn(sessionId: string, turnId: string, prompt: RuntimePrompt): Promise<void>;
|
|
308
314
|
cancelTurn(sessionId: string, turnId: string): Promise<void>;
|
|
309
315
|
interruptRemoteTurn?(remoteSessionId: string, turnId: string): Promise<void>;
|
|
@@ -320,7 +326,7 @@ export interface AgentRuntime {
|
|
|
320
326
|
sessionId: string,
|
|
321
327
|
settings: RuntimeExecutionSettings,
|
|
322
328
|
// Called after remote verification, before releasing the task's operation lock.
|
|
323
|
-
persist?: (session: RuntimeSession) => Promise<void>,
|
|
329
|
+
persist?: (session: RuntimeSession, replacement?: RuntimeSettingsReplacement) => Promise<void>,
|
|
324
330
|
): Promise<RuntimeSession>;
|
|
325
331
|
respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
|
|
326
332
|
listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]>;
|