fix: broaden prompt-echo guard and add heartbeat directive test

main
Peter Steinberger 2025-12-05 22:56:07 +00:00
parent 5b217b2042
commit d7a188fb34
3 changed files with 66 additions and 11 deletions

View File

@ -15,6 +15,7 @@
- Directive triggers (`/think`, `/verbose`, `/stop` et al.) now reply immediately using normalized bodies (timestamps/group prefixes stripped) without waiting for the agent.
- Batched history blocks no longer trip directive parsing; `/think` in prior messages won't emit stray acknowledgements.
- RPC fallbacks no longer echo the user's prompt (e.g., pasting a link) when the agent returns no assistant text.
- Heartbeat prompts with `/think` no longer send directive acks; heartbeat replies stay silent on settings.
## 1.4.1 — 2025-12-04

View File

@ -148,6 +148,47 @@ describe("runCommandReply (pi)", () => {
expect(payloads?.[0]?.text).not.toContain("hello");
});
it("does not echo the prompt even when the fallback text matches after stripping prefixes", async () => {
const rpcMock = mockPiRpc({
stdout: [
'{"type":"agent_start"}',
'{"type":"turn_start"}',
'{"type":"message_start","message":{"role":"user","content":[{"type":"text","text":"[Dec 5 22:52] https://example.com"}]}}',
'{"type":"message_end","message":{"role":"user","content":[{"type":"text","text":"[Dec 5 22:52] https://example.com"}]}}',
// No assistant content
'{"type":"agent_end"}',
].join("\n"),
stderr: "",
code: 0,
});
const { payloads } = await runCommandReply({
reply: {
mode: "command",
command: ["pi", "{{Body}}"],
agent: { kind: "pi" },
},
templatingCtx: {
...noopTemplateCtx,
Body: "[Dec 5 22:52] https://example.com",
BodyStripped: "[Dec 5 22:52] https://example.com",
},
sendSystemOnce: false,
isNewSession: true,
isFirstTurnInSession: true,
systemSent: false,
timeoutMs: 1000,
timeoutSeconds: 1,
commandRunner: vi.fn(),
enqueue: enqueueImmediate,
});
expect(rpcMock).toHaveBeenCalledOnce();
expect(payloads?.length).toBe(1);
expect(payloads?.[0]?.text).toMatch(/no output/i);
expect(payloads?.[0]?.text).not.toContain("example.com");
});
it("adds session args and --continue when resuming", async () => {
const rpcMock = mockPiRpc({
stdout:

View File

@ -22,6 +22,14 @@ import {
} from "./tool-meta.js";
import type { ReplyPayload } from "./types.js";
function stripStructuralPrefixes(text: string): string {
return text
.replace(/\[[^\]]+\]\s*/g, "")
.replace(/^[ \t]*[A-Za-z0-9+()\-_. ]+:\s*/gm, "")
.replace(/\s+/g, " ")
.trim();
}
function stripRpcNoise(raw: string): string {
// Drop rpc streaming scaffolding (toolcall deltas, audio buffer events) before parsing.
const lines = raw.split(/\n+/);
@ -776,10 +784,15 @@ export async function runCommandReply(
extractRpcAssistantText(trimmed) ??
extractAssistantTextLoosely(trimmed) ??
trimmed;
const normalize = (s?: string) =>
stripStructuralPrefixes((s ?? "").trim()).toLowerCase();
const bodyNorm = normalize(templatingCtx.Body ?? templatingCtx.BodyStripped);
const fallbackNorm = normalize(fallbackText);
const promptEcho =
fallbackText &&
(fallbackText === (templatingCtx.Body ?? "") ||
fallbackText === (templatingCtx.BodyStripped ?? ""));
fallbackText === (templatingCtx.BodyStripped ?? "") ||
(bodyNorm.length > 0 && bodyNorm === fallbackNorm));
const safeFallbackText = promptEcho ? undefined : fallbackText;
if (replyItems.length === 0 && safeFallbackText && !hasParsedContent) {