compaction: the leading system message is request chrome, never cut (fix the no-tools request)

Owner report (OpenRouter logs, 2026-10-05): the post-compaction request
arrived with NO system prompt and NO tool definitions — everything
before the cut replaced by the compaction message.

Root cause: pi-agent-core's Agent seeds a SystemMessage carrying the
system prompt AND the tool declarations at state.messages[0]
(createInitialSystemMessage — 'messages declare the provider-visible
loadout'). The record never persists it (ui-stream's settleWith filters
the role), but convertToLlm DOES receive it — and the cut projection
sliced from firstKeptMessageId, dropping [0] from the wire: no prompt,
no tools.

Fix (pi's own law — verified against pi-agent-core + pi's coding-agent):
the projection now carries the leading system message through AHEAD of
the compaction head, exactly as pi's prepareRequest re-applies
context.systemPrompt + context.tools around its compaction projection.
The cut slices the RECORD's history; the chrome is never compactable
payload. Loop start skips [0] when chrome is present (no duplicate);
record-path callers are unchanged.

Pinned: chrome-first-through-cut, exactly-once without a cut,
record-path head-first.
This commit is contained in:
amosblomqvist
2026-10-05 23:52:19 +03:00
parent ffc4df1938
commit 6dcad55175
3 changed files with 82 additions and 3 deletions
+17 -2
View File
@@ -491,8 +491,23 @@ export function toLlmMessages(messages: readonly AgentMessage[]): Message[] {
};
}
}
const out: Message[] = head ? [head] : [];
for (let i = start; i < messages.length; i++) {
// THE LEADING SYSTEM MESSAGE IS REQUEST CHROME, NEVER COMPACTABLE
// PAYLOAD (the OpenRouter no-tools incident, owner 2026-10-05):
// pi-agent-core's Agent seeds a SystemMessage carrying the system
// prompt AND the tool declarations at state.messages[0]
// (createInitialSystemMessage — "messages declare the provider-visible
// loadout"). The record never persists it (ui-stream's settleWith
// filters the role), so RECORD-path callers never see it — but the
// Agent's convertToLlm DOES. The cut slices the RECORD's history;
// when [0] is the chrome it must carry through AHEAD of the
// compaction head, exactly as pi's own prepareRequest re-applies
// context.systemPrompt + context.tools around its projection.
const leading =
(messages[0] as { role?: string } | undefined)?.role === "system" ? (messages[0] as Message) : null;
const out: Message[] = [];
if (leading) out.push(leading);
if (head) out.push(head);
for (let i = leading ? Math.max(start, 1) : start; i < messages.length; i++) {
const m = messages[i]!;
const role = (m as { role?: string }).role;
if (role === "custom") {