refactor(server): fetch boot context from ContexGin HTTP API#358
refactor(server): fetch boot context from ContexGin HTTP API#358dimakis wants to merge 1 commit into
Conversation
Replace the inline `import('contexgin')` library call with an HTTP fetch
to the running ContexGin server (`GET /api/agents/:name/context`).
The old approach imported contexgin as a library, read the agent recipe
from the worktree (which could be stale or missing), used the legacy
`compile()` pipeline, and fell back to a hardcoded 8k token budget.
The running server already compiles correctly with the recipe's 12k
budget — this change makes Mitzo use it.
- Extract `fetchBootContext()` as a testable, exported function
- Use `CONTEXGIN_URL` env var (already configured in .env)
- 5s timeout with graceful local-fallback on any error
- Remove ~140 lines of library import, recipe parsing, and response
mapping code
- Add 7 focused tests covering success, failure, and edge cases
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 5 issue(s) (2 warning).
server/chat.ts
Clean refactor from library import to HTTP API. Main concern is a missing .catch() on the fire-and-forget promise (unhandled rejection risk), and a UI regression where the included/trimmed section detail and budget-vs-actual distinction are silently lost from BootContextPill.
- 🟡 bugs (L835): Missing
.catch()on fire-and-forget promise chain. Iftransport.send()throws after the async gap (e.g., transport closed between fetch and send), this produces an unhandled promise rejection. The adjacentcapturePromptComparisoncall correctly uses.catch(() => {}). Fix:fetchBootContext('mitzo-conversational').then((msg) => send(transport, msg)).catch(() => {});[fixable] - 🟡 regressions (L160):
includedandtrimmedare now always empty arrays. The old code populated these fromcompiled.included/compiled.trimmedwith section-level detail (source, heading, token count, content). The frontend'sBootContextPillrenders expandable 'Included (N)' and 'N sections trimmed' sections from these fields — those UI sections will never appear now. If intentional (ContexGin manages compilation internally), these fields onBootContextMessageare effectively dead code and should be removed from the interface to avoid confusion. - 🔵 regressions (L160):
tokenBudgetis now always equal totokenCount(both set tobootTokens). The old code distinguished configured budget (from agent recipe YAML, default 8000) from actual usage. The frontend displays these as 'X.Xk / X.Xk' (count / budget) which is now always identical, making the budget half of the label redundant. In the fallback case, budget changed from 8000 to 0, so the pill shows '0 / 0' instead of '0 / 8.0k'.
server/__tests__/boot-context.test.ts
Clean refactor from library import to HTTP API. Main concern is a missing .catch() on the fire-and-forget promise (unhandled rejection risk), and a UI regression where the included/trimmed section detail and budget-vs-actual distinction are silently lost from BootContextPill.
- 🔵 missing_tests: Missing test for malformed JSON response — when
res.json()throws (e.g., body is not valid JSON despiteok: true), the outer catch block should returnFALLBACK_BOOT_CONTEXT. This edge case is handled correctly by the implementation but not verified by tests.[fixable] - 🔵 unsafe_assumptions (L167): Env var cleanup in the last test is not wrapped in
try/finally. If an assertion fails before line 180,process.env.CONTEXGIN_URLleaks its test value into subsequent tests. Usetry/finallyor Vitest'svi.stubEnv()for safer cleanup.[fixable]
| } | ||
| })(); | ||
| // Fire-and-forget: fetch boot context from running ContexGin server | ||
| fetchBootContext('mitzo-conversational').then((msg) => send(transport, msg)); |
There was a problem hiding this comment.
🟡 bugs: Missing .catch() on fire-and-forget promise chain. If transport.send() throws after the async gap (e.g., transport closed between fetch and send), this produces an unhandled promise rejection. The adjacent capturePromptComparison call correctly uses .catch(() => {}). Fix: fetchBootContext('mitzo-conversational').then((msg) => send(transport, msg)).catch(() => {}); [fixable]
| source: 'contexgin', | ||
| sourceCount: sources.length, | ||
| tokenCount: bootTokens, | ||
| tokenBudget: bootTokens, // server compiles to its own budget — report actual |
There was a problem hiding this comment.
🟡 regressions: included and trimmed are now always empty arrays. The old code populated these from compiled.included/compiled.trimmed with section-level detail (source, heading, token count, content). The frontend's BootContextPill renders expandable 'Included (N)' and 'N sections trimmed' sections from these fields — those UI sections will never appear now. If intentional (ContexGin manages compilation internally), these fields on BootContextMessage are effectively dead code and should be removed from the interface to avoid confusion.
| source: 'contexgin', | ||
| sourceCount: sources.length, | ||
| tokenCount: bootTokens, | ||
| tokenBudget: bootTokens, // server compiles to its own budget — report actual |
There was a problem hiding this comment.
🔵 regressions: tokenBudget is now always equal to tokenCount (both set to bootTokens). The old code distinguished configured budget (from agent recipe YAML, default 8000) from actual usage. The frontend displays these as 'X.Xk / X.Xk' (count / budget) which is now always identical, making the budget half of the label redundant. In the fallback case, budget changed from 8000 to 0, so the pill shows '0 / 0' instead of '0 / 8.0k'.
| expect(result.sourceCount).toBe(2); | ||
| }); | ||
|
|
||
| it('uses default URL from env when not provided', async () => { |
There was a problem hiding this comment.
🔵 unsafe_assumptions: Env var cleanup in the last test is not wrapped in try/finally. If an assertion fails before line 180, process.env.CONTEXGIN_URL leaks its test value into subsequent tests. Use try/finally or Vitest's vi.stubEnv() for safer cleanup. [fixable]
Summary
import('contexgin')library call with HTTP fetch to running ContexGin server (GET /api/agents/:name/context)fetchBootContext()as a testable function, reducing ~140 lines of library import + recipe parsing + response mapping to a single HTTP callRoot cause
chat.tsimportedcontexginas a Node library and called the legacycompile()function withDEFAULT_TOKEN_BUDGET = 8000. It attempted to readtokenBudgetfrom.agents/mitzo-conversational.yamlincwd(the worktree), which often didn't have the file or had a stale version. Meanwhile, the running ContexGin server atCONTEXGIN_URLwas already compiling correctly with 12k. Mitzo was bypassing its own infrastructure.Test plan
contexgin-smoke,context-baseline, andprompt-comparetests still pass (21/21)🤖 Generated with Claude Code