feat: add /reset prune to delete session entry and transcript
When user sends '/reset prune', the session entry is removed from the session store and the transcript .jsonl file is deleted. This is useful when deleting Telegram DM topics - users can first prune the session to avoid orphaned session files. Changes: - Add pruneRequested flag to SessionInitResult - Detect 'prune' argument after reset trigger - Add deleteSessionEntry() function to store.ts - Handle prune in get-reply.ts before normal reset flow
This commit is contained in:
parent
f29f51569a
commit
c5ddd12612
@ -7,6 +7,7 @@ import { resolveModelRefFromString } from "../../agents/model-selection.js";
|
|||||||
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
import { resolveAgentTimeoutMs } from "../../agents/timeout.js";
|
||||||
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js";
|
import { DEFAULT_AGENT_WORKSPACE_DIR, ensureAgentWorkspace } from "../../agents/workspace.js";
|
||||||
import { type ClawdbotConfig, loadConfig } from "../../config/config.js";
|
import { type ClawdbotConfig, loadConfig } from "../../config/config.js";
|
||||||
|
import { deleteSessionEntry, resolveSessionTranscriptPath } from "../../config/sessions.js";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
@ -116,6 +117,7 @@ export async function getReplyFromConfig(
|
|||||||
sessionId,
|
sessionId,
|
||||||
isNewSession,
|
isNewSession,
|
||||||
resetTriggered,
|
resetTriggered,
|
||||||
|
pruneRequested,
|
||||||
systemSent,
|
systemSent,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
storePath,
|
storePath,
|
||||||
@ -126,6 +128,19 @@ export async function getReplyFromConfig(
|
|||||||
bodyStripped,
|
bodyStripped,
|
||||||
} = sessionState;
|
} = sessionState;
|
||||||
|
|
||||||
|
// Handle /reset prune - delete session entry and transcript, then return confirmation
|
||||||
|
if (resetTriggered && pruneRequested && previousSessionEntry) {
|
||||||
|
const transcriptPath = previousSessionEntry.sessionId
|
||||||
|
? resolveSessionTranscriptPath(previousSessionEntry.sessionId, agentId)
|
||||||
|
: undefined;
|
||||||
|
await deleteSessionEntry({
|
||||||
|
storePath,
|
||||||
|
sessionKey,
|
||||||
|
transcriptPath,
|
||||||
|
});
|
||||||
|
return { text: "✅ Session pruned." };
|
||||||
|
}
|
||||||
|
|
||||||
await applyResetModelOverride({
|
await applyResetModelOverride({
|
||||||
cfg,
|
cfg,
|
||||||
resetTriggered,
|
resetTriggered,
|
||||||
|
|||||||
@ -43,6 +43,8 @@ export type SessionInitResult = {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
isNewSession: boolean;
|
isNewSession: boolean;
|
||||||
resetTriggered: boolean;
|
resetTriggered: boolean;
|
||||||
|
/** When true, the session entry and transcript should be deleted after reset. */
|
||||||
|
pruneRequested: boolean;
|
||||||
systemSent: boolean;
|
systemSent: boolean;
|
||||||
abortedLastRun: boolean;
|
abortedLastRun: boolean;
|
||||||
storePath: string;
|
storePath: string;
|
||||||
@ -125,6 +127,7 @@ export async function initSessionState(params: {
|
|||||||
let systemSent = false;
|
let systemSent = false;
|
||||||
let abortedLastRun = false;
|
let abortedLastRun = false;
|
||||||
let resetTriggered = false;
|
let resetTriggered = false;
|
||||||
|
let pruneRequested = false;
|
||||||
|
|
||||||
let persistedThinking: string | undefined;
|
let persistedThinking: string | undefined;
|
||||||
let persistedVerbose: string | undefined;
|
let persistedVerbose: string | undefined;
|
||||||
@ -186,6 +189,12 @@ export async function initSessionState(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if prune was requested (e.g., "/reset prune")
|
||||||
|
if (resetTriggered && bodyStripped?.toLowerCase() === "prune") {
|
||||||
|
pruneRequested = true;
|
||||||
|
bodyStripped = ""; // Clear the body since "prune" is a directive, not a message
|
||||||
|
}
|
||||||
|
|
||||||
sessionKey = resolveSessionKey(sessionScope, sessionCtxForState, mainKey);
|
sessionKey = resolveSessionKey(sessionScope, sessionCtxForState, mainKey);
|
||||||
const entry = sessionStore[sessionKey];
|
const entry = sessionStore[sessionKey];
|
||||||
const previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
|
const previousSessionEntry = resetTriggered && entry ? { ...entry } : undefined;
|
||||||
@ -364,6 +373,7 @@ export async function initSessionState(params: {
|
|||||||
sessionId: sessionId ?? crypto.randomUUID(),
|
sessionId: sessionId ?? crypto.randomUUID(),
|
||||||
isNewSession,
|
isNewSession,
|
||||||
resetTriggered,
|
resetTriggered,
|
||||||
|
pruneRequested,
|
||||||
systemSent,
|
systemSent,
|
||||||
abortedLastRun,
|
abortedLastRun,
|
||||||
storePath,
|
storePath,
|
||||||
|
|||||||
@ -438,3 +438,46 @@ export async function updateLastRoute(params: {
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a session entry from the store and optionally remove its transcript file.
|
||||||
|
* Used by `/reset prune` to fully remove a session.
|
||||||
|
*/
|
||||||
|
export async function deleteSessionEntry(params: {
|
||||||
|
storePath: string;
|
||||||
|
sessionKey: string;
|
||||||
|
/** Path to the transcript file to delete (optional). */
|
||||||
|
transcriptPath?: string;
|
||||||
|
}): Promise<{ deleted: boolean; transcriptDeleted: boolean }> {
|
||||||
|
const { storePath, sessionKey, transcriptPath } = params;
|
||||||
|
let deleted = false;
|
||||||
|
let transcriptDeleted = false;
|
||||||
|
|
||||||
|
await withSessionStoreLock(storePath, async () => {
|
||||||
|
const store = loadSessionStore(storePath, { skipCache: true });
|
||||||
|
if (sessionKey in store) {
|
||||||
|
delete store[sessionKey];
|
||||||
|
await saveSessionStoreUnlocked(storePath, store);
|
||||||
|
deleted = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete transcript file outside the lock (it's a separate file)
|
||||||
|
if (transcriptPath) {
|
||||||
|
try {
|
||||||
|
await fs.promises.unlink(transcriptPath);
|
||||||
|
transcriptDeleted = true;
|
||||||
|
} catch (err) {
|
||||||
|
const code =
|
||||||
|
err && typeof err === "object" && "code" in err
|
||||||
|
? String((err as { code?: unknown }).code)
|
||||||
|
: null;
|
||||||
|
// Ignore if file doesn't exist
|
||||||
|
if (code !== "ENOENT") {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { deleted, transcriptDeleted };
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user