Skip to content

Fix six context-engineering defects found in code review - #453

Merged
jamesrochabrun merged 1 commit into
mainfrom
jroch-feedback
Aug 14, 2026
Merged

Fix six context-engineering defects found in code review#453
jamesrochabrun merged 1 commit into
mainfrom
jroch-feedback

Conversation

@jamesrochabrun

@jamesrochabrun jamesrochabrun commented Aug 14, 2026

Copy link
Copy Markdown
Owner

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 main before fixing, and every new test was confirmed to fail against the pre-fix code.

# Finding Status
1 Switching repositories keeps the previous project's context attached Fixed
2 First personal-scope save publishes no JSON index Fixed
3 Binary project files reported as "missing" instead of attached Fixed
4 Payload store directory choice Not applicable here — two related issues fixed
5 Stale "Preview truncated" banner after clearing the selection Fixed
6 Choosing a saved set in the builder downgrades it to ad-hoc Fixed
7 No escaping of titles/paths in assembled-block attributes Fixed

What was wrong, and what changed

1 — Repo switch leaked context. Nothing cleared attachedContextProfile / pendingContextSelection / contextRemovedByUser when the repository changed; only reset() and removeAttachedContextProfile() did, and neither selectRepository() nor clearSelectedRepository() 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 a didSet on selectedRepository, which covers all seven assignment sites including the direct ones in preselectRepository.

2 — First personal save published nothing. ContextProfile.init forces projectPath = "" for personal scope and getProjectPathsWithContextProfiles() filters empty paths, so the very common first-run case (first set saved as "Global (all projects)") ran neither republish branch. save now takes the caller's real project path, mirroring the asymmetry already present in delete(id:projectPath:).

3 — Binary files reported missing. loadFiles funneled every readTextFile failure into missing and never populated attachments, contradicting both ContextFileLoadResult.attachments' own doc comment and the picker UI. A repo docs/spec.pdf rendered 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, mirroring maxExternalInlineBytes). Since attachment paths are handed to the agent unread, the oversized branch does its own project-root confinement check — a traversal path stays missing rather than becoming an attachment.

5 — Stale truncation banner. The empty-selection early return in refreshPreview() reset only assembledPreview, 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(), so applyCuratedContext populated pendingContextSelection, which outranks attachedContextProfile — the chip lost the set's name and its "default" badge. .launch now also reports the source profile when the selection is unmodified, and ContextLaunchSection calls attachContextProfile(_:), 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 the context-*.md files the store wrote.
  • ContextPayloadStore.defaultDirectoryURL built the Application Support path directly instead of resolving through AgentHubApplicationSupport.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

  • 84 tests across 8 context suites pass (ContextAssembler, ContextFileLoader, ContextPayloadStore, ContextProfileService, ContextBuilderViewModel, MultiSessionLaunchContext, ProjectContextViewModel, ContextProfileStore).
  • Negative verification: with each behavioral fix surgically reverted (APIs kept so everything still compiled), all 18 new assertions failed, reproducing the reported scenarios exactly — repo A's profile still attached after switching to B, indexStore.read → nil, missing == ["docs/spec.pdf", "gone.md"], a non-nil previewTruncatedByteCount after clearing, and the unescaped <snippet title="spec" mode="raw">.
  • Full core suite: 23 failures on this branch vs 18 on a stashed clean-tree baseline. The five extra are in WorktreeGenerationProgressCoordinatorTests and SimulatorHotReloadControllerTests (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.
  • Production session_metadata.sqlite verified byte-identical before and after every run.

Not changed

ProjectContextViewModel.refreshEstimates still uses fileMetrics, 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 with aliasPaths anywhere — the documented worktree-alias mirroring is still unimplemented, and is left for a follow-up.

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
jamesrochabrun merged commit 6ba53af into main Aug 14, 2026
1 check passed
@jamesrochabrun
jamesrochabrun deleted the jroch-feedback branch August 14, 2026 07:30
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).
@jamesrochabrun jamesrochabrun mentioned this pull request Aug 14, 2026
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