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

13 KiB
Raw Permalink Blame History

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 COPYs 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