Skip to content
←Return to Projects
Case Study

KIWI

Autonomous local AI agent and community of voices living in The Grove with private memory.

The Problem

Autonomous AI agents often suffer from leaky memory isolation, unpredictable side-effects on host systems, and lack of human-in-the-loop governance for high-stakes file operations and external device actions.

My Role

Architected and implemented the cognitive engine ("The Grove"), fail-closed host action safety policies, immutable exact-action plan ledgers, multi-device agent companion protocol, and unified multi-channel gateways.

Highlights

  • Community of six voices living in The Grove with local memory running on the user's machine
  • Local cognitive core ("The Grove") separating owner memory from credentialed client sessions
  • Fail-closed security policy requiring immutable exact plans with rollback for all destructive actions
  • DPAPI-secured Windows companion agent with local execution receipts and anti-re-execution guarantees
  • Multi-modal interaction surface: Next.js Web UI, Voice HUD (:4321), and Telegram bot gateway with inline approvals
  • Deterministic audit ledger tracking every state mutation, file movement, and memory association

Stack

TypeScriptNext.js 15Node.jsSQLiteDrizzle ORMDPAPITelegram Bot APIDocker

Constraints

  • Host safety is non-negotiable: no autonomous file deletion or batch reorganization without human-confirmed exact plans.
  • Offline-first engine: core cognitive loops and memory databases must run purely locally without cloud dependencies.
  • Multi-tenant memory isolation: administrative sessions and API token holders must never access the owner private mind context.

System Architecture

Interaction Channels
Next.js 15 Web App (:3333)
Voice HUD (:4321)
Telegram Bot
Embed Bridge (:3234)
Gateway & Protocol
Private Grove API (:8431)
Device Control API
DPAPI Token Auth
Cognitive Engine ("The Grove")
Mind Core
Behavior & Persona
Memory Tiering (STM/MTM/LTM)
Skill Registry
Execution & Safety
Host Action Policy
Exact-Action Ledger
Backup & Rollback Ledger
Distributed Node Runtime
Windows Companion Daemon
Local File Executor
SQLite / Drizzle State

Key Trade-offs

The decisions worth defending — what I chose, what I turned down, and why.

File management execution model

Chose

Immutable two-phase exact-action plans with hash verification and dry-run preview

Rejected

Direct agent tool execution with broad shell permissions

Agents making speculative shell or file modifications risk catastrophic data loss; two-phase plans guarantee human oversight and rollback capability.

Engine-to-web communication architecture

Chose

Dedicated loopback HTTP/SSE gateway with bearer token authentication

Rejected

Monolithic single-process Next.js application

Decoupling the AI engine process from the Next.js frontend ensures background monitoring, telegram polling, and long-running skills survive web redeployments.

Device pairing security model

Chose

Windows DPAPI machine-bound tokens with polling architecture

Rejected

Inbound SSH or remote socket servers on client machines

Inbound listening ports expose clients to local network scans; outbound HTTPS polling behind DPAPI keeps remote companion devices secure.

What I'd Do Differently

An honest retrospective — the stuff I'd change with more time, more users, or a second pass.

  1. 1Implement automated regression tests for memory graph decay sweeps earlier in the development lifecycle.
  2. 2Consolidate SQLite schema migrations between better-sqlite3 and pg-schema under a single unified migration runner.
  3. 3Add fine-grained telemetry dashboards to observe cognitive latency across multi-turn reasoning chains.

Interactive Autonomous Agent Shell

Test live KIWI autonomous agent supervisor routines (agent status, grove ping, mcp tools, help) in the emulated shell below.

icelegends-interactive-shell ~ [kiwi]
History: ↑ / ↓
kiwi@the-grove:~/voices$

Technical Deep-Dive

Architecture, specifications, and implementation details.

P0 architecture audit — account and execution boundaries

