Skip to content

feat: integrate agent context snapshots into chats#26389

Merged
kylecarbs merged 3 commits into
mainfrom
chatd-context-integration
Jun 16, 2026
Merged

feat: integrate agent context snapshots into chats#26389
kylecarbs merged 3 commits into
mainfrom
chatd-context-integration

Conversation

@kylecarbs

@kylecarbs kylecarbs commented Jun 15, 2026

Copy link
Copy Markdown
Member

Makes the chat context foundation from #26385 live. That PR added the storage columns, writer queries, and a dormant agentapi.ContextDirtyMarker trigger with no production callers; this PR wires them together end to end.

When a workspace agent pushes a context snapshot, bound chats now hydrate to that snapshot's hash, and a later push with a different hash flips already-pinned chats to dirty (emitting a context_dirty watch event after the transaction commits). Chat creation pins the agent's latest snapshot when one already exists. The experimental chat API exposes this as Chat.Context (*ChatContext with dirty, dirty_since, error), and a new PUT /api/experimental/chats/{chat}/context endpoint re-pins the agent's latest snapshot and clears the dirty marker.

context_dirty_resources stays NULL (the resource-level diff is deferred to the UI phase) and the live per-turn context pull is unchanged.

The end-to-end test provisions a workspace agent via the echo provisioner, connects it over the Agent API v2.10, and exercises the full path: an initial push hydrates a bound chat (clean), a second push with a different hash marks it dirty, the API reports the dirty state, and the refresh endpoint clears it.

Decision log
  • API shape — sub-struct. Dirty state is surfaced as codersdk.Chat.Context *ChatContext { Dirty bool; DirtySince *time.Time; Error string } rather than flat fields, matching the RFC's named ChatContext type and leaving room for future fields (resource diff, sources). db2sdk.Chat populates it when the chat is context-tracked (len(ContextAggregateHash) > 0), dirty, or carries a snapshot error, and leaves it nil (omitempty) otherwise. Dirty mirrors context_dirty_since being set.
  • Marker wiring. The chat daemon is injected directly as the agentapi.ContextDirtyMarker. It is unconditionally constructed (only its background worker is gated), so the marker is always non-nil and the wiring matches every other api.chatDaemon call site. agentapi still treats a nil marker as "chatd absent", so PushContextState stays a pure write path for any future caller that does not wire chatd in.
  • Refresh is atomic. RefreshChatContext reads the agent's latest snapshot and re-pins the chat in one repeatable-read transaction, so a concurrent push cannot land between the read and the write and leave the chat pinned to a stale hash with the dirty marker cleared.
  • Hydrate + dirty run inside the push transaction. The fan-out shares the push's transaction so a concurrent refresh cannot interleave with the version gate; context_dirty watch events publish only after commit. The pinned hash on dirtied chats is intentionally left unchanged — the refresh endpoint re-pins it.
  • Dirtied chats keep their pinned hash. Drift is advisory: a dirty chat stays usable, and refreshing is the only path that advances the pinned hash.
  • Test binds chats.agent_id directly. In production the binding is set lazily during a chat turn (chatd.persistBuildAgentBinding); the test sets it via dbgen so it exercises the context flow rather than turn resolution.

Plan: coderd/x/chatd context integration + E2E (sub-struct API, create-time + push-time hydration, refresh endpoint; context_dirty_resources and the per-turn pull untouched).

🤖 Generated by Coder Agents on behalf of @kylecarbs

@github-actions

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/reference/api/chats.md

@kylecarbs kylecarbs force-pushed the chatd-context-integration branch 2 times, most recently from 9c176dc to dd9a1ee Compare June 15, 2026 23:34

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-16 16:52 UTC by @kylecarbs
Spend: $100.59 / $100.00

Review history
  • R1 (2026-06-16): 19 reviewers, 4 Nit, 4 Note, 1 P2, 7 P3, 1 P4, COMMENT. Review
  • R2 (2026-06-16): 11 reviewers, 7 Nit, 4 Note, 1 P2, 10 P3, 1 P4, COMMENT. Review
  • R3 (2026-06-16): 5 reviewers, 7 Nit, 4 Note, 1 P2, 10 P3, 1 P4, APPROVE. Review

