Files
adolf/DESIGN-a2a-agents.md
alvis 4e67186c08
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Stale / stale (push) Has been cancelled
Stale / backfill-stale-closures (push) Has been cancelled
Stale / lock-closed-issues (push) Has been cancelled
OpenClaw Scheduled Live And E2E Checks / live_and_openwebui_checks (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
QA-Lab - All Lanes / Authorize workflow actor (push) Has been cancelled
QA-Lab - All Lanes / Validate selected ref (push) Has been cancelled
QA-Lab - All Lanes / Run QA Lab mock parity lane (push) Has been cancelled
QA-Lab - All Lanes / Run live runtime token-efficiency lane (push) Has been cancelled
QA-Lab - All Lanes / Run Matrix live QA lane (push) Has been cancelled
QA-Lab - All Lanes / Run Matrix live QA lane (e2ee-cli) (push) Has been cancelled
QA-Lab - All Lanes / Run Matrix live QA lane (e2ee-deep) (push) Has been cancelled
QA-Lab - All Lanes / Run Matrix live QA lane (e2ee-smoke) (push) Has been cancelled
QA-Lab - All Lanes / Run Matrix live QA lane (media) (push) Has been cancelled
QA-Lab - All Lanes / Run Matrix live QA lane (transport) (push) Has been cancelled
QA-Lab - All Lanes / Run Telegram live QA lane with Convex leases (push) Has been cancelled
QA-Lab - All Lanes / Run Discord live QA lane with Convex leases (push) Has been cancelled
QA-Lab - All Lanes / Run WhatsApp live QA lane with Convex leases (push) Has been cancelled
QA-Lab - All Lanes / Run Slack live QA lane with Convex leases (push) Has been cancelled
OpenClaw Performance / Kova live OpenAI GPT 5.5 agent turn (push) Has been cancelled
OpenClaw Performance / Kova mock provider performance (push) Has been cancelled
OpenClaw Performance / Kova mock provider deep profile (push) Has been cancelled
CodeQL Critical Quality / Select Critical Quality shards (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (core-auth-secrets) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (config-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (gateway-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (channel-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (network-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (agent-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (mcp-process-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (memory-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (session-diagnostics-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (plugin-sdk-reply-runtime) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (provider-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (ui-control-plane) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (web-media-runtime-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (plugin-boundary) (push) Has been cancelled
CodeQL Critical Quality / Critical Quality (plugin-sdk-package-contract) (push) Has been cancelled
CodeQL Android Critical Security / Critical Security (android) (push) Has been cancelled
CodeQL macOS Critical Security / Critical Security (macOS) (push) Has been cancelled
docs: A2A-3 — task schema, field-by-field KB-literal mapping
Adds §6a documenting how each Task field (id, intent, required tier,
target queue, priority, status, context refs, result ref, submitter,
deadline) is actually represented in Kanboard today (columns, tags,
score, priority, comments, native fields), per the already-agreed
KB-literal decision (§10.2). Ground truth taken from kb-claim and
kb_worker.py, not written in the abstract.

Kanboard #135
2026-07-22 11:06:34 +00:00

31 KiB
Raw Blame History

DESIGN — Agap Agent Platform v2.1: the agent algebra

Status: v2.1, agreed with alvis 2026-07-21 (v2 7be30c71 + hardening review) Owner: alvis · Written with Claude

One design, two axioms, one verb. Everything alvis asked for — per-model queues, quota parking, Hindsight reflect as an async task, the Claude Code loop as a task puller, semantic/tier/direct routing — falls out as a special case rather than a rule. This document is the reference; the Kanboard A2A tasks implement it.


0. Glossary

  • Task plane / "the fabric" — the task-passing substrate connecting all agents: Kanboard (the durable task store and queue — for humans and agents) + A2A protocol semantics (submit/status/result, Agent Cards, context-by-reference) + the conventions on top (claim/lease, priorities, parking, trust-class routing). Not a deployable component; the collective name, the way "the network" names cables + IP + routing.
  • Card — an agent's self-description: capabilities, tier, cost class, trust class, availability. Maps 1:1 to an A2A Agent Card.
  • Backbone — the concrete LLM an agent currently uses for reasoning.
  • Context ref — a pointer (Hindsight bank id, git ref, KB task id, file path) passed instead of pasted content.
  • fabric-keeper — the janitor daemon owning time semantics (§6b): lease sweeps, deadlines, dead-letter, inbox digests. It never assigns work and never triggers work.

1. Why

Observed on the live stack:

  • Duplicated LLM spend — an Adolf turn costs a ~32.8K-token reply call plus a ~22.8K-token background Hindsight extraction; ~425 tokens are the conversation.
  • Tool bloat — one Adolf carries ~84 MCP tool schemas + ~26 built-in tools every turn (~22K tokens), relevant or not.
  • Quota cliffs — Kimi's flat window (~60 msgs/5h, ~300/wk measured) makes Adolf go dark with no degradation path and no way to park work.
  • Hardcoded background cognition — Hindsight reflect/consolidation call a fixed model directly: no scheduling, no priority, no quota awareness.
  • No growth path — the ambition is autonomous research agents, remote llama nodes, more GPUs, more agents.

2. The algebra

Axiom 1 — everything that can receive work is an Agent

An agent is (identity, Card, Policy, State). The Card advertises capabilities, tier (model strength it offers or needs), cost class, trust class (§5), and an availability function a(t). Special cases:

Agent Persona Memory Card highlights
LLM endpoint (kimi, gemma3:4b, …) trivial (identity) none tier, cost, quota-shaped a(t)
Adolf proactive auditor (SOUL.md) Hindsight bank adolf trusted; scoped core tools
claude-coder (Claude Code loop) implementer session + repo trusted; pulls complex coding tasks
Torgash marketplace analyst own bank sandboxed; marketplace tools only
researcher autonomous researcher own bank sandboxed/untrusted inputs; own KB project
router delegator none resolves constraints → agents
alvis (the human) trust=human; a(t)=waking hours; inbox = KB "waiting-on-me"

The human being an agent is not a metaphor: approval gates, escalations and decisions are ordinary tasks submitted to his inbox. The KB column he already processes is that inbox.

Axiom 2 — one verb

submit(task, target) -> taskRef        # await(taskRef) optional => sync
task = (intent, context-refs, constraints, priority, deadline, provenance)
target ∈ { agent-id            # direct: “this backbone / this specialist”
         | constraint-set      # tier/capability: “any large model with tools”
         | auto }              # router decides by availability/quota/complexity

Context travels by reference, never by value — the single most important efficiency rule for inter-agent communication (A2A context-passing practice). sync vs async is not a second mechanism: sync = submit + await.

Transport rule. Sync and async share the algebra but not the transport: sync goes direct — an A2A message/send RPC straight to the target agent's endpoint, journaled to KB afterwards; async/durable goes through KB and is drained by polling workers. KB polling must never sit on a sync path — a sync call may not inherit poll-interval latency.

Completion vs verification

KB convention (native semantics, no new machinery): the Done column = unverified completion — the worker/agent finished and self-reported. Closing the task = verified completion. The producer never closes its own task; the submitter, a human, or (later) a reviewer-agent closes after checking the task's acceptance criteria. Lifecycle: … → done (unverified) → closed (verified). For code, the PR review is the verification; closing follows merge.

Theorems — the old rules become consequences

  1. "Queues are per model, not per agent." Every agent has an inbox, but queues accumulate only where a(t) or throughput binds — at scarce agents: model-agents and the human. Persona agents transform-and-delegate, so their inboxes stay near-empty. The v1 rule is the scarcity special case.

  2. Quota lifecycles are shapes of a(t). always-on: a(t)=1. quota-gated (Kimi): a(t)=0 when the window is spent — the queue parks, nothing fails, drains on reset. cost-gated: a(t)=0 past budget. on-demand (remote llama): a(t)=0 until woken. Four lifecycles, one function.

    Note on "cost": in this lab the binding constraint is quota and VRAM, not money — see §3a. The cost-gated shape exists for the optional paid fallback only; it is not the normal case.

  3. Hindsight reflect is just a submit{intent: reflect, refs: bank+query, constraints: tier≥large}, async. Same for consolidation (low priority).

  4. Backbone swap is a constraint edit. Persona agents name constraints, not endpoints; the backbone resolves per-submit. Adolf-on-Kimi today, Adolf-on-local tomorrow — no code change.

  5. The Claude Code loop is an ordinary consumer — an agent whose policy is "pull complex coding tasks from the fabric". It was never special.

  6. For free: escalation = re-submit with wider constraints (gated by policy, §5); approval = submit(…, target=alvis); proactivity/cron = delayed self-submission; the researcher = a low-priority self-submitting loop.

Granularity rule

A Task is a durable work item with a lifecycle worth auditing. A single LLM completion inside an agent's turn is not a Task — it is an implementation detail, observable in Langfuse, invisible to Kanboard. This keeps the KB-literal fabric free of micro-churn by construction.

3. Planes

┌─ Task plane (“the fabric”) ─────────────────────────────────────────┐
│ Kanboard = the queue + audit + human inboxes (KB-LITERAL: no        │
│ separate store). A2A semantics; claim/lease; priorities; parking.   │
└─────────────────────────────────────────────────────────────────────┘
┌─ Agent plane ───────────────────────────────────────────────────────┐
│ Registry of Cards (persona, memory bank, tool scope, trust class,   │
│ preferred tier, current backbone). Runtimes: OpenClaw (Adolf +      │
│ specialists), Claude Code CLI, thin workers.                        │
└─────────────────────────────────────────────────────────────────────┘
┌─ Model plane ───────────────────────────────────────────────────────┐
│ LiteLLM gateway (:4000). Auto Router v2 (2026-07-14) does the SYNC  │
│ routing natively: pinned model | tier pools | complexity/semantic   │
│ auto-routing (SIMPLE<MEDIUM<COMPLEX<REASONING), plus virtual keys,  │
│ budgets, 429-fallback. alvis's three routing modes map 1:1:         │
│   specific backbone → pinned model_name                             │
│   “tier” routing    → tier pool                                     │
│   automatic router  → auto_router/complexity_router                 │
│ Classification is EMBEDDING-based on the local bge-m3 (semantic-    │
│ router): no classifier LLM, no API spend (§3a).                     │
│ Deployments: kimi (wrapper) | local small model | local embedder;   │
│ metered/paid = opt-in fallback only, never implicit.                │
│ The fabric owns everything LiteLLM cannot: ASYNC queueing, parking  │
│ across quota windows, leases, cross-agent quota/GPU arbitration.    │
└─────────────────────────────────────────────────────────────────────┘
┌─ Context stores ────────────────────────────────────────────────────┐
│ Hindsight banks (per-agent memory) · gitea (code, docs, this file)  │
│ · KB task bodies · files. Tasks point here; payloads never inline.  │
└─────────────────────────────────────────────────────────────────────┘

3a. Cost model — no metered API by default

Hard scope constraint (alvis): the workflow is Claude Code + the Kimi wrapper + a local GPU embedder + a small weak local model. We do not pay per-token for API usage. Consequences that shape the design:

Resource Nature Constraint
Claude Code flat subscription; a runtime, not a metered API the claude-coder agent's capacity
Kimi (via adolf-llm / hindsight-llm wrappers) flat subscription, windowed ~60 msgs/5h, ~300/wk — quota, not money
local small model (ollama) free GPU/VRAM contention (§3b)
local embedder (bge-m3) free, already resident never-evict
paid API (Haiku/Flash/…) metered optional fallback only — disabled by default, explicitly opt-in

So the "cheap tier" is the local small model, not a cheap paid model. The budget governor (§5) therefore arbitrates quota and GPU, not spend. Any paid deployment in the LiteLLM config must be explicitly enabled per agent via its virtual key; nothing routes to a metered model implicitly.

Routing classification is embedding-based, not model-based. LiteLLM's Auto Router (built on semantic-router) takes a configurable embedding_model + match_threshold, so complexity/semantic classification runs on the local bge-m3 we already keep resident — no classifier LLM call, no API spend, ~zero marginal cost. (Auto Router v2 is new as of 2026-07-14 and has an open embedding-related bug report: verify hands-on and keep a heuristic keyword/length fallback for the classifier.)

3b. GPU residency — a local model's a(t) is not 1

Local "free" models contend for VRAM with interactive components (measured on the 8 GB GTX 1070: bge-m3 + gemma3:4b + tei-reranker ≈ 6.2 GB; loading anything bigger evicts the reranker and silently regresses recall latency). So a local model's availability is a(t) = f(VRAM headroom), and the model registry carries a residency policy: a never-evict set (embedder, reranker — interactive-critical), allowed co-residency groups, and a pre-load check every worker must pass before pulling a model onto a GPU. With more GPUs this becomes a placement problem — same policy, more slots.

Personas and Cards are code

SOUL.md files and agent Cards live in git and are deployed to runtimes — never edited live in volumes. "Who changed Adolf's soul" must be a git log answer. (The current SOUL.md in the adolf-state volume is migration debt.)

Eval gate on backbone/routing changes

Backbone swap being "one constraint edit" is quality-blind. Each agent keeps a golden set (1020 canonical exchanges); any backbone or routing change is shadow-replayed against it and compared (Langfuse datasets/evals) before taking effect. The algebra's flexibility must not become a silent-degradation machine.

4. A2A: the protocol, adopted now

The algebra maps 1:1 onto A2A v1.0 (Jan 2026), which is why we implement the real protocol immediately rather than "patterns first":

Algebra A2A
Card Agent Card (/.well-known/agent.json)
submit / await message/send (sync-ish) / tasks/get (async)
task lifecycle submitted → working → input-required → completed/failed/canceled
notify push notifications

Implementation: JSON-RPC 2.0 over HTTP on the LAN; each runtime (OpenClaw, Claude loop, workers) exposes/consumes A2A; Kanboard remains the durable state behind the endpoints. Scalability/extensibility later (remote nodes, third-party agents) then needs zero redesign.

Auth is mandatory on every A2A surface. The LAN is not trusted — the xray/3x-ui VPN terminates other people's peers on it. No unauthenticated JSON-RPC listener, ever: shared tokens minimum, mTLS preferred.

5. Trust & sandboxing

Trust classes (on every Card):

human > trusted > sandboxed > untrusted
  • trusted (Adolf, claude-coder): vault access yes; outward actions per existing ask-first rules.
  • sandboxed (Torgash, researcher): no vault, no outward sends; scoped MCP allowlist; KB access project-scoped (researcher gets its own KB project(s)).
  • untrusted = anything ingesting the open web: its outputs are tainted.

Taint / prompt-injection boundary: tainted output may be written only to the agent's own bank/notes/project. Promotion into a trusted agent's memory or into any action requires a gate (initially: a task to alvis's inbox; later possibly a reviewer-agent).

Escalation policy (initial): always-ask. A task that fails on its tier is not silently retried on a bigger model; it becomes a decision task in alvis's inbox. Revisit once behavior is observed (debugging phase by design).

Sandboxed coding — workspace lease: per task, not per agent: workspaces/<agent>/<task-id>/ = ephemeral gitea clone + branch; execution inside a container (no vault creds by default, network allowlist, resource caps); merge only via PR to gitea; autonomous agents never push to main. Reviewer = human, or later a reviewer-agent (just another persona).

Global budget governor: near the end of a quota window, interactive agents (Adolf) outrank background ones (researcher, consolidation) — arbitration lives in the fabric (priorities + a small governor rule), not in LiteLLM.

Fabric hygiene (runaway protection): agents submit tasks that cause agents to submit tasks — idempotency keys stop duplicates, not generative loops. So: per-agent task-creation quotas; an ancestry depth cap on provenance chains; cycle detection at submit; and a dead-letter state for poison tasks after max-retries — never an infinite retry loop through paid quota.

5b. Humans (plural) and memory partitioning

There is more than one human already (alvis and elizaveta are both on Adolf's Matrix allowlist) and there will be more. Every human is an agent with trust=human, their own inbox, and — critically — their own privacy domain.

Memory partitioning (hard rules):

  • Per-human private banks: adolf-alvis, adolf-elizaveta, … Everything learned in conversation with human H goes to H's private bank by default. Content from one human's conversations must never surface to another human. This is a correctness property, not a preference.
  • One shared household bank for facts that are explicitly household-wide (addresses, devices, routines, shared plans). Trusted agents may write; promotion from a private bank happens only by that human's explicit action or approval task — never automatically.
  • Recall is interlocutor-scoped: when Adolf talks to H it recalls from H's private bank + the shared bank, nothing else. The recall/retain hooks select the bank by interlocutor identity.
  • Sandboxed agents (Torgash, researcher) read at most the shared bank; never any private bank. This is the cross-human face of the memory matrix.
  • The current single adolf bank is migration debt: split into adolf-alvis + shared.

Human inbox design: notifications are priority-routed — gate/urgent tasks ping the human via Matrix (Adolf initiates them; cf. proactive-messaging work), everything else lands in a daily digest from the fabric-keeper. Ignored gate tasks park and re-remind; they never default-approve. Vacation mode: a human's a(t)=0 parks their inbox like any other scarce queue — gated flows wait; predefined degraded defaults apply where explicitly configured.

6. Executor — thin KB-polling workers

No Temporal/Hatchet: at homelab scale (dozens of tasks/day) a durable-execution platform would duplicate Kanboard as a second source of truth. Instead, one small worker daemon per model-queue (compose services, ~200 lines, shared lib):

loop:
  a(t) check        # quota/budget/health probe; if 0 → park (sleep, re-probe)
  poll KB view      # filtered: my queue, status=queued, by priority
  claim             # atomic: assign-to-self + column move + lease timestamp
  resolve refs      # fetch context by reference
  execute           # via LiteLLM (model-agents) / agent runtime (persona)
  write result ref  # to the shared store; never inline
  update status     # done | failed(retry policy) | input-required(→ inbox)

Leases + heartbeats make dead workers safe: an expired lease returns the task to queued. Two workers on one queue never double-run a task (claim is atomic). Idempotency keys on submission prevent duplicate proactive tasks. OpenClaw cron is the proactive submitter (Adolf's schedule); workers are the drainers.

6a. Task schema — the KB-literal mapping

Per the KB-literal decision (§10.2: "Kanboard is the queue, humans included; no separate store"): there is no separate task schema or task table anywhere — a Task (§2, Axiom 2: (intent, context-refs, constraints, priority, deadline, provenance)) is entirely represented by native Kanboard task fields plus a small set of conventions layered on top (columns, tags, comments). The table below is that mapping, field by field. It is not aspirational: every row is what kb-claim and kb_worker.py (/home/alvis/kanboard/bin/) already read or write today — those two files are the ground truth this table documents, not a separate spec to keep in sync by hand.

Task field Represented as Notes
id native Kanboard task id Globally unique per Kanboard instance. A taskRef in the algebra (§2) is (project_id, task_id) — project scopes columns/tags, so the pair (not the bare id) is what a worker needs to act on a task.
intent title (short) + description (full spec, Markdown) description is the literal task body — what to do, acceptance criteria, links. kb-claim next/take print it verbatim under --- spec ---; nothing paraphrases it.
required tier derived, not stored — from native score score is Kanboard's complexity field (Fibonacci: 1,2,3,5,8,13,21), normally set by the human, settable via create_task/update_task. kb-claim's tier_for(score) maps it: 0 (unrated) → sonnet (safe default, task should be flagged for rating, not silently run cheap) · ≤2haiku · ≤5sonnet · >5opus. The complexity gate (kb/CLAUDE.md): an unrated task is tagged blocked with a comment asking for a score, instead of being dispatched on the default tier.
target queue Kanboard project Each queue is a Kanboard project — e.g. the Adolf project is what the always-on-local and Kimi-quota worker configs point "project" at (workers/*.example.json). Trust-scoped agents get their own project (§5: researcher gets its own KB project). swimlane_id is read and carried through every claim/park/done move but is not yet used to subdivide a queue further — headroom for later without a new field.
priority native priority field, 03 Take-order (kb/CLAUDE.md): p1 > p2 > p0 > p3 (note p0 sits between p2 and p3, not below p3). kb-claim's eligible() sorts candidates by (-priority, board position); the interactive /kb loop layers a WIP-resume dimension on top (resuming WIP outranks fresh p2) — that extra ordering lives in the orchestrator convention, not in kb-claim itself.
status derived from column + tags + open/closed — no single status field Column (BacklogReadyWork in progressDone, looked up by title via columns()) is the coarse lifecycle stage. Open vs closed (status_id/closeTask) is unverified vs verified completion (§2 "Completion vs verification", decision log #15): Done+open = unverified (kb-claim done, never closes its own task); Done+closed = verified (kb-claim close, by someone other than the producer). Tags carry the remaining states: blocked = input-required / parked for a human (escalation, missing score, or a fabric-keeper deadline escalation — same tag, one park primitive); dead-letter = poison, retries exhausted (kb-claim deadletter); needs-human-verify = machine-verified but sensitive, left open for a human to close (kb-claim escalate). Maps onto the A2A lifecycle (§4): submitted≈Backlog/Ready, working≈WIP, input-required≈WIP+blocked, completed≈Done+closed, failed≈dead-letter. (canceled has no Kanboard convention yet — not in scope here.) Claim freshness (the lease) is a comment, not a field: 🔒 claimed by \agent` at /lease renewed by …, parsed back out by lease-statusvia regex — this is what lets an expired lease be swept back toReady` without a dedicated lease column.
context refs plain text inside description (and follow-up comments) — convention, not a typed field Per Axiom 2, "context travels by reference, never by value": a bank id, git ref/SHA, another KB task id (#N, which Kanboard auto-links), or a filesystem path, written as text. Enforced by review/convention, consistent with KB-literal — there is no schema-validation layer sitting in front of Kanboard to enforce it mechanically.
result ref the note on the done/park/deadletter comment kb_worker.py's Outcome.note (in-process) is flushed by report_outcome() to a Kanboard comment (✅ {note} for done) via kb-claim done --note …. No separate result store: the comment thread is the audit trail (kb#159's "closing is verification" model reads it).
submitter native Kanboard creator_id Set automatically by createTask; exists on every task already. Distinct from owner_id, which is the current claimant and is what claim/park/done mutate. Not yet read by kb-claim/kb_worker.py/fabric-keeper.py — available, unused, out of scope for this task.
deadline native Kanboard date_due field Enforced by fabric-keeper.py's enforce_deadlines() (§6b "time semantics"): a fabric-owned, open, unparked task with date_due in the past gets kb-claim parked (tagged blocked, comment naming the responsible inbox = current owner, or alvis if unassigned). Idempotent — already-blocked/dead-letter tasks are skipped so re-sweeps don't spam.

6b. Who is "the scheduler"? — decomposed, plus one janitor

There is deliberately no central dispatcher. Scheduling decomposes into four concerns, each with its own owner:

Concern Question Owner
Triggering when do tasks appear? OpenClaw cron, agents' delayed self-submissions, humans
Dispatch which task runs next? each queue's worker (claim by priority under its a(t))
Admission may it run now? LiteLLM budgets/rate + the budget governor
Time semantics expired leases, deadlines, stuck tasks? the fabric-keeper
Failure/anomaly response something went wrong — now what? Zabbix → the ops-agent (§6d)

The fabric-keeper is one tiny always-on daemon that neither assigns nor triggers work. It sweeps expired leases back to queued, enforces task deadlines (escalating to the responsible inbox), moves poison tasks to dead-letter, emits the human daily digest, and exports queue depths/ages to Zabbix.

Its defining property is being off the critical path: kill it and work still flows (workers keep pulling and running) — only hygiene degrades. That is what separates a janitor from a scheduler, which in a push system would stop everything.

Deliberately NOT a keeper duty: missed crons. A cron window missed because the host was down is not a task-lifecycle event — it's an operational fault, and re-firing it would make the keeper a trigger. Instead: schedules that must not be missed are monitored in Zabbix (a missed run raises a warning like any other infra fault), and the response is handled per-incident, not by a blanket catch-up policy (§6d).

6d. Faults are incidents, not keeper chores

Anything that "went wrong" — a missed critical cron, a service down, a queue backing up, a stale backup — surfaces as a Zabbix problem. Zabbix is the single place operational faults are detected.

Response is per-incident, not global: each Zabbix problem class gets its own mitigation path, expressed as a task. Later this is automated by a dedicated ops-agent — a worker that watches Zabbix problems and, per problem class, either applies a known mitigation or files a task (to the right agent, or to a human inbox) with the incident as context. One blanket "catch-up policy" would be exactly the wrong abstraction: a missed briefing, a missed backup and a missed consolidation want completely different responses.

6c. Kanboard is tier-0 now

Promoting KB to the fabric's backbone promotes its ops class: backups on par with the vault, Zabbix monitoring of the service and API, and a defined degraded mode — if KB is down, Adolf still answers Matrix chat (no fabric operations, no task memory), workers park, nothing crashes or data-loses.

7. Observability — Langfuse (kept), wired for real

Decision: keep Langfuse (already deployed; best-in-class self-hosted: traces + per-token cost + prompt management + evals, MIT). Grafana rejected for this role — generic metrics with no LLM semantics (the source of past dissatisfaction); Zabbix keeps infra monitoring. To do (it currently receives nothing): LiteLLM success/failure callbacks → Langfuse; tag every trace with agent, task-id, queue; per-agent cost dashboards; upgrade v2→v3. Every completion is traced here — this is where sub-Task granularity lives.

8. Growing the lab

  • More GPUs / remote llama → new model-agent Cards with on-demand a(t) (health probe, wake hook, graceful absence). Routing skips absent nodes.
  • More specialists → new Cards + scoped tools + own banks. The fabric and A2A don't change.
  • Autonomous research agents → low-priority loops on always-on local queues, escalating (via always-ask, initially) for large-model synthesis; own KB project; tainted outputs until promoted.

9. Migration order

  1. Registries: model Cards + agent Cards (schema + populate).
  2. First thin worker end-to-end on an always-on local queue.
  3. Quota-gated worker (Kimi park/resume). Claim/lease semantics.
  4. A2A protocol surface (JSON-RPC + Agent Cards) over the fabric.
  5. Cutovers: Hindsight reflect → fabric; consolidation → fabric (low prio); claude-coder + Adolf declared as registry agents (Adolf's tools shrink to scoped core).
  6. Trust enforcement: capability grants (virtual keys + MCP allowlists), taint gate, budget governor, langfuse wiring.
  7. Scale: Torgash, researcher (own KB project), on-demand nodes.

10. Decision log (2026-07-21, alvis)

  1. Single completions are not Tasks (langfuse-only) → no micro-churn.
  2. KB-literal: Kanboard is the queue, humans included; no separate store.
  3. Trust classes as §5; vault = trusted only.
  4. Escalation = always-ask initially.
  5. Researcher: KB access allowed, own project(s), scope-limited.
  6. Real A2A protocol now (JSON-RPC + Agent Cards).
  7. Proactive schedules: OpenClaw cron → fabric.
  8. Langfuse kept as the observability layer; Grafana rejected; Zabbix = infra.
  9. Executor = thin KB-polling workers; no Hatchet/Temporal at this scale.
  10. Sync routing = LiteLLM Auto Router v2; fabric owns async/parking.
  11. Sandbox = per-task workspace lease + container + PR-only merges.

Added in v2.1 (hardening review, same day):

  1. Multi-human: per-human private banks + shared household bank; interlocutor-scoped recall; cross-human leakage forbidden (hard rule); promotion to shared only by the owning human's action/approval.
  2. A2A auth mandatory everywhere — the LAN is untrusted (VPN peers).
  3. KB = tier-0 infrastructure (backup, monitoring, degraded mode).
  4. Done column = unverified completion; closed task = verified; the producer never closes its own task.
  5. Scheduler = decomposed (cron/self-submission triggers; workers dispatch; LiteLLM+governor admit); the fabric-keeper janitor owns time semantics (leases, deadlines, dead-letter, digests) and neither assigns nor triggers.
  6. Personas/Cards live in git, deployed — never edited live.
  7. Backbone/routing changes gated by golden-set shadow eval (Langfuse).
  8. Fabric hygiene: creation quotas, ancestry depth cap, cycle detection, dead-letter for poison tasks.
  9. GPU residency policy: local a(t)=f(VRAM); never-evict set (embedder, reranker); pre-load checks.
  10. Transport: sync = direct A2A RPC (journaled); async = KB polling; polling never on a sync path.
  11. Missed crons are not a keeper duty: schedules that must not be missed are monitored in Zabbix; faults are incidents handled per problem class by a future ops-agent (§6d), never by a blanket catch-up policy.
  12. No metered API by default (§3a): scope = Claude Code + Kimi wrapper + local GPU embedder + small local model. The cheap tier is the local model, not a paid one; paid deployments are opt-in fallback per virtual key. The governor arbitrates quota and GPU, not money.
  13. Routing classification is embedding-based on the local bge-m3 via LiteLLM Auto Router (semantic-router): no classifier LLM call, no API spend. Keep a heuristic fallback — Auto Router v2 is new (2026-07-14) with an open embedding bug report.
  14. Hindsight gains an optional caller-supplied cognition mode (#131) so it can run inside a Claude Code session with zero extra API calls; the API/queue path stays the default. Same verb, different target.