refactor(server): fetch boot context from ContexGin HTTP API#358
refactor(server): fetch boot context from ContexGin HTTP API#358dimakis wants to merge 3 commits into
Conversation
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]
dimakis
left a comment
There was a problem hiding this comment.
Centaur Review
Found 7 issue(s) (2 warning).
server/chat.ts
Clean refactoring from npm-imported contexgin to HTTP API with solid fallback chain and good test coverage. Main concerns: missing .catch() on fire-and-forget promise, and tokenBudget now always equals tokenCount which may be a behavioral regression for the frontend budget display.
- 🟡 bugs (L880): Unhandled rejection risk:
fetchBootContextnever rejects, butsend(transport, msg)inside.then()could throw (e.g., iftransport.send()throws on a closed/errored socket). Add.catch()to match the pattern used on the adjacent line (capturePromptComparison(...).catch(() => {})).[fixable] - 🟡 regressions (L205): The old code reported a separate
tokenBudgetread from the agent recipe YAML (.agents/mitzo-conversational.yaml), which could differ from actual token count. The new code setstokenBudget: bootTokens— always equal totokenCount. If the frontend usestokenBudgetto show budget vs. actual usage, it will now always show 100% utilization. Verify the frontendBootContextPilldoesn't rely on budget != count.[fixable] - 🔵 regressions (L200): The old code populated
includedandtrimmedarrays with section-level metadata (source, heading, tokens, content) from the contexgin compile result. The new HTTP API path always sends empty arrays for both. If ContexGin's HTTP response includes this data, it's being silently dropped. If it doesn't, theBootContextMessageinterface could be simplified to makeincluded/trimmedoptional.[fixable] - 🔵 unsafe_assumptions (L135): Hardcoded
sourceCount: 5and a hardcoded list of 5 source paths inlocalBootContextFallback. If the Python script'sSOURCE_PATHSchanges, this will silently drift. Consider parsing the actual sources from the script output if it provides them, or adding a comment noting this coupling.[fixable] - 🔵 unsafe_assumptions (L167):
agentNameis interpolated directly into the URL path without encoding. Currently hardcoded to'mitzo-conversational'at the call site, so safe in practice. But if the function is reused with user-supplied names containing slashes or special characters, the URL would be malformed. ConsiderencodeURIComponent(agentName)for defense in depth.[fixable] - 🔵 style (L198): Redundant cast:
(boot.content as string)is already inside atypeof boot.content === 'string'guard, so theas stringis unnecessary.[fixable]
server/__tests__/boot-context.test.ts
Clean refactoring from npm-imported contexgin to HTTP API with solid fallback chain and good test coverage. Main concerns: missing .catch() on fire-and-forget promise, and tokenBudget now always equals tokenCount which may be a behavioral regression for the frontend budget display.
- 🔵 missing_tests: No test for when
fetchsucceeds butres.json()throws (e.g., invalid JSON body). The implementation's outer catch handles it, but an explicit test would document this contract.[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: Unhandled rejection risk: fetchBootContext never rejects, but send(transport, msg) inside .then() could throw (e.g., if transport.send() throws on a closed/errored socket). Add .catch() to match the pattern used on the adjacent line (capturePromptComparison(...).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: The old code reported a separate tokenBudget read from the agent recipe YAML (.agents/mitzo-conversational.yaml), which could differ from actual token count. The new code sets tokenBudget: bootTokens — always equal to tokenCount. If the frontend uses tokenBudget to show budget vs. actual usage, it will now always show 100% utilization. Verify the frontend BootContextPill doesn't rely on budget != count. [fixable]
|
|
||
| const fullMarkdown = typeof boot.content === 'string' ? (boot.content as string) : undefined; | ||
|
|
||
| return { |
There was a problem hiding this comment.
🔵 regressions: The old code populated included and trimmed arrays with section-level metadata (source, heading, tokens, content) from the contexgin compile result. The new HTTP API path always sends empty arrays for both. If ContexGin's HTTP response includes this data, it's being silently dropped. If it doesn't, the BootContextMessage interface could be simplified to make included/trimmed optional. [fixable]
| return { | ||
| type: 'boot_context', | ||
| source: 'local-fallback', | ||
| sourceCount: 5, // hardcoded source count from build_boot_context.py SOURCE_PATHS |
There was a problem hiding this comment.
🔵 unsafe_assumptions: Hardcoded sourceCount: 5 and a hardcoded list of 5 source paths in localBootContextFallback. If the Python script's SOURCE_PATHS changes, this will silently drift. Consider parsing the actual sources from the script output if it provides them, or adding a comment noting this coupling. [fixable]
| repoRoot: string = BASE_REPO, | ||
| ): Promise<BootContextMessage> { | ||
| try { | ||
| const url = `${contexginUrl}/api/agents/${agentName}/context`; |
There was a problem hiding this comment.
🔵 unsafe_assumptions: agentName is interpolated directly into the URL path without encoding. Currently hardcoded to 'mitzo-conversational' at the call site, so safe in practice. But if the function is reused with user-supplied names containing slashes or special characters, the URL would be malformed. Consider encodeURIComponent(agentName) for defense in depth. [fixable]
| .filter((s): s is string => typeof s === 'string') | ||
| .map((s) => ({ path: s, kind: 'reference' })); | ||
|
|
||
| const fullMarkdown = typeof boot.content === 'string' ? (boot.content as string) : undefined; |
There was a problem hiding this comment.
🔵 style: Redundant cast: (boot.content as string) is already inside a typeof boot.content === 'string' guard, so the as string is unnecessary. [fixable]
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>
When ContexGin is unreachable, fall back to `build_boot_context.py --json` which reads the canonical source files directly (Profile, Constitution, Services) without any server dependency. This is the deterministic "old way" — always works, no budget drift. Fallback chain: ContexGin HTTP → build_boot_context.py → zero-value. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add .catch() on fire-and-forget boot context promise - Use encodeURIComponent for agent name in URL path - Remove redundant `as string` cast inside type guard - Add coupling comment on hardcoded SOURCE_PATHS list - Add test for malformed JSON response fallback - Wrap env var test cleanup in try/finally Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
330bb8d to
a1c5508
Compare
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