Commit Graph

79 Commits

Author SHA1 Message Date
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 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 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 f1fc43cc85 feat(router,server): wire workflow inventory and project profile into router context per turn 2026-06-11 13:15:32 +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 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 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 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 bb70c94a99 feat(kernel,events): freestyle graph re-routing — deterministic override core
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
2026-06-10 11:51:47 +04:00
kami ee24b7786a fix(kernel,server,tui,inference): generic resource gauge + F-002/F-003/F-004
- 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)
2026-06-10 11:07:08 +04:00
kami 89487db72a fix(kernel,router): rehydrate by CAS content hash (F-021) + narration budget
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.
2026-06-09 10:14:04 +04:00
kami b830528d31 feat(kernel,server): rehydrate() + POST /sessions/{id}/resume 2026-06-08 10:13:33 +04:00
kami 4d1b4ffbb6 feat(freestyle): soft-lock steering preemption (Slice 5)
- DecisionKind gains PREEMPT; DecisionJournalState tracks planLocked.
- DefaultDecisionJournalReducer: ExecutionPlanLockedEvent flips planLocked
  (no record); post-lock steering notes recorded as PREEMPT with a
  'PRIORITY DIRECTIVE' summary, pre-lock stay STEERING.
- SessionOrchestrator.buildSteeringNoteEntries: post-lock notes surfaced as
  pinned L0/SYSTEM 'PRIORITY DIRECTIVE' entries ahead of the plan; pre-lock
  keep L2. Graph re-routing intentionally deferred (priority surfacing only).
- Tests: PreemptSteeringJournalTest, SteeringPreemptionTest.
2026-06-08 02:58:21 +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 6c8c5e2ad9 feat(workflow): ExecutionPlanCompiler + ExecutionPlanLockedEvent (Slice 3)
- ExecutionPlanLockedEvent: registered, replay-deterministic record of the
  locked plan (CAS ref + compiled stage/start ids).
- ExecutionPlanCompiler: validated execution_plan JSON -> WorkflowGraph,
  reusing ConditionSpec.toCondition(); inline prompts via metadata[promptInline];
  malformed plans throw WorkflowValidationException.
- Replay test: locked event + plan JSON recompiles to the identical graph,
  no architect re-run, no FS reads.
2026-06-08 02:33:57 +04:00
kami 1b9896d2fa feat(kernel): SubagentRunner seam for stage dispatch
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.
2026-06-08 02:18:23 +04:00
kami 63e5bcea93 feat(kernel): inject needs-artifact content into stage context
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.
2026-06-08 01:12:38 +04:00
kami fe561ada09 feat(server): intent input channel — StartSession.input seeds decision journal 2026-06-04 02:16:23 +04:00
kami d552148048 feat(journal): ProjectMemoryDistiller — pure repo-scoped decision distillation 2026-06-04 01:00:49 +04:00
kami 440c96659d feat(events): RepoMapComputedEvent as recorded environment observation 2026-06-04 00:59:27 +04:00
kami fac6a29178 feat(kernel): runtime refinement guard on back-edges with iteration cap 2026-06-04 00:54:37 +04:00
kami 9d1fe397f1 feat(events): RefinementIterationEvent + orchestration state counter 2026-06-04 00:50:16 +04:00
kami bcc20509e0 feat(kernel): inject decision journal into stage context + wire repository 2026-06-04 00:48:07 +04:00
kami c21bbda85f feat(journal): core:journal module — decision journal projection 2026-06-04 00:35:11 +04:00
kami e6084dcbda feat(router): proper grounded system prompts for chat and narration
Replace the placeholder "You are a routing assistant" one-liner with two
purpose-written prompts: a conversational prompt that frames the router as
correx's operator-facing interface to the event-sourced engine (grounded in
workflow state, no fabrication, concise, steering-aware), and a dedicated
narrator prompt for buildNarrationContext that asks for one or two present-tense
sentences naming the stage and outcome.

The longer protected frame shifted token budgets, so the two L2 eviction tests
now size their budget from the measured protected-frame + per-entry cost instead
of hard-coded magic numbers, making them robust to prompt length.
2026-06-03 22:44:32 +04:00
kami ad3ec1965d fix(router,kernel): narration user turn + stage transition ordering
Two issues surfaced during live QA once workflows began progressing past the
first stage:

1. Narration prompts went out with no user message, so the chat template
   rejected them ("No user query found in messages"). buildNarrationContext
   filed the trigger instruction (a user turn) under ContextLayer.L0, and
   PromptRenderer folds every L0 entry into the system message regardless of
   role. Move the trigger to L1 so it renders as the user turn.

2. TransitionExecutedEvent was emitted after the next stage had already run:
   executeMove called enterStage (which executes the stage) before advanceStage
   (which emits the transition), so the event log showed a stage running before
   the event marking entry into it. Emit the transition before entering.

Adds a regression test asserting a rendered narration prompt carries a user
message.
2026-06-03 22:38:12 +04:00
kami 8fe3a504bb fix(context): preserve chronological turn order through pack assembly
Tool-calling loops were rendered as all-assistant-calls-then-all-tool-results
with the original task last, causing the model to re-issue the same tool call
indefinitely. Two stacked reorderings caused it:

- DefaultContextPackBuilder grouped entries by sourceType for per-type
  compression, discarding a,t,a,t interleaving.
- PromptRenderer forced L1 (the live user turn) to render last, pushing the
  task after the entire transcript.
- SessionOrchestrator's tool loop re-fed currentContext.layers.values.flatten()
  (grouped by layer) each round, compounding the scramble.

Add a chronological `ordinal` to ContextEntry, stamped by the builder from
input order and restored after grouping/compression (the compressor preserves
entry identity, so ordinals survive). PromptRenderer now orders non-system
messages by ordinal, with the old L1-last layer priority kept only as a
tiebreak so router chat (ordinal 0) is unchanged. The orchestrator keeps a
running ordered accumulator instead of reading back the grouped pack.

