Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
06977e5
feat(runtime-host): switch production entrypoints
M4n5ter Aug 7, 2026
132f28a
feat(runtime-host): complete interactive client migration
M4n5ter Aug 7, 2026
b247201
refactor(runtime-host): remove embedded authority debt
M4n5ter Aug 7, 2026
4223ddf
refactor(desktop): remove legacy skill ownership
M4n5ter Aug 7, 2026
2e3bd93
fix(runtime-host): close migration contract gaps
M4n5ter Aug 7, 2026
5b1555b
fix(runtime-host): accept production capability schemas
M4n5ter Aug 7, 2026
8623b5b
fix(runtime-host): preserve new defaults across cutover
M4n5ter Aug 7, 2026
bd02a96
fix(runtime-host): close final cutover gaps
M4n5ter Aug 7, 2026
6c7ac7c
fix(runtime-host): preserve provider create defaults
M4n5ter Aug 7, 2026
6d484e9
fix(runtime-host): complete skill and client cutover
M4n5ter Aug 8, 2026
c1345c3
Merge main and complete Runtime Host client cutover
M4n5ter Aug 8, 2026
57a0bd8
test: stabilize Runtime Host cutover gates
M4n5ter Aug 8, 2026
d23568a
test: observe terminal runtime events deterministically
M4n5ter Aug 8, 2026
aa0cd16
fix: preserve side conversation ownership and steering
M4n5ter Aug 8, 2026
58f65d1
fix: close Runtime Host recovery ownership gaps
M4n5ter Aug 8, 2026
cd4c2cc
fix: restore Runtime Host desktop parity
M4n5ter Aug 8, 2026
ac6e8ed
fix: make Session copy recovery main-owned
M4n5ter Aug 8, 2026
464fbad
Merge remote-tracking branch 'origin/main' into feat/runtime-host-m5-…
M4n5ter Aug 8, 2026
629ae47
fix: make Desktop recovery ownership explicit
M4n5ter Aug 8, 2026
14c4142
fix: close Runtime Host desktop cutover gaps
M4n5ter Aug 8, 2026
6a028cf
fix: wait for Windows verifier resources to release
M4n5ter Aug 8, 2026
6cff3a8
Merge remote-tracking branch 'origin/main' into feat/runtime-host-m5-…
M4n5ter Aug 8, 2026
c014786
fix: close Runtime Host multi-client parity gaps
M4n5ter Aug 8, 2026
b916c3f
fix: harden Runtime Host live catalog behavior
M4n5ter Aug 8, 2026
b8d814a
fix: attach global Host streams on recovery
M4n5ter Aug 8, 2026
a74ec54
Merge remote-tracking branch 'origin/main' into feat/runtime-host-m5-…
M4n5ter Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

**A local-first Agent workspace built for real work.**

Maka does more than answer questions. With controlled permissions, it can inspect projects, execute tools, produce artifacts, and preserve model messages, tool calls, and durable-task progress as recoverable execution facts. The same Runtime is available through the desktop app, terminal TUI, non-interactive CLI, and Headless runner.
Maka does more than answer questions. With controlled permissions, it can inspect projects, execute tools, produce artifacts, and preserve model messages, tool calls, and durable-task progress as recoverable execution facts. Desktop, the terminal TUI, and the non-interactive CLI are clients of one per-workspace Runtime Host. Headless owns a separate task runtime for durable evaluation and automation workloads.

> [!IMPORTANT]
> Maka is under active development. The macOS Apple Silicon desktop build is an early public release; data formats, CLI commands, and experimental capabilities may still change.
Expand Down Expand Up @@ -135,13 +135,13 @@ The CLI reads the same model connections and workspace configuration written by
The backend spine is:

```text
Desktop / TUI / Headless
SessionManager → AgentRun → Model + Tool Runtime
Runtime Event Log → Context / Session / UI projections
Task Event Log → TaskRun → Self-check / AHE evidence
Desktop / TUI / CLI → Runtime Host → SessionManager → AgentRun
Model + Tool Runtime → Runtime Event Log
Context / Session / UI projections

Headless / Eval → Task Event Log → TaskRun → Self-check / AHE evidence
```

Start with [ARCHITECTURE.md](./ARCHITECTURE.md). It provides the system map, code boundaries, problem-oriented reading paths, and six bilingual deep dives.
Expand Down
16 changes: 8 additions & 8 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

**一个为真实工作而生的本地优先 Agent 工作台。**