Revision 17: the audit closes the top-to-bottom P0 items left open below. Gap closed: the file-organizer automation could never move files yet, but the safe replacement flow did not exist; now the builtin file_organizer skill (kind file_organizer in the ledger) is the real executor — shared classification in src/skills/fileOrganize.ts (computeOrganizeMoves: EXTENSION_MAP categories, plausible destination tree, collision-safe (2) targets checked against claimed and on-disk paths, archiveAfterDays → _archive, unclassified files/directories counted and left in place; returns { moves:[{from,to,size,archived}], skipped }), dry-run stalls an exact plan with recovery copies, execution only with a validated exactPlan:{id,kind:'file_organizer'} + human approval (allow to prepare, confirm to run — same policy shape as revision 16; no reference → deny). The mind automation file-organizer (v1.1.0) is preview-only — it reports what would move and where, and it never touches the disk, so the only move path is the chat-approved exact plan. dev_task is now fail-closed by default: src/config/config.ts sets security.skillPolicy.dev_task: 'deny' (previously confirm, a latent footgun), and dev_status/dev_cancel refuse when dev mode is off — DevManager.start has no active production caller, so no background coding job can start (safe replacement: P1 device tasks / P2 interactive coding). Config/target drift: configPath() in src/config/config.ts now honors KELVIN_CONFIG_PATH (default unchanged; loadConfig/configExists/saveConfig all use it — tests run against a throwaway file, never the real kiwi.config.json holding encrypted secrets), and POST /api/documents/root in src/grove-api.ts syncs the persisted saveConfig result back into kiwi.mind.config.skills and kiwi.skillContext.documentsDir, so the live documents root no longer diverges from the runtime config object until restart (previously the auto-summarizer and indexer could see the new root while runtime config still had the old one). Memory continuity gap closed: GET /api/kiwi/knows in web/src/app/api/kiwi/knows/route.ts previously let any credentialed user proxy the owner's engine /api/knows (owner facts/nature/peeves/threads); it now returns engine-backed data only for an admin browser session without an authorization header, otherwise an embedded {source:'embedded', nature: getNatureObservations(userId), facts:[], ...} per-user payload (DELETE/PATCH were already admin-gated). Bounded input: the document chat stream route now reads through readChatBody (500 KB body / 100k-char message) and the engine rejects 413 on both /api/documents/chat and /api/documents/chat/stream before contacting the LLM. Attachment audit (production): scripts/audit-attachment-links.mjs is a read-only preflight on data/kelvin.db (still reports tableExists:false, migrationRequired:true — the message_files table only appears when the web app starts); new scripts/repair-attachment-links.mjs audits and, with --fix, repairs inside one transaction — dedupe (message_id,file_id) pairs keeping MIN(rowid), delete orphan associations (missing message/file) and mismatched-owner associations; it never touches file storage. /api/chat/files rejects 413 from Content-Length before buffering the multipart body (plan cap + 1 MB framing). New tests: engine 4 (file_organizer exact-plan prepare/deny/execution in tests/exact-plan.test.ts), engine 2 (tests/dev.test.ts: default config denies dev_task; dev_status/dev_cancel fail-closed), engine 2 (tests/documents-root-scope.test.ts), engine 1 (tests/documents-chat-bound.test.ts); web 4 updated in tests/mind-route-isolation.test.ts (+ knows-route coverage) + 5 new in web/tests/document-chat-body-bound.test.ts + 1 new in web/tests/chat-file-lifecycle.test.ts (Content-Length 413). The repair script and its --fix were exercised against a fixture DB (5 associations → 2, duplicates/orphans/mismatches removed). Verification: 132 engine tests (131 passed + one optional semantic skip); 228 mind/web tests in 34 suites; both typechecks passed. No production rollout.

