Commit Graph

272 Commits

Author SHA1 Message Date
kami 7558d32ad9 test(fixtures): DeterministicHarness — event-log-in, derived-state-out builder
Provides a reusable builder that packages the event-log-in → derived-state-out pattern for deterministic tests. Implements fixed clock and deterministic event ID generation to ensure byte-identical event JSON across test runs with same payloads.

Acceptance criteria met:
- Two harnesses fed same payloads yield byte-identical stored event JSON
- Fixed clock ensures timestamp determinism (2026-01-01T00:00:00Z)
- Event IDs are deterministically generated (event-0, event-1, etc.)
- Supports both givenEvents (append) and rebuild (replay via projection)
2026-06-12 14:46:55 +04:00
kami 64b58e3b05 feat(server,cli): session replay inspection — timeline + determinism digest 2026-06-12 14:39:54 +04:00
kami a04cd1fa24 feat(server,cli): event stream inspector — /sessions/{id}/events + correx events 2026-06-12 14:28:19 +04:00
kami a7d5211f3f feat(server,logging): correlation-structured MDC logging at seams
Implement T1 of observability epic: every server log line carries sessionId
(and stageId where known) via SLF4J MDC, propagated across coroutine
suspension points.

Changes:
- Create com.correx.apps.server.logging.Correlation: withSessionContext()
  helper that sets MDC (sessionId, stageId), wraps block in MDCContext(),
  and restores prior values on exit; supports safe nesting
- Wrap launchSessionRun() orchestrator loop in withSessionContext(sessionId)
  so all session logs carry the sessionId
- Wrap GlobalStreamHandler ChatInput and StartChatSession onUserInput call
  sites in withSessionContext(sessionId)
- Wrap SessionStreamHandler ChatInput onUserInput in withSessionContext(sessionId)
- Update log4j2.properties patterns: append
  %notEmpty{ [%X{sessionId}]}%notEmpty{ [%X{stageId}]} before %msg
  on both console and file appenders
- Add kotlinx-coroutines-slf4j:1.9.0 dependency for MDCContext

Test (6 tests):
- withSessionContext sets/restores sessionId ✓
- withSessionContext sets/restores stageId ✓
- Nested contexts stack and restore correctly ✓
- MDC cleared when stageId not provided ✓
- Propagation across coroutine boundaries via MDCContext ✓
- Cleanup after block exit ✓

Acceptance:
- Zero behavior change; no core module touched ✓
- All imports verified as used ✓
- No bare try-catch; try/finally used correctly ✓
- Detekt-compliant ✓
- Full test suite passes (120 tests, 119 passed, 1 skipped) ✓
2026-06-12 14:17:54 +04:00
kami df3ee0bbe0 fix(inference): narration model_id accepts bare provider id from TOML
The router matched only the adapter-prefixed registry id
(llama-cpp:narrator) while the operator's [router.narration] model_id
naturally repeats the bare [[providers]] id (narrator), so the match
always failed and narration silently fell back to the slow main model.
Match either form.
2026-06-12 13:56:28 +04:00
kami e195c79510 fix(server): record boot-time parking of stale sessions as OrchestrationPaused
A stale RUNNING session skipped by resumeAbandoned stayed RUNNING in
every projection — the TUI showed a phantom running workflow that no
process was executing, and each boot re-logged the same skip. Parking
now emits OrchestrationPaused(reason=ABANDONED_STALE): status becomes
PAUSED (truthful — not executing, resumable via POST /resume) and
subsequent boots skip it on status alone.
2026-06-12 13:54:08 +04:00
kami 9f43dc7022 fix(config): config writer silently dropped router.narration.model_id
Every TUI config-editor save regenerates the TOML through
CorrexConfigWriter, whose [router.narration] section never wrote
model_id — one save and narration silently fell back to the main
model, queueing every narration behind slow 9B inferences (live QA:
approval narrations arrived minutes late, after the workflow had
already failed). The writer now persists it and the field is editable
in the TUI (blank clears it).
2026-06-12 13:47:45 +04:00
kami 1a7eb05945 fix(workflow): reject field-condition edges the producing kind cannot satisfy
Live repro: architect emitted a verify stage with kind=analysis and a
verdict-equals edge. The kind schema doubles as the LLM response format,
so 'verdict' was never emitted and the workflow failed at its final hop
at runtime. The compiler now requires artifact_field_equals fields to be
declared by the producing stage's kind schema, failing the plan at lock
time with a pointer at review_report. Architect prompt rule added;
reachability check extracted alongside the new validation.
2026-06-12 13:42:13 +04:00
kami a455762dd5 fix(router,server,config,tui-go): close QA findings #2 #3 #9 #10 #12
#2: approval pauses narrate from ApprovalRequestedEvent (carries
tool/tier/preview/stage directly) instead of OrchestrationPaused +
a pending-approvals map the next event had not yet populated — the
narrator always lost that race and produced generic text.

