Skip to content

feat(chat-routes): let authorize suppress the user-row insert for a dispatched turn#207

Merged
vutuanlinh2k2 merged 1 commit into
mainfrom
feat/authorize-suppress-user-insert
Jul 20, 2026
Merged

feat(chat-routes): let authorize suppress the user-row insert for a dispatched turn#207
vutuanlinh2k2 merged 1 commit into
mainfrom
feat/authorize-suppress-user-insert

Conversation

@vutuanlinh2k2

Copy link
Copy Markdown
Contributor

What

Adds an optional insertUserMessage?: boolean to the ok:true branch of ChatTurnAuthorization. When a product's authorize returns insertUserMessage: false, the turn engine skips the role:'user' store.appendMessage for that turn.

Why

The turn engine owns the user-row insert (before any product seam runs), guarded only by its own retry-dedup (resolveChatTurn.shouldInsertUserMessage). A product-dispatched / synthetic turn — e.g. a plan follow-up the product raises itself when the user clicks "Approve", not something they typed — must run the turn (attach a producer, stream, persist the assistant row) without surfacing a new user bubble. There is currently no seam for that; the store adapter can't suppress it because the insert happens before beforeTurn.

This is the minimal, idiomatic addition matching the turnLock seam precedent (#200): one optional @experimental field on a type products already implement, no new lifecycle position.

Design

  • AND-composition, not override: the engine computes chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true). authorize runs before turn-identity resolves, so it cannot tell a retry from a fresh turn — the field may only subtract the insert, never resurrect a turn the engine already deduped. A stray insertUserMessage: true therefore can't force a double-insert.
  • Backward compatible: the field is optional. Omitting it → undefined ?? true → collapses to today's exact behavior. No existing consumer or test is affected; turnIndex / priorMessages / produce / turnLock / contextGate / beforeTurn are untouched, and resolveChatTurn needs no change.

Changes

  • src/chat-routes/turn-routes.ts — the optional field + the one-line AND-composition at the insert site.
  • tests/chat-routes/turn-routes.test.ts — two tests: (1) insertUserMessage:false suppresses the user row but still runs produce and persists the assistant row; (2) insertUserMessage:true cannot resurrect a deduped retry.
  • examples/chat-app.mdauthorize note showing the marker-driven insertUserMessage: false.

Verification

  • pnpm typecheck — clean.
  • pnpm test — chat-routes turn-routes suite 23/23; full suite 2514 pass. (Pre-existing src/sandbox/index.test.ts failures are unrelated — they source a local ~/.bash_profile alias and fail on main too.)
  • pnpm build — emits insertUserMessage?: boolean into dist/chat-routes/index.d.ts.

Consumer

gtm's chat vertical will return insertUserMessage: false from authorize when the request body carries a durable planFollowUp marker, letting the follow-up attach to its already-dispatched sidecar execution without a synthetic user row.

…ispatched turn

Add an optional insertUserMessage?: boolean to the ok:true branch of
ChatTurnAuthorization. When false, the engine skips the role:'user'
appendMessage for a product-dispatched / synthetic turn (e.g. a plan
follow-up the product raised itself) that must not surface a new user
row. AND-composed with the engine's retry-dedup so it can only subtract,
never resurrect a turn already deduped as a retry. Omitting the field
reproduces today's behavior exactly.

@tangletools tangletools left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟢 Value Audit — sound

Verdict sound
Concerns 1 (1 weak-concern)
Heuristic 0.0s
Duplication 0.0s
Interrogation 259.5s (2 bridge agents)
Total 259.5s

💰 Value — sound