Revision 16: the audit continues with exact-action plans for broad moves/deletions. The gap closed in revision 16: non-dryRun documents_organize and disk_doctor reap were refused (exact-plan reason) but no mechanism existed; now src/exactions/ledger.ts provides it — an immutable plan (id, kind, absolute-path ops with size; sha256 for move files ≤16 MB), prepareExactPlan (validates source exists/target free/size>0, recovery copies for moves in data/exactions/<id>/moves/N.bin under KELVIN_DATA_DIR, ledger data/exactions/ledger.json with status:'prepared'), validateExactPlan (canonical {kind, ops} immutability + on-disk state re-checked at every execution), executeExactPlan (move=rename; delete=rename into deletes/N, no rm; status:'partial' on first error; 'done' + executedAt on success), rollbackExactPlan (restores, idempotent; rolledback plans cannot be re-loaded). Policy (src/security/host-action-policy.ts): exactPlanRefOf accepts only exactPlan:{id,kind}; no ref → deny (unchanged), valid ref → confirm (mandatory human approval via ConversationManager, never auto-execution); preparing stays allow (dryRun:true / action:plan_reap). documents_organize stages the plan on dryRun:true (LLM classification, collision-free targets, recovery copies) and executes only with a valid plan; disk_doctor plan_reap stages, reap+exactPlan moves into backup (the old destructive reap was removed). dev_task and file-organizer remain refused (P1). New tests: 11 in tests/exact-plan.test.ts (move+delete prepare/validate/execute/rollback, collision, partial, documents_organize flow, disk_doctor flow, deny→confirm policy, real manager approval, ref shape, registry-level block). Verification: 124 engine tests (123 passed + 1 optional semantic skip; +11); 222 mind/web tests in 33 suites; both typechecks passed. No production rollout.

Revision 15: plugin skills move under a fail-closed declaration policy. The gap left open in revision 14: skills from plugins/ (loaded by src/skills/plugins.ts) carried no capability metadata — hostSkillPolicy treated them as unknown with an implicit allow, and direct registry.run callers (document skills in src/grove-api.ts:941, CLI diagnostics in src/cli/kiwi.ts:302) executed without the full policy. Corrections: Skill.capabilities (read/write/exec/network/system/media, src/skills/types.ts); hostSkillPolicy takes a plugin DeclaredSkillInfo and declaredSkillPolicy (src/security/host-action-policy.ts) — no declaration or any mutational effect → confirm, read-only → allow, and an old configured allow cannot weaken an undeclared plugin (fail-closed); SkillRegistry.skillDeclaration exposes the metadata to every policy path: Agent.declaredSkill (JSON/stream/offline, src/agent/agent.ts), ConversationManager (src/conversation/manager.ts), direct Telegram (src/telegram/run.ts); a backstop in registry.run refuses an undeclared plugin before run() so direct callers stay safe; src/skills/plugins.ts warns on load; plugins/example-dice.mjs declares ['read']. Built-ins and skills registered without a sourceFile are not plugins → allow preserved (the host-action-boundary/web-conversation-scope tests are unaffected). New tests: 8 in tests/plugin-capabilities.test.ts (fail-closed with a stale allow, configured deny, tool scope, skillDeclaration built-in vs plugin, backstop that never reaches run(), direct built-in run that still passes). Verification: 113 engine tests (112 passed + 1 optional semantic skip; +8); 222 mind/web tests in 33 suites; both typechecks passed. No production rollout.

Revision 14: the audit continues with mind automation and the documents root. The automation branch in mind.hear (src/mind/kelvin.ts) dispatched the automation skills with ALL permissions and no host-action policy — the only production caller is the brain-down fallback in src/grove-api.ts:122 (the scoped route returns 503 while the brain is down; ≤120s handshake window). Corrections: hostSkillPolicy classifies the automation skills (app-launcher/note-taker → confirm; non-dryRun file-organizer → deny with the exact-plan reason; read-only web-search/system-monitor/file-finder/daily-briefing/timer/reminder stay allow), and mind.hear applies the policy before execution — confirm → blocked like an internal thread (automationResult:'blocked', stored as such), deny → refusal with the reason; it grants only the skill's declared permissions (least privilege); app-launcher refuses shell metacharacters in appName/args (isSafeLaunchToken) before execSync. POST /api/documents/root now uses resolveDocumentsRoot (src/documents/request-scope.ts): new folders may only be created inside PROJECT_ROOT; selecting an existing folder anywhere stays allowed (current deployment C:\Users\alexm\Documents\KiwiDocs, untouched). Plugins remain reachable only through the registry under the same policy, but still have no capability metadata (open P0 migration). New tests: engine 4 (tests/mind-automation-policy.test.ts ×3: classification, exact-plan reason, metacharacter refusal + dry-run; tests/document-scope.test.ts +1: documents root). Verification: 105 engine tests, 104 passed + 1 optional semantic skip; 222 mind/web tests in 33 suites; both typechecks passed. No production rollout.

