Files
AgapHost/adolf/DESIGN-todoist-capture.md
alvis a5c625b9b6 adolf: bearer-authenticate the agap MCP server, fix audio config schema
openclaw.json now sends `Authorization: Bearer ${AGAP_MCP_TOKEN}` to the agap
MCP server, which requires it as of kb#180. The token is injected from
openai/.env via docker-compose.yml and only substituted here, never inlined.
It maps to agent id `adolf`, which is also what the kb#147 vault gate reads.

Fixes tools.media.audio, which had been added but never restart-validated:
the per-entry `apiKey: "not-needed"` is rejected by the schema
("tools.media.audio.models.0: Invalid input"), and an invalid config makes the
gateway refuse to start outright -- adolf crash-looped on the first restart
after the block landed. The old comment claimed the schema requires a
non-empty apiKey; it is the opposite, apiKey is not a valid per-entry key at
all. Isolated with `openclaw config validate` against the running image
(2026.6.11): {provider, model} and {provider, model, baseUrl} validate, and
adding apiKey alone reproduces the failure. baseUrl is kept -- that is the
per-entry override pointing the openai-shaped provider at the local
faster-whisper server. Provider auth follows the normal model auth order per
docs/nodes/audio.md, and faster-whisper-server has no auth to satisfy anyway.

Two lessons encoded in the comments: `enabled: false` does NOT exempt an entry
from schema validation, and a config edit is not done until a restart boots
healthy -- this sat invalid but latent because the running gateway still held
an older loaded config. The block stays enabled: false; turning STT on is
still a kb#175/#191 decision (GTX 1070 co-residency).

Also adds the proactive-prioritization and todoist-capture design notes and
the vw-mcp prototype.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 04:41:13 +00:00

