feat(auto-reply): greet on bare /new

This commit is contained in:
Peter Steinberger
2025-12-20 13:04:55 +00:00
parent 6e200ed1c0
commit c2c5b28c70
4 changed files with 26 additions and 14 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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");
});
});

View File

@@ -47,6 +47,9 @@ const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]);
const ABORT_MEMORY = new Map<string, boolean>();
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}`;
}