Skip to content

fix: synthetic suffix messages break title generation (#15)#34

Merged
ranxianglei merged 3 commits into
masterfrom
2026-06-29_synthetic-suffix-fixes-title-gen
Jun 28, 2026
Merged

fix: synthetic suffix messages break title generation (#15)#34
ranxianglei merged 3 commits into
masterfrom
2026-06-29_synthetic-suffix-fixes-title-gen

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Problem

opencode-acp prevents OpenCode's built-in session title generation. Sessions stay on New session - <timestamp> forever. Confirmed still reproducing on 1.5.0 (thanks @kratky-pavel for the detailed repro + logs in #15).

Root cause

This is a different path from the isInternalAgentRequest guard added in #16 (which only handles an internal-agent request flowing through messages.transform).

OpenCode's SessionPrompt.ensureTitle (prompt.ts:182) requires exactly one real user message, else it bails (title gen never scheduled):

const real = (m) => m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
if (input.history.filter(real).length !== 1) return

ACP's main-chat message-transform pipeline injects a "Compressed block context…" nudge as a second, standalone user message via createSuffixMessagecreateSyntheticUserMessage (lib/messages/inject/inject.ts:54). That function is named createSyntheticUserMessage but never set the synthetic flag on its text part — so ensureTitle counted the nudge as a 2nd real user message → filter(real).length === 2 → precondition failed → title generation was never scheduled (no agent=title / small=true stream appears, because it never starts).

Additionally, title({ history: msgs }) is Effect.forkIn (async), sharing the same msgs array reference that messages.transform (prompt.ts:1574) mutates — a race where the forked title fiber observes the injected 2nd message and bails.

Fix

lib/messages/utils.tscreateSyntheticUserMessage now marks its text part synthetic: true (matching its name and OpenCode's own convention). One-line, targeted; benefits all callers (the compress nudge via createSuffixMessage, and prune.ts compression summaries).

Verified safe against OpenCode source:

  • ensureTitle excludes all-synthetic user messages → exactly one real user message → title gen runs.
  • MessageV2.toModelMessagesEffect (message-v2.ts:802) includes synthetic text parts (checks only type === "text" && !ignored, no synthetic check) → nudge text still delivered to the LLM.
  • TUI already hides synthetic parts → no UI noise.
  • ACP's own isSyntheticMessage uses an ID-prefix check (msg_dcp_summary_), unaffected by the synthetic field.

Verification

  • npm run typecheck — PASS
  • npm test — PASS 487/487 (+1 new contract test replicating ensureTitle's real filter), 0 fail
  • npm run build — PASS (325.13 KB)

Devlog: devlog/2026-06-29_synthetic-suffix-fixes-title-gen/ (REQ.md + WORKLOG.md).

Refs #15.

…neration

ACP's message-transform pipeline injects a 'Compressed block context' nudge as a
second, standalone user message (createSuffixMessage -> createSyntheticUserMessage).
That message's text part lacked the synthetic flag, so OpenCode's
SessionPrompt.ensureTitle counted it as a real user message -> its
'filter(real).length !== 1' precondition failed -> title generation was never
scheduled (sessions stuck on 'New session - <timestamp>').

createSyntheticUserMessage now marks its text part synthetic: true (matching its
name and OpenCode's own synthetic-part convention). ensureTitle excludes
all-synthetic user messages, while toModelMessagesEffect still includes synthetic
text parts in the LLM call, so the nudge text is still delivered.

Refs: GitHub #15 (ranxianglei/opencode-acp)
@ranxianglei ranxianglei merged commit 31be741 into master Jun 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant