Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
0ef2547
feat(core): define the invocation opening fact as a RuntimeEvent
Astro-Han Sep 2, 2026
ac2a3d4
feat(runtime): open every invocation with a durable opening fact
Astro-Han Sep 2, 2026
a520e84
feat(storage): enumerate a Session's invocations from the event spine
Astro-Han Sep 2, 2026
72fc4d6
feat(storage): keep a migrated invocation's opening beside the ones e…
Astro-Han Sep 2, 2026
de8a701
refactor(core): record the Run Composition on the ledger instead of t…
Astro-Han Sep 2, 2026
e86f649
refactor(core): make a continuation claim name its target's opening, …
Astro-Han Sep 2, 2026
7ab80fb
refactor(core): match a hosted root against the opening's closed root…
Astro-Han Sep 2, 2026
aa975c5
refactor(core): retire three AgentRunEvent types nothing writes
Astro-Han Sep 2, 2026
bc99558
feat(storage): address, bound and page the invocation inventory
Astro-Han Sep 3, 2026
8e91df4
refactor(core): retire the AgentRunHeader as a record of the run
Astro-Han Sep 3, 2026
1c9d2bf
refactor(runtime): read every run off the invocation spine
Astro-Han Sep 3, 2026
b38fd2d
refactor(runtime-host): project every hosted Turn off the invocation …
Astro-Han Sep 3, 2026
4e0aa1b
test(runtime): assert every run off its invocation, not a header
Astro-Han Sep 3, 2026
e038289
test(runtime): seed and read runs through the event spine
Astro-Han Sep 3, 2026
dd48758
test(runtime): read graph, projection and resume facts off the invoca…
Astro-Han Sep 3, 2026
4c26642
test(runtime): name the runs to scan instead of enumerating headers
Astro-Han Sep 3, 2026
9ee1294
test(runtime): state every run's facts as its own invocation
Astro-Han Sep 3, 2026
69f505d
docs: describe the invocation spine the runtime actually has
Astro-Han Sep 3, 2026
9028bb8
style: apply Biome formatting
Astro-Han Sep 3, 2026
75670a4
fix(runtime): settle a drain-refused start as cancelled, not failed
Astro-Han Sep 3, 2026
fa8f189
fix(runtime): let recovery report only what it repaired
Astro-Han Sep 3, 2026
00fbeb3
test(runtime-host): read each hosted run's facts off its invocation
Astro-Han Sep 3, 2026
f4e6abc
refactor(runtime): keep one name and one fixture for an invocation ou…
Astro-Han Sep 3, 2026
de2a1f9
fix(runtime): leave an invocation open when its ledger states two end…
Astro-Han Sep 3, 2026
94a6d16
refactor(runtime): open an invocation on the spine, not on the operat…
Astro-Han Sep 3, 2026
624ba72
fix(runtime): read a terminal event that omits its class as unknown
Astro-Han Sep 3, 2026
4e21c86
test(runtime): drive steering recovery through the invocation spine
Astro-Han Sep 3, 2026
722efc5
test(runtime): seed invocations that operational tests only implied
Astro-Han Sep 3, 2026
fe469fd
refactor(runtime): finish reading every run off its own events
Astro-Han Sep 3, 2026
2bcbc41
style: apply Biome formatting
Astro-Han Sep 3, 2026
8706669
refactor(runtime): drop the last reads of the retired lifecycle events
Astro-Han Sep 3, 2026
2105183
test(runtime): keep the durable-order reader on the spine
Astro-Han Sep 3, 2026
c1d2835
fix(runtime-host): start the drained Turn outside its admission
Astro-Han Sep 3, 2026
bff9cbc
test: state one invocation the same way everywhere
Astro-Han Sep 3, 2026
5ead74f
test(runtime): read the WorkHub stop outcome off the spine
Astro-Han Sep 3, 2026
9b140f9
fix(storage): migrate every header the header era wrote, endings incl…
Astro-Han Sep 3, 2026
5fb8828
fix(runtime): give a copied run its shelved opening back
Astro-Han Sep 3, 2026
41e4b2d
refactor(core): state an invocation's ending once
Astro-Han Sep 3, 2026
2948c92
perf(storage): read one invocation by run id
Astro-Han Sep 3, 2026
74f0116
test(runtime-host): cover SessionAdmissionGate.detach
Astro-Han Sep 3, 2026
8e862a3
fix(storage): tie a shelved opening to the ledger it describes
Astro-Han Sep 4, 2026
1363390
fix(storage): read an invocation's ending the way every other reader …
Astro-Han Sep 4, 2026
d113577
refactor(storage): ask for a run's first event once, and for its endi…
Astro-Han Sep 4, 2026
52ad1b3
fix(storage): let a Session with a started continuation be purged
Astro-Han Sep 4, 2026
8cd09af
refactor(runtime): hold the test doubles to the seal, and name corrup…
Astro-Han Sep 4, 2026
2cf433a
refactor(runtime-host): read one invocation by run id, not by scanning
Astro-Han Sep 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/desktop/src/main/e2e-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { AGENT_GRAPH_CLIENT_PROJECTION_SCHEMA_VERSION } from '@maka/core/agent-g
import { MODEL_CALL_ATTEMPT_EVENT_TYPE } from '@maka/core/model-call-attempt';
import type { UiLocale } from '@maka/core/ui-locale';
import { createSqliteAgentRunStore } from '@maka/storage/agent-run-store';
import { createWorkspaceRuntimeStore } from '@maka/storage/runtime-event-persistence';
import { createProjectCatalog } from '@maka/storage/project-catalog';
import {
resolveStorageRoot,
Expand Down Expand Up @@ -305,13 +306,14 @@ export async function seedE2eFixture(input: {
// below. It MUST be the lease's canonicalPath, not the raw workspaceRoot —
// a /var vs /private/var realpath difference would open a different DB.
const runStore = createSqliteAgentRunStore(owner.lease.canonicalPath);
const runtimeEventStore = createWorkspaceRuntimeStore(owner.lease.canonicalPath);
try {
const records = usageStatsRecords(now);
// Model calls seed the CANONICAL ledger through the AgentRun event stream;
// tools stay on the legacy telemetry table (there is no canonical tool
// ledger). This is what actually exercises the canonical merge branch.
for (const { header: runHeader, attempt } of records.modelCalls) {
await runStore.createRun(runHeader);
for (const { opening, attempt } of records.modelCalls) {
await runtimeEventStore.appendRuntimeEvent(attempt.sessionId, attempt.runId, opening);
await runStore.appendEvent(attempt.sessionId, attempt.runId, {
id: attempt.attemptId,
type: MODEL_CALL_ATTEMPT_EVENT_TYPE,
Expand All @@ -323,6 +325,7 @@ export async function seedE2eFixture(input: {
});
}
for (const record of records.tools) await usage.telemetry.recordToolInvocation(record);
runtimeEventStore.close();
await runStore.close?.();
// Fold the appended attempts into the read model so the page's first read
// sees canonical usage (production's readCanonicalUsage also repairs).
Expand Down
51 changes: 35 additions & 16 deletions apps/desktop/src/main/e2e-fixture/scenarios-usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
* under the License.
*/

import type { AgentRunHeader } from '@maka/core/agent-run';
import type { RuntimeEvent } from '@maka/core/runtime-event';
import { buildInvocationOpenedEvent } from '@maka/core/runtime-invocation';
import {
MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
type ModelCallAttempt,
Expand Down Expand Up @@ -229,11 +230,11 @@ export function usageStatsSessions(
}

export function usageStatsRecords(now: number): {
modelCalls: Array<{ header: AgentRunHeader; attempt: ModelCallAttempt }>;
modelCalls: Array<{ opening: RuntimeEvent; attempt: ModelCallAttempt }>;
tools: PersistedToolInvocationRecord[];
} {
const sessions = usageStatsSessions(now);
const modelCalls: Array<{ header: AgentRunHeader; attempt: ModelCallAttempt }> = [];
const modelCalls: Array<{ opening: RuntimeEvent; attempt: ModelCallAttempt }> = [];
const tools: PersistedToolInvocationRecord[] = [];
for (const { header: session, messages } of sessions) {
const modelByTurn = new Map(
Expand All @@ -257,19 +258,37 @@ export function usageStatsRecords(now: number): {
// Run/attempt ids must match SAFE_ID_PATTERN ([A-Za-z0-9_-]); no colons.
const runId = `run-${message.id}`;
modelCalls.push({
header: {
runId,
sessionId: session.id,
turnId: message.turnId,
status: 'created',
backendKind: 'fake',
llmConnectionSlug: session.llmConnectionSlug,
modelId,
cwd: '/tmp/e2e-usage',
permissionMode: 'ask',
createdAt: message.ts - 2_000,
updatedAt: message.ts,
},
opening: buildInvocationOpenedEvent({
id: `${runId}-open`,
run: {
sessionId: session.id,
invocationId: runId,
runId,
turnId: message.turnId,
},
openedAt: message.ts - 2_000,
opening: {
kind: 'invocation_opened',
protocol: 'invocation_opened_v1',
route: {
provenance: 'runtime',
backendKind: 'fake',
llmConnectionId: session.llmConnectionSlug,
llmConnectionSlug: session.llmConnectionSlug,
modelId,
},
configuration: {
cwd: '/tmp/e2e-usage',
permissionMode: 'ask',
collaborationMode: 'agent',
orchestrationMode: 'default',
orchestrationSource: 'session',
toolMode: 'direct',
},
root: { kind: 'user' },
source: { kind: 'fresh' },
},
}),
attempt: {
schemaVersion: MODEL_CALL_ATTEMPT_SCHEMA_VERSION,
logicalCallId: message.id,
Expand Down
24 changes: 10 additions & 14 deletions docs/architecture/runtime-core-architecture-draft.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ Maka is not implementing Kafka inside one process, nor does it claim that Runtim

> **Log is the source of truth; state is a materialized view.**

That principle directly explains the most important terminal invariant later in this chapter: a Run header cannot declare completion on its own; a terminal RuntimeEvent must support it.
That principle directly explains the most important terminal invariant later in this chapter: nothing declares that a Run ended except the Run's own terminal RuntimeEvent.

## Three lifecycle identities, plus one correlation field

Expand Down Expand Up @@ -221,13 +221,12 @@ It is an orchestration boundary, not the model loop. A Backend should not own th

`AgentRun` gives one execution a durable identity and lifecycle. At startup it:

1. creates an `AgentRunHeader` in `created` state;
1. commits the invocation's opening fact as a RuntimeEvent;
2. writes the user message and a `running` Turn projection for a top-level Run;
3. writes the initial user `RuntimeEvent`;
4. locks the Session's connection configuration;
5. ensures a Backend exists and registers the active Run;
6. marks the Run as `running`;
7. builds model history from earlier RuntimeEvent ledgers.
6. builds model history from earlier RuntimeEvent ledgers.

While execution is active, `AgentRun` receives both legacy `SessionEvent`s and canonical `RuntimeEvent`s and writes each to the projection or ledger it belongs to. At the end, it unregisters the active Run, converges Session and Turn state, and commits the final Run state.

Expand Down Expand Up @@ -323,12 +322,12 @@ The important point is that permission is not a UI-only pause. Requests and deci

## One semantic truth, two supporting forms of state

Maka currently maintains three forms of durable data. They are not three equal sources of truth, nor do they store the same chat three times. `RuntimeEventStore` is the canonical semantic log of AI interaction; the other stores carry product projections and operational Run state.
Maka currently maintains three forms of durable data. They are not three equal sources of truth, nor do they store the same chat three times. `RuntimeEventStore` is the canonical semantic log of AI interaction; the other stores carry product projections and the operational record of what the runtime did.

| Store | Main contents | Question it answers best |
|---|---|---|
| `SessionStore` | `StoredMessage`s for users, assistants, tools, and Turn state | What should the UI and compatibility APIs display? What is the current in-flight projection? |
| `AgentRunStore` | Run header and operational Run events | When did this Run start, what is its state, and at which model or tool stage did it fail? |
| `AgentRunStore` | operational Run events | At which model or tool stage did this Run do what, and where did it fail? |
| `RuntimeEventStore` | canonical RuntimeEvents plus bounded partial snapshots | Which semantic facts occurred, and how should other state be rebuilt from them? |

The current implementation is backed by SQLite rather than a directory per Run: `AgentRunStore` and `RuntimeEventStore` both sit on the same operational state database, and RuntimeEvents land in the `runtime_events` table. Order is carried by that table's `event_seq` under a `(invocation_id, event_seq)` uniqueness constraint, so sequence numbers never repeat within one correlated execution stream — that constraint is what "ordered log" means at the storage layer. Session, Turn, Run, and the compatibility correlation field each occupy their own column, so "what happened in this Turn of this Run of this Session" is an indexed lookup.
Expand All @@ -343,26 +342,23 @@ Streaming text and thinking deltas are not appended forever to immutable JSONL.

One of the hardest runtime failure classes is disagreement about whether an execution ended. For example:

- The Run header says completed, but the RuntimeEvent ledger has no terminal event;
- the user stopped the Run, but a late complete event rewrites the Session to active;
- the Backend stream exhausts without saying whether it succeeded or failed;
- the terminal event is durable, but the process crashes before updating the Run header.
- a second writer tries to end a Run that has already ended.

Maka protects this core invariant:

> A terminal Run must have exactly one valid terminal RuntimeEvent, and a terminal Run header must be supported by that terminal fact.
> A Run ends exactly once, and its terminal RuntimeEvent is the only statement that it ended.

`AgentRun` therefore requires the terminal RuntimeEvent to be durable before committing a terminal Run header. A Backend stream without a terminal event becomes a `missing_terminal_event` failure. Duplicate terminal events are coalesced. Terminal events with a mismatched status, a different Run identity, or `partial: true` are rejected.

If the terminal RuntimeEvent exists but an interrupted header remains `running`, the read model can treat the event as the stronger fact and recovery can repair the header. In the opposite direction, if a header claims termination without a trustworthy terminal fact, the system does not blindly trust the header; it conservatively repairs the Run as a `missing_terminal_event` failure.
There is no separate record of the outcome to keep in step, so a crash cannot leave one saying the Run finished while the other says it is still running. A Backend stream without a terminal event becomes a `missing_terminal_event` failure. Duplicate terminal events are coalesced. Terminal events with a mismatched status, a different Run identity, or `partial: true` are rejected.

This invariant means recovery does not need to guess what the model intended to do next. It only needs to determine which facts are durable and converge all projections on one explainable outcome.

## How stop, errors, and crashes converge

### User stop

`RuntimeKernel.stopSession()` first marks active `AgentRun`s as stopped, then calls Backend `stop()`. `AiSdkBackend` aborts the provider stream, ends any pending sandbox boundary or user question, and emits abort/complete events. Even if a provider later produces a complete or error event, `RuntimeKernel` and `AgentRun` do not allow it to overwrite the established aborted semantics. The stop source, such as the renderer stop button, is retained in the terminal fact and Run header for diagnostics.
`RuntimeKernel.stopSession()` first marks active `AgentRun`s as stopped, then calls Backend `stop()`. `AiSdkBackend` aborts the provider stream, ends any pending sandbox boundary or user question, and emits abort/complete events. Even if a provider later produces a complete or error event, `RuntimeKernel` and `AgentRun` do not allow it to overwrite the established aborted semantics. The stop source, such as the renderer stop button, is retained in the terminal fact for diagnostics.

### Provider or runtime error

Expand Down Expand Up @@ -393,7 +389,7 @@ Continuing execution is a separate path. `safe_boundary_continuation` resumes fr
- `AiSdkBackend` remains large and coordinates history, context budgets, tool availability, the step loop, usage, and telemetry.
- The mapper is still a legacy-to-canonical bridge rather than consuming native RuntimeEvents from the Backend.
- `SessionStore` and RuntimeEvent projection must cooperate for active and in-flight reads.
- Startup recovery performs deterministic termination and repair, not arbitrary warm resume. Continuation is a separate path: `safe_boundary_continuation` resumes from a verified safe boundary, is marked by `continuationSource` on the Run header, and is admitted and dispatched by `RuntimeKernel`; see [Chapter 8](./runtime-resume-architecture.md) for the difference.
- Startup recovery performs deterministic termination and repair, not arbitrary warm resume. Continuation is a separate path: `safe_boundary_continuation` resumes from a verified safe boundary, is marked by the continuation source on the invocation's opening fact, and is admitted and dispatched by `RuntimeKernel`; see [Chapter 8](./runtime-resume-architecture.md) for the difference.

These are real architecture boundaries, not details to hide. Future Backend decomposition or checkpoint work must preserve request shape, tool visibility, event order, and the terminal invariant before optimizing for smaller files.

Expand Down
Loading