# Multi-agent support for Signet

> Source: <https://signetai.sh/docs/specs/approved/multi-agent-support/>
> Published: 2026-07-22 23:48:45+00:00

# Multi-agent support for Signet

## Context

Solvr deployment needs 3 agents (Dot, Rose, Miles) running on a single Mac Studio with one Signet daemon and one OpenClaw gateway. Today Signet is single-agent only — one identity, one memory pool, one personality per ~/.agents/ install. OpenClaw already supports multi-agent natively (agents.list + bindings in openclaw.json), so the gap is entirely on the Signet side.

User decisions:

**Memory**: Hybrid — single SQLite DB, daemon enforces scope at API level (agent_id column + scope flag: global vs private)** Directory**:`~/.agents/agents/{name}/`

subdirectories, each with their own identity files (SOUL.md, IDENTITY.md, etc.)**Skills**: Shared pool at`~/.agents/skills/`

, per-agent allowlist in agent.yaml**Harness scope**: OpenClaw + Compass only. Claude Code stays single-agent (it’s the operator’s personal agent).

## Critical files

`platform/daemon/src/daemon.ts`

— HTTP server, memory schema, file watcher, harness sync (~4650 LOC)`platform/daemon/src/hooks.ts`

— Request/response interfaces`platform/daemon/src/memory-config.ts`

— Config loading`platform/core/src/types.ts`

— AgentManifest, Memory types`platform/core/src/identity.ts`

— Identity file detection, IDENTITY_FILES spec (line 56-97), loadIdentityFiles (line 206)`platform/core/src/skills.ts`

— Skill registry, unification`surfaces/cli/src/cli.ts`

— CLI commands (~4650 LOC)`surfaces/dashboard/src/`

— SvelteKit dashboard`integrations/openclaw/connector/src/index.ts`

— OpenClaw sync`integrations/openclaw/memory-adapter/src/index.ts`

— Runtime plugin`~/.agents/agent.yaml`

— User config manifest

## Implementation phases

### Phase 1: Core types — AgentDefinition + roster

File: `platform/core/src/types.ts`

```
interface AgentDefinition {
  readonly name: string;
  readonly model?: string;
  readonly skills?: readonly string[];
  readonly personality?: string;  // relative path to SOUL.md
}
```

Extend AgentManifest with optional `agents`

field:

```
readonly agents?: {
  readonly roster: readonly AgentDefinition[];
};
```

### Phase 2: Agent registry — discovery + scaffold + inheritance

New file: `platform/core/src/agents.ts`

`discoverAgents(agentsDir)`

— scan`~/.agents/agents/*/`

for subdirs, return AgentDefinition[]`scaffoldAgent(name, agentsDir)`

— create directory + template SOUL.md, IDENTITY.md`getAgentIdentityFiles(name, agentsDir)`

— resolve identity file paths with**inheritance fallback**: check agent subdir first, fall back to root-level`~/.agents/`

for any missing file. Only SOUL.md and IDENTITY.md are expected to be overridden per agent. AGENTS.md, USER.md, TOOLS.md inherit from root by default.`resolveAgentSkills(agent, sharedSkills)`

— filter shared pool by agent’s allowlist. Empty/missing list = all skills.

**Identity inheritance** (addresses current gap in identity.ts
where loadIdentityFiles only checks one basePath):

```
resolve(file, agentName):
  1. ~/.agents/agents/{agentName}/{file}  ← agent override
  2. ~/.agents/{file}                     ← root default
```

### Phase 3: DB schema — add agent scoping columns

File: `platform/daemon/src/daemon.ts`

Add to `requiredColumns`

array at line ~4509:

```
{ table: 'memories', column: 'agent_id', type: 'TEXT DEFAULT "default"' }
{ table: 'memories', column: 'scope',    type: 'TEXT DEFAULT "global"' }
```

Existing memories get agent_id=‘default’, scope=‘global’. Fully backwards compatible via ALTER TABLE — no migration.

**FTS5 handling** — the FTS5 virtual table does NOT get an
agent_id column. Instead, agent scoping is done as a post-join
filter on the memories table after the FTS5 MATCH:

```
SELECT m.* FROM memories_fts
JOIN memories m ON memories_fts.rowid = m.rowid
WHERE memories_fts MATCH ?
  AND (m.scope = 'global' OR m.agent_id = ?)
ORDER BY bm25(memories_fts) LIMIT ?
```

This avoids rebuilding the FTS5 virtual table and its 3 triggers (INSERT/UPDATE/DELETE at lines 4560-4584), which would be a breaking migration for existing databases. The post-join filter is slightly less efficient but the memories table is small enough (3441 rows currently) that it won’t matter.

