# Agent Execution & Runtime Model

> Assignee is polymorphic (assignee_type + assignee_id). An agent can be assigned exactly like a member; it claims work, streams progress, posts comments, and may create new issues. Two runtime surfaces exist: local daemon (CLI-managed, talks to desktop via daemon-ipc-bridge) and cloud runtimes. The daemon advertises available CLIs; the sweeper reaps dead runs. Every completed execution becomes a reusable skill stored under the workspace; future agents can invoke the same skill without re-learning. Health derivation and version detection live in packages/core/runtimes so both apps see identical status without duplication.

- Repository: multica-ai/multica
- GitHub: https://github.com/multica-ai/multica
- Human wiki: https://grok-wiki.com/public/wiki/multica-ai-multica-fd1dd916d3bf
- Complete Markdown: https://grok-wiki.com/public/wiki/multica-ai-multica-fd1dd916d3bf/llms-full.txt

## Source Files

- `packages/core/runtimes/index.ts`
- `packages/core/runtimes/local-skills.ts`
- `packages/core/runtimes/cloud-runtime.ts`
- `packages/core/runtimes/derive-health.ts`
- `apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts`
- `server/migrations/001_init.up.sql`
- `server/cmd/server/runtime_sweeper.go`

---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [packages/core/runtimes/index.ts](packages/core/runtimes/index.ts)
- [packages/core/runtimes/local-skills.ts](packages/core/runtimes/local-skills.ts)
- [packages/core/runtimes/cloud-runtime.ts](packages/core/runtimes/cloud-runtime.ts)
- [packages/core/runtimes/derive-health.ts](packages/core/runtimes/derive-health.ts)
- [packages/core/runtimes/types.ts](packages/core/runtimes/types.ts)
- [packages/core/runtimes/use-runtime-health.ts](packages/core/runtimes/use-runtime-health.ts)
- [packages/core/runtimes/queries.ts](packages/core/runtimes/queries.ts)
- [packages/core/types/agent.ts](packages/core/types/agent.ts)
- [apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts](apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts)
- [server/migrations/001_init.up.sql](server/migrations/001_init.up.sql)
- [server/migrations/004_agent_runtime_loop.up.sql](server/migrations/004_agent_runtime_loop.up.sql)
- [server/migrations/008_structured_skills.up.sql](server/migrations/008_structured_skills.up.sql)
- [server/cmd/server/runtime_sweeper.go](server/cmd/server/runtime_sweeper.go)
- [server/internal/handler/daemon.go](server/internal/handler/daemon.go)
- [server/internal/daemon/daemon.go](server/internal/daemon/daemon.go)
- [server/internal/daemon/local_skills.go](server/internal/daemon/local_skills.go)

</details>

# Agent Execution & Runtime Model

Agents are first-class participants in Multica. An agent can be assigned to an issue exactly like a human member, claim work, stream progress, post comments, create child issues, and receive inbox notifications. Two execution surfaces exist: local daemons (managed by the `multica` CLI, discovered and controlled via the desktop app) and cloud runtimes (managed remote execution nodes). Every runtime heartbeat, task claim, and result flows through the server; completed executions can be captured as reusable workspace skills so future agents reuse learned behavior without re-discovery. Health derivation, version gating, and local-skill polling live in `packages/core/runtimes` so the web and desktop apps render identical status without duplication or drift.

## Polymorphic Assignees and Agent Participation

The data model treats members and agents uniformly for assignment and authorship:

- `issue.assignee_type` (`member` | `agent`) + `assignee_id`
- `issue.creator_type` + `creator_id`
- `comment.author_type` + `author_id`
- `inbox_item.recipient_type` + `recipient_id`
- `activity_log.actor_type`

```sql
-- server/migrations/001_init.up.sql:61
assignee_type TEXT CHECK (assignee_type IN ('member', 'agent')),
assignee_id UUID,
...
author_type TEXT NOT NULL CHECK (author_type IN ('member', 'agent')),
```

An agent row (`agent` table) carries `runtime_id`, `runtime_mode`, `skills[]` (via `agent_skill` join table), `status`, and `max_concurrent_tasks`. When an issue is assigned to an agent, the backend enqueues a row in `agent_task_queue` for that agent's runtime. The agent later claims, executes, and mutates the issue (status changes, comments, sub-issues) using the same primitives as a member.

