Skip to content

fix(desktop): send quote-only and attachment-only messages - #4805

Open
Jarad-z wants to merge 1 commit into
apache:mainfrom
Jarad-z:codex/structured-only-messages
Open

fix(desktop): send quote-only and attachment-only messages#4805
Jarad-z wants to merge 1 commit into
apache:mainfrom
Jarad-z:codex/structured-only-messages

Conversation

@Jarad-z

@Jarad-z Jarad-z commented Sep 4, 2026

Copy link
Copy Markdown

Summary

Maka Desktop treated MessageContent.text as the only substantive input at several boundaries even though quotes and attachments are model-visible content. A quote-only send could be blocked in the composer or rejected during admission; after reaching the runtime, the durable replay predicate classified it as empty and failed the turn before the provider request. Attachment-only sends hit the same text-only admission assumption.

This change:

  • makes staged quotes and attachments sufficient to enable and submit the composer;
  • admits structured-only content through Desktop IPC, Runtime Host protocol, and root-turn persistence;
  • preserves quote and attachment content through durable RuntimeEvent replay;
  • suppresses the filled user bubble when there is no text body while retaining message metadata;
  • keeps truly empty messages invalid.

Fixes #4804

Verification

Claim Command Result
Desktop production build and workspace typechecks npm run build:with-deps from apps/desktop passed
Composer and transcript regressions node --test packages/ui/dist/__tests__/composer-send-toggle.test.js packages/ui/dist/__tests__/chat-turn-answer-identity.test.js 18 passed, 0 failed
Desktop IPC admission node --test apps/desktop/dist/main/__tests__/permission-response-ipc-boundary.test.js 6 passed, 0 failed
Core visibility and root admission node --test packages/core/dist/__tests__/runtime-event.test.js packages/storage/dist/__tests__/root-turn-admission-normalization.test.js 29 passed, 0 failed
Runtime Host wire boundary node --test packages/runtime-host/dist/__tests__/protocol.test.js 77 passed, 0 failed
Durable model replay node --test packages/runtime/dist/__tests__/ai-sdk-backend.test.js packages/runtime/dist/__tests__/directory-reference-model-context.test.js packages/runtime/dist/__tests__/runtime-event-read-model.test.js passed
Desktop quote-only journey npx playwright test e2e/send-message.spec.ts --grep "reference-sized paste" --config e2e/playwright.config.ts from apps/desktop 1 passed
Lint npm run lint 3,278 files clean
Changed-file format and whitespace npx biome check on the 16 changed files; git diff --check passed

Not run: the full Electron E2E suite. The focused user journey and all affected lower-tier suites passed.

Full-tree npm run format:check and npm run check:asf-headers are not claimed: the checkout contains unrelated untracked artifacts and Windows-line-ending baseline files outside this PR that fail those whole-tree commands. All changed files pass the targeted formatter check and retain their ASF headers.

Root cause

runtimeEventHasModelVisibleContent returned false for every text event whose text field was empty. The model-history formatter separately folds quotes, attachments, and directory references into provider-visible text, so the visibility predicate and the materializer disagreed. The durable current-turn replay therefore emitted empty_text_skipped, materialized no current user message, and failed closed as a non-replayable projection. The UI and admission boundaries repeated the same text-only predicate.

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex — reproduced the Desktop failures, traced the persisted RuntimeEvent and replay diagnostics, implemented the UI/admission/replay fixes, added regression coverage, ran the verification above, and drafted the issue and PR descriptions. The commit carries a Generated-by: OpenAI Codex trailer.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@Jarad-z
Jarad-z force-pushed the codex/structured-only-messages branch from c1036d8 to 0311ad1 Compare September 4, 2026 18:11
@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 4, 2026
@likun666661

Copy link
Copy Markdown
Member

Thanks for tracing this across the full Desktop admission and durable replay path. The main direct-send path now has the right semantic model: non-empty inline text is not the only form of model-visible user input.

I found three remaining gaps before this is fully converged:

  1. History compaction still classifies quote-only and attachment-only events as empty.

    runtimeEventHasModelVisibleContent() now admits quotes, attachments, and directory references, but estimateRuntimeEventChars() still counts only content.text.length for text events, and isHistoryCompactContentEvent() requires that estimate to be greater than zero.

    As a result, structured-only events are filtered out before compaction planning/replay (packages/runtime/src/history-compaction.ts:443-444,493-495). They are neither included in the checkpoint summary nor retained in the successor tail, so a successfully sent quote-only/attachment-only historical message can disappear from model context after /compact or automatic compaction. A structured-only current head anchor also cannot initialize mid-turn capacity compaction (packages/runtime/src/ai-sdk-compaction.ts:796-805).

    The compactable predicate should use the same model-visible-content semantics, and the size estimate should account for the effective formatTextWithInlineRefs(content) projection. A regression test should compact and replay a structured-only turn.

  2. Quote Companion exposes an enabled Send button that still rejects the submission.

    The shared Composer now enables Send when pendingQuotes or pendingAttachments is present, and QuoteCompanionPanel supplies both. However, useQuoteCompanion.send() returns false on !text.trim() before snapshotting the quote or processing attachments (apps/desktop/src/renderer/features/workbar/tools/side-chat/use-quote-companion.ts:849-866). During streaming, dispatchQuoteCompanionInput() similarly routes only the empty text to steer() and does not carry the structured input.

    This leaves quote-only/attachment-only sends as a visible no-op in that Desktop surface. The consumer needs to adopt the same submission predicate, with a side-chat integration test.

  3. Structured-only follow-ups render as blank queue rows.

    The new streaming send affordance allows an empty-text structured message to enter the follow-up queue, but ComposerMessageQueue labels a row only with displayText ?? text (packages/ui/src/composer-message-queue.tsx:166-170). Quote-only and attachment-only entries therefore have no visible label. A quote label, attachment name, or localized fallback is needed so queued entries remain distinguishable.