Vector search (sqlite-vec) already joins through the embeddings table, so adding the same WHERE clause to the final JOIN is trivial.

**Orphaned memory cleanup**: When an agent is removed via CLI,
don’t auto-delete memories. Instead mark them scope=‘archived’.
A `signet agent purge <name>`

command explicitly deletes.

### Phase 4: Daemon API — scoped memory + agent endpoints

File: `platform/daemon/src/daemon.ts`

a) Helper: `buildAgentScopeClause(agentId?)`

— returns SQL
WHERE fragment. If agentId is provided, returns memories
where scope=‘global’ OR agent_id matches. If omitted,
returns only scope=‘global’ (safe default).

b) Update `/memory/search`

(line ~1300), `/api/memories`

,
remember/recall handlers to accept `agentId`

query param.

c) New endpoints:

`GET /api/agents`

— list discovered agents + roster`GET /api/agents/:name`

— agent detail, resolved identity files, effective skills`POST /api/agents`

— scaffold new agent`DELETE /api/agents/:name`

— archive agent (marks memories, doesn’t delete directory — CLI handles that)

File: `platform/daemon/src/hooks.ts`

Add to RememberRequest and RecallRequest:

```
agentId?: string;
scope?: 'global' | 'private';
```

(SessionStartRequest already has agentId.)

### Phase 5: File watcher — watch agent subdirectories

File: `platform/daemon/src/daemon.ts`

, `startFileWatcher()`

Add `~/.agents/agents/`

to chokidar watch paths. On change
inside an agent subdir, trigger sync for that agent only
(not a full harness rebuild). Parse the agent name from the
changed file path.

### Phase 6: Harness sync — per-agent OpenClaw workspaces

File: `platform/daemon/src/daemon.ts`

New function `syncAgentsToOpenClaw()`

called from
`syncHarnessConfigs()`

(line ~3750):

For each agent in the roster:

- Resolve merged identity (root defaults + agent overrides)
- Write assembled AGENTS.md to a per-agent workspace dir
at
`~/.agents/agents/{name}/workspace/`

- Call connector to patch openclaw.json (see Phase 8)

### Phase 7: CLI — `signet agent`

commands

File: `surfaces/cli/src/cli.ts`

New subcommand group:

`signet agent list`

— discovered agents + roster status`signet agent add <name>`

— scaffold, prompt for SOUL.md personality description, add to roster in agent.yaml`signet agent remove <name>`

— archive memories, remove from roster, optionally delete directory`signet agent purge <name>`

— permanently delete agent memories and directory`signet agent info <name>`

— resolved identity, skills, memory stats

Add `--agent <name>`

flag to:

`signet remember`

— sets agent_id + scope on memory`signet recall`

— scopes search to agent’s visible memories

### Phase 8: OpenClaw connector — multi-agent sync

File: `integrations/openclaw/connector/src/index.ts`

New `syncMultipleAgents(roster, basePath)`

method:

Currently the connector patches `agents.defaults.workspace`

to `~/.agents`

(line 257-275, deep merge). For multi-agent:

a) Build `agents.list`

array from roster:

```
{
  agents: {
    defaults: { workspace: basePath },
    list: roster.map(agent => ({
      id: agent.name,
      name: agent.name,
      workspace: join(basePath, 'agents', agent.name, 'workspace'),
      skills: agent.skills ?? undefined,
      identity: {
        // resolved SOUL.md content for this agent
      }
    }))
  }
}
```

b) Optionally generate `bindings`

array if user provides
channel routing config in agent.yaml.

c) Keep the deep-merge pattern — idempotent, safe to re-run.

File: `integrations/openclaw/memory-adapter/src/index.ts`

The adapter already accepts `agentId`

on `onSessionStart()`

(line 66-100). The gap is that `createPlugin()`

(line 274)
doesn’t extract agentId from the OpenClaw session context.
Fix: read `ctx.agentId`

(provided by OpenClaw when it
resolves which agent handles the current chat) and pass it
through to all daemon calls.

### Phase 9: Dashboard — agent roster view

File: `surfaces/dashboard/src/`

a) New API functions in `lib/api.ts`

:

`listAgents()`

→ GET /api/agents`getAgent(name)`

→ GET /api/agents/:name`createAgent(name)`

→ POST /api/agents

b) Add agent context to LeftSidebar.svelte (section 01, currently just “Identity”). Show a dropdown/switcher listing discovered agents. Selected agent scopes the Memory tab’s queries.

c) New “Agents” tab in center panel (7th tab alongside Config, Memory, Embeddings, Logs, Secrets, Skills):

- Agent cards showing name, personality summary, skill count, memory count
- Add/remove agent actions
- Per-agent memory stats

d) Update `+page.ts`

data loader to fetch agent list on
initial load.