238 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DESIGN — Todoist capture + AI classification (kb#170)
Status: **v1, components 1/2 built + proven; components 3/4 designed, not
built** — written 2026-07-23
Owner: alvis · Written with Claude
Scope: kanboard Adolf task **#170**. Related, not duplicated: Welfare
#102 (proactive-secretary umbrella), #105 (idea capture, pre-Todoist
version of the same need), #106 (people/events reminders).
---
## 1. Why Todoist, and why encoder-only classification
Todoist becomes Adolf's **inbox for ideas and quick tasks** — the place a
thought gets captured immediately, before it's clear whether it's a
one-liner or a project. Two things must stay true per
`DESIGN-a2a-agents.md` v2.1:
- **No metered API by default** (§3a): classification must not spend a
Kimi/gemma turn per capture. The stack already keeps **bge-m3** resident
(Hindsight's embedder, never-evict, `model-registry.yaml`) — reusing it
for classification is ~0 marginal cost, same reasoning §3a already
applies to LiteLLM's Auto Router.
- **Native commands run before the agent** (proven pattern:
`quota-command-openclaw-plugin`, kb#62): a `/idea` command that never
invokes Kimi at all keeps the entire capture path — not just the
classification step — off the metered/quota-gated path.
So: **nearest-centroid classification over bge-m3 embeddings**, not a
classifier LLM call and not hard tag rules. This is genuinely an
encoder-only model in the literal sense (bge-m3 is an encoder, not a
generative LLM) — no fine-tuning, no training loop, because **no labelled
dataset exists** (inventing one would be guessing scope that wasn't
asked for). The "training data" is a small, git-editable exemplar list per
class (`agap-mcp/src/classifier.js`) — extending accuracy later means
adding exemplars, not retraining.
## 2. Architecture
```
Matrix "/idea <текст>" Adolf prose ("запомни идею...")
| |
v v
todoist-capture-plugin Adolf (Kimi) -> MCP tool call
(native command, 0 Kimi calls) todoist_capture_idea (agap-mcp)
| |
+--------------------+-------------------+
v
POST /capture-idea (agap-mcp, plain REST)
|
v
classifier.js: embed(text) via bge-m3
(1 embedding call, reused for all 3 axes)
|
+----------------+----------------+
v v v
area centroid urgency centroid decompose centroid
(5 classes) (3 classes) (2 classes)
| | |
+----------------+------------------+
v
capture.js: label + priority mapping
|
v
todoistCreateTask() -> real Todoist task
labels: area-*, urgency-*, [decompose], [area-uncertain]
```
Two entry points converge on one pipeline (`capture.js`'s
`todoistCaptureIdea`), reachable either as an MCP tool
(`todoist_capture_idea`, for Adolf's/Claude's model-driven path — "запомни
идею: ...") or as a plain REST route (`POST /capture-idea`, for the native
`/idea` command, which cannot speak MCP JSON-RPC). Both call the exact
same function — no duplicated classification/creation logic.
## 3. Component 2 — AI classification (built, proven)
Three independent axes per idea, one bge-m3 embedding shared across all
three:
| Axis | Classes | Source of exemplars |
|---|---|---|
| **area** | `adolf`, `welfare`, `дом`, `семья`, `здоровье` (kb#170 spec, verbatim) | `AREA_EXEMPLARS` |
| **urgency** | `high`, `medium`, `low` | `URGENCY_EXEMPLARS` |
| **decompose** | `simple-task`, `needs-decomposition` | `DECOMPOSE_EXEMPLARS` |
Classification = cosine similarity of the idea's embedding against each
class's centroid (mean of that class's exemplar embeddings), argmax per
axis. Each result also carries a **margin** (gap between the top two
scores) and an `ambiguous: true` flag when the margin is small
(< 0.03, an empirical starting threshold — same "tune later" posture as
`DESIGN-proactive-prioritization.md`'s constants). Ambiguous area
classifications get an extra `area-uncertain` label instead of being
silently forced — component 4 (periodic review) is where a human
resolves them, not an auto-retry on a bigger model (consistent with
`DESIGN-a2a-agents.md` §5's always-ask escalation policy, scaled down:
this isn't a costly/irreversible action, so the "escalation" here is just
a label, not a blocking gate to alvis's inbox).
**Proven** (`agap-mcp/src/classifier.test.mjs`, run against the real,
live bge-m3 at `:11436` — 7/7 pass): area/urgency/decompose all resolve
sensibly on hand-written Russian idea text spanning all 5 areas, both
urgency bands, and both decompose classes. `capture.test.mjs` (6/6 pass)
proves the label/priority/project mapping with a **stubbed** Todoist
client — no test data was written to the real Todoist account while
proving this out.
### 3.1 Project vs. label mapping — a decision made, not guessed
Todoist's real, live projects today (`todoist_list_projects`, confirmed
2026-07-23): `Inbox`, `One-Off`, `Family`, `Planning`, `Pending`. These do
**not** line up with the 5 kb#170 areas except `семья``Family`.
Creating four new Todoist projects (`Adolf`, `Welfare`, `дом`,
`здоровье`) to match would be a structural change to the user's real
Todoist account — **not done here without sign-off** (see §6, open
question 1). Instead, v1 uses **labels** (`area-*`, `urgency-*`,
`decompose`, `area-uncertain`) for every axis — purely additive and
reversible (Todoist auto-creates labels on first use; deleting a label
loses no task data) — and auto-routes to an existing project only for the
one unambiguous match (`семья``Family`), never inventing a project
selection the classifier merely guessed at.
## 4. Component 1 — capture command (built, not activated)
`openai/todoist-capture-plugin/` — same shape as `quota-command-openclaw-
plugin` (kb#62): `definePluginEntry` + `api.registerCommand({ name:
"idea", acceptsArgs: true, requireAuth: true, handler })`. Verified
against the real `PluginCommandHandler`/`PluginCommandContext`/
`OpenClawPluginCommandDefinition` types in the OpenClaw source
(`/home/alvis/adolf/src/plugins/types.ts`) — `ctx.args` is the raw string
after `/idea`, handler returns `{ text, suppressReply }`.
`requireAuth: true` (default) keeps it behind the same Matrix DM allowlist
(`channels.matrix.dm.allowFrom`) gating every other Adolf interaction — no
new privilege tier, since creating a Todoist task in the operator's own
inbox isn't a privileged/destructive action.
**Wired but not live**: `docker-compose.yml` gets the read-only bind mount
(same pattern as `quota-command`/`hindsight-memory`/`kimi-quota-footer`),
`openclaw.json` gets `plugins.entries.todoist-capture.enabled: true`, and
`agap-mcp/src/server.js` gets the `POST /capture-idea` route the plugin
calls. All three are plain config/code edits, proven end-to-end with a
stub Todoist client (see kb#170 report) — but **none of this takes effect
until the adolf container is restarted** (same activation gate every prior
plugin in this repo has hit: bind-mounts and `plugins.entries` are read at
process start). That restart is the kb#170 handoff — see report.
## 5. Component 3 — sync with services (designed, not built)
kb#170's spec: "идеи из Todoist синхронизируются с Kanboard, календарём,
проектами." This is underspecified enough that building a concrete
bidirectional sync now would be guessing scope (direction? conflict
resolution? which Todoist state maps to which Kanboard column?) rather
than following it. **v1 proposal, one-way, human-gated — not built yet:**
- Todoist is the **source of truth for the idea itself** (text, labels,
done/not-done). Kanboard is the source of truth for **anything that
became real, tracked work**.
- Sync fires only from component 4's periodic review (§6), not on a
schedule or webhook: when Adolf proposes "this idea is ready to become
work" and the human agrees, Adolf creates **one** Kanboard task whose
description contains a `context ref` back to the Todoist task id (per
`DESIGN-a2a-agents.md` §2's "context travels by reference" rule — no
content duplication) and the Todoist task gets a `kanboard-<id>` label
and stays open until the Kanboard task closes.
- **No calendar sync is proposed in v1.** A Todoist due-date does not
imply a calendar event (most captured ideas won't have a real due
time), and the reverse (creating calendar entries from arbitrary idea
due-dates) risks cluttering Radicale with noise. Calendar involvement
belongs with Welfare #106's people/events reminders design, not
invented here.
- **No two-way Kanboard→Todoist sync.** Completing the Kanboard task does
not need to close the Todoist item automatically for v1 — a human
glancing at Todoist can see the `kanboard-<id>` label and close it
manually; automating that closure is a small, safe follow-up once the
one-way direction above is live and observed, not blocking v1.
This keeps sync a **consequence of the human-gated review** (§6), never
an autonomous background writer to three services at once — consistent
with `DESIGN-a2a-agents.md` §5's always-ask posture for anything crossing
a service boundary. Building this is out of scope for this pass; flagged
in the kb#170 report as a natural follow-up task once components 1/2 are
live and real capture data exists to review.
## 6. Component 4 — periodic review (designed, not built)
"Adolf предлагает, какие идеи созрели для превращения в задачи" — this is
explicitly Adolf proposing, not auto-converting; matches
`DESIGN-a2a-agents.md` §5's always-ask escalation policy exactly (a
decision task to alvis's inbox, not a silent action). Proposed shape,
**not implemented**:
- A low-priority proactive cadence job (same shape as Welfare #124's
cadence design, once that lands) that runs `todoist_list_tasks` scoped
to labels `decompose` or `area-uncertain`, drafts a short proposal per
candidate ("эта идея выглядит готовой к декомпозиции — завести задачу
в Kanboard?"), and sends it via Matrix — same impact/cost gate as
`DESIGN-proactive-prioritization.md` (kb#123) should apply here too,
once that gate exists, rather than inventing a second one.
- On accept: component 3's one-way sync (§5) fires for that one idea.
- On dismiss: the idea's `decompose`/`area-uncertain` label is cleared so
the same candidate doesn't re-surface every cadence run.
Not built now because it depends on #124 (cadence) and, for a well-
calibrated gate, #123 (impact/cost gate) — both cited as dependencies
rather than duplicated, same posture `DESIGN-proactive-prioritization.md`
itself takes toward its own siblings.
## 7. What's built vs. handed off
| Piece | State |
|---|---|
| `agap-mcp/src/classifier.js` + test | Built, proven live against bge-m3 |
| `agap-mcp/src/capture.js` + test | Built, proven with a stubbed Todoist client (no real writes) |
| `agap-mcp/src/server.js`: `todoist_capture_idea` MCP tool + `POST /capture-idea` | Built; **not live** — needs an agap-mcp rebuild/restart (already true of the whole Todoist tool surface per kb#170 orchestrator note) |
| `adolf/openclaw.json`, `openai/shared-mcp.json`, `openai/agent-registry.yaml` | Edited, `validate_capability_grants.py` passes clean at both layers |
| `openai/todoist-capture-plugin/` (`/idea` command) | Built, HTTP contract proven with a local harness; **not live** — needs the adolf container restarted (bind mount + `plugins.entries` already wired) |
| Component 3 (sync) | Designed (§5), not built — genuine scope decisions flagged, not guessed |
| Component 4 (periodic review) | Designed (§6), not built — depends on Welfare #123/#124 |
## 8. Open questions for alvis (not guessed)
1. **Todoist project structure**: keep the label-only v1 (§3.1), or
create dedicated Todoist projects per area? The latter is a real,
visible change to the account structure and needs explicit sign-off.
2. **Activation**: rebuilding agap-mcp (its own repo/compose,
`agap_git/agap-mcp/docker-compose.yml`, `build: .` — the `Dockerfile`
`COPY`s `src/` into the image, no bind mount, so a code change needs a
rebuild, not just a restart) and restarting the adolf container (its
compose is `agap_git/openai/docker-compose.yml`, to re-read
`openclaw.json` and pick up the new plugin bind mount) are the two
outward-facing steps this task deliberately did not take. Exact
commands, once approved:
```
cd /home/alvis/agap_git/agap-mcp && docker compose build && docker compose up -d
cd /home/alvis/agap_git/openai && docker compose up -d adolf
```