#3: NarrationTrigger now carries the triggering event's stageId; the
narration status line and RouterNarrationEvent.stageId use it instead
of the router projection's currentStageId, which terminal events have
already cleared ('Stage: none').

#9: ExecutionPlanLocked -> plan.locked and ArtifactValidated ->
artifact.validated mapped to the TUI (Go: feed lines, plan shows the
compiled stage chain); ArtifactValidating explicitly dropped.

#10: blank router turn guard — a length-finish with empty text now
emits an explanatory turn naming the max_tokens budget instead of a
silent blank; blank steering responses no longer emit steering notes.

#12: resumeAbandoned only auto-resumes sessions whose last event is
within orchestration.resume_abandoned_max_age_minutes (default 24h,
0 disables); staler sessions stay parked for POST /resume. Knob is
TUI-editable and persisted.
2026-06-12 13:20:04 +04:00
kami a4f0c6d0a2 fix(workflow): reject execution plans with unreachable stages
Live repro: the architect wired all three stages straight to 'done'
instead of chaining them, so phase 2 legitimately completed after
stage 1 of 3 — a silently truncated plan reported as success. The
compiler now walks the edge graph from start and fails compilation
when any declared stage is unreachable, surfacing the broken topology
as ExecutionPlanRejected at lock time instead. Architect prompt edge
rules rewritten to demand explicit chaining (the old wording was
garbled).
2026-06-12 12:55:17 +04:00
kami c25ba27e57 fix(workflow,kernel): split plan stage 'produces' (slot name) from 'kind' (artifact kind)
ExecutionPlanCompiler treated the plan's single 'produces' string as a
registry kind id, while the architect prompt described it as a unique
artifact id referenced by needs/edges — the model followed the prompt,
invented descriptive ids (files_created), and every freestyle plan
failed to compile. Collapsing both onto one string is also unsound:
two stages producing the same kind would collide on slot name and make
artifact_validated edge conditions ambiguous.