### Phase 10: Setup wizard — multi-agent configuration

File: `surfaces/cli/src/cli.ts`

, `setupWizard()`

(line 1413)

Add a new path in the existing setup wizard decision tree:

After detecting an existing Signet install (line 1447), add option 7: “Configure multiple agents”. This launches a sub-wizard:

- “How many agents?” → prompt for count
- For each agent: name, personality description (writes SOUL.md), skill allowlist (from available skills)
- “Configure OpenClaw routing?” → optional bindings setup
- Writes agents.roster to agent.yaml
- Scaffolds directories, triggers sync

For fresh installs (line 1552), after single-agent identity creation, ask: “Set up additional agents?” → enters the same sub-wizard.

## Updates and migrations

**Existing installs upgrading to multi-agent Signet**:

- DB columns added via
`requiredColumns`

ALTER TABLE — this is the established live-upgrade pattern (daemon.ts:4509). No migration script needed. Existing memories get agent_id=‘default’, scope=‘global’ automatically. - FTS5 virtual table is NOT modified — no rebuild needed.
- agent.yaml gains optional
`agents.roster`

field — old configs without it continue working (single-agent mode). - The
`signet setup`

wizard detects whether agents.roster exists and shows appropriate options. - Dashboard gracefully handles zero agents (shows single identity view as today).

**Version compatibility**: The daemon serves both single-agent
and multi-agent installs. All new API params are optional with
sensible defaults (no agentId = default agent = current
single-agent behavior).

**Update checker** (daemon.ts:2652-2710) is unaffected — it
checks npm/GitHub for new versions, not schema changes.

## Edge cases

**FTS5 + agent scoping**: Post-join filter instead of FTS5
column. Slightly less efficient but avoids breaking migration.
At 3441 memories this is negligible. If scale becomes an issue
later, rebuild FTS5 with agent_id column behind a flag.

**Concurrent agent writes**: SQLite WAL mode + busy_timeout=5s
(daemon.ts:1098) handles this. Two agents writing
simultaneously: one waits up to 5s. At expected write volumes
(a few memories per session) this is fine. Not a hot path.

**Deleted agent with orphaned memories**: Memories are NOT
auto-deleted when an agent is removed. They’re marked
scope=‘archived’ and excluded from search. Explicit
`signet agent purge`

deletes permanently. This prevents
accidental data loss.

**Agent with no SOUL.md override**: Falls back to root
~/.agents/SOUL.md via inheritance chain. Agent still works
but has the default personality. `signet agent info`

shows
which files are inherited vs overridden.

**Empty skills allowlist**: Means “all skills” (same as
OpenClaw convention where omitting `skills`

= all). An
explicit empty array `skills: []`

means no skills.

**Config validation**: agent.yaml parsing currently has no
schema validation (memory-config.ts uses unsafe casts, yaml.ts
silently produces NaN). This is a pre-existing issue, not
caused by multi-agent. We should add basic validation for the
new `agents.roster`

field (name required, unique names,
allowlist entries exist in skills pool) but a full validation
overhaul is out of scope.

**Race in harness sync**: If two agent identity files change
within the 2s debounce window, both trigger sync. The sync
function is idempotent (writes final state, not incremental),
so this is safe — worst case is two syncs instead of one.

## What we are NOT doing

- NOT creating multiple daemon instances
- NOT creating multiple OpenClaw instances
- NOT changing Claude Code behavior (stays single-agent)
- NOT rebuilding FTS5 virtual table (post-join filter instead)
- NOT adding per-agent databases (single DB, column scoping)
- NOT adding crypto-level memory isolation (API-enforced scope is sufficient — HIPAA compliance comes from on-prem + FDE, not from memory partitioning within a trusted daemon)
- NOT overhauling config validation (out of scope, pre-existing)

## Verification

`bun test`

passes after each phase`bun run build`

succeeds`bun run lint`

clean- Single-agent regression: existing install with no agents.roster works identically to before
`signet agent add dot`

scaffolds ~/.agents/agents/dot/ with template SOUL.md, IDENTITY.md`signet agent list`

shows roster with resolved skills`signet remember "test" --agent dot`

stores with agent_id=‘dot’, scope defaults to ‘global’`signet remember "private note" --agent dot --private`

stores with scope=‘private’`signet recall "test" --agent dot`

returns global + dot’s private memories`signet recall "test"`

without —agent returns global only- Dashboard shows agent roster in sidebar, agent tab with cards, memory tab respects agent selector
`GET /api/agents`

returns discovered agents from daemon- OpenClaw config gets agents.list entries after sync
- File changes in ~/.agents/agents/dot/ trigger sync for dot only (check daemon logs)
`signet agent remove dot`

archives memories, removes from roster
