11 KiB
11 KiB
spec, created, status
| spec | created | status |
|---|---|---|
| docs/specs/ui/requirements/context-compaction-count.md | 2026-08-02 | complete |
Implementation Plan: Context Compaction Count
Overview
Extend the existing context-window persistence boundary so one database update stores the latest sample and atomically increments a session-owned lifetime count when usage decreases. Propagate that count through the existing session metadata event, hydrate it into the shared session-runtime store, and add a compact translated row with an accuracy disclosure to the existing context hover. Focused repository/orchestrator tests prove restart and duplicate-delivery behavior; existing desktop and mobile context-hover E2E scenarios prove the user-visible path.
Backend
Session metadata contract
- Add
SessionMetaKeyContextWindowandSessionMetaKeyContextCompactionCountconstants inapps/backend/internal/task/models/models.goso reset, model-change, repository, and orchestrator paths use one spelling for the two session-owned metadata keys. - Keep the feature in
task_sessions.metadata; no schema migration or backfill is needed. A missingcontext_compaction_countreads as zero.
Atomic context-window persistence
- Add
UpdateSessionContextWindow(ctx context.Context, sessionID string, contextWindow map[string]interface{}) (int64, error)to the concrete task repository and the orchestrator's narrowsessionExecutorStoreinapps/backend/internal/orchestrator/service.go. Keeping it on the narrow context-window boundary avoids forcing unrelated repository test doubles to implement a method they do not use. The returned value is the persisted lifetime count. - Implement the method in
apps/backend/internal/task/repository/sqlite/session.gowith dialect-specific SQL that, in one row update, compares the incomingusedvalue with the previously persistedmetadata.context_window.used, incrementsmetadata.context_compaction_countonly for a strict decrease, stores the newcontext_window, and returns the resulting count. - Treat an absent/non-numeric previous sample or absent count as zero without incrementing. Re-delivering the same decreased sample is idempotent because the first update makes it equal to the persisted baseline.
- Add SQLite behavior coverage in
apps/backend/internal/task/repository/sqlite/session_test.gofor the first sample, increase/equality, decrease, repeated decrease, a legacy missing count, and preservation of unrelated metadata. - Add the equivalent environment-gated PostgreSQL behavior coverage in
apps/backend/internal/task/repository/sqlite/postgres_schema_test.go, as required for changed dialect-sensitive task-repository methods.
Orchestrator event propagation
- Update
apps/backend/internal/orchestrator/context_window.goso the existing per-session generation guard callsUpdateSessionContextWindowand returns the persisted count with the generation result. - Update
apps/backend/internal/orchestrator/event_handlers_git.goso a successful write publishes onesession.state_changedmetadata patch containing bothcontext_windowandcontext_compaction_count. Failed or generation-stale writes publish neither. - Replace literal context-window metadata keys in the touched reset/model-change paths with the model constants while preserving the existing behavior: clearing the current sample never clears or increments the lifetime count.
- Include the persisted count in context-window replay snapshots used during WebSocket session hydration.
- Extend
apps/backend/internal/orchestrator/event_handlers_git_test.goto prove a decrease increments, a backend-service recreation continues from persisted metadata, duplicate delivery does not double count, reset leaves the count intact, and the published patch contains the resulting count.
Frontend
Session-runtime state and hydration
- Extend
ContextWindowEntryinapps/web/lib/state/slices/session-runtime/types.tswithcompactionCount: number. - Update
parseContextWindowEntryinapps/web/lib/state/slices/session-runtime/context-window.tsto accept the adjacent metadata count, normalize missing/invalid/negative values to zero, and include it in the parsed entry. - Update
apps/web/lib/ws/handlers/agent-session.tsto readcontext_compaction_countfrom the samemetadataorsession_metadataobject ascontext_window, then pass it to the parser. Preserve explicitcontext_window: nullcache invalidation. - Update
apps/web/hooks/domains/session/use-session-context-window.tsso both Zustand subscriptions and initial session-metadata hydration retain the count across refresh and restart recovery. - Cover parsing, live metadata patches, legacy missing counts, and hydrated session metadata in the existing focused unit tests.
Context hover
- Update
apps/web/components/task/chat/token-usage-display.tsxto render aCompactionsrow with the tabular count and an inline info control. The help text states that Kandev infers the count from observed token drops and that missing samples or provider resets can make it approximate. - Reuse the component's current inline help pattern instead of nesting another Radix tooltip inside
TooltipContent; the help must remain reachable by pointer hover, keyboard focus, and touch focus while the parent context hover stays open. - Add the new user-facing strings to
apps/web/src/locales/en/common.json, regenerateapps/web/src/locales/pseudo/common.json, and resolve them at render time withuseTranslation. - Extend
apps/web/components/task/chat/token-usage-display.test.tsxfor zero and non-zero counts plus the accessible disclosure. Update context-window store/parser tests and fixtures that constructContextWindowEntryvalues.
Mobile design contract
- Desktop outcome: the existing context ring hover adds the lifetime count and accuracy help below the token/source row.
- Mobile entry point and exemplar: the existing context ring remains tap-pinnable;
apps/web/e2e/tests/chat/mobile-context-window-source.spec.tsand the component's existing source-help control are the nearest shipped pattern. - Hierarchy and presentation: current percent, progress, token count, and source remain primary; compaction history is a secondary row inside the same compact hover. No drawer, route, new scroll owner, breakpoint branch, or persistent mobile preference is introduced.
- Shared behavior: desktop and mobile use the same session-runtime entry, translated copy, count row, and help behavior. Existing parent-tooltip pinning continues to own outside-tap and Escape dismissal.
- Mobile proof: the mobile E2E scenario taps the ring and compaction help, asserts the disclosure remains visible inside the pinned hover, and confirms no document-level horizontal overflow.
Tests
- What: first/equal/increasing/decreasing/duplicate samples update one durable count while preserving unrelated metadata.
- File:
apps/backend/internal/task/repository/sqlite/session_test.go - How: table-driven repository test against a real SQLite database.
- File:
- What: the same atomic compare-and-increment contract works with PostgreSQL JSONB.
- File:
apps/backend/internal/task/repository/sqlite/postgres_schema_test.go - How: environment-gated real PostgreSQL repository test.
- File:
- What: restart-style service recreation uses persisted metadata, resets retain the count, and live session patches include it exactly once.
- File:
apps/backend/internal/orchestrator/event_handlers_git_test.go - How: real task repository plus fresh
Serviceinstances and the recording event bus.
- File:
- What: live and hydrated session metadata normalize the count into
ContextWindowEntry, including legacy missing values.- Files:
apps/web/lib/state/slices/session-runtime/context-window.test.ts,apps/web/lib/ws/handlers/agent-session.test.ts,apps/web/hooks/domains/session/use-session-context-window.test.ts - How: focused parser, store-handler, and hook tests.
- Files:
- What: the hover shows zero/non-zero counts and exposes the translated accuracy explanation accessibly.
- File:
apps/web/components/task/chat/token-usage-display.test.tsx - How: render with mocked session state under the existing tooltip test boundary and focus the inline help control.
- File:
E2E Tests
- Scenario: GIVEN a counted session has a reliable context sample, WHEN a desktop user hovers the context ring and then the compaction info control, THEN the count and approximate-count explanation remain visible in the same hover.
- File:
apps/web/e2e/tests/chat/context-window-source.spec.ts - What to verify: count text, pointer-hover disclosure, and parent-hover stability.
- File:
- Scenario: GIVEN the same counted session on a phone viewport, WHEN the user taps the ring and compaction info control, THEN the count and explanation are reachable without horizontal overflow.
- File:
apps/web/e2e/tests/chat/mobile-context-window-source.spec.ts - What to verify: tap-pinned hover, disclosure visibility, and document containment.
- File:
Verification Results
- Task 01: SQLite, backend-app replay, task-service, orchestrator, and full
go test ./...checks passed; PostgreSQL behavior test is present but skipped withoutKANDEV_TEST_POSTGRES_DSN. - Task 02: focused frontend suite passed (4 files, 68 tests); typecheck, i18n check, and i18n ratchet passed.
- Task 03: managed Chromium desktop and mobile-chrome E2E scenarios passed (1 test each).
- Review fixup: the context-update handler now persists synchronously to preserve arrival order; model-change clears use the guarded reset boundary; the compaction help has explicit touch toggle state; and parser/E2E selectors now require the adjacent count contract. Full
go test -race ./...was not rerun locally; the PR's race-enabled backend job had already exposed an unrelatedinternal/integrationfailure before this fixup.
Implementation Waves And Parallel Candidates
Execution is sequential in the primary conversation because the frontend contract depends on the backend metadata shape and E2E depends on both implementation slices.
- Task 01: Persist inferred compaction counts — done
- Task 02: Show compactions in context hover — done
- Task 03: Cover desktop and mobile hover — done
Risks and boundaries
- ACP supplies usage samples, not compaction events. Strict decreases are intentionally approximate and the UI must not describe them as provider-confirmed.
- The database update must compare and increment atomically so duplicate event delivery or multiple backend subscribers cannot double count one observed drop.
- Reset and model-change paths may clear
context_window, but must not clearcontext_compaction_count; the next sample after a cleared baseline must not increment. - Public session documentation now mentions the inferred count and its approximate nature; no separate reference page is needed for this contextual hover detail.