fix: preprompt would show after loading session#8744
Conversation
When the frontend sends a message with persona-instructions wrapping a user-message, the backend now splits it into two persisted messages: a hidden context message (user_visible: false) and a visible user message. The agent still receives the full combined content. On session replay, the hidden context message is filtered out, preventing raw XML tags from being shown to the user. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of the frontend wrapping persona/project context and user message in XML tags and the backend parsing them apart, the system prompt is now sent as a separate field in the ACP _meta object. The backend reads it directly from _meta.systemPrompt, creating hidden and visible messages without any XML parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the custom _meta.systemPrompt extension with ACP's standard
annotations/audience mechanism. The frontend sends the system prompt as
a text content block with annotations: { audience: ["assistant"] }. The
backend preserves annotations through convert_acp_prompt_to_message and
session replay. The frontend filters out assistant-only blocks in
MessageBubble, preventing raw system prompt text from being shown to
the user.
This eliminates the _meta.systemPrompt field, backend message splitting
(hidden + visible), and the combined agent_message construction.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Short-circuit annotation filtering in MessageBubble for non-user messages to avoid allocating a new array on every assistant render. - Extract filterUserVisibleContent helper and merge duplicate thinking/reasoning switch cases to stay within the file-size limit. - Remove unused meta parameter from acpApi.prompt() (dead code since system prompt now uses annotations instead of _meta.systemPrompt). - Add runtime typeof guard for annotations extraction in acpNotificationHandler instead of a bare 'as' cast, so malformed wire data won't silently produce an unexpected shape. Signed-off-by: Matt Toohey <contact@matttoohey.com>
- Restore sanitize_unicode_tags() for unannotated text blocks in
convert_acp_prompt_to_message. The previous refactor bypassed
sanitization for all text by constructing RawTextContent directly
instead of calling with_text(). Now only annotated blocks (which
originate from the frontend, not user input) skip sanitization.
- Skip with_audience() when the resolved audience vec is empty.
Previously, annotations with no audience field produced
Some(vec![]) via unwrap_or_default(), which downstream
filter_for_audience treats as "visible to nobody" — silently
dropping the block. Now an empty audience is treated as
"no restriction" (no_annotation).
- Extract shared Audience type alias in messages.ts and use it in
TextContent and acpNotificationHandler.ts via TextContent["annotations"],
replacing inline ("user" | "assistant")[] literals so the type stays
in sync across files.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Remove the type === "text" guard in filterUserVisibleContent so the audience filter applies to any content block that carries annotations, not just text blocks. This future-proofs the filter against other block types (image, toolRequest, etc.) gaining audience annotations later. Uses "annotations" in b to detect the field generically instead of narrowing on the discriminated union variant. Signed-off-by: Matt Toohey <contact@matttoohey.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 92d86d004d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Remove unnecessary `as ContentBlock` cast in acp.ts since the SDK's
ContentBlock union already includes TextContent with annotations.
- Extract shared ContentAnnotations interface in messages.ts so the
annotation shape is reusable across content block types.
- Use ContentAnnotations in filterUserVisibleContent instead of an
inline `{ audience?: string[] }` cast for type safety.
- Return null for user messages where all content blocks were filtered
out (assistant-only system prompt with no visible user text), avoiding
empty bubbles. Scoped to user role so streaming assistant messages
with initially empty content still render.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
- Add optional annotations?: ContentAnnotations to all MessageContent
variant interfaces (ImageContent, ToolRequestContent,
ToolResponseContent, ThinkingContent, RedactedThinkingContent,
ReasoningContent, ActionRequiredContent, SystemNotificationContent).
TextContent already had it.
- Remove the 'as { annotations?: ContentAnnotations }' cast and
'annotations in b' runtime check in filterUserVisibleContent — the
compiler now verifies the .annotations access directly on the
MessageContent union.
- Remove unused ContentAnnotations import from MessageBubble.tsx.
- Restore blank line between textContent derivation and the
if (role === 'system') early-return for readability.
Signed-off-by: Matt Toohey <contact@matttoohey.com>
User messages are never streamed token-by-token — each replay chunk is already a complete content block. The previous coalescing logic merged consecutive text chunks, which caused annotated (assistant-only system prompt) and unannotated (user text) blocks to combine. The audience filter then hid the entire merged block, making the user's message disappear after session reload. Always push a new content block for user_message_chunk instead of appending to the previous one. Coalescing remains for agent_message_chunk where token-by-token streaming requires reassembly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b444a3e588
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Annotated text blocks bypassed sanitize_unicode_tags because the annotated branch constructed RawTextContent directly from text.text instead of calling with_text(). This meant invisible Unicode Tag characters in assistant-only system prompt blocks could be persisted and sent to the model while being hidden from the user, undermining the existing prompt-injection mitigation. Apply sanitize_unicode_tags to all text blocks regardless of annotation presence. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Alphabetize grouped imports in use statements to satisfy cargo fmt --check. Affects top-level imports and two function-scoped imports. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 01bfa8881b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Previously, assistant-only system prompt blocks (audience: ["assistant"]) were pushed into chat state during replay and only filtered out in MessageBubble at render time. Other consumers reading raw message.content (e.g. getTextContent in MessageTimeline) could still see the persona/ project instructions, affecting search and scroll behavior. Filter out blocks whose audience does not include "user" at the replay ingestion point, so they never enter chat state at all. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add length > 0 guard so that an empty audience array (audience: []) is treated as "no restriction" rather than "visible to nobody". Applied to both the replay ingestion filter in acpNotificationHandler and the render-time filterUserVisibleContent in MessageBubble. The backend already avoids emitting empty audience arrays, but this hardens the frontend against unexpected wire data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c76c5e1de0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
* main: fix: preprompt would show after loading session (#8744) commands to acp+ migration: extensions management (#8733) feat: desktop notification when goose finishes a task (#8647) harden code review skill for async state and default-resolution bugs (#8740) Feature/at agent mention (#8571) fix: removed hardcoded dependency of goose-acp-macro (#8753) perf: split agent setup into staged phases to reduce startup blocking (#8746) Add /skills command (#8600) Replace deprecated Claude ACP package links (#8625)
* main: (34 commits) fix(goose-server): cache TLS cert to disk to avoid slow startup on first launch (#8174) feat: add Exa AI-powered search tool (#8487) fix: preprompt would show after loading session (#8744) commands to acp+ migration: extensions management (#8733) feat: desktop notification when goose finishes a task (#8647) harden code review skill for async state and default-resolution bugs (#8740) Feature/at agent mention (#8571) fix: removed hardcoded dependency of goose-acp-macro (#8753) perf: split agent setup into staged phases to reduce startup blocking (#8746) Add /skills command (#8600) Replace deprecated Claude ACP package links (#8625) removed the specific code owner for documentation change (#8749) fix(providers): handle missing delta field in streaming chunks (#8700) refactor(providers): extract http_status module and rename handle_status_openai_compat (#8620) fix(providers/openai): accept streaming chunks with both reasoning fields (#8715) feat: associate threads with projects (#8745) upgrade goose sdk and tui to be compatible with the latest agentclientprotocol/sdk package (#8667) feat: extend goose2 context window ux with auto-compaction (#8721) improve goose2 agent management flows (#8737) alexhancock/tui-improvements (#8736) ...
Previously after loading a session:

When loading sessions we would show the preprompt text we added before starting the session.
We now separate the two into two text content blocks. The preprompt now has audience=[assistant], which can then filter out from the UI.
Summary
audience: ["assistant"]) to properly hide persona/project instructions from the user-visible message streamMessageBubblethinking/reasoningrendering branches inMessageBubbleTest plan