@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
|
@@ -21,6 +21,7 @@ import { classifyFileContent } from "../local-files/LocalFileViewerServer.js";
|
|
|
21
21
|
import { errorLogValue } from "../logging/errorLogValue.js";
|
|
22
22
|
import { createTurnViewState, hydrateTurnViewState } from "../presentation/TurnStateReducer.js";
|
|
23
23
|
import { buildTurnGraphRows } from "../presentation/turnGraph.js";
|
|
24
|
+
import { displayRuntimeError, isResponseContinuationError } from "../runtime/responseErrors.js";
|
|
24
25
|
import { isActiveShellCommandJob, ShellCommandJobManager, } from "../shell/ShellCommandJobManager.js";
|
|
25
26
|
import { SessionOperationQueue } from "./SessionOperationQueue.js";
|
|
26
27
|
import { createId } from "../utils/id.js";
|
|
@@ -41,6 +42,7 @@ const MESSAGE_RECEIVED_REACTION = "OnIt";
|
|
|
41
42
|
const MESSAGE_COMPLETED_REACTION = "DONE";
|
|
42
43
|
const MESSAGE_FAILED_REACTION = "ERROR";
|
|
43
44
|
const MESSAGE_CANCELLED_REACTION = "CrossMark";
|
|
45
|
+
const INBOUND_MESSAGE_DELAY_WARNING_MS = 60_000;
|
|
44
46
|
const SESSION_PAGE_SIZE = 10;
|
|
45
47
|
const SESSION_PROMPT_PREVIEW_LENGTH = 50;
|
|
46
48
|
const DIRECTORY_PAGE_SIZE = 15;
|
|
@@ -54,7 +56,7 @@ const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
|
54
56
|
const SHELL_COMMAND_JOB_POLL_INTERVAL_MS = 1_000;
|
|
55
57
|
const SHELL_COMMAND_JOB_RELAUNCH_DELAY_MS = 5_000;
|
|
56
58
|
const APP_SERVER_RELEASE_POLL_INTERVAL_MS = 500;
|
|
57
|
-
const
|
|
59
|
+
const REFERENCE_INSTRUCTION_WINDOW_MS = 1_500;
|
|
58
60
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
59
61
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
60
62
|
const TURN_STOP_REQUESTED_MESSAGE = "已发送停止信号,任务会自动停止。";
|
|
@@ -223,6 +225,7 @@ export class ProxySessionController {
|
|
|
223
225
|
windowsDriveLister;
|
|
224
226
|
shellCommandJobs;
|
|
225
227
|
threadWriterProcesses;
|
|
228
|
+
referenceInstructionWindowMs;
|
|
226
229
|
router = new CommandRouter();
|
|
227
230
|
cardRenderer = new CardRenderer();
|
|
228
231
|
messageQueues = new Map();
|
|
@@ -245,6 +248,7 @@ export class ProxySessionController {
|
|
|
245
248
|
lastSessionListings = new Map();
|
|
246
249
|
threadInitializations = new Map();
|
|
247
250
|
llmRetryingSessions = new Set();
|
|
251
|
+
responseContinuations = new Map();
|
|
248
252
|
planHandoffs = new Map();
|
|
249
253
|
retriedFailureTurnIds = new Set();
|
|
250
254
|
queuedPromptRetryTimers = new Map();
|
|
@@ -252,8 +256,7 @@ export class ProxySessionController {
|
|
|
252
256
|
recoveryRetryStartedAt = new Map();
|
|
253
257
|
recoveredLiveAttempts = new Set();
|
|
254
258
|
recoveryClosed = false;
|
|
255
|
-
|
|
256
|
-
pendingResourceForwards = new Map();
|
|
259
|
+
pendingMessageReferences = new Map();
|
|
257
260
|
relatedReactionMessageIds = new Map();
|
|
258
261
|
shellCommandJobMonitors = new Map();
|
|
259
262
|
appServerReleaseSchedules = new Map();
|
|
@@ -262,7 +265,7 @@ export class ProxySessionController {
|
|
|
262
265
|
recoveryActivityHeartbeat;
|
|
263
266
|
startupRecovery;
|
|
264
267
|
queuedPromptsRestored = false;
|
|
265
|
-
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()) {
|
|
268
|
+
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(), referenceInstructionWindowMs = REFERENCE_INSTRUCTION_WINDOW_MS) {
|
|
266
269
|
this.config = config;
|
|
267
270
|
this.store = store;
|
|
268
271
|
this.runtimes = runtimes;
|
|
@@ -273,6 +276,7 @@ export class ProxySessionController {
|
|
|
273
276
|
this.windowsDriveLister = windowsDriveLister;
|
|
274
277
|
this.shellCommandJobs = shellCommandJobs;
|
|
275
278
|
this.threadWriterProcesses = threadWriterProcesses;
|
|
279
|
+
this.referenceInstructionWindowMs = referenceInstructionWindowMs;
|
|
276
280
|
for (const [, runtime] of this.runtimes.entries()) {
|
|
277
281
|
this.unsubscribe.push(runtime.onEvent((event) => {
|
|
278
282
|
void this.handleRuntimeEvent(event).catch((error) => {
|
|
@@ -305,14 +309,26 @@ export class ProxySessionController {
|
|
|
305
309
|
// retries cannot add duplicate reactions.
|
|
306
310
|
if (!this.store.claimInboundEvent(message.messageId, "message"))
|
|
307
311
|
return;
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
312
|
+
const acceptedAt = Date.now();
|
|
313
|
+
const receivedAt = message.receivedAt ?? acceptedAt;
|
|
314
|
+
const deliveryDelayMs = message.createdAt === undefined
|
|
315
|
+
? 0 : Math.max(0, receivedAt - message.createdAt);
|
|
316
|
+
const dispatchDelayMs = Math.max(0, acceptedAt - receivedAt);
|
|
317
|
+
if (deliveryDelayMs + dispatchDelayMs >= INBOUND_MESSAGE_DELAY_WARNING_MS) {
|
|
318
|
+
this.logger.warn({
|
|
319
|
+
messageId: message.messageId,
|
|
320
|
+
eventId: message.eventId,
|
|
321
|
+
contextKey: message.contextKey,
|
|
322
|
+
messageCreatedAt: message.createdAt,
|
|
323
|
+
receivedAt,
|
|
324
|
+
acceptedAt,
|
|
325
|
+
deliveryDelayMs,
|
|
326
|
+
dispatchDelayMs,
|
|
327
|
+
}, "Accepted a delayed Feishu message.");
|
|
328
|
+
}
|
|
329
|
+
const attachmentReservation = this.reserveReferenceInstruction(message);
|
|
330
|
+
const pendingReference = !attachmentReservation && isReferenceSource(message)
|
|
331
|
+
? this.registerPendingReference(message)
|
|
316
332
|
: undefined;
|
|
317
333
|
try {
|
|
318
334
|
const reactionId = await this.outbound.addReaction(message.contextKey, message.messageId, MESSAGE_RECEIVED_REACTION);
|
|
@@ -324,8 +340,10 @@ export class ProxySessionController {
|
|
|
324
340
|
this.logger.warn({ error, messageId: message.messageId, contextKey: message.contextKey }, "Failed to acknowledge the incoming Feishu message with a reaction.");
|
|
325
341
|
}
|
|
326
342
|
await this.waitForCurrentSessionReset(message.contextKey);
|
|
327
|
-
if (
|
|
328
|
-
|
|
343
|
+
if (this.recoveryClosed)
|
|
344
|
+
return;
|
|
345
|
+
if (attachmentReservation && this.completeReferenceInstruction(message, attachmentReservation)) {
|
|
346
|
+
this.store.audit(message.contextKey, "referenced_message_coalesced", {
|
|
329
347
|
messageId: message.messageId,
|
|
330
348
|
parentMessageId: message.parentMessageId,
|
|
331
349
|
});
|
|
@@ -344,20 +362,22 @@ export class ProxySessionController {
|
|
|
344
362
|
this.store.recordChatContext(chatContextKey, message.chatType);
|
|
345
363
|
this.store.markChatActive(chatContextKey);
|
|
346
364
|
}
|
|
347
|
-
if (
|
|
348
|
-
const
|
|
349
|
-
const attachment = await this.
|
|
365
|
+
if (pendingReference && !message.mergedForwardMessageId) {
|
|
366
|
+
const sourceMessage = message;
|
|
367
|
+
const attachment = await this.waitForReferenceInstruction(message.messageId, pendingReference);
|
|
368
|
+
if (this.recoveryClosed)
|
|
369
|
+
return;
|
|
350
370
|
if (attachment) {
|
|
351
371
|
coalescedForwardAttachment = true;
|
|
352
|
-
const sourceText =
|
|
353
|
-
const instruction = normalizeFeishuPostText(attachment.text)
|
|
354
|
-
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(
|
|
355
|
-
forwardedImages = (
|
|
356
|
-
messageId:
|
|
372
|
+
const sourceText = sourceMessage.text.trim();
|
|
373
|
+
const instruction = normalizeFeishuPostText(attachment.displayText ?? attachment.text)
|
|
374
|
+
|| (sourceText ? DEFAULT_REFERENCED_MESSAGE_INSTRUCTION : defaultResourcePrompt(sourceMessage));
|
|
375
|
+
forwardedImages = (sourceMessage.images ?? []).map((image) => ({
|
|
376
|
+
messageId: sourceMessage.messageId,
|
|
357
377
|
imageKey: image.imageKey,
|
|
358
378
|
}));
|
|
359
|
-
forwardedFiles = (
|
|
360
|
-
messageId:
|
|
379
|
+
forwardedFiles = (sourceMessage.files ?? []).map((file) => ({
|
|
380
|
+
messageId: sourceMessage.messageId,
|
|
361
381
|
fileKey: file.fileKey,
|
|
362
382
|
fileName: file.fileName,
|
|
363
383
|
}));
|
|
@@ -366,18 +386,18 @@ export class ProxySessionController {
|
|
|
366
386
|
text: sourceText ? referencedMessagePrompt(instruction, sourceText) : instruction,
|
|
367
387
|
displayText: instruction,
|
|
368
388
|
};
|
|
369
|
-
this.relatedReactionMessageIds.set(message.messageId, [
|
|
389
|
+
this.relatedReactionMessageIds.set(message.messageId, [sourceMessage.messageId]);
|
|
370
390
|
}
|
|
371
391
|
}
|
|
372
392
|
if (message.mergedForwardMessageId) {
|
|
373
393
|
const mergedForwardMessage = message;
|
|
374
394
|
const mergedForwardResult = this.outbound.readMergedForward(message.contextKey, message.mergedForwardMessageId).then((content) => ({ content }), (error) => ({ error }));
|
|
375
|
-
const attachment =
|
|
376
|
-
? await this.
|
|
395
|
+
const attachment = pendingReference
|
|
396
|
+
? await this.waitForReferenceInstruction(message.messageId, pendingReference)
|
|
377
397
|
: undefined;
|
|
378
398
|
if (attachment) {
|
|
379
399
|
coalescedForwardAttachment = true;
|
|
380
|
-
const instruction = normalizeFeishuPostText(attachment.text)
|
|
400
|
+
const instruction = normalizeFeishuPostText(attachment.displayText ?? attachment.text)
|
|
381
401
|
|| ((attachment.images?.length ?? 0) > 0 ? "请查看附带的内容。" : "请结合参考聊天记录处理。");
|
|
382
402
|
message = {
|
|
383
403
|
...attachment,
|
|
@@ -387,6 +407,8 @@ export class ProxySessionController {
|
|
|
387
407
|
this.relatedReactionMessageIds.set(message.messageId, [mergedForwardMessage.messageId]);
|
|
388
408
|
}
|
|
389
409
|
const result = await mergedForwardResult;
|
|
410
|
+
if (this.recoveryClosed)
|
|
411
|
+
return;
|
|
390
412
|
if ("error" in result) {
|
|
391
413
|
const replyTarget = message.replyInThread
|
|
392
414
|
? { messageId: message.messageId, replyInThread: true }
|
|
@@ -456,6 +478,9 @@ export class ProxySessionController {
|
|
|
456
478
|
const fileCount = fileReferences.length;
|
|
457
479
|
this.store.audit(message.contextKey, "incoming_message", {
|
|
458
480
|
messageId: message.messageId,
|
|
481
|
+
eventId: message.eventId,
|
|
482
|
+
messageCreatedAt: message.createdAt,
|
|
483
|
+
receivedAt: message.receivedAt,
|
|
459
484
|
text: message.text,
|
|
460
485
|
...(imageCount > 0 ? { imageCount } : {}),
|
|
461
486
|
...(fileCount > 0 ? { fileCount } : {}),
|
|
@@ -994,12 +1019,9 @@ export class ProxySessionController {
|
|
|
994
1019
|
this.remoteTurnHistoryRefreshes.clear();
|
|
995
1020
|
this.forkHistoryCursors.clear();
|
|
996
1021
|
this.turnHistoryHydrations.clear();
|
|
997
|
-
for (const pending of this.
|
|
1022
|
+
for (const pending of this.pendingMessageReferences.values())
|
|
998
1023
|
pending.resolveAttachment(undefined);
|
|
999
|
-
this.
|
|
1000
|
-
for (const pending of this.pendingResourceForwards.values())
|
|
1001
|
-
pending.resolveAttachment(undefined);
|
|
1002
|
-
this.pendingResourceForwards.clear();
|
|
1024
|
+
this.pendingMessageReferences.clear();
|
|
1003
1025
|
this.relatedReactionMessageIds.clear();
|
|
1004
1026
|
for (const timer of this.queuedPromptRetryTimers.values())
|
|
1005
1027
|
clearTimeout(timer);
|
|
@@ -3132,6 +3154,16 @@ export class ProxySessionController {
|
|
|
3132
3154
|
}
|
|
3133
3155
|
async startTurn(loaded, text, replyTarget, localImagePaths, options = {}) {
|
|
3134
3156
|
return this.settingsOperations.run(loaded.record.localSessionId, async () => {
|
|
3157
|
+
if (options.responseContinuationTurnId) {
|
|
3158
|
+
const current = this.store.getSession(loaded.record.localSessionId);
|
|
3159
|
+
if (!current || current.status === "closed" || current.lastTurnId !== options.responseContinuationTurnId
|
|
3160
|
+
|| this.sessionResets.has(current.localSessionId)
|
|
3161
|
+
|| this.responseContinuations.get(current.localSessionId)?.turnId !== options.responseContinuationTurnId
|
|
3162
|
+
|| this.store.getPendingProviderSettings(current.localSessionId)) {
|
|
3163
|
+
throw new Error("任务或运行设置已变化,未自动恢复模型续接。请发送消息继续。");
|
|
3164
|
+
}
|
|
3165
|
+
return this.startTurnNow(loaded, text, replyTarget, localImagePaths, options);
|
|
3166
|
+
}
|
|
3135
3167
|
try {
|
|
3136
3168
|
await this.applyPendingProviderSettings(loaded);
|
|
3137
3169
|
}
|
|
@@ -3196,15 +3228,26 @@ export class ProxySessionController {
|
|
|
3196
3228
|
|| this.sessionResets.has(loaded.record.localSessionId))) {
|
|
3197
3229
|
throw new Error("方案执行已取消。");
|
|
3198
3230
|
}
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3231
|
+
if (options.responseContinuationTurnId) {
|
|
3232
|
+
const recovery = this.responseContinuations.get(loaded.record.localSessionId);
|
|
3233
|
+
if (recovery?.turnId !== options.responseContinuationTurnId
|
|
3234
|
+
|| this.sessionResets.has(loaded.record.localSessionId) || !loaded.runtime.recoverResponseContinuation) {
|
|
3235
|
+
throw new Error("模型续接恢复已取消。");
|
|
3236
|
+
}
|
|
3237
|
+
turnId = await loaded.runtime.recoverResponseContinuation(loaded.record.localSessionId, options.responseContinuationTurnId, recovery.abort.signal);
|
|
3238
|
+
}
|
|
3239
|
+
else {
|
|
3240
|
+
turnId = await loaded.runtime.startTurn(loaded.record.localSessionId, options.approvedPlanTurnId
|
|
3241
|
+
? { text: runtimeText, collaborationMode: "default" }
|
|
3242
|
+
: runtimePrompt(runtimeText, localImagePaths));
|
|
3243
|
+
}
|
|
3202
3244
|
break;
|
|
3203
3245
|
}
|
|
3204
3246
|
catch (error) {
|
|
3205
3247
|
const message = error instanceof Error ? error.message : String(error);
|
|
3206
3248
|
const attempt = this.store.getTurnAttempt(attemptId);
|
|
3207
3249
|
const retryAttempt = attempt
|
|
3250
|
+
&& !options.responseContinuationTurnId
|
|
3208
3251
|
&& isRetryableLlmTurnFailure(message)
|
|
3209
3252
|
&& attempt.retryCount < MAX_LLM_TURN_RETRIES
|
|
3210
3253
|
? this.store.prepareUnstartedTurnAttemptRetry(attemptId)
|
|
@@ -3770,7 +3813,23 @@ export class ProxySessionController {
|
|
|
3770
3813
|
if (event.type === "turn_failed") {
|
|
3771
3814
|
if (this.retriedFailureTurnIds.has(event.turnId))
|
|
3772
3815
|
return;
|
|
3773
|
-
|
|
3816
|
+
const source = this.store.getSession(event.sessionId);
|
|
3817
|
+
const responseContinuation = failedAttempt?.retryCount === 0 && failedAttempt.status !== "cancelling"
|
|
3818
|
+
&& source && source.status !== "closed" && source.lastTurnId === event.turnId
|
|
3819
|
+
&& !this.sessionResets.has(event.sessionId) && !this.store.getPendingProviderSettings(event.sessionId)
|
|
3820
|
+
&& Boolean(this.runtimes.forAgent(source.agentName).recoverResponseContinuation)
|
|
3821
|
+
&& isResponseContinuationError(event.message);
|
|
3822
|
+
if (failedAttempt && responseContinuation) {
|
|
3823
|
+
const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
|
|
3824
|
+
if (retryAttempt) {
|
|
3825
|
+
this.retriedFailureTurnIds.add(event.turnId);
|
|
3826
|
+
this.responseContinuations.set(event.sessionId, { turnId: event.turnId, abort: new AbortController() });
|
|
3827
|
+
await this.retryFailedLlmTurn(event, retryAttempt, true);
|
|
3828
|
+
return;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
if (failedAttempt && !this.responseContinuations.has(event.sessionId)
|
|
3832
|
+
&& isRetryableLlmTurnFailure(event.message) && failedAttempt.retryCount < MAX_LLM_TURN_RETRIES) {
|
|
3774
3833
|
const retryAttempt = this.store.prepareTurnAttemptRetry(failedAttempt.attemptId, event.turnId);
|
|
3775
3834
|
if (retryAttempt) {
|
|
3776
3835
|
this.retriedFailureTurnIds.add(event.turnId);
|
|
@@ -3923,7 +3982,7 @@ export class ProxySessionController {
|
|
|
3923
3982
|
await this.scheduleNextQueuedPrompt(sessionId);
|
|
3924
3983
|
}
|
|
3925
3984
|
}
|
|
3926
|
-
async retryFailedLlmTurn(event, attempt) {
|
|
3985
|
+
async retryFailedLlmTurn(event, attempt, responseContinuation = false) {
|
|
3927
3986
|
const retryNumber = attempt.retryCount;
|
|
3928
3987
|
const session = this.store.getSession(event.sessionId);
|
|
3929
3988
|
if (!session || session.status === "closed") {
|
|
@@ -3939,13 +3998,16 @@ export class ProxySessionController {
|
|
|
3939
3998
|
attemptId: attempt.attemptId,
|
|
3940
3999
|
failedTurnId: event.turnId,
|
|
3941
4000
|
retryNumber,
|
|
3942
|
-
maxRetries: MAX_LLM_TURN_RETRIES,
|
|
4001
|
+
maxRetries: responseContinuation ? 1 : MAX_LLM_TURN_RETRIES,
|
|
4002
|
+
responseContinuation,
|
|
3943
4003
|
error: event.message,
|
|
3944
4004
|
});
|
|
3945
4005
|
try {
|
|
3946
4006
|
await this.outbound.onEvent({
|
|
3947
4007
|
...event,
|
|
3948
|
-
message:
|
|
4008
|
+
message: responseContinuation
|
|
4009
|
+
? `${event.message}\n\n模型续接状态失效,正在恢复连接并继续(1/1)。`
|
|
4010
|
+
: `${event.message}\n\n检测到临时模型服务错误,正在自动重试(${retryNumber}/${MAX_LLM_TURN_RETRIES})。`,
|
|
3949
4011
|
});
|
|
3950
4012
|
}
|
|
3951
4013
|
catch (error) {
|
|
@@ -3957,25 +4019,36 @@ export class ProxySessionController {
|
|
|
3957
4019
|
try {
|
|
3958
4020
|
const current = this.store.getSession(session.localSessionId) ?? session;
|
|
3959
4021
|
const loaded = await this.loadSession(current, Boolean(this.store.getPendingProviderSettings(current.localSessionId)));
|
|
3960
|
-
const retryTurnId = await this.startTurn(loaded, llmRetryRuntimePrompt(attempt.promptText, retryNumber), replyTarget, attempt.localImagePaths, {
|
|
4022
|
+
const retryTurnId = await this.startTurn(loaded, responseContinuation ? "" : llmRetryRuntimePrompt(attempt.promptText, retryNumber), replyTarget, responseContinuation ? undefined : attempt.localImagePaths, {
|
|
3961
4023
|
attemptId: attempt.attemptId,
|
|
3962
4024
|
messageId: attempt.messageId,
|
|
3963
|
-
displayPrompt: llmRetryCardPrompt(attempt.promptText, retryNumber),
|
|
4025
|
+
displayPrompt: responseContinuation ? `恢复模型续接:${attempt.promptText}` : llmRetryCardPrompt(attempt.promptText, retryNumber),
|
|
3964
4026
|
preserveAttemptOnFailure: true,
|
|
4027
|
+
...(responseContinuation ? { responseContinuationTurnId: event.turnId } : {}),
|
|
3965
4028
|
});
|
|
3966
4029
|
this.store.rebindPendingTurnMessages(session.localSessionId, event.turnId, retryTurnId);
|
|
3967
4030
|
}
|
|
3968
4031
|
catch (error) {
|
|
3969
|
-
this.
|
|
3970
|
-
this.store.
|
|
3971
|
-
|
|
4032
|
+
const aborted = responseContinuation && !this.responseContinuations.has(session.localSessionId);
|
|
4033
|
+
this.store.updateTurnAttempt(attempt.attemptId, { status: aborted ? "cancelled" : "failed" });
|
|
4034
|
+
const latest = this.store.getSession(session.localSessionId);
|
|
4035
|
+
if (!responseContinuation || (latest?.lastTurnId === event.turnId && latest.status !== "closed")) {
|
|
4036
|
+
this.store.updateSession(session.localSessionId, { status: aborted ? "ready" : "failed" });
|
|
4037
|
+
}
|
|
4038
|
+
await this.finalizeTurnMessageReactions(event.turnId, aborted ? "cancelled" : "failed").catch((reactionError) => {
|
|
3972
4039
|
this.logger.warn({ error: reactionError, sessionId: session.localSessionId, turnId: event.turnId }, "Failed to finalize reactions after an LLM retry could not start.");
|
|
3973
4040
|
});
|
|
3974
4041
|
this.logger.error({ error, sessionId: session.localSessionId, retryNumber }, "Failed to start an automatic LLM turn retry.");
|
|
4042
|
+
if (responseContinuation && !aborted) {
|
|
4043
|
+
await this.outbound.sendText(attempt.contextKey, `模型续接恢复未能启动:${displayRuntimeError(runtimeErrorMessage(error))}。请发送消息继续。`);
|
|
4044
|
+
}
|
|
3975
4045
|
await this.scheduleNextQueuedPrompt(session.localSessionId);
|
|
3976
4046
|
}
|
|
3977
4047
|
finally {
|
|
3978
4048
|
this.llmRetryingSessions.delete(session.localSessionId);
|
|
4049
|
+
if (this.responseContinuations.get(session.localSessionId)?.turnId === event.turnId) {
|
|
4050
|
+
this.responseContinuations.delete(session.localSessionId);
|
|
4051
|
+
}
|
|
3979
4052
|
}
|
|
3980
4053
|
}
|
|
3981
4054
|
scheduleNextQueuedPrompt(sessionId) {
|
|
@@ -4090,7 +4163,7 @@ export class ProxySessionController {
|
|
|
4090
4163
|
await this.finalizeTurnMessageReactions(turnId, session.lastTurnStatus);
|
|
4091
4164
|
}
|
|
4092
4165
|
}
|
|
4093
|
-
|
|
4166
|
+
registerPendingReference(message) {
|
|
4094
4167
|
let resolveAttachment;
|
|
4095
4168
|
const attachmentPromise = new Promise((resolve) => {
|
|
4096
4169
|
resolveAttachment = resolve;
|
|
@@ -4101,60 +4174,53 @@ export class ProxySessionController {
|
|
|
4101
4174
|
attachmentPromise,
|
|
4102
4175
|
resolveAttachment,
|
|
4103
4176
|
};
|
|
4104
|
-
|
|
4177
|
+
this.pendingMessageReferences.set(message.messageId, pending);
|
|
4105
4178
|
return pending;
|
|
4106
4179
|
}
|
|
4107
|
-
|
|
4108
|
-
|
|
4180
|
+
reserveReferenceInstruction(message) {
|
|
4181
|
+
const parentMessageId = quotedMessageId(message);
|
|
4182
|
+
if (!parentMessageId || isCommandMessage(message))
|
|
4109
4183
|
return undefined;
|
|
4110
|
-
const
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
if (!pending || pending.contextKey !== message.contextKey
|
|
4117
|
-
|| pending.userId !== message.userId || pending.attachmentMessageId)
|
|
4118
|
-
continue;
|
|
4119
|
-
pending.attachmentMessageId = message.messageId;
|
|
4120
|
-
return {
|
|
4121
|
-
sourceMessageId: message.parentMessageId,
|
|
4122
|
-
sourceKind: candidate.sourceKind,
|
|
4123
|
-
pending,
|
|
4124
|
-
registry: candidate.registry,
|
|
4125
|
-
};
|
|
4126
|
-
}
|
|
4127
|
-
return undefined;
|
|
4184
|
+
const pending = this.pendingMessageReferences.get(parentMessageId);
|
|
4185
|
+
if (!pending || pending.contextKey !== message.contextKey
|
|
4186
|
+
|| pending.userId !== message.userId || pending.attachmentMessageId)
|
|
4187
|
+
return undefined;
|
|
4188
|
+
pending.attachmentMessageId = message.messageId;
|
|
4189
|
+
return { sourceMessageId: parentMessageId, pending };
|
|
4128
4190
|
}
|
|
4129
|
-
|
|
4130
|
-
|
|
4191
|
+
completeReferenceInstruction(message, reservation) {
|
|
4192
|
+
const parentMessageId = quotedMessageId(message);
|
|
4193
|
+
if (!parentMessageId || reservation.sourceMessageId !== parentMessageId)
|
|
4131
4194
|
return false;
|
|
4132
|
-
if (
|
|
4133
|
-
return false;
|
|
4134
|
-
if (reservation.registry.get(message.parentMessageId) !== reservation.pending)
|
|
4195
|
+
if (this.pendingMessageReferences.get(parentMessageId) !== reservation.pending)
|
|
4135
4196
|
return false;
|
|
4136
4197
|
if (reservation.pending.attachmentMessageId !== message.messageId)
|
|
4137
4198
|
return false;
|
|
4138
4199
|
reservation.pending.resolveAttachment(message);
|
|
4139
4200
|
return true;
|
|
4140
4201
|
}
|
|
4141
|
-
async
|
|
4142
|
-
const
|
|
4202
|
+
async waitForReferenceInstruction(messageId, pending) {
|
|
4203
|
+
const expire = () => {
|
|
4143
4204
|
// Arrival reserves the pair before acknowledgement. A slow Reaction must not split it.
|
|
4144
4205
|
if (pending.attachmentMessageId)
|
|
4145
4206
|
return;
|
|
4146
|
-
if (
|
|
4147
|
-
|
|
4207
|
+
if (this.pendingMessageReferences.get(messageId) === pending)
|
|
4208
|
+
this.pendingMessageReferences.delete(messageId);
|
|
4148
4209
|
pending.resolveAttachment(undefined);
|
|
4149
|
-
}
|
|
4150
|
-
timer.
|
|
4210
|
+
};
|
|
4211
|
+
const timer = this.referenceInstructionWindowMs > 0 ? setTimeout(expire, this.referenceInstructionWindowMs) : undefined;
|
|
4212
|
+
if (timer)
|
|
4213
|
+
timer.unref?.();
|
|
4214
|
+
else
|
|
4215
|
+
expire();
|
|
4151
4216
|
try {
|
|
4152
4217
|
return await pending.attachmentPromise;
|
|
4153
4218
|
}
|
|
4154
4219
|
finally {
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4220
|
+
if (timer)
|
|
4221
|
+
clearTimeout(timer);
|
|
4222
|
+
if (this.pendingMessageReferences.get(messageId) === pending) {
|
|
4223
|
+
this.pendingMessageReferences.delete(messageId);
|
|
4158
4224
|
}
|
|
4159
4225
|
}
|
|
4160
4226
|
}
|
|
@@ -4213,6 +4279,8 @@ export class ProxySessionController {
|
|
|
4213
4279
|
await this.cancelSession(record);
|
|
4214
4280
|
}
|
|
4215
4281
|
async cancelSession(record) {
|
|
4282
|
+
this.responseContinuations.get(record.localSessionId)?.abort.abort();
|
|
4283
|
+
this.responseContinuations.delete(record.localSessionId);
|
|
4216
4284
|
this.planHandoffs.delete(record.localSessionId);
|
|
4217
4285
|
const runtime = this.runtimes.forAgent(record.agentName);
|
|
4218
4286
|
if (runtime.kind === "codex" && record.remoteSessionId && runtime.interruptRemoteTurn) {
|
|
@@ -4290,6 +4358,8 @@ export class ProxySessionController {
|
|
|
4290
4358
|
});
|
|
4291
4359
|
}
|
|
4292
4360
|
async runSessionReset(sessionId, operation) {
|
|
4361
|
+
this.responseContinuations.get(sessionId)?.abort.abort();
|
|
4362
|
+
this.responseContinuations.delete(sessionId);
|
|
4293
4363
|
this.planHandoffs.delete(sessionId);
|
|
4294
4364
|
const previous = this.sessionResets.get(sessionId) ?? Promise.resolve();
|
|
4295
4365
|
const reset = previous.catch(() => undefined).then(operation);
|
|
@@ -4671,9 +4741,16 @@ export class ProxySessionController {
|
|
|
4671
4741
|
throw new Error("当前运行时不支持 Provider 设置。");
|
|
4672
4742
|
loaded.session = loaded.runtime.getSession(loaded.record.localSessionId) ?? loaded.session;
|
|
4673
4743
|
const remoteSessionId = loaded.session.remoteSessionId;
|
|
4674
|
-
const persist = (candidate) => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
4744
|
+
const persist = (candidate, replacement) => this.agentDefaultsOperations.run(loaded.record.agentName, async () => {
|
|
4675
4745
|
if (candidate.remoteSessionId !== remoteSessionId) {
|
|
4676
|
-
|
|
4746
|
+
const saved = this.store.getSession(loaded.record.localSessionId);
|
|
4747
|
+
if (replacement?.emptyRemoteSessionId !== remoteSessionId || !saved || saved.remoteSessionId !== remoteSessionId
|
|
4748
|
+
|| saved.status === "closed" || saved.status === "running" || saved.lastTurnId || loaded.session.activeTurnId || candidate.activeTurnId
|
|
4749
|
+
|| candidate.localSessionId !== saved.localSessionId || !candidate.remoteSessionId
|
|
4750
|
+
|| candidate.agentName !== saved.agentName || candidate.cwd !== saved.cwd || candidate.runtimeKind !== loaded.session.runtimeKind
|
|
4751
|
+
|| this.store.hasTurnAttemptsForSession(saved.localSessionId) || this.store.getForkHistorySource(saved.localSessionId)) {
|
|
4752
|
+
throw new Error("设置切换返回了不同的任务 ID,未保存设置;仅允许已验证的空会话更换底层任务。");
|
|
4753
|
+
}
|
|
4677
4754
|
}
|
|
4678
4755
|
assertProviderSettingsApplied(candidate, settings);
|
|
4679
4756
|
const agent = this.ensureAgent(loaded.record.agentName);
|
|
@@ -4681,6 +4758,7 @@ export class ProxySessionController {
|
|
|
4681
4758
|
await this.persistAgentExecutionDefaultsNow(loaded.record.localSessionId, candidate);
|
|
4682
4759
|
try {
|
|
4683
4760
|
this.store.updateRuntimeSession(loaded.record.localSessionId, {
|
|
4761
|
+
remoteSessionId: candidate.remoteSessionId,
|
|
4684
4762
|
modelProvider: candidate.modelProvider,
|
|
4685
4763
|
model: candidate.model,
|
|
4686
4764
|
reasoningEffort: candidate.reasoningEffort,
|
|
@@ -4694,6 +4772,7 @@ export class ProxySessionController {
|
|
|
4694
4772
|
}
|
|
4695
4773
|
});
|
|
4696
4774
|
const session = await loaded.runtime.setExecutionSettings(loaded.record.localSessionId, settings, persist);
|
|
4775
|
+
loaded.record = this.store.getSession(loaded.record.localSessionId) ?? loaded.record;
|
|
4697
4776
|
this.settingsOnlySessions.delete(loaded.record.localSessionId);
|
|
4698
4777
|
this.outbound.updateSessionModel(loaded.record.localSessionId, session.model, session.modelProvider);
|
|
4699
4778
|
return session;
|
|
@@ -5858,6 +5937,7 @@ export class ProxySessionController {
|
|
|
5858
5937
|
throw new Error(`这不是目录:${directory}`);
|
|
5859
5938
|
}
|
|
5860
5939
|
async createDirectoryFromCard(contextKey, action) {
|
|
5940
|
+
const updateMessageId = requiredCardMessageId(action.messageId);
|
|
5861
5941
|
const directory = directoryActionPath(action.value.directory);
|
|
5862
5942
|
await this.assertBrowsableDirectory(directory);
|
|
5863
5943
|
const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
|
|
@@ -5873,9 +5953,9 @@ export class ProxySessionController {
|
|
|
5873
5953
|
throw new Error(`目录已存在:${newDirectory}`);
|
|
5874
5954
|
throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
|
|
5875
5955
|
}
|
|
5876
|
-
await this.openDirectoryBrowser(contextKey,
|
|
5877
|
-
updateMessageId
|
|
5878
|
-
page:
|
|
5956
|
+
await this.openDirectoryBrowser(contextKey, newDirectory, {
|
|
5957
|
+
updateMessageId,
|
|
5958
|
+
page: 0,
|
|
5879
5959
|
});
|
|
5880
5960
|
await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
|
|
5881
5961
|
}
|
|
@@ -8145,13 +8225,10 @@ function isCommandMessage(message) {
|
|
|
8145
8225
|
const text = normalizeFeishuPostText(message.text).trimStart();
|
|
8146
8226
|
return text.startsWith("/") || text.startsWith("!");
|
|
8147
8227
|
}
|
|
8148
|
-
function
|
|
8149
|
-
return !message
|
|
8150
|
-
&& !message.parentMessageId
|
|
8228
|
+
function isReferenceSource(message) {
|
|
8229
|
+
return !quotedMessageId(message)
|
|
8151
8230
|
&& !message.contextKey.startsWith("console:")
|
|
8152
|
-
&& !isCommandMessage(message)
|
|
8153
|
-
&& (message.messageType === "post"
|
|
8154
|
-
|| (message.images?.length ?? 0) > 0 || (message.files?.length ?? 0) > 0);
|
|
8231
|
+
&& !isCommandMessage(message);
|
|
8155
8232
|
}
|
|
8156
8233
|
function deduplicateImageReferences(images) {
|
|
8157
8234
|
const seen = new Set();
|