Plan stages now carry an optional 'kind' selecting the registered kind
(mirroring TOML's produces = { name, kind }); 'produces' stays the
unique slot name. Missing 'kind' falls back to the old produces-as-kind
reading so existing plans still compile. Vocabulary entry, architect
prompt, and execution_plan schemas updated to match.
2026-06-12 12:44:22 +04:00
kami 4107a595db refactor(server): single guarded freestyleHandoff shared by run and resume launchers
launchSessionRun and launchSessionResumeWithRehydrate each inlined their
own copy of the freestyle phase-2 handoff with different guard sets — the
run path had none, so a failed planning run still called lockAndRun and
emitted a spurious ExecutionPlanRejected. Both now call one handoff that
checks graph id, planning result, and an existing plan lock before
locking and running phase 2.
2026-06-12 12:32:57 +04:00
kami a0da220e8e fix(server): boot/event-driven resume paths get rehydrate + freestyle handoff
resumeAbandonedSessions (boot pickup of RUNNING sessions) and the
restart-while-approval-pending subscription still used the bare
launchSessionResume: no artifact-cache rehydrate and no freestyle
plan-lock handoff. A freestyle session killed mid-planning was grabbed
by the boot path before the operator could POST /resume, completed the
planning graph, and stranded — WorkflowCompleted but never locked
(live repro: session b36b0e45, 2026-06-12).

Both paths now use launchSessionResumeWithRehydrate; the bare launcher
is deleted.
2026-06-12 12:26:04 +04:00
kami 35e921c6d1 fix(inference): tokenize endpoint sent wrong field — every token estimate was 0
LlamaCppTokenizer posted {"tokens":[text]} but llama.cpp's /tokenize expects
{"content":text}; the server silently answered {"tokens":[]}, so countTokens
returned 0 for every string. Every context entry carried tokenEstimate=0 and
the whole budget/trim pipeline was blind — live QA saw a 34k-token prompt
sail through a 16384 stage budget (three raw file_read results of plan docs),
crater t/s, degrade output, and fail the stage on validation 3x.

Also guard estimateTokens: a zero count for non-blank content falls back to
the chars/4 heuristic so a lying tokenizer can never blind budgeting again.
2026-06-11 22:50:53 +04:00
kami e503a28db2 fix(server,kernel): freestyle survives kill/restart at every phase (B4)
Three gaps found by live QA (kill between planning-complete and plan lock):
- launchSessionResumeWithRehydrate never handed off to FreestyleDriver, so a
  resumed freestyle session stranded at planning-complete with no plan lock
  and no phase 2; now locks+runs when planning completed and no
  ExecutionPlanLockedEvent exists yet.
- resume route 400'd on phase-2 sessions (workflowId freestyle-<sid> is
  compiled, never registered); now recompiles the locked plan after
  rehydrating the artifact cache.
- orchestrator.resume threw when currentStageId was terminal ('done') or
  unknown to the graph; now returns WorkflowResult.Completed.
2026-06-11 22:35:51 +04:00
kami 99bca1703b fix(config): SimpleToml multi-line string arrays
The line-based parser treated 'conventions = [' as the complete value,
binding conventions to listOf("[") and dropping every entry on the
following lines. Accumulate value lines until the top-level bracket
closes (quote-aware), skipping comments inside the array.
2026-06-11 22:30:32 +04:00
kami 50602b0f11 fix(server): bind project profile on chat-session start
Chat sessions never emitted ProjectProfileBoundEvent — only the workflow
path did — so router triage always saw a null profile and could not cite
conventions or commands. Extract the binding from launchSessionRun into
ServerModule.bindProjectProfile and call it from handleStartChatSession.
2026-06-11 22:09:34 +04:00
kami 07819ec82d fix(inference): fold non-L0 SYSTEM entries into the leading system message
Strict chat templates (Qwen3.5) reject any system message after index 0.
Recalled L3 memory, L2 stage summaries, and retrieval entries carry
EntryRole.SYSTEM and rendered as standalone mid-conversation system turns,
making llama-server 400 on every router turn once L3 recall returned hits.
2026-06-11 21:55:28 +04:00
kami 8bf56604c7 chore(examples): root-level workflow descriptions + curated .correx project profile 2026-06-11 21:47:39 +04:00
kami f521ac89e5 feat(memory): markdown heading extraction in RepoMapIndexer
h1-h3 headings become the file's symbols, so design docs and GDDs are
embedded and retrievable alongside code. .md files now also participate in
workspace fingerprinting (INDEXED_EXTENSIONS derives from pattern keys).
2026-06-11 14:39:05 +04:00
kami 81a21bfe57 feat(memory): GDScript symbol extraction in RepoMapIndexer
Adds 'gd' to SYMBOL_PATTERNS (func/class_name/class/signal/enum at column 0,
optional static prefix). Because INDEXED_EXTENSIONS derives from the pattern
keys, .gd files now also participate in WorkspaceStateProbe fingerprinting,
making fp: state keys content-sensitive for Godot repos.
2026-06-11 14:32:04 +04:00
kami 1976d8ad2b test(replay,integration): repo-knowledge replay derivation + state-keyed scan reuse
Adds two test files to prove B4/B3 invariants end-to-end:

