feat: auto-delete old conversations, and stop a tool call that repeats (#15)
This commit was merged in pull request #15.
This commit is contained in:
+89
-4
@@ -1,5 +1,5 @@
|
||||
import { getPreferenceValues } from "@vicinae/api";
|
||||
import { DEFAULT_HOST, DEFAULT_MODEL } from "./constants";
|
||||
import { DEFAULT_HOST, DEFAULT_MAX_TOOL_ROUNDS, DEFAULT_MODEL } from "./constants";
|
||||
|
||||
/**
|
||||
* Preferences, read and normalised.
|
||||
@@ -22,6 +22,8 @@ export interface LamPreferences {
|
||||
showToolCalls?: boolean;
|
||||
showReasoning?: boolean;
|
||||
resumeTimeout?: string;
|
||||
autoDeleteAfter?: string;
|
||||
maxToolRounds?: string;
|
||||
}
|
||||
|
||||
/** Preferences with every optional value filled in, so callers never branch on undefined. */
|
||||
@@ -49,6 +51,22 @@ export interface ResolvedPreferences {
|
||||
showReasoning: boolean;
|
||||
/** Idle time after which opening the command starts a fresh chat. Undefined means never. */
|
||||
resumeTimeoutMs?: number;
|
||||
/**
|
||||
* Age past which a conversation is deleted the next time a command runs.
|
||||
*
|
||||
* Undefined means the sweep is off entirely, which is the default. Every path
|
||||
* that cannot answer the question confidently resolves here, so a preference
|
||||
* that is missing, empty, or unrecognised deletes nothing.
|
||||
*/
|
||||
autoDeleteMs?: number;
|
||||
/**
|
||||
* Rounds of tool calls one turn may spend. Undefined means unlimited.
|
||||
*
|
||||
* Note the opposite convention to {@link ResolvedPreferences.autoDeleteMs}:
|
||||
* there, "unrecognised" falls back to doing nothing, and here it falls back to
|
||||
* the cap. See {@link resolveMaxToolRounds}.
|
||||
*/
|
||||
maxToolRounds?: number;
|
||||
}
|
||||
|
||||
const MINUTE = 60_000;
|
||||
@@ -68,18 +86,82 @@ export function resolveResumeTimeout(value: string | undefined): number | undefi
|
||||
return RESUME_TIMEOUTS[(value ?? "").trim()];
|
||||
}
|
||||
|
||||
/** Keep in sync with the `autoDeleteAfter` dropdown in `package.json`. */
|
||||
const AUTO_DELETE_WINDOWS: Record<string, number> = {
|
||||
"1d": DAY,
|
||||
"7d": 7 * DAY,
|
||||
"30d": 30 * DAY,
|
||||
"90d": 90 * DAY,
|
||||
};
|
||||
|
||||
/**
|
||||
* `undefined` for "disabled" and for anything unrecognised.
|
||||
*
|
||||
* Nothing here can turn an unrecognised value into a deletion: the fallback is
|
||||
* the off switch, which is the only safe reading of a preference this codebase
|
||||
* does not understand.
|
||||
*/
|
||||
export function resolveAutoDelete(value: string | undefined): number | undefined {
|
||||
return AUTO_DELETE_WINDOWS[(value ?? "").trim().toLowerCase()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Round limits, keyed by the preference values in `package.json`.
|
||||
*
|
||||
* `undefined` here means unlimited, so the table cannot be indexed the way the
|
||||
* other two are: an absent key and an "unlimited" key would be indistinguishable.
|
||||
*/
|
||||
const TOOL_ROUNDS: Record<string, number | undefined> = {
|
||||
"4": 4,
|
||||
"8": 8,
|
||||
"16": 16,
|
||||
unlimited: undefined,
|
||||
};
|
||||
|
||||
/**
|
||||
* The tool-call round cap, where `undefined` means unlimited.
|
||||
*
|
||||
* Falls back to the default cap rather than to unlimited, unlike
|
||||
* {@link resolveResumeTimeout} and {@link resolveAutoDelete}, which both treat an
|
||||
* unrecognised value as "off". Those two are safe when they do nothing; this one
|
||||
* guards a loop, so the safe reading of a value this codebase does not recognise
|
||||
* is the cap and not the absence of one.
|
||||
*/
|
||||
export function resolveMaxToolRounds(value: string | undefined): number | undefined {
|
||||
const key = (value ?? "").trim().toLowerCase();
|
||||
return key in TOOL_ROUNDS ? TOOL_ROUNDS[key] : DEFAULT_MAX_TOOL_ROUNDS;
|
||||
}
|
||||
|
||||
/** Human-readable form of {@link ResolvedPreferences.autoDeleteMs}, for the setup screen. */
|
||||
export function describeAutoDelete(milliseconds: number | undefined): string {
|
||||
if (milliseconds === undefined) return "Disabled (conversations are kept)";
|
||||
|
||||
return `After ${describeSpan(milliseconds)}`;
|
||||
}
|
||||
|
||||
/** Human-readable form of {@link ResolvedPreferences.maxToolRounds}, for the setup screen. */
|
||||
export function describeMaxToolRounds(rounds: number | undefined): string {
|
||||
if (rounds === undefined) return "Unlimited (no cap on tool calls)";
|
||||
return `${rounds} round${rounds === 1 ? "" : "s"} per answer`;
|
||||
}
|
||||
|
||||
/** Human-readable form of {@link ResolvedPreferences.resumeTimeoutMs}, for the setup screen. */
|
||||
export function describeResumeTimeout(milliseconds: number | undefined): string {
|
||||
if (milliseconds === undefined) return "Never (always reopen the last chat)";
|
||||
|
||||
return `After ${describeSpan(milliseconds)}`;
|
||||
}
|
||||
|
||||
/** "15 minutes", "6 hours", "7 days". Shared so the two windows phrase themselves the same way. */
|
||||
function describeSpan(milliseconds: number): string {
|
||||
const minutes = Math.round(milliseconds / MINUTE);
|
||||
if (minutes < 60) return `After ${minutes} minutes`;
|
||||
if (minutes < 60) return `${minutes} minutes`;
|
||||
|
||||
const hours = Math.round(minutes / 60);
|
||||
if (hours < 24) return `After ${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
if (hours < 24) return `${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
|
||||
const days = Math.round(hours / 24);
|
||||
return `After ${days} day${days === 1 ? "" : "s"}`;
|
||||
return `${days} day${days === 1 ? "" : "s"}`;
|
||||
}
|
||||
|
||||
export function resolvePreferences(raw: LamPreferences): ResolvedPreferences {
|
||||
@@ -102,6 +184,9 @@ export function resolvePreferences(raw: LamPreferences): ResolvedPreferences {
|
||||
// Falls back to the manifest's declared default, so a missing preference
|
||||
// behaves the same whether Vicinae substitutes it or not.
|
||||
resumeTimeoutMs: resolveResumeTimeout(raw.resumeTimeout ?? "1h"),
|
||||
// Defaults to off, the one setting here that destroys data.
|
||||
autoDeleteMs: resolveAutoDelete(raw.autoDeleteAfter ?? "disabled"),
|
||||
maxToolRounds: resolveMaxToolRounds(raw.maxToolRounds ?? String(DEFAULT_MAX_TOOL_ROUNDS)),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user