Skip to content

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.

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.

PriorityFindingEvidenceSimplification
DoneThe npm boundary is explicit and regression-testedsrc/index.ts lists root exports; package.json declares root, harness, and sandbox entry points; src/test/public-api.test.ts snapshots runtime exportsTreat additions as compatibility decisions and update the contract test deliberately
DoneConversation identity and path derivation have one ownersrc/office/ owns OfficeAddress/OfficeKey, the Workspace/Office values, the registry, and the legacy migration; consumers take values, not stringsKeep path math inside the module; a new consumer receives an Office rather than a root plus an id
P0Platform input has two overlapping canonical shapesConversationEvent and ConversationMessage repeat id/session/kind/user/text/attachments/thread fields and are passed togetherIntroduce one ConversationInput envelope; derive compatibility views during migration
P0MessagingBot has too many reasons to changelifecycle, output, upload/reaction, private diagnostics, event queue, and metadata are one interfaceSplit normalized platform port from optional capabilities; keep one adapter object that composes them
P1Sandbox extensibility is only nominalSandboxAdapter is exported, but a closed array in sandbox/index.ts owns parsing/creationAdd a registry or make adapters internal; move experimental backends out of the default core assembly
P1Runtime construction leaks command implementation detailsConversationRuntimeOptions is an Omit<CommandServices, ...> and reconstructs command services internallyDefine explicit runtime dependencies, then build CommandServices in a command adapter
P1Commands still have two inventoriesThe manifest owns platform registration while the registry owns handlers; adding a command requires bothA CommandDefinition should contain manifest metadata and a handler factory/reference
P1The harness is both a public SDK and an internal engineRoot exports session parsing, credentials, settings, loader, event schema, hooks, and runner details togetherPublish ./extension, ./runtime, and optionally ./harness subpaths with separate stability policies
P2Optional runtime products are compiled into the central CLIPlatform SDKs, portals, six sandbox modes, and Gondolin runtime code are assembled in main.tsMake main.ts a composition root over registries; lazy/optional product modules become replaceable
P2Several capability interfaces use optional methodsMessagingBot and ConversationResponder grow by adding ? methodsUse named capability objects (reactions, uploads, streaming) so absence and requirements are explicit
P2HTTP route contracts are manually dispatched and mostly untypedweb/server.ts and portal modules branch on method/path; payload types are localKeep the UI API internal, but centralize route descriptors and request/response schemas where payloads cross modules

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.
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.

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.

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.

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.

Phase 1 — enforce the package boundary (complete)

Section titled “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.

  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.

  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

Section titled “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

Section titled “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.

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.

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.

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.