Maka 不只回答问题。它可以在受控权限下阅读项目、执行工具、生成产物,并把模型消息、工具调用和长程任务进度保存为可恢复的运行事实。你可以从桌面应用、终端 TUI、非交互 CLI Headless runner 使用同一套 Runtime。
Maka 不只回答问题。它可以在受控权限下阅读项目、执行工具、生成产物,并把模型消息、工具调用和长程任务进度保存为可恢复的运行事实。桌面应用、终端 TUI 和非交互 CLI 共享每个工作区唯一的 Runtime Host;Headless 使用独立的任务 Runtime 承载持久评测和自动化工作负载

> [!IMPORTANT]
> Maka 仍在活跃开发中。macOS Apple Silicon 桌面版是首个早期公开版本,数据格式、CLI 和实验能力仍可能变化。
Expand Down Expand Up @@ -135,13 +135,13 @@ CLI 读取 Desktop 写入的同一份模型连接和 workspace 配置。Headless
Maka 后端可以用一条主线概括:

```text
Desktop / TUI / Headless
SessionManager → AgentRun → Model + Tool Runtime
Runtime Event Log → Context / Session / UI projections
Task Event Log → TaskRun → Self-check / AHE evidence
Desktop / TUI / CLI → Runtime Host → SessionManager → AgentRun
Model + Tool Runtime → Runtime Event Log
Context / Session / UI projections

Headless / Eval → Task Event Log → TaskRun → Self-check / AHE evidence
```

从 [ARCHITECTURE.zh-CN.md](./ARCHITECTURE.zh-CN.md) 开始阅读。它提供总体架构图、代码边界、按问题组织的阅读路径,以及六篇中英双语深度文章。
Expand Down
15 changes: 8 additions & 7 deletions apps/desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ npm Electron bundle, which macOS will not accept as a durable grant.

| Layer | Path | Role |
|---|---|---|
| main | `src/main/` | Node/Electron main process. Owns window lifecycle, credentials, attachments, permissions, IPC handlers, and the bridge to `@maka/runtime` + `@maka/storage`. |
| main | `src/main/` | Node/Electron client process. Owns windows, OS capabilities, client-local settings, IPC projection, and the Runtime Host connection. Runtime execution and canonical runtime policy belong to Runtime Host. |
| preload | `src/preload/preload.ts` (single file) | `contextBridge.exposeInMainWorld('maka', …)` — the only surface the renderer may call to reach Node/Electron. No Node API is directly exposed. |
| renderer | `src/renderer/` | React UI body. See `src/renderer/README.md`. |

Expand All @@ -93,20 +93,21 @@ npm Electron bundle, which macOS will not accept as a durable grant.

| Suffix | Role | Examples |
|---|---|---|
| `*-ipc-main.ts` | Exports a `register*Ipc(...)` that wires `ipcMain.handle` / `ipcMain.on` for one IPC domain | `connections-ipc-main`, `daily-review-ipc-main`, `memory-ipc-main`, `web-search-ipc-main`, `workspace-resources-ipc-main` |
| `*-main.ts` / `*-service.ts` | A service owned by main (no `ipcMain` calls of its own) | `daily-review-main`, `system-prompt-main`, `oauth-model-connections-main`, `local-memory-service` |
| `runtime-host-*-ipc-main.ts` | Projects one Runtime Host protocol domain onto renderer IPC | `runtime-host-connections-ipc-main`, `runtime-host-session-execution-ipc-main`, `runtime-host-settings-ipc-main` |
| `*-ipc-main.ts` | Registers a client-local Electron or OS-facing IPC domain | `browser-ipc-main`, `notifications-ipc-main`, `workspace-search-ipc-main` |
| `*-service.ts` / `*-controller.ts` | A client-local service without direct IPC ownership | `app-update-service`, `project-management-service`, `project-root-controller` |
| `*-guard.ts` | Validation / security boundary | `external-link-guard`, `open-path-guard`, `permission-response-guard` |
| (other) | Window, state, platform wiring | `main.ts` (entry), `main-window`, `window-state`, `theme-source`, `credential-store`, `skills`, `attachment-*` |

Sub-folders: `browser/` (embedded browser view), `oauth/`, `search/` (thread search), `web-search/`, `types/`. The browser IPC handler itself (`browser-ipc-main.ts`) is flat in `src/main/`, not under `browser/`.
Sub-folders hold OS-facing implementations such as `browser/`, `computer-use/`, `oauth/`, and `permission-overlay/`. Runtime Host adapters stay flat and carry the `runtime-host-` prefix so ownership is visible at the import boundary.

