fix(sessions): lock store saves; wait for bash close
This commit is contained in:
@@ -436,7 +436,9 @@ export function createBashTool(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
child.once("exit", (code, exitSignal) => {
|
// `exit` can fire before stdio fully flushes (notably on Windows).
|
||||||
|
// `close` waits for streams to close, so aggregated output is complete.
|
||||||
|
child.once("close", (code, exitSignal) => {
|
||||||
handleExit(code, exitSignal);
|
handleExit(code, exitSignal);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -448,15 +448,32 @@ export function loadSessionStore(
|
|||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveSessionStore(
|
async function saveSessionStoreUnlocked(
|
||||||
storePath: string,
|
storePath: string,
|
||||||
store: Record<string, SessionEntry>,
|
store: Record<string, SessionEntry>,
|
||||||
) {
|
): Promise<void> {
|
||||||
// Invalidate cache on write to ensure consistency
|
// Invalidate cache on write to ensure consistency
|
||||||
invalidateSessionStoreCache(storePath);
|
invalidateSessionStoreCache(storePath);
|
||||||
|
|
||||||
await fs.promises.mkdir(path.dirname(storePath), { recursive: true });
|
await fs.promises.mkdir(path.dirname(storePath), { recursive: true });
|
||||||
const json = JSON.stringify(store, null, 2);
|
const json = JSON.stringify(store, null, 2);
|
||||||
|
|
||||||
|
// Windows: avoid atomic rename swaps (can be flaky under concurrent access).
|
||||||
|
// We serialize writers via the session-store lock instead.
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
try {
|
||||||
|
await fs.promises.writeFile(storePath, json, "utf-8");
|
||||||
|
} catch (err) {
|
||||||
|
const code =
|
||||||
|
err && typeof err === "object" && "code" in err
|
||||||
|
? String((err as { code?: unknown }).code)
|
||||||
|
: null;
|
||||||
|
if (code === "ENOENT") return;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const tmp = `${storePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
const tmp = `${storePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
||||||
try {
|
try {
|
||||||
await fs.promises.writeFile(tmp, json, "utf-8");
|
await fs.promises.writeFile(tmp, json, "utf-8");
|
||||||
@@ -490,6 +507,15 @@ export async function saveSessionStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveSessionStore(
|
||||||
|
storePath: string,
|
||||||
|
store: Record<string, SessionEntry>,
|
||||||
|
): Promise<void> {
|
||||||
|
await withSessionStoreLock(storePath, async () => {
|
||||||
|
await saveSessionStoreUnlocked(storePath, store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
type SessionStoreLockOptions = {
|
type SessionStoreLockOptions = {
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
pollIntervalMs?: number;
|
pollIntervalMs?: number;
|
||||||
@@ -571,7 +597,7 @@ export async function updateSessionStoreEntry(params: {
|
|||||||
if (!patch) return existing;
|
if (!patch) return existing;
|
||||||
const next = mergeSessionEntry(existing, patch);
|
const next = mergeSessionEntry(existing, patch);
|
||||||
store[sessionKey] = next;
|
store[sessionKey] = next;
|
||||||
await saveSessionStore(storePath, store);
|
await saveSessionStoreUnlocked(storePath, store);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -584,20 +610,22 @@ export async function updateLastRoute(params: {
|
|||||||
accountId?: string;
|
accountId?: string;
|
||||||
}) {
|
}) {
|
||||||
const { storePath, sessionKey, provider, to, accountId } = params;
|
const { storePath, sessionKey, provider, to, accountId } = params;
|
||||||
const store = loadSessionStore(storePath);
|
return await withSessionStoreLock(storePath, async () => {
|
||||||
const existing = store[sessionKey];
|
const store = loadSessionStore(storePath);
|
||||||
const now = Date.now();
|
const existing = store[sessionKey];
|
||||||
const next = mergeSessionEntry(existing, {
|
const now = Date.now();
|
||||||
updatedAt: Math.max(existing?.updatedAt ?? 0, now),
|
const next = mergeSessionEntry(existing, {
|
||||||
lastProvider: provider,
|
updatedAt: Math.max(existing?.updatedAt ?? 0, now),
|
||||||
lastTo: to?.trim() ? to.trim() : undefined,
|
lastProvider: provider,
|
||||||
lastAccountId: accountId?.trim()
|
lastTo: to?.trim() ? to.trim() : undefined,
|
||||||
? accountId.trim()
|
lastAccountId: accountId?.trim()
|
||||||
: existing?.lastAccountId,
|
? accountId.trim()
|
||||||
|
: existing?.lastAccountId,
|
||||||
|
});
|
||||||
|
store[sessionKey] = next;
|
||||||
|
await saveSessionStoreUnlocked(storePath, store);
|
||||||
|
return next;
|
||||||
});
|
});
|
||||||
store[sessionKey] = next;
|
|
||||||
await saveSessionStore(storePath, store);
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decide which session bucket to use (per-sender vs global).
|
// Decide which session bucket to use (per-sender vs global).
|
||||||
|
|||||||
Reference in New Issue
Block a user