Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix: address review findings
- Invert ReloadMessages fallback priority: prefer captured instruction
  (fresh from workspace) over stale DB content after compaction
- Distinguish reachable-with-no-content from unreachable using
  workspaceConnOK; honor AGENTS.md deletion by clearing instruction
- Remove stale comment fragment from old persistInstructionFiles
- Remove empty default clause in switch
- Restore DisableChainMode field to its own line
  • Loading branch information
kylecarbs committed Apr 15, 2026
commit bc24c34005cc4526c73014475495eb76727ab4dd
40 changes: 25 additions & 15 deletions coderd/x/chatd/chatd.go
Original file line number Diff line number Diff line change
Expand Up @@ -4626,7 +4626,7 @@ func (p *Server) runChat(
// are picked up without requiring a new chat. Falls back
// to persisted parts if the workspace dial fails.
g2.Go(func() error {
_, freshParts, discoveredSkills, _, fetchErr := p.fetchWorkspaceContext(
_, freshParts, discoveredSkills, workspaceConnOK, fetchErr := p.fetchWorkspaceContext(
ctx,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit discoveredSkills inside the closure shadows the outer-scope variable (Style Reviewer)

The := creates a closure-local discoveredSkills that then writes to the outer skills. Renaming to freshSkills or fetchedSkills would reduce cognitive load when verifying which variable is being used.

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.

Renamed to fetchedSkills inside the closure.

🤖 Generated by Coder Agents

chat,
workspaceCtx.getWorkspaceAgent,
Expand All @@ -4637,17 +4637,26 @@ func (p *Server) runChat(
return workspaceCtx.getWorkspaceConn(instructionCtx)
},
)
if fetchErr == nil && len(freshParts) > 0 {
switch {
case fetchErr == nil && len(freshParts) > 0:
// Workspace returned fresh context files.
instruction = formatSystemInstructionsFromParts(freshParts)
skills = selectSkillMetasForInstructionRefresh(
persistedSkills,
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.

P2 Comment says "Workspace unreachable" but this branch also fires when the workspace IS reachable with zero parts. (Edge Case Analyst P2, Contract Auditor P2)

When fetchWorkspaceContext succeeds but the workspace legitimately returns no context-file parts (e.g. user deleted AGENTS.md), len(freshParts) is 0, so the else branch uses stale persisted content. The workspaceConnOK return value is discarded via _, so the caller cannot distinguish "workspace reachable, empty config" from "workspace dial failed." Deleting AGENTS.md from a workspace won't clear the injected instruction until the user creates a new chat.

Consider using the discarded workspaceConnOK return to distinguish the two cases: when agent != nil && workspaceConnOK && len(freshParts) == 0, set instruction = "" to honor the deletion. Also update the comment to match the actual semantics.

discoveredSkills,
uuid.NullUUID{UUID: currentWorkspaceAgentID, Valid: hasCurrentWorkspaceAgent},
uuid.NullUUID{UUID: latestInjectedAgentID, Valid: hasLatestInjectedAgent},
)
} else {
// Workspace unreachable: fall back to persisted
// context-file parts from the message history.
case fetchErr == nil && workspaceConnOK:
// Workspace reachable but returned no context
// files (e.g. AGENTS.md was deleted). Honor the
// removal by clearing the instruction.
instruction = ""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is this tested?

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.

The deletion scenario (AGENTS.md removed → instruction cleared) didn't have direct test coverage. Added instructionCleared bool to prevent compaction from resurrecting stale content, and added TestInstructionFromContextFiles tests covering the reconstruction, empty-messages, and no-context-file-parts cases. The three-way switch logic is now also structurally simpler after dropping the dead error return.

🤖 Generated by Coder Agents

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.

Reworked the approach based on Kyle's feedback. The per-turn workspace dial was wrong — the actual problem is that create_workspace runs mid-turn as a builtin tool (inside the chatloop), after runChat's instruction setup has already executed with no workspace.

The fix now lives in the onChatUpdated callback: when a workspace is first attached and instruction is empty, it calls persistInstructionFiles right there. This:

  1. Makes AGENTS.md available for the rest of the current turn
  2. Persists the marker so subsequent turns just read from DB (no workspace dial)
  3. Only fires once — the instruction == "" guard prevents redundant fetches

🤖 Generated by Coder Agents

skills = discoveredSkills
default:
// Workspace unreachable or fetch failed: fall
// back to persisted context-file parts from the
// message history.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What if the workspace was deleted?

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.

Added a comment explaining: if the workspace was deleted, getWorkspaceAgent returns an error which causes fetchWorkspaceContext to return nil agent with workspaceConnOK=false, landing in this default case. The persisted context is the best available data.

🤖 Generated by Coder Agents

instruction = instructionFromContextFiles(messages)
skills = persistedSkills
}
Expand Down Expand Up @@ -5341,19 +5350,22 @@ func (p *Server) runChat(
// messages so that any context added during the
// chatloop (e.g. via agent-added context or new
// persisted instruction files) is picked up after
// compaction.
reloadedInstruction := instructionFromContextFiles(reloadedMsgs)
// compaction. The captured instruction (set at turn
// start from the workspace) takes priority because
// it may be fresher than the persisted DB content.
reloadedInstruction := instruction
if reloadedInstruction == "" {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P1 Compaction resurrects cleared instruction/skills after AGENTS.md deletion (Edge Case Analyst P2, Contract Auditor P3, Go Architect Obs)

When fetchWorkspaceContext succeeds and the workspace has no context files (AGENTS.md deleted), line 4634 sets instruction = "". Later if ReloadMessages fires (compaction), reloadedInstruction := instruction is empty (correct), but the fallback at line 5337 calls instructionFromContextFiles(reloadedMsgs) which reads old persisted DB messages — those messages still hold the pre-deletion content because only persistInstructionFiles writes to the DB (not the refresh path). So the deleted AGENTS.md content reappears.

Similarly for skills (line 5343): skillsFromParts(reloadedMsgs) finds old persisted skill parts, overriding the freshly-cleared skills variable.

The PR’s goal is “pick up workspace changes between turns,” but this edge case defeats that for the deletion scenario when compaction fires within the same turn. Consider tracking a boolean like instructionCleared so the reload callback can distinguish “empty because cleared” from “empty because never set.” Alternatively, persist the deletion (write an empty sentinel to the DB) so instructionFromContextFiles returns empty too.

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.

Fixed. Added instructionCleared bool that gets set to true when the deletion branch fires. The ReloadMessages fallback now checks !instructionCleared before reading from persisted DB content:

if reloadedInstruction == "" && !instructionCleared {
    reloadedInstruction = instructionFromContextFiles(reloadedMsgs)
}

This prevents compaction from resurrecting cleared instruction/skills after AGENTS.md deletion.

🤖 Generated by Coder Agents

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

P2 Double injection of instruction after compaction when instruction is acquired mid-turn (Edge Case Analyst, Contract Auditor)

When onChatUpdated sets instruction mid-turn (via create_workspace), instructionInjected stays false. If compaction fires at the end of that step, ReloadMessages injects reloadedInstruction (which equals the captured instruction) into the reloaded prompt via InsertSystem. Then on the next step, PrepareMessages sees instructionInjected == false && instruction != "" and calls InsertSystem again — duplicating AGENTS.md in the prompt.

This only manifests when compaction triggers on the same step as a workspace-creating tool call (conversation near context limit), so it's uncommon but not unrealistic for long-running chats.

Fix: set instructionInjected = true inside this ReloadMessages callback when reloadedInstruction != "".

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.

Fixed in a132d45 — added instructionInjected = true inside the ReloadMessages callback after InsertSystem, so PrepareMessages won't double-inject on the next step.

🤖 Generated by Coder Agents

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.

Good catch — fixed in a132d45. instructionInjected = true is now set inside the ReloadMessages callback right after InsertSystem, so PrepareMessages won't re-inject on the next step.

🤖 Generated by Coder Agents

// Fall back to the captured instruction if the
// reloaded messages don't contain context files
// (e.g. they were compacted away).
reloadedInstruction = instruction
// No fresh instruction was captured at turn start.
// Try to recover from persisted context-file parts
// in the reloaded messages.
reloadedInstruction = instructionFromContextFiles(reloadedMsgs)
}
if reloadedInstruction != "" {
reloadedPrompt = chatprompt.InsertSystem(reloadedPrompt, reloadedInstruction)
}
reloadedPrompt = renderPlanPathPrompt(reloadedPrompt, resolvePlanPathBlock(reloadCtx))
reloadedSkills := skillsFromParts(reloadedMsgs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit reloadedSkills name is misleading after fallback (Style Reviewer)

After the fallback on line 5346, reloadedSkills may hold skills from turn-start, not from reloaded messages. A name like effectiveSkills or a comment on the fallback line would clarify.

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.

Renamed to effectiveSkills.

🤖 Generated by Coder Agents

if len(reloadedSkills) == 0 {
reloadedSkills = skills
}
Expand All @@ -5371,7 +5383,8 @@ func (p *Server) runChat(
)
}
return reloadedPrompt, nil
}, DisableChainMode: func() {
},
DisableChainMode: func() {
chainModeActive = false
},

Expand Down Expand Up @@ -5767,7 +5780,6 @@ func contextFileAgentID(messages []database.ChatMessage) (uuid.UUID, bool) {
return lastID, found
}

// persistInstructionFiles reads instruction files and discovers
// fetchWorkspaceContext retrieves fresh instruction files and
// skills from the workspace agent without persisting. It handles
// agent connection, context configuration fetching, content
Expand Down Expand Up @@ -5883,10 +5895,8 @@ func (p *Server) persistInstructionFiles(
if part.ContextFileContent != "" {
hasContent = true
}
default:
}
}

directory := agent.ExpandedDirectory
if directory == "" {
directory = agent.Directory
Expand Down