---
title: "Core simplification"
description: "Evidence-based plan to reduce mikan's public surface and internal coupling without changing behavior."
url: "https://geminixiang.github.io/core-simplification/"
---

# Core simplification

This audit uses the boundaries in [Core interface reference](/core-interfaces/). The goal is not to minimize file count; it is to make the core explainable as a small set of stable contracts and move optional policy behind them.

## Target core

The smallest coherent mikan core is:

1. one conversation identity and one place that turns it into paths;
2. one normalized conversation input and one response port;
3. one runtime that serializes a session and owns runner lifecycle;
4. one harness interface for a model turn;
5. one session store and one platform log store, intentionally separate;
6. one executor interface for all tool I/O;
7. one credential-resolution seam;
8. registries for optional platform, command, tool, extension, and sandbox capabilities.

Web portals, particular chat SDKs, Docker provisioning, Gondolin runtime management, Firecracker, Cloudflare, and GitHub-specific tools are products or plugins around this core.

## Findings

| Priority | Finding                                                         | Evidence                                                                                                                                                    | Simplification                                                                                                       |
| -------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Done     | The npm boundary is explicit and regression-tested              | `src/index.ts` lists root exports; `package.json` declares root, harness, and sandbox entry points; `src/test/public-api.test.ts` snapshots runtime exports | Treat additions as compatibility decisions and update the contract test deliberately                                 |
| Done     | Conversation identity and path derivation have one owner        | `src/office/` owns `OfficeAddress`/`OfficeKey`, the `Workspace`/`Office` values, the registry, and the legacy migration; consumers take values, not strings | Keep path math inside the module; a new consumer receives an `Office` rather than a root plus an id                  |
| P0       | Platform input has two overlapping canonical shapes             | `ConversationEvent` and `ConversationMessage` repeat id/session/kind/user/text/attachments/thread fields and are passed together                            | Introduce one `ConversationInput` envelope; derive compatibility views during migration                              |
| P0       | `MessagingBot` has too many reasons to change                   | lifecycle, output, upload/reaction, private diagnostics, event queue, and metadata are one interface                                                        | Split normalized platform port from optional capabilities; keep one adapter object that composes them                |
| P1       | Sandbox extensibility is only nominal                           | `SandboxAdapter` is exported, but a closed array in `sandbox/index.ts` owns parsing/creation                                                                | Add a registry or make adapters internal; move experimental backends out of the default core assembly                |
| P1       | Runtime construction leaks command implementation details       | `ConversationRuntimeOptions` is an `Omit<CommandServices, ...>` and reconstructs command services internally                                                | Define explicit runtime dependencies, then build `CommandServices` in a command adapter                              |
| P1       | Commands still have two inventories                             | The manifest owns platform registration while the registry owns handlers; adding a command requires both                                                    | A `CommandDefinition` should contain manifest metadata and a handler factory/reference                               |
| P1       | The harness is both a public SDK and an internal engine         | Root exports session parsing, credentials, settings, loader, event schema, hooks, and runner details together                                               | Publish `./extension`, `./runtime`, and optionally `./harness` subpaths with separate stability policies             |
| P2       | Optional runtime products are compiled into the central CLI     | Platform SDKs, portals, six sandbox modes, and Gondolin runtime code are assembled in `main.ts`                                                             | Make `main.ts` a composition root over registries; lazy/optional product modules become replaceable                  |
| P2       | Several capability interfaces use optional methods              | `MessagingBot` and `ConversationResponder` grow by adding `?` methods                                                                                       | Use named capability objects (`reactions`, `uploads`, `streaming`) so absence and requirements are explicit          |
| P2       | HTTP route contracts are manually dispatched and mostly untyped | `web/server.ts` and portal modules branch on method/path; payload types are local                                                                           | Keep the UI API internal, but centralize route descriptors and request/response schemas where payloads cross modules |

## What should not be simplified away

Some apparent duplication protects important semantics:

- Keep `log.jsonl` separate from agent session JSONL. They record different truths and support recovery.
- Keep office keys distinct from raw platform ids. Naming a directory or a vault by a raw id lets two platforms that share an id reach each other's data; the registry exists precisely because the key is not reversible.
- Keep session keys raw. They are platform values, and pairing them with an office is what makes a non-globally-unique key safe — rewriting them into office-scoped strings would move the check from the runtime into every caller.
- Keep host paths separate from runtime paths. Collapsing them breaks container/remote execution and can expose host-only data.
- Keep model-provider credentials separate from sandbox vault credentials.
- Keep per-session queues. Global serialization would waste concurrency; no serialization would corrupt conversation order.
- Keep platform trust policy explicit. Inferring credential safety from platform names is unsafe.
- Keep executor-owned file transport. Replacing it with shell snippets reintroduces quoting, size, and partial-write failures.
- Keep extension code host-only and sandbox workspace data separate.

