Fix six context-engineering defects found in code review - #453
Merged
Conversation
Review of the AirChatUI port of #446/#448 (airbnb/apps#368207) surfaced seven issues, six of which apply to this repo verbatim. Each was reproduced against the current code before fixing, and every new test fails without its fix. 1. Switching repositories kept the previous project's context attached. Nothing cleared the context state when `selectedRepository` changed, so repo A's set stayed attached, resolved A's relative paths against B (chip showed every file missing), the launch block carried `(missing)` entries, and B's own default never attached. Removing the chip in A also suppressed B's default for the rest of the session. Cleared via a `didSet` on `selectedRepository`, which covers every assignment site. 2. A first personal-scope save published no JSON index. `ContextProfile.init` forces an empty `projectPath` for personal scope and `getProjectPathsWithContextProfiles()` filters empty paths, so a user whose first context set is "Global (all projects)" got no CLI-visible mirror at all. `save` now takes the caller's project path, mirroring `delete(id:projectPath:)`. 3. Binary project files were reported missing instead of attached. `loadFiles` funneled every read failure into `missing` and never populated `attachments`, so a repo PDF or PNG rendered struck through and the agent was told an existing file did not exist. Non-UTF-8 and oversized project files now classify as attachments, matching the documented contract and the external-file path. Project files also gain the inline size cap they lacked. 5. A stale "Preview truncated" banner survived clearing the selection, because the empty-selection early return reset only `assembledPreview`. 6. Choosing a saved set in the builder downgraded it to ad-hoc. Launch mode handed back only the selection, so the chip lost the set's name and default badge. It now reports the source profile when the selection is unmodified, and the launcher calls the previously unused `attachContextProfile`. 7. Snippet titles and paths were interpolated into attribute positions unescaped, so a title like `spec" mode="raw` broke the block structure. Finding 4 (payload directory) was fork-specific, but two upstream points held: `pruneStalePayloads()` deleted any file in the directory rather than only the `context-*.md` files it wrote, and `defaultDirectoryURL` built the Application Support path directly instead of resolving through `AgentHubApplicationSupport.baseDirectoryURL` — meaning payload writes escaped the test sandbox. Both are fixed. Verified: 84 tests across 8 context suites pass; each new test was confirmed to fail against the pre-fix code. The full core suite's remaining failures are timing flakes present on a clean-tree baseline (18 there, and the five extra ones pass in isolation).
jamesrochabrun
added a commit
that referenced
this pull request
Aug 14, 2026
A code review of the context-engineering work from #446/#448 surfaced seven issues, six of which apply to this repo. Each was reproduced against the current code before fixing, and every new test fails without its fix. 1. Switching repositories kept the previous project's context attached. Nothing cleared the context state when `selectedRepository` changed, so repo A's set stayed attached, resolved A's relative paths against B (chip showed every file missing), the launch block carried `(missing)` entries, and B's own default never attached. Removing the chip in A also suppressed B's default for the rest of the session. Cleared via a `didSet` on `selectedRepository`, which covers every assignment site. 2. A first personal-scope save published no JSON index. `ContextProfile.init` forces an empty `projectPath` for personal scope and `getProjectPathsWithContextProfiles()` filters empty paths, so a user whose first context set is "Global (all projects)" got no CLI-visible mirror at all. `save` now takes the caller's project path, mirroring `delete(id:projectPath:)`. 3. Binary project files were reported missing instead of attached. `loadFiles` funneled every read failure into `missing` and never populated `attachments`, so a repo PDF or PNG rendered struck through and the agent was told an existing file did not exist. Non-UTF-8 and oversized project files now classify as attachments, matching the documented contract and the external-file path. Project files also gain the inline size cap they lacked. 5. A stale "Preview truncated" banner survived clearing the selection, because the empty-selection early return reset only `assembledPreview`. 6. Choosing a saved set in the builder downgraded it to ad-hoc. Launch mode handed back only the selection, so the chip lost the set's name and default badge. It now reports the source profile when the selection is unmodified, and the launcher calls the previously unused `attachContextProfile`. 7. Snippet titles and paths were interpolated into attribute positions unescaped, so a title like `spec" mode="raw` broke the block structure. Finding 4 concerned a directory choice that does not apply here, but two related points did: `pruneStalePayloads()` deleted any file in the directory rather than only the `context-*.md` files it wrote, and `defaultDirectoryURL` built the Application Support path directly instead of resolving through `AgentHubApplicationSupport.baseDirectoryURL` — meaning payload writes escaped the test sandbox. Both are fixed. Verified: 84 tests across 8 context suites pass; each new test was confirmed to fail against the pre-fix code. The full core suite's remaining failures are timing flakes present on a clean-tree baseline (18 there, and the five extra ones pass in isolation).
Closed
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.
Addresses findings from a code review of the context-engineering work in #446/#448. Seven issues were raised; six apply to this repo. Each was reproduced against current
mainbefore fixing, and every new test was confirmed to fail against the pre-fix code.What was wrong, and what changed
1 — Repo switch leaked context. Nothing cleared
attachedContextProfile/pendingContextSelection/contextRemovedByUserwhen the repository changed; onlyreset()andremoveAttachedContextProfile()did, and neitherselectRepository()norclearSelectedRepository()called them. Repo A's set stayed attached,refreshContextSummary()resolved A's project-relative paths against B (chip showed every file missing), the launch prepended a<context>block of(missing)entries, and B's own default never attached. The inverse stuck too: removing the chip in A suppressed B's default for the session. Now cleared from adidSetonselectedRepository, which covers all seven assignment sites including the direct ones inpreselectRepository.2 — First personal save published nothing.
ContextProfile.initforcesprojectPath = ""for personal scope andgetProjectPathsWithContextProfiles()filters empty paths, so the very common first-run case (first set saved as "Global (all projects)") ran neitherrepublishbranch.savenow takes the caller's real project path, mirroring the asymmetry already present indelete(id:projectPath:).3 — Binary files reported missing.
loadFilesfunneled everyreadTextFilefailure intomissingand never populatedattachments, contradicting bothContextFileLoadResult.attachments' own doc comment and the picker UI. A repodocs/spec.pdfrendered struck through and the agent was told an existing file didn't exist. Non-UTF-8 and oversized project files now classify as attachments. Project files also gain the inline size cap they previously lacked entirely (maxProjectInlineBytes, mirroringmaxExternalInlineBytes). Since attachment paths are handed to the agent unread, the oversized branch does its own project-root confinement check — a traversal path staysmissingrather than becoming an attachment.5 — Stale truncation banner. The empty-selection early return in
refreshPreview()reset onlyassembledPreview, so "Preview truncated — the full block is 4.2 MB" rendered directly above "Nothing selected."6 — Saved set downgraded to ad-hoc. Launch mode handed back only
currentSelection(), soapplyCuratedContextpopulatedpendingContextSelection, which outranksattachedContextProfile— the chip lost the set's name and its "default" badge..launchnow also reports the source profile when the selection is unmodified, andContextLaunchSectioncallsattachContextProfile(_:), which previously had no production callers.7 — Unescaped attributes.
&,<,>and"are now escaped in<file path=…>and<snippet title=…>values.On finding 4
The directory relocation itself doesn't apply here (AgentHub legitimately owns
Application Support/AgentHub), but two related problems in that code are real and fixed:pruneStalePayloads()deleted any file in the directory older than 7 days, not just thecontext-*.mdfiles the store wrote.ContextPayloadStore.defaultDirectoryURLbuilt the Application Support path directly instead of resolving throughAgentHubApplicationSupport.baseDirectoryURL, violating the repo's own persistent-state rule. This meant payload writes escaped the test sandbox — verified concretely: against the old code the new test resolves to the real user Application Support directory while the sandbox base is a per-process temp directory. Given the prior incident where a test run overwrote real workspace state, this one mattered.Testing
ContextAssembler,ContextFileLoader,ContextPayloadStore,ContextProfileService,ContextBuilderViewModel,MultiSessionLaunchContext,ProjectContextViewModel,ContextProfileStore).indexStore.read → nil,missing == ["docs/spec.pdf", "gone.md"], a non-nilpreviewTruncatedByteCountafter clearing, and the unescaped<snippet title="spec" mode="raw">.WorktreeGenerationProgressCoordinatorTestsandSimulatorHotReloadControllerTests(announcement/debounce timing) and all 29 of their tests pass in isolation; one baseline failure conversely passed here, confirming the set is nondeterministic. These are the known parallel-execution flakes, unrelated to this diff.session_metadata.sqliteverified byte-identical before and after every run.Not changed
ProjectContextViewModel.refreshEstimatesstill usesfileMetrics, so a binary file contributes 0 tokens to the Project Details estimate rather than its path length. That surface only displays an estimate (it never renders "missing") and isn't the picker described in the review, so it's left out of scope. Separately,republishIndex(forProjectPath:aliasPaths:)is never called withaliasPathsanywhere — the documented worktree-alias mirroring is still unimplemented, and is left for a follow-up.