Commit Graph

134 Commits

Author SHA1 Message Date
kami 5c03d1a772 feat(server): wire real inference into ProfileAdaptationService 2026-06-08 10:59:23 +04:00
kami 90d76e7bd1 feat(personalization): ProfileAdaptationService — propose-only learned adaptation 2026-06-08 10:42:19 +04:00
kami 0c7fa70f8d feat(personalization): OperatorProfileBoundEvent + bind-at-start + inject about context
Records operator profile as a snapshot event at session start (invariants #8/#9),
populates SessionState.boundProfile via reducer, and injects the about text as a
pinned L0 SYSTEM context entry in every stage. Gated by PersonalizationConfig.enabled.
2026-06-08 10:38:47 +04:00
kami 7af5d602bc fix(kernel,server): remove stub compaction + log missing summary artifact 2026-06-08 10:22:16 +04:00
kami 2b1faeaadd fix(kernel,server): wire JournalCompactionService in production + fix SessionSummaryProjector import 2026-06-08 10:20:30 +04:00
kami 7cec168e86 feat(server): real GET /sessions via SessionSummaryProjector 2026-06-08 10:17:20 +04:00
kami b830528d31 feat(kernel,server): rehydrate() + POST /sessions/{id}/resume 2026-06-08 10:13:33 +04:00
kami 37ac767d1f feat(freestyle): two-phase planning->execution driver (Slice 4)
- freestyle_planning.toml: analyst -> (approval) -> architect, producing
  execution_plan; analyst_freestyle.md surfaces open questions.
- TomlWorkflowLoader: requires_approval stage flag -> metadata.
- SessionOrchestrator: inline-prompt branch (metadata[promptInline]) +
  requestStageApproval helper reusing the existing pause/approve path.
- DefaultSessionOrchestrator: enterStage gates on requiresApproval
  (idempotent via resolved-approval lookup), preview derived from the
  gated stage's needs artifact; validatedArtifactContent accessor.
- FreestyleDriver: compiles validated plan, emits ExecutionPlanLockedEvent,
  runs phase 2 in-session via a runPhase2 seam; wired through ServerModule
  + Main (execution_plan kind registered).
2026-06-08 02:50:36 +04:00
kami 638f57709d feat(tui): workflow-start intent input box wired to StartSession.input 2026-06-04 02:24:41 +04:00
kami fe561ada09 feat(server): intent input channel — StartSession.input seeds decision journal 2026-06-04 02:16:23 +04:00
kami 8b6eedcf87 feat(server,kernel): repo-map indexer + droppable L3 context injection 2026-06-04 01:48:05 +04:00
kami 0ca7d4c86b feat(server): ProjectMemoryService — cross-session repo-scoped memory wiring 2026-06-04 01:06:10 +04:00
kami bcc20509e0 feat(kernel): inject decision journal into stage context + wire repository 2026-06-04 00:48:07 +04:00
kami 371e0df340 feat(config): expose router generation + narration knobs in config file
The router/narration model behaviour was hardcoded (Main built RouterConfig()
with defaults). Surface it under the [router] section so it's tunable without a
rebuild:

  [router]            conversation_keep_last, retrieval_k, token_budget
  [router.generation] temperature, top_p, max_tokens          (chat/steering)
  [router.narration]  temperature, top_p, max_tokens, max_per_run

ConfigLoader parses the new sections (adds asDouble); Main maps the config-layer
RouterConfig onto the domain RouterConfig and threads narration.max_per_run into
ServerModule. All values default to the previous constants, so behaviour is
unchanged when the sections are absent. Documents the block in sample-config.toml
and adds parser tests for present/absent cases.
2026-06-03 23:09:48 +04:00
kami e95d2633f8 feat(narration): ground pause narration in the pending approval + roomier budget
Live QA showed two problems with paused-approval narration:

- The narrator was told to "name the stage, outcome, and reason" but the pause
  trigger only carried "APPROVAL_PENDING" with no detail — on a first-stage pause
  there's no L2 history either, so it had nothing to ground on. NarrationSubscriber
  now captures the latest ApprovalRequestedEvent per session (tool name, tier,
  preview) and folds it into the pause instruction (preview truncated to 800
  chars).
- Narration reused the 512-token chat generation config; a reasoning model spent
  the whole budget "thinking" and returned empty content (finishReason=length).
  Add a separate narrationGenerationConfig (maxTokens=1024) so thinking can
  complete and still leave room for the one or two sentences.

Adds a subscriber test asserting the pause narration names the tool and includes
the preview.
2026-06-03 22:55:13 +04:00
kami 2bef4b96a5 feat(tui): Task 3.5 — decode + render router.narration as bright narration_llm lines
Add TypeRouterNarration constant, mark it event-bearing, decode golden test,
applyServer case appending RouterEntry{Role:"narration_llm"} with metrics, and
routerRows render branch showing bright ◆ prefix with FgStrong content + metrics suffix.
2026-06-03 19:09:28 +04:00
kami da1aca411a feat(server): Task 3.4 — ServerMessage.Narration + DomainEventMapper arm 2026-06-03 18:55:57 +04:00
kami 689020e161 feat(server): Task 3.3 — NarrationSubscriber + wiring 2026-06-03 18:41:58 +04:00
kami 80a72d370e feat(tui): show router inference metrics beside router lines 2026-06-03 15:41:51 +04:00
kami c2fb17f6ee feat(server): carry latencyMs + totalTokens through ServerMessage.ChatTurn
ChatTurn wire frame now includes latencyMs: Long? and totalTokens: Int?.
DomainEventMapper reads them from ChatTurnEvent.latencyMs and
tokensUsed?.totalTokens. Positional call site in DomainEventMapperTest
converted to named args. Two new serialization tests cover the ROUTER
(metrics present) and USER (metrics absent) paths.
2026-06-03 15:33:43 +04:00
kami 44c26affe5 test(tui): drop redundant stage_feed_test, superseded by narration_test 2026-06-03 15:26:53 +04:00
kami e9a87febc4 feat(tui): render workflow progress as dim router-feed narration lines 2026-06-03 15:07:31 +04:00
kami 46c567c835 feat(tui): render stage lifecycle frames as dim lines in the router feed
Stage started/completed/failed events are now injected into the router
transcript (role "stage") and rendered in Faint colour, giving the
operator a chronological in-feed view of workflow progress alongside
user/router/tool turns.
2026-06-03 14:54:21 +04:00
kami 622b331de3 feat(workspace): Axis 2 Phase B — client→server workspace handshake
Wires the workspace handshake end to end so a session's workspace is bound
from the client's cwd at connect time and is event-sourced for replay.

- Go TUI sends Hello{workingDir=os.Getwd()} as the first WS frame on connect
  (protocol.go encoder + client.go connect path; golden test pins the wire
  format against the Kotlin discriminator).
- Server adds ClientMessage.Hello, stashes the per-connection workingDir, and
  on session start resolves it through WorkspaceResolver's trust pipeline,
  emits SessionWorkspaceBoundEvent (invariant #9), and threads the resolved
  workspace into OrchestrationConfig for the live run. A Hello after the first
  StartSession is ignored (warn); a rejected path binds the resolver fallback.
- Replay derives the workspace from the recorded event: SessionState gains
  boundWorkspace, DefaultSessionReducer fills it from SessionWorkspaceBoundEvent,
  and ReplayOrchestrator uses it (Path.of only, no filesystem re-query —
  invariant #8) with graceful fallback to config for pre-Phase-B logs.

Absent Hello / null resolver degrades to the prior config-workspace behavior.
2026-06-03 13:18:17 +04:00
kami 5721ed21bb feat(tui): show session workspace (cwd) in the status bar
SessionWorkspaceBoundEvent already recorded the bound workspace root but
it was never surfaced. Map it to a new session.workspace_bound frame
(sealed ServerMessage variant, auto-registered; re-emitted on reconnect
via the snapshot replay), carry it on Session.WorkspaceRoot, and show it
home-abbreviated in the status bar (⌂ ~/Programs/correx). Golden test
pins the decode.
2026-06-03 01:56:46 +04:00
kami 00b08660bd feat(infra): AMD/ROCm resource probe for the VRAM gauge
The resource gauge was NVIDIA-only — on an AMD/ROCm box Main fell back to
UnavailableProbe, so the TUI showed no VRAM. Add AmdResourceProbe backed
by `rocm-smi --showmeminfo vram --showuse --csv`: it skips the warning
preamble, locates the header by column name (order/version tolerant), and
converts the byte VRAM figures to MiB. Process RSS reuses /proc/<pid>/status
(only populated for a managed-model pid). Main now selects NVIDIA → AMD →
Unavailable. Tests cover the real ROCm 4.0 CSV + reordered columns.
2026-06-03 01:52:51 +04:00
kami c63929d79f fix(tui): keep the steering note visible in the approval band
The steer note replaced the action row while editing and vanished once
you esc'd out — so you couldn't see the note or the approve/reject keys,
and it was unclear the note still applied (it did: decide() reads the
buffer). Now the note is a persistent line above the action row (editable
with a caret while typing, a dim reminder once set), and the action row
stays visible. Editing hints clarify enter=approve+send, esc=keep note.
2026-06-03 01:49:24 +04:00
kami f7fc10ddf5 feat(tui): full event history, approval-decision events, status bar
- Events were hard-capped to the last 7 per session in addEvent, so the
  e-inspector and EVENTS panel could never show more. Raise to 1000
  (effectively all, bounded); the EVENTS panel now shows the latest that
  fit (tail), and the inspector scrolls a window around the selection
  with a position indicator.
- The TUI had no approval.resolved handling at all — the decision frame
  was ignored and pending only cleared on session.resumed. Add the
  constant + case: clear the gate and record an ApprovalResolved event
  (APPROVED/REJECTED/AUTO_APPROVED + reason) into the stream.
- Status bar now shows the current stage and session status alongside the
  name. Relabel the cryptic "N bg" badge to "N elsewhere".
2026-06-03 01:24:52 +04:00
kami b56f0e88ca fix(tui): approval nav lockups, shell non-diff preview, hint dedup
Three QA-found issues in the approval gate:

- displayState gated the in-session/approval surfaces on hasApproval
  before sessionEntered, so moving the list cursor onto a session with a
  pending gate auto-opened it, and `l` back-to-list couldn't escape
  (the gate re-popped). Require sessionEntered first.
- The band ran every preview through the two-column diff renderer, so a
  shell tool's argv JSON rendered as an identical-column "diff". Detect
  real unified diffs (isUnifiedDiff); render other previews as plain
  text, and drop the ^x fullscreen hint when there's nothing to expand.
- The footer duplicated the band's approve/reject/steer/diff keys. It now
  shows navigation (l back / e events / q quit) instead.
2026-06-03 01:16:04 +04:00
kami 3600ec6897 feat(tui): surface plane-2 rationale in the approval band
The plane-2 tool-call assessor records verified preconditions
("[PATH_OUTSIDE_WORKSPACE] …") and the server already ships them in
RiskSummaryDto.rationale, but the Go TUI's RiskSummaryDto had no
Rationale field — so the justification was silently dropped at decode
and the gate showed only an opaque tier.

Decode the rationale, carry it on Approval, and render it under the
header in the approval band (warn-marked, above the diff). This closes
the last deferred item of the plane-2 slice-1 plan: the assessment
surfaced to the approval UX. Golden test pins the rationale decode.
2026-06-03 00:30:26 +04:00
kami 03ccac76c7 feat(tui): dock approval gate + two-column diff viewer
Replace the floating approval modal (which blanked the whole UI) with an
opencode-style band that takes the input bar's slot while a gate is
pending: tool/tier/risk header above a side-by-side old|new diff, with
the session output and event stream still visible above it. The band
height adapts to the diff length.

Add a unified-diff parser that aligns removals/additions into split rows
(blank left cell for a create, blank right for a delete) and a
two-column renderer shared by the band preview and the ^x fullscreen
view. Wire plain a/r to approve/reject to match the advertised keys.
2026-06-03 00:17:08 +04:00
kami 6956102cf7 fix(tui): clamp diff scroll offset to last full page
The diff overlay incremented diffScrollOffset on every down-key with no
upper bound; the render clamped only the displayed offset, so the stored
value grew unbounded and the up-key appeared dead for N presses. Clamp
the increment against a new diffMaxScroll() (len(lines) - body height),
shared with the render path via diffBodyHeight().
2026-06-02 23:39:38 +04:00
kami 1017bfffef feat(tui): event-derived ordering + retire Kotlin TUI
Rebuild the Go TUI transcript from the event stream instead of
optimistic local echoes. ChatTurnEvent now maps to ServerMessage.ChatTurn
(chat.turn); the TUI auto-vivifies sessions and renders user+router turns
from events. SessionStarted is de-special-cased into SessionAnnounced
(still carries workflowId), dropping the router.response shadow and
optimistic echoes.

Delete the unused Kotlin TUI (apps/tui) and add a kotlinx<->Go
golden-fixture test to lock the wire protocol. Gitignore server runtime
logs.
2026-06-02 23:38:20 +04:00
kami b2f60d09bb feat(workspace): per-session workspace-scoped tools, policy, and undo
Compute the effective tool registry/executor and plane-2 WorkspacePolicy per run
from config.workspace (effectivesFor), threaded through the orchestrators; a null
workspace falls back to the boot instances byte-for-byte. Wire the concrete
WorkspaceToolRegistryProvider in Main (buildToolConfigForWorkspace). Session undo
unions the session's recorded workspace into its jail roots. (Axis 2 Phase A, tasks 3 + 7.)
2026-06-02 19:57:51 +04:00
kami 7d7e524756 feat(workspace): WorkspaceResolver trust pipeline + allowed_workspace_roots
Resolve a client-supplied workspace dir safely: canonicalize via toRealPath,
reject privileged locations, clamp to [tools] allowed_workspace_roots (permissive
when unset, logged), fall back to the boot default on any rejection. PathJail made
non-internal so the server can reuse its symlink-safe containment. (Axis 2 Phase A, task 4.)
2026-06-02 19:57:03 +04:00
kami d1c6774d05 feat(artifacts): config-driven custom artifact kinds with schema validation
Users declare artifact kinds in [[artifacts]] (id + schema_path to a JSON-schema file + llm_emitted); ConfigArtifactKind registers them at startup via createWorkflowLoader(extraKinds). New JsonSchemaValidator validates any non-built-in kind generically against its declared deriveJsonSchema(), so an LLM-emitted custom kind is checked against its shape, never trusted. Removed the dead payloadSerializer from the ArtifactKind contract. Schema source is path-to-file (not inline TOML — the hand-rolled parser can't nest).
2026-06-02 13:58:23 +04:00
kami 91ededa4ca test(server): real llama-server WebSocket lifecycle tests 2026-06-01 23:23:29 +04:00
kami da3f6c84a3 feat(tui): QA fixes + approval Ctrl keys, workflow focus, session UUID
- gate session-list nav on idle so in-session arrows/jk don't move the list (5a)
- approval dismiss on esc + in-session 'a' reopens a pending approval (5b)
- auto-focus a newly started workflow session (was running invisibly)
- approval actions on Ctrl chords (^a/^r/^x) matching the modal; bare/Alt letters no longer approve; diff closes on ^x/esc
- steer hint in the approval modal; session UUID in the input bar
- env-gated debug logging (CORREX_TUI_LOG) with k.Alt in the key trace
2026-06-01 23:23:29 +04:00
kami 55a18b4b3a feat: correx-managed model lifecycle slice 5 — Go TUI model swap + telemetry
Go TUI decodes model.changed / model.list / resource.status and renders a
status-bar VRAM/GPU%/RAM gauge plus a models overlay (m key / palette 'models'):
lists configured models with the resident one marked, enter swaps (SwapModel),
c clears the pin (ClearModelPin). All three new server messages are
non-event-bearing control/gauge frames, applied immediately like
provider.status_changed.

Server addition required for the picker: ServerMessage.ModelList (model.list,
NonEventMessage) sent once in the initial snapshot from
ManagedInferenceRouter.availableModelIds()/currentModelId() — the TUI otherwise
cannot enumerate swap targets.

Completes the 5-slice model-lifecycle feature. ./gradlew check green; go build/vet clean.

Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 5 of 5).
2026-06-01 13:36:01 +04:00
kami 7b1df95627 feat: correx-managed model lifecycle slice 4 — resource telemetry
Vendor-agnostic ResourceProbe (commons): NvidiaResourceProbe reads nvidia-smi
(injectable runner) for whole-device VRAM/util and /proc/<pid>/status for the
managed llama-server RSS, both fail-soft to null; UnavailableProbe for non-GPU
hosts / the static path. DefaultModelManager.currentPid() + LlamaProcess.pid
expose the managed pid. ServerMessage.ResourceStatus (resource.status,
NonEventMessage, all-nullable) pushed every 2.5s on the global stream plus one
in the initial snapshot. Live gauge only — never event-sourced (feeds no core
decision), so invariants #8/#9 hold. Main wires the NVIDIA probe on the managed
path when nvidia-smi is present, else UnavailableProbe.

Tests: csv parsing (single/multi/malformed), gpu+rss combine, fallbacks.

Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 4 of 5).
2026-06-01 12:36:38 +04:00
kami 7341ab578e feat: correx-managed model lifecycle slice 3 — manual swap + pin
ManagedInferenceRouter owns a live @Volatile pin (decision D1): swap(modelId)
makes a model resident and pins it; clearPin() releases it. Pin is highest
precedence in route() (pin > stage.modelId > capability > default). New
SwapModel/ClearModelPin client messages handled in GlobalStreamHandler via
ServerModule.modelSwapper (null on the static path). ServerMessage.ModelChanged
(model.changed) mapped from ModelLoadedEvent/ModelUnloadedEvent — the swap's load
event surfaces to clients through streamGlobal, so the handler doesn't push it
directly. Tests: pin precedence + swap/clear, Model* -> ModelChanged.

Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 3 of 5).
2026-06-01 11:48:24 +04:00
kami 382194b498 feat: correx-managed model lifecycle slice 2 — per-stage model selection
StageConfig.modelId; InferenceRouter gains a backward-compatible model-aware
route() overload (default ignores modelId, so static routers and test fakes are
unaffected). New ManagedInferenceRouter (infra commons) resolves a target model
per stage (stage.modelId > capability match > default) and ensureLoaded()s it
before routing, returning the live managed provider — its id changes with the
resident model, so there is no stale health cache to invalidate on swap.
ModelManager.ensureLoaded default delegates to the idempotent load(). Main wires
the managed router on the [[models]] path; the static path keeps DefaultInferenceRouter.

Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 2 of 5).
2026-06-01 11:35:51 +04:00
kami e45a626cc4 feat: correx-managed model lifecycle slice 1 — config + manager factory + boot/shutdown
[[models]] config (ModelConfig/ModelsSettings) parsed by ConfigLoader;
InfrastructureModule.createModelManager + modelConfigToDescriptor; Main.kt
managed boot path spawns the default llama-server when [[models]] is present
(static [[providers]] path preserved when absent) and kills it on shutdown.

