























@@ -22,6 +22,8 @@ import { resolveGatewaySessionStoreTarget } from "../gateway/session-utils.js";
2222import { createSubsystemLogger } from "../logging/subsystem.js";
2323import { CommandLane } from "../process/lanes.js";
2424import { isAcpSessionKey, isCronSessionKey, isSubagentSessionKey } from "../routing/session-key.js";
25+import { normalizeOptionalString } from "../shared/string-coerce.js";
26+import { deliveryContextFromSession } from "../utils/delivery-context.shared.js";
2527import { resolveAgentSessionDirs } from "./session-dirs.js";
2628import type { SessionLockInspection } from "./session-write-lock.js";
2729@@ -30,6 +32,9 @@ const log = createSubsystemLogger("main-session-restart-recovery");
3032const DEFAULT_RECOVERY_DELAY_MS = 5_000;
3133const MAX_RECOVERY_RETRIES = 3;
3234const RETRY_BACKOFF_MULTIPLIER = 2;
35+const UNRESUMABLE_SESSION_NOTICE =
36+"I was interrupted by a gateway restart and couldn't safely resume the previous turn. " +
37+"Please send that last request again and I'll pick it up cleanly.";
33383439function shouldSkipMainRecovery(entry: SessionEntry, sessionKey: string): boolean {
3540if (typeof entry.spawnDepth === "number" && entry.spawnDepth > 0) {
@@ -274,6 +279,57 @@ async function markSessionFailed(params: {
274279log.warn(`marked interrupted main session failed: ${params.sessionKey} (${params.reason})`);
275280}
276281282+async function sendUnresumableSessionNotice(params: {
283+entry: SessionEntry;
284+reason: string;
285+sessionKey: string;
286+}): Promise<boolean> {
287+const deliveryContext = deliveryContextFromSession(params.entry);
288+const channel = normalizeOptionalString(deliveryContext?.channel);
289+const to = normalizeOptionalString(deliveryContext?.to);
290+if (!channel || !to) {
291+return false;
292+}
293+294+const messageParams: Record<string, unknown> = {
295+ to,
296+message: UNRESUMABLE_SESSION_NOTICE,
297+bestEffort: true,
298+};
299+if (deliveryContext?.threadId != null) {
300+messageParams.threadId = deliveryContext.threadId;
301+}
302+const actionParams: Record<string, unknown> = {
303+ channel,
304+action: "send",
305+sessionKey: params.sessionKey,
306+sessionId: params.entry.sessionId,
307+idempotencyKey: `main-session-restart-recovery:${params.entry.sessionId}:failed-notice`,
308+params: messageParams,
309+};
310+const accountId = normalizeOptionalString(deliveryContext?.accountId);
311+if (accountId) {
312+actionParams.accountId = accountId;
313+}
314+315+try {
316+await callGateway({
317+method: "message.action",
318+params: actionParams,
319+timeoutMs: 10_000,
320+});
321+log.info(
322+`sent interrupted main session recovery notice: ${params.sessionKey} (${params.reason})`,
323+);
324+return true;
325+} catch (err) {
326+log.warn(
327+`failed to send interrupted main session recovery notice ${params.sessionKey}: ${String(err)}`,
328+);
329+return false;
330+}
331+}
332+277333async function resumeMainSession(params: {
278334storePath: string;
279335sessionKey: string;
@@ -432,6 +488,11 @@ async function recoverStore(params: {
432488433489const resumeBlockReason = resolveMainSessionResumeBlockReason(messages);
434490if (resumeBlockReason) {
491+await sendUnresumableSessionNotice({
492+ entry,
493+ sessionKey,
494+reason: resumeBlockReason,
495+});
435496await markSessionFailed({
436497storePath: params.storePath,
437498 sessionKey,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。