`main.ts` startup order: stores and the runtime/controller are created synchronously at module load; `registerIpc()` runs at top level, **before** `app.whenReady()`; inside `whenReady`, the main window is created **hidden** early and background startup (connection bootstrapping, telemetry, bots, schedulers) runs concurrently without blocking first paint. The window is created hidden and revealed after the renderer's first AppShell paint (the `window:notifyRendererReady` gate in `app.tsx`); a fallback timer reveals it if the renderer never signals, so a fail-soft loading state can show (e.g. if `main.tsx`'s onboarding prefetch times out). The real invariant for IPC: handlers must be registered before the renderer entry runs, because `main.tsx` prefetches the onboarding snapshot before mounting React. Background startup may mutate state after the renderer's first read, so don't assume it has already settled when wiring the UI.
`main.ts` performs only pre-ready Electron identity and single-instance work. After `app.whenReady()`, it dynamically imports `runtime-host-boot.ts`. Boot validates the storage root before any Runtime Host state can be written, registers persistent client-local IPC, connects or spawns Runtime Host, registers connection-scoped Host IPC, and only then creates the first renderer window. The window remains hidden until the renderer's first AppShell paint (`window:notifyRendererReady`), with a fallback reveal timer for fail-soft startup.

## IPC contract

Three patterns, all rooted in preload's `maka` namespace. Channel names are `<domain>:<action>`.

- **Request/response** — `ipcRenderer.invoke('<domain>:<action>', …args)` in preload ↔ `ipcMain.handle('<domain>:<action>', …)`. The handler lives either inline in `main.ts` (e.g. `sessions:list`, `settings:get`) or in a `*-ipc-main.ts` extracted by domain (e.g. `connections-ipc-main`, `daily-review-ipc-main`). Both forms coexist; prefer extracting a new domain to its own `*-ipc-main.ts`.
- **Request/response** — `ipcRenderer.invoke('<domain>:<action>', …args)` in preload ↔ `ipcMain.handle('<domain>:<action>', …)`. Runtime domains are projected by `runtime-host-*-ipc-main.ts`; OS-facing client domains use a focused `*-ipc-main.ts` module.
- **Main→renderer push** — main sends through the safe-send guard (`safeSendToRenderer` via `mainWindowController.send`), not raw `webContents.send` (which throws when the window/`webContents` is destroyed); preload subscribes via `ipcRenderer.on` and returns an unsubscribe fn (e.g. `sessions:changed`, `plans:changed`, `artifacts:changed`). The safe-send contract test scans a fixed list of main-source files for direct `mainWindow.webContents.send(...)` forms — new `*-ipc-main.ts` files aren't auto-covered, so route sends through the guard in every new file (an alias for `mainWindow` can bypass the literal scan).
- **Renderer→main fire-and-forget** — `ipcRenderer.send('<domain>:<action>', …)` in preload ↔ `ipcMain.on('<domain>:<action>', …)`. Used when no response is needed (e.g. `browser:active-session`, `browser:setViewport`).