## Proposed interfaces

### One normalized input

```ts
interface ConversationInput {
  platform: MessagingInfo;
  conversation: {
    address: OfficeAddress;
    kind: ConversationKind;
    vaultId?: string;
  };
  message: {
    id: string;
    parentId?: string;
    sessionKey?: string;
    actor: { id: string; name?: string };
    text: string;
    attachments: readonly Attachment[];
  };
  respond: ConversationResponder;
}
```

The runtime derives `ConversationEvent` and `ConversationMessage` only for old call sites. Once adapters and tests use the envelope, delete both compatibility shapes.

### Composed platform capabilities

```ts
interface PlatformPort {
  readonly info: MessagingInfo;
  lifecycle: { start(): Promise<void>; stop(): Promise<void> };
  messages: PlatformMessages;
  capabilities?: {
    reactions?: PlatformReactions;
    uploads?: PlatformUploads;
    privateMessages?: PlatformPrivateMessages;
  };
}
```

This removes feature detection by arbitrary method name and lets commands/extensions request the exact capability they need.

### Explicit runtime dependencies

```ts
interface ConversationRuntimeOptions {
  workspace: Workspace;
  sandbox: SandboxConfig;
  createRunner: RunnerFactory;
  commands?: readonly CommandDefinition[];
  vault?: VaultResolver;
  resources?: SandboxResourceController;
  portals?: PortalServices;
  platformCapabilities?: PlatformCapabilities;
  platformToolPacks?: readonly PlatformToolPackFactory[];
}
```

The runtime should not inherit its constructor shape from another subsystem's service bag.

### Honest sandbox registry

```ts
interface SandboxRegistry {
  register<T extends SandboxConfig>(adapter: SandboxAdapter<T>): void;
  parse(value: string): SandboxConfig;
  validate(config: SandboxConfig): Promise<void>;
  createExecutor(config: SandboxConfig, context: ExecutorContext): Promise<Executor>;
}
```

The default CLI registers stable backends. Gondolin, Firecracker, and Cloudflare can be registered by optional product modules without changing tool or runtime code.

## Migration sequence

### Phase 1 — enforce the package boundary (complete)

1. `package.json` declares root plus compatibility `./harness` and `./sandbox` entry points.
2. The root entry point uses an explicit export list rather than a wildcard harness export.
3. `src/test/public-api.test.ts` snapshots runtime exports so accidental additions fail CI.

The remaining phases can now refactor internals without silently expanding the package interface.

### Phase 2 — unify platform input

1. Add `ConversationInput` and an adapter from it to the current runtime call.
2. Convert one platform adapter and its tests at a time.
3. Change `handleEvent(event, bot, context)` to `handle(input)`.
4. Delete duplicated fields and compatibility adapters after all platforms migrate.

This should remove consistency bugs around `ts` versus `message.id`, `thread_ts` versus `threadTs`, and event `user` versus message `userId`.

### Phase 3 — compose capabilities

1. Extract message, reaction, upload, and private-message ports from `MessagingBot`.
2. Adapt existing bots without changing their SDK code.
3. Pass narrow capabilities to commands, extensions, events, and admin instead of the whole bot.
4. Retire `ChatAdapter` if no independent caller remains.

### Phase 4 — simplify runtime construction and commands

1. Replace the `Omit<CommandServices, ...>` inheritance with explicit runtime options.
2. Make portal services one optional object rather than three token-store fields plus a URL.
3. Join command metadata and handler registration into `CommandDefinition`.
4. Generate platform registration, parsing inventory, and handler order from that definition list.

### Phase 5 — separate optional execution products

1. Introduce `SandboxRegistry` while preserving the existing default functions as wrappers.
2. Register host/container/image as the default distribution.
3. Register Gondolin, Firecracker, and Cloudflare from optional composition modules.

### Phase 6 — reduce the composition root

`main.ts` should parse CLI configuration, instantiate registries/services, start selected products, and coordinate shutdown. Platform SDK initialization and backend-specific policy should remain in their modules. A useful completion criterion is that the composition root reads like configuration, not business logic.

## Acceptance criteria

The simplification is complete when:

- a new platform implements one normalized input adapter plus declared output capabilities;
- a new sandbox registers one adapter without editing a core switch or array;
- runtime construction does not mention individual portal token-store types;
- adding a command changes one definition and one handler implementation, not several platform inventories;
- the root npm declaration surface contains only documented symbols;
- persisted session/event/config formats and trust/path invariants remain compatible;
- existing unit tests, build, lint, and `knip` pass after each phase.

## Recommended next pull request

The package-boundary work is complete. Keep the next implementation PR behavior-neutral:

1. introduce `ConversationInput` plus conversion helpers, but migrate only one adapter;
2. record deprecations without deleting functionality.

Do not combine this with removing a sandbox backend or changing persisted formats. Those changes have different rollback and operational risks.