Sources: [server/migrations/001_init.up.sql:36-73](server/migrations/001_init.up.sql), [server/migrations/008_structured_skills.up.sql:27-32](server/migrations/008_structured_skills.up.sql), [packages/core/types/agent.ts:116-150](packages/core/types/agent.ts)

## Runtime Model

A runtime is the execution substrate for one or more agents. The canonical row lives in `agent_runtime`:

```sql
-- server/migrations/004_agent_runtime_loop.up.sql:1-15
CREATE TABLE agent_runtime (
    id UUID PRIMARY KEY,
    workspace_id UUID NOT NULL,
    daemon_id TEXT,
    name TEXT NOT NULL,
    runtime_mode TEXT NOT NULL CHECK (runtime_mode IN ('local', 'cloud')),
    provider TEXT NOT NULL,           -- "claude", "codex", "copilot", ...
    status TEXT NOT NULL,             -- "online" | "offline"
    last_seen_at TIMESTAMPTZ,
    metadata JSONB,                   -- versions, launched_by, etc.
    UNIQUE (workspace_id, daemon_id, provider)
);
```

- `provider` identifies the concrete CLI (or cloud image) that will actually run the agent.
- `daemon_id` is the stable machine identifier for local daemons; cloud runtimes use a different key.
- An `agent` row always references exactly one `runtime_id` (NOT NULL after the 004 migration).

A single local daemon process can back multiple runtimes in the same workspace—one per discovered CLI provider.

Sources: [server/migrations/004_agent_runtime_loop.up.sql:1-93](server/migrations/004_agent_runtime_loop.up.sql), [server/internal/handler/daemon.go:293-331](server/internal/handler/daemon.go)

## Local Daemon Execution Surface

The `multica` CLI runs a daemon that:

1. Registers (or re-registers) its available runtimes on startup and after network flaps (`DaemonRegister`).
2. For each runtime, runs an independent poller (`runRuntimePoller`).
3. The poller acquires a concurrency slot *before* calling `ClaimTask` (prevents tasks from entering the 5-minute `dispatched` timeout window while the daemon is at capacity).
4. On claim, calls `StartTask`, spawns the provider CLI, streams `ReportProgress`, writes comments/results, and finally `CompleteTask` or `FailTask`.
5. Heartbeats every ~15 s; the server flushes to DB every 60 s.

Desktop augments the picture with an instantaneous path: `useDaemonIPCBridge` subscribes to Electron IPC status changes from the local daemon and patches the React Query runtime list cache directly, giving sub-second feedback while the server-side sweeper still sees the old `last_seen_at`.

```ts
// apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts:27
function mergeDaemonStatus(rt: AgentRuntime, status: DaemonStatusLike): AgentRuntime {
  if (status.state === "running") {
    return { ...rt, status: "online", last_seen_at: new Date().toISOString() };
  }
  ...
}
```

Sources: [server/internal/daemon/daemon.go:1831-1954](server/internal/daemon/daemon.go), [apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts:55-76](apps/desktop/src/renderer/src/platform/daemon-ipc-bridge.ts), [server/internal/handler/daemon.go:238-411](server/internal/handler/daemon.go)

## Cloud Runtimes

Cloud runtimes (`runtime_mode = 'cloud'`) are represented by the same `agent_runtime` rows but are created and lifecycle-managed through the `cloudRuntime` surface (`packages/core/runtimes/cloud-runtime.ts`). They appear as nodes with `instance_type`, `region`, `status` (launching/pending/running/...), and are billed/terminated via the cloud provider. Task dispatch and claim semantics are identical; only the execution environment differs.

Sources: [packages/core/runtimes/cloud-runtime.ts:4-81](packages/core/runtimes/cloud-runtime.ts)

## Task Lifecycle and the Runtime Sweeper

```
queued → dispatched (ClaimTask) → running (StartTask) → completed | failed | cancelled
```

The sweeper (`runRuntimeSweeper`, 30 s interval) enforces liveness:

- Marks `online` runtimes `offline` when `last_seen_at` > 150 s stale (Redis liveness store is the fast path; DB is the fallback).
- Fails any `dispatched`/`running` tasks whose runtime just went offline.
- Expires `queued` tasks older than 2 h (backlog safety valve).
- GCs `offline` runtimes with no active agents after 7 days (`offlineRuntimeTTLSeconds`).