Revision 13: crash recovery for the owner web chat. When the admin browser disconnects mid-turn, the engine still completes and persists the round in the scoped thread, but the web SSE route stops at stopped() and never saves the assistant reply. The engine exposes GET /api/chat/scoped/thread in src/grove-api.ts (read-only; validates the owner-web:v1:<sha256> identity with isWebConversation and returns { ok, conversationId, turns: [{ role, content }] }) via ConversationManager.getTurns(chatId) in src/conversation/manager.ts (copy of the thread; untouched thread → []; reading never mutates the store). The web consumes it on GET /api/chat/history?channel=web&sessionId=... for an admin account through reconcileOwnerChat in web/src/lib/chat-reconcile.ts (bridge ownerChatTurns in web/src/lib/owner-chat.ts): it collapses threads into { user, assistant } rounds, and if the last web message is a user with no assistant after it and the last completed engine round has the same user content, it appends the missing reply (appendMessage('assistant', ...)) — tail-only, at most one append, idempotent. An engine round ending in a lone user means it is still generating; no reconciliation. Best-effort: an engine error is ignored/logged and the history is still served; multi-gap is deliberately rejected (appending at the tail would scramble ordering). New tests: engine 3 (getTurns copy without mutation; thread endpoint identity validation + correct reply; empty/untouched thread), web 6 (missing reply appended once; last already answered → 0; content divergence → 0; lone trailing user → 0; empty thread/history → 0; history route admin vs non-admin, non-web channel, engine offline never blocks). Verification: 101 engine tests passed, one optional semantic skip; 222 mind/web tests in 33 suites; both typechecks passed. No production rollout.

Revision 12: the Telegram webhook and the message quota can no longer be disturbed by redelivery or concurrency. web/src/app/api/telegram/webhook/route.ts reads update_id, records it exactly once in telegram_webhook_updates ((chat_id, update_id) key, INSERT OR IGNORE, 200 without processing on a duplicate, pruning of rows older than 48h) and consumes the quota via consumeMessageIfUnder (a single UPDATE ... WHERE messages_used < limit, changes === 1), placed after the file-only filter and before appendMessage — file-only/turnless messages do not consume quota. web/src/lib/db/index.ts, sqlite-schema.ts and pg-schema.ts carry the table (parity). src/telegram/run.ts persists the update_id offset in data/telegram-owner-offset.json (KELVIN_DATA_DIR in tests) and resumes from it after a restart. New tests: web 4 (web/tests/telegram-webhook-dedup.test.ts: duplicate processed once, atomic consumption at the ceiling for concurrent requests, over-quota rejection, file-only message without consumption), engine 1 (offset persisted, resumed on restart). Verification: 97 engine tests passed, one optional semantic skip; 210 mind/web tests in 31 suites; both typechecks passed. No production rollout.

Revision 11: the chat routes now enforce body and message bounds before any call to the brain or engine. The web reads the body through web/src/lib/chat-body.ts with a 500,000-byte cap (streaming read, no unbounded buffer), validates JSON (400 for a missing/invalid body or empty message) and rejects messages over 100,000 characters with 413 on both routes (/api/chat JSON and /api/chat/stream SSE); fileIds stays raw so resolveChatAttachments rejects malformed values. The engine (/api/chat/scoped in src/grove-api.ts) rejects non-string or over-cap messages with 413 without contacting the manager. New tests: engine 1 (over-cap bodies rejected without contacting the manager), web 5 (web/tests/chat-body-bound.test.ts). Verification: 97 engine tests passed, one optional semantic skip; 205 mind/web tests in 30 suites; both typechecks passed. No production rollout.

