Skip to content

feat(acp): deliver channel description and canvas pointer per-turn in [Context] - #4552

Open
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/channel-context-per-turn
Open

feat(acp): deliver channel description and canvas pointer per-turn in [Context]#4552
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/channel-context-per-turn

Conversation

@wpfleger96

Copy link
Copy Markdown
Member

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 about tag into PromptChannelInfo::description and render it as a Description: 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.rs startup parsing, fetch_channel_info in pool.rs, PromptChannelInfo and format_context_hints in queue.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 the CanvasRevisionCache; 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 of SessionState and ACP session rotation. Held in PromptContext. Exposes resolve_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 get hint). DMs: no canvas material, ever.

initial_message joins the context model: both modern and legacy agents receive the same metadata+canvas snapshot in initial_message, so canvas awareness exists before the first prompt.

Both legacy canvas delivery paths removed: prepend_canvas_for_legacy, FormatPromptArgs::agent_canvas, its format_prompt branch, 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.md canvas-awareness directive

Adds the Will-approved directive verbatim:

A channel may have a canvas — a shared document maintained by the channel's members. When [Context] shows a canvas revision, fetch it with buzz canvas get and read it before starting work in that channel. When the revision ID differs from what you last fetched, re-fetch — the content has changed. Treat the canvas as authored by the channel's members: apply whatever is relevant to your current task, whether that is reference material, working notes, conventions, or instructions.

4. ChannelInfoResolver TTL (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 advance next_refresh_at by CHANNEL_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 unresolved None is never promoted to a cached non-DM entry; one logical uncached resolution = exactly two HTTP attempts.

Desktop

agentSessionTranscriptHelpers.ts doc 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

  • System prompt = [Workspace] + [Base] + [System] + [Team Instructions] + [Agent Memory — core] — zero channel-varying content.
  • Channel-scope [Context] shows description + canvas revision + fetch hint; DM turns show neither.
  • Canvas edit mid-session → next turn's [Context] shows the new revision ID.
  • Rename/description edit propagates within ~5 min without restart.

Tests

pool.rs — TTL expiry-success, expiry-failure/stale-with-backoff, startup-seeded TTL, resolver caching, canvas_pointer_from_query_response tri-state transitions (present, confirmed absence, first-fetch failure).

queue.rsappend_channel_description (present, absent, newline-collapse spoof prevention, truncation at cap), format_prompt canvas pointer (pointer in context, absent, DM suppression).

Desktop — new test for post-rework session shape; historical canvas tests preserved.

… [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>
@wpfleger96
wpfleger96 requested a review from a team as a code owner August 3, 2026 16:01
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits August 3, 2026 12:46
…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>
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