Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coderd/agentapi/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type ContextDirtyMarker interface {
// already-pinned chats whose hash differs from aggregateHash. It
// returns a callback that publishes the resulting dirty watch events;
// the caller invokes it only after the transaction commits. The
// callback is nil when nothing transitioned to dirty.
// callback is a no-op when nothing transitioned to dirty.
HydrateAndMarkChatsDirty(ctx context.Context, tx database.Store, agentID uuid.UUID, aggregateHash []byte, snapshotError string, now time.Time) (publishDirty func(), err error)
}

Expand Down
68 changes: 66 additions & 2 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 62 additions & 2 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1360,6 +1360,7 @@ func New(options *Options) *API {
r.Post("/title/regenerate", api.regenerateChatTitle)
r.Post("/title/propose", api.proposeChatTitle)
r.Get("/diff", api.getChatDiffContents)
r.Put("/context", api.refreshChatContext)
r.Route("/queue/{queuedMessage}", func(r chi.Router) {
r.Delete("/", api.deleteChatQueuedMessage)
r.Post("/promote", api.promoteChatQueuedMessage)
Expand Down
13 changes: 13 additions & 0 deletions coderd/database/db2sdk/db2sdk.go
Original file line number Diff line number Diff line change
Expand Up @@ -1755,6 +1755,19 @@ func Chat(c database.Chat, diffStatus *database.ChatDiffStatus, files []database
chat.LastInjectedContext = parts
}
}
// Report pinned-context state when the chat is context-tracked
// (has a pinned hash), dirty, or carries a snapshot error.
if len(c.ContextAggregateHash) > 0 || c.ContextDirtySince.Valid || c.ContextError != "" {
chatContext := &codersdk.ChatContext{
Dirty: c.ContextDirtySince.Valid,
Error: c.ContextError,
}
if c.ContextDirtySince.Valid {
dirtySince := c.ContextDirtySince.Time
chatContext.DirtySince = &dirtySince
}
chat.Context = chatContext
}
return chat
}

Expand Down
5 changes: 5 additions & 0 deletions coderd/database/db2sdk/db2sdk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,11 @@ func TestChat_AllFieldsPopulated(t *testing.T) {
RawMessage: json.RawMessage(`[{"name":"tool1","description":"test tool","inputSchema":{"type":"object"}}]`),
Valid: true,
},
// Pinned-context columns drive codersdk.Chat.Context. Set all of
// them so the converted sub-struct's fields are non-zero too.
ContextAggregateHash: []byte{0x01, 0x02, 0x03},
ContextDirtySince: sql.NullTime{Time: now, Valid: true},
ContextError: "context boom",
}
// Only ChatID is needed here. This test checks that
// Chat.DiffStatus is non-nil, not that every DiffStatus
Expand Down
33 changes: 33 additions & 0 deletions coderd/exp_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -2618,6 +2618,39 @@ func (api *API) applyChatTitleUpdate(
return updatedChat, false
}

// refreshChatContext re-pins a chat to its agent's latest context snapshot
// and clears the dirty marker.
//
Comment thread
kylecarbs marked this conversation as resolved.
// @Summary Refresh chat context
// @ID refresh-chat-context
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param chat path string true "Chat ID" format(uuid)
// @Success 200 {object} codersdk.Chat
// @Router /api/experimental/chats/{chat}/context [put]
// @Description Experimental: this endpoint is subject to change.
func (api *API) refreshChatContext(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
chat := httpmw.ChatParam(r)

if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
httpapi.ResourceNotFound(rw)
return
}

updated, err := api.chatDaemon.RefreshChatContext(ctx, chat)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error refreshing chat context.",
Detail: err.Error(),
})
return
}

httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Chat(updated, nil, nil))
}

// patchChat updates a chat resource. Supports updating labels,
// workspace binding, archiving, pinning, and pinned-chat ordering.
//
Expand Down
3 changes: 3 additions & 0 deletions coderd/workspaceagentsrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ func (api *API) workspaceAgentRPC(rw http.ResponseWriter, r *http.Request) {

// Optional:
UpdateAgentMetricsFn: api.UpdateAgentMetrics,
// chatDaemon is always constructed (only its worker is gated), so
// this is non-nil; agentapi treats a nil marker as "chatd absent".
ContextDirtyMarker: api.chatDaemon,
Comment thread
kylecarbs marked this conversation as resolved.
}, workspace, workspaceAgent)

streamID := tailnet.StreamID{
Expand Down
5 changes: 5 additions & 0 deletions coderd/x/chatd/chatd.go
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,11 @@ func (p *Server) CreateChat(ctx context.Context, opts CreateOptions) (database.C
// committed and emitted its own state-machine notifications. The
// watch endpoint is maintained separately from chatstate notifications.
p.publishChatPubsubEvent(chat, codersdk.ChatWatchEventKindCreated, nil)

// Pin the chat to the agent's latest context snapshot if one exists.
// Best-effort: a chat created before its agent has pushed is hydrated
// by that agent's next push.
p.hydrateChatContextOnCreate(ctx, chat)

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.

Note [CRF-17] The created pubsub event is published (line 1459) before hydrateChatContextOnCreate runs. The event payload carries the pre-hydration chat struct, so Context is nil. Watch subscribers see a chat created with no context and have no follow-up event for the hydration. The HTTP handler re-reads after CreateChat returns, so the creator sees the hydrated state. Worth noting for the UI phase where watch events may drive real-time context badges. (Mafuuu, Meruem)

🤖

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Acknowledged — deferring to the UI phase as the Note suggests. No watcher consumes Context yet, and the HTTP creator re-reads after CreateChat, so the creator sees the hydrated state. When watch events drive context badges we'll reorder (or emit a follow-up event).

— 🤖 Coder Agents, on behalf of @kylecarbs

return chat, nil
}

Expand Down
Loading
Loading