diff --git a/docs/clawd.md b/docs/clawd.md index 62fc97bcf..9b3b0df5f 100644 --- a/docs/clawd.md +++ b/docs/clawd.md @@ -145,7 +145,7 @@ Example: - Session files: `~/.clawdis/sessions/{{SessionId}}.jsonl` - Session metadata (token usage, last route, etc): `~/.clawdis/sessions/sessions.json` (legacy: `~/.clawdis/sessions.json`) -- `/new` starts a fresh session for that chat (configurable via `resetTriggers`) +- `/new` starts a fresh session for that chat (configurable via `resetTriggers`). If sent alone, the agent replies with a short hello to confirm the reset. ## Heartbeats (proactive mode) diff --git a/docs/session.md b/docs/session.md index 9279fa850..cbf228d14 100644 --- a/docs/session.md +++ b/docs/session.md @@ -26,7 +26,7 @@ All session state is **owned by the gateway** (the “master” Clawdis). UI cli ## Lifecyle - Idle expiry: `inbound.session.idleMinutes` (default 60). After the timeout a new `sessionId` is minted on the next message. -- Reset triggers: exact `/new` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. +- Reset triggers: exact `/new` (plus any extras in `resetTriggers`) start a fresh session id and pass the remainder of the message through. If `/new` is sent alone, Clawdis 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. ## Configuration (optional rename example) diff --git a/src/auto-reply/reply.triggers.test.ts b/src/auto-reply/reply.triggers.test.ts index 21f8ced3f..174984aa5 100644 --- a/src/auto-reply/reply.triggers.test.ts +++ b/src/auto-reply/reply.triggers.test.ts @@ -98,8 +98,16 @@ describe("trigger handling", () => { }); }); - it("acknowledges a bare /new without treating it as empty", async () => { + it("runs a greeting prompt for a bare /new", async () => { await withTempHome(async (home) => { + vi.mocked(runEmbeddedPiAgent).mockResolvedValue({ + payloads: [{ text: "hello" }], + meta: { + durationMs: 1, + agentMeta: { sessionId: "s", provider: "p", model: "m" }, + }, + }); + const res = await getReplyFromConfig( { Body: "/new", @@ -119,8 +127,11 @@ describe("trigger handling", () => { }, ); const text = Array.isArray(res) ? res[0]?.text : res?.text; - expect(text).toMatch(/fresh session/i); - expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); + expect(text).toBe("hello"); + expect(runEmbeddedPiAgent).toHaveBeenCalledOnce(); + const prompt = + vi.mocked(runEmbeddedPiAgent).mock.calls[0]?.[0]?.prompt ?? ""; + expect(prompt).toContain("A new session was started via /new"); }); }); diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index fd75da95d..7b230e2ca 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -47,6 +47,9 @@ const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]); const ABORT_MEMORY = new Map(); const SYSTEM_MARK = "⚙️"; +const BARE_SESSION_RESET_PROMPT = + "A new session was started via /new. Say hi briefly and ask what the user wants to do next."; + export function extractThinkDirective(body?: string): { cleaned: string; thinkLevel?: ThinkLevel; @@ -580,20 +583,18 @@ export async function getReplyFromConfig( })() : ""; const baseBody = sessionCtx.BodyStripped ?? sessionCtx.Body ?? ""; - const baseBodyTrimmed = baseBody.trim(); const rawBodyTrimmed = (ctx.Body ?? "").trim(); + const baseBodyTrimmedRaw = baseBody.trim(); const isBareSessionReset = - isNewSession && baseBodyTrimmed.length === 0 && rawBodyTrimmed.length > 0; + isNewSession && + baseBodyTrimmedRaw.length === 0 && + rawBodyTrimmed.length > 0; + const baseBodyFinal = isBareSessionReset ? BARE_SESSION_RESET_PROMPT : baseBody; + const baseBodyTrimmed = baseBodyFinal.trim(); // Bail early if the cleaned body is empty to avoid sending blank prompts to the agent. // This can happen if an inbound platform delivers an empty text message or we strip everything out. if (!baseBodyTrimmed) { await onReplyStart(); - if (isBareSessionReset) { - cleanupTyping(); - return { - text: "Started a fresh session. Send a new message to continue.", - }; - } logVerbose("Inbound body empty after normalization; skipping agent run"); cleanupTyping(); return { @@ -603,7 +604,7 @@ export async function getReplyFromConfig( const abortedHint = abortedLastRun ? "Note: The previous agent run was aborted by the user. Resume carefully or ask for clarification." : ""; - let prefixedBodyBase = baseBody; + let prefixedBodyBase = baseBodyFinal; if (groupIntro) { prefixedBodyBase = `${groupIntro}\n\n${prefixedBodyBase}`; }