Skip to content

Commit 6e99277

Browse files
jkyberneeesclaude
andcommitted
fix(telegram): seed system prompt into chat history (IDENTITY.md was ignored)
handleChatMessage ran the agent via RunWithMessages(cs.Messages), but RunWithMessages — unlike Run — does not inject the engine's system message, and the per-chat session was seeded with only the user message. So the system prompt (IDENTITY.md, the default, and the Telegram Quick Facts) was computed and set on the agent but never sent to the model: fresh chats reached the LLM with no system prompt and the agent answered as the provider's base identity (e.g. "I am Claude"). The run/continue commands already prepend the system message before RunWithMessages; Telegram did not. Add seedSystemMessage() and call it before appending the user turn: prepend the prompt for new/system-less histories, refresh messages[0] for resumed ones so IDENTITY.md / prompt changes take effect on the next turn. Covers all message types (text, voice, photo, commands) since they all funnel through handleChatMessage. Add a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent aaf6ff2 commit 6e99277

2 files changed

Lines changed: 69 additions & 0 deletions

File tree

cmd/odek/telegram.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -931,6 +931,24 @@ func spawnChild() error {
931931
return err
932932
}
933933

934+
// seedSystemMessage guarantees the system prompt is messages[0].
935+
//
936+
// RunWithMessages — unlike Run — does NOT inject the engine's system message,
937+
// so every caller that resumes a message history must seed it (the run/continue
938+
// commands do the same). Without this, a fresh Telegram chat reaches the model
939+
// with no system prompt at all and the agent answers as the provider's base
940+
// identity (e.g. "I am Claude") instead of from IDENTITY.md / the default.
941+
//
942+
// New or system-less histories get the prompt prepended; resumed histories get
943+
// messages[0] refreshed so IDENTITY.md / prompt changes take effect next turn.
944+
func seedSystemMessage(messages []llm.Message, system string) []llm.Message {
945+
if len(messages) == 0 || messages[0].Role != "system" {
946+
return append([]llm.Message{{Role: "system", Content: system}}, messages...)
947+
}
948+
messages[0].Content = system
949+
return messages
950+
}
951+
934952
// handleChatMessage processes a user message from Telegram in a background
935953
// goroutine. It creates or loads the chat session, creates a TelegramApprover
936954
// for approval prompts, runs the agent loop with RunWithMessages, and sends
@@ -996,6 +1014,9 @@ func handleChatMessage(
9961014
return
9971015
}
9981016

1017+
// Ensure the system prompt is messages[0] before the agent runs.
1018+
cs.Messages = seedSystemMessage(cs.Messages, systemMessage)
1019+
9991020
// Append user message to session.
10001021
cs.Messages = append(cs.Messages, llm.Message{Role: "user", Content: text})
10011022
cs.LastActive = time.Now()

cmd/odek/telegram_identity_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/BackendStack21/odek/internal/llm"
7+
)
8+
9+
// TestSeedSystemMessage locks in the fix for the bug where Telegram chats
10+
// reached the model with no system prompt (RunWithMessages does not inject it),
11+
// causing the agent to answer as the provider's base identity ("I am Claude")
12+
// instead of from IDENTITY.md / the default system prompt.
13+
func TestSeedSystemMessage(t *testing.T) {
14+
const sys = "You are Molty — AI Chief of Staff."
15+
16+
t.Run("new empty session prepends system", func(t *testing.T) {
17+
got := seedSystemMessage(nil, sys)
18+
if len(got) != 1 || got[0].Role != "system" || got[0].Content != sys {
19+
t.Fatalf("got %+v", got)
20+
}
21+
})
22+
23+
t.Run("user-first history prepends system and keeps the user message", func(t *testing.T) {
24+
got := seedSystemMessage([]llm.Message{{Role: "user", Content: "hi"}}, sys)
25+
if len(got) != 2 {
26+
t.Fatalf("want 2 messages, got %d: %+v", len(got), got)
27+
}
28+
if got[0].Role != "system" || got[0].Content != sys {
29+
t.Errorf("messages[0] = %+v, want system %q", got[0], sys)
30+
}
31+
if got[1].Role != "user" || got[1].Content != "hi" {
32+
t.Errorf("messages[1] = %+v, want user 'hi'", got[1])
33+
}
34+
})
35+
36+
t.Run("resumed history refreshes stale system without duplicating", func(t *testing.T) {
37+
got := seedSystemMessage([]llm.Message{
38+
{Role: "system", Content: "OLD PROMPT"},
39+
{Role: "user", Content: "hi"},
40+
}, sys)
41+
if len(got) != 2 {
42+
t.Fatalf("want 2 messages (no duplicate system), got %d: %+v", len(got), got)
43+
}
44+
if got[0].Role != "system" || got[0].Content != sys {
45+
t.Errorf("messages[0] = %+v, want refreshed system %q", got[0], sys)
46+
}
47+
})
48+
}

0 commit comments

Comments
 (0)