Skip to content

Add offline configuration migration and stream reasoning in realtime mode - #66

Merged
maiphucgiang merged 6 commits into
mainfrom
feat/config-migration
Oct 17, 2026
Merged

maiphucgiang merged 6 commits into
mainfrom
feat/config-migration

Conversation

@maiphucgiang

@maiphucgiang maiphucgiang commented Oct 17, 2026 •

Copy link
Copy Markdown
Owner

Summary

Configuration store and offline migration

  • converter.py config migrate|status moves runtime settings from .env or the process environment into control.sqlite3 without editing the user's file. It resolves the env file first, then the data directory that file declares (CODEBUDDY_AUTH_DIR), and accepts --env-file, --no-env, --data-dir, --db, --dry-run, --keys and --force.
  • The target is verified read-only before any write: SQLite header, user_version=2, control/runtime_state/gateway_secrets, readable payload.settings and quick_check. A missing path, a foreign SQLite file, the audit logs.sqlite3, an older schema or a corrupt database is refused without creating or modifying anything.
  • Only runtime-scoped keys migrate; deployment keys (API key, listener, credential/import paths) and sensitive values never move. Unknown --keys are rejected, and a value that already matches the store does not bump the revision. config status shares the same resolution and identity code and never writes.
  • A running instance publishes a service_lease heartbeat every 30s (stale after 90s); migrate also probes BEGIN IMMEDIATE before writing. Either signal refuses the run with the live pid unless --force is given. Lease release is installed as a SIGTERM/SIGINT handler because uvicorn restores the default handler and re-raises the signal, so a finally block never runs.
  • Settings and config_notice_muted commit in one revision through ControlStore.update_settings, so a failed validation or a revision race leaves both untouched.
  • SCHEMA now records env_scope (deployment/runtime/ui/none) and lockable: deployment keys stay authoritative through .env/CLI, language is a hot preference with no environment mapping and is never locked, and the hidden config_notice_muted stores the one-time notice state.

Dashboard notice

  • GET /admin/dashboard reports runtime keys still supplied by the environment; the banner lists them with a copyable python3 converter.py config migrate command and a documentation link.
  • "Do not remind me" opens a confirmation drawer explaining that dismissing changes no settings, then calls POST /admin/config-notice/dismiss (admin auth + CSRF, idempotent, writes a config.notice_dismissed audit event). Migrating or dismissing sets the same flag, so later .env additions never re-open the notice.

Interface language

  • The language moves from browser-only state into the store: the WebUI reads the stored value after login, writes it on every switch, and keeps localStorage only as the pre-login fallback (login page cannot read the store). serverPreference maps auto/unknown to the system language.
  • --lang zh|en|auto writes the interface language on serve, login and config migrate; config status is read-only and uses --lang only for its own output. A new app/i18n.py provides the zh/en CLI catalog.

Refresh-token import

  • POST /admin/credentials/refresh-import accepts 1–50 tokens behind the existing admin authentication and a shared lock, validates the endpoint, skips duplicates and reports per-token failures.
  • auth_oauth.refresh_tokens exchanges a token without any access token, reads the uid from the JWT subject, refreshes against the selected profile and fetches the account block so enterprise tenants survive; a uid mismatch is rejected. Successes go through the normal credential writer.
  • The import drawer gains a "refresh token" tab with an endpoint selector and one token per line, reusing the existing result list.

Realtime reasoning

  • realtime now forwards reasoning deltas as they arrive; compatible keeps the historical single-block replay. Responses and Anthropic still emit one logical thinking item/block with incremental deltas.
  • The wrapping helper is skipped only for realtime requests; its docstring now states the compatible-only behaviour.

Layout

  • The credential row menu renders through a portal with viewport-clamped fixed coordinates and repositions on scroll/resize; local position: fixed resolved against a backdrop-filtered panel and left the screen.
  • Settings tabs, the mobile navigation and selects wrap or cap their width, the expanded trend table scrolls, and e2e/layout.spec.ts locks the 390px behaviour.

Deployment and images

  • Images publish the release version, edge from main and latest from release tags; pull-request builds keep an unpublished type=ref,event=pr tag so the build still has an identity.
  • docker-compose.yml loads the whole .env through env_file with required: false, so a missing file still boots from store settings; only CODEBUDDY_AUTH_DIR and CODEBUDDY2API_ALLOW_OPEN_NOAUTH remain fixed, and the default image tag is latest.
  • The image itself passes --desensitize; docs/clients.md and its Chinese counterpart follow.
  • requirements.in is removed: pyproject.toml is the single source for direct dependencies and the build-lock test compares uv.lock against it.
  • Documentation is updated in both languages: .env.example migration note, deployment (host and container commands), advanced configuration precedence, WebUI and clients pages.

Commits

  • 63ca33c Simplify image tags, Compose environment and dependency locks
  • 84b1fb4 Keep menus and tab rows inside the viewport
  • 9613159 Import credentials from refresh tokens
  • 66e5205 Stream reasoning in realtime mode
  • ff54647 Add offline configuration migration for runtime settings
  • 4a0677e Keep a build-only tag on pull requests

Validation

  • Backend: every tests/test_*.py file exits 0 (70 files), including the new tests/test_config_migration.py (target identity, lease refusal, dry-run, key filters, language transaction, invalid-value atomicity) and updated test_environment_config.py, test_admin_api.py, test_identity_sync.py.
  • Web: vp test run 19 files / 153 tests, including configNotice.test.tsx and refreshImport.test.tsx; vp check formatting, lint and type checks clean; full Playwright suite 18 passed including the new e2e/layout.spec.ts.
  • Live instance: config status reported the running lease; config migrate was refused while the service was up; after stopping, migration wrote max_images/stream_mode, kept host in .env, and the WebUI showed source=management, locked=false.
  • Live reasoning check with deepseek-v4.1-flash and stream_mode=realtime: 73 reasoning deltas starting at 1.741s, then 38 content deltas starting at 2.263s (previously one reasoning block arrived with the first content).
  • --lang en saved language=en without locking it; a later WebUI switch back succeeded; config status --lang only changed that invocation's output.
  • CI on the final head: test, web, image and all three CodeQL analyses passed; CodeQL results for the exact PR head are 0.

