Three features built on top of @onecli-sh/sdk 0.3.1, landed together because
they share wiring surfaces (session DB schema, delivery dispatcher, Chat SDK
bridge, channel adapter contract).
## OneCLI manual-approval handler
* `src/onecli-approvals.ts` — long-polls OneCLI via the SDK's
`configureManualApproval`; on each request, delivers an `ask_question` card
to the admin agent group's first messaging group, persists a
`pending_approvals` row, and waits on an in-memory Promise resolved by the
admin's button click or an expiry timer. Expired cards are edited to
"Expired (...)" and a startup sweep flushes any rows left over from a
previous process.
* Short 11-byte approval id (`oa-<8 base36>`) instead of the SDK's UUID so the
Telegram 64-byte `callback_data` limit is respected; the OneCLI UUID stays
in the persisted payload for audit.
* Migration 003 consolidated: `pending_approvals` now has the OneCLI-aware
columns from the start (`agent_group_id`, `channel_type`, `platform_id`,
`platform_message_id`, `expires_at`, `status`), `session_id` relaxed to
nullable so cross-session approvals fit.
* `handleQuestionResponse` in `src/index.ts` now routes OneCLI approvals
through `resolveOneCLIApproval` before falling back to the
session-bound approval path.
## Credential collection from chat
New `trigger_credential_collection` MCP tool — the agent researches a
third-party API, calls the tool with `{name, hostPattern, headerName,
valueFormat, description}`, and blocks until the host reports saved, rejected,
or failed. The credential value never enters the agent's context: the user
submits it into a Chat SDK Modal on the host side, the host writes it to
OneCLI via a thin facade (`src/onecli-secrets.ts` — shells out to
`onecli secrets create`, shape mirrors the SDK we expect upstream), and only
the status string flows back to the container via a system message.
* `src/credentials.ts` — host-side handler: delivers the card to the
conversation's own channel (not the admin channel — credential collection
is a user-facing flow, distinct from admin approval), persists a
`pending_credentials` row, drives the submit → `createSecret` → notify
pipeline. Falls back gracefully when the channel doesn't support modals.
* `src/db/credentials.ts` + migration 005: `pending_credentials` table.
* `src/channels/chat-sdk-bridge.ts`: renders a `credential_request` card,
handles the `nccr:` action prefix by opening a Modal with a TextInput,
registers an `onModalSubmit` handler for the `nccm:` callback prefix.
* `container/agent-runner/src/mcp-tools/credentials.ts`: the blocking MCP
tool, mirroring the `ask_user_question` polling pattern.
* `container/agent-runner/src/db/messages-in.ts`: `findCredentialResponse`
helper to pick up the system message the host writes back.
## Threaded adapter routing
The destination layer previously didn't carry thread context, so agent replies
to Discord always landed in the root channel regardless of which thread the
inbound came from.
* `ChannelAdapter.supportsThreads: boolean` — declared by every channel skill
at `createChatSdkBridge`. Threaded: Discord, Slack, Teams, Google Chat,
Linear, GitHub, Webex. Non-threaded: Telegram, WhatsApp Cloud, Matrix,
Resend, iMessage.
* `src/router.ts`: non-threaded adapters strip `threadId` at ingest (threads
collapse to channel-level sessions). Threaded adapters override the
wiring's `session_mode` to `'per-thread'` so each thread = a session
(except `agent-shared`, which is preserved as a cross-channel intent the
adapter can't know about).
* `session_routing` table in `inbound.db` — single-row default reply routing
written by the host on every container wake from
`session.messaging_group_id` + `session.thread_id`. Forward-compat
`CREATE TABLE IF NOT EXISTS` handles older session DBs lazily.
* `container/agent-runner/src/db/session-routing.ts` — container-side reader.
* `send_message` / `send_file` / `ask_user_question` / `send_card` /
scheduling tools all default their routing (channel, platform, **and**
thread) from the session when no explicit `to` is given. Explicit `to`
uses the destination's channel with `thread_id = null` (cross-destination
sends start a new conversation elsewhere).
* `poll-loop.ts::sendToDestination` (the final-text single-destination
shortcut) now inherits `thread_id` from `RoutingContext` too — this was
the root cause of Discord replies landing in the root channel even after
`send_message` was wired correctly.
## Related cleanups
* `src/container-runner.ts`: OneCLI agent identifier switched from the lossy
folder-derived string to `agent_group.id`, making `getAgentGroup(externalId)`
a trivial reverse lookup for per-agent scoping.
* `wakeContainer` race fix via an in-flight promise map — concurrent wakes
during the async buildContainerArgs / OneCLI `applyContainerConfig` window
no longer double-spawn containers against the same session directory.
* `src/db/db-v2.test.ts`: dropped the brittle `expect(row.v).toBe(N)` schema
version assertion — it had to be bumped on every migration addition.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
5.5 KiB
TypeScript
165 lines
5.5 KiB
TypeScript
/**
|
|
* Reference copy of the current v2 schema.
|
|
* Read this to understand the DB structure.
|
|
* Actual creation is done by migrations — do not use this at runtime.
|
|
*/
|
|
|
|
export const SCHEMA = `
|
|
-- Agent workspaces: folder, skills, CLAUDE.md, container config
|
|
CREATE TABLE agent_groups (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
folder TEXT NOT NULL UNIQUE,
|
|
is_admin INTEGER DEFAULT 0,
|
|
agent_provider TEXT,
|
|
container_config TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
-- Platform groups/channels
|
|
CREATE TABLE messaging_groups (
|
|
id TEXT PRIMARY KEY,
|
|
channel_type TEXT NOT NULL,
|
|
platform_id TEXT NOT NULL,
|
|
name TEXT,
|
|
is_group INTEGER DEFAULT 0,
|
|
admin_user_id TEXT,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(channel_type, platform_id)
|
|
);
|
|
|
|
-- Which agent groups handle which messaging groups
|
|
CREATE TABLE messaging_group_agents (
|
|
id TEXT PRIMARY KEY,
|
|
messaging_group_id TEXT NOT NULL REFERENCES messaging_groups(id),
|
|
agent_group_id TEXT NOT NULL REFERENCES agent_groups(id),
|
|
trigger_rules TEXT,
|
|
response_scope TEXT DEFAULT 'all',
|
|
session_mode TEXT DEFAULT 'shared',
|
|
priority INTEGER DEFAULT 0,
|
|
created_at TEXT NOT NULL,
|
|
UNIQUE(messaging_group_id, agent_group_id)
|
|
);
|
|
|
|
-- Sessions: one folder = one session = one container when running
|
|
CREATE TABLE sessions (
|
|
id TEXT PRIMARY KEY,
|
|
agent_group_id TEXT NOT NULL REFERENCES agent_groups(id),
|
|
messaging_group_id TEXT REFERENCES messaging_groups(id),
|
|
thread_id TEXT,
|
|
agent_provider TEXT,
|
|
status TEXT DEFAULT 'active',
|
|
container_status TEXT DEFAULT 'stopped',
|
|
last_active TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
CREATE INDEX idx_sessions_agent_group ON sessions(agent_group_id);
|
|
CREATE INDEX idx_sessions_lookup ON sessions(messaging_group_id, thread_id);
|
|
|
|
-- Pending interactive questions
|
|
CREATE TABLE pending_questions (
|
|
question_id TEXT PRIMARY KEY,
|
|
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
message_out_id TEXT NOT NULL,
|
|
platform_id TEXT,
|
|
channel_type TEXT,
|
|
thread_id TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
`;
|
|
|
|
/**
|
|
* Session DB schemas — split into two files so each has exactly one writer.
|
|
* This eliminates SQLite write contention across the host-container mount boundary.
|
|
*
|
|
* inbound.db — host writes, container reads (read-only mount or open read-only)
|
|
* outbound.db — container writes, host reads (read-only open)
|
|
*/
|
|
|
|
/** Host-owned: inbound messages + delivery tracking + destination map. */
|
|
export const INBOUND_SCHEMA = `
|
|
CREATE TABLE messages_in (
|
|
id TEXT PRIMARY KEY,
|
|
seq INTEGER UNIQUE,
|
|
kind TEXT NOT NULL,
|
|
timestamp TEXT NOT NULL,
|
|
status TEXT DEFAULT 'pending',
|
|
process_after TEXT,
|
|
recurrence TEXT,
|
|
tries INTEGER DEFAULT 0,
|
|
platform_id TEXT,
|
|
channel_type TEXT,
|
|
thread_id TEXT,
|
|
content TEXT NOT NULL
|
|
);
|
|
|
|
-- Host tracks delivery outcomes for messages_out IDs.
|
|
-- Avoids writing to outbound.db (container-owned).
|
|
CREATE TABLE delivered (
|
|
message_out_id TEXT PRIMARY KEY,
|
|
platform_message_id TEXT,
|
|
status TEXT NOT NULL DEFAULT 'delivered',
|
|
delivered_at TEXT NOT NULL
|
|
);
|
|
|
|
-- Destination map for this session's agent.
|
|
-- Host overwrites on every container wake AND on demand (admin rewires, new child agents, etc.).
|
|
-- Container queries this live on every lookup, so admin changes take effect
|
|
-- mid-session without requiring a container restart.
|
|
CREATE TABLE destinations (
|
|
name TEXT PRIMARY KEY,
|
|
display_name TEXT,
|
|
type TEXT NOT NULL, -- 'channel' | 'agent'
|
|
channel_type TEXT, -- for type='channel'
|
|
platform_id TEXT, -- for type='channel'
|
|
agent_group_id TEXT -- for type='agent'
|
|
);
|
|
|
|
-- Default reply routing for this session. Single-row table (id=1).
|
|
-- Host overwrites on every container wake from the session's messaging_group
|
|
-- and thread_id. Container reads it in send_message / ask_user_question /
|
|
-- trigger_credential_collection to default the channel/thread of outbound
|
|
-- messages when the agent doesn't specify an explicit destination.
|
|
CREATE TABLE session_routing (
|
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
channel_type TEXT,
|
|
platform_id TEXT,
|
|
thread_id TEXT
|
|
);
|
|
`;
|
|
|
|
/** Container-owned: outbound messages + processing acknowledgments. */
|
|
export const OUTBOUND_SCHEMA = `
|
|
CREATE TABLE messages_out (
|
|
id TEXT PRIMARY KEY,
|
|
seq INTEGER UNIQUE,
|
|
in_reply_to TEXT,
|
|
timestamp TEXT NOT NULL,
|
|
deliver_after TEXT,
|
|
recurrence TEXT,
|
|
kind TEXT NOT NULL,
|
|
platform_id TEXT,
|
|
channel_type TEXT,
|
|
thread_id TEXT,
|
|
content TEXT NOT NULL
|
|
);
|
|
|
|
-- Container tracks processing status here instead of updating messages_in.
|
|
-- Host reads this to know which messages have been processed.
|
|
-- On container startup, stale 'processing' entries are cleared (crash recovery).
|
|
CREATE TABLE processing_ack (
|
|
message_id TEXT PRIMARY KEY,
|
|
status TEXT NOT NULL,
|
|
status_changed TEXT NOT NULL
|
|
);
|
|
|
|
-- Persistent key/value state owned by the container. Used (among other things)
|
|
-- to store the SDK session ID so the agent's conversation resumes across
|
|
-- container restarts. Cleared by /clear.
|
|
CREATE TABLE session_state (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
`;
|