Adds an optional insertUserMessage flag on authorize's ok-branch that lets a product suppress the user-row insert for a dispatched/synthetic turn; minimal, idiomatic, AND-composed with the engine's existing retry dedup — ship.

  • What it does: Adds an optional insertUserMessage?: boolean field to the ok:true branch of ChatTurnAuthorization<TContext> (src/chat-routes/turn-routes.ts:117-131). At the user-insert site (src/chat-routes/turn-routes.ts:583-591) the engine now computes chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true) and skips store.appendMessage({role:'user', ...}) when that's false. The assistant
  • Goals it achieves: Lets a product run a turn through the assembled chat vertical WITHOUT surfacing a new user bubble — the canonical case is a product-dispatched follow-up (e.g. the user clicks Approve on a plan and the product raises the next turn itself). Today the engine unconditionally inserts a role:'user' row before any product seam runs (the insert sits in the try block before contextGate/beforeTurn),
  • Assessment: Coherent and in the grain. (1) It matches the exact precedent of the other single-consumer @experimental seams (turnLock/contextGate/beforeTurn/onRawEvent from #200) — one optional typed field, no new lifecycle position, additive-only. (2) The AND-composition is the right design: authorize runs at src/chat-routes/turn-routes.ts:464 BEFORE resolveChatTurn at :476, so authorize genui
  • Better / existing approach: none — this is the right approach. Searched: (a) shouldInsertUserMessage is only in src/stream/turn-identity.ts:12,54,62 — the engine's own dedup signal, which this change composes with rather than replaces; (b) no existing seam can suppress the insert — beforeTurn (src/chat-routes/turn-routes.ts:605) runs AFTER the insert, contextGate (:596) also after, turnLock (:529) acquires before but
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 2
  • Bridge warning: opencode/kimi-for-coding/k2p7: bridge stream ended without value-audit content

🎯 Usefulness — sound

A minimal, backward-compatible seam on the existing authorize return that lets a product suppress the user-row insert for a dispatched/synthetic turn; AND-composes with the engine's retry-dedup so it can only subtract, never resurrect.

  • Integration: Wired directly at the insert site — src/chat-routes/turn-routes.ts:583-591 — exactly one line of new logic plus the optional field on ChatTurnAuthorization at src/chat-routes/turn-routes.ts:122-130. Reachable today: the reference consumer doc examples/chat-app.md:58-70 was updated in this same PR to demonstrate the concrete usage (body?.planFollowUp ? { insertUserMessage: false }), and t
  • Fit with existing patterns: Matches the established single-consumer @experimental seam precedent exactly (turnLock/contextGate/beforeTurn/onRawEvent, #200 #202, documented at src/chat-routes/turn-routes.ts:33-39): one optional field on a type products already implement, no new lifecycle position, additive-only. Placement on authorize's ok-branch is the right spot and not a competitor to existing seams — `before
  • Real-world viability: Holds up under the relevant edges. AND-composition (chatTurn.shouldInsertUserMessage && (auth.insertUserMessage ?? true) at src/chat-routes/turn-routes.ts:583) is the correct primitive because authorize runs before resolveChatTurn (src/chat-routes/turn-routes.ts:464 vs :476), so it cannot distinguish retries from fresh turns — the field can only subtract. Backward compat is exact: omit
  • Model: opencode/zai-coding-plan/glm-5.2
  • Bridge attempts: 1

💰 Value Audit

🟡 Behavior hint on the authorize seam is a mild semantic stretch [maintenance] ``

authorize is conceptually about access (who is allowed to do this turn), and now also carries a persistence-behavior hint (insertUserMessage). In practice this is fine — authorize already returns context/tenantId/userId, so it's effectively 'the product's per-turn preamble to the engine', not pure auth. But if a second behavior hint of this kind shows up later, consider grouping these as a dedicated turnOptions? field on the ok-branch rather than letting them accumulate top-level.


What this audit checks

It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.

Pass What it asks
Heuristic Vague title? Whitespace-only or cruft-bearing diff? (content signals only)
Duplication Do added function/class names already exist elsewhere in the repo?
Value Audit What does it do? What goal does it achieve? Is it good? Better architecture or already-exists?
Usefulness Audit Does it integrate and fit? Will it hold up in real use and actually get used?

Findings are concerns, not blocks — the human reviewer decides what to do with them.

value-audit · 20260720T034735Z

@vutuanlinh2k2
vutuanlinh2k2 merged commit a827e66 into main Jul 20, 2026
1 check passed
@vutuanlinh2k2
vutuanlinh2k2 deleted the feat/authorize-suppress-user-insert branch July 20, 2026 03:48
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.

2 participants