- RepoKnowledgeReplayTest (testing/replay): projection-based replay tests that
  verify recorded RepoKnowledgeRetrievedEvent drives relevantFiles context assembly
  without consulting a retriever (test A), and that a log without the retrieval event
  falls back to the RepoMapComputedEvent legacy path (test B).

- RepoMapReuseIntegrationTest (testing/integration): end-to-end test using the real
  ProjectMemoryService + L3RepoKnowledgeRetriever + InMemoryL3MemoryStore proving
  exactly one index walk across two sessions sharing the same stateKey, and that the
  second session receives non-empty repo hits via the shared L3 embeddings despite
  having no per-session RepoMapComputedEvent.

New test deps (noted per constraint):
- testing/replay: adds core:context (needed to import ContextLayer/ContextEntry from
  the buildRelevantFilesEntry return type).
- testing/integration: adds apps:server (ProjectMemoryService, L3RepoKnowledgeRetriever,
  FakeWorkspaceStateProbe), core:router (InMemoryL3MemoryStore), core:config (ProjectConfig).
  No new inter-core or upward production dependencies introduced.
2026-06-11 13:52:06 +04:00
kami 7a9e060a55 feat(kernel): RepoKnowledgeRetriever port + relevance-retrieved repo context with recency fallback 2026-06-11 13:39:57 +04:00
kami 9d38a89c88 feat(memory): state-keyed repo-map reuse via L3 presence check; embed entries on fresh scan
- Add existsByTurnIdPrefix(prefix) to L3MemoryStore interface; implemented in
  InMemoryL3MemoryStore (mutex-guarded) and TurboVecL3MemoryStore (metadata map).
  All test fakes (TrackingL3MemoryStore, CapturingStore) updated.
- Introduce RepoMapIndexerPort interface; RepoMapIndexer implements it. Allows
  injection of test doubles without framework overhead.
