/api/linq/inbound. HMAC verified. Row inserted in messages.The platonic ideal realized with no workspace filesystem. Each turn is a fresh claude --print that receives system prompt + recent transcript + manifest-scoped context. Closest to how noos's existing Slack bot already operates. Fastest path to ship.
One box for everything bot-related. No per-chat Linux users, no home dirs, no tmux, no SSH. Turns are stateless; continuity lives in SQLite + a system prompt rebuilt per turn. noos graph is still a drop-in over HTTPS.
UX is the same as the platonic ideal. For the user-facing scenes, see Mockup 00.
| Component | Where | Persistence |
|---|---|---|
| Channel adapter + everything bot-related | One Fastify process on Fly Machine | Restart-survivable via SQLite |
| SQLite canonical log | Fly volume (1-5 GB, cheap) | Only durable state. Backed up nightly. |
| Claude session state | Nowhere. Rebuilt every turn from SQLite messages. | N/A — intentionally stateless |
| Per-chat workspace FS | Does not exist | N/A |
| Consent-broker paused turns | SQLite row: paused_turns(id, chat_id, proposed_reply, expires_at, …) | Survives restart |
| noos graph | Existing Lightsail deploy, HTTPS | — |
/api/linq/inbound. HMAC verified. Row inserted in messages.chat_config + last N messages + manifest. Calendar not in scope.y. Inbound handler matches the DM reply to the paused turn. Row updated: approval_mode=approve-exact.claude --print --system "<sys with calendar ok>" "<transcript + ask>"// src/turn.ts — stateless per-turn execution (Option 4)
import { spawn } from 'child_process'
export async function executeTurn(chat: Chat, msg: Message, scope: Scope) {
const transcript = await loadTranscript(chat.id, { last: 12 }) // from SQLite
const graphCtx = await noos.fetchContext(chat.id, scope) // HTTPS
const systemPrompt = composeSystemPrompt(chat, scope, graphCtx)
const turnInput = formatTranscript(transcript) + '\n\n' + msg.text
const claude = spawn('claude', ['--print', '--system', systemPrompt, turnInput], {
env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY },
timeout: 90_000,
})
const out = await collectStdout(claude)
return recordAndSend(chat, out)
}
CREATE TABLE paused_turns (
id TEXT PRIMARY KEY,
chat_id TEXT NOT NULL,
source_msg_id TEXT NOT NULL,
dm_msg_id TEXT NOT NULL, -- Jacob's DM we're awaiting approval from
proposed_reply TEXT NOT NULL,
scope_expand JSON NOT NULL, -- {calendar: "2026-04-28T19:00/21:00"}
approval_mode TEXT, -- null | approve-exact | approve-subset | deny | timeout
expires_at INTEGER NOT NULL, -- unix ms
created_at INTEGER NOT NULL,
resolved_at INTEGER
);
| Failure | User-visible | Recovery |
|---|---|---|
| Process restart during a consent loop | Brief delay; bot posts final answer when approved | Paused turns rehydrated from SQLite on boot; DM reply match resumes |
| Claude CLI crash mid-turn | "I had a glitch, try again" | Single-turn fail; no state to clean up |
| noos down | "I can't reach my knowledge graph right now" | Non-graph questions still answered; graph questions retry |
| Anthropic API rate-limit / outage | Queue + "I'm saturated, one moment" | Exponential backoff; drops to tier-2 model if configured |
| SQLite corruption (rare) | Full outage until restore | Nightly Fly-volume snapshot; restore in minutes |
If PRD 002's v0.1 minimum slice ships without a dev surface (Q2 says "defer"), this is the simplest thing that hits every pillar. P4 consent-loop works cleanly with a single SQLite paused-turns table. Reactions and threading come for free from Claude CLI + Linq. Cost per month is lunch money. Option 2 becomes a possible upgrade path if/when code execution becomes the use case — but you don't need it to ship.
rg" — not just "bot answers questions".spawn('claude', ['--print', …]) becomes an SSH exec to box B. Everything else is identical.claude --session-id <chat> -p so Claude's own memory is in B's ~/.claude/ — full No-Docker (Option 2) shape.This migration is additive, not a rewrite. The Channel, SQLite, manifest, consent broker, and audit layer never move.
Compare with the platonic ideal (Mockup 00) or the 2-box No-Docker design.