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.
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.
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.
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).
[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)
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.
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
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.
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).
Implements the replay-safe half of preemptive redirect (graph re-routing). An
operator-confirmed PreemptRedirectEvent overrides the resolver's edge at the next
transition boundary; the pure resolver stays untouched.
- PreemptRedirectEvent / PreemptRedirectBlockedEvent (+ serialization registration)
- TransitionExecutedEvent + TransitionDecision.Move gain optional redirectId — the
durable 'consumed once' marker
- PreemptRedirect.decide: pure decision over the event log (override / block / none),
so replay reaches the identical outcome without re-classifying (invariant #8)
- override wired in DefaultSessionOrchestrator.step above resolveTransition; back-edge
jumps count against the existing maxRetries cap via executeMove
- blocks jumps to unknown stages or targets with unsatisfied needs
- DomainEventMapper: redirect events mapped to null (operator surface ships with the
LLM-proposal + approval-confirm front-half)
- tests: override+consume-once, block-on-needs, block-on-unknown, ignore-consumed
Front-half (LLM proposal -> approval-gate confirm -> emit PreemptRedirectEvent) is
deferred; needs live LLM verification. Until it ships no redirect event is emitted in
production, so the override is inert. Spec: docs/plans/2026-06-10-freestyle-graph-rerouting.md
A multi-line paste put raw newlines into the single-line input slot, overflowing
the fixed-height box and leaving a smeared/stale region. flattenForDisplay collapses
newlines (↵) and tabs for rendering only; the inputBuffer keeps real characters for
submission.
- generic SystemResourceProbe: owner-agnostic /proc/meminfo system RAM
overlaid on the vendor GPU probe, wired on both managed and static paths
so the VRAM/GPU/RAM gauge renders with an external llama-server (was dormant)
- F-002: classify provider 4xx (e.g. llama.cpp 400) as terminal, not retryable
(except 408/429); stops retrying deterministic request failures to exhaustion
- F-003: FileSystemPromptLoader expands leading ~ / ~/ to user home
- F-004: map InferenceFailedEvent + RetryAttemptedEvent to new
ServerMessage.InferenceFailed / RetryAttempted, decoded+rendered in tui-go;
ArtifactContentStoredEvent explicitly mapped to null (internal, no warn)
- tests: tilde expansion, SystemResourceProbe (static/managed/fail-soft)
Two operator features over the existing WebSocket protocol, plus a tui-go nav fix.
Config editor (g / palette "config"):
- core:config gains a thin registry stack: OrchestrationKnobs ([orchestration]
block), CorrexConfigWriter (round-trip TOML serializer; parseToml(write(c))==c),
ConfigHolder (AtomicReference live config), and EditableConfig (allowlist of
editable fields + patch applier; security fields are absent so they can never
be patched).
- ConfigService validates -> persists (atomic temp+move) -> swaps the holder ->
rebuilds config-derived services. Live-apply is scoped to safe seams: stage
timeout, compaction threshold (now a supplier), router knobs (routerFacade
rebuild), and personalization/project toggles all take effect on the next
session/turn; in-flight sessions keep the config they started with.
server.host/port are persisted + badged "restart required", not hot-swapped.
- Protocol: GetConfig / UpdateConfig / ConfigSnapshot; TUI overlay with
type-aware editing (bool toggle, enum cycle, inline int/string edit) and save.
Artifact viewer (v / palette "artifacts"):
- Server assembles a session's artifacts from the event log and resolves bytes
from the CAS store (StreamQueries.listArtifacts) — a replay-neutral snapshot
read. TUI overlay lists artifacts and shows scrollable content.
tui-go fix: workflow failure no longer clears selectedID (which yanked the user
to the list); listNav lands on the first session when nothing is selected
instead of wrapping to a random one.
Config is not event-sourced (it lives in TOML); editing stays outside the log.
rehydrate() now scans ArtifactContentStoredEvent (the durable slot->CAS-hash
bridge) and fetches stored bytes by content hash, keying the cache by the
logical slot name. The slot name is never passed to the hash-keyed CAS, so the
odd-length-hex crash is gone and artifact content is restored on cold-start
resume. RehydrateTest reworked to use a content-addressed fake store (slot name
!= content hash) so it actually exercises the bug. Replay (#8) is unaffected —
rehydrate is live-resume-only.
Also bump router narration maxTokens 1024 -> 4096: reasoning models were
spending the whole budget thinking and emitting empty content (finishReason=
length), so narrations never reached the TUI.
Removes the stale TUI-refactor progress doc.
Live QA audit of role_pipeline against a sample repo surfaced and fixed a
chain of defects that prevented any tool+artifact workflow from running
end-to-end. The pipeline now completes cleanly with a real, validated,
reviewed file change.
- F-008 workflow: propagate stage token_budget -> generationConfig.maxTokens
(TomlWorkflowLoader) so large stages stop truncating at the 2048 default.
- F-009 tools/kernel: file_read missing-file is recoverable; recoverable tool
errors feed back into the loop instead of aborting the stage.
- F-010/F-018 kernel: reject premature stage_complete and nudge the model to
emit the owed artifact / invoke a write tool (bounded by MAX_TOOL_ROUNDS).
- F-011 schemas: list-typed artifact fields string -> array (analysis/design/
impl_plan) to match model output.
- F-012 kernel: strip an outer markdown code fence from LLM artifacts.
- F-013 validation: payload-validation failures are retryable; structural
failures stay terminal (wires the invariant-#7 reject+retry contract).
- F-014 tools: file_edit schema matches its real params; edits emit a diff.
- F-015 kernel: record tool-execution events + materialise a real file_written
artifact from the on-disk write; gate validation so a no-write stage cannot
be rubber-stamped (no more false-success runs).
- F-016 server: raise stageTimeoutMs 60s -> 180s for slow local models.
- F-019 artifacts/kernel: file_written artifact carries the unified diff so the
reviewer can verify the change (shared DiffUtil; file_write emits a diff too).
- F-020 tools: model-correctable file_edit failures are recoverable + steer
toward replace/file_write over the fragile patch op.
Full findings register (F-001..F-021, incl. open F-021 resume rehydrate bug)
in qa/audit-report-2026-06-08.md. All module tests green.
ArtifactPayloadValidator passed the logical slot name into the
content-hash-keyed CAS store, crashing every workflow with a typed
produces slot. Validator now reads payloads from a content map threaded
through ValidationContext (populated from the orchestrator's per-session
artifact cache across all stages) and no longer depends on ArtifactStore.
Also records the slot->CAS-hash mapping as a new ArtifactContentStoredEvent
(registered for serialization), emitted at both CAS write sites, so
artifact content is recoverable from CAS by hash after a cold start.
Two fixes surfaced by the 2026-06-08 QA audit running role_pipeline on a local
llama-server.
F-001: LlamaCppInferenceProvider sent both a GBNF grammar and tools on the same
request; llama.cpp rejects the combination ("Cannot use custom grammar
constraints with tools"), failing every stage that has tools and produces a
schema-constrained artifact. Drop the grammar when tools are present and rely on
post-hoc schema validation + retry (invariant #7 still holds). TODO left for a
first-class emit_artifact tool as the proper fix.
F-005: FileReadTool resolved relative paths via toAbsolutePath() against the JVM
process cwd instead of the bound workspace, so the file-read jail anchored to the
wrong root and disagreed with the Plane-2 PATH_CONTAINMENT check. Add workingDir
to FileReadConfig, wire workspace.workingDir in the per-workspace tool builder,
and give FileReadTool a resolvePath() mirroring FileWriteTool/FileEditTool.
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.
Add docs/schemas/execution_plan.json (goal/stages/edges), the
architect_freestyle prompt that instructs emitting the execution pipeline as
JSON, and register the execution_plan llm-emitted kind in both
artifacts.config.toml and sample-config.toml. Validated via
ExecutionPlanSchemaValidationTest (minimal plan passes; missing stages rejected).
Extract a SubagentRunner fun-interface (SubagentRunRequest/SubagentRunResult)
and an InSessionSubagentRunner default that delegates to the existing
executeStage path, with per-run effectives captured in the lambda closure so
the seam stays free of the internal RunEffectives type.
DefaultSessionOrchestrator.enterStage now dispatches through subagentRunner.run
instead of calling executeStage inline; effectives threading is dropped from
run/step/executeMove/enterStage/resume. ReplayOrchestrator supplies its own
runner. Behaviour is identical — RefinementLoopTest and full check stay green.
Stages declaring needs=[...] now receive each needed artifact's validated
JSON (from artifactContentCache) as an L1/USER neededArtifact context entry,
so a one-shot subagent sees its inputs.
Also emit llm-emitted artifact lifecycle events (Created/Validating/Validated)
on successful validation via emitLlmArtifacts, closing a latent gap where
verifyProduces would fail for stages producing only an llm-emitted artifact.
Events are emitted only after validation passes, preserving invariant #7.