Plan: docs/plans/2026-05-31-model-lifecycle-management.md (slice 1 of 5).
2026-06-01 11:03:05 +04:00
kami 5beb866036 feat: rehydrate L3 metadata from event log on startup
TurboVec persists vectors via the sidecar but kept entry metadata
(sessionId/turnId/text) only in an in-memory map, so restarting with
backend=turbovec silently dropped every query hit (the id→metadata
lookup missed). Rebuild that map from the ChatTurnEvent log at startup.

- RehydratableL3MemoryStore: capability interface for stores whose
  vectors persist independently of the JVM. TurboVec implements it;
  InMemory deliberately does not (its vectors are volatile too).
- L3MetadataRehydrator replays ChatTurnEvents into the metadata map.
  No embedder: the query path never reads entry.vector, so vectors
  stay in the sidecar and metadata comes from the event log (the
  source of truth, invariant #1). Short-circuits for non-rehydratable
  backends before scanning the log.
- Wired into Main before the server accepts queries; no sidecar spawn.

Backfill of never-embedded history (needs the embedder) is a separate
follow-up. The in_memory non-durability warning already exists.
2026-05-31 22:08:52 +04:00
kami 7fa1db8345 feat: surface ToolCallAssessedEvent to clients (plane-2 UX)
Final plane-2 step: the tool-call intent assessment was decided
server-side but never reached the client — DomainEventMapper dropped
ToolCallAssessedEvent through the else/null branch.

- ServerMessage.ToolAssessed (tool.assessed): disposition as a plain
  String + AssessedIssueDto list; observations stay off the wire
  (internal replay facts, invariant #9).
- DomainEventMapper maps the event 1:1 (no filtering — rendering
  decisions belong in the client).
- Go TUI consumes tool.assessed, records an event entry only for
  non-PROCEED or issue-bearing assessments (noise control), and is
  added to IsEventBearing() so it buffers correctly during snapshot
  replay like its sibling tool.* events.
2026-05-31 16:49:00 +04:00
kami 7682429b3d feat: undo session file mutations via server endpoint + CLI command 2026-05-31 11:01:05 +04:00
kami 95e79bffb8 refactor: retire sandbox dual-write; file tools write in-place into workspace 2026-05-31 10:39:58 +04:00
kami b778c06f6c feat: capture file pre/post-image to CAS and emit FileWrittenEvent 2026-05-31 10:34:30 +04:00
kami ea80597cab feat: add ExecInterpreterRule and NetworkHostRule plane-2 rules 2026-05-31 07:35:18 +04:00
kami 545068d222 feat: plane-2 tool-call intent validation (path containment slice)
Implements the full vertical slice for invariant #9 tool-call assessment:
- core:toolintent — new module with ToolCallRule seam, ToolCallAssessor,
  WorldProbe/FileSystemWorldProbe, WorkspacePolicy, PathContainmentRule,
  and RiskMapping (assessment → RiskSummary / AssessedIssue)
- core:tools — ToolCallAssessmentRecord + ToolInvocationRecord.assessment
  field; DefaultToolReducer handles ToolCallAssessedEvent (replay proof)
- core:config — ToolsConfig gains workspaceRoot + privilegedLocations;
  ConfigLoader parses both; DEFAULT_PRIVILEGED_LOCATIONS built-in
- core:kernel — OrchestratorEngines gains toolCallAssessor/workspacePolicy/
  worldProbe fields; SessionOrchestrator.dispatchToolCalls runs
  runPlane2Assessment before the tier gate: BLOCK → hard-reject without
  executing; PROMPT_USER → elevates tier into approval path with plane2Risk
  in the ApprovalRequestedEvent
- apps/server — constructs PathContainmentRule + ToolCallAssessor +
  WorkspacePolicy from config and wires them into OrchestratorEngines

Assessment is recorded as ToolCallAssessedEvent (environment observed once,
facts stored, replay reads events — invariant #9). Assessor and WorldProbe
are only invoked on the live orchestrator path, never in replay.
2026-05-31 04:22:58 +04:00