- ProjectMemoryService.indexAndRecord: reads latest WorkspaceStateObservedEvent
  from event store (invariant #9 — recorded fact, not re-probe); skips scan+embed
  when L3 already holds entries for "repomap:<root>:<stateKey>"; passes stateKey
  to RepoMapComputedEvent; embeds each scanned entry after append; per-entry
  failures warn-logged (CancellationException rethrown).
- Tests: InMemoryL3MemoryStoreTest +2 cases; new ProjectMemoryServiceReuseTest
  covers same-key skip, changed-key re-index, no-observation always-reindex,
  and embedding turnId tag verification.
2026-06-11 13:30:57 +04:00
kami 4dbe637f4a feat(memory): WorkspaceStateProbe (git or fingerprint) + observeAndRecord at session start 2026-06-11 13:22:50 +04:00
kami f1fc43cc85 feat(router,server): wire workflow inventory and project profile into router context per turn 2026-06-11 13:15:32 +04:00
kami 823c2e0ade feat(events): WorkspaceStateObservedEvent, RepoKnowledgeRetrievedEvent, stateKey on RepoMapComputedEvent 2026-06-11 13:11:12 +04:00
kami 72e7002623 feat(router): replace bare refusal with triage directive in system prompt 2026-06-11 11:15:59 +04:00
kami a32e9acac4 feat(router): inject available-workflows and project-profile into router L0 context 2026-06-11 11:11:04 +04:00
kami ac458d83e5 feat(workflow): root description field in workflow TOML surfaced through registry 2026-06-11 11:08:33 +04:00
kami ed0dca8b78 feat(kernel,artifacts): inject artifact-kind vocabulary into architect context 2026-06-11 10:06:40 +04:00
kami beed501d60 feat(kernel): label reviewer artifacts as critic feedback on back-edge re-entry 2026-06-10 21:56:05 +04:00
kami 629b910e7f feat(kernel): retried stages receive explicit failure feedback; default retry backoff 1s 2026-06-10 21:53:45 +04:00
kami 91cca9aee3 feat(server,kernel): bind ProjectProfile per session and inject as L0 context 2026-06-10 21:49:31 +04:00
kami 4389163c5c feat(events,sessions): ProjectProfileBoundEvent reduced into session state 2026-06-10 21:47:30 +04:00
kami 5eda64949f feat(config): ProjectProfile — per-repo standing context at .correx/project.toml 2026-06-10 21:45:46 +04:00
kami 0de02bb3a7 feat(server,kernel): post-plan approval gate — operator reviews plan before lock
After a freestyle plan compiles successfully, requestPlanApproval is invoked
before ExecutionPlanLockedEvent is emitted. Rejection emits
ExecutionPlanRejectedEvent(source="operator") with no lock and no phase-2 run.
Adds public SessionOrchestrator.requestPlanApproval seam delegating to
requestStageApproval, wired in Main.kt. Two new tests cover the reject and
approve paths; existing tests stay green via the default passthrough param.
2026-06-10 21:36:17 +04:00
kami 3d43f34d3b fix(narration): use plan_rejected kind and include source in instruction 2026-06-10 21:31:34 +04:00
kami 04836b751c feat(server): narrate execution-plan rejection 2026-06-10 21:30:07 +04:00
kami 0553b1b6ee feat(events,server): ExecutionPlanRejectedEvent — freestyle compile failure visible in log
Both silent-failure branches in FreestyleDriver.lockAndRun now emit
ExecutionPlanRejectedEvent (source=compile or missing_content) before
returning, satisfying invariant #1. Event registered in eventModule for
correct polymorphic serialization.
2026-06-10 21:26:18 +04:00
kami fba2bbb17e fix(kernel): llm-emitted artifacts require stored content — close phantom path
emitLlmArtifacts previously emitted ArtifactCreated/Validating/Validated for every
llm-emitted slot unconditionally, fabricating "validated" artifacts when the model
produced no content. Gate now mirrors the F-015 file_written check: slots with null
or blank cached content are skipped with a warn log. Regression test added in
NeedsArtifactInjectionTest verifies no artifact events appear for a blank-response stage.
2026-06-10 21:20:28 +04:00
kami 113f403f5a chore(examples): add qa_ping smoke workflow; ignore tui-go binary and qa/ scratch
qa_ping is a minimal single-stage file_write workflow used for live QA smoke
runs (writes 'pong' into the qa/ scratch repo). The qa/ workspace itself is a
nested git repo and stays untracked; the compiled tui-go binary is ignored.
2026-06-10 20:54:59 +04:00
kami 3ad42ff29e fix(llama-cpp): raise default request timeout 60s -> 600s
Long generations (plan/patch stages on larger contexts) exceeded the 1-minute
HTTP timeout and surfaced as provider failures mid-pipeline.
2026-06-10 20:54:39 +04:00
kami c82a86477d fix(tui): wrap long artifact lines in viewer instead of truncating
Artifact content wider than the modal was silently cut off. Lines are now
hard-wrapped at rune boundaries to the modal content width, and scroll
clamping uses the wrapped line count so the last page stays reachable.
2026-06-10 20:54:39 +04:00
kami cba9dd4583 fix(server): artifact viewer showed FileWrittenArtifact envelope, not file content
file_written artifacts store a JSON envelope ({path, contentHash, size, mode})
whose contentHash points at the real bytes in CAS. The viewer rendered the
envelope itself. StreamQueries now detects the envelope shape (string "path" +
"contentHash") and dereferences the inner hash, falling back to the raw string
when parsing fails or the inner hash is missing (WARN logged — likely evicted).
2026-06-10 20:54:29 +04:00
kami 883e23dec7 feat(router,inference,config): pin narration to a configured model_id
[router.narration] model_id selects which provider handles narration turns
instead of generic capability routing — lets a small/fast model own narration
while the main model keeps CHAT/STEERING.

- NarrationSettings.modelId parsed from TOML, threaded via RouterConfig
  .narrationModelId into DefaultRouterFacade.narrate (3-arg route)
- DefaultInferenceRouter: exact-id match against healthy providers, with a
  post-select health check; any miss logs WARN and falls back to capability
  routing (never fails the narration turn)
- onUserInput unchanged — only narrate uses the pinned model (tested)
2026-06-10 20:53:28 +04:00
kami cc6b402a23 fix(server,kernel,events): harden artifact read + restore workspace on reopen + cleanups
Follow-ups spotted while fixing the artifact-content bug:

- #1 robustness: listArtifacts resolves content via runCatching, degrading to null
  instead of failing the whole listing on one bad CAS read (resolveContent helper)
- #2 eviction visibility: WARN when a referenced artifact hash resolves to null
  (most likely evicted/compacted while still referenced by the event log)
- #3 ordering trap: document on ArtifactCreated/ArtifactContentStored that 'Created'
  fires at validation, AFTER ContentStored at inference — the latent fold hazard
- #4 reopen gap: SessionSnapshot now carries workspaceRoot (derived from
  SessionWorkspaceBound), so a reopened session restores its workspace label
