feat: improve /new model hints and reset confirmation

This commit is contained in:
Peter Steinberger
2026-01-20 14:28:53 +00:00
parent 41f6d06967
commit 15e5bb3459
16 changed files with 385 additions and 32 deletions

View File

@@ -60,7 +60,7 @@ the workspace is writable. See [Memory](/concepts/memory) and
- Idle reset (optional): `idleMinutes` adds a sliding idle window. When both daily and idle resets are configured, **whichever expires first** forces a new session.
- Legacy idle-only: if you set `session.idleMinutes` without any `session.reset`/`resetByType` config, Clawdbot stays in idle-only mode for backward compatibility.
- Per-type overrides (optional): `resetByType` lets you override the policy for `dm`, `group`, and `thread` sessions (thread = Slack/Discord threads, Telegram topics, Matrix threads when provided by the connector).
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
- Reset triggers: exact `/new` or `/reset` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. `/new <model>` accepts a model alias, `provider/model`, or provider name (fuzzy match) to set the new session model. If `/new` or `/reset` is sent alone, Clawdbot runs a short “hello” greeting turn to confirm the reset.
- Manual reset: delete specific keys from the store or remove the JSONL transcript; the next message recreates them.
- Isolated cron jobs always mint a fresh `sessionId` per run (no idle reuse).

View File

@@ -73,7 +73,7 @@ Text + native (when enabled):
- `/dock-slack` (alias: `/dock_slack`) (switch replies to Slack)
- `/activation mention|always` (groups only)
- `/send on|off|inherit` (owner-only)
- `/reset` or `/new`
- `/reset` or `/new [model]` (optional model hint; remainder is passed through)
- `/think <off|minimal|low|medium|high|xhigh>` (dynamic choices by model/provider; aliases: `/thinking`, `/t`)
- `/verbose on|full|off` (alias: `/v`)
- `/reasoning on|off|stream` (alias: `/reason`; when on, sends a separate message prefixed `Reasoning:`; `stream` = Telegram draft only)
@@ -91,6 +91,7 @@ Text-only:
Notes:
- Commands accept an optional `:` between the command and args (e.g. `/think: high`, `/send: on`, `/help:`).
- `/new <model>` accepts a model alias, `provider/model`, or a provider name (fuzzy match); if no match, the text is treated as the message body.
- For full provider usage breakdown, use `clawdbot status --usage`.
- `/usage` controls the per-response usage footer; `/usage cost` prints a local cost summary from Clawdbot session logs.
- `/restart` is disabled by default; set `commands.restart: true` to enable it.

View File

