Core simplification
Evidence-based plan to reduce mikan's public surface and internal coupling without changing behavior.
This audit uses the boundaries in Core interface reference. 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
Section titled “Target core”The smallest coherent mikan core is:
- one conversation identity and one place that turns it into paths;
- one normalized conversation input and one response port;
- one runtime that serializes a session and owns runner lifecycle;
- one harness interface for a model turn;
- one session store and one platform log store, intentionally separate;
- one executor interface for all tool I/O;
- one credential-resolution seam;
- 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
Section titled “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
Section titled “What should not be simplified away”Some apparent duplication protects important semantics:
- Keep
log.jsonlseparate 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
Section titled “Proposed interfaces”One normalized input
Section titled “One normalized input”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
Section titled “Composed platform capabilities”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
Section titled “Explicit runtime dependencies”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
Section titled “Honest sandbox registry”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
Section titled “Migration sequence”Phase 1 — enforce the package boundary (complete)
Section titled “Phase 1 — enforce the package boundary (complete)”package.jsondeclares root plus compatibility./harnessand./sandboxentry points.- The root entry point uses an explicit export list rather than a wildcard harness export.
src/test/public-api.test.tssnapshots 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
Section titled “Phase 2 — unify platform input”- Add
ConversationInputand an adapter from it to the current runtime call. - Convert one platform adapter and its tests at a time.
- Change
handleEvent(event, bot, context)tohandle(input). - 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
Section titled “Phase 3 — compose capabilities”- Extract message, reaction, upload, and private-message ports from
MessagingBot. - Adapt existing bots without changing their SDK code.
- Pass narrow capabilities to commands, extensions, events, and admin instead of the whole bot.
- Retire
ChatAdapterif no independent caller remains.
Phase 4 — simplify runtime construction and commands
Section titled “Phase 4 — simplify runtime construction and commands”- Replace the
Omit<CommandServices, ...>inheritance with explicit runtime options. - Make portal services one optional object rather than three token-store fields plus a URL.
- Join command metadata and handler registration into
CommandDefinition. - Generate platform registration, parsing inventory, and handler order from that definition list.
Phase 5 — separate optional execution products
Section titled “Phase 5 — separate optional execution products”- Introduce
SandboxRegistrywhile preserving the existing default functions as wrappers. - Register host/container/image as the default distribution.
- Register Gondolin, Firecracker, and Cloudflare from optional composition modules.
Phase 6 — reduce the composition root
Section titled “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
Section titled “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
knippass after each phase.
Recommended next pull request
Section titled “Recommended next pull request”The package-boundary work is complete. Keep the next implementation PR behavior-neutral:
- introduce
ConversationInputplus conversion helpers, but migrate only one adapter; - 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.