feat: reset idle assistant sessions after one hour

When a conversation's assistant session has been idle longer than
runtime.acp.assistantSessionResetIdleMs (default 1h, 0 disables) and
its owner has no unfinished proposals, the next inbound message starts
a fresh session instead of resuming. Binding lastActiveAt is persisted
per turn so the decision survives restarts.
This commit is contained in:
zenord
2026-08-19 16:17:54 +08:00
parent 9c3139380d
commit bd96595ba8
7 changed files with 128 additions and 8 deletions
+27 -2
View File
@@ -645,8 +645,12 @@ export class AssistantManager implements ConversationRuntime {
}
await this.reserveAssistantCapacity();
const persisted = this.store.getBinding(conversationKey);
const binding = persisted && this.validBinding(persisted) ? persisted : undefined;
let binding = persisted && this.validBinding(persisted) ? persisted : undefined;
if (persisted && !binding) await this.store.deleteBinding(conversationKey);
if (binding && this.shouldResetAssistantSession(conversationKey, binding)) {
await this.store.deleteBinding(conversationKey);
binding = undefined;
}
const cwd = this.prepareAssistantWorkspace(conversationKey, binding);
const spawnWorker = async (nativeSessionId?: string): Promise<AcpWorker> => {
const worker = new AcpWorker(this.bot, this.config, (crashed, error) => {
@@ -688,7 +692,8 @@ export class AssistantManager implements ConversationRuntime {
assistantWorkspace: cwd,
botFingerprint: this.bot.fingerprint,
createdAt: now,
updatedAt: now
updatedAt: now,
lastActiveAt: now
});
} catch (error) {
if (this.assistantWorkers.get(conversationKey) === worker) this.assistantWorkers.delete(conversationKey);
@@ -703,6 +708,26 @@ export class AssistantManager implements ConversationRuntime {
return binding.botFingerprint === this.bot.fingerprint && binding.agentId === this.bot.agent.id;
}
// Lazy idle reset: a long-inactive assistant session is dropped (fresh session on the next
// message) unless its owner still has an unfinished proposal that needs the old context.
private shouldResetAssistantSession(conversationKey: string, binding: AssistantBinding): boolean {
const resetMs = this.config.assistantSessionResetIdleMs;
if (!resetMs) return false;
const lastActiveAt = binding.lastActiveAt ?? binding.updatedAt;
const idleMs = Date.now() - lastActiveAt;
if (idleMs <= resetMs) return false;
const separator = conversationKey.lastIndexOf("#");
const chatKey = conversationKey.slice(0, separator);
const userId = conversationKey.slice(separator + 1);
const hasUnfinished = this.proposals.list().some((proposal) => proposal.ownerChatKey === chatKey
&& proposal.requesterUserId === userId
&& (proposal.status === "proposed" || proposal.status === "queued" || proposal.status === "working" || proposal.status === "pending"));
if (hasUnfinished) return false;
const keyHash = crypto.createHash("sha256").update(conversationKey).digest("hex").slice(0, 8);
console.log(`Assistant session reset for conversation ${keyHash} after ${Math.floor(idleMs / 1000)}s idle`);
return true;
}
private async reserveAssistantCapacity(): Promise<void> {
while (this.assistantWorkers.size >= this.maxAssistantSessions
|| this.assistantWorkers.size + (this.active ? 1 : 0) >= this.config.maxProcesses) {