Adds builder + renderer ordering regression tests.
2026-06-03 22:22:52 +04:00
kami 38b007a6cd feat(router): Task 3.2 — NarrationTrigger + buildNarrationContext + RouterFacade.narrate
- Add NarrationTrigger(kind, instruction) model to core:router
- Add RouterContextBuilder.buildNarrationContext: minimal ContextPack
  (system + workflow status + L2 + trigger instruction); excludes
  conversationHistory and L3 recalled memory
- Add RouterFacade.narrate: emits RouterNarrationEvent with content,
  latencyMs, tokensUsed; skips event on blank inference; never writes
  ChatTurnEvent or l3MemoryStore.store
- Add RouterNarrationTest covering all acceptance criteria
- Add narrate default to RouterContextBuilder interface so existing
  anonymous test stubs compile without a stub override
2026-06-03 15:52:08 +04:00
kami 8d8b2914e0 feat(router): capture latency + tokens on ROUTER ChatTurnEvent
ChatTurnEvent gains nullable latencyMs and tokensUsed fields (defaults preserve
backward-compat; legacy JSON without them deserializes cleanly). emitChatTurn
accepts optional metrics; the ROUTER emit site passes inferenceResponse values
through; the USER emit site leaves both null.
2026-06-03 15:11:37 +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 57cf6f09f4 feat(kernel): enforce stage allowedTools at tool-call dispatch
A stage's allowedTools only shaped which tool definitions were offered to
the model; the dispatch path resolved returned tool calls straight from the
full registry and executed them without checking stage membership. A
hallucinated, jailbroken, or edited-transcript tool call for any registered
tool would run unchecked (violating invariant #5 / policy-is-absolute).

dispatchToolCalls now rejects any tool call whose name is not in the stage's
allowedTools (STAGE_COMPLETE_TOOL exempt), emitting ToolExecutionRejectedEvent
and feeding an error ContextEntry back to the model instead of executing.
Empty allowedTools means deny-all domain tools, consistent with offering
only STAGE_COMPLETE_TOOL at inference time.
2026-06-03 13:16:47 +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 d8e36b8282 feat(kernel): make approval steering actually affect the run
A steering note attached to a tool approval was recorded but only
consumed by the NEXT stage's prompt build (buildSteeringNoteEntries runs
once, before the tool loop), so on a single-stage task it had no visible
effect. Two changes:

- Approve+note: capture userDecision.userSteering at the gate and inject
  it as a context entry right after the tool result, so the same stage's
  loop re-infers with the note and the model acts on it.
- Reject: buildSteeringNoteEntries now also emits anti-repeat feedback for
  REJECTED decisions with no note, so the model does not re-propose the
  identical call on the retryable re-run (a bare reject previously fed
  back only "approval denied").

Integration test drives an approval with a steering note and asserts the
next inference's context carries it.
2026-06-03 01:44:08 +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 d1dc9e2f5c fix(kernel): require tool activity for produce-less stage completion
A stage declaring no `produces` passed verifyProduces vacuously, letting an
agent stage_complete with zero work. Gate on allowedTools: a produce-less stage
with tools available must have >=1 ToolInvocationRequestedEvent scoped to its
stageId, else Failure(retryable). Produce-less + no tools is exempt (only
stage_complete is ever offered), so pure-reasoning stages aren't bricked.
2026-06-02 19:56:34 +04:00
kami 1dc7eae8a1 feat(router): enrich L2 decision points with steering and failure reason
Fix broken stage-summary interpolation ($payload.stageId rendered the event's toString()). RouterL2Entry gains reason + steeringNotes; the reducer folds SteeringNoteAddedEvent into pendingSteeringNotes and harvests them into the completing stage's L2 entry, then clears (adr-0003 §4, pure field extraction, zero inference). RouterContextBuilder renders summary + reason + steering.
2026-06-02 13:58:41 +04:00
kami b976a5c92a feat(kernel): emit ContextTruncatedEvent on workflow context overflow
DefaultContextPackBuilder already enforced the token budget, but SessionOrchestrator discarded the entriesDropped signal at both build sites — overflow was enforced yet unobserved. Emit ContextTruncatedEvent when entriesDropped>0, matching the router path (open-threads 6.2 contract).
2026-06-02 13:58:06 +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 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 d03479cea7 feat: tool-declared output compression (Tier A)
Tools can declare how their output is compressed before it enters the
context pack, replacing uniform handling. Open-threads 6.3 / ADR-0003.

- core:tools/compression: ToolOutputCompressor interface,
  IdentityCompressor default, declarative OutputCompressionSpec +
  CompressionRule (StripBlankLines, StripLeadingWhitespace,
  DropMatching, HeadTail), and the pure DeclarativeCompressor engine
  (regexes compiled at construction; bypassOnError passes raw on
  non-zero exit; never throws on input).
- Tool gains `outputCompressor` (default IdentityCompressor). FileReadTool
  strips blanks + leading whitespace; ShellTool strips blanks + head/tail.
- SessionOrchestrator applies the resolved tool's compressor on the
  ToolResult -> ContextEntry path only. Raw output stays authoritative
  in the event log (invariant #6); compression is a pure function of
  (raw, exitCode, spec), so no new event and replay stays deterministic.
- Spec is @Serializable/config-shaped (JSON round-trip tested). No
  custom-tool-from-config path exists today, so first-party tools wire
  in code; TOML-declared compression rides along when that path lands.

Tier B (format-aware parsers: failures-only test output, git-log oneline)
is a documented per-tool extension behind the interface, not built here.
2026-05-31 21:20:16 +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