| # Runtime Kernel Extraction |
| |
| > Archived on 2026-07-13. This is a point-in-time extraction record, not current architecture authority. Start with `ARCHITECTURE.md`. |
| |
| This document explains the runtime-kernel work in this change set: what changed, |
| why it was needed, what stayed stable, and how it was verified. |
| |
| ## Summary |
| |
| Maka already had the pieces of a local desktop coding agent: sessions, model |
| streams, tool calls, permission prompts, abort handling, usage telemetry, bot and |
| gateway entry points, and persisted session messages. The problem was that the |
| core execution responsibilities were still concentrated in a few large runtime |
| paths, especially `AiSdkBackend` and `SessionManager.sendMessage()`. |
| |
| This change set keeps the product surfaces stable but introduces clearer |
| internal runtime boundaries: |
| |
| ```text |
| SessionManager |
| -> AgentRun |
| -> AiSdkBackend |
| -> ModelAdapter |
| -> ToolRuntime |
| -> RunTrace |
| -> AgentRunStore |
| ``` |
| |
| The intent is not to rewrite the runtime or replace the Vercel AI SDK. The goal |
| is to make the existing runtime easier to reason about, easier to recover after |
| interruption, and easier to extend with future backends or workflow integrations. |
| |
| ## What Changed |
| |
| ### ToolRuntime |
| |
| `ToolRuntime` is now the internal boundary for the lifecycle of model-requested |
| tools. The extracted runtime owns the work that used to be interleaved inside the |
| AI SDK backend: |
| |
| - validate tool input before execution |
| - evaluate permission policy |
| - wait for parked permission decisions with timeout behavior |
| - pause and resume the stream watchdog around tool execution |
| - propagate abort signals into tools |
| - classify tool failures |
| - record tool telemetry and artifacts |
| - emit tool and permission trace events |
| |
| `AiSdkBackend` still bridges model stream behavior, but it no longer needs to own |
| every detail of tool execution. |
| |
| ### ModelAdapter |
| |
| `ModelAdapter` is the provider-facing stream and error normalization layer. It |
| keeps AI SDK-specific stream chunks, provider setup, usage normalization, and |
| provider error mapping out of the higher-level backend orchestration shell. |
| |
| This makes the boundary explicit: |
| |
| ```text |
| provider / AI SDK details |
| -> ModelAdapter |
| -> Maka runtime events and usage records |
| ``` |
| |
| That separation matters because future providers should not need to duplicate |
| permission, tool, run, or session-state behavior. |
| |
| ### RunTrace |
| |
| `RunTrace` is an internal best-effort trace path for runtime events. It records |
| milestones such as: |
| |
| - turn started |
| - model resolved / stream started / stream completed / stream failed |
| - tool started / completed / failed |
| - permission requested / decided / failed |
| - usage recorded |
| - abort requested |
| |
| Trace recorder failures are intentionally non-fatal. A failed trace write must |
| not alter model or tool execution. |
| |
| ### AgentRun Types and Store |
| |
| The core package now defines internal AgentRun contracts: |
| |
| - `AgentRunHeader` |
| - `AgentRunEvent` |
| - `AgentRunStatus` |
| - `AgentRunStore` |
| |
| The storage package adds a file-backed run store: |
| |
| ```text |
| sessions/<sessionId>/runs/<runId>/run.json |
| sessions/<sessionId>/runs/<runId>/events.jsonl |
| ``` |
| |
| The store provides: |
| |
| - atomic `run.json` writes |
| - append-only run event JSONL |
| - same-run write serialization |
| - session run listing |
| - corrupt committed event-line recovery via `event_corrupt` |
| - malformed unterminated tail tolerance |
| |
| This ledger is separate from the existing session message JSONL so runtime |
| diagnostics and recovery state do not pollute user-visible conversation history. |
| |
| ### AgentRun Execution |
| |
| `SessionManager.sendMessage()` now delegates the heavy turn lifecycle to |
| internal `AgentRun.execute()`. |
| |
| `AgentRun` owns: |
| |
| - generating and recording run identity |
| - appending the user message |
| - writing initial turn state |
| - locking the connection snapshot for the run |
| - ensuring the active backend exists |
| - driving backend stream events |
| - projecting session status changes |
| - writing turn completion, failure, abort, or permission-wait states |
| - recording run started/completed/failed/cancelled status |
| - routing `RunTrace` events into the durable run ledger |
| - preserving abort source diagnostics such as `renderer.stop_button` |
| |
| `SessionManager` remains the public runtime API and continues to own session |
| CRUD, backend registry orchestration, active run lookup, and legacy recovery |
| entry points. |
| |
| ### Active Run Registry |
| |
| The old active stream counters and turn-id side maps were replaced with: |
| |
| ```ts |
| activeRuns: Map<string, AgentRun> |
| turnToRunId: Map<string, string> |
| ``` |
| |
| This makes overlapping run behavior explicit. It also avoids masking active |
| stream accounting bugs with defensive counter clamping. |
| |
| ### Startup Recovery |
| |
| `recoverInterruptedSessions()` now prefers the AgentRun ledger when run rows are |
| available. It scans persisted run headers and events, classifies stale |
| non-terminal runs, repairs them, and then converges the existing session/turn |
| projection. |
| |
| Recovered cases include: |
| |
| - stale `created` or `running` runs |
| - runs whose last event is `run_started` or `model_stream_started` |
| - stale tool tails after `tool_started` |
| - stale permission waits after `permission_requested` |
| - `model_stream_completed` without a terminal run event |
| - corrupt run event lines represented as `event_corrupt` |
| |
| Legacy sessions without run ledger rows still use the prior message and |
| turn-state recovery path. |
| |
| ## What Stayed Stable |
| |
| This work intentionally does not change: |
| |
| - `window.maka.*` preload API |
| - Electron IPC channel names |
| - renderer-visible `SessionEvent` behavior |
| - user-visible permission modes |
| - session message JSONL compatibility |
| - builtin tool names or public tool behavior |
| - model provider settings UI |
| - bot and OpenGateway public entry points |
| - Rive workflow integration semantics |
| |
| The runtime kernel is internal. Public behavior should remain compatible while |
| the internals become more explicit and recoverable. |
| |
| ## Why This Was Needed |
| |
| Before this work, a single turn execution was spread across session management, |
| backend stream handling, tool wrapping, permission policy, telemetry, and abort |
| logic. That made several questions difficult to answer after a failure: |
| |
| - Did the model stream start? |
| - Which backend/model/connection did the turn use? |
| - Was the run waiting for permission? |
| - Which tool was running when the app exited? |
| - Did the user press stop? |
| - Was usage recorded? |
| - Did a stale session status reflect a real active run or a crashed process? |
| |
| The AgentRun ledger gives the runtime a durable fact record for those answers. |
| ToolRuntime and ModelAdapter then reduce the amount of model/tool/provider logic |
| that has to be understood at once. |
| |
| ## Recovery Model |
| |
| The new recovery path is conservative. It does not replay model streams or tools. |
| Instead, it repairs stale state into deterministic terminal states so the app |
| does not reopen with sessions permanently stuck in `running` or |
| `waiting_for_user`. |
| |
| When a stale non-terminal run is recovered, the runtime: |
| |
| 1. Updates the AgentRun header to terminal state. |
| 2. Appends a durable recovery event such as `run_failed` or `run_completed`. |
| 3. Writes the existing session `turn_state` projection. |
| 4. Updates the session header out of active running/waiting states. |
| |
| The failure class for app-restart recovery is recorded as `app_restarted`. |
| Diagnostics are limited to small reason-code fields such as `recovered`, |
| `failureClass`, `recoveryReason`, `lastEventType`, and `eventCorrupt`; raw user |
| text and raw event payloads are not copied into recovery diagnostics. |
| |
| ## Files Added |
| |
| - `packages/runtime/src/tool-runtime.ts` |
| - `packages/runtime/src/model-adapter.ts` |
| - `packages/runtime/src/run-trace.ts` |
| - `packages/core/src/agent-run.ts` |
| - `packages/storage/src/agent-run-store.ts` |
| - `packages/runtime/src/agent-run.ts` |
| - `packages/runtime/src/agent-run-recovery.ts` |
| |
| ## Tests Added or Expanded |
| |
| The runtime test suite now covers: |
| |
| - ToolRuntime permission allow/block/prompt/timeout paths |
| - watchdog pause/resume behavior around tools |
| - tool abort and failure classification behavior |
| - tool telemetry and artifact recording behavior |
| - ModelAdapter stream, usage, and provider error normalization |
| - RunTrace recording and best-effort failure behavior |
| - overlapping AgentRuns |
| - backend build failure after user-message append |
| - permission handoff without stuck run state |
| - stop button abort source preservation |
| - late complete after stop not overwriting aborted state |
| - durable trace redaction |
| - AgentRunStore create/read/update/list behavior |
| - same-run AgentRun event append serialization |
| - corrupt AgentRun event JSONL recovery |
| - startup recovery from stale AgentRun ledger states |
| - legacy startup recovery fallback |
| - terminal AgentRun recovery idempotency |
| |
| ## Verification |
| |
| The changes were verified with: |
| |
| ```sh |
| npm --workspace @maka/core run typecheck |
| npm --workspace @maka/storage run test |
| npm --workspace @maka/runtime run typecheck |
| npm --workspace @maka/runtime run test |
| npm --workspace @maka/desktop run build:main |
| git diff --check |
| ``` |
| |
| The final runtime suite included 315 passing tests after the AgentRun recovery |
| work. |
| |
| ## Follow-Up Work |
| |
| This PR establishes the internal runtime-kernel shape, but it does not finish |
| every possible cleanup. Good follow-up slices are: |
| |
| - merge usage and cost accounting more tightly with the run ledger |
| - expose an internal run-inspection/debug read model |
| - reduce the remaining `SessionManager` hook surface used by `AgentRun` |
| - consider Gateway/Bot policy unification through the same run policy layer |
| - consider Rive workflow/run mapping only after there is a product need |