From an Occam razor perspective, the problem statement is already minimal: message validity must be based on model-visible content, not only authored text. The production-file breadth is justified because the message crosses several trust boundaries. The implementation is not fully converged yet, though, because Composer, IPC, protocol/storage, replay, compaction, and side chat still carry different definitions of substantive content. A canonical MessageContent semantic helper, plus boundary-specific normalization where raw attachments/skills have not yet become MessageContent, would be simpler and less drift-prone than continuing to add local condition lists.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 0311ad1cfe906fc7765632f1feb54ad011e234d9 (OPEN). Two P1s plus three P3s below — and neither P1 is a defect in the code itself; both are about this PR's relationship to the repo's current state.

P1 — the green checks are stale; the epoch gate fails on current main

The CI invocation (.github/workflows/ci.yml:100) runs node scripts/protocol-epoch-check.mjs --base 'HEAD^1' on GitHub's merge ref — whose base 3fdaddb4 is an old main. Re-running the same command on the current merge ref fails with exit 1: protocol files changed (protocol/turn.ts) with no compatible-change declaration, epoch still 112 — while current origin/main is already at 113 (protocol/index.ts:104, latest dd7d1d595). So this PR now needs ≥ 114, not 113. The moment CI re-runs or the PR rebases, the check goes red; the current green cannot be relied on for merge. Note: this is the #3313 hole (sibling branches bumping the same number merge without git conflict) firing in practice, not in theory.

P1 — same change as still-open PR #4815, in the same files; the two will collide

PR #4815 (OPEN, unmerged) admits "empty text with quote or attachment" at the admission decoder decodeMessageAdmissionContent; this PR does the same thing one layer down, in the shared decodeMessageContent via hasStructuredContent relaxing requireUtf8String. Both touch protocol/turn.ts and runtime-event.ts's runtimeEventHasModelVisibleContent — textual conflict and semantic duplication. Technical comparison: this PR's placement is better — the earlier review of #4815 flagged a P1 where admission was widened but read-back was not, so an admitted structured message would break the whole queue snapshot frame; because this PR changes the shared decoder, the read-back paths (queue snapshot in message.ts, steering events in session-continuity.ts) widen automatically and that P1 does not exist here. But the two must be merged into one or one explicitly closed — not merged independently.

P3s (non-blocking)

  • Three "what counts as content" predicates now disagree on directoryReferences: runtime-event.ts counts it, turn.ts's hasStructuredContent and agent-run-store.ts's hasModelContent do not. Currently unreachable (the composer gate only looks at quotes/attachments), but three copies of one predicate have started drifting — either all three should recognize it (if the formatTextWithInlineRefs claim holds) or runtime-event.ts should not.
  • Empty-text messages lose the bubble container (chat-turn.tsx): content (quotes/attachments render separately) is not lost, but the user-message visual container is. Worth the author confirming it is deliberate, with one line of comment either way.
  • While streaming with a staged quote, Stop becomes Send (stopShown gains !hasPendingQuote && !hasPendingAttachment). A defensible consequence of "quotes are sendable", with Esc as the documented fallback — but the comment describing Stop's disappearance should be updated to include this new case.

Checked and found sound

The asymmetric quotes.quotes === undefined check in the permission guard is safe (normalizeOptionalQuotes returns {} for empty arrays, so the two forms are equivalent here). Hoisting the two throwing validators into locals is correct (avoids double validation). Widening is conditional on structured content — bare empty text is still rejected.

What I could not judge

Desktop not run (bubble/Stop-button points are from code reading; actual look needs eyes). Whether formatTextWithInlineRefs really folds directoryReferences into provider text was not verified beyond the comment. The #4805/#4815 sequencing call belongs to orchestration/maintainers — technical comparison only, no ruling made here.


Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.

简体中文

本条结论全部来自 @Opus-Qronos-AstroHan 的审查。我自己没有读这份 diff;我核的是当前 head 有没有漂移。当前 head 是 0311ad1,未关闭。两条 P1 都不是代码缺陷,是和仓库现状的关系:绿检查过期了,纪元门禁实际会红;另和还没合的 #4815 做同一件事,两者会撞,需二选一。另有三条小的提醒。先别合,等裁决。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

desktop: quote-only and attachment-only messages require inline text

3 participants