```go
// server/cmd/server/runtime_sweeper.go:72
func runRuntimeSweeper(...) {
    for {
        ...
        sweepStaleRuntimes(...)   // marks offline + fails orphans
        sweepStaleTasks(...)
        sweepExpiredQueuedTasks(...)
        gcRuntimes(...)           // 7-day TTL
    }
}
```

Sources: [server/cmd/server/runtime_sweeper.go:72-87,89-169,208-235,259-280](server/cmd/server/runtime_sweeper.go)

## Health Derivation (Shared, Time-Bucketed)

Raw server state is binary (`status` + `last_seen_at`). The UI needs four buckets so users can distinguish "just died" from "long dead and about to disappear."

```ts
// packages/core/runtimes/derive-health.ts:15
export function deriveRuntimeHealth(runtime: AgentRuntime, now: number): RuntimeHealth {
  if (runtime.status === "online") return "online";
  const offlineFor = now - (lastSeen ?? 0);
  if (offlineFor < 5 * 60_000) return "recently_lost";
  if (offlineFor > 6 * 24 * 3600_000) return "about_to_gc";
  return "offline";
}
```

`useRuntimeHealth` (core) re-renders on a 30 s tick so the buckets advance even without new server data. Desktop's IPC bridge overrides the local daemon's row in the cache for near-instant transition out of "recently_lost".

Sources: [packages/core/runtimes/derive-health.ts:15-27](packages/core/runtimes/derive-health.ts), [packages/core/runtimes/types.ts:6-11](packages/core/runtimes/types.ts), [packages/core/runtimes/use-runtime-health.ts:28-46](packages/core/runtimes/use-runtime-health.ts)

## Skills: Persisted, Reusable Execution Knowledge

After the 008 migration, skills are first-class workspace entities:

- `skill` + `skill_file` (the `SKILL.md` + supporting files)
- `agent_skill` many-to-many

Local daemons expose a discovery surface (`resolveRuntimeLocalSkills`) that walks provider-specific directories (`~/.claude/skills`, `~/.codex/skills`, `~/.cursor/skills`, etc.), parses frontmatter, and returns summaries. The UI can then import a selected skill; the daemon bundles the tree and the server materializes it as a workspace `skill` record. Agents attached to that skill (or given it later) receive the content at task execution time via `LoadAgentSkills`.

Thus a successful agent run that produced useful artifacts or refined instructions can be captured once and reused by any agent in the workspace without the next run "re-learning" the same pattern.

Sources: [server/migrations/008_structured_skills.up.sql:4-42](server/migrations/008_structured_skills.up.sql), [packages/core/runtimes/local-skills.ts:29-78](packages/core/runtimes/local-skills.ts), [server/internal/daemon/local_skills.go:54-84,242-272](server/internal/daemon/local_skills.go)

## Why the Core Package Boundary Matters

`packages/core/runtimes/*` (derive-health, cli-version checks, local-skill polling, cloud node hooks, React Query keys) is deliberately free of `react-dom`, `localStorage`, and framework routing. Both the Next.js web app and the Electron renderer import the identical functions. Desktop-only fast-path logic lives in `apps/desktop/.../platform/daemon-ipc-bridge.ts` and only mutates the shared Query cache. Any change to health bucketing or version parsing is automatically consistent across surfaces.

If health logic had lived in each app, the 5-minute / 6-day thresholds would have diverged the moment one side was edited.

Sources: [packages/core/runtimes/index.ts:1-12](packages/core/runtimes/index.ts), [packages/core/runtimes/queries.ts:51-56](packages/core/runtimes/queries.ts)

## Summary

The execution model is deliberately simple at the boundary: polymorphic assignees + a task queue + per-runtime claim loops + a single source of truth for runtime health and skills in the core package. The daemon and cloud surfaces differ only in where the provider process actually runs; everything else (registration, heartbeats, sweeping, skill import, UI derivation) is shared. This keeps the mental model small and prevents the classic "web works, desktop is stale" or "local daemon shows green while server already GC'd it" failure modes.

Sources: [server/cmd/server/runtime_sweeper.go:89-169](server/cmd/server/runtime_sweeper.go)
