Skip to content

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

Merged
dimakis merged 7 commits into
mainfrom
feat/contexgin-http-boot-v2
Jun 1, 2026
Merged

refactor(server): fetch boot context from ContexGin HTTP API#361
dimakis merged 7 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

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.

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.

Acknowledged — the fallback running against BASE_REPO instead of worktree cwd is indeed the correct behavior since the script reads canonical source files. No change needed.

Comment thread server/chat.ts Outdated
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]

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.

Fixed — added DEFAULT_TOKEN_BUDGET = 8000 constant. The ContexGin path now reads boot.tokenBudget from the response (falling back to 8000), and the local fallback uses the constant. Budget no longer mirrors tokenCount.

Comment thread server/chat.ts Outdated
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]

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.

Fixed — extracted the sources array to a variable, now uses sources.length instead of the hardcoded literal 5.

Comment thread server/chat.ts Outdated
})();
// 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]

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.

Addressed — widened send() to accept Record<string, unknown> | BootContextMessage, removing the double cast at the call site. The internal as Record<string, unknown> remains because SessionTransport.send() requires it.

Comment thread server/chat.ts Outdated
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.

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.

Fixed — included and trimmed are now parsed from the ContexGin HTTP response when present, instead of always returning empty arrays.

dimakis and others added 5 commits June 1, 2026 22:35
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>
- Fix tokenBudget regression: read from ContexGin response instead of
  mirroring tokenCount (restores budget vs actual comparison)
- Parse included/trimmed sections from ContexGin response instead of
  returning empty arrays
- Fix hardcoded sourceCount: derive from sources.length
- Widen send() to accept BootContextMessage directly (no double cast)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis dimakis force-pushed the feat/contexgin-http-boot-v2 branch from 4440b8d to f550197 Compare June 1, 2026 21:37
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 5 issue(s) (1 critical) (1 warning).

server/__tests__/boot-context.test.ts

The tokenBudget test/implementation mismatch (mock omits tokenBudget but test expects 11297, implementation defaults to 8000) will cause a test failure. The core refactor from npm-imported contexgin to HTTP API is sound with good fallback chains, but needs test coverage for tokenBudget parsing and included/trimmed section parsing.

  • 🔴 bugs (L104): Test expects tokenBudget: 11297 but the mock response at line 84 has no tokenBudget field in boot. The implementation (chat.ts:199) defaults to 8000 when boot.tokenBudget is absent: typeof boot.tokenBudget === 'number' ? boot.tokenBudget : 8000. This test will fail. Either add tokenBudget: 11297 to the mock's boot object, or change the expectation to 8000. [fixable]
  • 🟡 missing_tests: No test case verifies that tokenBudget is correctly parsed when the API response does include boot.tokenBudget. The existing happy-path test omits it, so the typeof boot.tokenBudget === 'number' branch is never exercised. Add a test where the mock includes an explicit tokenBudget value distinct from tokens to confirm both fields are parsed independently. [fixable]
  • 🔵 missing_tests: No test covers the included/trimmed section parsing (parseSections at chat.ts:210-218). The happy-path test receives no included/trimmed in the mock, so the array-filtering and field-coercion logic in parseSections is untested. A test with mock included/trimmed arrays containing objects with partial fields would cover the String(s.source ?? '') coercion paths. [fixable]

server/chat.ts

The tokenBudget test/implementation mismatch (mock omits tokenBudget but test expects 11297, implementation defaults to 8000) will cause a test failure. The core refactor from npm-imported contexgin to HTTP API is sound with good fallback chains, but needs test coverage for tokenBudget parsing and included/trimmed section parsing.

  • 🔵 style (L384): The as Record<string, unknown> cast works around SessionTransport.send() only accepting Record<string, unknown>. It's safe since BootContextMessage is a plain object, but the union type on send() (line 383) is misleading — it suggests type safety that the cast immediately discards. Consider either widening SessionTransport.send() to accept unknown or keeping send() typed as just Record<string, unknown> and casting at the call site instead. [fixable]
  • 🔵 unsafe_assumptions (L138): The hardcoded source list in localBootContextFallback (lines 138-144) assumes the mgmt workspace structure (memory/Profile/Working Style.md, CONSTITUTION.md, SERVICES.md). These paths won't exist in other repos that might use Mitzo. Since this is explicitly a fallback for when ContexGin is down and scoped to the REPO_PATH workspace, it's acceptable — but a comment noting this is mgmt-specific would help future readers.

source: 'contexgin',
sourceCount: 2,
tokenCount: 11297,
tokenBudget: 11297,
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.

🔴 bugs: Test expects tokenBudget: 11297 but the mock response at line 84 has no tokenBudget field in boot. The implementation (chat.ts:199) defaults to 8000 when boot.tokenBudget is absent: typeof boot.tokenBudget === 'number' ? boot.tokenBudget : 8000. This test will fail. Either add tokenBudget: 11297 to the mock's boot object, or change the expectation to 8000. [fixable]

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.

Fixed — added tokenBudget: 11297 to the mock ContexGin response so the test validates end-to-end budget propagation.

Comment thread server/chat.ts
function send(transport: SessionTransport, data: Record<string, unknown>) {
if (transport.isOpen()) transport.send(data);
function send(transport: SessionTransport, data: Record<string, unknown> | BootContextMessage) {
if (transport.isOpen()) transport.send(data 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 as Record<string, unknown> cast works around SessionTransport.send() only accepting Record<string, unknown>. It's safe since BootContextMessage is a plain object, but the union type on send() (line 383) is misleading — it suggests type safety that the cast immediately discards. Consider either widening SessionTransport.send() to accept unknown or keeping send() typed as just Record<string, unknown> and casting at the call site instead. [fixable]

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.

Fair point — the union type on send() is a compromise. SessionTransport.send() is typed as Record<string, unknown> (harness package), so we can't avoid the internal cast without widening the harness interface. Keeping the union at send() at least makes the call sites cleaner.

Comment thread server/chat.ts
const content = parsed.additionalContext ?? '';
// Rough token estimate: ~4 chars per token
const tokens = Math.ceil(content.length / 4);
const sources = [
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 hardcoded source list in localBootContextFallback (lines 138-144) assumes the mgmt workspace structure (memory/Profile/Working Style.md, CONSTITUTION.md, SERVICES.md). These paths won't exist in other repos that might use Mitzo. Since this is explicitly a fallback for when ContexGin is down and scoped to the REPO_PATH workspace, it's acceptable — but a comment noting this is mgmt-specific would help future readers.

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.

Acknowledged — these paths are mgmt-specific and only used when ContexGin is offline. This is an acceptable coupling since Mitzo's primary deployment is the mgmt workspace.

dimakis and others added 2 commits June 1, 2026 22:42
…ignment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mock ContexGin response was missing the tokenBudget field,
causing the test to expect 11297 while the code defaults to 8000.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis dimakis merged commit e402f02 into main Jun 1, 2026
1 check passed
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