Expand All @@ -119,7 +120,7 @@ renderer (React)
└─ window.maka.<ns>.<method>(…) // typed surface, see preload.ts
└─ ipcRenderer.invoke / send / on
└─ main: safeSendToRenderer / ipcMain.handle / ipcMain.on
└─ @maka/runtime (agent runtime) + @maka/storage (JSONL persistence)
└─ Runtime Host protocol → @maka/runtime + @maka/storage
```

The renderer never imports `@maka/runtime` or `@maka/storage` at runtime — all Node-side access goes through the preload `maka` bridge. The renderer only pulls `import type` from them for a few shared types. Types shared across the IPC boundary mostly come from `@maka/core`, with some from `@maka/runtime`, `@maka/storage`, and `@maka/ui` (see `preload.ts` imports).
Expand Down
9 changes: 2 additions & 7 deletions apps/desktop/e2e/bot-onboarding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,17 @@ test('IM 快捷接入完成真实 QR session、凭据落盘,取消与过期二

await dialog.getByRole('button', { name: '完成' }).click();
await expect(dialog).toBeHidden();

// Same window, next channels: cancellation races, expiry regeneration, and
// the Lark variant are independent flows over the same seeded settings.
await settings.getByRole('button', { name: '返回远程接入' }).click();
await settings.getByRole('button', { name: '接入 微信' }).click();
await settings.getByRole('button', { name: '扫码登录' }).click();
const wechatDialog = page.getByRole('dialog', { name: '微信扫码登录' });
await expect(wechatDialog.getByRole('img', { name: '微信扫码登录二维码' })).toBeVisible();
await page.waitForTimeout(1_150);
// The fixture deliberately has a provider result in flight here. Bypass
// Playwright's stability wait so the result-driven rerender cannot win the
// race before the cancellation click is dispatched.
// Poll snapshots replace the dialog subtree. A real pointer dispatch does
// not wait for that subtree to become stable, so neither should this click.
await wechatDialog.getByRole('button', { name: '取消' }).click({ force: true });
await expect(wechatDialog).toBeHidden();
await page.waitForTimeout(1_300);

const afterCancel = await page.evaluate(() => window.maka.settings.get());
expect(afterCancel.botChat.channels.wechat.token).toBe('');
Expand All @@ -56,7 +52,6 @@ test('IM 快捷接入完成真实 QR session、凭据落盘,取消与过期二
await settings.getByRole('button', { name: '接入 企业微信' }).click();
await settings.getByRole('button', { name: '开始快捷绑定' }).click();
const wecomDialog = page.getByRole('dialog', { name: '配置企业微信扫码接入' });
await expect(wecomDialog.getByRole('img', { name: '配置企业微信二维码' })).toBeVisible();
await expect(wecomDialog.getByText('二维码已过期,请重新生成')).toBeVisible({ timeout: 4_000 });
await wecomDialog.getByRole('button', { name: '重新生成' }).click();
await expect(wecomDialog.getByRole('img', { name: '配置企业微信二维码' })).toBeVisible();
Expand Down
21 changes: 15 additions & 6 deletions apps/desktop/e2e/composer-skill-invocation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ test('slash suggestions: project gating, collaboration modes, and Deep Research
await expect.poll(async () => (await page.evaluate(() => window.maka.sessions.list())).length).toBe(1);
const [session] = await page.evaluate(() => window.maka.sessions.list());
if (!session) throw new Error('the composer did not create a session');
await expect
.poll(async () => (await page.evaluate(() => window.maka.sessions.list()))[0]?.status)
.not.toBe('running');

const listNames = (sessionId: string) =>
page.evaluate(
Expand All @@ -77,12 +80,18 @@ test('slash suggestions: project gating, collaboration modes, and Deep Research
await expect(listbox).toContainText('Agent Write');

await expect
.poll(async () => (await page.evaluate(() => window.maka.sessions.list()))[0]?.status)
.not.toBe('running');
await page.evaluate(
({ sessionId }) => window.maka.sessions.setCollaborationMode(sessionId, 'plan'),
{ sessionId: session.id },
);
.poll(() =>
page.evaluate(async ({ sessionId }) => {
try {
await window.maka.sessions.setCollaborationMode(sessionId, 'plan');
return true;
} catch (error) {
if (String(error).includes('linked Turn is active')) return false;
throw error;
}
}, { sessionId: session.id }),
)
.toBe(true);
await expect.poll(() => listNames(session.id)).not.toContain('Agent Write');
await expect(listbox).not.toContainText('Agent Write');

Expand Down
9 changes: 6 additions & 3 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ async function withE2eWindow(
e2eFixtureScenario,
locale,
platform,
showWindow,
invocableSkills,
gitReviewExtraFiles,
extraConnectionCount,
Expand All @@ -220,6 +221,8 @@ async function withE2eWindow(
locale?: 'zh' | 'en';
/** #1312: force app:info's platform so the window boots natively into that platform's `data-os` cascade. */
platform?: 'darwin' | 'win32' | 'linux';
/** Show fixtures whose contract depends on compositor-paced frames. */
showWindow?: boolean;
invocableSkills?: boolean;
gitReviewExtraFiles?: number;
extraConnectionCount?: number;
Expand Down Expand Up @@ -251,9 +254,9 @@ async function withE2eWindow(
scenario: e2eFixtureScenario,
locale,
platform,
// xvfb throttles a hidden window's compositor to ~1fps; only that
// isolated display gets a visible window.
showWindow: isCiLinuxDisplay(),
// xvfb throttles a hidden window's compositor to ~1fps. Geometry
// fixtures opt in locally; every fixture is visible on isolated CI X.
showWindow: showWindow || isCiLinuxDisplay(),
}),
});
app.on('console', (message) => {
Expand Down
16 changes: 0 additions & 16 deletions apps/desktop/e2e/mcp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,22 +35,6 @@ test('MCP module completes stdio add, discovery, disable, JSON import, and delet
await expect(page.getByRole('main', { name: '扩展' })).toBeVisible();
await expect(extensionSelector).toHaveAccessibleName('扩展内容:MCP');

const dingtalkRow = mcp.locator('[data-maka-contract="mcp-market-row"]').filter({ hasText: '钉钉' });
const installDingtalk = dingtalkRow.getByRole('button', { name: '安装 钉钉' });
await installDingtalk.click();
const cancelDingtalk = dingtalkRow.getByRole('button', { name: '取消安装 钉钉' });
await expect(cancelDingtalk).toBeVisible();
await page.mouse.move(0, 0);
await expect(cancelDingtalk.locator('.maka-mcp-install-spinner')).toHaveCSS('opacity', '1');
await cancelDingtalk.hover();
await expect(cancelDingtalk.locator('.maka-mcp-install-cancel')).toHaveCSS('opacity', '1');
await cancelDingtalk.click();
await expect(dingtalkRow.getByRole('button', { name: '安装 钉钉' })).toBeVisible();
await expect.poll(async () => {
const next = await page.evaluate(() => window.maka.mcp.getConfig());
return next.mcpServers.dingtalk;
}).toBeUndefined();

await mcp.getByRole('button', { name: '添加 MCP' }).click();
const editor = page.getByRole('dialog', { name: '添加 MCP' });
await expect(editor.getByLabel('服务器 ID')).toBeFocused();
Expand Down
23 changes: 11 additions & 12 deletions apps/desktop/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@ import { defineConfig } from '@playwright/test';
*
* Each test launches a real Electron window backed by the deterministic fake
* backend (MAKA_E2E=1) against its OWN throwaway userData dir (the fixture
* mkdtemps one per test), so windows share no *state*. The wall clock is
* dominated by Electron boot, which overlaps well: a past full-suite run
* measured 1 worker ≈ 7min against 4 workers ≈ 2.4min. Deliberately no test
* count here — the previous note carried a stale one that outlived two rounds
* of pruning. `playwright test --list` is the only figure that cannot rot.
* mkdtemps one per test), so windows share no *state*. A Runtime Host-backed
* window owns both Electron and an execution candidate process. Keep the
* default at one worker: concurrent hidden windows throttle animation frames
* and share OS focus, invalidating geometry and focus contracts. Developers
* can still pass `--workers` explicitly for a subset that has neither concern.
* Deliberately no test count here — the previous note carried a stale one that
* outlived two rounds of pruning. `playwright test --list` is the only figure
* that cannot rot.
*
* What parallel windows DO share is OS focus. Specs that assert
* `toBeFocused()` (e.g. plan-reminders) fail when another window steals
* activation mid-assertion — Chromium blurs the document when its window
* deactivates. Each CI shard therefore keeps one worker on an isolated X
* display; local runs take the parallel win, and a local focus failure re-runs
* alone to confirm.
* CI shards run on isolated X displays, so jobs still overlap without sharing
* focus or a compositor. Local parallelism is opt-in for the same reason.
*
* Run from apps/desktop via `npm run e2e`, which builds the app first.
*/
export default defineConfig({
testDir: '.',
fullyParallel: true,
workers: process.env.CI ? 1 : 4,
workers: 1,
// CI publishes no Playwright report that consumes Git metadata. Disable its
// best-effort shallow-history fetch, which otherwise waits on a fixed timeout.
captureGitInfo: { commit: false, diff: false },
Expand Down
11 changes: 0 additions & 11 deletions apps/desktop/e2e/providers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,17 +221,6 @@ test('provider connections: the canonical API-key journey and two-field rows', a
});

await test.step('deletion stays reachable and reversible in a short viewport', async () => {
// The fixture intentionally cannot discover models with its placeholder
// credential. Dismiss that independently tested transient before changing
// the viewport so it cannot pause its own auto-hide timer over the control
// this step is meant to exercise.
const discoveryError = page
.getByRole('alert')
.filter({ has: page.locator('[data-type="supporting"]') });
await expect(discoveryError).toBeVisible();
await discoveryError.getByRole('button').click();
await expect(discoveryError).toBeHidden();

// Short-viewport invariant: the detail is a page, so the settings content
// area owns the scrolling and the trailing action stays reachable. The test
// asserts reachability, not which node scrolls.
Expand Down
Loading
Loading