@@ -11,6 +11,7 @@ import type { CliBackendConfig } from "../../config/types.js";
import { runExec } from "../../process/exec.js";
import type { EmbeddedContextFile } from "../pi-embedded-helpers.js";
import { buildSystemPromptParams } from "../system-prompt-params.js";
import { resolveDefaultModelForAgent } from "../model-selection.js";
import { buildAgentSystemPrompt } from "../system-prompt.js";
const CLI_RUN_QUEUE = new Map<string, Promise<unknown>>();
@@ -174,6 +175,11 @@ export function buildSystemPrompt(params: {
modelDisplay: string;
agentId?: string;
}) {
const defaultModelRef = resolveDefaultModelForAgent({
cfg: params.config ?? {},
agentId: params.agentId,
});
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.config,
agentId: params.agentId,
@@ -183,6 +189,7 @@ export function buildSystemPrompt(params: {
arch: os.arch(),
node: process.version,
model: params.modelDisplay,
defaultModel: defaultModelLabel,
},
});
return buildAgentSystemPrompt({

View File

@@ -1,6 +1,8 @@
import type { ClawdbotConfig } from "../config/config.js";
import type { ModelCatalogEntry } from "./model-catalog.js";
import { normalizeGoogleModelId } from "./models-config.providers.js";
import { resolveAgentModelPrimary } from "./agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
export type ModelRef = {
provider: string;
@@ -141,6 +143,38 @@ export function resolveConfiguredModelRef(params: {
return { provider: params.defaultProvider, model: params.defaultModel };
}
export function resolveDefaultModelForAgent(params: {
cfg: ClawdbotConfig;
agentId?: string;
}): ModelRef {
const agentModelOverride = params.agentId
? resolveAgentModelPrimary(params.cfg, params.agentId)
: undefined;
const cfg =
agentModelOverride && agentModelOverride.length > 0
? {
...params.cfg,
agents: {
...params.cfg.agents,
defaults: {
...params.cfg.agents?.defaults,
model: {
...(typeof params.cfg.agents?.defaults?.model === "object"
? params.cfg.agents.defaults.model
: undefined),
primary: agentModelOverride,
},
},
},
}
: params.cfg;
return resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
});
}
export function buildAllowedModelSet(params: {
cfg: ClawdbotConfig;
catalog: ModelCatalogEntry[];

View File

@@ -43,6 +43,7 @@ import {
resolveSkillsPromptForRun,
} from "../../skills.js";
import { buildSystemPromptReport } from "../../system-prompt-report.js";
import { resolveDefaultModelForAgent } from "../../model-selection.js";
import { isAbortError } from "../abort.js";
import { buildEmbeddedExtensionPaths } from "../extensions.js";
@@ -212,6 +213,11 @@ export async function runEmbeddedAttempt(
})
: undefined;
const defaultModelRef = resolveDefaultModelForAgent({
cfg: params.config ?? {},
agentId: sessionAgentId,
});
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.config,
agentId: sessionAgentId,
@@ -221,6 +227,7 @@ export async function runEmbeddedAttempt(
arch: os.arch(),
node: process.version,
model: `${params.provider}/${params.modelId}`,
defaultModel: defaultModelLabel,
channel: runtimeChannel,
capabilities: runtimeCapabilities,
channelActions,

View File

@@ -13,6 +13,7 @@ export type RuntimeInfoInput = {
arch: string;
node: string;
model: string;
defaultModel?: string;
channel?: string;
capabilities?: string[];
/** Supported message actions for the current channel (e.g., react, edit, unsend) */

View File

@@ -288,6 +288,7 @@ describe("buildAgentSystemPrompt", () => {
arch: "arm64",
node: "v20",
model: "anthropic/claude",
defaultModel: "anthropic/claude-opus-4-5",
},
"telegram",
["inlineButtons"],
@@ -299,6 +300,7 @@ describe("buildAgentSystemPrompt", () => {
expect(line).toContain("os=macOS (arm64)");
expect(line).toContain("node=v20");
expect(line).toContain("model=anthropic/claude");
expect(line).toContain("default_model=anthropic/claude-opus-4-5");
expect(line).toContain("channel=telegram");
expect(line).toContain("capabilities=inlineButtons");
expect(line).toContain("thinking=low");

View File

@@ -582,6 +582,7 @@ export function buildRuntimeLine(
arch?: string;
node?: string;
model?: string;
defaultModel?: string;
},
runtimeChannel?: string,
runtimeCapabilities: string[] = [],
@@ -597,6 +598,7 @@ export function buildRuntimeLine(
: "",
runtimeInfo?.node ? `node=${runtimeInfo.node}` : "",
runtimeInfo?.model ? `model=${runtimeInfo.model}` : "",
runtimeInfo?.defaultModel ? `default_model=${runtimeInfo.defaultModel}` : "",
runtimeChannel ? `channel=${runtimeChannel}` : "",
runtimeChannel
? `capabilities=${runtimeCapabilities.length > 0 ? runtimeCapabilities.join(",") : "none"}`

View File

@@ -312,12 +312,14 @@ function buildChatCommands(): ChatCommandDefinition[] {
nativeName: "reset",
description: "Reset the current session.",
textAlias: "/reset",
acceptsArgs: true,
}),
defineChatCommand({
key: "new",
nativeName: "new",
description: "Start a new session.",
textAlias: "/new",
acceptsArgs: true,
}),
defineChatCommand({
key: "compact",

View File

@@ -7,6 +7,7 @@ import { getSkillsSnapshotVersion } from "../../agents/skills/refresh.js";
import { buildAgentSystemPrompt } from "../../agents/system-prompt.js";
import { buildSystemPromptReport } from "../../agents/system-prompt-report.js";
import { buildSystemPromptParams } from "../../agents/system-prompt-params.js";
import { resolveDefaultModelForAgent } from "../../agents/model-selection.js";
import { buildToolSummaryMap } from "../../agents/tool-summaries.js";
import { resolveBootstrapContextForRun } from "../../agents/bootstrap-files.js";
import type { SessionSystemPromptReport } from "../../config/sessions/types.js";
@@ -93,6 +94,11 @@ async function resolveContextReport(
sessionKey: params.sessionKey,
config: params.cfg,
});
const defaultModelRef = resolveDefaultModelForAgent({
cfg: params.cfg,
agentId: sessionAgentId,
});
const defaultModelLabel = `${defaultModelRef.provider}/${defaultModelRef.model}`;
const { runtimeInfo, userTimezone, userTime, userTimeFormat } = buildSystemPromptParams({
config: params.cfg,
agentId: sessionAgentId,
@@ -102,6 +108,7 @@ async function resolveContextReport(
arch: "unknown",
node: process.version,
model: `${params.provider}/${params.model}`,
defaultModel: defaultModelLabel,
},
});
const sandboxInfo = sandboxRuntime.sandboxed

View File

@@ -1,16 +1,15 @@
import {
resolveAgentDir,
resolveAgentModelPrimary,
resolveDefaultAgentId,
resolveSessionAgentId,
} from "../../agents/agent-scope.js";
import { lookupContextTokens } from "../../agents/context.js";
import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
import {
buildModelAliasIndex,
type ModelAliasIndex,
modelKey,
resolveConfiguredModelRef,
resolveDefaultModelForAgent,
resolveModelRefFromString,
} from "../../agents/model-selection.js";
import type { ClawdbotConfig } from "../../config/config.js";
@@ -239,36 +238,14 @@ export function resolveDefaultModel(params: { cfg: ClawdbotConfig; agentId?: str
defaultModel: string;
aliasIndex: ModelAliasIndex;
} {
const agentModelOverride = params.agentId
? resolveAgentModelPrimary(params.cfg, params.agentId)
: undefined;
const cfg =
agentModelOverride && agentModelOverride.length > 0
? {
...params.cfg,
agents: {
...params.cfg.agents,
defaults: {
...params.cfg.agents?.defaults,
model: {
...(typeof params.cfg.agents?.defaults?.model === "object"
? params.cfg.agents.defaults.model
: undefined),
primary: agentModelOverride,
},
},
},
}
: params.cfg;
const mainModel = resolveConfiguredModelRef({
cfg,
defaultProvider: DEFAULT_PROVIDER,
defaultModel: DEFAULT_MODEL,
const mainModel = resolveDefaultModelForAgent({
cfg: params.cfg,
agentId: params.agentId,
});
const defaultProvider = mainModel.provider;
const defaultModel = mainModel.model;
const aliasIndex = buildModelAliasIndex({
cfg,
cfg: params.cfg,
defaultProvider,
});
return { defaultProvider, defaultModel, aliasIndex };

View File

@@ -38,6 +38,7 @@ import { SILENT_REPLY_TOKEN } from "../tokens.js";
import type { GetReplyOptions, ReplyPayload } from "../types.js";
import { runReplyAgent } from "./agent-runner.js";
import { applySessionHints } from "./body.js";
import { routeReply } from "./route-reply.js";
import type { buildCommandContext } from "./commands.js";
import type { InlineDirectives } from "./directive-handling.js";
import { buildGroupIntro } from "./groups.js";
@@ -51,7 +52,7 @@ type AgentDefaults = NonNullable<ClawdbotConfig["agents"]>["defaults"];
type ExecOverrides = Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
const BARE_SESSION_RESET_PROMPT =
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
"A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. If the runtime model differs from default_model in the system prompt, mention the default model in the greeting. Do not mention internal steps, files, tools, or reasoning.";
type RunPreparedReplyParams = {
ctx: MsgContext;
@@ -92,9 +93,11 @@ type RunPreparedReplyParams = {
};
typing: TypingController;
opts?: GetReplyOptions;
defaultProvider: string;
defaultModel: string;
timeoutMs: number;
isNewSession: boolean;
resetTriggered: boolean;
systemSent: boolean;
sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>;
@@ -220,9 +223,11 @@ export async function runPreparedReply(
perMessageQueueOptions,
typing,
opts,
defaultProvider,
defaultModel,
timeoutMs,
isNewSession,
resetTriggered,
systemSent,
sessionKey,
sessionId,
@@ -369,6 +374,27 @@ export async function runPreparedReply(
}
}
}
if (resetTriggered && command.isAuthorizedSender) {
const channel = ctx.OriginatingChannel || (command.channel as any);
const to = ctx.OriginatingTo || command.from || command.to;
if (channel && to) {
const modelLabel = `${provider}/${model}`;
const defaultLabel = `${defaultProvider}/${defaultModel}`;
const text =
modelLabel === defaultLabel
? `✅ New session started · model: ${modelLabel}`
: `✅ New session started · model: ${modelLabel} (default: ${defaultLabel})`;
await routeReply({
payload: { text },
channel,
to,
sessionKey,
accountId: ctx.AccountId,
threadId: ctx.MessageThreadId,
cfg,
});
}
}
const sessionIdFinal = sessionId ?? crypto.randomUUID();
const sessionFile = resolveSessionFilePath(sessionIdFinal, sessionEntry);
const queueBodyBase = [threadStarterNote, baseBodyFinal].filter(Boolean).join("\n\n");

View File

@@ -19,6 +19,7 @@ import { handleInlineActions } from "./get-reply-inline-actions.js";
import { runPreparedReply } from "./get-reply-run.js";
import { finalizeInboundContext } from "./inbound-context.js";
import { initSessionState } from "./session.js";
import { applyResetModelOverride } from "./session-reset-model.js";
import { stageSandboxMedia } from "./stage-sandbox-media.js";
import { createTypingController } from "./typing.js";
@@ -103,6 +104,7 @@ export async function getReplyFromConfig(
sessionKey,
sessionId,
isNewSession,
resetTriggered,
systemSent,
abortedLastRun,
storePath,
@@ -110,8 +112,24 @@ export async function getReplyFromConfig(
groupResolution,
isGroup,
triggerBodyNormalized,
bodyStripped,
} = sessionState;
await applyResetModelOverride({
cfg,
resetTriggered,
bodyStripped,
sessionCtx,
ctx: finalized,
sessionEntry,
sessionStore,
sessionKey,
storePath,
defaultProvider,
defaultModel,
aliasIndex,
});
const directiveResult = await resolveReplyDirectives({
ctx: finalized,
cfg,
@@ -256,9 +274,11 @@ export async function getReplyFromConfig(
perMessageQueueOptions,
typing,
opts,
defaultProvider,
defaultModel,
timeoutMs,
isNewSession,
resetTriggered,
systemSent,
sessionEntry,
sessionStore,

View File

@@ -0,0 +1,74 @@
import { describe, expect, it, vi } from "vitest";
import type { ClawdbotConfig } from "../../config/config.js";
import { buildModelAliasIndex } from "../../agents/model-selection.js";
import { applyResetModelOverride } from "./session-reset-model.js";
vi.mock("../../agents/model-catalog.js", () => ({
loadModelCatalog: vi.fn(async () => [
{ provider: "minimax", id: "m2.1", name: "M2.1" },
{ provider: "openai", id: "gpt-4o-mini", name: "GPT-4o mini" },
]),
}));
describe("applyResetModelOverride", () => {
it("selects a model hint and strips it from the body", async () => {
const cfg = {} as ClawdbotConfig;
const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" });
const sessionEntry = {
sessionId: "s1",
updatedAt: Date.now(),
};
const sessionStore = { "agent:main:dm:1": sessionEntry };
const sessionCtx = { BodyStripped: "minimax summarize" };
const ctx = { ChatType: "direct" };
await applyResetModelOverride({
cfg,
resetTriggered: true,
bodyStripped: "minimax summarize",
sessionCtx,
ctx,
sessionEntry,
sessionStore,
sessionKey: "agent:main:dm:1",
defaultProvider: "openai",
defaultModel: "gpt-4o-mini",
aliasIndex,
});
expect(sessionEntry.providerOverride).toBe("minimax");
expect(sessionEntry.modelOverride).toBe("m2.1");
expect(sessionCtx.BodyStripped).toBe("summarize");
});
it("skips when resetTriggered is false", async () => {
const cfg = {} as ClawdbotConfig;
const aliasIndex = buildModelAliasIndex({ cfg, defaultProvider: "openai" });
const sessionEntry = {
sessionId: "s1",
updatedAt: Date.now(),
};
const sessionStore = { "agent:main:dm:1": sessionEntry };
const sessionCtx = { BodyStripped: "minimax summarize" };
const ctx = { ChatType: "direct" };
await applyResetModelOverride({
cfg,
resetTriggered: false,
bodyStripped: "minimax summarize",
sessionCtx,
ctx,
sessionEntry,
sessionStore,
sessionKey: "agent:main:dm:1",
defaultProvider: "openai",
defaultModel: "gpt-4o-mini",
aliasIndex,
});
expect(sessionEntry.providerOverride).toBeUndefined();
expect(sessionEntry.modelOverride).toBeUndefined();
expect(sessionCtx.BodyStripped).toBe("minimax summarize");
});
});

View File

@@ -0,0 +1,191 @@
import { loadModelCatalog } from "../../agents/model-catalog.js";
import {
buildAllowedModelSet,
modelKey,
normalizeProviderId,
resolveModelRefFromString,
type ModelAliasIndex,
} from "../../agents/model-selection.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { SessionEntry } from "../../config/sessions.js";
import { updateSessionStore } from "../../config/sessions.js";
import type { MsgContext, TemplateContext } from "../templating.js";
import { formatInboundBodyWithSenderMeta } from "./inbound-sender-meta.js";
import { resolveModelDirectiveSelection, type ModelDirectiveSelection } from "./model-selection.js";
type ResetModelResult = {
selection?: ModelDirectiveSelection;
cleanedBody?: string;
};
function splitBody(body: string) {
const tokens = body.split(/\s+/).filter(Boolean);
return {
tokens,
first: tokens[0],
second: tokens[1],
rest: tokens.slice(2),
};
}
function buildSelectionFromExplicit(params: {
raw: string;
defaultProvider: string;
defaultModel: string;
aliasIndex: ModelAliasIndex;
allowedModelKeys: Set<string>;
}): ModelDirectiveSelection | undefined {
const resolved = resolveModelRefFromString({
raw: params.raw,
defaultProvider: params.defaultProvider,
aliasIndex: params.aliasIndex,
});
if (!resolved) return undefined;
const key = modelKey(resolved.ref.provider, resolved.ref.model);
if (params.allowedModelKeys.size > 0 && !params.allowedModelKeys.has(key)) return undefined;
const isDefault =
resolved.ref.provider === params.defaultProvider && resolved.ref.model === params.defaultModel;
return {
provider: resolved.ref.provider,
model: resolved.ref.model,
isDefault,
...(resolved.alias ? { alias: resolved.alias } : undefined),
};
}
function applySelectionToSession(params: {
selection: ModelDirectiveSelection;
sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>;
sessionKey?: string;
storePath?: string;
}) {
const { selection, sessionEntry, sessionStore, sessionKey, storePath } = params;
if (!sessionEntry || !sessionStore || !sessionKey) return;
let updated = false;
if (selection.isDefault) {
if (sessionEntry.providerOverride || sessionEntry.modelOverride) {
delete sessionEntry.providerOverride;
delete sessionEntry.modelOverride;
updated = true;
}
} else {
if (sessionEntry.providerOverride !== selection.provider) {
sessionEntry.providerOverride = selection.provider;
updated = true;
}
if (sessionEntry.modelOverride !== selection.model) {
sessionEntry.modelOverride = selection.model;
updated = true;
}
}
if (!updated) return;
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
if (storePath) {
updateSessionStore(storePath, (store) => {
store[sessionKey] = sessionEntry;
}).catch(() => {
// Ignore persistence errors; session still proceeds.
});
}
}
export async function applyResetModelOverride(params: {
cfg: ClawdbotConfig;
resetTriggered: boolean;
bodyStripped?: string;
sessionCtx: TemplateContext;
ctx: MsgContext;
sessionEntry?: SessionEntry;
sessionStore?: Record<string, SessionEntry>;
sessionKey?: string;
storePath?: string;
defaultProvider: string;
defaultModel: string;
aliasIndex: ModelAliasIndex;
}): Promise<ResetModelResult> {
if (!params.resetTriggered) return {};
const rawBody = params.bodyStripped?.trim();
if (!rawBody) return {};
const { tokens, first, second } = splitBody(rawBody);
if (!first) return {};
const catalog = await loadModelCatalog({ config: params.cfg });
const allowed = buildAllowedModelSet({
cfg: params.cfg,
catalog,
defaultProvider: params.defaultProvider,
defaultModel: params.defaultModel,
});
const allowedModelKeys = allowed.allowedKeys;
if (allowedModelKeys.size === 0) return {};
const providers = new Set<string>();
for (const key of allowedModelKeys) {
const slash = key.indexOf("/");
if (slash <= 0) continue;
providers.add(normalizeProviderId(key.slice(0, slash)));
}
const resolveSelection = (raw: string) =>
resolveModelDirectiveSelection({
raw,
defaultProvider: params.defaultProvider,
defaultModel: params.defaultModel,
aliasIndex: params.aliasIndex,
allowedModelKeys,
});
let selection: ModelDirectiveSelection | undefined;
let consumed = 0;
if (providers.has(normalizeProviderId(first)) && second) {
const composite = `${normalizeProviderId(first)}/${second}`;
const resolved = resolveSelection(composite);
if (resolved.selection) {
selection = resolved.selection;
consumed = 2;
}
}
if (!selection) {
selection = buildSelectionFromExplicit({
raw: first,
defaultProvider: params.defaultProvider,
defaultModel: params.defaultModel,
aliasIndex: params.aliasIndex,
allowedModelKeys,
});
if (selection) consumed = 1;
}
if (!selection) {
const resolved = resolveSelection(first);
const allowFuzzy = providers.has(normalizeProviderId(first)) || first.trim().length >= 6;
if (allowFuzzy) {
selection = resolved.selection;
if (selection) consumed = 1;
}
}
if (!selection) return {};
const cleanedBody = tokens.slice(consumed).join(" ").trim();
params.sessionCtx.BodyStripped = formatInboundBodyWithSenderMeta({
ctx: params.ctx,
body: cleanedBody,
});
params.sessionCtx.BodyForCommands = cleanedBody;
applySelectionToSession({
selection,
sessionEntry: params.sessionEntry,
sessionStore: params.sessionStore,
sessionKey: params.sessionKey,
storePath: params.storePath,
});
return { selection, cleanedBody };
}

View File

@@ -40,6 +40,7 @@ export type SessionInitResult = {
sessionKey: string;
sessionId: string;
isNewSession: boolean;
resetTriggered: boolean;
systemSent: boolean;
abortedLastRun: boolean;
storePath: string;
@@ -327,6 +328,7 @@ export async function initSessionState(params: {
sessionKey,
sessionId: sessionId ?? crypto.randomUUID(),
isNewSession,
resetTriggered,
systemSent,
abortedLastRun,
storePath,