Status: in progress, plan revision 17, 2026-09-09. Local changes only; no deployment. This is a partial audit, not a complete project certification. Revision 17: file_organizer closes its exact-plan flow (kind file_organizer in the ledger; builtin skill allow to prepare/confirm to run; the mind automation is preview-only, never moves), dev_task is fail-closed by default (dev_task:'deny' in config; dev_status/dev_cancel refuse without dev mode), config/target drift is closed (configPath() honors KELVIN_CONFIG_PATH; POST /api/documents/root syncs kiwi.mind.config.skills), owner memory is isolated (GET /api/kiwi/knows engine-backed for admin browser only; other accounts get embedded per-user payloads), document chat routes are bounded (web readChatBody + engine 413 on both routes) and the attachment audit is complete with a transactional --fix (scripts/repair-attachment-links.mjs; production data/kelvin.db still lacks message_files until the web restarts — preflight only, no writes). Revision 16: documents_organize and disk_doctor reap move to persistent exact-action plans with backup/rollback (src/exactions/ledger.ts) and execution only with a valid exactPlan:{id,kind} + human approval; dev_task stays blocked. Revision 15: plugin skills are fail-closed on declared capabilities (Skill.capabilities, declaredSkillPolicy, registry.run refuses an undeclared plugin before execution) — the implicit-allow gap is closed. Revision 14: mind automation (mind.hear) passes through the host-action policy with least privilege and a shell-injection-proof app-launcher; POST /api/documents/root restricts folder creation to the project. Revision 13: crash recovery — GET /api/chat/scoped/thread (read-only) + reconcileOwnerChat on GET /api/chat/history?channel=web&sessionId=... for admins appends an assistant reply lost to an interrupt (tail-only, idempotent, best-effort). Revision 12: Telegram update_id deduplication and atomic quota consumption. Revision 11: body/message bounds on the chat routes. Revision 10: ConversationManager serialized per-thread delivery (runSerialized), the engine exposed DELETE /api/chat/scoped (validating the owner-web:v1:<sha256> identity and waiting for the in-flight turn), the web asked the engine to forget the matching scoped thread on admin session deletion without blocking on engine failure, and POST /api/documents/organize refused any non-dryRun. Verification revision 17: 132 engine tests (131 passed + one optional semantic skip); 228 mind/web tests in 34 suites; both typechecks passed.

Revision 8: chat transport continuity routes admin browser JSON and SSE through the same owner endpoint. An engine failure is explicit and never switches to the embedded account mind; the browser never automatically repeats a failed or partial request through JSON. SSE rejects Authorization headers, including alongside an admin cookie. The owner reply is buffered, not incremental model streaming. Current verification: 192 mind/web tests in 28 suites, web typecheck, isolated production build and Edge chat probe passed. Persistent personal state, internal engine fallback and explicit conversation boundaries remain open. No production deployment.

Revision 7: host action authorization now enforces tool scope in JSON/stream/offline paths, composes configured and caller restrictions, preserves the originating approval policy and prevents internal routine text from authorizing mutations. Known host changes require confirmation; previews are complete or refused. Direct Telegram rechecks policy. Autonomous dev launch, bulk document moves and disk reaping are blocked pending exact plans. Revision 7 validation: 90 engine tests passed, one optional semantic test skipped; 180 mind/web tests in 27 suites passed. P0 remains open for memory continuity, background automation/plugin paths and exact target migration. No production rollout.

#Revision 6 corrections and evidence

  • Invalid, empty or revoked Authorization credentials no longer inherit a browser identity through currentUserIdFrom; ISO download also rejects that fallback. Browser attachment endpoints reject Authorization headers.
  • Attachment lifecycle: upload/list require an owned conversation; SQLite reserves quota before asynchronous writes; generated storage names replace supplied paths; deletion checks the managed root and retains metadata on errors. Expired attachments cannot enter new prompts.
  • Telegram cleanup previously deleted all shared upload files older than 24 hours, including unexpired web files. It now removes only explicitly expired managed records/files, preserving legacy null-expiry records and unknown files. New Telegram uploads expire after 30 days. No production cleanup or PDF-library changes were performed.
  • Current web validation: 177 tests in 27 suites passed, web typecheck and companion compilation passed. Isolated production build and browser/HTTP probe passed with synthetic accounts, a real temporary edit, foreign-account denial, result persistence after probe-server restart, and revocation. Local approval is simulated; this is one PC, not two-PC acceptance. The prior engine baseline remains 80 passed and one optional semantic test skipped.
  • Eager email-client initialization was moved to send time, allowing builds without provider credentials. No real emails were sent. Production migration, host mutations and memory continuity remain open.