- #5 orchestrator headroom: isBackEdge externalised to a top-level function so
  DefaultSessionOrchestrator drops below the TooManyFunctions threshold (was at it)

#6 (orphaned empty F-010-era artifacts) needs no code change — empty content is
already normalised to null ('(no content stored)'); emission is prevented going
forward by the shipped F-010/F-018/F-020 fixes.
2026-06-10 12:58:28 +04:00
kami 5f7b6f1f18 fix(server): artifact viewer dropped content hash due to event ordering
Root cause of empty artifact content (even for role_pipeline sessions with intact CAS
data): ArtifactContentStored is emitted at inference time, BEFORE ArtifactCreated (which
fires at validation). listArtifacts only applied the content hash via accs[id]?.let{} —
a no-op because the artifact wasn't in the map yet — so the hash was silently dropped and
every artifact showed blank.

Verified against the live store (session 4679ed1e): all 5 artifacts had hash=none before,
now resolve to their validated content (analysis 867B, design 1074B, impl_plan 966B,
patch 574B, review_report 369B).

- ArtifactContentStored now seeds the accumulator (getOrPut), order-independent
- fold extracted to pure StreamQueries.planArtifacts for unit testing
- regression test: content hash captured when ContentStored precedes Created
- inference-output fallback retained for genuinely content-less stages
2026-06-10 12:49:11 +04:00
kami 90ba7244d5 fix(server): artifact viewer falls back to raw stage output when no content hash
Artifacts with no ArtifactContentStored mapping (older sessions, or stages where the
slot->hash link was lost) showed a blank pane. listArtifacts now falls back to the
stage's last InferenceCompleted output so the operator can still see what the stage
produced. Empty-string content is normalised to null so the viewer shows a clear
'(no content stored)' instead of a blank pane.

Verified: role_pipeline sessions already resolve content from CAS (index + segments
intact, store rooted at ~/.config/correx/artifacts); the previously-blank sessions are
healthcheck/degenerate runs whose inference output was genuinely empty.
2026-06-10 12:35:07 +04:00
kami 3c5c2999d3 fix(server,tui): restore output panel + full event history on session reopen
Reopening a session after relaunch sent only a thin summary snapshot, so the output
panel was blank and the event list showed at most 7 entries (of a 100+ event run).

- SessionSnapshot now carries lastOutput/lastResponse, resolved from the last
  InferenceCompleted artifact in CAS; tui-go onSnapshot restores the output panel
- recentEvents cap raised 7 -> 200 so a normal session's event list isn't truncated
- resolveLastOutput extracted as a helper

Note: artifact content is unaffected — verified intact and CAS-retrievable for
role_pipeline sessions (artread); content-less artifacts occur only in workflows that
record no ArtifactContentStored (e.g. healthcheck/process-result stages).
2026-06-10 12:22:54 +04:00