feat(acp): deliver channel description and canvas pointer per-turn in [Context] - #4552
Open
wpfleger96 wants to merge 3 commits into
Open
feat(acp): deliver channel description and canvas pointer per-turn in [Context]#4552wpfleger96 wants to merge 3 commits into
wpfleger96 wants to merge 3 commits into
Conversation
… [Context] Move channel-scoped context from the frozen system prompt to per-turn [Context] injection. Four coordinated changes: 1. Channel description: parse the kind-39000 `about` tag into `PromptChannelInfo::description`, render as `Description:` in [Context] with newline-collapse (spoof prevention) and 500-char truncation. DMs suppress the field fail-closed. 2. Canvas pointer per-turn: introduce `CanvasRevisionCache` (process- and channel-scoped, independent of SessionState / ACP session rotation) with tri-state fetch result — Present, Absent (confirmed), Failed (serves stale). Single-attempt 3 s timeout, never wrapped in fetch_with_retry. Canvas revision + fetch hint injected into [Context] for both modern and legacy agents, including initial_message. Remove canvas from the frozen system prompt: SessionState::canvas_sections, with_canvas, prepend_canvas_for_legacy, FormatPromptArgs::agent_canvas, and the session-new canvas fetch block all deleted. 3. base_prompt.md: add Will-approved canvas-awareness directive so agents know to fetch and re-fetch on revision change. 4. ChannelInfoResolver TTL: ~5-min per-entry TTL with startup-seeded entries included. Stale entries served immediately with background refresh spawned. Failed refresh advances next_refresh_at by a 60 s backoff (prevents retry storms on degraded relay). All negative-cache invariants preserved: unknown-name sentinel excluded from session titles, unresolved None never promoted to a cached non-DM entry, one logical resolution = exactly two HTTP attempts. Desktop: update agentSessionTranscriptHelpers.ts doc comment and add test for the new session shape. The [Channel Canvas] extractor is retained for backward compat with historical session recordings. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…esh guard, query_once Five fixes addressing pass-1 review findings: 1. canvas_pointer_from_query_response now returns CanvasFetchResult directly. Validation rejections (malformed JSON, bad sig, wrong kind, wrong channel tag, invalid timestamp) map to Failed — stale cache entry is preserved. Only confirmed-empty array or valid blank content maps to Absent. 2. run_prompt_task resolves metadata and canvas exactly once per turn; the single snapshot (turn_channel_info, is_dm_turn, canvas_pointer) feeds both initial_message and the batch prompt. initial_message uses format_context_hints with is_dm_turn so DM turns emit Scope: dm, no description, no canvas. 3. ChannelInfoResolver::resolve atomically claims an in_flight slot before spawning background_refresh. Concurrent callers on an expired entry skip the spawn rather than stampeding the relay. 4. RestClient::query_once issues exactly one HTTP request with no retry loop. fetch_canvas_pointer now calls query_once instead of query. 5. All clippy errors resolved: unused CanvasRevisionCache::get removed, doc-comment empty lines fixed, overindented doc-list items corrected, unused bindings removed. Tests added: stale-preservation on malformed/tampered/wrong-channel parse failures; concurrent expired-entry resolve asserts one refresh spawned; query_once request-count on 503 asserts no retry; initial_message context composition for channel turn (Scope: channel, description, canvas) and DM turn (Scope: dm, no description, no canvas). Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…sumers Pass is_dm_turn (fail-closed: unresolved metadata → true) into FormatPromptArgs::is_dm and fetch_conversation_context rather than letting each consumer re-derive it from channel_info with opposite defaults. Before: format_prompt derived is_dm via channel_info.unwrap_or(false), and fetch_conversation_context did the same — so an unresolved turn emitted Scope: dm in initial_message but Scope: channel in the batch prompt. After: a single is_dm_turn value computed at turn start flows through FormatPromptArgs and directly into fetch_conversation_context; both prompt paths share the same classification. The channel_info parameter is removed from fetch_conversation_context (it was only used to derive is_dm). Tests: added test_unresolved_metadata_both_prompts_render_scope_dm proving that initial_message and batch both produce Scope: dm, no description, and no canvas when metadata is unresolved. Updated 8 existing DM-scope tests to pass is_dm: true explicitly since the field now governs scope rendering. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Move channel-scoped context out of the frozen session system prompt and into per-turn
[Context]injection. Four coordinated changes.Changes
1. Channel description per-turn
Parse the kind-39000
abouttag intoPromptChannelInfo::descriptionand render it as aDescription:line in[Context]. Multiline descriptions are collapsed to a single space (prevents a crafted description from spoofing adjacent[Context]fields) and capped at 500 chars with a…truncation marker. DMs suppress the field fail-closed.Touch points:
relay.rsstartup parsing,fetch_channel_infoinpool.rs,PromptChannelInfoandformat_context_hintsinqueue.rs.2. Canvas pointer per-turn (A1, A2, A4)
Tri-state fetch result (
CanvasFetchResult):Present(CanvasPointer),Absent(confirmed blank/deleted),Failed(transport/parse error). Present and Absent both overwrite theCanvasRevisionCache; Failed retains the prior value — a cleared canvas must clear the pointer, but a transport failure must not.CanvasRevisionCache: process-and-channel-scoped, keyed by channel UUID, independent ofSessionStateand ACP session rotation. Held inPromptContext. Exposesresolve_for_turn(channel_id, result).Per-turn fetch: single-attempt 3 s timeout, never wrapped in
fetch_with_retry. After the DM check establishes a non-DM turn, the canvas revision is fetched and injected into[Context](event ID + last-modified timestamp +buzz canvas gethint). DMs: no canvas material, ever.initial_messagejoins the context model: both modern and legacy agents receive the same metadata+canvas snapshot ininitial_message, so canvas awareness exists before the first prompt.Both legacy canvas delivery paths removed:
prepend_canvas_for_legacy,FormatPromptArgs::agent_canvas, itsformat_promptbranch, the caller, and their tests. Legacy agents now receive canvas only inside[Context], not twice.Removed from session system-prompt composition:
SessionState::canvas_sections,with_canvas, the session-new canvas fetch block.3.
base_prompt.mdcanvas-awareness directiveAdds the Will-approved directive verbatim:
4.
ChannelInfoResolverTTL (A3)Per-entry ~5-minute TTL (
CHANNEL_INFO_TTL = 300 s), including startup-seeded entries. Stale entries are served immediately with a background refresh spawned. On failed refresh: serve stale and advancenext_refresh_atbyCHANNEL_INFO_REFRESH_BACKOFF = 60 s— prevents the ~6.5 s retry sequence from repeating on every consumer (the resolver sits on the inbound DM author-gate hot path).Preserved invariants: the
"unknown"-name sentinel is excluded from session titles; an unresolvedNoneis never promoted to a cached non-DM entry; one logical uncached resolution = exactly two HTTP attempts.Desktop
agentSessionTranscriptHelpers.tsdoc comment updated to document the new session shape and clarify that the[Channel Canvas]extractor is retained for historical session recordings (backward compat). New test added for the post-rework session shape.Success Criteria
[Workspace]+[Base]+[System]+[Team Instructions]+[Agent Memory — core]— zero channel-varying content.[Context]shows description + canvas revision + fetch hint; DM turns show neither.[Context]shows the new revision ID.Tests
pool.rs— TTL expiry-success, expiry-failure/stale-with-backoff, startup-seeded TTL, resolver caching,canvas_pointer_from_query_responsetri-state transitions (present, confirmed absence, first-fetch failure).queue.rs—append_channel_description(present, absent, newline-collapse spoof prevention, truncation at cap),format_promptcanvas pointer (pointer in context, absent, DM suppression).Desktop — new test for post-rework session shape; historical canvas tests preserved.