#Revision 5 corrections and evidence

  • /api/kiwi/status and /api/kiwi/memory had the same owner-engine leak as JSON chat. Ordinary accounts and credential-bearing requests now use their own embedded state; admin browser access is retained. Four handler tests cover two identities, missing auth and credential/admin mixing.
  • Web Telegram accepts only a matching private sender/chat, claims unexpired pairing codes atomically and never rotates an expired code from a remote message. Linked-chat lookup rejects ambiguous owners. Legacy conversation lookup includes owner; new IDs hash owner + chat. Six real-SQLite/handler tests cover ownership, expiry and replay.
  • Conversation confirmation binds the raw channel identity, retains a copy of the arguments, expires after five minutes and rechecks current deny policy. Ambiguous text does not authorize execution. Direct Telegram uses telegram:<sender> for this boundary, although configured shared history remains shared.
  • Auto-resolve checks proposal expiry before decisions and bulk execution. Its model may use only diagnostic tools through src/security/diagnostic-policy.ts; it no longer receives an unrestricted execution gate. Exact repair execution remains to be integrated with device tasks.
  • The document gateway formerly dispatched arbitrary skills and mutated the shared engine documents directory. It now validates a document-only allowlist, rejects owner overrides and actual file organization moves, and uses a cloned request context. Document chat is stateless and uses only the current grounding, without shared owner history, tools, status payload or fallback. Three engine tests and one web handler test verify these boundaries; live provider/engine endpoint behavior still needs validation.
  • node scripts/audit-attachment-links.mjs read data/kelvin.db without writes and returned tableExists: false, migrationRequired: true. No production associations were repaired. A differently configured database must be audited separately.
  • State ownership and remaining continuity gaps are documented. P1 pilot has real temporary SQLite/file tests and an isolated package installation; it is disabled and has not passed two-PC acceptance.
  • Latest completed full run before the tunnel-origin regression addition: 158 mind/web tests in 23 suites; 79 engine tests passed, one optional semantic test skipped. Both typechecks and companion compilation passed. The pilot document/checkpoint record subsequent verification. No web build, deployment or restart in this milestone.

#Confirmed defect and correction

The non-streaming web handler at web/src/app/api/chat/route.ts sent every authenticated user's message and persona notes to the host engine without a user identifier. src/grove-api.ts processes that endpoint through kiwi.manager.reply('web', ...) against the shared owner mind.

The handler now permits the host engine only for an admin browser session without an Authorization header. Ordinary accounts and requests carrying credentials use chat(message, userId, briefs). Credentials cannot inherit an unrelated browser admin session's engine access. History, attachments and billing are retained; revision 8 replaces the owner web fallback with an explicit error and also routes owner SSE through the engine.

This restores the boundary documented by ADR-0006. The chat correction alone does not implement device routing, prove embedded-brain isolation or reconcile the owner's streaming and non-streaming memories. The separate P1 pilot does not resolve those memory gaps. Neither has been deployed.

#Attachment ownership and clean database startup

Both POST /api/chat and POST /api/chat/stream now call web/src/lib/chat-attachments.ts before creating history or consuming quota. Optional fileIds must be an array of nonempty string IDs; every unique ID must belong to the authenticated user. A foreign, missing or malformed ID rejects the entire request with HTTP 400 and Invalid or unavailable attachments. Missing and foreign IDs have the same response. Duplicate IDs are linked once. Owned files with pending extraction remain attached, while only available extracted text enters the prompt.

Previously, content selection was owner-filtered but message associations used the raw requested IDs. Both paths now use the same verified list. Startup in web/src/lib/db/index.ts also creates the missing message_files table with cascading foreign keys and its existing Drizzle indexes, including uniqueness of (message_id, file_id). Clean temporary SQLite databases exercise this path. Existing production associations have not been audited or repaired; check duplicates/orphans before rollout. This is not a PostgreSQL migration or an audit of every upload/download route.

#Direct Telegram owner channel