deep-review v0.7.1 | Round 3 | 210261b..30b54eb

Last posted: Round 3, 23 findings (1 P2, 10 P3, 1 P4, 7 Nit, 4 Note), APPROVE. Review

Finding inventory

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 Nit Author fixed (bedf763) coderd/exp_chats.go:2627 Missing @produce json swagger annotation on refreshChatContext R1 Netero, Meruem Yes
CRF-2 Note Author fixed (bedf763) coderd/x/chatd/context_hydration.go:86 hydrateChatContextOnCreate has zero test coverage R1 Netero Yes
CRF-3 Note Author accepted R2 (no consumer for clean-transition event yet; deferred to UI phase) coderd/x/chatd/context_hydration.go:116 RefreshChatContext does not emit watch event when clearing dirty R1 Netero Yes
CRF-4 P2 Author fixed (bedf763) coderd/x/chatd/context_hydration.go:126 RefreshChatContext reads snapshot and writes pin in separate non-transactional calls; concurrent push can leave chat falsely clean R1 Mafuuu P2, Knov P2, Meruem P2, Hisoka P3, Ryosuke P3, Chopper P3, Kite P3, Knuckle P3, Razor P3 Yes
CRF-5 P3 Author fixed (bedf763) PR description Decision log claims "helper that returns nil interface" that does not exist R1 Mafu-san Yes
CRF-6 P3 Author fixed (bedf763) coderd/x/chatd/context_hydration.go:102 hydrateChatContextOnCreate unconditional SetChatContextSnapshot can overwrite concurrent push stamp R1 Hisoka P2, Ryosuke P3, Kite P3, Zoro P3 Yes
CRF-7 P3 Author fixed (bedf763) coderd/x/chatd/context_integration_test.go:115 Refresh test does not prove pinned hash advanced; a third push of hashB after refresh would close the loop R1 Bisky Yes
CRF-8 P3 Author fixed (bedf763) coderd/x/chatd/context_integration_test.go:83 ChatContext.Error field never exercised end-to-end R1 Bisky P3, Chopper P3 Yes
CRF-9 P3 Author fixed (bedf763) coderd/x/chatd/context_integration_test.go:60 No negative assertion that unrelated chats are unaffected by push fan-out R1 Chopper Yes
CRF-10 P3 Author fixed (bedf763) coderd/x/chatd/context_hydration.go:59 GetActiveChatsByAgentID fetches all active chats inside push tx when only flipped IDs needed R1 Knov P3, Hisoka Note, Knuckle Note Yes
CRF-11 P3 Author fixed (bedf763) coderd/x/chatd/context_hydration.go:86 hydrateChatContextOnCreate and RefreshChatContext duplicate resolve-latest-snapshot-and-pin sequence R1 Robin Yes
CRF-12 P4 Author fixed (bedf763) coderd/x/chatd/context_hydration.go:29 Comment bloat pattern: 7 nolint/mechanism comments restate code (lines 29, 56, 90, 117) R1 Gon P2 (downgraded: no runtime impact, comments are verbose not incorrect) Yes
CRF-13 Nit Author fixed (bedf763) coderd/x/chatd/context_hydration.go:51 Interface doc says callback is nil when nothing transitioned; implementation returns non-nil func(){} R1 Mafuuu, Knov, Razor Yes
CRF-14 Nit Author fixed (bedf763) coderd/x/chatd/context_hydration.go:44 flipped does not encode direction; dirtied matches the domain R1 Gon Yes
CRF-15 Nit Author fixed (bedf763) coderd/x/chatd/context_hydration.go:74 Inline publish loop duplicates existing publishChatPubsubEvents (chatd.go:3495) R1 Zoro Yes
CRF-16 Note Author accepted R2 (lives in #26385 foundation migration; widening scope not warranted) coderd/database/queries/chats.sql:1494 HydrateAgentChatsContext filters by archived=false but not status; stamps terminal chats too R1 Chopper Yes
CRF-17 Note Author accepted R2 (no watcher consumes Context yet; deferred to UI phase) coderd/x/chatd/chatd.go:1464 Created pubsub event published before hydration; watchers see nil Context R1 Mafuuu, Meruem Yes
CRF-18 P3 Author fixed (30b54eb) coderd/x/chatd/context_hydration.go:82 Post-commit callback uses request-scoped ctx; gRPC stream death drops dirty events R2 Hisoka Yes
CRF-19 P3 Author fixed (30b54eb) coderd/x/chatd/context_hydration.go:79 Post-commit re-read TOCTOU: event kind=dirty but payload may show clean R2 Hisoka P3, Chopper Note Yes
CRF-20 P3 Author fixed (30b54eb) coderd/x/chatd/context_integration_test.go:134 Dirty-step assertions omit Context.Error; design invariant unverified R2 Chopper Yes
CRF-21 Nit Author fixed (30b54eb) coderd/x/chatd/context_hydration.go:18 latestAgentSnapshot doc restates signature in 2 of 3 sentences R2 Gon Yes
CRF-22 Nit Author fixed (30b54eb) coderd/x/chatd/context_hydration.go:99 "NULL-guarded" is SQL implementation detail in Go doc R2 Gon Yes
CRF-23 Nit Author fixed (30b54eb) commit message Commit subject names process, not fix R2 Leorio Yes

Contested and acknowledged

CRF-3 (Note, coderd/x/chatd/context_hydration.go:116) - RefreshChatContext does not emit watch event on dirty clear

  • Finding: No corresponding "context clean" event when the refresh endpoint clears the dirty marker; other watchers see stale dirty state.
  • Author defense: No watcher consumes Context yet. The initiating HTTP client sees the cleared state in the 200 response. Clean-transition event deferred to the UI phase.
  • Author accepted: Consequence bounded to watch subscribers; no current consumer affected.

CRF-16 (Note, coderd/database/queries/chats.sql:1494) - HydrateAgentChatsContext stamps terminal chats

  • Finding: HydrateAgentChatsContext filters by archived=false but not by active status, unlike MarkChatsContextDirtyByAgent. Terminal chats receive a pinned hash.
  • Author defense: The query lives in the merged foundation migration (feat: add chat context pinning storage and push trigger #26385). Gating by status to match the dirty query would widen this PR's scope.
  • Author accepted: Harmless today; no UI consumer keys visibility off Context != nil.

CRF-17 (Note, coderd/x/chatd/chatd.go:1464) - Created event published before hydration

  • Finding: The "created" pubsub event carries nil Context because it fires before hydrateChatContextOnCreate runs.
  • Author defense: No watcher consumes Context yet. The HTTP creator re-reads after CreateChat and sees the hydrated state. Reordering deferred to UI phase.
  • Author accepted: Consequence bounded; no current consumer affected.

Round log

Round 1

Panel. 19 reviewers (Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, Ging-Go, Ryosuke, Chopper, Robin, Knov, Kurapika, Kite, Meruem, Knuckle, Zoro, Razor, Killua). 1 P2, 7 P3, 1 P4, 4 Nit, 4 Note. Reviewed against 210261b..dd9a1ee04b7a.

Round 2

Churn guard: PROCEED. 14 addressed, 3 acknowledged. Panel: 11 reviewers (Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Gon, Leorio, Meruem, Kite, Chopper, Knov). 3 P3, 3 Nit new. Reviewed against 210261b..bedf763fcd0a.

Round 3

Churn guard: PROCEED. 6 addressed, 0 silent. Panel: 5 reviewers (Bisky, Hisoka, Mafuuu, Pariston, Meruem). 0 new findings. All R1+R2 fixes verified. APPROVE.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot 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.

The transactional design of HydrateAndMarkChatsDirty is strong: running hydration and dirty marking inside the push's ReadModifyUpdate transaction prevents phantom events from rolled-back retries, and the post-commit callback pattern ensures watch events only fire for committed state. The SDK type design (nullable ChatContext sub-struct, omitempty) is clean.

1 P2, 7 P3, 1 P4, 4 Nit, 4 Note across 19 reviewers.

"I tried to build a case against this change and could not." (Pariston)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/context_hydration.go Outdated
Comment thread coderd/x/chatd/context_hydration.go Outdated
Comment thread coderd/x/chatd/context_integration_test.go Outdated
Comment thread coderd/x/chatd/context_integration_test.go
Comment thread coderd/x/chatd/context_integration_test.go
Comment thread coderd/x/chatd/chatd.go
Comment thread coderd/x/chatd/context_hydration.go
Comment thread coderd/x/chatd/context_hydration.go
Comment thread coderd/workspaceagentsrpc.go
Comment thread coderd/x/chatd/context_hydration.go

Copy link
Copy Markdown
Member Author

/coder-agents-review

@coder-agents-review coder-agents-review Bot 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.

All 14 fixed R1 findings verified against bedf763. The R1 P2 (TOCTOU in RefreshChatContext) is properly closed: snapshot read and re-pin now share a single ReadModifyUpdate transaction with serialization-error retry. Create-time hydration correctly uses the NULL-guarded HydrateAgentChatsContext. The shared latestAgentSnapshot helper eliminated the duplication. Test coverage is substantially improved.

3 P3, 2 Nit across 11 reviewers. R1 acknowledged Notes (CRF-3, CRF-16, CRF-17) remain deferred to the UI phase.

"This is a gem." (Bisky, on the integration test)

Process note: the fix commit subject (fix(chatd): address deep-review feedback on chat context) names the process, not the fix. If not squash-merged, consider fix(chatd): close TOCTOU in context refresh, tighten create-time hydration. (Leorio)

🤖 This review was automatically generated with Coder Agents.

Comment thread coderd/x/chatd/context_hydration.go Outdated
Comment thread coderd/x/chatd/context_hydration.go
Comment thread coderd/x/chatd/context_integration_test.go
Comment thread coderd/x/chatd/context_hydration.go Outdated
Comment thread coderd/x/chatd/context_hydration.go Outdated
Comment thread coderd/x/chatd/context_hydration.go
@kylecarbs kylecarbs requested a review from hugodutka June 16, 2026 16:44
@kylecarbs kylecarbs marked this pull request as ready for review June 16, 2026 16:45

Copy link
Copy Markdown
Member Author

/coder-agents-review

kylecarbs pushed a commit that referenced this pull request Jun 16, 2026
## Problem

`test-go-pg (windows-2022)` has been red on `main` since the Vercel CLI
was added to `mise.toml`. Every PR that runs Windows CI (including
#26389) fails at `Restore Go cache` -> setup-mise -> `mise install`, and
the `:x: CI Failure` Slack notifications have been firing ~hourly
because of it.

The Vercel CLI lives in the top-level `[tools]` block, so mise tries to
install it on every runner. It is installed via the `npm:vercel`
backend, which runs:

```
C:\Windows\system32\cmd.exe /d /s /c node install.js
```

as an npm post-install script. mise installs node into its own shim dir;
`node` is not on cmd.exe's PATH at the moment npm spawns the
post-install script, so it fails with:

```
npm error command C:\Windows\system32\cmd.exe /d /s /c node install.js
npm error 'node' is not recognized as an internal or external command,
npm error operable program or batch file.
mise ERROR Failed to install npm:vercel@54.14.0: npm.cmd exited with non-zero status: exit code 1
```

The whole setup-mise step aborts, so the rest of the job is skipped.
Tracked in coder/internal#1596.

## Fix

The Vercel CLI is only used to deploy / preview from dogfood workspaces
(Linux). Restrict it to linux/macos via the documented
[`os`](https://mise.jdx.dev/dev-tools/#os-specific-tools) filter:

```toml
vercel = { version = "54.14.0", os = ["linux", "macos"] }
```

On Windows mise will now skip the install entirely. On Linux/macOS
behavior is unchanged, the binary still resolves through the same
`npm:vercel` backend.

## Lockfile

`os` is an install-time filter and has no representation in `mise.lock`.
`mise lock` against the pinned `min_version = "2026.5.12"` produces no
diff, so the lockfile is intentionally left as-is.

## Verification

- `mise trust && mise ls` on Linux still resolves `vercel 54.14.0` from
`mise.toml`.
- `mise lock` against pinned v2026.5.12 is a no-op on `mise.lock`.
- Windows runners will report `(skipped)` for `vercel` and continue past
setup-mise.

Fixes coder/internal#1596.

Created on behalf of @kylecarbs.

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Makes the chat context foundation (#26385) live. Agent context pushes now
hydrate bound chats and mark them dirty on drift, chat creation pins the
agent's latest snapshot, the experimental chat API reports the pinned
context state, and a new refresh endpoint re-pins the latest snapshot and
clears the dirty marker.

- coderd/x/chatd/context_hydration.go: HydrateAndMarkChatsDirty (push-time
  hydrate + dirty fan-out, publishes context_dirty after commit),
  hydrateChatContextOnCreate (create-time pin, best-effort), and
  RefreshChatContext (re-pin latest snapshot + clear dirty).
- coderd/workspaceagentsrpc.go: wire the chat daemon in as the agentapi
  ContextDirtyMarker, nil-guarded so a disabled daemon stays a pure write
  path.
- codersdk + db2sdk: expose Chat.Context (*ChatContext) carrying dirty
  state and snapshot error; re-add the context_dirty watch event.
- coderd: PUT /api/experimental/chats/{chat}/context refresh endpoint.

context_dirty_resources stays NULL and the live per-turn context pull is
unchanged. Adds an end-to-end test where an echo-provisioned agent pushes
an initial snapshot (hydrate), re-pushes a different hash (drift -> dirty),
the API reports dirty, and refresh clears it.
- RefreshChatContext now reads the snapshot and re-pins in one
  repeatable-read transaction, closing a TOCTOU window where a
  concurrent push could leave the chat pinned to a stale hash with the
  dirty marker cleared (CRF-4).
- hydrateChatContextOnCreate pins via the NULL-guarded
  HydrateAgentChatsContext so a concurrent push that already hydrated
  the chat is not clobbered (CRF-6).
- The dirty fan-out resolves only the dirtied chats after commit
  instead of scanning every active chat for the agent (CRF-10), and
  reuses publishChatPubsubEvents (CRF-15).
- Extract a shared latestAgentSnapshot helper (CRF-11), rename
  flipped->dirtied (CRF-14), align the no-op callback contract with its
  doc (CRF-13), and trim restated comments (CRF-12).
- Add @produce json to the refresh endpoint (CRF-1).
- Tests: exercise the snapshot-error path end-to-end, assert an
  agent-less chat is untouched by the fan-out, and re-push the pinned
  hash to prove refresh advanced the pin (CRF-7/8/9); add an internal
  test for create-time hydration (CRF-2).
Read the dirtied chats inside the push transaction and publish the
captured rows after commit, so the post-commit callback needs no
database access. This closes two issues introduced when the read was
moved post-commit:

- the callback used the request-scoped RPC context for the read, so a
  dying agent stream could silently drop every dirty notification;
- the post-commit re-read could observe a concurrent refresh clearing
  the marker, publishing a context_dirty event whose payload was clean.

The read still touches only the transitioned chats, not every active
chat for the agent.

Also assert the pinned error is unchanged at the dirty step (the
dirty fan-out must not overwrite it) and trim two doc comments that
restated the signature / leaked a SQL detail.

@coder-agents-review coder-agents-review Bot 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.

All 23 findings across 3 rounds resolved. R1 P2 (TOCTOU in RefreshChatContext) closed with ReadModifyUpdate. R2 P3s (post-commit context lifetime and TOCTOU) closed by capturing dirtied rows in-transaction. Test coverage is thorough: integration test exercises the full state machine (nil -> hydrated -> dirty -> refreshed -> re-push stays clean) with negative guards and error propagation; internal unit tests cover the three create-time hydration branches.

"The threads I pulled held." (Hisoka, R3)

🤖 This review was automatically generated with Coder Agents.

@kylecarbs kylecarbs force-pushed the chatd-context-integration branch from 30b54eb to ae3c50b Compare June 16, 2026 17:35
@kylecarbs kylecarbs enabled auto-merge (squash) June 16, 2026 17:45
@kylecarbs kylecarbs merged commit bca0ce0 into main Jun 16, 2026
34 checks passed
@kylecarbs kylecarbs deleted the chatd-context-integration branch June 16, 2026 17:46
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants