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>
238 lines
13 KiB
Markdown
238 lines
13 KiB
Markdown
# 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
|
||
```
|