src/telegram/run.ts now accepts only explicitly allowlisted positive Telegram user IDs in their own private chats. Empty allowlists deny access and send no broadcast output; groups cannot issue owner commands. Unauthorized messages cannot change the dev-notification fallback recipient, and explicit dev recipients must also be trusted IDs.

src/telegram/approvals.ts binds action and auto-resolve buttons to the user, private chat and delivered message. Approvals expire after five minutes, are consumed before execution, and disappear on restart. Invalid choices, unknown tokens, another sender/message and replay are rejected. Broadcast proposals are consumed once across trusted recipients; cleanup uses each original chat/message pair. /open now asks before launching a program.

This remains an owner-administration channel with shared owner history, not per-account device access. Allowlisted people administer the same host. Auto-resolve is diagnostic-only. Since revision 16, /docs organize move and disk reaping run on exact approved plans with backup/rollback; application OpenCode launches (dev_task) and file-organizer remain blocked pending exact action plans; generic host actions now have mandatory confirmation. Resolved target/configuration binding and remaining background/plugin paths still need migration. Web-managed Telegram is a separate account-scoped path with the revision 5 corrections above.

#Test environment corrections

Vitest resolves server-only to a test-only empty module, matching the server entrypoint behavior; production imports remain intact. Test bootstrap SQLite connections now close before temporary-directory deletion. The instance fixture no longer inserts an obsolete users.mission column. Offline embedded-chat tests no longer load web/.env and block fetch; real-provider mode remains explicit through RUN_LLM_E2E=1 and was not run.

#Previous validation baseline — revision 4

  • Web typecheck passed before and after the change.
  • npm run test:mind -- --run web/tests/chat-routing-isolation.test.ts: before the fix, 2 failed and 3 passed; after the fix, all 5 passed.
  • The tests invoke the real route handler with mocked auth, persistence and brains. They cover two ordinary identities, API credentials alongside an admin session, owner engine access, owner fallback and unauthenticated rejection. They do not exercise a live DB, provider or physical PC.
  • Earlier baseline: 11 suites passed, 4 failed; 99 tests passed, 20 skipped, with unresolved server-only and Windows EPERM. After the test environment repairs, 119 tests passed; after this step's regression additions, npm run test:mind passes all 17 suites / 138 tests.
  • npm run test: 66 passed, 1 optional semantic-model test skipped, 0 failed. npm run typecheck and npm run typecheck:web pass.
  • web/tests/chat-attachments-isolation.test.ts: 14 real-handler tests using a temporary SQLite database and two owners; brains/auth/billing are mocked. Checks rejection without history/quota consumption, owned content and unique associations for both routes.
  • tests/telegram-approvals.test.ts: 6 tests for identity, message binding, expiry, restart invalidation, single use and broadcast recipients. tests/mind/unit/telegram-owner-channel.test.ts: 5 real-polling-handler tests with mocked transport and no host execution, including /open, denial, forged callbacks and replay.
  • These checks do not prove live Telegram/provider behavior, production migration compatibility or two-device isolation. No build, service restart or release was performed.

#Remaining P0 work

  1. Resolve shared conversation boundaries, internal engine fallback and volatile embedded memory; audit remaining mixed-credential routes and conversation boundaries.
  2. Migrate the remaining blocked bulk/coding operations (dev_task, file-organizer) to exact approved plans — documents_organize and disk_doctor reap are migrated in revision 16; audit background automation/plugins and resolved target/configuration drift outside the corrected conversation/Telegram paths. Diagnostic-only auto-resolve remains a restriction, not completed repair functionality.
  3. Review production attachment paths/associations and backups before rollout; complete streaming multipart limits, crash reconciliation and concurrent Telegram quota enforcement. The local lifecycle corrections do not repair historical data.
  4. Complete live two-account/two-PC acceptance and deployment review before enabling the isolated P1 pilot. Automated logical-device tests are not physical device acceptance.

Sources: approved plan, checkpoint, web/src/lib/access.ts, web/src/app/api/chat/stream/route.ts, src/telegram/run.ts, web/tests/chat-routing-isolation.test.ts.

~ End of Document ~