Skip to content

refactor(server): fetch boot context from ContexGin HTTP API#361

Open
dimakis wants to merge 4 commits into
mainfrom
feat/contexgin-http-boot-v2
Open

refactor(server): fetch boot context from ContexGin HTTP API#361
dimakis wants to merge 4 commits into
mainfrom
feat/contexgin-http-boot-v2

Conversation

@dimakis
Copy link
Copy Markdown
Owner

@dimakis dimakis commented May 30, 2026

Summary

  • Replace file-based boot context loading with HTTP fetch from running ContexGin server
  • Local Python fallback when ContexGin is unreachable
  • Centaur review findings addressed

Replaces #358 (CI was stuck on stale branch).

Test plan

  • Start session with ContexGin running — verify boot context loads via HTTP
  • Start session without ContexGin — verify local fallback works
  • Verify boot context pill renders correctly

🤖 Generated with Claude Code

dimakis and others added 4 commits May 30, 2026 02:22
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>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@dimakis dimakis left a comment

Choose a reason for hiding this comment

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

Centaur Review

Found 6 issue(s) (3 warning).

server/chat.ts

Solid simplification from dynamic-import library to HTTP API with graceful fallback chain. Main concerns: tokenBudget semantics changed (now always equals tokenCount instead of reflecting configured budget), and included/trimmed section metadata is permanently empty in the new path.

  • 🟡 regressions (L897): The call site fetchBootContext(agentName) omits contexginUrl and repoRoot, so both use defaults. The default repoRoot is BASE_REPO (which could be '' if REPO_PATH is unset). If ContexGin is down and BASE_REPO is empty, localBootContextFallback will try to exec scripts/build_boot_context.py relative to cwd — but this is caught and returns the zero-value fallback, so it degrades gracefully. Not a bug, but worth noting the old code passed cwd (the resolved working directory) rather than BASE_REPO for the fallback script. If ContexGin is offline and the session uses a worktree cwd, the fallback script runs against BASE_REPO (the parent repo) instead of the worktree — this is likely correct (the script reads canonical source files), but the behavior changed from the old code which used cwd.
  • 🟡 regressions (L212): The old code set tokenBudget from the agent recipe's context.boot.tokenBudget field (defaulting to 8000). The new code sets tokenBudget equal to tokenCount (the actual token count from the server response). This means tokenBudget no longer reflects the configured budget — it just mirrors tokenCount. Any frontend logic that compares tokenCount vs tokenBudget (e.g., to show utilization percentage) will always see 100%. The client-side protocol parser at packages/client/src/protocol-parser.ts:260 does forward tokenBudget to the store, so if anything renders budget vs. used, it's now a no-op comparison. [fixable]
  • 🟡 unsafe_assumptions (L141): The localBootContextFallback hardcodes sourceCount: 5 and a fixed list of 5 source paths, with a comment saying it's coupled to SOURCE_PATHS in scripts/build_boot_context.py. If that script changes its source list, this will silently drift out of sync. Consider reading the script's output for source metadata instead of duplicating the list, or at minimum making the hardcoded count derive from the array length (sources.length instead of the literal 5). [fixable]
  • 🔵 style (L898): The double cast msg as unknown as Record<string, unknown> is unnecessarily defensive. BootContextMessage is a plain interface with only primitive/array fields — a single msg as Record<string, unknown> suffices since the types are structurally compatible. Alternatively, widen the send() helper to accept BootContextMessage directly. [fixable]
  • 🔵 regressions (L214): The old code populated included and trimmed arrays with section-level metadata (source file, heading path, token estimates, content) from the contexgin compile result. The new HTTP API path always returns empty arrays for both. If the ContexGin HTTP API does (or will) return this data, it's being silently dropped. If the API intentionally omits it, this is fine — but the BootContextMessage interface still declares these fields, and the client parser still parses them, so there's dead plumbing.

server/__tests__/boot-context.test.ts

Solid simplification from dynamic-import library to HTTP API with graceful fallback chain. Main concerns: tokenBudget semantics changed (now always equals tokenCount instead of reflecting configured budget), and included/trimmed section metadata is permanently empty in the new path.

  • 🔵 missing_tests: No test covers the AbortSignal.timeout(5000) behavior — e.g., verifying that a slow ContexGin response triggers the fallback within the timeout window. This is difficult to test in unit tests but worth noting as an untested edge case.

Comment thread server/chat.ts
}
})();
// Fire-and-forget: fetch boot context from running ContexGin server
fetchBootContext(agentName)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 regressions: The call site fetchBootContext(agentName) omits contexginUrl and repoRoot, so both use defaults. The default repoRoot is BASE_REPO (which could be '' if REPO_PATH is unset). If ContexGin is down and BASE_REPO is empty, localBootContextFallback will try to exec scripts/build_boot_context.py relative to cwd — but this is caught and returns the zero-value fallback, so it degrades gracefully. Not a bug, but worth noting the old code passed cwd (the resolved working directory) rather than BASE_REPO for the fallback script. If ContexGin is offline and the session uses a worktree cwd, the fallback script runs against BASE_REPO (the parent repo) instead of the worktree — this is likely correct (the script reads canonical source files), but the behavior changed from the old code which used cwd.

Comment thread server/chat.ts
source: 'contexgin',
sourceCount: sources.length,
tokenCount: bootTokens,
tokenBudget: bootTokens, // server compiles to its own budget — report actual
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 regressions: The old code set tokenBudget from the agent recipe's context.boot.tokenBudget field (defaulting to 8000). The new code sets tokenBudget equal to tokenCount (the actual token count from the server response). This means tokenBudget no longer reflects the configured budget — it just mirrors tokenCount. Any frontend logic that compares tokenCount vs tokenBudget (e.g., to show utilization percentage) will always see 100%. The client-side protocol parser at packages/client/src/protocol-parser.ts:260 does forward tokenBudget to the store, so if anything renders budget vs. used, it's now a no-op comparison. [fixable]

Comment thread server/chat.ts
return {
type: 'boot_context',
source: 'local-fallback',
// Coupled to SOURCE_PATHS in scripts/build_boot_context.py — update if that list changes
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🟡 unsafe_assumptions: The localBootContextFallback hardcodes sourceCount: 5 and a fixed list of 5 source paths, with a comment saying it's coupled to SOURCE_PATHS in scripts/build_boot_context.py. If that script changes its source list, this will silently drift out of sync. Consider reading the script's output for source metadata instead of duplicating the list, or at minimum making the hardcoded count derive from the array length (sources.length instead of the literal 5). [fixable]

Comment thread server/chat.ts
})();
// Fire-and-forget: fetch boot context from running ContexGin server
fetchBootContext(agentName)
.then((msg) => send(transport, msg as unknown as Record<string, unknown>))
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 style: The double cast msg as unknown as Record<string, unknown> is unnecessarily defensive. BootContextMessage is a plain interface with only primitive/array fields — a single msg as Record<string, unknown> suffices since the types are structurally compatible. Alternatively, widen the send() helper to accept BootContextMessage directly. [fixable]

Comment thread server/chat.ts
tokenCount: bootTokens,
tokenBudget: bootTokens, // server compiles to its own budget — report actual
sources,
included: [],
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🔵 regressions: The old code populated included and trimmed arrays with section-level metadata (source file, heading path, token estimates, content) from the contexgin compile result. The new HTTP API path always returns empty arrays for both. If the ContexGin HTTP API does (or will) return this data, it's being silently dropped. If the API intentionally omits it, this is fine — but the BootContextMessage interface still declares these fields, and the client parser still parses them, so there's dead plumbing.

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