Three deployment concerns had drifted apart from how the project is actually
run: images published extra floating tags, Compose repeated runtime settings as
per-variable fallbacks, and requirements.in existed only to mirror pyproject.

- .github/workflows/docker.yml publishes exactly the release version, `edge`
  for main and `latest` for release tags; PR and sha tags are gone, so pulled
  tags always name a supported build.
- docker-compose.yml loads the whole .env through env_file and fixes only the
  container paths, so a runtime key is configured once and a commented line
  stays WebUI-editable instead of being locked by a fallback default. The image
  defaults to `latest`, and the image passes --desensitize itself.
- requirements.in is removed: pyproject.toml is the single source for direct
  dependencies, and the build-lock test compares uv.lock against it directly.
- tests/test_deployment.py follows the new Compose shape and the image-tag test
  pins the three published tags.
Narrow layouts clipped interactive content: the settings tab row pushed the page
16px sideways at 390px, a credential row menu opened inside the scrolling table
wrapper and disappeared below it, the expanded trend data table was cut off, and
the mobile navigation hid its last entries behind an unmarked scroll area.

- The row menu renders through a portal with viewport-clamped fixed coordinates,
  repositioning on scroll and resize; a panel's backdrop-filter otherwise made a
  locally fixed menu resolve against the panel and leave the screen.
- `.tabs` and the mobile nav wrap instead of overflowing, `.chartData` scrolls
  its expanded table, and selects cap their width at the container.
- web/e2e/layout.spec.ts locks both regressions: no horizontal page or tab
  overflow at 390px, and the last row menu stays portaled and fully visible.
A refresh token plus its endpoint identifies an account, but the gateway only
accepted a complete .info file, so users holding a single refresh token could
not enrol an account at all.

- auth_oauth.refresh_tokens exchanges one token without any access token, reads
  the account uid from the JWT subject, refreshes against the selected profile
  and fetches the account block so the tenant is preserved for enterprise
  accounts; a uid mismatch is rejected.
- POST /admin/credentials/refresh-import accepts up to 50 tokens behind the
  existing admin authentication and a shared lock, validates the endpoint, skips
  duplicates, reports per-token failures and stores successes through the normal
  credential writer.
- The import drawer gains a refresh-token tab with an endpoint selector and one
  token per line, reusing the existing result list; prepareImports parsing is
  covered by imports.test.ts and the API/UI flow by refreshImport.test.tsx.
Realtime was named for its output cadence but still buffered every reasoning
delta until text, a tool call or the finish marker appeared, so clients saw a
long silent period and then one oversized thinking event. That hid the real
first-token time and inflated client-side TPS, and it made stream_mode=realtime
misleading for reasoning models.

- _stream_upstream and _stream_adapted only wrap the Chat SSE stream with
  _coalesce_reasoning_sse when the request is not realtime; realtime forwards
  each reasoning delta as it arrives.
- Responses and Anthropic adapters already keep one logical thinking output
  item/block and append incremental deltas, so no client sees N separate
  thoughts and no adapter change is needed.
- compatible keeps the historical single-block replay for clients that depend
  on it; the wrapping helper's docstring now says so.
- tests/test_realtime_streaming.py splits the old coalescing assertion into a
  protocol matrix: realtime yields three Chat/reasoning events, three Responses
  summary deltas on one output item and three Anthropic thinking deltas on one
  block, while compatible still emits one coalesced reasoning event.
Runtime settings could be supplied by .env, the process environment or the CLI
with no way to move them into the configuration store, so every such key stayed
read-only in the WebUI and the same value lived in three places. The WebUI also
had no shared interface preference, and instances had no way to tell offline
tools to keep their hands off a live database.

- `converter.py config migrate|status` resolves the env file first and then the
  data directory it declares, accepts --env-file/--no-env/--data-dir/--db and
  refuses to touch a database that is not this project's control store (SQLite
  header, user_version=2, required tables, payload, quick_check). Migration
  writes runtime-scoped values and config_notice_muted in one revision; status
  never writes.
- A running instance claims a service_lease heartbeat, so migrate refuses while
  the database is in use unless --force is given; both paths share the same
  read-only identity check.
- SCHEMA records env_scope and lockable: deployment keys stay authoritative,
  language is a hot preference with no environment mapping, and hidden
  config_notice_muted records the one-time notice state.
- The Dashboard shows a banner listing runtime keys still supplied by the
  environment, with a copyable migration command and a confirmation drawer
  before muting; either migration or dismissal stops the notice permanently.
- `--lang zh|en|auto` writes the interface language on serve/login/config
  migrate, and the WebUI language preference now round-trips through the store
  instead of staying browser-local; config status only uses --lang for its own
  output.
@sourcery-ai

sourcery-ai Bot commented Oct 17, 2026

Copy link
Copy Markdown

Sorry @maiphucgiang, your pull request is larger than the review limit of 150,000 diff characters

Removing every PR tag left metadata-action with nothing to name, so the image
job warned that no tag or version was generated on pull requests. PR builds are
still useful as validation, so give them an unpublished ref tag while the three
published tags stay unchanged.
@maiphucgiang
maiphucgiang merged commit 546333a into main Oct 17, 2026
9 checks passed
@maiphucgiang
maiphucgiang deleted the feat/config-migration branch October 17, 2026 21:28
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