diff --git a/FORK_PATCHES.md b/FORK_PATCHES.md
index 92e8f6e34706a..b3aca2874f54c 100644
--- a/FORK_PATCHES.md
+++ b/FORK_PATCHES.md
@@ -19,7 +19,7 @@ When a conflict occurs during upstream merge, it passes through these resolvers
## 2026-04-17 — Claude Code bridge (Enchanted Mountain)
-New fork-only plugin `extensions/tinkerclaw-cc-bridge/` registers a provider
+New fork-only plugin `extensions/tinkerclaw-tinker-bridge/` registers a provider
`claude-code` that drives the real `claude` CLI as a persistent subprocess
per session. Inherits the Claude Code OAuth at `~/.claude/.credentials.json`
— flat-rate entitlement, no API key.
@@ -41,8 +41,8 @@ Plugin contents (all fork-only, so no merge collisions expected):
Config additions in `~/.openclaw/openclaw.json`:
-- `plugins.allow` += `"tinkerclaw-cc-bridge"`
-- `plugins.entries.tinkerclaw-cc-bridge.enabled = true`
+- `plugins.allow` += `"tinkerclaw-tinker-bridge"`
+- `plugins.entries.tinkerclaw-tinker-bridge.enabled = true`
- `auth.profiles.claude-code:oauth`
- `auth.order.claude-code = ["claude-code:oauth"]`
- `models.providers.claude-code` (baseUrl `local://claude-cli`, 3 models)
@@ -62,7 +62,7 @@ Risks / known-gaps:
been banned in the wild but is policy-forbidden. Accepted risk.
No core files modified. No new fork patch functions needed in the wiring
-script. Plugin lives entirely under `extensions/tinkerclaw-cc-bridge/`.
+script. Plugin lives entirely under `extensions/tinkerclaw-tinker-bridge/`.
## 2026-04-15 — Silent-failure trio
diff --git a/FORK_SETUP.md b/FORK_SETUP.md
index f5b5c6f9dadab..2577054969584 100644
--- a/FORK_SETUP.md
+++ b/FORK_SETUP.md
@@ -6,14 +6,14 @@
The repo (`~/src/tinkerclaw/`) is upstream's. `git pull` will rewrite it. Your persona, briefing, recipes, and any prompt overrides go in `~/.openclaw/workspace/` — outside the repo, where `git pull` cannot reach.
-The bundled defaults at `extensions/tinkerclaw-cc-bridge/{personas,prompts}/` are what you get on day 0: JARVIS persona, working briefing on `/new`, full grandma-proof tool narration. They work without any setup. To make Jarvis your own, drop a file at `~/.openclaw/workspace/SOUL.md` (overrides the persona) or `~/.openclaw/workspace/BRIEFING.md` (overrides the briefing template). The gateway always prefers your workspace file over the bundled default; `git pull` keeps refreshing the bundle without ever touching your override.
+The bundled defaults at `extensions/tinkerclaw-tinker-bridge/{personas,prompts}/` are what you get on day 0: JARVIS persona, working briefing on `/new`, full grandma-proof tool narration. They work without any setup. To make Jarvis your own, drop a file at `~/.openclaw/workspace/SOUL.md` (overrides the persona) or `~/.openclaw/workspace/BRIEFING.md` (overrides the briefing template). The gateway always prefers your workspace file over the bundled default; `git pull` keeps refreshing the bundle without ever touching your override.
Resolution order for every overridable prompt:
```
1. Explicit config in ~/.openclaw/openclaw.json (outside repo)
2. ~/.openclaw/workspace/ (outside repo)
-3. extensions/tinkerclaw-cc-bridge/.../ (in repo, bundled default)
+3. extensions/tinkerclaw-tinker-bridge/.../ (in repo, bundled default)
```
See `TINKER_UI_DESIGN_BIBLE.md` §5.76 for the full contract, the "Sam test" (fresh-clone day-0 experience) and the "Day-90 test" (existing user `git pull` safety).
diff --git a/README.md b/README.md
index 9b85fd46e8c50..f2330b2f0e25c 100644
--- a/README.md
+++ b/README.md
@@ -12,8 +12,8 @@
-
-
+
+
@@ -29,9 +29,9 @@
-No other agent has learned to think about its own thinking, developed computational humor, or trained itself to be safer than NeMo Guardrails at a fraction of the cost. Yours can. Fifteen research papers and four months of 24/7 operation made the difference.
+No other agent has learned to think about its own thinking, developed computational humor, or trained itself to be safer than NeMo Guardrails at a fraction of the cost. Yours can. Eighteen research papers and four months of 24/7 operation made the difference.
-**TinkerClaw's agent gets smarter every single day.** Fifteen research papers. Each one a real problem we hit, solved, and turned into a system that prevents recurrence:
+**TinkerClaw's agent gets smarter every single day.** Eighteen research papers. Each one a real problem we hit, solved, and turned into a system that prevents recurrence:
- 🌿 **It thinks about its own thinking.** When something breaks, it doesn't just fix the bug — it asks _why the bug exists_, then fixes the system that produced it. Like a mechanic who doesn't just patch the flat tire but asks "why do I keep getting flats on this road?" ([Fractal Reasoning](https://thetinkerzone.com/fractal-reasoning-multi-resolution-memory-and-self-similar-metacognition-for-llm-agents/))
@@ -209,7 +209,7 @@ These are just the ones with personality. **15+ total crons**, each with its own
### 📊 The Research
-All fifteen papers are published at **[thetinkerzone.com](https://thetinkerzone.com/)** and linked in the [intro above](#the-singularity-point-). Each one started as a real problem, became a research paper, became a production system. Read them — they're the best proof that this isn't marketing.
+All eighteen papers are published at **[thetinkerzone.com](https://thetinkerzone.com/)** and linked in the [intro above](#the-singularity-point-). Each one started as a real problem, became a research paper, became a production system. Read them — they're the best proof that this isn't marketing.
---
@@ -274,7 +274,6 @@ Claude (primary) → Gemini (rate limit) → Local Model (offline fallback)
| Skill | What it does |
| -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
-| [`tinker-command-center`](https://clawhub.ai/globalcaos/tinker-command-center) | The dashboard above. Every token, every dollar, every context byte — real time. |
| [`token-panel-ultimate`](https://clawhub.ai/globalcaos/token-panel-ultimate) | Multi-provider token tracking, budget alerts, REST API. |
| [`token-efficiency-guide`](https://clawhub.ai/globalcaos/token-efficiency-guide) | Go from weekly limit on Tuesday to weekly limit on Sunday. 10 steps, one afternoon. |
@@ -282,22 +281,18 @@ Claude (primary) → Gemini (rate limit) → Local Model (offline fallback)
No API keys. No admin consent. Your authenticated browser session IS the API.
-| Skill | What it does |
-| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
-| [`outlook-hack`](https://clawhub.ai/globalcaos/outlook-hack) | Reads Outlook all day, drafts replies — won't send without approval. Code-enforced. |
-| [`teams-hack`](https://clawhub.ai/globalcaos/teams-hack) | Reads Teams chats, posts to channels, searches everything. One browser handshake. |
-| [`factorial-hack`](https://clawhub.ai/globalcaos/factorial-hack) | Reads your HR portal — attendance, leave, payslips. No admin consent required. |
+| Skill | What it does |
+| ------------------------------------------------------------ | ----------------------------------------------------------------------------------- |
+| [`outlook-hack`](https://clawhub.ai/globalcaos/outlook-hack) | Reads Outlook all day, drafts replies — won't send without approval. Code-enforced. |
+| [`teams-hack`](https://clawhub.ai/globalcaos/teams-hack) | Reads Teams chats, posts to channels, searches everything. One browser handshake. |
### 🤖 Agent & DevOps
-| Skill | What it does |
-| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
-| [`coding-agent`](https://clawhub.ai/globalcaos/coding-agent) | Hand off a coding task, come back to a diff. Codex, Claude Code, or Pi — your call. |
-| [`subagent-overseer`](https://clawhub.ai/globalcaos/subagent-overseer) | Sub-agents that go silent don't go unnoticed. Health checks, zero babysitting. |
-| [`fork-and-skill-scanner-ultimate`](https://clawhub.ai/globalcaos/fork-and-skill-scanner-ultimate) | Scan 1,000 GitHub forks per run. Surface the gold, skip the clones. |
-| [`memory-bench-pioneer`](https://clawhub.ai/globalcaos/memory-bench-pioneer) | Peer-review-grade evaluation suite — LLM-as-judge, nDCG, MAP, MRR metrics. |
-| [`model-prompt-adapter`](https://clawhub.ai/globalcaos/model-prompt-adapter) | Universal prompt addenda for cross-provider fallback chains. Fixes per-model failure modes. |
-| [`smart-model-router`](https://clawhub.ai/globalcaos/smart-model-router) | Auto-selects the optimal model per task. Cost vs capability, no manual routing. |
+| Skill | What it does |
+| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
+| [`subagent-overseer`](https://clawhub.ai/globalcaos/subagent-overseer) | Sub-agents that go silent don't go unnoticed. Health checks, zero babysitting. |
+| [`fork-and-skill-scanner-ultimate`](https://clawhub.ai/globalcaos/fork-and-skill-scanner-ultimate) | Scan 1,000 GitHub forks per run. Surface the gold, skip the clones. |
+| [`memory-bench-pioneer`](https://clawhub.ai/globalcaos/memory-bench-pioneer) | Peer-review-grade evaluation suite — LLM-as-judge, nDCG, MAP, MRR metrics. |
### 🛡️ Security & Governance
@@ -313,30 +308,12 @@ No API keys. No admin consent. Your authenticated browser session IS the API.
| -------------------------------------------------------------------------- | -------------------------------------------------------------- |
| [`computational-humor`](https://clawhub.ai/globalcaos/computational-humor) | 12 humor patterns based on embedding space bisociation theory. |
-### 📖 Knowledge & Onboarding
-
-| Skill | What it does |
-| ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- |
-| [`agent-sensei-ultimate`](https://clawhub.ai/globalcaos/agent-sensei-ultimate) | The sensei your agent never had. 40 lessons on ethics, memory, budget, self-evolution. Day 1: mediocre. Day 30: expert. |
-
### 📋 Data & Migration
| Skill | What it does |
| -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [`chatgpt-exporter-ultimate`](https://clawhub.ai/globalcaos/chatgpt-exporter-ultimate) | Leaving ChatGPT? Take your conversations with you. Full export, clean format. |
-### 🛰️ Location & IoT
-
-| Skill | What it does |
-| ------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------- |
-| [`owntracks-location`](https://clawhub.ai/globalcaos/owntracks-location) | Real-time phone location tracking with named places and distance queries. Always know where you are. |
-
-### 🌐 Web & CMS
-
-| Skill | What it does |
-| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- |
-| [`wordpress-ultimate`](https://clawhub.ai/globalcaos/wordpress-ultimate) | Three env vars, one script — your agent manages your entire WordPress site. Draft-only safety included. |
-
---
## 📖 The Field Guide
@@ -484,30 +461,28 @@ Both are excellent standalone tools. We folded their ideas into a single embedde
## 📚 The J-Series Papers
-Every system in TinkerClaw began as a research paper about a real failure we hit — each named for the brain region it imitates. **18 papers** in all. J1–J14 are published on [thetinkerzone.com](https://thetinkerzone.com/); J15–J18 are manuscripts in the pipeline (†).
-
-| # | Paper | Codename | What it is |
-| --- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
-| J1 | [Total Recall](https://thetinkerzone.com/total-recall-pointer-based-compaction-and-task-conditioned-retrieval-for-persistent-llm-agents/) | **ENGRAM** | Most agents compact memory by summarizing — and quietly lose the one detail that mattered. Lossless, pointer-based compaction instead. |
-| J2 | [Instant Recall](https://thetinkerzone.com/instant-recall-a-pre-computed-concept-index-for-o1-memory-retrieval-in-persistent-ai-agents/) | **HIPPOCAMPUS** | The answer is right there in memory and it still can't find it. An offline concept index makes recall O(1), not a brute-force scan. |
-| J3 | [Fractal Reasoning](https://thetinkerzone.com/fractal-reasoning-multi-resolution-memory-and-self-similar-metacognition-for-llm-agents/) | **DENDRITE** | Flat memory fetches a fact or a gist, never both. Multi-resolution indexing lets the agent zoom in and out at will. |
-| J4 | [Identity Persistence](https://thetinkerzone.com/identity-persistence-keeping-an-llm-agents-personality-stable-across-sessions-model-swaps-and-restarts/) | **CORTEX** | It remembers every fact yet stops sounding like itself. Pins the persona across sessions, model swaps, and restarts. |
-| J5 | [Sleep Consolidation](https://thetinkerzone.com/sleep-consolidation-how-nightly-prompting-makes-a-stateless-agent-get-better-over-time/) | **CEREBELLUM** | 79% fewer incidents in 30 days — no fine-tuning, just a nightly loop that rewrites its own instructions while you sleep. |
-| J6 | [Round Table](https://thetinkerzone.com/round-table-exploiting-cognitive-diversity-as-a-computational-resource-in-persistent-ai-agents/) | **SYNAPSE** | Stop crowning one "best" model. Seat Claude, GPT, and Gemini at one table and let cognitive diversity carry the answer. |
-| J7 | [Humor Embeddings](https://thetinkerzone.com/humor-embeddings-laughter-from-inverted-memory-bisociation-in-computational-embedding-space/) | **LIMBIC** | Memory retrieves what's nearest; humor finds what's at the right distance. Computational comedy from embedding geometry. |
-| J8 | [Curiosity Drive](https://thetinkerzone.com/the-wondering-machine-giving-an-llm-the-drive-to-learn-on-its-own/) | **THALAMUS** | LLMs answer brilliantly but never wonder. A drive that spots its own knowledge gaps and goes to close them. |
-| J9 | [Agent Security](https://thetinkerzone.com/aegis-a-multi-layered-security-framework-for-autonomous-ai-agents/) | **AEGIS** | The question isn't whether your agent is a risk — it's which risks apply. A layered framework, safer than NeMo at a fraction of the cost. |
-| J10 | [Corporate Swarm](https://thetinkerzone.com/hivemind-hierarchical-agent-swarms-for-enterprise-knowledge-management/) | **HIVEMIND** | A hierarchical agent swarm that lets a whole company run agents — deep integration, hard clearance boundaries. |
-| J11 | [Learned Intuition](https://thetinkerzone.com/learned-intuition-a-reflex-layer-that-stops-your-agent-before-it-does-the-wrong-thing/) | **AMYGDALA** | It had all the context and still did the wrong thing. A learned reflex layer that pauses danger before it happens. |
-| J12 | [Budget Prompting](https://thetinkerzone.com/budget-prompting-cutting-the-cost-of-always-on-memory-agents-2-3x/) | **MYELIN** | Leave it running overnight and the bill is brutal — every turn re-bills the whole context. 20 techniques that cut it 2–3×. |
-| J13 | [Executive Function](https://thetinkerzone.com/prefrontal-giving-your-agent-an-executive-function-with-a-recipe-execution-substrate/) | **PREFRONTAL** | A brilliant worker and a terrible executive. The missing executive layer: a recipe substrate for planning and follow-through. |
-| J14 | [Memory Hooks](https://thetinkerzone.com/mnemosyne-four-hooks-that-upgrade-your-agents-memory-without-forking-it/) | **MNEMOSYNE** | Four quiet memory failures — slow lookups, task-blind retrieval, silent contradictions, no decay. Four hooks that fix them without forking. |
-| J15 | Recipe Abstractions † | **RSC** | Recipes as a programming language — intermediate abstractions so workflows compose instead of endlessly repeat. |
-| J16 | Salience Pyramid † | **SALIENCE** | The death of fixed thresholds: a pyramid of significance and cheap traversal as the basis of next-gen vibe programming. |
-| J17 | Recipe Grammar † | **BROCA** | Gives agent recipes a grammar — a gradual type system and combinator algebra for self-composing workflows. |
-| J18 | Personality Tuning † | **STRIATUM** | Personality that tunes itself — learned modulation from your feedback, so corrections stick without editing a file. |
-
-_† Manuscript in the pipeline — not yet posted to thetinkerzone._
+Every system in TinkerClaw began as a research paper about a real failure we hit — each named for the brain region it imitates. **18 papers**, all published on [thetinkerzone.com](https://thetinkerzone.com/).
+
+| # | Paper | Codename | What it is |
+| --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| J1 | [Total Recall](https://thetinkerzone.com/total-recall-pointer-based-compaction-and-task-conditioned-retrieval-for-persistent-llm-agents/) | **ENGRAM** | Most agents compact memory by summarizing — and quietly lose the one detail that mattered. Lossless, pointer-based compaction instead. |
+| J2 | [Instant Recall](https://thetinkerzone.com/instant-recall-a-pre-computed-concept-index-for-o1-memory-retrieval-in-persistent-ai-agents/) | **HIPPOCAMPUS** | The answer is right there in memory and it still can't find it. An offline concept index makes recall O(1), not a brute-force scan. |
+| J3 | [Fractal Reasoning](https://thetinkerzone.com/fractal-reasoning-multi-resolution-memory-and-self-similar-metacognition-for-llm-agents/) | **DENDRITE** | Flat memory fetches a fact or a gist, never both. Multi-resolution indexing lets the agent zoom in and out at will. |
+| J4 | [Identity Persistence](https://thetinkerzone.com/identity-persistence-keeping-an-llm-agents-personality-stable-across-sessions-model-swaps-and-restarts/) | **CORTEX** | It remembers every fact yet stops sounding like itself. Pins the persona across sessions, model swaps, and restarts. |
+| J5 | [Sleep Consolidation](https://thetinkerzone.com/sleep-consolidation-how-nightly-prompting-makes-a-stateless-agent-get-better-over-time/) | **CEREBELLUM** | 79% fewer incidents in 30 days — no fine-tuning, just a nightly loop that rewrites its own instructions while you sleep. |
+| J6 | [Round Table](https://thetinkerzone.com/round-table-exploiting-cognitive-diversity-as-a-computational-resource-in-persistent-ai-agents/) | **SYNAPSE** | Stop crowning one "best" model. Seat Claude, GPT, and Gemini at one table and let cognitive diversity carry the answer. |
+| J7 | [Humor Embeddings](https://thetinkerzone.com/humor-embeddings-laughter-from-inverted-memory-bisociation-in-computational-embedding-space/) | **LIMBIC** | Memory retrieves what's nearest; humor finds what's at the right distance. Computational comedy from embedding geometry. |
+| J8 | [Curiosity Drive](https://thetinkerzone.com/the-wondering-machine-giving-an-llm-the-drive-to-learn-on-its-own/) | **THALAMUS** | LLMs answer brilliantly but never wonder. A drive that spots its own knowledge gaps and goes to close them. |
+| J9 | [Agent Security](https://thetinkerzone.com/aegis-a-multi-layered-security-framework-for-autonomous-ai-agents/) | **AEGIS** | The question isn't whether your agent is a risk — it's which risks apply. A layered framework, safer than NeMo at a fraction of the cost. |
+| J10 | [Corporate Swarm](https://thetinkerzone.com/hivemind-hierarchical-agent-swarms-for-enterprise-knowledge-management/) | **HIVEMIND** | A hierarchical agent swarm that lets a whole company run agents — deep integration, hard clearance boundaries. |
+| J11 | [Learned Intuition](https://thetinkerzone.com/learned-intuition-a-reflex-layer-that-stops-your-agent-before-it-does-the-wrong-thing/) | **AMYGDALA** | It had all the context and still did the wrong thing. A learned reflex layer that pauses danger before it happens. |
+| J12 | [Budget Prompting](https://thetinkerzone.com/budget-prompting-cutting-the-cost-of-always-on-memory-agents-2-3x/) | **MYELIN** | Leave it running overnight and the bill is brutal — every turn re-bills the whole context. 20 techniques that cut it 2–3×. |
+| J13 | [Executive Function](https://thetinkerzone.com/prefrontal-giving-your-agent-an-executive-function-with-a-recipe-execution-substrate/) | **PREFRONTAL** | A brilliant worker and a terrible executive. The missing executive layer: a recipe substrate for planning and follow-through. |
+| J14 | [Memory Hooks](https://thetinkerzone.com/mnemosyne-four-hooks-that-upgrade-your-agents-memory-without-forking-it/) | **MNEMOSYNE** | Four quiet memory failures — slow lookups, task-blind retrieval, silent contradictions, no decay. Four hooks that fix them without forking. |
+| J15 | [Recipe Abstractions](https://thetinkerzone.com/why-your-pre-push-privacy-gate-is-lying-to-you-and-the-recipe-as-the-missing-middle-layer/) | **RSC** | Recipes as a programming language — intermediate abstractions so workflows compose instead of endlessly repeat. |
+| J16 | [Salience Pyramid](https://thetinkerzone.com/salience-the-death-of-fixed-thresholds-the-pyramid-of-significance-and-cheap-traversal-as-the-basis-of-next-generation-vibe-programming/) | **SALIENCE** | The death of fixed thresholds: a pyramid of significance and cheap traversal as the basis of next-gen vibe programming. |
+| J17 | [Recipe Grammar](https://thetinkerzone.com/broca-recipe-grammar-gradual-type-system-combinator-algebra/) | **BROCA** | Gives agent recipes a grammar — a gradual type system and combinator algebra for self-composing workflows. |
+| J18 | [Personality Tuning](https://thetinkerzone.com/striatum-learned-personality-modulation-tuning-an-agents-behaviour-from-feedback-not-static-prompt-text/) | **STRIATUM** | Personality that tunes itself — learned modulation from your feedback, so corrections stick without editing a file. |
---
diff --git a/TINKER_UI_DESIGN_BIBLE/INDEX.md b/TINKER_UI_DESIGN_BIBLE/INDEX.md
index ff4b01f3eb2a7..d802ae6ecc48a 100644
--- a/TINKER_UI_DESIGN_BIBLE/INDEX.md
+++ b/TINKER_UI_DESIGN_BIBLE/INDEX.md
@@ -26,7 +26,7 @@ This directory IS the bible. The monolithic `TINKER_UI_DESIGN_BIBLE.md` was spli
- "what is this config key, who reads it, who can override?" → `config-shape.md`
- "when X breaks, where does the symptom appear?" → `failures.md`
- "what probes exist for inspecting Y?" → `probes.md`
- - "why is cc-bridge's tool loop different?" → `tool-loop.md`
+ - "why is tinker-bridge's tool loop different?" → `tool-loop.md`
- "which model gets picked, in what order?" → `auth-routing.md`
- "which crons run, when, last status?" → `crons.md`
- "is this string safe to publish?" → `pii-boundary.md`
@@ -63,7 +63,7 @@ This directory IS the bible. The monolithic `TINKER_UI_DESIGN_BIBLE.md` was spli
| `config-shape.md` | Settings flow from openclaw.json to runtime; override chains; dead-code traps | mixed | ~400 |
| `failures.md` | Failure-mode propagation maps | hand | ~300 |
| `probes.md` | Inspection primitives registry (live + proposed) | auto from gateway methods + hand | ~150 |
-| `tool-loop.md` | Why cc-bridge tool calls don't round-trip through OpenClaw exec | hand | ~150 |
+| `tool-loop.md` | Why tinker-bridge tool calls don't round-trip through OpenClaw exec | hand | ~150 |
| `auth-routing.md` | Cost-aware model routing, failover, billing tiers | mixed | ~150 |
| `crons.md` | Cron registry + auto-merge policy | auto from jobs.json + hand | ~150 |
| `pii-boundary.md` | Public-OK vs private-only, leak-grep regex, sanitization workflow | hand | ~100 |
diff --git a/TINKER_UI_DESIGN_BIBLE/auth-routing.md b/TINKER_UI_DESIGN_BIBLE/auth-routing.md
index cfc6810427c01..5f4fe86665d17 100644
--- a/TINKER_UI_DESIGN_BIBLE/auth-routing.md
+++ b/TINKER_UI_DESIGN_BIBLE/auth-routing.md
@@ -13,6 +13,8 @@ verify:
cmd: python3 -c 'import json,os; cfg=json.load(open(os.path.expanduser("~/.openclaw/openclaw.json"))); p=cfg["agents"]["defaults"]["model"]["primary"]; assert p.startswith("claude-code/"), f"primary {p} is metered, not the flat-rate subscription"'
- name: primary model is the best-ranked subscription model (rank table is cron-updated daily — derive it per design-principles.md, never freeze a model name)
cmd: python3 -c 'import json,os; cfg=json.load(open(os.path.expanduser("~/.openclaw/openclaw.json"))); d=cfg["agents"]["defaults"]; p=d["model"]["primary"]; subs={k:v["rank"] for k,v in d["models"].items() if k.startswith("claude-code/") and isinstance(v,dict) and "rank" in v}; best=min(subs,key=subs.get); assert p==best, f"primary {p} != best-ranked subscription model {best} @ rank {subs[best]}"'
+ - name: all four thinking-level resolution sites clamp via resolveSupportedThinkingLevel (none rejects an over-ceiling level)
+ cmd: python3 -c 'import os; r=os.path.expanduser("~/src/tinkerclaw/src"); sites=["auto-reply/reply/get-reply-run.ts","auto-reply/reply/directive-handling.impl.ts","gateway/sessions-patch.ts","agents/agent-command.ts"]; [exec("t=open(os.path.join(r,s)).read(); assert \"resolveSupportedThinkingLevel\" in t, s+\": resolveSupportedThinkingLevel call missing — over-ceiling thinking level may hard-reject again\"") for s in sites]; sp=open(os.path.join(r,"gateway/sessions-patch.ts")).read(); assert "next.thinkingLevel = resolveSupportedThinkingLevel" in sp, "sessions-patch.ts no longer clamps the persisted thinkingLevel"'
---
# Auth + model routing
@@ -23,6 +25,8 @@ Source of truth: `agents.defaults.models[].rank` in `openclaw.js
**The primary is DERIVED, not frozen (design-principles.md #19).** `agents.defaults.model.primary` is the **best-ranked _subscription_ (cli-gm / `claude-code/*`) model** — never a metered model, however highly the leaderboard ranks it. The rank numbers churn daily; the routing rule does not. The frontmatter `verify:` enforces the derived rule (primary is a `claude-code/*` model AND equals the lowest-rank `claude-code/*` entry), so it survives a new model landing at the top instead of re-breaking on every cron run. The table below is a dated snapshot, illustrative only.
+**Live-config rank decision (2026-06-23, in `~/.openclaw/openclaw.json`, uncommitted config — NOT a tinkerclaw code change):** `agents.defaults.models` ranks were set so `claude-code/claude-opus-4-8` = **rank 1** (matching `agents.defaults.model.primary`) and the **unavailable** `claude-code/claude-fable-5` was demoted to **rank 25**. This makes the derived-primary `verify:` (line below — primary == best-ranked `claude-code/*` entry) pass: with fable-5 no longer the lowest-rank subscription model, the best-ranked subscription model is opus-4-8, which IS the primary. The `verify:` reads `~/.openclaw/openclaw.json` **live** (not a snapshot), so this config change alone flips the gate green — no fork code or bible regeneration is needed. Fable 5 is export-controlled / UNAVAILABLE, so it must never be allowed to win the best-rank derivation.
+
Current snapshot (2026-06-10):
| Rank | Model | Alias | Tier | Notes |
@@ -55,7 +59,7 @@ Current snapshot (2026-06-10):
### claude-code (Anthropic subscription via claude-cli)
-- **Driver:** `tinkerclaw-cc-bridge` plugin → claude-cli subprocess.
+- **Driver:** `tinkerclaw-tinker-bridge` plugin → claude-cli subprocess.
- **Auth profile:** `anthropic:cli-gm` (OAuth, `~/.claude/.credentials-gm.json`).
- **Order:** `[cli-gm]` only. The metered `anthropic:api` profile is DISABLED in `auth.order.anthropic`.
- **Tier:** subscription (max_20x at $200/month per `env.ANTHROPIC_SUBSCRIPTION_TIER`).
@@ -83,6 +87,25 @@ Current snapshot (2026-06-10):
- **Base URL:** `http://127.0.0.1:11434`.
- **Tier:** free / local. Currently used only for `mxbai-embed-large` (memorySearch embeddings), not for chat.
+## Thinking-level clamp — unsupported levels clamp, never reject (cross-model, FORK 2026-06-24)
+
+Each model exposes a thinking profile (the ordered set of levels it admits, ranked by `THINKING_LEVEL_RANKS` in `thinking.shared.ts`). The effort slider's top stop is **Max**, but not every model admits `max` — e.g. `openai/gpt-5.5` tops out at `xhigh`. When the requested level exceeds a model's ceiling, the resolver **clamps DOWN to that model's highest supported level and proceeds** — it never hard-errors the turn.
+
+The canonical resolver is `resolveSupportedThinkingLevel({ provider, model, level, catalog })` (`src/auto-reply/thinking.ts`): if the level is in the profile it passes through, otherwise it returns the highest profile level whose rank `<=` the requested rank (falling back to the highest non-`off` level, then `off`). This is the single source of truth for "what level does this model actually get."
+
+There are FOUR resolution sites where a requested level meets a model that may not support it; ALL FOUR clamp via `resolveSupportedThinkingLevel` (none rejects):
+
+| Path | Site | Surfaces the clamp via |
+| ------------------------------- | ------------------------------------ | --------------------------------------------------------------------------------------------------- |
+| chat.send / Tinker | `get-reply-run.ts` (~:630) | `logVerbose` info note (no ack channel on this path) |
+| `/think` directive | `directive-handling.impl.ts` (~:318) | ack note, guarded on `requested !== applied` |
+| persisted `thinkingLevel` patch | `sessions-patch.ts` (~:512) | silent clamp — the patch always succeeds (previously a `"thinkingLevel" in patch → invalid` REJECT) |
+| CLI `agent` | `agent-command.ts` (~:862) | stderr note |
+
+**Why the slider could trigger a reject:** the slider's Max injects an **EXPLICIT** `/think max` directive (`chat-command-body.ts`), so the resolver classified it as explicit and (pre-fix) took a reject branch instead of the clamp that already existed for the non-explicit case. The fix unified all four sites to clamp regardless of explicit-vs-derived. The slider's Max is a **ceiling request**, not a contract the model must honor exactly. Models that DO support `max` (`claude-code/*`) are unaffected. This is the cross-model analogue of the 2026-06-19 `claude-code` thinking-profile gate (which rejected a level the model DID support because its profile was missing — opposite cause, same "reject instead of admit/clamp" symptom). See bug-log.md `### FIXED [think-clamp+detection-pattern]` (2026-06-24).
+
+The "all four sites clamp via `resolveSupportedThinkingLevel`, none rejects" contract is enforced by this file's frontmatter `verify:` block (asserts each of the four source files calls the resolver and that `sessions-patch.ts` still clamps the persisted level).
+
## Failover and cost-aware routing
The fork patched the upstream failover bug 2026-02-19 (bible §11.x):
diff --git a/TINKER_UI_DESIGN_BIBLE/bible.md b/TINKER_UI_DESIGN_BIBLE/bible.md
index 1e8f62928e59d..b1b535b627cd6 100644
--- a/TINKER_UI_DESIGN_BIBLE/bible.md
+++ b/TINKER_UI_DESIGN_BIBLE/bible.md
@@ -15,7 +15,7 @@ verify:
> Living document. Updated every time we work on Tinker UI features, fixes, or design changes.
> Location: `~/src/tinkerclaw/TINKER_UI_DESIGN_BIBLE.md` (tracked in GitHub fork)
> Last updated: 2026-05-09 (§5.78b — supersession pointer added: §5.78b's 2026-04-29 diagram is the historical local-only-`develop` workflow; §5.78g is the live rule — `develop` is the working branch, both local and pushed; we tinker on `develop` and merge into `main` only by mutual agreement when it's robust enough to ship. Earlier same day: §11.6b — `/new` briefing injection redesign: when the user types `/new` in Tinker UI, the client awaits a new gateway RPC `briefing.resolve` (workspace `BRIEFING.md` → bundled `briefing-default.md` fallback), builds an imperative prompt with the briefing's full content inlined, and sends it as the user message. The user bubble renders as a collapsed `` with summary `⚡ Executing `; the path is a clickable monospace link calling the new `files.openInEditor` (allowlist-guarded ADMIN_SCOPE) RPC to open the file via `xdg-open`. Replaces the soft "Read and follow" suffix that caused Opus 4.7 to acknowledge BRIEFING.md and ask permission instead of executing it (10:00 turn, 33s, 258B reply, zero tool calls). New imperative wording sentinel `"Execute the morning briefing NOW"` is dual-purpose — model directive + client-side detection regex; change in lockstep. 7 commits on develop. Earlier same day: §11.6a — per-chat strategy added: every WhatsApp inbound now carries `[chat-profile]` (groups only — purpose, stakes, audience, format prefs, guardrails) + `[chat-rhythm]` (median + P90 word count over last 20 non-bot messages with the "match this rhythm; propose long answers, don't dump them" directive) at the top of the prelude, before the existing people/sender/recent-thread/escalation blocks. Profile authorship is agent-driven and lazy: Jarvis writes `chat-profiles/.md` (or appends to `.notes.jsonl`) when he observes something profile-worthy; unprofiled chats fall back to `_default.md`. Strategy doc at `~/.openclaw/workspace/memory/knowledge/whatsapp-strategy.md` is hooked from `SOUL.md` so it loads once into the persona, not per-message. Persona scaffolding (🤖 prefix + 🤔↔🤖 thinking reaction + ⚡ done-separator) is wire-level non-negotiable — never dropped to fit a length budget. Earlier same day: owner-prefix global invariant — owner+"Jarvis" prefix MUST trigger from any chat (DM, group, LID, self) without per-chat allowlisting; bug fixed by propagating `msg.ownerPrefixTriggered` from on-message.ts → group-gating.ts so the two gates agree. Earlier same day: prelude→BodyForAgent wiring corrected (was silently dead in `Body` since 2026-05-04) + `[thread-escalation]` hint added (exact `whatsapp_history` tool call with chat JID + ISO `until` cursor inlined for adaptive read-back). Earlier: §5.44 thinking-reaction upgraded to persona-aware alternating heartbeat — single source of truth in `outbound-prefix.ts`. Two regression-class gotchas pinned: (1) reactions MUST route through `wmClient.sendReaction` not `sendMessage({react})`; (2) any helper calling `requireRuntimeConfig` needs cfg plumbed in or fetched at call-time.)
-> Previously (2026-04-28): §5.76 public/private boundary + git-pull contract — Jarvis ships as the day-0 default; user overrides live in `~/.openclaw/workspace/`; resolution order config → workspace → bundled; five hardcoded `/home//...` paths in `worker.ts` and `db-probe.mjs` to fix; chrome-extension token-leak placeholder to replace; narration / subagent-helper / tool-choice / persona / briefing default prompts extracted to `extensions/tinkerclaw-cc-bridge/{personas,prompts}/` and loaded via shared resolver. The "Sam test" + "Day-90 test" are the structural guarantees.
+> Previously (2026-04-28): §5.76 public/private boundary + git-pull contract — Jarvis ships as the day-0 default; user overrides live in `~/.openclaw/workspace/`; resolution order config → workspace → bundled; five hardcoded `/home//...` paths in `worker.ts` and `db-probe.mjs` to fix; chrome-extension token-leak placeholder to replace; narration / subagent-helper / tool-choice / persona / briefing default prompts extracted to `extensions/tinkerclaw-tinker-bridge/{personas,prompts}/` and loaded via shared resolver. The "Sam test" + "Day-90 test" are the structural guarantees.
---
@@ -88,15 +88,17 @@ extensions/hippocampus/ ← Fork-only plugin stub
- **What:** When a recipe is active, three visual indicators appear: (1) persistent recipe banner below topbar showing recipe name + step progress with category color, (2) thinking indicator annotation showing current step alongside model name, (3) assistant message tags showing which recipe step produced each message. Zero extra tokens — all from hook state.
- **Files:** `extensions/tinkerclaw-prefrontal/index.ts` (prefrontal-recipe-status broadcast), `tinker-ui/src/app.ts` (banner + thinking + tags), `tinker-ui/src/styles/base.css`
-### 5.66 Claude-Code Provider Bridge — `tinkerclaw-cc-bridge`
+### 5.66 Claude-Code Provider Bridge — `tinkerclaw-tinker-bridge`
→ moved 2026-05-11 to `./tool-loop.md` (the divergence rationale was already there; provider mechanics + workspace skills wrapper §5.66a are now appended under "Provider mechanics").
+**Renamed `cc-bridge` → `tinker-bridge` (2026-06-19), name FULLY PURGED 2026-06-20.** Extension dir `extensions/tinkerclaw-cc-bridge` → `tinkerclaw-tinker-bridge`; plugin id `tinkerclaw-tinker-bridge`; ClawHub package `@globalcaos/tinker-bridge` with `publishToClawHub:true`. The `cc-bridge` name is retired EVERYWHERE — code symbols (`CcBridge*`→`TinkerBridge*`, `CC_BRIDGE_*`→`TINKER_BRIDGE_*`, file `cc-bridge-session-map.ts`→`tinker-bridge-session-map.ts`), the `ccBridge:true` cross-extension flag (both the `stream.ts` emit and the `learned-intuition` listener), the `cc_bridge_*` error codes, comments/docs/prompts — AND the persisted identifiers, with migrations so no live state is lost: the worker-pool prefix `cc-sp-`→`tinker-sp-` and the state dir `~/.openclaw/cc-bridge/`→`~/.openclaw/tinker-bridge/` migrate on first load (copy + rekey — `session-map.ts:migrateLegacyMap`, plus a legacy-path read fallback in `tinker-bridge-session-map.ts`); the `cc-bridge-tool` history customType→`tinker-bridge-tool` with a legacy-kind read back-compat in `session-utils.fs.ts` so pre-rename transcripts still render. **KEPT (genuinely NOT the bridge):** the provider id `claude-code` (auth/model routing keys on this, not the plugin id), and `cc-skills-bridge` (prefrontal's SKILL.md→recipe transpiler — a different component). The actual ClawHub push stays human-gated (the package description documents the Anthropic gray-zone).
+
### 5.67 Amygdala + Fractal Injection Pipeline (2026-04-18)
-- **Status:** `FRACTAL DEPLOYED; AMYGDALA RETIRED 2026-06-10` — the per-turn `🧠 AMYGDALA` section was retired (split/render extracted to `tinker-ui/src/sectioned-reply.ts`; see `tinker-ui.md` §5.74 for the authoritative current state + don't-regress). **The in-band injection design below is being superseded by §5.67a — Parallel Fractal Reflection v2 (DESIGN, 2026-06-11).**
+- **Status:** `FRACTAL DEPLOYED; AMYGDALA RETIRED 2026-06-10` — the per-turn `🧠 AMYGDALA` section was retired (split/render extracted to `tinker-ui/src/sectioned-reply.ts`; see `tinker-ui.md` §5.74 for the authoritative current state + don't-regress). **The in-band injection design below is being superseded by §5.67a — Parallel Fractal Reflection v2 (DESIGN, 2026-06-11).** **UPDATE 2026-06-19 (Bug A): the `💬 ANSWER` section marker is RETIRED.** The UI now separates narration/answer STRUCTURALLY (text after the last tool = answer; `reply-grouping.ts` `narrationIndices`), so `buildInjectedPrompt` no longer injects `💬 ANSWER` (kept `🌿 FRACTAL`, which is also system-prompt-mandated). Root cause: the marker was injected only transiently/toggle-gated while `🌿 FRACTAL` was always-mandated → the model dropped `💬 ANSWER` and the positional collapse hid the answer. Authoritative current state: `tinker-ui.md` §5.8; root cause + fix: `bug-log.md` FIXED A. Lines below describing the two-section `💬 ANSWER → 🌿 FRACTAL` injection are now HISTORICAL.
- **What (current):** ONE topbar toggle (🌿 Fractal — enabled by default) appends a short pointer instruction to every outgoing prompt so Opus emits a two-section reply: `💬 ANSWER` → `🌿 FRACTAL`. The sections are split client-side and rendered as stacked bubbles: answer expanded, fractal collapsed. **(History:** there used to be a second `🧠 AMYGDALA` section + topbar button; both were removed — the always-on Amygdala side panel is the gate-decision surface now. The UI no longer splits, compacts, or fabricates a `🧠` block.)
-- **Pointer-based prompt:** the injected suffix doesn't inline the full instructions. It references `fractal-prompt.md` (MEMORY/PATTERN/RIPPLE/IMPROVE + ACTION-prefix rule + SELF-HEAL layers), read once at cc-bridge spawn time and appended to claude's system prompt. Compact prompts, persistent behavior. (`amygdala-prompt.md` was deleted — no longer loaded.)
+- **Pointer-based prompt:** the injected suffix doesn't inline the full instructions. It references `fractal-prompt.md` (MEMORY/PATTERN/RIPPLE/IMPROVE + ACTION-prefix rule + SELF-HEAL layers), read once at tinker-bridge spawn time and appended to claude's system prompt. Compact prompts, persistent behavior. (`amygdala-prompt.md` was deleted — no longer loaded.)
- **User bubble:** when amygdala/fractal is enabled, the user sees only their original text; the full appended prompt collapses behind a "view full prompt" expandable (`renderUserBubbleWithPromptToggle`). When both toggles are off, nothing is appended.
- **Section splitter:** `splitSectionedReply` accepts markers wrapped in `**`/`__` bold, optional trailing `:`, AND optional markdown heading prefixes `#` through `####` — `💬 ANSWER`, `🧠 AMYGDALA` (with 🫀 fallback), `🌿 FRACTAL` (optionally `FRACTAL ACTION`). Regex pattern: `(^|\n)\s*#{0,4}\s*(?:\*\*|__)?\s*\s*(?:\*\*|__)?\s*\s*:?\s*(?:\*\*|__)?\s*:?\s*`. `renderSectionedReply` promotes `other` to the answer slot when Opus emits a non-sectioned response with amygdala/fractal after. **Don't regress (commit `d32e44cc24`, 2026-05-24):** when Opus starts emitting H2 headings (`## 💬 ANSWER`) instead of bare markers, the `#{0,4}\s*` slot in the regex MUST stay — without it the splitter returns null and the entire reply renders as ONE bubble with all three section markers as literal H2 text. The variant shapes the regex must match: `💬 ANSWER:`, `💬 **ANSWER**`, `💬 **ANSWER:**`, `## 💬 ANSWER`, `### 💬 ANSWER`, `**💬 ANSWER**`.
- **Icons:** Models icon changed from 🧠 to 🕸️ (pink brain reserved for amygdala). Icon glyphs: `🧠` amygdala, `🌿` fractal.
@@ -107,16 +109,16 @@ extensions/hippocampus/ ← Fork-only plugin stub
- **Status:** `DESIGN — building` (owner-approved 2026-06-11; supersedes the in-band injection of §5.67 — flip §5.67 to `SUPERSEDED` when v2 ships). Full design history + file-level grounding: jarvis-icu commit `b9aedcf` + three code-verified research passes. **AMENDED by §5.67b (v3, same day — 34-agent adversarial redesign pass): read both; where they conflict §5.67b wins.** The "build the whole thing in one drop" owner decision is revised there into staged owner-visible drops (Drop 1 touches zero core code).
- **Why:** the in-band fractal (§5.67) stopped contributing — the strong standalone plugin is `enabled:false`, and the baked-in `--append-system-prompt` section is trivially phoned-in. When the standalone plugin WAS on, commit `58cc3dc772e` (2026-04-17) regressed it from a polite follow-up to a `sessions.steer`/`interruptIfActive` racer that cancels the very turn it reflects on (one run-lane per sessionKey). **Cost reframe:** the live brain is subscription-billed ($0/token; the real cost is Max-plan quota + latency), so the original "cheap baked-in section to save tokens" trade was the wrong lever — it saved nothing and gutted the feature.
- **What (v2):** fractal becomes a **parallel, off-channel reflection plugin** fired from the public `agent_end` hook (fire-and-forget; carries `runId`, `messages`, `sessionKey`), running on its **own runId** in a dedicated `"fractal"` command lane. Lanes are independent queues, so it **never blocks the user's main reply** — the `ReplyRunAlreadyActiveError` retry/backoff machinery in `fractal-inject.ts` becomes obsolete.
-- **Two-phase thinking:** a cheap **low-thinking triage** every turn; on a find it **escalates to a fresh max-thinking fix lane** — NOT an in-place dial-up, because cc-bridge pins `MAX_THINKING_TOKENS` at worker spawn (see `tool-loop.md`), so escalation is a new spawn, not a slider change on the warm worker.
-- **Cache — don't micro-manage it:** ZERO custom cache code on the cc-bridge subscription path. claude-cli auto-caches (Claude-Code-grade); warm re-read comes from **forking the main session** (`--resume --fork-session`) so the transcript prefix serves warm and only the short fractal instruction + reflection output are fresh. Explicit `cache_control` tagging (`applyAnthropicPayloadPolicyToParams`, gated on `enableCacheControl`) exists ONLY on the metered native-Anthropic/pi-ai provider — the vanilla path — NOT cc-bridge. See `tool-loop.md` (cc-bridge) + `config-shape.md` (`enableCacheControl`).
+- **Two-phase thinking:** a cheap **low-thinking triage** every turn; on a find it **escalates to a fresh max-thinking fix lane** — NOT an in-place dial-up, because tinker-bridge pins `MAX_THINKING_TOKENS` at worker spawn (see `tool-loop.md`), so escalation is a new spawn, not a slider change on the warm worker.
+- **Cache — don't micro-manage it:** ZERO custom cache code on the tinker-bridge subscription path. claude-cli auto-caches (Claude-Code-grade); warm re-read comes from **forking the main session** (`--resume --fork-session`) so the transcript prefix serves warm and only the short fractal instruction + reflection output are fresh. Explicit `cache_control` tagging (`applyAnthropicPayloadPolicyToParams`, gated on `enableCacheControl`) exists ONLY on the metered native-Anthropic/pi-ai provider — the vanilla path — NOT tinker-bridge. See `tool-loop.md` (tinker-bridge) + `config-shape.md` (`enableCacheControl`).
- **Honest file attribution:** "fractal changed X" vs "main turn changed Y" derives from **per-runId `tool.result` events** (fractal runId vs main runId), retiring the fake `detectFileChange` prose-scrape in `sectioned-reply.ts`.
- **Autonomy:** FULL autonomy — acts on anything (files, code, memory, recipes) — bounded ONLY by **ORCA safe-write leasing** so it never clobbers a file the user's concurrent follow-up is editing. No other hard "never" lines (owner decision 2026-06-11).
- **Render:** a `stream:"fractal"` agent event carries `{parentRunId, status, verdict, fractalChanges[], mainChanges[], reasoning}`; the UI docks a **compacted block under the answer it judged** (matched by `parentRunId`), reusing the collapsed Commentary chrome (``). Status word is the liveness signal: `acted` / `clean` / `⚠ error`. Structural owner: `tinker-ui.md` §5.72/§5.74 (which also deletes the two legacy standalone-fractal render blocks in `app.ts`).
-- **Vanilla-portable:** ONE bundled plugin — `agent_end` + `api.runtime.subagent.run` (the primitive the stock memory-core "dreaming" plugin already uses) + provider-branch (fork-session on cc-bridge / explicit `cache_control` on the API path) + a `fractal.byRunId` gateway RPC. Stock webchat can't inject reply markup (`registerControlUiDescriptor` is metadata-only), so vanilla degrades to a plain-text reflection line; the pretty compacted bubble is the only fork-only piece.
+- **Vanilla-portable:** ONE bundled plugin — `agent_end` + `api.runtime.subagent.run` (the primitive the stock memory-core "dreaming" plugin already uses) + provider-branch (fork-session on tinker-bridge / explicit `cache_control` on the API path) + a `fractal.byRunId` gateway RPC. Stock webchat can't inject reply markup (`registerControlUiDescriptor` is metadata-only), so vanilla degrades to a plain-text reflection line; the pretty compacted bubble is the only fork-only piece.
- **Loop guard (don't regress):** structured run meta `{kind:'fractal-reflection', parentRunId}` with a first-line short-circuit at the hook. **NO string-matching** on `🌿` / `# FRACTAL REFLECTION` — that brittle guard (`fractal-inject.ts`, `heartbeat-runner.ts`) caused the §11.14 8×-per-turn fire and a permanent self-detection block. Retire it.
- **v1 scope:** always-fire every turn (prove reliability first); substantive-turn gating is deferred future work (when it returns: skips are made at the hook and surfaced as `skipped(reason)` ledger rows — never a silent prompt-side skip — and gated on live signal, not turn category: "an announce revealing something genuinely new — unexpected failure, novel error, structural surprise — still reflects" [salvaged from the v1 prompt's skip_list], per #19; it graduates INTO the §5.67b governor as the derived judge of substance).
-- **cc-bridge tweak in scope (owner-approved):** the load-bearing new primitive is "boot a named lane's cc-bridge worker by **forking a specific parent session id**" — the stock subagent spawn mints a fresh child sessionKey (cold prefix); the warm path needs this small extension. Phase the build so the pipeline (parallel + render + attribution) can validate on a cold-but-correct fallback first, then settle the warm-fork and confirm with a live `cache_read_input_tokens` probe.
-- **Structural owners to populate as v2 lands (with `verify` blocks):** `tool-loop.md` (cc-bridge fork-session + thinking-pinned-at-spawn + the parent-session fork extension), `tinker-ui.md` §5.72/§5.74 (compacted render + legacy-path/prose-scrape deletion), `flows.md` (the `agent_end → triage → escalate → fractal-event → render` sequence; today's §555 "NOT a fractal trigger" note stays accurate), `subagents-and-recipes.md` (the `"fractal"` lane spawn + `setCommandLaneConcurrency`), `config-shape.md` (plugin config keys: `enabled`, `triageThinkLevel`, `fixThinkLevel`, `laneConcurrency`, `maxFixTurnsCeiling` — `minGapMs` RETIRED per §5.67b).
+- **tinker-bridge tweak in scope (owner-approved):** the load-bearing new primitive is "boot a named lane's tinker-bridge worker by **forking a specific parent session id**" — the stock subagent spawn mints a fresh child sessionKey (cold prefix); the warm path needs this small extension. Phase the build so the pipeline (parallel + render + attribution) can validate on a cold-but-correct fallback first, then settle the warm-fork and confirm with a live `cache_read_input_tokens` probe.
+- **Structural owners to populate as v2 lands (with `verify` blocks):** `tool-loop.md` (tinker-bridge fork-session + thinking-pinned-at-spawn + the parent-session fork extension), `tinker-ui.md` §5.72/§5.74 (compacted render + legacy-path/prose-scrape deletion), `flows.md` (the `agent_end → triage → escalate → fractal-event → render` sequence; today's §555 "NOT a fractal trigger" note stays accurate), `subagents-and-recipes.md` (the `"fractal"` lane spawn + `setCommandLaneConcurrency`), `config-shape.md` (plugin config keys: `enabled`, `triageThinkLevel`, `fixThinkLevel`, `laneConcurrency`, `maxFixTurnsCeiling` — `minGapMs` RETIRED per §5.67b).
### 5.67b Parallel Fractal Reflection — v3 amendments (DESIGN, 2026-06-11, adversarially verified)
@@ -126,20 +128,20 @@ extensions/hippocampus/ ← Fork-only plugin stub
- **Status union grows:** `pending | flagged | skipped(reason: quota|superseded|budget) | proposed | applied | dismissed | suspended` + fields `evidence{claim,path,verbatimQuote}`, `abstained?`, `verification{kind,passed}`, `hard?`, `artifactsTouched[]`, verdict arm `gap`. `fractalChanges`/`mainChanges` ship present-but-empty in Drop 1 (`flagged` = found-not-fixed; sizes the fix backlog before autonomy is armed).
- **Two-event contract (liveness):** a `pending` stub emits at spawn (UI docks a collapsed placeholder instantly, fills in place), the final event replaces it; a server watchdog converts dead-run stubs to `error` **only on verified deadness** — the run/lane registry reports the runId terminal-or-gone, or zero run events for that runId within the liveness ceiling — never wall-clock since spawn (#19: the 120s idle kill is the canonical bug; FOUNDATION: liveness checks never assert doneness); the ceiling boots as a frozen safety CEILING (~120s of total event silence) and is re-derived from the ledger's own p50/p95 timeToDock once rows exist. Gateway-crash covered client-side (reconnect re-queries `fractal.byRunId`). Dock-latency = recorded metric + tuning target (numbers set from live data, not a CI gate). This contract RETIRES the prompt's "Always respond" heartbeat rule (`fractal-prompt.md:200`) — liveness is infrastructure, not model prose: `skipped` turns spawn no model and emit only the terminal event.
- **Non-fire negative evidence (NEW — principle #12, FOUNDATION #5):** the watchdog only converts stubs that EXIST; a dead handler / unloaded plugin / lost enable flag emits no stub and no row — the exact silent non-fire that killed v1 (`enabled:false`) and the dead-producer class FOUNDATION #5 most distrusts. Invariant under always-fire: every main-turn `agent_end` yields exactly one ledger row (incl. `skipped`/`suspended`). Detect OUTSIDE the plugin's fate, zero core code: (1) `fractal.stats` exposes `missedTurns` = independent main-turn count minus ledger row count over the window; (2) a `failures.md` entry `fractal-silent-non-fire` whose `diagnose_with:` probe reconciles main-session turn ends (session jsonl) against `results.jsonl` rows, `manifest_via:` = flip the plugin enable flag off (the #13 round-trip); (3) tinker-ui flags a missing `pending` stub under a fresh answer after a derived window. Ships with Drop 1; spec §11 gains the negative test: kill the plugin → detector fires.
-- **Attribution (mechanism REPLACED, don't-regress):** cc-bridge `tool.result` bus events carry NO tool name/args (`stream.ts:316-345`) and R_main's tool events pre-date `agent_end` — live-bus collection is impossible. Source = persisted `customType:'cc-bridge-tool'` transcript entries (toolCallId start↔result join), read behind a settle/retry guard (the drain at `attempt-hooks.ts:829-877` is fire-and-forget async). Ships with the fix-lane drop. **Reporting-channel rule (FOUNDATION #4/#5, #11):** any fractal capability claim ("fixed/changed X") renders ONLY from tool-event-derived ledger fields (`artifactsTouched[]`, `verification{}`, per-runId transcript attribution) — model prose is narrative, never telemetry; no successor of the `detectFileChange` prose-scrape or "🌿 FRACTAL ACTION:" self-attestation may become a data source again.
-- **Lane observability grain (FOUNDATION #4 — tool use fully rendered; NEW):** fractal lanes are ordinary runs through `attempt.ts`, so the per-run machinery fires automatically — anatomy row + forensic dump, per-subagent `stream:"effort"` chip, prefrontal call-tree row. The cross-consumer skip in prefrontal MUST be scoped to the recipe-panel state writer ONLY — never the call-tree/`tool_call` feed — so a running fractal lane keeps a live row for the operator. The docked block's EXPANDED view replays the lane's full tool timeline from the same persisted `customType:'cc-bridge-tool'` join the attribution bullet reads (zero new producers). Live grain while a fix lane edits: the pending stub's fill-in-place channel carries safeWrite/ORCA lease acquisitions ("fixing — editing ") — the plugin mediates every lease, so live file-touch visibility costs no cc-bridge plumbing.
-- **Flood control (NEW):** lane splits into `fractal-triage`/`fractal-fix` (coalescing can never cancel a queued fix). Single-flight + latest-wins per session (`clearCommandLane` rejects QUEUED only and is lane-global — keep pending-slot bookkeeping per sessionKey). Quota governor on `usage.status` with a plugin-side TTL memo (`provider-usage.cache.ts` is DEAD CODE, zero importers — do not rely on it; the gateway has NO fresh quota signal for the cc-bridge subscription path, so 403/absent ⇒ fail-to-neutral: the throttle branch falls back to the `maxFixSpawnsPerHour` CEILING, the spend branch disarms): **BIDIRECTIONAL, driven by one DERIVED pressure score p = f(5h utilization, time-to-reset, value-of-finding)** — FOUNDATION #2 names this exact case ("budget weighs real remaining allowance AND time-to-reset — never a frozen '70%'"); mode degrades progressively full → triage-only → `skipped:quota` as derived pressure rises (an evidence-backed queued fix outranks speculative triage; high utilization minutes before reset is spendable surplus, not pressure). **Surplus-spend branch** (FOUNDATION Budget doctrine — surplus before a reset is welcome spend): low utilization + short time-to-reset LOWERS the escalation bar — more fix-lane spawns, deeper post-edit verification, opportunistic runs of the deferred L3 meta-pass / staleness scan; arms with the fix-lane drop (Drop 1's cold triage-only has nothing to spend on); surplus mode NEVER raises the safety ceilings (the loop-guard token bucket and circuit breaker are plumbing alarms, not quota optimizers). Any numeric constants in the score are documented safety CEILINGS with their derivation formula in `config-shape.md` per #19, with a `quota-pressure` warn/trail event as pressure rises instead of a cliff. When substantive-turn gating lands (deferred per §5.67a) it graduates INTO this governor as the derived judge of substance — never a second frozen layer. Circuit breaker 3 fails → OPEN 15min → half-open probe; one `suspended` event. **Supervised detach:** handler returns inside the 30s `agent_end` void-hook timeout; the whole cycle is ONE rooted promise chain, zero floating promises (every contained rejection writes a stability bundle; 25/60s exits the gateway — `unhandled-rejections.ts:484-498`; the playwright-relay crash class).
+- **Attribution (mechanism REPLACED, don't-regress):** tinker-bridge `tool.result` bus events carry NO tool name/args (`stream.ts:316-345`) and R_main's tool events pre-date `agent_end` — live-bus collection is impossible. Source = persisted `customType:'tinker-bridge-tool'` transcript entries (toolCallId start↔result join), read behind a settle/retry guard (the drain at `attempt-hooks.ts:829-877` is fire-and-forget async). Ships with the fix-lane drop. **Reporting-channel rule (FOUNDATION #4/#5, #11):** any fractal capability claim ("fixed/changed X") renders ONLY from tool-event-derived ledger fields (`artifactsTouched[]`, `verification{}`, per-runId transcript attribution) — model prose is narrative, never telemetry; no successor of the `detectFileChange` prose-scrape or "🌿 FRACTAL ACTION:" self-attestation may become a data source again.
+- **Lane observability grain (FOUNDATION #4 — tool use fully rendered; NEW):** fractal lanes are ordinary runs through `attempt.ts`, so the per-run machinery fires automatically — anatomy row + forensic dump, per-subagent `stream:"effort"` chip, prefrontal call-tree row. The cross-consumer skip in prefrontal MUST be scoped to the recipe-panel state writer ONLY — never the call-tree/`tool_call` feed — so a running fractal lane keeps a live row for the operator. The docked block's EXPANDED view replays the lane's full tool timeline from the same persisted `customType:'tinker-bridge-tool'` join the attribution bullet reads (zero new producers). Live grain while a fix lane edits: the pending stub's fill-in-place channel carries safeWrite/ORCA lease acquisitions ("fixing — editing ") — the plugin mediates every lease, so live file-touch visibility costs no tinker-bridge plumbing.
+- **Flood control (NEW):** lane splits into `fractal-triage`/`fractal-fix` (coalescing can never cancel a queued fix). Single-flight + latest-wins per session (`clearCommandLane` rejects QUEUED only and is lane-global — keep pending-slot bookkeeping per sessionKey). Quota governor on `usage.status` with a plugin-side TTL memo (`provider-usage.cache.ts` is DEAD CODE, zero importers — do not rely on it; the gateway has NO fresh quota signal for the tinker-bridge subscription path, so 403/absent ⇒ fail-to-neutral: the throttle branch falls back to the `maxFixSpawnsPerHour` CEILING, the spend branch disarms): **BIDIRECTIONAL, driven by one DERIVED pressure score p = f(5h utilization, time-to-reset, value-of-finding)** — FOUNDATION #2 names this exact case ("budget weighs real remaining allowance AND time-to-reset — never a frozen '70%'"); mode degrades progressively full → triage-only → `skipped:quota` as derived pressure rises (an evidence-backed queued fix outranks speculative triage; high utilization minutes before reset is spendable surplus, not pressure). **Surplus-spend branch** (FOUNDATION Budget doctrine — surplus before a reset is welcome spend): low utilization + short time-to-reset LOWERS the escalation bar — more fix-lane spawns, deeper post-edit verification, opportunistic runs of the deferred L3 meta-pass / staleness scan; arms with the fix-lane drop (Drop 1's cold triage-only has nothing to spend on); surplus mode NEVER raises the safety ceilings (the loop-guard token bucket and circuit breaker are plumbing alarms, not quota optimizers). Any numeric constants in the score are documented safety CEILINGS with their derivation formula in `config-shape.md` per #19, with a `quota-pressure` warn/trail event as pressure rises instead of a cliff. When substantive-turn gating lands (deferred per §5.67a) it graduates INTO this governor as the derived judge of substance — never a second frozen layer. Circuit breaker 3 fails → OPEN 15min → half-open probe; one `suspended` event. **Supervised detach:** handler returns inside the 30s `agent_end` void-hook timeout; the whole cycle is ONE rooted promise chain, zero floating promises (every contained rejection writes a stability bundle; 25/60s exits the gateway — `unhandled-rejections.ts:484-498`; the playwright-relay crash class).
- **Safety ceilings carry #19 discipline (NEW):** every fixed number in this design is a CEILING, never the working bound (design-principles #19; FOUNDATION Budget doctrine). Each gets its derivation formula in `config-shape.md` in the same drop that ships it: token bucket ≈ p95 observed main-turn rate × 2 lanes/turn × safety factor (recompute from ledger rows); breaker open-time derived per failure class (quota-blind vs crash); quota-memo TTL ≈ usage-endpoint staleness, not a flat 5 min. Each emits an adaptive-pressure `warn` trail event as it is approached (~80% of ceiling), not only `skipped` at the cliff. `maxFixSpawnsPerHour` binds ONLY when `usage.status` is blind (403/absent), and even then the plugin first estimates consumption from its own ledger `usage{}` rows, keeping the fixed cap as the outermost ceiling. Triage `low`≈4000 is the existing harness think-level mapping (configurable), not a new number; `laneConcurrency` keeps its v1 single-user rationale and gets a formula only when widened.
-- **Lane caps (live path corrected):** SS5b `allowTools`/`maxTokens`/`maxToolCalls` are INERT on cc-bridge (it doesn't own the tool loop — `tool-loop.md`). Live enforcement at claude-cli spawn: triage → per-spawn `disallowedTools` (Edit/Write/NotebookEdit/Bash) making "triage never edits" **structural, not prompt obedience** — the doctrine prompts split to match: the triage prompt says FLAG-with-evidence (never "edit now"), the fix-lane prompt inherits the act-before-explaining / convert-should-to-action imperatives verbatim; fix → `--max-turns` as a safety CEILING only (#19): the working turn budget is derived per-spawn from the finding's scope (files in evidence, findingKind) and capped by config `maxFixTurnsCeiling`; the budget + wind-down instruction live in the SPAWN prompt ("approaching your turn budget: stop editing, persist a summary of remaining work") — never mid-run steering (the §5.67a retired racer). On `error_max_turns` the terminal status is `flagged` with `incomplete:true` + `artifactsTouched[]` (from the transcript join, which survives exhaustion) and the cheapest scoped verifier STILL runs plugin-side and is stamped — NOT `error:`; budget-exhaustion does NOT count toward the circuit breaker, and the ledger row is resumable by the next triage or the L3 pass. SS5b params remain as the vanilla half only.
+- **Lane caps (live path corrected):** SS5b `allowTools`/`maxTokens`/`maxToolCalls` are INERT on tinker-bridge (it doesn't own the tool loop — `tool-loop.md`). Live enforcement at claude-cli spawn: triage → per-spawn `disallowedTools` (Edit/Write/NotebookEdit/Bash) making "triage never edits" **structural, not prompt obedience** — the doctrine prompts split to match: the triage prompt says FLAG-with-evidence (never "edit now"), the fix-lane prompt inherits the act-before-explaining / convert-should-to-action imperatives verbatim; fix → `--max-turns` as a safety CEILING only (#19): the working turn budget is derived per-spawn from the finding's scope (files in evidence, findingKind) and capped by config `maxFixTurnsCeiling`; the budget + wind-down instruction live in the SPAWN prompt ("approaching your turn budget: stop editing, persist a summary of remaining work") — never mid-run steering (the §5.67a retired racer). On `error_max_turns` the terminal status is `flagged` with `incomplete:true` + `artifactsTouched[]` (from the transcript join, which survives exhaustion) and the cheapest scoped verifier STILL runs plugin-side and is stamped — NOT `error:`; budget-exhaustion does NOT count toward the circuit breaker, and the ledger row is resumable by the next triage or the L3 pass. SS5b params remain as the vanilla half only.
- **Autonomy boundary (⚠ amends the 2026-06-11 "no hard lines" owner decision — pending owner ratification):** full autonomy stays EXCEPT four host-self-mod classes that become one-click PROPOSALS (`/fractal/proposals/*.patch`, `status:'proposed'`) instead of direct action: (1) the live `openclaw.json` (chokidar hot-reload — one wrong edit applies mid-session); (2) fractal's own prompts/source — **v1's "self-improve this prompt" doctrine is RETIRED in ALL FOUR instances (`fractal-prompt.md:214` response_rules; `:124` SELF-HEAL Immunize; `:138` RECIPE "patch it here too"; `:212`); v3 prompts replace each with "emit a `status:'proposed'` record carrying the exact patch"**; (3) gateway `dist/` + build invocations; (4) `systemctl` on its own unit. (1)-(2) enforced in safeWrite; (3)-(4) at the tool-permission layer (they are commands, not writes). Accident guard, not adversarial sandbox. **Paired read/act surface (principle #11 — ships in the SAME drop that arms the boundary):** pending-list = `fractal.feed` filtered `status:'proposed'` + `fractal.byRunId` (the ledger row carries the patch path + evidence — no new list RPC, #18); mutations `fractal.proposals.apply`/`fractal.proposals.dismiss` transition the row `proposed→applied|dismissed`; UI = a pending-proposals count badge. **Loosening path (what keeps this FOUNDATION #1/#7-compliant — a confidence-staged brake, NOT a forbidden-zone list):** each class starts propose-only because its rollback machinery lives inside the thing being modified; per class, after a run of consecutive owner-accepted proposals with zero rollbacks (tracked in the ledger, threshold owner-set per #19 — a ceiling, reviewed by the L3 meta-pass), the class graduates to apply-with-snapshot: direct action + automatic rollback artifact + `fractal.suspend` kill-switch, proposals file retained as the rollback record. No class is permanently propose-only. **Reversibility brake (FOUNDATION #1/#7-derived, same pending ratification):** beyond the four classes, the fix lane classifies every action by REVERSIBILITY — a derived property, not a zone list: reversible (file edits in git-tracked trees under ORCA lease, additive memory writes) → act freely; irreversible (external sends, deletions outside never-delete stores, `git push`, service restarts, financial commitments) → the same proposals queue. Any fractal-initiated public push runs the PII leak-grep gate first. Enforced at the tool-permission layer — structural, not prompt obedience.
-- **Prompt doctrine ownership (NEW — closes the v1 prompt's fate):** `fractal-prompt.md` is today a dead producer — cc-bridge loads it at worker spawn then discards it (`void rulesBody`, `worker.ts:~475`). Single canonical source (#18): the plugin ships `triage-prompt.md` + `fix-prompt.md`; `fractal-prompt.md` is REWRITTEN into that pair in Drop 1 — surviving doctrine migrates (zoom levels as un-numbered prose, horizontal branch axes, the seven questions recut: triage gets the detect half FLAG-with-evidence, fix gets the act half), the skip_list dies (always-fire is structural; its "genuinely new still reflects" nuance is salvaged into the future-gating note), the "Always respond" heartbeat rule dies (liveness is infrastructure: the two-event contract — never instruct a model to emit filler), the 🌿 output-marker contract dies (the plugin parses ONE structured verdict block; no consumer parses prose; 🌿 survives only as UI chrome), SELF-HEAL Layer-1 self-introspection dies (a never-firing reflection cannot observe its own silence — that layer is owned by the loop guard + ledger + watchdog; Layers 2-4 renumber 1-3, triage probes read-only, repairs are fix-lane work), and the PREEMPT doubt-default flips from "when in doubt treat as irreversible" to "check reversibility against the live situation; create a rollback path and act; propose only when genuinely irreversible AND external" (FOUNDATION #1). Bible `verify:` asserts the pair exists and `fractal-prompt.md` is gone (#16); de-stale §5.66's Files line + the memory pointer. The dead cc-bridge loader is deleted in the first drop that touches cc-bridge core (NOT Drop 1 — zero-core rule).
+- **Prompt doctrine ownership (NEW — closes the v1 prompt's fate):** `fractal-prompt.md` is today a dead producer — tinker-bridge loads it at worker spawn then discards it (`void rulesBody`, `worker.ts:~475`). Single canonical source (#18): the plugin ships `triage-prompt.md` + `fix-prompt.md`; `fractal-prompt.md` is REWRITTEN into that pair in Drop 1 — surviving doctrine migrates (zoom levels as un-numbered prose, horizontal branch axes, the seven questions recut: triage gets the detect half FLAG-with-evidence, fix gets the act half), the skip_list dies (always-fire is structural; its "genuinely new still reflects" nuance is salvaged into the future-gating note), the "Always respond" heartbeat rule dies (liveness is infrastructure: the two-event contract — never instruct a model to emit filler), the 🌿 output-marker contract dies (the plugin parses ONE structured verdict block; no consumer parses prose; 🌿 survives only as UI chrome), SELF-HEAL Layer-1 self-introspection dies (a never-firing reflection cannot observe its own silence — that layer is owned by the loop guard + ledger + watchdog; Layers 2-4 renumber 1-3, triage probes read-only, repairs are fix-lane work), and the PREEMPT doubt-default flips from "when in doubt treat as irreversible" to "check reversibility against the live situation; create a rollback path and act; propose only when genuinely irreversible AND external" (FOUNDATION #1). Bible `verify:` asserts the pair exists and `fractal-prompt.md` is gone (#16); de-stale §5.66's Files line + the memory pointer. The dead tinker-bridge loader is deleted in the first drop that touches tinker-bridge core (NOT Drop 1 — zero-core rule).
- **Fix-lane contract (verify-before-and-after):** `act` requires falsifiable evidence incl. a verbatim quote from disk; the plugin re-verifies the quote ON DISK before spawning the expensive fix (stale/already-fixed re-flags — structurally guaranteed, the fork never sees the fix lane's edits — converge to one file read + `abstained`); post-edit, the cheapest scoped verifier runs and is stamped on the record (vitest with zero matched tests = `kind:'none'`, never `passed:true`; `tsgo:extensions` judged by touched-path baseline diff — it exits non-zero on pre-existing drift).
-- **Transport + model (coupled config):** `triageArm: 'fork-warm'|'observer'|'cold'` + `fixModel` (default = main model). Validation: fork-warm LOCKS model = parent (**caches are per-model — fork + model switch = silent cold re-prefill**, the most likely future regression); models with `reasoning:false` invalid for thinking arms (the think-budget mapper never consults the flag). Observer arm = one persistent per-main-session observer via `api.runtime.subagent.run` with a plugin-minted stable key (dreaming pattern, zero cc-bridge changes) fed per-turn delta digests — the only arm that reliably docks in seconds. Fork-lane workers get one-shot eviction (no 15-min idle parking, `worker-pool.ts:69`).
+- **Transport + model (coupled config):** `triageArm: 'fork-warm'|'observer'|'cold'` + `fixModel` (default = main model). Validation: fork-warm LOCKS model = parent (**caches are per-model — fork + model switch = silent cold re-prefill**, the most likely future regression); models with `reasoning:false` invalid for thinking arms (the think-budget mapper never consults the flag). Observer arm = one persistent per-main-session observer via `api.runtime.subagent.run` with a plugin-minted stable key (dreaming pattern, zero tinker-bridge changes) fed per-turn delta digests — the only arm that reliably docks in seconds. Fork-lane workers get one-shot eviction (no 15-min idle parking, `worker-pool.ts:69`).
- **Pre-build decision gate (Drop 0, replaces the post-build probe):** `--fork-session` exists NOWHERE in shipped code (grep zero; only `--resume` at `worker.ts:495-500`) — A2's honest footprint is ~6 touchpoints mirroring the `__openclawThinkLevel` chain, and the warm-fork economics are unknowable from code. Before building A2: zero-spend mining of `~/.openclaw/agents/main/sessions/*.jsonl` cacheRead ratios + a standalone 3-arm probe (`claude --resume --fork-session -p … --output-format json` against `session-map.json`; arms: warm-opus-fork / cold / fork+cheaper-model). Kill rules: fork shows cacheWrite≈prefix → warm premise dead → ship cold arm (equal cost, zero core risk). Byte-stable prefix check: capture a live worker's argv verbatim from `/proc//cmdline`, never re-implement the assembly.
- **Cross-consumer hygiene (NEW):** `agent_end` has THREE source subscribers; tinkerclaw-prefrontal's is LIVE and would stomp the active-main recipe-panel state every time a fractal lane ends. Add ONE shared optional run-kind discriminator to the core hook ctx (both emit sites — `attempt.ts` and `cli-runner.ts` diverge in payload semantics; tolerate both) + one-line skips in prefrontal, skill-workshop, memory-lancedb. Per #18 the run-kind concept gets ONE canonical predicate: a single exported helper (`isFractalRun(ctx)`) + the `fractal-reflection:` prefix as a single exported constant, both in one shared module; the consumer skips AND the plugin's L1/L2 reads route through it — consumers never inline the prefix string (the plugin's runId Set stays private: it answers "did I spawn this run", an ownership guard, not the classifier). Drop-1 interim (the discriminator needs core emit-site edits, which Drop 1 excludes): the skips call the same helper in prefix-only mode. Canonical home: `subagents-and-recipes.md`, with a `verify:` that fails on any literal `fractal-reflection:` outside the canonical module.
- **Multi-scale (the paper's §9, staged):** Canonical level ladder (the ONLY "Level" taxonomy, per #18 — paper v8 §9 and the v3 prompts both index by the OBJECT observed): **L0 GROUNDING** (owned-knowledge vs answer; the `gap` verdict), **L1 OUTCOME** (the turn's concrete result), **L2 PROCESS** (this episode's trace; = every-turn triage), **L3 META-PATTERN** (cross-episode recurrence from ledger rows; = nightly pass; worldview/assumption revision is an L3 adapt-target, not a fifth rung). The v1 prompt's "Level 1-4 (thing/pattern/system/worldview)" numbering is RETIRED; the zoom rhetoric survives as un-numbered prose inside the L2/L3 instructions. The killed-by-review path was a unified `reflect(scope)` engine (recreates the monolith-prompt failure; plumbing is already scale-agnostic — add only an optional `scope:'turn'` field to the run identity now). What ships instead, in order: **gap verdict** (paper L0 post-hoc: triage checks owned-knowledge grounding against a digest the PLUGIN assembles — the retrieval pack is pinned at worker spawn and a fork child does NOT inherit it, carry it explicitly; gap-fix re-searches `recall`/engram, additive writes only) → **staleness scan** (after each fix, grep configured `artifactRoots` — bible + papers + workspace notes — for documented claims the finding contradicts; repair under the same safety machinery; acceptance = `bible:invariants`) → **write-back** (tactical notes on `acted` into dated episodic memory; engram seam deferred — engram has zero plugin-SDK surface) → **L3 nightly meta-pass** (managed cron ~04:30 reading LEDGER SLICES never transcripts; spots recurring finding-classes; ordinary fixes autonomous, self-mods → the proposals queue; "fix the column, not the cell" — needs weeks of ledger rows first) → **deliberation seam** (`hard:boolean` tag now; `fork.reasoning.search` wiring gated on a live A/B — the RPC is ungated-but-cold-spawning and its `parentSessionKey` is hardcoded `agent:main:main`).
- **UI (don't-regress):** add `"fractal"` to `KNOWN_STREAMS` (`app.ts:4022`) — else the 06-11 unknown-stream fallback renders a raw `[fractal]` bubble; renderer body lives in a NEW one-concern module `tinker-ui/src/fractal-dock.ts` (+ test), mirroring the 2026-06-10 `sectioned-reply.ts` extraction (principles #1/#6) — `app.ts` keeps ONLY the `KNOWN_STREAMS` entry, a one-line dispatch into the module, and the dock-attach lookup that reads app.ts-owned message state (#18 colocation); **emit the event envelope under the MAIN session's sessionKey** (all stream consumers are sessionKey-gated return-early; lane runIds ride in `data`). Dock via `_fractalParentRunId`-tagged message (the `_reasoningRunId`/`_subagentId` precedent — answer bubbles carry NO runId in the DOM). Re-pinned anchors: stream handlers `app.ts:~3158-3309`, legacy fractal blocks `5888-5909`/`6066-6085`, `detectFileChange` `sectioned-reply.ts:254-268` (it moved INTO sectioned-reply with its tests). Legacy in-band render stays toggle-default-off until Drop-1 smoke, deleted Drop 1.1.
-- **Deploy (REVISED — staged drops, kill switch):** **Drop 1 touches ZERO core code** (the agent RPC already accepts `{message,sessionKey,lane,thinking,deliver:false,idempotencyKey,label}`; `AgentEventStream` is open — no union edit): cold-arm triage-only + ledger + governor + UI dock. Manifest `enabledByDefault:false` (inverts the plan's enable-by-default); go-live = one witnessed `plugins.entries` flag flip strictly after smoke — **the flip is Drop 1's EXIT CRITERION: Drop 1 is not DONE until it happens** (default-off is kill-switch staging per FOUNDATION #1, never an end-state). Any drop touching cc-bridge core gets its OWN build-to-completion → healthz 200 → clean-main-turn gate (the nightly fork-sync cron rebuilds the live repo — committed core code deploys unverified otherwise). `fractal.suspend`/`fractal.resume` RPCs, paired (#11) with a **`fractal.status`** read returning the live control state every transition mutates: `{enabled, suspended, suspendReason, breakerState(+opensAtTs), governorMode, derivedPressure, bucketRemaining}` — the one-shot `suspended` event is notification, NOT the probe; a late-connecting client queries `fractal.status`, which is also the data source for the deferred panel chip. Suspension persists in plugin state — restart does NOT re-arm.
+- **Deploy (REVISED — staged drops, kill switch):** **Drop 1 touches ZERO core code** (the agent RPC already accepts `{message,sessionKey,lane,thinking,deliver:false,idempotencyKey,label}`; `AgentEventStream` is open — no union edit): cold-arm triage-only + ledger + governor + UI dock. Manifest `enabledByDefault:false` (inverts the plan's enable-by-default); go-live = one witnessed `plugins.entries` flag flip strictly after smoke — **the flip is Drop 1's EXIT CRITERION: Drop 1 is not DONE until it happens** (default-off is kill-switch staging per FOUNDATION #1, never an end-state). Any drop touching tinker-bridge core gets its OWN build-to-completion → healthz 200 → clean-main-turn gate (the nightly fork-sync cron rebuilds the live repo — committed core code deploys unverified otherwise). `fractal.suspend`/`fractal.resume` RPCs, paired (#11) with a **`fractal.status`** read returning the live control state every transition mutates: `{enabled, suspended, suspendReason, breakerState(+opensAtTs), governorMode, derivedPressure, bucketRemaining}` — the one-shot `suspended` event is notification, NOT the probe; a late-connecting client queries `fractal.status`, which is also the data source for the deferred panel chip. Suspension persists in plugin state — restart does NOT re-arm.
### 5.68 Clickable Filesystem Path Links (2026-04-19, **server handler restored 2026-05-09 after upstream-merge wipe**)
@@ -155,8 +157,8 @@ extensions/hippocampus/ ← Fork-only plugin stub
- **Status:** `DEPLOYED`
- **What:** Provider errors (400 auth, 429 rate, 500 overload, 401 subscription-exhausted, etc.) are emitted as a single assistant text payload `__ERR_ENV__:{...JSON envelope}`. The UI detects the sentinel with a brace-matched parser and renders a red/orange bubble with one stable icon per category (💳 subscription, 💸 billing, 🔐 auth, 🚦 rate_limit, 🌊 overload, 📡 network, ⏱️ timeout, 🔄 lane_busy, ⏳ reply_run_already_active, 🫥 incomplete_turn, 🔧 tool_error, 🧹 compaction_error, ⚠️ generic) and the full error detail (raw message, provider, model, duration, classification). Fatal=red, recoverable=orange.
-- **cc-bridge integration:** on claude subprocess error or non-zero result, `stream.ts` RESETS accumulated text and emits the envelope as the sole final message — no markdown-emphasis-strip risk from the `__ERR_ENV__` underscores.
-- **Files:** `src/fork/error-envelope.ts` (classifier + icon table + builder), `extensions/tinkerclaw-cc-bridge/src/stream.ts` (envelope emission paths), `tinker-ui/src/app.ts` (`extractEnvelope`, `renderEnvelopeBubble`), `tinker-ui/src/styles/base.css` (envelope-fatal, envelope-recoverable).
+- **tinker-bridge integration:** on claude subprocess error or non-zero result, `stream.ts` RESETS accumulated text and emits the envelope as the sole final message — no markdown-emphasis-strip risk from the `__ERR_ENV__` underscores.
+- **Files:** `src/fork/error-envelope.ts` (classifier + icon table + builder), `extensions/tinkerclaw-tinker-bridge/src/stream.ts` (envelope emission paths), `tinker-ui/src/app.ts` (`extractEnvelope`, `renderEnvelopeBubble`), `tinker-ui/src/styles/base.css` (envelope-fatal, envelope-recoverable).
### 5.70 Stale ReplyRunRegistry Force-Clear (2026-04-19)
@@ -182,12 +184,12 @@ extensions/hippocampus/ ← Fork-only plugin stub
- **Status:** `DEPLOYED`
- **What:** The `anatomy-timeline.db` ingestion path went dark from 2026-04-15 → 2026-04-20 because the 309-commit upstream merge (`378684e4f5`) stripped the fork's `attempt-hooks` call site out of `attempt.ts`, and the `jarvis-working` baseline branch was created from `4a6a289d5a` — before the last auto-wiring commit (`d941184bad`) could re-apply it. Result: zero rows inserted for any provider for 5 days, even as turns completed successfully and the UI looked healthy.
-- **Fix:** re-import `onTurnComplete` from `src/fork/attempt-hooks.js` and invoke it fire-and-forget right after the `llm_output` hook block, before `buildAttemptReplayMetadata`. This single call writes the anatomy row, triggers forensic dump, and does post-turn bookkeeping — persona injection / mid-context reinject / intercept-text-tool-call were intentionally NOT restored (cc-bridge bypasses them; they're only needed for local ollama/lmstudio/vllm).
+- **Fix:** re-import `onTurnComplete` from `src/fork/attempt-hooks.js` and invoke it fire-and-forget right after the `llm_output` hook block, before `buildAttemptReplayMetadata`. This single call writes the anatomy row, triggers forensic dump, and does post-turn bookkeeping — persona injection / mid-context reinject / intercept-text-tool-call were intentionally NOT restored (tinker-bridge bypasses them; they're only needed for local ollama/lmstudio/vllm).
- **Verification:** `tinker-probe` post-turn shows `timeline-db: total=4519 newThisTurn=1, last row provider=claude-code model=claude-opus-4-7`.
- **Known gap:** `response_thinking_tokens` / `response_text_tokens` / `response_tool_call_tokens` are still null — these columns have been null across all 4518 historic rows, so the capture-side wiring in `embedded-agent-subscribe` + `attempt-hooks.updateAnatomyResponse` needs a separate pass (tracked, not fixed here).
- **Files:** `src/agents/embedded-agent-runner/run/attempt.ts` (import + call site, commit `7eccc0fe6d`).
-### 5.73 cc-bridge System-Prompt Fingerprint — The Subscription-Billing Boundary (2026-04-24)
+### 5.73 tinker-bridge System-Prompt Fingerprint — The Subscription-Billing Boundary (2026-04-24)
- **Status:** `DEPLOYED` (commit `a307dca393`)
- **Why this entry exists:** after the 2026-04-20 regression Jarvis started getting `API Error 400 "out-of-extra-usage"` on every turn even though the Claude Max subscription had ~5% usage. We spent two sessions patching environment variables, cgroup paths, PPID lineage, and systemd-run flags before discovering the boundary is neither env nor cgroup — **it's prompt content**. This section documents what the boundary actually is so the next regression gets caught in minutes instead of days.
@@ -198,7 +200,7 @@ extensions/hippocampus/ ← Fork-only plugin stub
#### What actually makes a request "blocked" vs "allowed"
-| Signal | Observed impact on routing | Status in cc-bridge |
+| Signal | Observed impact on routing | Status in tinker-bridge |
| ----------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ANTHROPIC_API_KEY` / `ANTHROPIC_AUTH_TOKEN` in env | Bypasses OAuth → billed to API key, not subscription | Stripped by `worker.ts` (allowlist env) |
| `CLAUDE_AI_SESSION_KEY` in env | Same — claude-cli prefers it over OAuth creds | Stripped |
@@ -248,7 +250,7 @@ Conclusion: **the OAuth credential pipeline is healthy.** Reauth concerns, proac
#### Fix
-In `extensions/tinkerclaw-cc-bridge/src/worker.ts`:
+In `extensions/tinkerclaw-tinker-bridge/src/worker.ts`:
```ts
const OPENCLAW_SYSPROMPT_CUTOFF = "You are a personal assistant running inside OpenClaw";
@@ -266,7 +268,7 @@ Dropped: everything after the sentinel (OpenClaw tool catalog, CLI quick-referen
No functional loss: `claude-cli` maintains its own tool catalog via `--permission-mode` and the fork's OpenClaw tools are mediated by the bridge, not the subprocess. The persona block still carries Jarvis's identity.
-> _Current block order (FORK 2026-05-21):_ `persona → ethical-rules → narration → subagent-helper → tool-choice → plan-tools`. The ethical-rules layer was added as a foundation between persona and the mechanics blocks. See `tool-loop.md` "combinedSystemPrompt block order" and `config-shape.md` "cc-bridge ethical-rules prompt loader" for the loader path.
+> _Current block order (FORK 2026-05-21):_ `persona → ethical-rules → narration → subagent-helper → tool-choice → plan-tools`. The ethical-rules layer was added as a foundation between persona and the mechanics blocks. See `tool-loop.md` "combinedSystemPrompt block order" and `config-shape.md` "tinker-bridge ethical-rules prompt loader" for the loader path.
#### Diagnosis kit (use this when it comes back)
@@ -281,16 +283,16 @@ No functional loss: `claude-cli` maintains its own tool catalog via `--permissio
- Don't touch the `systemd-run --user --pipe` wrapper first. It's correct cgroup/PPid hygiene and costs nothing to keep.
- Don't strip more env vars on a hunch — the allowlist is already minimal. If stripping helped it would show up in the bare-shell probe.
- Don't blame budget-panel's `forceRefreshToken()` unless journalctl actually shows rotation events in the failure window.
-- Don't assume `--resume ` is tainting routing — wipe `~/.openclaw/cc-bridge/session-map.json`, restart, and confirm a truly fresh spawn still fails before going down that path.
+- Don't assume `--resume ` is tainting routing — wipe `~/.openclaw/tinker-bridge/session-map.json`, restart, and confirm a truly fresh spawn still fails before going down that path.
-- **Files:** `extensions/tinkerclaw-cc-bridge/src/worker.ts` (commit `a307dca393`).
-- **Knowledge:** `~/.openclaw/workspace/memory/knowledge/tinkerclaw-cc-bridge.md` (§ "2026-04-24: Subscription-billing regression — root cause").
+- **Files:** `extensions/tinkerclaw-tinker-bridge/src/worker.ts` (commit `a307dca393`).
+- **Knowledge:** `~/.openclaw/workspace/memory/knowledge/tinkerclaw-tinker-bridge.md` (§ "2026-04-24: Subscription-billing regression — root cause").
#### 5.73a Stream parser: post-tool text recovery (2026-04-27)
- **Status:** `DEPLOYED`
- **Symptom:** every `/new` (and any tool-heavy turn) appeared to "die after N tool calls" — the UI showed user prompt + tool bubbles + the brief opener Jarvis writes before the first tool, then nothing. The Morning Briefing, the post-tool summary, the actual answer — all gone. Refreshing didn't help; the assistant message persisted in `agent:main:main`'s jsonl was only the 122-char preamble even though claude-cli had emitted ~5.5KB of output.
-- **Root cause:** two related parser bugs in `extensions/tinkerclaw-cc-bridge/src/stream.ts`.
+- **Root cause:** two related parser bugs in `extensions/tinkerclaw-tinker-bridge/src/stream.ts`.
1. **Multi-block accumulation gap.** The parser tracked one `accumulatedText` for the whole turn and gated updates on `cumulative.startsWith(accumulatedText)`. claude-cli's stream-json emits SEPARATE text blocks before and after a tool_use chain (`message.content[0]` = preamble, `message.content[N]` = post-tool summary). Block N's cumulative didn't start with block 0's accumulated text, so the prefix check fell through and no delta was pushed — the post-tool block was silently dropped.
2. **`result.result` ignored on success.** claude-cli's stream emits a `result` line at the end of each turn whose `result` field carries the FULL final assistant text. The error-path code at line 595 already used it (to build the error envelope), but the success-path at line 626 just called `buildContent()` over `accumulatedText` and trusted whatever streamed through. In dense tool chains where the post-tool summary never appears as a separate `assistant.content` text block, `result.result` was the only source of the answer — and the success path was throwing it away.
- **Fix:**
@@ -301,60 +303,60 @@ No functional loss: `claude-cli` maintains its own tool catalog via `--permissio
- Not a workaround for `/new`-specific behaviour. The bug applied to ANY turn where claude-cli ran tool calls; `/new`'s briefing just made it most visible because the prompt always produces a substantial post-tool summary.
- Not the same as §5.74's tool-call replay. That fix landed tool_use/tool_result entries in the jsonl; this fix lands the assistant TEXT in the jsonl. They compose: `/new` history now shows user prompt → tool bubbles (from §5.74) → preamble + briefing (from §5.73a).
- Not retroactive — turns persisted before this commit are stuck with whatever truncated text made it. Future turns are whole.
-- **Files:** `extensions/tinkerclaw-cc-bridge/src/stream.ts` (per-block maps, multi-block parser branches, `result.result` reconciliation in the success path).
+- **Files:** `extensions/tinkerclaw-tinker-bridge/src/stream.ts` (per-block maps, multi-block parser branches, `result.result` reconciliation in the success path).
-**§5.66b cc-bridge idle-watchdog timeout bumped to 600s (FORK 2026-05-05).**
+**§5.66b tinker-bridge idle-watchdog timeout bumped to 600s (FORK 2026-05-05).**
- **Status:** `DEPLOYED`
- **Symptom:** A heavy WhatsApp ask ("Read outlook + list project state") surfaced as `🤖 ⚠️ Something went wrong while processing your request.` Both first AND retry attempts SIGTERMed at ~128s; journal showed `[llm-idle-timeout] claude-code/claude-opus-4-7 produced no reply before the idle watchdog`.
-- **Root cause:** pi-agent-core's `streamWithIdleTimeout` (`src/agents/embedded-agent-runner/run/llm-idle-timeout.ts`) resets per pi-ai stream event. cc-bridge intentionally does NOT push `stream` events during tool work — tool_use blocks would trigger re-execution via OpenClaw's exec tool (see FORK 2026-04-22 in stream.ts). On a long claude-cli tool chain (e.g. several outlook-mail-fetch + people.read calls in series), no pi-ai events flow → idle timer ticks past 120s default → subprocess SIGTERMed mid-work. Both retries hitting this surface as `surface_error/timeout`.
-- **Fix:** provider-level `timeoutSeconds: 600` in `extensions/tinkerclaw-cc-bridge/src/catalog.ts:buildClaudeCodeProviderConfig`. pi-agent-core's `resolveLlmIdleTimeoutMs` ONLY reads `providerConfig.timeoutSeconds` (via `applyConfiguredProviderOverrides → resolveProviderRequestTimeoutMs`); a `requestTimeoutMs` field on the catalog model object is silently ignored. The new constant `DEFAULT_REQUEST_TIMEOUT_MS = 600_000` lives in `defaults.ts`.
+- **Root cause:** pi-agent-core's `streamWithIdleTimeout` (`src/agents/embedded-agent-runner/run/llm-idle-timeout.ts`) resets per pi-ai stream event. tinker-bridge intentionally does NOT push `stream` events during tool work — tool_use blocks would trigger re-execution via OpenClaw's exec tool (see FORK 2026-04-22 in stream.ts). On a long claude-cli tool chain (e.g. several outlook-mail-fetch + people.read calls in series), no pi-ai events flow → idle timer ticks past 120s default → subprocess SIGTERMed mid-work. Both retries hitting this surface as `surface_error/timeout`.
+- **Fix:** provider-level `timeoutSeconds: 600` in `extensions/tinkerclaw-tinker-bridge/src/catalog.ts:buildClaudeCodeProviderConfig`. pi-agent-core's `resolveLlmIdleTimeoutMs` ONLY reads `providerConfig.timeoutSeconds` (via `applyConfiguredProviderOverrides → resolveProviderRequestTimeoutMs`); a `requestTimeoutMs` field on the catalog model object is silently ignored. The new constant `DEFAULT_REQUEST_TIMEOUT_MS = 600_000` lives in `defaults.ts`.
- **Don't regress:** if you ever switch to `requestTimeoutMs` on individual models (which feels more natural), pi-agent-core won't pick it up — the override path is the provider-level field. The model object's `requestTimeoutMs` IS read further down (provider-transport-fetch.ts) but only AFTER the provider config has populated it via `applyConfiguredProviderOverrides`.
- **Open follow-up (proper fix):** stream.ts should push a no-op stream event (or repurpose `start`) for every claude-cli line during tool work, so the idle watchdog resets the way pi-agent-core expects rather than relying on a wider absolute timeout. The current bump masks the symptom; a tool chain >10 min would still hit it.
-**§5.73b cc-bridge truncation — `text_end` fired before tail-recover (FORK 2026-05-04).**
+**§5.73b tinker-bridge truncation — `text_end` fired before tail-recover (FORK 2026-05-04).**
- **Status:** `DEPLOYED`
-- **Symptom:** Multi-step turns where the streamed scratch text diverged from `result.result` only delivered the streamed preamble (~100 B) to the user, even though the cc-bridge logged `tail-recover: streamed 105B, result_text 2457B, replacing (diverged)` and `done.message.content` carried the full text.
+- **Symptom:** Multi-step turns where the streamed scratch text diverged from `result.result` only delivered the streamed preamble (~100 B) to the user, even though the tinker-bridge logged `tail-recover: streamed 105B, result_text 2457B, replacing (diverged)` and `done.message.content` carried the full text.
- **Root cause:** `pushTextEnd()` was called immediately after `worker.send` resolved (old line 579), BEFORE the tail-recover reconciliation block (lines 626-665). The downstream `embedded-agent-subscribe.handleMessageEnd` (`src/agents/embedded-agent-subscribe.handlers.messages.ts:841`) drained its block-chunker on `text_end` and recorded `lastBlockReplyText`. The late `text_delta` from the tail-recover then arrived AFTER `text_end` (a protocol violation), and the message_end safety re-send was guarded by `lastBlockReplyText != null`, silently dropping the actual answer.
- **Fix:** removed the early `pushTextEnd()`; added it just before each `done` push (success path AFTER the tail-recover, error path AFTER the envelope reset; catch block already had it). Net: `text_end` always carries the FINAL accumulated text.
- **Don't regress:** `pushTextEnd` is guarded by `if (!textStarted || textEnded) return;`, so call sites are idempotent — if you ADD a new exit path that emits `done`, you also need a `pushTextEnd()` immediately before it.
-- **Files:** `extensions/tinkerclaw-cc-bridge/src/stream.ts`.
+- **Files:** `extensions/tinkerclaw-tinker-bridge/src/stream.ts`.
-### 5.74 cc-bridge Tool Call Replay in Session History (2026-04-25)
+### 5.74 tinker-bridge Tool Call Replay in Session History (2026-04-25)
- **Status:** `DEPLOYED`
-- **Why:** §5.66 explains that cc-bridge cannot put `tool_use` blocks in the assistant message — pi-agent-core would re-execute them through OpenClaw's exec tool and trip the prefrontal "Exploration required" gate (red bubbles for every claude internal Bash call). That kept context clean but left the OpenClaw session transcript with **only** the user prompt and the final assistant text. Reloading `agent:main:main` after a 46-tool-call turn showed `[user prompt → 64-character opener]` and nothing else — every command Jarvis ran was invisible after refresh, which broke the "see Jarvis working" promise of the webchat.
+- **Why:** §5.66 explains that tinker-bridge cannot put `tool_use` blocks in the assistant message — pi-agent-core would re-execute them through OpenClaw's exec tool and trip the prefrontal "Exploration required" gate (red bubbles for every claude internal Bash call). That kept context clean but left the OpenClaw session transcript with **only** the user prompt and the final assistant text. Reloading `agent:main:main` after a 46-tool-call turn showed `[user prompt → 64-character opener]` and nothing else — every command Jarvis ran was invisible after refresh, which broke the "see Jarvis working" promise of the webchat.
- **Fix:** three-piece pipeline that lands tool events in the transcript without polluting the LLM context.
- 1. **Buffer (cc-bridge):** `extensions/tinkerclaw-cc-bridge/src/tool-buffer.ts` keeps an in-process `Map`. Both `emitToolStart` and `emitToolResult` in `stream.ts` push their events into the buffer alongside the existing live `emitAgentEvent` call. Buffer state is not persisted to disk — gateway crash loses it, but the user already lost the turn at that point.
- 2. **Drain (fork hook):** `src/fork/attempt-hooks.ts:onTurnComplete` resolves the active `SessionManager` (the runner casts `activeSession` to `SessionManager`, but the real instance is on `activeSession.sessionManager`, which the hook discovers defensively) and writes each buffered event with `appendCustomEntry("cc-bridge-tool", { runId, ...event })`. `appendCustomEntry` is the "Extension state — not in context" primitive (per `pi-coding-agent` `session.md`), so the entries persist on disk and ship to Tinker via `chat.history`, but pi-agent-core does NOT replay them into the message array on the next turn — no double-execution.
- 3. **Surface (chat.history transform):** `src/gateway/session-utils.fs.ts:readSessionMessages` recognises `type:"custom"` + `customType:"cc-bridge-tool"` entries and emits them as synthetic `tool_use` (assistant role) and `tool_result` (user role) messages with `__openclaw.kind:"cc-bridge-tool"`. They reuse the exact block types Tinker already renders for live tool events at `tinker-ui/src/app.ts:1512`, so no client-side change is needed — the existing live-tool render path runs again on history load.
-- **Reorder logic:** the drain runs in `onTurnComplete`, AFTER the assistant text was already persisted, so cc-bridge-tool entries trail the assistant message in jsonl order. `reorderCcBridgeToolBlocks` walks the read messages and splices each cc-bridge-tool message into the position immediately before the most-recent preceding assistant _text_ message, ignoring intervening `compaction` system entries. Final chat reading order: `[user → tool_use → tool_result → … → assistant text → compaction]`.
+ 1. **Buffer (tinker-bridge):** `extensions/tinkerclaw-tinker-bridge/src/tool-buffer.ts` keeps an in-process `Map`. Both `emitToolStart` and `emitToolResult` in `stream.ts` push their events into the buffer alongside the existing live `emitAgentEvent` call. Buffer state is not persisted to disk — gateway crash loses it, but the user already lost the turn at that point.
+ 2. **Drain (fork hook):** `src/fork/attempt-hooks.ts:onTurnComplete` resolves the active `SessionManager` (the runner casts `activeSession` to `SessionManager`, but the real instance is on `activeSession.sessionManager`, which the hook discovers defensively) and writes each buffered event with `appendCustomEntry("tinker-bridge-tool", { runId, ...event })`. `appendCustomEntry` is the "Extension state — not in context" primitive (per `pi-coding-agent` `session.md`), so the entries persist on disk and ship to Tinker via `chat.history`, but pi-agent-core does NOT replay them into the message array on the next turn — no double-execution.
+ 3. **Surface (chat.history transform):** `src/gateway/session-utils.fs.ts:readSessionMessages` recognises `type:"custom"` + `customType:"tinker-bridge-tool"` entries and emits them as synthetic `tool_use` (assistant role) and `tool_result` (user role) messages with `__openclaw.kind:"tinker-bridge-tool"`. They reuse the exact block types Tinker already renders for live tool events at `tinker-ui/src/app.ts:1512`, so no client-side change is needed — the existing live-tool render path runs again on history load.
+- **Reorder logic:** the drain runs in `onTurnComplete`, AFTER the assistant text was already persisted, so tinker-bridge-tool entries trail the assistant message in jsonl order. `reorderTinkerBridgeToolBlocks` walks the read messages and splices each tinker-bridge-tool message into the position immediately before the most-recent preceding assistant _text_ message, ignoring intervening `compaction` system entries. Final chat reading order: `[user → tool_use → tool_result → … → assistant text → compaction]`.
- **Verification:**
- Live: jarvis-inject probe runs `pwd` + `whoami`, three blocks visible in real time (existing §5.6 path).
- - Persist: jsonl gains 4 `{"type":"custom","customType":"cc-bridge-tool", "data":{ runId, phase, toolCallId, name, args, result, isError, ... }}` lines per turn.
+ - Persist: jsonl gains 4 `{"type":"custom","customType":"tinker-bridge-tool", "data":{ runId, phase, toolCallId, name, args, result, isError, ... }}` lines per turn.
- Replay: `chat.history` returns those entries as `[assistant tool_use, user tool_result]` pairs spliced before the final assistant text. Tinker renders them as the same single-line/expandable bubbles it shows live.
- **What this is NOT:**
- - Not "in context" — pi-agent-core never re-feeds these entries to the LLM. cc-bridge handles its own tool loop inside claude-cli (`~/.claude/projects//*.jsonl`); the OpenClaw side just keeps a render-only mirror.
+ - Not "in context" — pi-agent-core never re-feeds these entries to the LLM. tinker-bridge handles its own tool loop inside claude-cli (`~/.claude/projects//*.jsonl`); the OpenClaw side just keeps a render-only mirror.
- Not retroactive — turns that ran before this fix have no buffered events to drain. Their history still shows only the assistant opener + final text. Future turns are fully captured.
- Not a replacement for `agent.stream:"tool"` events — the live WS path still drives Tinker's real-time tool bubbles. The persistence path is a parallel record for after-the-fact reload, not a substitute.
-- **Files:** `extensions/tinkerclaw-cc-bridge/src/tool-buffer.ts` (new), `extensions/tinkerclaw-cc-bridge/src/tool-buffer.types.ts` (new), `extensions/tinkerclaw-cc-bridge/src/stream.ts` (record at emit), `src/fork/attempt-hooks.ts` (drain on turn complete), `src/gateway/session-utils.fs.ts` (history transform + reorder), `scripts/check-history-probe.mjs` (verification harness).
+- **Files:** `extensions/tinkerclaw-tinker-bridge/src/tool-buffer.ts` (new), `extensions/tinkerclaw-tinker-bridge/src/tool-buffer.types.ts` (new), `extensions/tinkerclaw-tinker-bridge/src/stream.ts` (record at emit), `src/fork/attempt-hooks.ts` (drain on turn complete), `src/gateway/session-utils.fs.ts` (history transform + reorder), `scripts/check-history-probe.mjs` (verification harness).
-### 5.75 `/clear` and `/new` — Symmetric Reset Cascade Through cc-bridge (2026-04-27)
+### 5.75 `/clear` and `/new` — Symmetric Reset Cascade Through tinker-bridge (2026-04-27)
- **Status:** `DEPLOYED`
-- **Why:** §5.5 says session deletes from Tinker are soft (transcript archived, never wiped). §5.66 explains cc-bridge's worker pool is keyed by a hash of the system prompt, which has the side effect of pinning Jarvis to the same `claude --resume ` across every "session reset" — `/new` rotated the OpenClaw sessionId but cc-bridge kept the old conversation alive. `/clear` was even worse: it never reached the gateway's reset path at all (it called `sessions.delete` instead of `sessions.reset`), so the `command:reset` plugin lifecycle, transcript archival, and `session-memory` save never fired. The two commands looked similar in the UI but did very different things server-side, and neither actually reset Jarvis.
+- **Why:** §5.5 says session deletes from Tinker are soft (transcript archived, never wiped). §5.66 explains tinker-bridge's worker pool is keyed by a hash of the system prompt, which has the side effect of pinning Jarvis to the same `claude --resume ` across every "session reset" — `/new` rotated the OpenClaw sessionId but tinker-bridge kept the old conversation alive. `/clear` was even worse: it never reached the gateway's reset path at all (it called `sessions.delete` instead of `sessions.reset`), so the `command:reset` plugin lifecycle, transcript archival, and `session-memory` save never fired. The two commands looked similar in the UI but did very different things server-side, and neither actually reset Jarvis.
- **Fix (three pieces, must move together):**
- 1. **Smuggle the OpenClaw sessionId** to cc-bridge: `src/agents/embedded-agent-runner/run/attempt.ts` adds `__openclawSessionId: params.sessionId` next to the existing `__openclawRunId` / `__openclawSessionKey` fields piped through `agent.streamFn`. The sessionId is the per-reset UUID minted by `performGatewaySessionReset`, not the stable `sessionKey` label.
- 2. **Hash sessionId into the cc-bridge worker key**: `extensions/tinkerclaw-cc-bridge/src/stream.ts:deriveSessionKey` now takes `(explicit, systemPrompt, openclawSessionId)` and djb2-hashes `${systemPrompt}\u0001${sessionId}`. A reset that mints a new sessionId yields a new `cc-sp-`; the worker pool treats it as a brand-new session, spawns a fresh `claude` subprocess with no `--resume` flag, and `~/.openclaw/cc-bridge/session-map.json` accumulates a fresh entry instead of looking up the old one. The previous entry stays in the file as orphaned state — never queried again, cheap to leave; a TTL prune is future work.
+ 1. **Smuggle the OpenClaw sessionId** to tinker-bridge: `src/agents/embedded-agent-runner/run/attempt.ts` adds `__openclawSessionId: params.sessionId` next to the existing `__openclawRunId` / `__openclawSessionKey` fields piped through `agent.streamFn`. The sessionId is the per-reset UUID minted by `performGatewaySessionReset`, not the stable `sessionKey` label.
+ 2. **Hash sessionId into the tinker-bridge worker key**: `extensions/tinkerclaw-tinker-bridge/src/stream.ts:deriveSessionKey` now takes `(explicit, systemPrompt, openclawSessionId)` and djb2-hashes `${systemPrompt}\u0001${sessionId}`. A reset that mints a new sessionId yields a new `tinker-sp-`; the worker pool treats it as a brand-new session, spawns a fresh `claude` subprocess with no `--resume` flag, and `~/.openclaw/tinker-bridge/session-map.json` accumulates a fresh entry instead of looking up the old one. The previous entry stays in the file as orphaned state — never queried again, cheap to leave; a TTL prune is future work.
3. **`/clear` calls `sessions.reset`**: `tinker-ui/src/app.ts` replaces the old `sessions.delete({deleteTranscript:false})` fire-and-forget with `sessions.reset({key, reason:"reset"})`. The local-state wipe + tab key rotation still happen first (so a stale `chat.history` reload mid-flight can't surface the abandoned transcript), then the server-side cascade handles the real work: archives the old transcript to `sessions-archive/`, mints a new sessionId on the same sessionKey, fires `command:reset` → `before_reset` → `session_end` → `session_start` plugin hooks. Zero LLM calls; same zero-token guarantee as before. Symmetric `sessions.reset({key, reason:"new"})` is also fired from the topbar `/new` button when the active tab is a `tinker:*` non-main tab — previously its old session was orphaned on disk because the tab simply rotated to a fresh key without telling the gateway anything.
- **Verification (commit ``):**
- - `scripts/check-reset-cascade.mjs` opens a fresh `cli:reset-` sessionKey, sends `echo TURN-A`, calls `sessions.reset`, sends `echo TURN-B`. Both turns succeed; `session-map.json` gains TWO new `cc-sp-` entries (one per turn) with different sessionIds; the gateway log shows both `spawning claude` lines ending at `--model claude-opus-4-7` (no `--resume` argument on either) — claude-cli started fresh both times. Before the fix the second turn would have reused the first turn's `cc-sp-` key and `--resume`-d into the same conversation.
+ - `scripts/check-reset-cascade.mjs` opens a fresh `cli:reset-` sessionKey, sends `echo TURN-A`, calls `sessions.reset`, sends `echo TURN-B`. Both turns succeed; `session-map.json` gains TWO new `tinker-sp-` entries (one per turn) with different sessionIds; the gateway log shows both `spawning claude` lines ending at `--model claude-opus-4-7` (no `--resume` argument on either) — claude-cli started fresh both times. Before the fix the second turn would have reused the first turn's `tinker-sp-` key and `--resume`-d into the same conversation.
- **What this is NOT:**
- Not a guarantee about `/new` on the main tab — `/new` still fires `chat.send "/new\n…"` to deliver the BRIEFING.md prelude through the model. The cascade still runs (the auto-reply session.ts trigger detects `/new` and calls `performGatewaySessionReset` server-side), but the LLM call is intentional and counts toward usage.
- Not retroactive — entries already in `session-map.json` from before this commit (hashed by systemPrompt only) stay readable but won't be matched by future requests, which now hash with sessionId. They become inert dead weight.
- Not a substitute for `/clear`'s tab-key rotation. The local rotation is still important: `chat.history` with the old key would still return the just-archived transcript in flight, and the tab-key swap prevents that race.
-- **Files:** `src/agents/embedded-agent-runner/run/attempt.ts` (smuggle `__openclawSessionId`), `extensions/tinkerclaw-cc-bridge/src/stream.ts` (`deriveSessionKey` hashes sessionId + reorders pipedOptions extraction so sessionId is available before key derivation), `tinker-ui/src/app.ts` (`/clear` calls `sessions.reset` instead of `sessions.delete`; topbar `/new` resets the abandoned `tinker:*` key before rotating), `scripts/check-reset-cascade.mjs` (new verification harness).
+- **Files:** `src/agents/embedded-agent-runner/run/attempt.ts` (smuggle `__openclawSessionId`), `extensions/tinkerclaw-tinker-bridge/src/stream.ts` (`deriveSessionKey` hashes sessionId + reorders pipedOptions extraction so sessionId is available before key derivation), `tinker-ui/src/app.ts` (`/clear` calls `sessions.reset` instead of `sessions.delete`; topbar `/new` resets the abandoned `tinker:*` key before rotating), `scripts/check-reset-cascade.mjs` (new verification harness).
### 5.76 Public/Private Boundary & The Git-Pull Contract (2026-04-28)
@@ -366,7 +368,7 @@ This section defines the rules that let `tinkerclaw` ship as a public GitHub rep
1. Explicit config (`~/.openclaw/openclaw.json` — outside repo)
2. Workspace file (`~/.openclaw/workspace/` — outside repo)
-3. Bundled default (`extensions/tinkerclaw-cc-bridge/{personas,prompts}/` — in repo)
+3. Bundled default (`extensions/tinkerclaw-tinker-bridge/{personas,prompts}/` — in repo)
The bundled default is always present; first-boot never hits a missing-file path. Override layers are opt-in and live outside the repo so `git pull` cannot touch them.
@@ -400,7 +402,7 @@ There is no fourth category called "ship a default but expect users to edit it i
#### 5.76d Resolution order in code
-For each bundled-default-with-override-hook file, the loader applies the same three-step resolution. Implemented as a shared helper in `extensions/tinkerclaw-cc-bridge/src/prompt-loader.ts`:
+For each bundled-default-with-override-hook file, the loader applies the same three-step resolution. Implemented as a shared helper in `extensions/tinkerclaw-tinker-bridge/src/prompt-loader.ts`:
```
resolvePromptPath(name) →
@@ -411,25 +413,25 @@ resolvePromptPath(name) →
The first existing path wins. The bundled default at step 3 is guaranteed to exist (shipped in the repo). Steps 1 and 2 are opt-in. No file-not-found errors at boot under any combination of presence/absence.
-For the persona, the resolution lives in `src/fork/attempt-hooks.ts:getPersonaBlock` which reads `/SOUL.md` first and falls back to the bundled `jarvis-default.md`. For the briefing, the resolution lives in `tinker-ui/src/app.ts:buildInjectedPrompt` which reads `briefingPath` from cc-bridge config (resolved by the gateway). For the cc-bridge prompt blocks (narration / subagent-helper / tool-choice), the resolution lives in `cc-bridge/src/worker.ts:buildAppendedPromptRules`.
+For the persona, the resolution lives in `src/fork/attempt-hooks.ts:getPersonaBlock` which reads `/SOUL.md` first and falls back to the bundled `jarvis-default.md`. For the briefing, the resolution lives in `tinker-ui/src/app.ts:buildInjectedPrompt` which reads `briefingPath` from tinker-bridge config (resolved by the gateway). For the tinker-bridge prompt blocks (narration / subagent-helper / tool-choice), the resolution lives in `tinker-bridge/src/worker.ts:buildAppendedPromptRules`.
#### 5.76e Day-0 defaults — what ships in the repo
-- **`extensions/tinkerclaw-cc-bridge/personas/jarvis-default.md`** — JARVIS persona, day-0 default. Sardonic, capable, formal-British voice; based on the canonical Iron Man character (widely known, not personal). Cloners override by creating `~/.openclaw/workspace/SOUL.md`.
-- **`extensions/tinkerclaw-cc-bridge/prompts/briefing-default.md`** — generic morning-briefing template. Pattern + voice rules + category-based source discovery (HEARTBEAT, daily memory, recent commits — skip silently if missing). Cloners override by creating `~/.openclaw/workspace/BRIEFING.md`.
-- **`extensions/tinkerclaw-cc-bridge/prompts/narration-contract.md`** — the grandma-proof bar: per-tool narration rule + banned phrasings + bad/good examples. Extracted from `worker.ts:buildChatNarrationBlock`.
-- **`extensions/tinkerclaw-cc-bridge/prompts/subagent-helper.md`** — how to spawn OpenClaw subagents from inside cc-bridge. Extracted from `worker.ts:buildSubagentHelperBlock`.
-- **`extensions/tinkerclaw-cc-bridge/prompts/tool-choice.md`** — tool-routing decision tree (WebSearch vs WebFetch, etc.). Extracted from `worker.ts:buildToolChoiceBlock`.
+- **`extensions/tinkerclaw-tinker-bridge/personas/jarvis-default.md`** — JARVIS persona, day-0 default. Sardonic, capable, formal-British voice; based on the canonical Iron Man character (widely known, not personal). Cloners override by creating `~/.openclaw/workspace/SOUL.md`.
+- **`extensions/tinkerclaw-tinker-bridge/prompts/briefing-default.md`** — generic morning-briefing template. Pattern + voice rules + category-based source discovery (HEARTBEAT, daily memory, recent commits — skip silently if missing). Cloners override by creating `~/.openclaw/workspace/BRIEFING.md`.
+- **`extensions/tinkerclaw-tinker-bridge/prompts/narration-contract.md`** — the grandma-proof bar: per-tool narration rule + banned phrasings + bad/good examples. Extracted from `worker.ts:buildChatNarrationBlock`.
+- **`extensions/tinkerclaw-tinker-bridge/prompts/subagent-helper.md`** — how to spawn OpenClaw subagents from inside tinker-bridge. Extracted from `worker.ts:buildSubagentHelperBlock`.
+- **`extensions/tinkerclaw-tinker-bridge/prompts/tool-choice.md`** — tool-routing decision tree (WebSearch vs WebFetch, etc.). Extracted from `worker.ts:buildToolChoiceBlock`.
- **JARVIS voice (`skills/jarvis-voice/SKILL.md`)** — default-on TTS skill. Cloners disable via the skills panel or replace with another voice skill.
-Day-0 user experience: clone, install, build, start. Get JARVIS speaking sardonically through cc-bridge with full grandma-proof tool narration, full briefing on `/new`, and JARVIS voice (assuming `ffmpeg` + `aplay` are present from the skill manifest).
+Day-0 user experience: clone, install, build, start. Get JARVIS speaking sardonically through tinker-bridge with full grandma-proof tool narration, full briefing on `/new`, and JARVIS voice (assuming `ffmpeg` + `aplay` are present from the skill manifest).
#### 5.76f Drift detection on startup
Each bundled default ships with a frontmatter `default-version: ` line. When a workspace override exists, the loader compares the workspace file's stamped `default-version` (written by `openclaw init`) to the current bundled `default-version`. If they differ:
```
-[cc-bridge] persona override at default-version 1.0; bundled default is at 1.2.
+[tinker-bridge] persona override at default-version 1.0; bundled default is at 1.2.
run 'openclaw persona diff' to see changes; 'openclaw persona reset' to reseed.
(your override always wins; this is informational.)
```
@@ -456,9 +458,9 @@ The CLI scaffolding is filed as a follow-up — bible §5.76g documents the shap
#### 5.76h Scenario-B trap: in-repo edits
-Some users will ignore the documented path and edit `extensions/tinkerclaw-cc-bridge/personas/jarvis-default.md` in place. `git pull` will conflict the next time upstream touches that file. The repo defends with two redundant signals:
+Some users will ignore the documented path and edit `extensions/tinkerclaw-tinker-bridge/personas/jarvis-default.md` in place. `git pull` will conflict the next time upstream touches that file. The repo defends with two redundant signals:
-1. **`pnpm doctor`** (or first-run check on gateway boot) scans `extensions/tinkerclaw-cc-bridge/{personas,prompts}/` for local modifications via `git diff --quiet` and prints:
+1. **`pnpm doctor`** (or first-run check on gateway boot) scans `extensions/tinkerclaw-tinker-bridge/{personas,prompts}/` for local modifications via `git diff --quiet` and prints:
> Your repo has edits to bundled `jarvis-default.md`. These belong in `~/.openclaw/workspace/SOUL.md`, not in the repo. Run `openclaw persona migrate` to copy your edits to the workspace and revert the repo file. Until then, `git pull` will conflict on this file.
2. **`git-hooks/pre-merge`** (opt-in via `core.hooksPath`) prints the same warning before letting a `git pull` proceed.
@@ -468,7 +470,7 @@ Some users will ignore the documented path and edit `extensions/tinkerclaw-cc-br
Two tests every change to the public-repo surface must pass:
-- **The "Sam test"** (fresh clone): a stranger named Sam clones the repo, runs `pnpm install && pnpm build && openclaw start`, says hello in Tinker. Jarvis replies through cc-bridge with the bundled persona, the bundled briefing on `/new`, the bundled voice, full grandma-proof tool narration. No setup, no errors, no missing-file references in the assistant's mouth. Sam has done nothing personal yet — everything works from the bundle.
+- **The "Sam test"** (fresh clone): a stranger named Sam clones the repo, runs `pnpm install && pnpm build && openclaw start`, says hello in Tinker. Jarvis replies through tinker-bridge with the bundled persona, the bundled briefing on `/new`, the bundled voice, full grandma-proof tool narration. No setup, no errors, no missing-file references in the assistant's mouth. Sam has done nothing personal yet — everything works from the bundle.
- **The "Day-90 test"** (existing user `git pull`): a cloner who has been using the repo for 90 days has a personalized `~/.openclaw/workspace/SOUL.md`, a custom `BRIEFING.md`, a trained `personality_*.onnx`, custom recipes under `~/.openclaw/workspace/recipes/`. They run `git pull`. The pull updates the bundled defaults, the library code, the contracts. **Their workspace is untouched.** Boot logs flag the drift between their SOUL.md (default-version 1.0) and the new bundled default (1.2); they read `openclaw persona diff` if interested; they keep their override otherwise. Total disruption: zero.
Both tests must pass by design (resolution order + filesystem separation), not by user discipline.
@@ -496,18 +498,18 @@ That's the entire list — narrow because most personalization paths route to `~
#### 5.76k Files (this contract)
-- `extensions/tinkerclaw-cc-bridge/personas/jarvis-default.md` _(NEW — day-0 persona default)_
-- `extensions/tinkerclaw-cc-bridge/prompts/briefing-default.md` _(NEW — day-0 briefing default)_
-- `extensions/tinkerclaw-cc-bridge/prompts/narration-contract.md` _(NEW — extracted from worker.ts)_
-- `extensions/tinkerclaw-cc-bridge/prompts/subagent-helper.md` _(NEW — extracted from worker.ts)_
-- `extensions/tinkerclaw-cc-bridge/prompts/tool-choice.md` _(NEW — extracted from worker.ts)_
-- `extensions/tinkerclaw-cc-bridge/src/prompt-loader.ts` _(NEW — three-step resolution helper)_
-- `extensions/tinkerclaw-cc-bridge/src/worker.ts` (use loader; remove the five hardcoded `/home//...` paths)
-- `extensions/tinkerclaw-cc-bridge/README.md` _(NEW — anatomy + override conventions)_
+- `extensions/tinkerclaw-tinker-bridge/personas/jarvis-default.md` _(NEW — day-0 persona default)_
+- `extensions/tinkerclaw-tinker-bridge/prompts/briefing-default.md` _(NEW — day-0 briefing default)_
+- `extensions/tinkerclaw-tinker-bridge/prompts/narration-contract.md` _(NEW — extracted from worker.ts)_
+- `extensions/tinkerclaw-tinker-bridge/prompts/subagent-helper.md` _(NEW — extracted from worker.ts)_
+- `extensions/tinkerclaw-tinker-bridge/prompts/tool-choice.md` _(NEW — extracted from worker.ts)_
+- `extensions/tinkerclaw-tinker-bridge/src/prompt-loader.ts` _(NEW — three-step resolution helper)_
+- `extensions/tinkerclaw-tinker-bridge/src/worker.ts` (use loader; remove the five hardcoded `/home//...` paths)
+- `extensions/tinkerclaw-tinker-bridge/README.md` _(NEW — anatomy + override conventions)_
- `extensions/tinkerclaw-browser-relay/chrome-extension/options.html` (replace literal gateway-token placeholder)
- `scripts/db-probe.mjs` (replace `/home//.openclaw/...` with `os.homedir()`-resolved path)
- `src/fork/attempt-hooks.ts` (`getPersonaBlock` falls back to bundled `jarvis-default.md`)
-- `tinker-ui/src/app.ts:buildInjectedPrompt` (briefing path resolved from cc-bridge config, falls back to bundled `briefing-default.md`)
+- `tinker-ui/src/app.ts:buildInjectedPrompt` (briefing path resolved from tinker-bridge config, falls back to bundled `briefing-default.md`)
- `FORK_SETUP.md` (new "Personalize in the workspace, never in the repo" paragraph at the top)
---
@@ -546,7 +548,7 @@ This section is the standard. Every `.md` file under `~/src/tinkerclaw/extension
6. **Add an explicit `` block** when a prompt could be overridden by user instructions. State the priority order plainly: user explicit instructions > this prompt > system defaults. Without it, 4.7 may treat the prompt as the highest authority and ignore conflicting user input.
-7. **Concrete over abstract.** Replace vague nouns ("the code", "the user", "things") with specific anchors: "the `cc-bridge` worker.ts", "the user's `~/.openclaw/workspace/`". Vague nouns let the model wander; concrete anchors keep it on the artifact you actually mean.
+7. **Concrete over abstract.** Replace vague nouns ("the code", "the user", "things") with specific anchors: "the `tinker-bridge` worker.ts", "the user's `~/.openclaw/workspace/`". Vague nouns let the model wander; concrete anchors keep it on the artifact you actually mean.
8. **Length: keep or shorten.** Don't pad. If a section can lose 30% of its words without losing meaning, do it. 4.7 reads everything literally — every word competes for attention.
@@ -601,7 +603,7 @@ If Anthropic publishes new guidance for Opus 4.8 or later, append §5.78 rather
**Rule.** The heartbeat is a **computational gate**, not an AI loop. TypeScript (not an LLM) decides every fire whether anything actually needs Jarvis's attention. The LLM is invoked only when the gate confirms there is work to do.
-The model summoned by the gate is **`claude-code/claude-opus-4-7`** — full reasoning, our flat-rate via cc-bridge.
+The model summoned by the gate is **`claude-code/claude-opus-4-7`** — full reasoning, our flat-rate via tinker-bridge.
#### 5.79a How the gate decides
@@ -660,7 +662,7 @@ The parser at `src/auto-reply/heartbeat.ts:parseHeartbeatTasks` reads this forma
#### 5.79d Common regressions and how to spot them
-- **`heartbeat.model` set to a dead/metered account** → every gate-positive fire surfaces `LLM request rejected: out of extra usage` in the gateway log. Fix: switch to a flat-rate model (cc-bridge claude-code/\* or local ollama).
+- **`heartbeat.model` set to a dead/metered account** → every gate-positive fire surfaces `LLM request rejected: out of extra usage` in the gateway log. Fix: switch to a flat-rate model (tinker-bridge claude-code/\* or local ollama).
- **`heartbeat.every` shrunk to `5m` or smaller** → more gate ticks, faster reaction to events but linear cost increase if the gate's ever wrong. Default back to `1h` unless there's a real reason.
- **HEARTBEAT.md filled with prose** that looks like tasks but isn't in the `tasks:` YAML block → parser ignores it, no gate trigger from there. Visible only in journalctl as gate-positive fires that come from cron events, not HEARTBEAT.md.
- **`session: "main"`** (the original bug) → heartbeat content contaminates main session, leaks to webchat as a red box. Always `"heartbeat"`.
@@ -695,7 +697,7 @@ This section codifies four issues diagnosed on 2026-04-29 after a session compac
#### 5.80b Standing rules
-- **Per-model `contextWindow` is mandatory** in `~/.openclaw/openclaw.json` under `models.providers..models[i]`. Do not rely on cc-bridge's `defaults.ts` fallback; runtime reads the live config first. Current values: `claude-opus-4-7: 1_000_000`, `claude-sonnet-4-6: 1_000_000`, `claude-haiku-4-5: 200_000`. After any merge that rewrites the models block, restore these values.
+- **Per-model `contextWindow` is mandatory** in `~/.openclaw/openclaw.json` under `models.providers..models[i]`. Do not rely on tinker-bridge's `defaults.ts` fallback; runtime reads the live config first. Current values: `claude-opus-4-7: 1_000_000`, `claude-sonnet-4-6: 1_000_000`, `claude-haiku-4-5: 200_000`. After any merge that rewrites the models block, restore these values.
- **A "compacted" session is the same conversation as before.** The Tinker UI must show the user a banner at the boundary (`▼ N messages compacted into a summary above`), and clicking it expands the pre-compaction transcript inline. The user must never have to ask "where did my history go".
- **Architect-level prompts to Jarvis stay in `agent:main:main`.** Do not spawn subagent sessionKeys to dodge compaction; that hides the work from the user, which is exactly what 5.80 forbids. If the main session is approaching budget, show the warning, summarize on demand, but do not silently relocate the conversation.
- **Memory eviction is upstream of compaction, not in the `before_compaction` hook.** A real `tinkerclaw-memory-enhancements` v0.2 must persist evictable messages (or chunks of them) to memory-core _while there is still budget headroom_, not when compaction has already been triggered. Until v0.2 lands, do not market the plugin as a compaction-delayer.
@@ -1002,6 +1004,67 @@ The first real instance of §5.83's U11 (external skill acquisition): integratin
---
+### 5.84 Fluid model×effort — real autonomy + honest visualization (DEPLOYED 2026-06-14)
+
+- **Status:** `DEPLOYED` (Drop 1 + 2 + 3, 2026-06-14) — amends §5.8h (EEG, as-built updated). **Drop 1 (UI, live):** EEG depth-shaded concurrency stack + sticky client-side effort slider (commits `1083112371`/`a9a1e093c3`/`c9de8530fc`; tinker-ui dist, no restart). **Drop 2 (backend, live):** per-unit `thinking` on orchestrate `agent()` + BROCA recipe `model:`/`thinking:` step directives + dead `validateModelAssignment`/hardcoded tier-list retired (commits `7811541bd7`/`1ddc2734ce`/`7ded898778` + type-fix `3e3672df67`). The build first needed `pnpm install` (it fixed an UNRELATED `amazon-bedrock-mantle` runtime-dep-staging failure in `runtime-postbuild` — not Drop 2); then `pnpm build` + gateway restart 2026-06-14 (healthz OK). **Live proof:** an orchestrate `agent({thinking:'high'})` smoke spawned a child with `MAX_THINKING_TOKENS=16000` (=high) — per-unit effort reaches the child budget end-to-end. **Drop 3 (the dynamic allocator, live):** Auto = a fluid effort allocator (`src/agents/effort-allocator.ts`, wired at `agent-command.ts:591` as a `?? chooseAutoEffort()` tail) — annealed exploration (bold when cold → task-weighted as the `effort-ledger.jsonl` matures), aggressiveness scaled by quota surplus (`pressure = util5h − elapsedFraction` from `anthropic-ratelimit-store` + OAuth `resets_at`; abundant headroom near a reset → reaches Max), coverage so each level is experienced; an explicit slider/`/think`/persisted level always wins (it short-circuits the `??` chain). Plus **B-live-rank** (`buildConfiguredModelCatalog` carries the live daily rank) and the **model-force slider's per-turn model reach** (client `modelPinBySession` → `chat.send {model}`, mirroring the effort pin). **`adaptive` finding:** it is a REAL backend level (Anthropic adaptive-thinking API mode `thinking:{type:"adaptive"}` + a per-model default), NOT a redundant duplicate — so it is KEPT as a backend level and only removed from the UI effort slider (Auto is the single "system decides" stop); `normalizeThinkLevel` maps `auto→undefined`, `adaptive→adaptive`. Commits: Auto/Adaptive collapse `eb09728589`/`f0eba60378`/`e7a39558ba`/`5826d55278` + `7c120840a5` (adaptive-as-backend fix); `1d8b0c9bd7` B-live-rank; `7fc786c172` allocator; `16beda4961` model-pin. Gateway rebuilt + restarted 2026-06-14 (healthz OK; allocator confirmed in the bundle `dist/agent-command-*.js`); allocator policy unit-tested 6/6 (tsx) + `tsgo:core` clean. **Live-firing** surfaces on a true-Auto `:main` turn → watch the EEG effort column vary + the `effort-ledger.jsonl` grow (at `$OPENCLAW_STATE_DIR`). **Next refinement (non-blocking):** outcome-quality exploit (join the ledger with run telemetry to pick best-for-task) — the data is captured now. Plans: `jarvis-icu/docs/superpowers/plans/2026-06-14-fluid-model-effort-drop{1,2,3}.md`. Pre-existing extension-wide tsgo debt (676 errors incl. zalo/plan-rpcs/topology) is out of scope and does not block the build (tsdown skips typecheck). Grounded by parallel recon sweeps over current code; superseded the diagnosis-only handoff `docs/context/2026-06-14-effort-system-handoff.md`.
+- **Owner steer (2026-06-14):** "Auto should let Jarvis use its best judgement to adjust thinking effort … a smart combination of model and effort … for different parts of its thinking and for subagent spawns." And: **"maximum autonomy and fluidity, minimum hard rules."** This section is derived from FOUNDATION, not from a classifier.
+
+**Why this exists (the diagnosis, verified).** "Effort" today has a doing half and a visualizing half; the visualizing half (EEG, §5.8h) is honest, the doing half is broken in two independent ways, and the would-be "decision engine" is computed but **advisory-only on every surface**:
+
+- The prefrontal effort-router CLASSIFIES well — `classifyComplexity()` (`effort-router.ts:229-317`) returns `{level∈trivial/standard/deep/ultra, score, modelTier, thinkingHint, orchestration}` — but its sole consumer is `buildEffortGuidance()` (`:324-339`) which emits an `` **prose** block via `before_prompt_build`'s `prependSystemContext` (`index.ts:876-877, 1080-1081`). `modelTier`/`thinkingHint` reach nothing that selects a model or a budget.
+- The real budget IS enforced but is never fed by the router: `thinkLevelToMaxThinkingTokens()` (`thinking-budget.ts:25-63`: minimal 2k / low 4k / medium 8k / adaptive 8k / high 16k / xhigh 22k / max 28k) → `params.thinkLevel` → `__openclawThinkLevel` (`attempt.ts:1922`, `worker.ts:681-687`). `params.thinkLevel = thinkOnce ?? thinkOverride ?? persistedThinking` (`agent-command.ts:591`) — never `?? routerDerived`.
+- `validateModelAssignment()` (`effort-router.ts:92-122`) is **dead/log-only** (`index.ts:282-292` only warns). The enforced model-override seam `before_model_resolve` (`setup.ts:44-105`, applies `{modelOverride,providerOverride}`; result type `hook-before-agent-start.types.ts:14-19`) **exists but prefrontal never registers it.**
+- The manual sliders don't persist or reach the model: the Tinker UI is a webchat client; `rejectWebchatSessionMutation` (`sessions.ts:253-279`) blocks metadata patches, and the slider's `req("sessions.update", {patch:{thinkingLevel}})` (`app.ts:9361-9373`) hits a **method that does not exist** (only `sessions.patch`), error swallowed → snaps back to Auto on the next `updateBudgetPanel` re-render. **This contradicts §5.8h:505, which claims the sliders persist via `sessions.update {thinkingLevel}/{model}`; §5.8h:505 is wrong and is corrected here.**
+- The `DEFAULT_EFFORT_ROUTING_CONFIG` tier map (`effort-router.ts:30-35`, `maximum:["claude-code/claude-opus-4-7"]`) **hardcodes model ids that drift from the daily `model-rank-refresh` cron** (`auth-routing.md`) — itself a FOUNDATION #2 violation.
+
+**Governing principle (FOUNDATION #1/#2, the apex outranks every optic).** The decision engine is **Jarvis's own fluid judgement, guided by the `model-effort-gating` skill + `orchestration-disposition.md` matrix** — expressed through its three autonomy vehicles: **direct subagent spawns** (`openclaw-spawn-subagent.mjs --model --thinking`), **dynamic workflows** (`openclaw-orchestrate.mjs`, `agent()/parallel()/pipeline()`), and **BROCA** (recipes composed/authored on the fly — `prefrontal.recipe.compose` mechanical skill-search + `prefrontal.kit.author` on NO-MATCH; see `subagents-and-recipes.md`). We add **levers, live signals, and observability — never a hard classifier** (FOUNDATION #2 forbids exactly the fixed keyword lists/thresholds/model ids the effort-router embodies). The ONE categorical boundary that stays hard is **explicit user intent** (the sliders) — alongside the PII split, security gates, tool whitelists.
+
+**Work-streams.**
+
+- **(A) Complete the EFFORT lever everywhere MODEL already works.** Jarvis can already pick `model` per dispatched part (enforced: `--model` → `resolveSubagentModelAndThinkingPlan` → child-session store, `subagent-spawn.ts:645-922`) but **not effort**: `openclaw-orchestrate.mjs` `agent({model})` doesn't thread `thinking`; recipe `SpawnOpts` (`recipe-runner.ts:1180-1211`) carries neither model nor effort; `spawnStep` (`:1197-1256`) passes neither flag. Thread per-unit `thinking` (and surface `model`) through all three so "a different model **and effort** for different parts of its thinking" is real. Per-unit `--thinking` is already plumbed end-to-end into the child session (`openclaw-spawn-subagent.mjs:53` → `subagents-rpc.ts` → `resolveSubagentModelAndThinkingPlan` → `thinkingLevel`); the work is exposing it in the orchestrate/recipe authoring surfaces, not new core plumbing.
+- **(B) Make model selection LIVE, not frozen (FOUNDATION #2).** Retire the hardcoded tier list and the dead `validateModelAssignment`; any tier/model hint derives from the rank cron's live "sense of best." The skill/disposition stays the **fluid advisor** — advisory BY DESIGN is correct here, not a gap to "enforce away."
+- **(C) Fix the manual slider (the user-override lever; explicit intent stays hard).** Replace the broken `sessions.update` persist with a **client-side per-session override** (`Map` mirroring `eegStores` `app.ts:1543`) that **re-applies on every `chat.send`** (the webchat-safe path: `chat.send`'s `thinking` param → `/think ` injection into `BodyForCommands`/`CommandBody` `chat.ts:2161-2163`, display `Body` stays clean; OR a new clean per-turn `thinkingOnce` param threaded to `agent-command.ts:304`). Sticky per session, survives re-renders, reaches the budget, other sessions untouched; reset on `/clear`, optional localStorage for reload-survival. The EEG `forced`/dashed flag (`viewedSessionForced()` `app.ts:8688-8695`) must consult this override map (it currently reads `sessions[].thinkingLevel`, which we won't write).
+- **(D) EEG (amends §5.8h).** **Restyle the concurrency stack:** §5.8h:501 currently specs "laterally offset" strands — replace with a **depth-shaded vertical stack: up to 5 lines stacked on top of one another, the bottom line darkest and each higher one lighter; for >5, a count badge shows how many run at that moment** (owner spec 2026-06-14). Invariant 4 (cap 5 + mandatory count badge) holds; only the offset→depth-shade rendering changes. **De-stale §5.8h:491 gap (1):** the subagent feed (`sessionKeyMatches` admitting `:subagent:` descendants) was fixed 2026-06-14 (`cfbd6b4953` admit + `0668ac93e7` hover labels) — verify live and update the gap list. The EEG already carries everything needed (`EegSample{model,provider,chosenLevel,forced,subagent,parentRunId,startedAt,endedAt}`, `eeg-trace.ts:32-48`); once the fan-out runs at varied model+effort (A), the existing main-line-plus-branches view lights up with no further data work.
+
+**Precedence (falls out, no new rule):** an explicit slider/`/think` choice resolves into `params.thinkLevel`/model FIRST; the agent's fluid choice fills the **Auto** vacuum only. Explicit always wins, even a low pick. In the EEG this reads correctly: slider-pin → dashed (forced); agent/router choice → solid at the chosen column.
+
+**Safety = reversibility + kill-switch + the FRACTAL budget governor** (the derived-pressure score of §5.67b — real remaining allowance × time-to-reset × value-of-work; **surplus-spend before a reset is welcome**, FOUNDATION Budget doctrine) — **never a frozen aggressiveness cap.** Fable stays out of routing while US-gov-disabled (a live availability signal, not a hardcoded exclusion). Categorical boundaries unchanged.
+
+**Drops + deploy gates.**
+
+- **Drop 1 (UI-only, instant deploy via `tinker-ui pnpm build`, no gateway restart):** (C) slider override + send-payload reach + `forced` map read; (D) EEG depth-shade restyle + de-stale §5.8h. Verify the served artifact (stale-vite gotcha).
+- **Drop 2 (backend, own rebuild→healthz 200→clean-turn gate):** (A) per-unit `thinking` through orchestrate + recipe `SpawnOpts`; (B) live tier derivation + retire dead `validateModelAssignment`/hardcoded ids. If a clean per-turn `thinkingOnce` param is chosen for (C) over the `/think` path, it lands here too.
+
+**Don't-regress / where facts land at implementation:** structural facts + `verify[]` go to the OWNING optic (single owner per fact): EEG render → `tinker-ui.md` §5.8h; tier/rank derivation + Fable-availability → `auth-routing.md`; per-unit effort lever in spawn/orchestrate/recipe → `subagents-and-recipes.md`. This §5.84 holds the decision/intent only. No hard classifier may be introduced (FOUNDATION #2); the sliders remain the only hard model/effort boundary (explicit user intent).
+
+---
+
+### 5.84a Burn-down allocator (live quota signal) + slider/EEG as-built corrections (2026-06-18)
+
+**Status:** `DEPLOYED` + live-verified 2026-06-18 — as-built corrections (`387db5d42e`, `ff6d2ec183`) AND the burn-down allocator (design `8cb2b8ac5c`, impl `4ebbed16f1`), all on `develop` (NOT pushed). **Live proof of the burn-down:** a true-Auto Tinker turn now logs live `util5h`/`util7d`/`weekElapsed` with a COMPUTED `pressure` (the dead `−0.5` is gone); at 3% into the week + 3% consumed (on pace) it chose `low` (chill early), exactly per design. The "through-the-roof Wed/Thu" branch is covered by the policy unit tests (5/5) + the now-live `weekElapsed`, and surfaces organically as the week progresses.
+
+**As-built corrections to §5.84 (four bugs surfaced by daily use after the 06-15→06-18 power outage; all DEPLOYED + live-verified):**
+
+- **Slider hang.** The post-outage restart came up on a stale dist that REJECTED the `chat.send {model}` param → the model-force slider hung every turn ("spins forever, never replies"). The real fix (accept `model` in `ChatSendParamsSchema`, `gateway/protocol/schema/logs-chat.ts`; inject `/model` via `buildChatSendCommandBody`) was authored 06-15 but never committed/deployed. Now live (`387db5d42e`). NB: `chat.send` validates against `ChatSendParamsSchema` (logs-chat.ts), NOT `SessionsSendParamsSchema` (schema/sessions.ts).
+- **§5.84(C) directive path was silently broken for ALL real prompts.** `get-reply-directives.ts:303-321` (a PR #3705 timestamp-injection guard) called `clearInlineDirectives()` whenever ANY inline directive preceded real message text — wiping BOTH the slider's leading `/model` AND `/think`. So every real prompt ran the default brain + default 4000 budget; the EFFORT slider never reached the budget either. Narrowed the clear to NON-leading directives only (the slider always injects leading). VERIFIED LIVE: `/model sonnet /think high ` → spawn `--model claude-sonnet-4-6` + `MAX_THINKING_TOKENS=16000` (`ff6d2ec183`).
+- **Allocator wired on the WRONG path for webchat.** `chooseAutoEffort` was only called at `agent-command.ts:609` (CLI `agent` path); chat.send/Tinker turns resolve effort in `get-reply-directives.ts:424` and NEVER reached it → Auto parked at the default for every Tinker tab (the `agent:main:main` ledger rows were all CLI-path). Wired the allocator as the true-Auto fallback in `get-reply-directives.ts` for primary keys `/^agent:[^:]+:(?:main|tinker)(?::|$)/`; also broadened the agent-command gate from `endsWith(":main")` to the same allowlist. VERIFIED: Tinker Auto now SCALES — 30-char prompt→`low`(4000), 2251-char→`high`(16000); `:tinker:` ledger rows grow. **There are two effort-resolution paths — a per-turn lever must touch the one the surface uses.**
+- **EEG honest redesign — SUPERSEDES the `forced`/dashed + halo of §5.84(D) and the Precedence para above.** Owner steer 2026-06-18: _"everything is what happens; nothing should be forced."_ Removed the `forced` field, the dashed forced-strand styling, AND the transparent "measured-reality halo." The strand now sits at the **EXECUTED** effort level (gateway-echoed `thinkLevel`); thickness=model for main + subagents. Supervision is now _requested(slider) vs executed(line)_, and the directive fixes make them match. (Earlier text "slider-pin → dashed (forced)" / §5.84(D) halo are obsolete.)
+
+**The burn-down allocator (DESIGN — owner steer 2026-06-18).** _"If I ask a thinking level in the prompt it's a MUST. Near reset, think of our fastest-ever consumption rate, see the 5h window, project whether we can still consume all our tokens. Arriving at reset with half our tokens unused = we were too cautious = the WORST outcome; a few-hours outage is fine. Effort through the roof every Thursday, maybe Wednesday. Consume more, adapt better."_ This realizes §5.84's already-stated intent (surplus-scaled, "abundant headroom near a reset → reaches Max") whose implementation read dead stores. Aligns with FOUNDATION Budget doctrine (§5.84 Safety para: "surplus-spend before a reset is welcome").
+
+- **Live signal revival.** The token panel (`extensions/tinkerclaw-budget-panel`) already fetches live Anthropic usage (`GET /api/oauth/usage`): `five_hour.{utilization, resets_at}` + `seven_day.{utilization, resets_at}` (percent 0–100 + ISO). Publish each fetch into the in-process sync bridge `src/infra/usage-snapshot-store.ts` (extend `UsageSnapshot` with `fiveHourResetAt`/`sevenDayResetAt`) and add a ~10-min background poller in the panel's `register()` so the signal stays fresh with no UI open. `deriveQuotaPressure` (`effort-allocator.ts:53-71`) reads `getUsageSnapshot()` synchronously, replacing the two dead reads (`getRateLimitSnapshot`/`getCachedUsage`).
+- **Policy (decided with owner 2026-06-18).** Per true-Auto turn: **pace** = `seven_day.utilization%` vs % of the week elapsed (week = 7d ending at `seven_day.resets_at`); **urgency** rises CONVEXLY as `resets_at` nears (chill Sun–Tue, through-the-roof Wed/Thu — driven by the LIVE reset time, robust to the actual day/tz, NOT a hardcoded weekday); **headroom** = `1 − seven_day.utilization`. Effort ramps to **Max** when late AND headroom remains; asymmetrically biased aggressive (under-consume is the sin). **Burn through the 5h cap** — do NOT ease off near the `five_hour` limit; a throttle/outage is acceptable, under-consuming is not. **Feasibility ("fastest rate"):** the 5h cap is the natural max-rate ceiling; if even Max can't catch the cap, go Max (salvage). v1 needs no history; ALSO append `five_hour.utilization`/`seven_day.utilization` to each effort-ledger row so a v2 can derive the true fastest sustained rate.
+- **Explicit `/think` is law (unchanged, reinforced):** an explicit level short-circuits the allocator (`directives.thinkLevel` wins the `??` chain); the allocator fills only the Auto vacuum.
+- **Skill doctrine:** update `model-effort-gating` to encode the consume-more philosophy — under-consumption is the failure mode; burn aggressively as reset nears; explicit requests are absolute.
+- **Where facts land (per §5.84 rule):** decision/intent here; structural facts → `effort-allocator.ts` (policy) + `usage-snapshot-store.ts` (bridge) + the budget-panel poller. No hard classifier (FOUNDATION #2) — a live-signal-driven lever, not a fixed threshold.
+- **Deploy gate:** backend → snapshot dist → `pnpm build` → idle-safe restart → verify a true-Auto Tinker turn's ledger `pressure` is no longer pinned at `−0.5` and the chosen level rises as `seven_day.resets_at` nears with headroom.
+
+### 5.85 Bible-currency gate for BROCA tasks (DECISION, 2026-06-19)
+
+Any BROCA task that changes tinkerclaw must auto-keep the bible current. Mechanism: DOCTRINE (`orchestration-disposition.md` + `model-effort-gating` skill) + the `bible-currency-gate` recipe as a mandatory completion step. The EDIT is judgment-based (single-owner discipline + `pnpm bible:invariants`), NOT a mechanical auto-writer (the bible is gated + curated; an auto-dump would corrupt it). Scope: tinkerclaw-changing tasks only (read-only/marketing/research exempt). Level: orchestrator, once. Structural owner: `subagents-and-recipes.md` §"Bible-currency gate". Don't-regress: do not downgrade to a soft reminder or an auto-writer.
+
+---
+
## 6. Backend Fork Patches That Feed Tinker
These are upstream files modified to support Tinker features. They require re-application after every merge.
@@ -1262,59 +1325,59 @@ These are fork-exclusive backend systems that run server-side. They are not part
- **Lesson:** before adding ANY new file-open / shell-out RPC, search for existing patterns first — `grep -rn 'xdg-open\|openExternalFile\|fs-link'` would have surfaced the existing handler. The `files.openInEditor` RPC sat live for ~30 minutes before the user caught the duplication.
- **Don't regress:** the imperative wording sentinel `"Execute the morning briefing NOW"` is dual-purpose — it tells the model what to do AND lets the call site detect a briefing injection (regex-match for `_briefingPath` extraction). Changing the wording requires updating both `buildBriefingPrompt` and the detection regex in the `send` call site.
-### 11.6c Restart-survival visible orange chip + cc-bridge context preservation (2026-05-10)
+### 11.6c Restart-survival visible orange chip + tinker-bridge context preservation (2026-05-10)
-- **Status:** `DEPLOYED + verified end-to-end with marker quote-back proof`. Three intertwined features (restart chip, always-resume, cc-bridge sessionId fallback) plus a separate bare-filename resolver, all proven via journal trace + persisted transcript + Tinker UI snapshot + Jarvis quoting a unique marker after restart.
+- **Status:** `DEPLOYED + verified end-to-end with marker quote-back proof`. Three intertwined features (restart chip, always-resume, tinker-bridge sessionId fallback) plus a separate bare-filename resolver, all proven via journal trace + persisted transcript + Tinker UI snapshot + Jarvis quoting a unique marker after restart.
- **What (#1, restart chip):** Whenever a `status:"running"` main session is detected at gateway boot, the recovery code pushes a visible `__ERR_ENV__:` envelope (orange `envelope-recoverable`, icon 🔄) into that session's transcript via `chat.inject` BEFORE the `[System] continue` resume dispatch. Single uniform wording: `Gateway restarted at HH:MM — picking up where I stopped`. We never tell the user to retry — the session always attempts resume.
-- **What (#1b, always-resume):** The original `resolveMainSessionResumeBlockReason` tail-check is now informational only. We always dispatch `[System] continue from existing transcript` regardless of tail content. cc-bridge sessions whose agent transcript is empty (subprocess hasn't flushed yet) get the same resume treatment as native sessions; the cc-bridge worker pool then handles `--resume` lookup itself.
-- **What (#1c, openclaw sessionId fallback in cc-bridge):** cc-bridge's session-map is now indexed by openclaw agent sessionId in addition to the hash-derived `cc-sp-` sessionKey. The worker-pool prefers the openclaw-sessionId lookup when available, since the openclaw sessionId is canonical (one openclaw session = one conversation thread, /new mints a new sessionId). This sidesteps the cc-bridge sessionKey hash drift that happens when the `[System] continue` dispatch shifts the systemPrompt prefix.
+- **What (#1b, always-resume):** The original `resolveMainSessionResumeBlockReason` tail-check is now informational only. We always dispatch `[System] continue from existing transcript` regardless of tail content. tinker-bridge sessions whose agent transcript is empty (subprocess hasn't flushed yet) get the same resume treatment as native sessions; the tinker-bridge worker pool then handles `--resume` lookup itself.
+- **What (#1c, openclaw sessionId fallback in tinker-bridge):** tinker-bridge's session-map is now indexed by openclaw agent sessionId in addition to the hash-derived `tinker-sp-` sessionKey. The worker-pool prefers the openclaw-sessionId lookup when available, since the openclaw sessionId is canonical (one openclaw session = one conversation thread, /new mints a new sessionId). This sidesteps the tinker-bridge sessionKey hash drift that happens when the `[System] continue` dispatch shifts the systemPrompt prefix.
- **What (#2, bare-filename resolver):** New gateway RPC `files.resolveBareName({name})` walks an allowlist of project roots (workspace → ~/src/tinkerclaw → ~/src/jarvis-icu → ~/.openclaw) and returns absolute path matches. The Tinker UI `md()` post-processor wraps inline `FOO.md` in ``; the click handler resolves the bare name on first click via the RPC, caches it on the element + in a session-scope `Map`, then opens via the existing `config.openExternalFile` RPC. No user input needed; LLM disambiguation slot is wired but currently picks first match (root-walk order). All new resolutions skip `node_modules`, `.git`, `dist`, `build`, etc.
- **Files:**
- `src/agents/main-session-restart-recovery.ts` — extracted `pushRestartWarningEnvelope()` helper; tail-check guard removed (always attempt resume); single chip-wording variant.
- - `extensions/tinkerclaw-cc-bridge/src/session-map.ts` — `MapEntry.openclawSessionId` field; new `getLatestResumeSessionIdByOpenclawSessionId` helper; `setResumeSessionId` accepts optional openclawSessionId.
- - `extensions/tinkerclaw-cc-bridge/src/worker-pool.ts` — lookup priority reordered: openclaw-sessionId first, sessionKey fallback.
- - `extensions/tinkerclaw-cc-bridge/src/worker.ts` — `WorkerSpawnParams.openclawSessionId` field; passed to `setResumeSessionId` on the system_init event.
- - `extensions/tinkerclaw-cc-bridge/src/stream.ts` — threads `openclawSessionId` (already smuggled via `__openclawSessionId`) into the worker spawn params.
+ - `extensions/tinkerclaw-tinker-bridge/src/session-map.ts` — `MapEntry.openclawSessionId` field; new `getLatestResumeSessionIdByOpenclawSessionId` helper; `setResumeSessionId` accepts optional openclawSessionId.
+ - `extensions/tinkerclaw-tinker-bridge/src/worker-pool.ts` — lookup priority reordered: openclaw-sessionId first, sessionKey fallback.
+ - `extensions/tinkerclaw-tinker-bridge/src/worker.ts` — `WorkerSpawnParams.openclawSessionId` field; passed to `setResumeSessionId` on the system_init event.
+ - `extensions/tinkerclaw-tinker-bridge/src/stream.ts` — threads `openclawSessionId` (already smuggled via `__openclawSessionId`) into the worker spawn params.
- `src/gateway/server-methods/files-resolve-bare.ts` (new) — `files.resolveBareName` RPC.
- `src/gateway/server-methods.ts` + `src/gateway/method-scopes.ts` — handler registration + READ_SCOPE entry.
- `tinker-ui/src/app.ts` — bare-filename `md()` pass + dual-mode click handler.
- **Verification (FINAL proof, 2026-05-10 13:06):**
- 1. Dispatched task with unique marker `MARKER-FIBONACCI-1-1-2-3-5-8-PROOF-FINAL`. cc-bridge began work at 13:06:06 (cc-sp-771eab65).
+ 1. Dispatched task with unique marker `MARKER-FIBONACCI-1-1-2-3-5-8-PROOF-FINAL`. tinker-bridge began work at 13:06:06 (tinker-sp-771eab65).
2. After 4 tool reads completed, `openclaw-restart --full` at 13:06:31.
3. Journal at 13:06:49 → `marked 1 interrupted main session(s)`.
4. Journal at 13:06:55 → `chat.inject 235ms ✓`. Journal at 13:06:56 → `pushed restart-warning envelope to agent:main:main`.
5. Journal at 13:06:57 → `resumed interrupted main session: agent:main:main` (recovered=1).
- 6. Journal at 13:07:07 → cc-bridge `turn start sessionKey=cc-sp-44b1d6f5` (different hash, expected). The fallback found the prior cli session via openclaw-sessionId index.
- 7. Journal at 13:07:13 → cc-bridge result. **Jarvis's reply explicitly quotes the unique marker:** `"…I posted the one-line summary quoting MARKER-FIBONACCI-1-1-2-3-5-8-PROOF-FINAL …"`. Context preserved end-to-end across the gateway restart.
+ 6. Journal at 13:07:07 → tinker-bridge `turn start sessionKey=tinker-sp-44b1d6f5` (different hash, expected). The fallback found the prior cli session via openclaw-sessionId index.
+ 7. Journal at 13:07:13 → tinker-bridge result. **Jarvis's reply explicitly quotes the unique marker:** `"…I posted the one-line summary quoting MARKER-FIBONACCI-1-1-2-3-5-8-PROOF-FINAL …"`. Context preserved end-to-end across the gateway restart.
- **Don't regress:**
- Envelope inject MUST stay BEFORE the agent resume dispatch in `resumeMainSession` (so the chip lands first in the transcript order).
- - In `worker-pool.getOrCreate`, openclaw-sessionId lookup MUST come BEFORE the cc-bridge sessionKey lookup. Reversing this order brings back the bug where stale entries from prior tests win and Jarvis loses context.
+ - In `worker-pool.getOrCreate`, openclaw-sessionId lookup MUST come BEFORE the tinker-bridge sessionKey lookup. Reversing this order brings back the bug where stale entries from prior tests win and Jarvis loses context.
- `setResumeSessionId` MUST be called with the openclawSessionId on every system_init event. If not, new entries lack the index and the fallback is empty.
- The bare-filename click handler MUST cache misses (`null` value) so a missing file isn't re-resolved on every click.
- Bare-filename extension whitelist is a deliberate guard — adding more extensions means more chance of false-positive wraps in unrelated `` blocks.
-### 11.6d cc-bridge idle-watchdog 120s SIGTERM regression — fixed via openclaw.json (2026-05-10)
+### 11.6d tinker-bridge idle-watchdog 120s SIGTERM regression — fixed via openclaw.json (2026-05-10)
-- **Symptom (matches the 2026-05-05 entry verbatim):** WhatsApp ask "install printingpress.dev" + "read this YouTube" surfaced as `🤖 ⚠️ Something went wrong while processing your request.` Jarvis's prepared reply ("Done. Installed Go 1.26.3 / starter-pack / printing-press") was queued in his cli session but never delivered to WhatsApp. Tinker UI was simultaneously stuck on `sending...` after the user typed `/new`. Journal showed both lanes timing out: `lane=session:agent:main:whatsapp:direct: durationMs=267533 error="FailoverError: LLM request timed out."` and `lane=session:agent:main:main durationMs=279617`. cc-bridge worker SIGTERMed at ~138s on each turn, despite the 2026-05-05 fix that bumped `timeoutSeconds` to 600.
-- **Root cause (real, this time):** the cc-bridge plugin's `buildClaudeCodeProviderConfig()` returns `{ timeoutSeconds: 600, ... }` via discovery, but `applyConfiguredProviderOverrides` in `src/agents/embedded-agent-runner/model.ts` reads `providerConfig` from `resolveConfiguredProviderConfig(cfg, "claude-code")` — i.e. `cfg.models.providers["claude-code"]` in `openclaw.json`, NOT the plugin-discovered config. The plugin discovery is consulted for model availability but its provider-level `timeoutSeconds` is silently dropped on the way to model resolution. As a result `model.requestTimeoutMs` was undefined and `resolveLlmIdleTimeoutMs` fell through to `clampImplicitTimeoutMs(agentTimeoutMs)` which `Math.min`s against `DEFAULT_LLM_IDLE_TIMEOUT_MS = 120_000` (`src/config/agent-timeout-defaults.ts`). Hence the watchdog at 120s, not 600s.
+- **Symptom (matches the 2026-05-05 entry verbatim):** WhatsApp ask "install printingpress.dev" + "read this YouTube" surfaced as `🤖 ⚠️ Something went wrong while processing your request.` Jarvis's prepared reply ("Done. Installed Go 1.26.3 / starter-pack / printing-press") was queued in his cli session but never delivered to WhatsApp. Tinker UI was simultaneously stuck on `sending...` after the user typed `/new`. Journal showed both lanes timing out: `lane=session:agent:main:whatsapp:direct: durationMs=267533 error="FailoverError: LLM request timed out."` and `lane=session:agent:main:main durationMs=279617`. tinker-bridge worker SIGTERMed at ~138s on each turn, despite the 2026-05-05 fix that bumped `timeoutSeconds` to 600.
+- **Root cause (real, this time):** the tinker-bridge plugin's `buildClaudeCodeProviderConfig()` returns `{ timeoutSeconds: 600, ... }` via discovery, but `applyConfiguredProviderOverrides` in `src/agents/embedded-agent-runner/model.ts` reads `providerConfig` from `resolveConfiguredProviderConfig(cfg, "claude-code")` — i.e. `cfg.models.providers["claude-code"]` in `openclaw.json`, NOT the plugin-discovered config. The plugin discovery is consulted for model availability but its provider-level `timeoutSeconds` is silently dropped on the way to model resolution. As a result `model.requestTimeoutMs` was undefined and `resolveLlmIdleTimeoutMs` fell through to `clampImplicitTimeoutMs(agentTimeoutMs)` which `Math.min`s against `DEFAULT_LLM_IDLE_TIMEOUT_MS = 120_000` (`src/config/agent-timeout-defaults.ts`). Hence the watchdog at 120s, not 600s.
- **Fix:** added `"timeoutSeconds": 600` to `~/.openclaw/openclaw.json` under `models.providers["claude-code"]`, alongside `apiKey` / `baseUrl` / `api`. Verified via the new `[idle-timeout-diag]` log line in `attempt.ts` immediately after `resolveLlmIdleTimeoutMs`: post-fix it reports `idleTimeoutMs=600000 model.requestTimeoutMs=600000`, was `idleTimeoutMs=120000 model.requestTimeoutMs=undefined` pre-fix. Restart-recovery code unstuck the live TUI session by injecting the orange chip + dispatching `[System] continue`, which let Jarvis close the turn cleanly.
-- **Open architectural follow-up:** plugin-discovered providerConfig values (especially `timeoutSeconds`) should merge into the resolved `cfg.models.providers[provider]` so a plugin's defaults take effect without duplicating them in `openclaw.json`. Today the cfg-side patch is the only path that works; the cc-bridge catalog's `timeoutSeconds: 600` is dead code as far as the LLM idle watchdog is concerned.
+- **Open architectural follow-up:** plugin-discovered providerConfig values (especially `timeoutSeconds`) should merge into the resolved `cfg.models.providers[provider]` so a plugin's defaults take effect without duplicating them in `openclaw.json`. Today the cfg-side patch is the only path that works; the tinker-bridge catalog's `timeoutSeconds: 600` is dead code as far as the LLM idle watchdog is concerned.
- **Don't regress:**
- The `timeoutSeconds: 600` entry in `openclaw.json` is load-bearing. If you reset the file or migrate it, port the field forward.
- The diagnostic log line `[idle-timeout-diag] resolved idleTimeoutMs=…` in `attempt.ts:1862-1880` stays. It is one line per turn and gives instant visibility into regressions of this exact bug. If a future turn shows `model.requestTimeoutMs=undefined` again, the cfg got reset.
- - The 2026-05-05 fix in `extensions/tinkerclaw-cc-bridge/src/catalog.ts` (`timeoutSeconds: Math.floor(DEFAULT_REQUEST_TIMEOUT_MS / 1000)`) was correct in spirit but incomplete — leave it in place as a belt to the suspenders, but understand the actual surface that takes effect is the openclaw.json one.
-- **Secondary bug observed but not yet fixed (TUI stuck on `sending`):** when the TUI cc-bridge timed out at 16:59:34, the failover error envelope was generated and `sendFinalPayload returned queuedFinal=true routedFinalCount=0` — meaning `routeReplyToOriginating` returned null (one of `shouldRouteToOriginating || routeReplyChannel || routeReplyTo || routeReplyRuntime` is false for webchat surface) and the fallback `dispatcher.sendFinalReply` queued the reply but the webchat WS subscription never picked it up. The Tinker UI client kept its `sending...` thinking-indicator and never received the error chip. The restart-recovery cycle is the only thing that currently unsticks it. Real fix needed in `dispatch-from-config.ts:sendFinalPayload` so the surface_error envelope reaches webchat too.
+ - The 2026-05-05 fix in `extensions/tinkerclaw-tinker-bridge/src/catalog.ts` (`timeoutSeconds: Math.floor(DEFAULT_REQUEST_TIMEOUT_MS / 1000)`) was correct in spirit but incomplete — leave it in place as a belt to the suspenders, but understand the actual surface that takes effect is the openclaw.json one.
+- **Secondary bug observed but not yet fixed (TUI stuck on `sending`):** when the TUI tinker-bridge timed out at 16:59:34, the failover error envelope was generated and `sendFinalPayload returned queuedFinal=true routedFinalCount=0` — meaning `routeReplyToOriginating` returned null (one of `shouldRouteToOriginating || routeReplyChannel || routeReplyTo || routeReplyRuntime` is false for webchat surface) and the fallback `dispatcher.sendFinalReply` queued the reply but the webchat WS subscription never picked it up. The Tinker UI client kept its `sending...` thinking-indicator and never received the error chip. The restart-recovery cycle is the only thing that currently unsticks it. Real fix needed in `dispatch-from-config.ts:sendFinalPayload` so the surface_error envelope reaches webchat too.
### 11.6e Plugin provider-config overlay + chat.send broadcast backstop (2026-05-10 evening)
-- **Status:** `DEPLOYED + verified end-to-end`. Removes the 2026-05-09 openclaw.json `timeoutSeconds: 600` patch as load-bearing — the value now flows from the cc-bridge plugin's discovery output through a runtime overlay, so plugin defaults take effect without manual config duplication.
-- **What (#1, plugin overlay):** New `src/agents/plugin-provider-config-overlay.ts` exposes `registerPluginProviderConfigOverlay(providerId, partial)` via `src/plugin-sdk/provider-config-overlay.ts`. The cc-bridge plugin's `register()` hook calls it with `{ timeoutSeconds: Math.floor(DEFAULT_REQUEST_TIMEOUT_MS / 1000) }`. `resolveConfiguredProviderConfig` in `src/agents/embedded-agent-runner/model.ts` now returns `{...overlay, ...explicit}` so explicit `openclaw.json` keys still win on a per-key basis but plugin-supplied defaults fill gaps. Verified: with the explicit `timeoutSeconds: 600` REMOVED from `openclaw.json`, the diagnostic log shows `idleTimeoutMs=600000 model.requestTimeoutMs=600000`. The 2026-05-05 catalog `timeoutSeconds` setting now actually does something at runtime.
+- **Status:** `DEPLOYED + verified end-to-end`. Removes the 2026-05-09 openclaw.json `timeoutSeconds: 600` patch as load-bearing — the value now flows from the tinker-bridge plugin's discovery output through a runtime overlay, so plugin defaults take effect without manual config duplication.
+- **What (#1, plugin overlay):** New `src/agents/plugin-provider-config-overlay.ts` exposes `registerPluginProviderConfigOverlay(providerId, partial)` via `src/plugin-sdk/provider-config-overlay.ts`. The tinker-bridge plugin's `register()` hook calls it with `{ timeoutSeconds: Math.floor(DEFAULT_REQUEST_TIMEOUT_MS / 1000) }`. `resolveConfiguredProviderConfig` in `src/agents/embedded-agent-runner/model.ts` now returns `{...overlay, ...explicit}` so explicit `openclaw.json` keys still win on a per-key basis but plugin-supplied defaults fill gaps. Verified: with the explicit `timeoutSeconds: 600` REMOVED from `openclaw.json`, the diagnostic log shows `idleTimeoutMs=600000 model.requestTimeoutMs=600000`. The 2026-05-05 catalog `timeoutSeconds` setting now actually does something at runtime.
- **What (#2, chat.send backstop):** `chat.ts` `.then()` previously emitted `broadcastChatFinal` only when `!agentRunStarted`. When the agent ran but the lifecycle event from `server-chat.ts:emitChatFinal` was dropped (because `isControlUiVisible=false`, or because surface_error timeouts complete the run without throwing and without firing the lifecycle hook), the TUI received NO `state="final"` and the spinner stayed on `sending...` forever. Backstop: in the agent-started branch, also call `broadcastChatFinal` with whatever `deliveredReplies` contains (or empty). Idempotent versus the lifecycle path because `broadcastChatFinal` `.delete()`-s `agentRunSeq[runId]`. The TUI client de-dupes by runId+state.
- **Files:**
- `src/agents/plugin-provider-config-overlay.ts` (new) — module-level Map + `registerPluginProviderConfigOverlay` / `getPluginProviderConfigOverlay`.
- `src/plugin-sdk/provider-config-overlay.ts` (new) — public re-export for plugins.
- `src/agents/embedded-agent-runner/model.ts` — `resolveConfiguredProviderConfig` merges overlay under explicit.
- - `extensions/tinkerclaw-cc-bridge/index.ts` — calls `registerPluginProviderConfigOverlay(PROVIDER_ID, { timeoutSeconds })` from `register()`.
+ - `extensions/tinkerclaw-tinker-bridge/index.ts` — calls `registerPluginProviderConfigOverlay(PROVIDER_ID, { timeoutSeconds })` from `register()`.
- `src/gateway/server-methods/chat.ts` — backstop `broadcastChatFinal` in the `else` branch of `.then()` (agentRunStarted=true path).
- `~/.openclaw/openclaw.json` — `timeoutSeconds: 600` removed from `models.providers["claude-code"]` since the overlay now supplies it.
- **Don't regress:**
@@ -1475,13 +1538,13 @@ These are fork-exclusive backend systems that run server-side. They are not part
- **Pipeline:** `anthropic-vertex-stream.ts` (fetch wrapper) → `ratelimit-store.ts` (in-memory keyed by authProfileId) → `emitAgentEvent("ratelimit-update")` → Tinker UI `onEvent()` → `renderUsageBarsOnly()`
- **Files:** `src/agents/embedded-agent-runner/anthropic-vertex-stream.ts`, `src/agents/auth-profiles/ratelimit-store.ts` (new), `src/agents/embedded-agent-runner/attempt-hooks.ts`, `tinker-ui/src/app.ts`
-### 11.17 cc-bridge Worker Tool-Choice Injection (2026-04-20)
+### 11.17 tinker-bridge Worker Tool-Choice Injection (2026-04-20)
- **Status:** `DEPLOYED`
-- **What:** `extensions/tinkerclaw-cc-bridge/src/worker.ts::buildToolChoiceBlock()` appends a ~60-line markdown block to every spawned Claude-Code subagent's system prompt. Teaches the WebSearch-vs-WebFetch decision, when to load Deferred tools via `ToolSearch`, when to use Monitor/PushNotification/TaskCreate, and names the common anti-patterns (guessing URLs then WebFetching them, polling via `sleep+test -f` loops, posting routine status to chat).
+- **What:** `extensions/tinkerclaw-tinker-bridge/src/worker.ts::buildToolChoiceBlock()` appends a ~60-line markdown block to every spawned Claude-Code subagent's system prompt. Teaches the WebSearch-vs-WebFetch decision, when to load Deferred tools via `ToolSearch`, when to use Monitor/PushNotification/TaskCreate, and names the common anti-patterns (guessing URLs then WebFetching them, polling via `sleep+test -f` loops, posting routine status to chat).
- **Why:** Claude Code 2.1.114 exposes a dozen tools as DEFERRED — the names show in the initial system prompt but schemas must be loaded via `ToolSearch({query:"select:"})` before use. Jarvis was reflexing to WebFetch on guessed domains and TLS-erroring out, because nothing in the spawn-time prompt told him WebSearch existed as a separate tool with different purpose.
- **Pipeline:** Worker spawn → `combinedSystemPrompt = [systemPromptBody, rulesBody, subagentHelpBody, toolChoiceBody].filter(Boolean).join("")` → Claude Code `--append-system-prompt`. (FORK 2026-05-21: the live order is now `persona → ethical-rules → narration → subagent-helper → tool-choice → plan-tools`; see `tool-loop.md` "combinedSystemPrompt block order" for the current shape and `config-shape.md` for the ethical-rules loader path.)
-- **Files:** `extensions/tinkerclaw-cc-bridge/src/worker.ts` (lines 203-270 for `buildToolChoiceBlock`, 369-373 for the combine step)
+- **Files:** `extensions/tinkerclaw-tinker-bridge/src/worker.ts` (lines 203-270 for `buildToolChoiceBlock`, 369-373 for the combine step)
### 11.18 04:00 Cron Pipeline Chain + md-File-Only Policy (2026-04-20)
@@ -1519,8 +1582,8 @@ These are fork-exclusive backend systems that run server-side. They are not part
### 11.21 AGENTS.md Compaction + Tool-Choice Pointer (2026-04-20)
- **Status:** `DEPLOYED`
-- **What:** Two small additions to `~/.openclaw/workspace/AGENTS.md`. (1) Context Hygiene gains a compaction-awareness bullet: save unfinished state to today's daily log BEFORE the auto-compact fires (short declarative headers survive, chat-style summaries don't). (2) New "Tool Choice" section points at the cc-bridge `buildToolChoiceBlock` and explicitly names Deferred tools needing `ToolSearch` schema-load first.
-- **Why:** Anthropic prompt-engineering rule: inform Claude about its harness so it behaves accordingly when context fills up. The tool-choice pointer gives the main session the same decision framework subagents now get from cc-bridge.
+- **What:** Two small additions to `~/.openclaw/workspace/AGENTS.md`. (1) Context Hygiene gains a compaction-awareness bullet: save unfinished state to today's daily log BEFORE the auto-compact fires (short declarative headers survive, chat-style summaries don't). (2) New "Tool Choice" section points at the tinker-bridge `buildToolChoiceBlock` and explicitly names Deferred tools needing `ToolSearch` schema-load first.
+- **Why:** Anthropic prompt-engineering rule: inform Claude about its harness so it behaves accordingly when context fills up. The tool-choice pointer gives the main session the same decision framework subagents now get from tinker-bridge.
- **Files:** `~/.openclaw/workspace/AGENTS.md`
### 11.22 /clear — Pure Client Transaction, No LLM Call (2026-04-20, persistence fix 2026-04-21)
diff --git a/TINKER_UI_DESIGN_BIBLE/branch-policy.md b/TINKER_UI_DESIGN_BIBLE/branch-policy.md
index 2f6eb91e171fe..cf126623810f2 100644
--- a/TINKER_UI_DESIGN_BIBLE/branch-policy.md
+++ b/TINKER_UI_DESIGN_BIBLE/branch-policy.md
@@ -62,7 +62,7 @@ A non-negotiable checklist:
- `pnpm build` exits 0 with `NODE_OPTIONS=--max-old-space-size=8192`.
- Gateway boots cleanly (`openclaw-restart --full`, `curl /healthz` returns `{"ok":true,"status":"live"}`).
- `apply-fork-wiring.mjs` runs idempotent (re-running prints "already wired" for everything).
-- A smoke probe through cc-bridge replies (e.g. SMOKE-OK).
+- A smoke probe through tinker-bridge replies (e.g. SMOKE-OK).
- For changes to plugin manifests: each plugin still appears in the gateway plugin list at boot.
- For changes to docs/scripts only: skip the build gate, but verify the doc renders or the script `node --check`s clean.
diff --git a/TINKER_UI_DESIGN_BIBLE/bug-log.md b/TINKER_UI_DESIGN_BIBLE/bug-log.md
index 510a1489afaca..c2b342356e654 100644
--- a/TINKER_UI_DESIGN_BIBLE/bug-log.md
+++ b/TINKER_UI_DESIGN_BIBLE/bug-log.md
@@ -29,27 +29,28 @@ Tags let an AI scan for recurring patterns ("how many `auth-token` bugs
have we seen?") without re-reading each prose entry. When adding a new
fix, pick from this list — extend it only if no tag fits.
-| Tag | Meaning |
-| --------------------- | --------------------------------------------------------------------------------- |
-| `auth-token` | OAuth tokens — refresh, content-type, scope-downgrade, refresh-failed |
-| `auth-scope` | Scope/permission gate dropped legitimate clients |
-| `billable-noop` | A paid LLM turn fired that nobody consumes (skip-gate failed / was dead code) |
-| `bridge-leak` | Cross-channel state bleed (real or suspected) |
-| `bundler-trap` | tsdown/onlyBuiltDependencies/\_\_filename/native-deps wiped or misconfigured |
-| `cache-staleness` | TTL not invalidated after dependent change |
-| `cleanup-race` | Drain deadlock, orphan processes, stuck resurrection across restarts |
-| `config-dead-code` | Config key looked live but didn't actually apply |
-| `crash-on-startup` | Bad SDK call / missing artifact prevented plugin or gateway boot |
-| `detection-pattern` | Substring/regex/startsWith assumption broke under prefix change |
-| `display-misclassify` | UI rendered system as user, error as raw object, etc. |
-| `event-ordering` | text_end before tail-recover, lifecycle dropped, race on stream state |
-| `lid-routing` | WhatsApp LID rescue / sister-DM trigger class |
-| `merge-wipe` | Upstream merge dropped fork code/config/scope (often combined with another) |
-| `outbound-drop` | Outbound message lost / queued without delivery |
-| `plugin-load` | Plugin failed to register — manifest missing field, wrong SDK call, name mismatch |
-| `timeout-tuning` | Idle watchdog / request timeout fired prematurely or wrongly |
-| `ui-state-clear` | File-watcher / event handler cleared UI state too aggressively |
-| `workspace-shadow` | workspace/ override of bundled/ with stale content |
+| Tag | Meaning |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
+| `auth-token` | OAuth tokens — refresh, content-type, scope-downgrade, refresh-failed |
+| `auth-scope` | Scope/permission gate dropped legitimate clients |
+| `billable-noop` | A paid LLM turn fired that nobody consumes (skip-gate failed / was dead code) |
+| `bridge-leak` | Cross-channel state bleed (real or suspected) |
+| `bundler-trap` | tsdown/onlyBuiltDependencies/\_\_filename/native-deps wiped or misconfigured |
+| `cache-staleness` | TTL not invalidated after dependent change |
+| `cleanup-race` | Drain deadlock, orphan processes, stuck resurrection across restarts |
+| `config-dead-code` | Config key looked live but didn't actually apply |
+| `crash-on-startup` | Bad SDK call / missing artifact prevented plugin or gateway boot |
+| `detection-pattern` | Substring/regex/startsWith assumption broke under prefix change |
+| `display-misclassify` | UI rendered system as user, error as raw object, etc. |
+| `think-clamp` | A requested thinking level exceeded a model's ceiling and was REJECTED (hard error) instead of clamped down to the nearest supported level |
+| `event-ordering` | text_end before tail-recover, lifecycle dropped, race on stream state |
+| `lid-routing` | WhatsApp LID rescue / sister-DM trigger class |
+| `merge-wipe` | Upstream merge dropped fork code/config/scope (often combined with another) |
+| `outbound-drop` | Outbound message lost / queued without delivery |
+| `plugin-load` | Plugin failed to register — manifest missing field, wrong SDK call, name mismatch |
+| `timeout-tuning` | Idle watchdog / request timeout fired prematurely or wrongly |
+| `ui-state-clear` | File-watcher / event handler cleared UI state too aggressively |
+| `workspace-shadow` | workspace/ override of bundled/ with stale content |
**Recurring patterns visible from the chips:**
@@ -58,6 +59,36 @@ fix, pick from this list — extend it only if no tag fits.
- `event-ordering` repeats 7 times — async race conditions around stream lifecycle / button-state / session-resume.
- `auth-token` repeats 5 times — OAuth machinery is the largest single class of fragility.
+### FIXED [display-misclassify+detection-pattern+timeout-tuning]: Recoverable provider error (quota/rate-limit/overload) → centered orange warning + client-side auto-retry with live countdown (2026-06-24)
+
+- **Summary:** a recoverable surfaced error now renders as a centered ORANGE warning that auto-resends the last turn on a `3s→10s→30s→2m→7m→15m` backoff ladder (6 attempts) with a live 1s countdown + hover "stop retrying", exhausted→red, unifying rate-limit/quota AND overload-class surfaced errors into one countdown/stop UI. Commits `6d9320d97e` (pure `retry-policy.ts` module + test 14/14), `c618805fcd` (app.ts controller), `a62de24f8e` (base.css hover stop-link + `.retrying` pulse), `de8281702d` (`reason`+`retryAfter` on `ChatEventSchema`), `9206b1e7fb` (populate `reason` at the chat-error emit site via `resolveFailoverReasonFromError`).
+- **Symptom:** a turn that hit an OpenAI quota / rate-limit / `429` (incident: a `gpt-5.5`-pinned tab showing `You exceeded your current quota` / `All models temporarily rate-limited`), or a transient overload `529`/`502`/`503` / "draining for restart", surfaced as a DEAD-END red error bubble — the user had to manually resend.
+- **Root cause:** a recoverable, retry-able provider error was misclassified as a terminal failure and surfaced with the same dead-end red error treatment as a fatal one (`display-misclassify`); there was no automatic backed-off resend track for surfaced errors (only the §5.49 gateway-drain case had one). An in-turn SERVER retry was not viable because the long ladder waits (up to 15m) exceed the 900s gateway turn timeout (`timeout-tuning`), so the retry had to move CLIENT-side.
+- **Fix:** detection is `structured reason + frontend text-match fallback` (`detection-pattern`) — the backend tags the surfaced `chat` error event with an optional `reason` (`rate_limit`/`quota`/`overloaded`/`unavailable`) via `resolveFailoverReasonFromError`, and the frontend `classifyRecoverable(reason?, errorText?)` trusts a known `reason` else regex-matches the human text. Retry is CLIENT-side: the controller re-issues a FRESH-`idempotencyKey` `chat.send` (the original key would dedup-block it) on the `RETRY_LADDER_MS` ladder, pushing one new orange `_isRetryWarning` bubble per attempt; `nextRetryDelayMs` returns `null` at exhaustion → terminal red `🛑 Gave up after 6 retries`. The pre-surface server-side in-turn overload-retry is UNCHANGED (no double-retry). `retryAfter` is intentionally omitted from the emit (not cleanly available; the frontend ladder owns timing).
+- **Files:** `tinker-ui/src/retry-policy.ts` (NEW pure module), `tinker-ui/src/retry-policy.test.ts` (NEW), `tinker-ui/src/app.ts` (controller/classification/countdown/resend/exhausted/hover-stop/abort-warning), `tinker-ui/src/styles/base.css` (`.retry-stop-link` + `.msg-overload-bubble.retrying`), `src/gateway/protocol/schema/logs-chat.ts` (`reason`+`retryAfter` on `ChatEventSchema`), `src/gateway/server-methods/chat.ts` (populate `reason`).
+- **Rule:** a recoverable provider error must not surface as a dead-end — auto-retry it CLIENT-side (fresh `idempotencyKey`, never the original) on a bounded ladder with a visible countdown + stop control; an in-turn server retry on the long steps would blow the 900s turn timeout. Detection prefers the structured `reason`; the text-match is the fallback for older/un-tagged emits — keep both. See `tinker-ui.md` §5.8j and `failures.md` (surfaced-error categories `rate_limit`/`overload`).
+
+### FIXED [display-misclassify+detection-pattern]: A — answer collapsed into the "Reasoning" block; only text after a 💬 ANSWER marker rendered (2026-06-19)
+
+- **Symptom:** assistant replies showed most bubbles compacted into the grey "Reasoning (N steps)" group and the visible answer was only "what comes after 💬 ANSWER" — intermittently, depending on whether the model emitted the marker.
+- **Root cause:** run-grouping in `app.ts` marked `assistantTextIndices.slice(0, -1)` as thinking — every assistant text bubble EXCEPT the last collapsed BY POSITION. The literal `💬 ANSWER` marker was the only thing that let an earlier/structured bubble escape, but it is injected only transiently (and only when the fractal toggle is on) while `🌿 FRACTAL` is double-reinforced in the always-loaded system prompt — so the model reliably emits FRACTAL but drops ANSWER, and whenever it did, real answer content was hidden.
+- **Fix:** STRUCTURAL, marker-free — an assistant text bubble is between-tool NARRATION (→ collapse) iff a tool occurs LATER in the same run; text after the last tool is the ANSWER and stays visible; the run renders MULTIPLE answer bubbles. Retired the `💬 ANSWER` injection (kept `🌿 FRACTAL`). Decision extracted as the pure unit-tested `narrationIndices()` (`reply-grouping.ts`). Commit `bac187e372`.
+- **Files:** `tinker-ui/src/app.ts` (run-grouping + buildInjectedPrompt + reconstructInjectionFields), `tinker-ui/src/reply-grouping.ts` (new), `tinker-ui/src/sectioned-reply.test.ts`.
+
+### FIXED [detection-pattern]: B — duplicated text in answers (cross-block / offset stream re-send) (2026-06-19)
+
+- **Symptom:** an answer sentence/paragraph appeared twice in the same bubble ("Good catches…Good catches…").
+- **Root cause:** `tinker-bridge` (now `tinker-bridge`) flattens claude-cli's interleaved `content_block_delta` + cumulative `assistant` re-emits into one buffer; the dedup guard only DROPPED a clean 60-char _prefix_ restart on the assistant_cumulative source, keyed per block index — partial / offset / cross-block re-sends slipped through (content_block_delta path was WARN-only).
+- **Fix:** pure `dedupStreamingOverlap(acc, delta)` (`stream.ts`) trims the overlapping prefix when a delta begins by repeating the accumulator TAIL (drops it entirely on a full re-send), source-agnostic + cross-block, conservative 60-char floor so legit short repeats survive. Commit `d13c16b542`.
+- **Files:** `extensions/tinkerclaw-tinker-bridge/src/stream.ts`, `stream.dedup.test.ts` (new).
+
+### FIXED [event-ordering]: C — a prompt typed mid-turn jumped above the response (2026-06-19)
+
+- **Symptom:** typing a new prompt while Jarvis was still streaming put the green user bubble ABOVE some already-written response bubbles.
+- **Root cause:** chat renders in pure `messages[]` order; a mid-turn prompt is held in `pendingQueuedSends` and flushed at `chat.final`, but the flush ran BEFORE the final-answer bubbles were promoted/pushed (the `!hadTemps` re-slice and tool-only `messages.push` append a NEW bubble), so the queued prompt was spliced in ahead of the turn's own final bubbles.
+- **Fix:** defer the flush to AFTER finalization; plus a pure `shouldQueue()` (`queued-sends.ts`) that also consults the optimistic `sending` flag to close the turn-START gate gap. Commit `14ecaccc42`. Broadened the orphaned `vitest.tinkerui-panels.config.ts` glob to `tinker-ui/src/**`.
+- **Files:** `tinker-ui/src/app.ts` (chat-final handler + gate), `tinker-ui/src/queued-sends.ts` + `.test.ts`.
+
### FIXED [billable-noop+config-dead-code]: G1 Hourly interval heartbeat fired a BILLABLE Opus turn every tick (2026-06-04)
- **Symptom:** The hourly interval heartbeat fired a full, billable Opus turn on every tick (`target:"none"` — nobody consumes the output). A self-poll meant to be a cheap no-op was costing one Opus turn per hour, silently, forever.
@@ -117,10 +148,10 @@ fix, pick from this list — extend it only if no tag fits.
### FIXED [event-ordering+display-misclassify]: U5 Queued prompt bubble rendered in the MIDDLE of the still-streaming answer (2026-06-04)
- **Symptom:** A prompt queued mid-turn rendered its user bubble in the middle of the last answer — the queued bubble appeared above the streaming turn's later continuation/tool bubbles. A hard refresh fixed it (the server returns correct chronological order).
-- **Root cause:** the queued prompt's user bubble was pushed to the END of `messages[]`, but the still-streaming turn ALSO pushes its own continuation/tool bubbles to the end as they arrive → those landed AFTER the queued bubble, so the queued prompt appeared mid-answer. Note on server behavior: cc-bridge `worker.ts` genuinely QUEUES a mid-turn send (`turnQueue` → `drainQueue` = a separate NEXT turn) — it does NOT steer or blend into the running turn — so the queued bubble truly belongs AFTER the current turn finishes, not interleaved into it.
+- **Root cause:** the queued prompt's user bubble was pushed to the END of `messages[]`, but the still-streaming turn ALSO pushes its own continuation/tool bubbles to the end as they arrive → those landed AFTER the queued bubble, so the queued prompt appeared mid-answer. Note on server behavior: tinker-bridge `worker.ts` genuinely QUEUES a mid-turn send (`turnQueue` → `drainQueue` = a separate NEXT turn) — it does NOT steer or blend into the running turn — so the queued bubble truly belongs AFTER the current turn finishes, not interleaved into it.
- **Fix (tinker-ui/src/app.ts, HMR-live, committed develop `0bdb090c437`):** a `pendingQueuedSends` buffer holds the queued bubble OUT of `messages[]` and renders it as a TRAILING bubble; on turn-final it is flushed into `messages[]` in correct chronological order (after the completed turn's bubbles). True mid-turn steer/blend is deferred — it depends on claude-cli headless input injection.
- **Files:** `tinker-ui/src/app.ts` (`pendingQueuedSends` buffer, trailing-bubble render, turn-final flush). Task `task-mpwfiot2`.
-- **Rule:** a mid-turn-queued user prompt must be held OUT of the shared `messages[]` array (rendered as a trailing bubble) until the running turn finalizes — appending it eagerly races the streaming turn's own end-pushed bubbles and misorders the transcript. See `tool-loop.md` for the cc-bridge `turnQueue`/`drainQueue` next-turn semantics.
+- **Rule:** a mid-turn-queued user prompt must be held OUT of the shared `messages[]` array (rendered as a trailing bubble) until the running turn finalizes — appending it eagerly races the streaming turn's own end-pushed bubbles and misorders the transcript. See `tool-loop.md` for the tinker-bridge `turnQueue`/`drainQueue` next-turn semantics.
### FIXED [event-ordering+config-dead-code]: U10 ToT deliberation never applied for the current turn AND leaked into the next — runtimeContext-override path (2026-06-02)
@@ -184,9 +215,9 @@ fix, pick from this list — extend it only if no tag fits.
### FIXED [plugin-load+merge-wipe]: Plugin-SDK export drift — new subpath ships without manifest entry (2026-05-21)
-- **Symptom:** Gateway crashed with `EPIPE` at 15:39, systemd restarted it, and the now-cold module cache hit a missing `dist/plugin-sdk/provider-config-overlay.js`. cc-bridge plugin failed to load → Tinker UI and WhatsApp DM both went silent simultaneously because no LLM worker route was registered. Orphaned worker PID 19196 survived (bounded by the 0e475ba6 worker-pool-leak fix) but unreachable.
-- **Root cause:** `src/plugin-sdk/provider-config-overlay.ts` had existed since `566bf478a6` (2026-05-10) and `extensions/tinkerclaw-cc-bridge/index.ts` imported it via `openclaw/plugin-sdk/provider-config-overlay`, but the entry was missing from BOTH `scripts/lib/plugin-sdk-entrypoints.json` (the tsdown subpath manifest) AND the `./plugin-sdk/provider-config-overlay` entry in `package.json#exports`. tsdown therefore never built the dist artifact. Production `NODE_ENV=production` (systemd) prefers `dist/` over source via `root-alias.cjs`, so the resolver fell through to a synthesised `.../root-alias.cjs/provider-config-overlay` path that does not exist, and Node threw `ERR_MODULE_NOT_FOUND` on every plugin reload.
-- **Fix (commit `e065bc94f5`):** add `provider-config-overlay` to `scripts/lib/plugin-sdk-entrypoints.json` and regenerate `package.json#exports` via `pnpm plugin-sdk:sync-exports`. Verified after rebuild + restart: `chat.send` runId returned `result_text=ALIVE` in 3.4s; `openclaw plugins list` shows `@globalcaos/cc-bridge` as `status=enabled`.
+- **Symptom:** Gateway crashed with `EPIPE` at 15:39, systemd restarted it, and the now-cold module cache hit a missing `dist/plugin-sdk/provider-config-overlay.js`. tinker-bridge plugin failed to load → Tinker UI and WhatsApp DM both went silent simultaneously because no LLM worker route was registered. Orphaned worker PID 19196 survived (bounded by the 0e475ba6 worker-pool-leak fix) but unreachable.
+- **Root cause:** `src/plugin-sdk/provider-config-overlay.ts` had existed since `566bf478a6` (2026-05-10) and `extensions/tinkerclaw-tinker-bridge/index.ts` imported it via `openclaw/plugin-sdk/provider-config-overlay`, but the entry was missing from BOTH `scripts/lib/plugin-sdk-entrypoints.json` (the tsdown subpath manifest) AND the `./plugin-sdk/provider-config-overlay` entry in `package.json#exports`. tsdown therefore never built the dist artifact. Production `NODE_ENV=production` (systemd) prefers `dist/` over source via `root-alias.cjs`, so the resolver fell through to a synthesised `.../root-alias.cjs/provider-config-overlay` path that does not exist, and Node threw `ERR_MODULE_NOT_FOUND` on every plugin reload.
+- **Fix (commit `e065bc94f5`):** add `provider-config-overlay` to `scripts/lib/plugin-sdk-entrypoints.json` and regenerate `package.json#exports` via `pnpm plugin-sdk:sync-exports`. Verified after rebuild + restart: `chat.send` runId returned `result_text=ALIVE` in 3.4s; `openclaw plugins list` shows `@globalcaos/tinker-bridge` as `status=enabled`.
- **Prevention (commit `0b5c17f614`):** pre-push **Gate 4** (FORK 2026-05-21) installs `pnpm lint:plugins:plugin-sdk-subpaths-exported` (src→manifest drift) + `pnpm plugin-sdk:check-exports` (manifest→`package.json#exports` drift). Both checks were available as `pnpm` scripts before, but no pre-push hook was installed at all (`.git/hooks/` was empty), so they were never enforced on push. Bypass for intentional WIP: `SDK_EXPORTS_GUARD=off git push`.
- **Files:** `package.json` (exports), `scripts/lib/plugin-sdk-entrypoints.json`, `git-hooks/pre-push` (Gate 4).
- **Rule:** never add a new `src/plugin-sdk/*.ts` file without also adding its entry to the manifest AND regenerating `package.json#exports` in the same commit. Gate 4 will block the push otherwise. See `failures.md` M13.
@@ -467,7 +498,7 @@ fix, pick from this list — extend it only if no tag fits.
### FIXED [restart-recovery]: Architect re-prompt required after gateway restart (2026-05-13)
- **Symptom:** After `openclaw-restart --full`, Jarvis's session resumed via the openclaw-sessionId fallback (FORK 2026-05-10) but he did not autonomously continue mid-task; the user had to type "keep going".
-- **Root cause:** cc-bridge resume only re-attaches the claude-cli session; no `[System] continue` is injected. The 2026-04-20 generic continue had been bypassed by the 2026-05-10 fallback. No persisted plan meant the agent had nothing concrete to resume from.
+- **Root cause:** tinker-bridge resume only re-attaches the claude-cli session; no `[System] continue` is injected. The 2026-04-20 generic continue had been bypassed by the 2026-05-10 fallback. No persisted plan meant the agent had nothing concrete to resume from.
- **Fix:** new `prefrontal.plan.*` RPCs + boot-time `runRestartContinue` that dispatches a plan-aware `[System] continue` via `chat.send {deliver:false, dispatchAgent:true}`. The grey `__SYS_PLAN_RESUME__` chip surfaces the action in TUI.
- **Spec:** `docs/superpowers/specs/2026-05-12-prefrontal-plan-board-design.md` (commit `131f26d`).
- **Plan:** `docs/superpowers/plans/2026-05-13-prefrontal-plan-board-implementation.md` (commit `f991621`).
@@ -491,14 +522,14 @@ fix, pick from this list — extend it only if no tag fits.
- **Commit:** `ffbc4cb5cc`
- **Rule:** when a commit handler walks DOM state set by earlier event handlers, order strictly — validate → walk → strip → commit. Visual teardown is the LAST step, not the first. Treat marker-not-found as a hard error (warn + bail), never a fallback that pushes the dragged element to a default slot. See `[[feedback_walk_before_teardown]]` in jarvis-icu memory.
-### FIXED [event-ordering]: cc-bridge dual-path stream duplication after --include-partial-messages (2026-05-24)
+### FIXED [event-ordering]: tinker-bridge dual-path stream duplication after --include-partial-messages (2026-05-24)
- **Symptom:** "I still do not see answer-amygdala-fractal rendered correctly. On top of that, we are hitting the 'truncated' issue again. Messages should never truncate." User saw every block of streamed text appear twice in the rendered bubble — `"Good catches…Good catches…## 💬 ANSWER…"` — with later sections appearing to truncate.
-- **Root cause:** the prior commit `3e343cb5ee` added `--include-partial-messages` to the cc-bridge spawn args so claude-cli would emit fine-grained `stream_event.content_block_delta.text_delta` lines token-by-token. claude-cli STILL emits its cumulative `assistant` block-complete frames in parallel. cc-bridge's `stream.ts` handled both paths but the fine-grained handler did NOT update `blockTextSeen[ev.index]`. When the cumulative `assistant` frame arrived, the block-handler saw `prev = blockTextSeen[bi] = ""` → its slice condition `cumulative.length > prev.length` fired → it pushed the entire cumulative text as a "new delta" ON TOP OF what the fine-grained deltas had already pushed. With gap-split bubbles in the mix, `_segmentStart` cursors went past `finalText.length` during the tail-recover at finalization, which the user perceived as truncation.
-- **Fix:** in the `stream_event` handler at `extensions/tinkerclaw-cc-bridge/src/stream.ts:516`, mirror every fine-grained text_delta into `blockTextSeen[ev.index] += delta` (and the equivalent for thinking). The `index` field on `content_block_delta` is documented by the Anthropic API but typed only as `unknown` on `CcStreamStdoutStreamEvent`, so read via a narrow cast. The cumulative handler's slice condition then no longer fires for blocks the fine-grained path already covered.
-- **File:** `extensions/tinkerclaw-cc-bridge/src/stream.ts`
+- **Root cause:** the prior commit `3e343cb5ee` added `--include-partial-messages` to the tinker-bridge spawn args so claude-cli would emit fine-grained `stream_event.content_block_delta.text_delta` lines token-by-token. claude-cli STILL emits its cumulative `assistant` block-complete frames in parallel. tinker-bridge's `stream.ts` handled both paths but the fine-grained handler did NOT update `blockTextSeen[ev.index]`. When the cumulative `assistant` frame arrived, the block-handler saw `prev = blockTextSeen[bi] = ""` → its slice condition `cumulative.length > prev.length` fired → it pushed the entire cumulative text as a "new delta" ON TOP OF what the fine-grained deltas had already pushed. With gap-split bubbles in the mix, `_segmentStart` cursors went past `finalText.length` during the tail-recover at finalization, which the user perceived as truncation.
+- **Fix:** in the `stream_event` handler at `extensions/tinkerclaw-tinker-bridge/src/stream.ts:516`, mirror every fine-grained text_delta into `blockTextSeen[ev.index] += delta` (and the equivalent for thinking). The `index` field on `content_block_delta` is documented by the Anthropic API but typed only as `unknown` on `CcStreamStdoutStreamEvent`, so read via a narrow cast. The cumulative handler's slice condition then no longer fires for blocks the fine-grained path already covered.
+- **File:** `extensions/tinkerclaw-tinker-bridge/src/stream.ts`
- **Commit:** `d32e44cc24`
-- **Rule:** when two paths can both deliver the same logical content (token-deltas + cumulative re-emit), the secondary tracker MUST be updated by BOTH paths or the consuming path will double-emit. Diagnostic recipe: count occurrences of identical text blocks in the rendered UI snapshot at `~/.openclaw/data/tinker-ui-snapshot.html` vs the source JSONL at `~/.claude/projects//.jsonl` — if JSONL has it once but UI has it twice → dual-path push regression. See `[[project_cc_bridge_streaming_partial_messages]]` in jarvis-icu memory.
+- **Rule:** when two paths can both deliver the same logical content (token-deltas + cumulative re-emit), the secondary tracker MUST be updated by BOTH paths or the consuming path will double-emit. Diagnostic recipe: count occurrences of identical text blocks in the rendered UI snapshot at `~/.openclaw/data/tinker-ui-snapshot.html` vs the source JSONL at `~/.claude/projects//.jsonl` — if JSONL has it once but UI has it twice → dual-path push regression. See `[[project_tinker_bridge_streaming_partial_messages]]` in jarvis-icu memory.
### FIXED [config-dead-code]: Gateway-side code change didn't take effect after `openclaw gateway restart` (no rebuild step) (2026-05-24)
@@ -512,7 +543,7 @@ fix, pick from this list — extend it only if no tag fits.
- **Symptom:** "Prefrontal says claude is still running, with frozen clock, but I don't see thinking activity anywhere else. Should it not go back to idle?" Server-side `prefrontal.tree` RPC returned `{active:false, root:null}` while the Tinker UI's prefrontal panel still showed an active claude run with a frozen elapsed timer.
- **Root cause:** the in-tab graceful-restart path skipped enrolling runs in `unconfirmedRuns`. When the gateway sends a `shutdown` frame with `restartExpectedMs`, `app.ts` line 1281-1292 marks every `activeRuns` entry with `state="restarting"` (to hold the indicator across the restart) but does NOT add the runIds to `unconfirmedRuns`. The reconnect-hello handler at line ~1269 then calls `scheduleUnconfirmedPrune()` unconditionally, but that function early-returns at `unconfirmedRuns.size === 0`. So the 30s prune timer is never scheduled, the gateway process that owned those runs is dead (no lifecycle:end will ever come), and the entries stay in `activeRuns` forever. Page-reload was the only path that cleared them because `restoreActiveRuns()` re-runs and repopulates `unconfirmedRuns` from sessionStorage.
-- **Fix:** in the shutdown-frame handler, `unconfirmedRuns.add(runId)` for each active run alongside the `state="restarting"` mark. After the next reconnect, `scheduleUnconfirmedPrune()` schedules the 30s timer for these runs and they're cleaned up automatically. If lifecycle:start for the same runId arrives after reconnect (cc-bridge resume preserves runId), the existing `unconfirmedRuns.delete(p.runId)` at line 2338 confirms it — no spurious prune.
+- **Fix:** in the shutdown-frame handler, `unconfirmedRuns.add(runId)` for each active run alongside the `state="restarting"` mark. After the next reconnect, `scheduleUnconfirmedPrune()` schedules the 30s timer for these runs and they're cleaned up automatically. If lifecycle:start for the same runId arrives after reconnect (tinker-bridge resume preserves runId), the existing `unconfirmedRuns.delete(p.runId)` at line 2338 confirms it — no spurious prune.
- **File:** `tinker-ui/src/app.ts` (in-tab shutdown handler at the WS-frame dispatch)
- **Commit:** see HEAD of this commit batch (paired with the bible verify added in `tool-loop.md`)
- **Rule:** any state-clear pathway that depends on `unconfirmedRuns` being populated MUST be paired with an enrollment site at the moment the state becomes orphan-eligible. The early-return-on-empty optimization is fine for the no-runs case but it silently fails the no-enrollment case — paired enrollment + prune is the contract.
@@ -523,12 +554,12 @@ fix, pick from this list — extend it only if no tag fits.
- **Root cause:** Opus has started emitting the section markers as markdown H2/H3 headings (`## 💬 ANSWER` etc.) instead of bare emoji+label. The splitter regexes at `tinker-ui/src/app.ts:3474-3477` only tolerated optional `**`/`__` bold wrapping, not `#` heading marks. With `## ` (non-whitespace) before the emoji, `(^|\n)\s*💬` failed to match → `text.search(ANS_MARKER_RE)` returned -1 for all three markers → `splitSectionedReply` returned null → `renderSectionedReply` never fired → fallback regular-assistant-bubble path rendered the entire reply with markers as literal H2 text.
- **Fix:** insert `#{0,4}\s*(?:\*\*|__)?\s*` after the `(^|\n)\s*` anchor in all three marker regexes (`AMY_MARKER_RE`, `ANS_MARKER_RE`, `FRA_MARKER_RE`). Tolerates `#` through `####` headings and optional bold wrapping in either order around the emoji. Confirmed matches against all six observed shapes: `💬 ANSWER:` `💬 **ANSWER**` `💬 **ANSWER:**` `## 💬 ANSWER` `### 💬 ANSWER` `**💬 ANSWER**`.
- **File:** `tinker-ui/src/app.ts` (`AMY_MARKER_RE` / `ANS_MARKER_RE` / `FRA_MARKER_RE`)
-- **Commit:** `d32e44cc24` (shipped together with the cc-bridge dual-path fix above)
+- **Commit:** `d32e44cc24` (shipped together with the tinker-bridge dual-path fix above)
- **Rule:** marker-detection regexes for content emitted by an LLM must tolerate the model's natural surface variants (heading marks, bold wrapping, mixed casing) instead of pinning to one shape. When the splitter returns null the entire reply falls through to a generic render and the user sees raw markers as text — far worse than a slightly permissive regex.
### FIXED [ui-state-clear]: Unsent composer draft lost on hard refresh (2026-06-06)
-- **Symptom:** text typed into a tab's chat composer but not yet sent was lost on a hard refresh / crash. Oscar lost a 20-minute prompt this way.
+- **Symptom:** text typed into a tab's chat composer but not yet sent was lost on a hard refresh / crash. the owner lost a 20-minute prompt this way.
- **Root cause:** drafts lived only in the in-memory `tabStates` Map (`TabState.draft`), which a hard refresh wipes, plus a SINGLE global `localStorage` key `DRAFT_STORAGE_KEY="tinker-draft"`. On reload only that one global slot was restored — so per-tab drafts were lost and all tabs shared one draft.
- **Fix:** persist drafts PER TAB in `localStorage` keyed by tab id (`tinker-draft:`), replacing the single global key; the composer input listener write-throughs to the active tab's per-tab key + `TabState.draft` on every keystroke; the connect/init flow rehydrates each restored tab's draft and loads the active tab's into the composer; a successful send clears both.
- **Status / files:** `tinker-ui/src/app.ts` (`saveDraftFor`/`loadDraftFor`/`clearDraftFor`, `DRAFT_STORAGE_KEY_PREFIX`). HMR-live; **uncommitted** (recovery patch jarvis-icu `9fe305a`; lands on develop at next commit). Task `task-mpzzs5nc`.
@@ -544,3 +575,146 @@ fix, pick from this list — extend it only if no tag fits.
- **Rule:** protect user-meaningful titles with an EXPLICIT lock flag, not a format heuristic — a heuristic that infers "is this a custom name?" from the string shape will misfire. The server `cookiePhrase` may only fill a tab that has no locked title.
---
+
+### FIXED [ui-state-clear+event-ordering]: thinking indicator stuck ON after a fractal turn (2026-06-22)
+
+- **Symptom:** Jarvis finished answering and the fractal-reflection row docked as complete, yet the chat thinking-dots stayed lit as if still working — indefinitely.
+- **Root cause:** the fractal-triage lane runs as a UI-visible subagent (`agent::subagent:`; `deliver:false` gates delivery, NOT visibility), so its `lifecycle:start` adds an `activeRuns` entry that `renderThinkingIndicator` shows (as a `▸` subagent of the viewed session). Its chat events take the `handleSubagentChatEvent` path, which `return`s BEFORE the main-run chat handler's authoritative tier-1 `activeRuns.delete` (done-signals.md §2 #1). So a subagent's ONLY `activeRuns` terminator was the tier-3 debounced `lifecycle:end`, which (a) is gated on `p.data?.model` and (b) is dropped entirely on hard teardown (SIGTERM / gateway-restart / timeout). With R2 (no UI stale-run watchdog) nothing backstopped it → the run stayed pinned and the dots never cleared.
+- **Fix:** `handleSubagentChatEvent` now extends tier-1 authority to subagents — on `final/aborted/end/error` it cancels the pending lifecycle:end timer, `activeRuns.delete`s, `rememberTerminated`s (blocks a late-delta resurrection; a genuine fallback restart re-admits via `lifecycle:start`), and recomputes `sending` from the viewed tab — mirroring the main-run path. Subagents now have the same two independent terminators (tier-1 chat + tier-3 lifecycle) as the main run.
+- **Status / files:** `tinker-ui/src/app.ts` (`handleSubagentChatEvent`). HMR-live + dist rebuilt. Owning optic updated: `done-signals.md` §2 R1 subagent corollary + new verify block + §5 row.
+- **Rule:** any run that can appear in `activeRuns` needs a tier-1 (authoritative, immediate) terminator, not just the tier-3 debounced one. The subagent chat path was added later (FORK 2026-05-30, live subagent streaming) and inherited only half the done-signals contract — it rendered subagent liveness but never closed the run.
+
+---
+
+### FIXED [timeout-tuning+cleanup-race]: Bridge resume-stall — a fat resumed transcript chokes `claude --resume`, then auto-retry re-resumes the same fat transcript (2026-06-23)
+
+- **Symptom:** "Jarvis not responding" — a chat turn produced 2–3 init lines then `text.len=0` / `thinking.len=0` for the full 600s `DEFAULT_REQUEST_TIMEOUT_MS` window, the idle watchdog SIGTERM'd the worker, and the automatic idle-timeout retry re-resumed the SAME session and reproduced the identical stall.
+- **Root cause:** a 14.5MB resumed session transcript fed to `claude --resume ` choked claude-cli's transcript ingestion at spawn — it emitted only its init lines and never began producing content, so the turn looked alive (process up) but was effectively wedged until the 600s idle watchdog fired. The auto-retry then re-resumed the unchanged 14.5MB transcript → same choke → same SIGTERM. The `gm`/credential angle was a **RED HERRING**: the bridge injects no credential; the spawned `claude` reads its own `~/.claude/.credentials.json` (see tool-loop.md §Auth), so the stall was purely transcript-size-driven, not auth.
+- **Fix (three commits):**
+ 1. **`b7ea26b0a6`** — fail-open **oversized-resume guard**: before spawning `claude --resume `, `stat` the transcript; if it exceeds `RESUME_MAX_TRANSCRIPT_BYTES` (8MB) start a **FRESH** session instead of resuming the fat one. Fail-open on any stat error (resume as before — never block a turn on a stat failure). New module `transcript-path.ts` resolves the transcript path.
+ 2. **`923be5f3e3`** — **fast-fail init-only stall watchdog**: abort early (SIGTERM) when the worker has emitted only init lines AND `text.len=0`/`thinking.len=0` past `FAST_FAIL_INIT_SILENT_MS` (90s), **gated on `linesSeen <= FAST_FAIL_MAX_INIT_LINES`** so a heavy legitimate tool turn (which emits many lines) is never killed. Does NOT lower the 600s `DEFAULT_REQUEST_TIMEOUT_MS` — it only catches the specific never-started shape fast.
+ 3. **`fbebe20648`** — **suppress futile same-model re-resume**: when the aborted attempt produced zero content (`producedNoContent`), the idle-timeout failover no longer re-resumes the same model/session — re-resuming an unchanged fat transcript can only reproduce the stall.
+- **Files:** `extensions/tinkerclaw-tinker-bridge/src/{transcript-path.ts (new),worker.ts,stream.ts,defaults.ts}`, `src/agents/embedded-agent-runner/run.ts`; tests `transcript-path.test.ts`, `stream.fast-fail.test.ts`, `run/assistant-failover.test.ts`.
+- **Rule:** a resumed claude-cli transcript over `RESUME_MAX_TRANSCRIPT_BYTES` (8MB) must start FRESH, not `--resume`; and an idle-timeout retry must never re-resume an attempt that produced zero content — an unchanged transcript reproduces the stall. The bridge injects no credential, so "Jarvis not responding" is a transcript/timeout problem, not an auth one. See tool-loop.md §"Resume size guard + init-only fast-fail (FORK 2026-06-23)" and failures.md M1 (idle-watchdog SIGTERM).
+
+### FIXED [config-dead-code]: memory-core vec0 table never re-dropped after a restart — a stale 3072-dim table blocked every 1024-dim insert (2026-06-23)
+
+- **Symptom:** memorySearch embedding inserts threw `Expected 3072 dimensions but received 1024` — 79 times before a reboot. The 1024-dim `mxbai-embed-large` (ollama) vectors could not be written into a vec0 table that had been created at 3072 dims by a previous Gemini embedder.
+- **Root cause:** `ensureVectorTable` only dropped + recreated the vec0 table when `this.vector.dims` was truthy. After a gateway restart `this.vector.dims` is `undefined` (not yet resolved), so the guard short-circuited and the stale 3072-dim table from the prior Gemini embedder **survived the restart** — and every new 1024-dim insert mismatched it. A looked-fine / never-corrected schema: the table existed, so nothing recreated it, even though its dim was wrong.
+- **Fix (commit `a33cc63200`):** `ensureVectorTable` now reads the **actual on-disk** `FLOAT[N]` dimension from `sqlite_master` and drops + recreates the table only on a **genuine** mismatch between the on-disk N and the live embedder's dim — independent of whether `this.vector.dims` is set yet. A restart with `dims` still undefined no longer leaves a wrong-dim table standing.
+- **Files:** `extensions/memory-core/src/memory/manager-sync-ops.ts` (`ensureVectorTable`).
+- **Rule:** a schema-correcting guard must compare against the **on-disk** shape (read `sqlite_master`), never gate the correction on an in-memory field that is unset early in boot — or a wrong-shaped artifact survives every restart. See memory-layout.md §"memorySearch vec0 table — embedder-dim reconcile (FORK 2026-06-23)".
+
+---
+
+### FIXED [think-clamp+detection-pattern]: an over-ceiling thinking level on a model that doesn't support it ERRORED instead of clamping down (cross-model) (2026-06-24)
+
+- **Symptom:** a Tinker tab pinned to `openai/gpt-5.5` with the effort slider at **Max** errored `Thinking level "max" is not supported for openai/gpt-5.5. Use one of: off, minimal, low, medium, high, xhigh.` — the turn refused to run instead of just thinking as hard as the model allows.
+- **Root cause:** the effort slider's top stop is injected as an **EXPLICIT** `/think max` directive (`chat-command-body.ts`), so the resolver classified it as explicit and took the **REJECT** branch (`get-reply-run.ts` ~:626), skipping the clamp that already existed for the non-explicit case two lines below. The same reject-on-explicit-and-unsupported shape existed in three sibling resolution sites — the cross-model analogue of the 2026-06-19 `claude-code` thinking-profile gate (which had the dual problem: a level the model DID support was rejected because the profile was missing; this is a level a model genuinely does NOT support being rejected instead of clamped).
+- **Fix:** clamp DOWN in ALL FOUR reject sites — an over-ceiling level now resolves to the model's highest supported level (`max`→`xhigh` for gpt-5.5) via the canonical `resolveSupportedThinkingLevel` (`thinking.ts`, ordered by `THINKING_LEVEL_RANKS` in `thinking.shared.ts`) and the turn PROCEEDS, with an info note when `requested !== applied`. Model-agnostic; models that DO support `max` (`claude-code/*`) are unaffected. SHAs (develop): `8e4055c773` chat.send path (`get-reply-run.ts`, removed the explicit-think early reject; `logVerbose` note), `f1f2ffaefd` `/think` directive (`directive-handling.impl.ts`, in-place clamp of `directives.thinkLevel`; ack-note guard generalized `=== "max"` → `requested !== applied`), `3036658974` persisted level (`sessions-patch.ts`, **removed the `"thinkingLevel" in patch → invalid` reject** — a clamped patch now always succeeds; behavioral change worth recording), `b5950c7015` CLI agent path (`agent-command.ts`, dropped the explicit-think throw; stderr note), `86f537a7da` tests (`thinking.clamp.test.ts` — `max`→`xhigh` on gpt-5.5, unchanged on supported, `max` stays `max` on claude-opus-4-8).
+- **Files:** `src/auto-reply/reply/get-reply-run.ts`, `src/auto-reply/reply/directive-handling.impl.ts`, `src/gateway/sessions-patch.ts`, `src/agents/agent-command.ts`, `src/auto-reply/thinking.clamp.test.ts`.
+- **Rule:** an unsupported thinking level (explicit slider directive OR persisted/non-explicit) must CLAMP to the model's nearest supported level and proceed — never hard-error. The slider's Max is a CEILING request, not a contract the model must honor exactly. See auth-routing.md §"Thinking-level clamp — unsupported levels clamp, never reject (cross-model, FORK 2026-06-24)" (the verify block asserting all four reject sites clamp via `resolveSupportedThinkingLevel`). Cross-model analogue of the 2026-06-19 claude-code thinking-profile gate (memory `reference_thinking_profile_gate_and_cc_bridge_real_name`).
+
+---
+
+### FIXED [tab-bleed]: subagent messages from one tab streamed into a second open tab (2026-06-25)
+
+- **Symptom:** with two tabs open, a fan-out launched from tab A streamed its subagent sub-bubbles into tab B as well — two separate conversations bled into each other.
+- **Root cause:** subagent session keys are minted FLAT under the agent root — `agent:main:subagent:` — with NO parent-tab encoding (the spawning tab's key does not propagate into the child). `chatEventIsSubagentOfView()` (`tinker-ui/src/app.ts`) admitted a subagent into the view whenever `evtKey.startsWith(agentRoot + ":subagent:")`, and EVERY tab is under the same `agent:main` root (main = `agent:main:main`, extra tabs = `agent:main:dashboard:`). So the agent-root match claimed every subagent for every tab. This was a deliberate 2026-06-15 loosening (the prior strict full-key prefix made subagent lanes vanish from non-`:main` tabs) that over-corrected from "vanish" into "bleed" — the two-bug seesaw of a key format that simply lacks the parent link.
+- **Fix:** attribute each subagent to the ONE tab that spawned it via its `parentRunId`. A new `subagentOwnerTab` map is populated at subagent birth (`recordSubagentOwner` at the agent-event handler entry) by resolving `parentRunId → activeRuns[parentRunId].sessionKey` — the owning tab's key — while the parent run is still active, resolved transitively past intermediate subagent runs. `chatEventIsSubagentOfView()` now returns true for a subagent ONLY when its resolved owner is the viewed session; when the owner is not yet resolved it falls back to the loose agent-root match ONLY if a single attached tab shares the root (no sibling to bleed into), else refuses (the subagent still appears in the Prefrontal/EEG panels and the parent turn's output is intact). Fixes both the chat consumer and the EEG/effort consumer (same shared predicate), and does NOT regress the 2026-06-15 case — a subagent of a tinker/dashboard tab resolves to that tab and shows in it. `tinker-ui/src/app.ts`; built via `vite build` (tsc OOMs on app.ts).
+- **Files:** `tinker-ui/src/app.ts` (`subagentOwnerTab`, `recordSubagentOwner`, `attachedTabCountForRoot`, `chatEventIsSubagentOfView`).
+- **Rule:** when a child entity's identity key cannot encode its parent, attribute it by the run graph (`parentRunId → owning session`), captured at birth while the parent is live — never by a shared-prefix heuristic, which conflates siblings. A matcher that must choose between "vanish" and "bleed" is under-specified: it needs the parent link, not a looser/stricter prefix. See tinker-ui.md §5.8L.
+
+---
+
+### FIXED [scope-mismatch+timeout-tuning]: Tinker tab auto-rename — webchat scope, cc-bridge cold-spawn timeout, clones have no trigger, intermittent doubled title (2026-06-25)
+
+- **Symptom:** tab auto-titling regressed in layers: (a) titles stopped generating at all; (b) after a fix, NEW tabs renamed but CLONED tabs never did; (c) some new-tab titles came out doubled (`"Fix auth bugFix auth bug"`); (d) tabs that had blinked the rename shimmer for hours could never be renamed again.
+- **Root cause** (four distinct causes, peeled one at a time — three were only found after a 5-agent fan-out refuted the obvious client-logic guesses and a gateway-log read showed the title RPC firing then timing out):
+ 1. **Scope.** The title was generated by a webchat-issued `fork.subagents.spawn`, which is `operator.admin`-scoped; the Tinker UI is a webchat client (`operator.read`), so the spawn was rejected and no title was ever produced.
+ 2. **Timeout.** After moving to a dedicated `sessions.suggestTitle` RPC running a one-shot cc-bridge Sonnet completion, the bridge COLD-SPAWNS a full `claude` worker per title (~14–19s wall-clock, mostly startup). At `timeoutMs: 15_000` the run hit `FailoverError: LLM request timed out`, the RPC returned `null`, and the tab silently failed to rename. This is why NEW tabs renamed (titled at turn-`end`, when the brain had just freed a bridge worker → fast) but CLONES did not (titled at clone-time, mid-cold-spawn → >15s → timeout). A leak of ~500 transient `llm-client-*` systemd units (one per spawn, never reaped) had progressively slowed spawns (first title ~6s → later 14–19s).
+ 3. **No trigger for clones.** The ONLY automatic titler trigger is the assistant-turn `end` handler; a freshly cloned tab emits no turn-end event, so even with the timeout fixed the clone was never titled. (Buffer-seeding attempts were dead ends — an active clone reads the global `messages` buffer that `loadChat()` overwrites, and a forked clone already carries the parent's user prompts server-side; the missing piece was the TRIGGER.)
+ 4. **Stale persisted flag + intermittent doubled payload.** `titleGenerating` (the shimmer/in-flight flag) was persisted in `saveTabs()` and restored by `loadTabs()`; a tab saved mid-generate restored `titleGenerating=true` forever and the dedup guard `if (tab.titleGenerating) return` then blocked every future rename. Separately, the cc-bridge intermittently duplicates a one-shot's text payload in a single block and the client passed it straight to the tab name.
+- **Fix:**
+ - **RPC + scope** (commit `ef337eb1dd`): new `sessions.suggestTitle {sessionKey, prompt}` → `suggestTitleViaBridge()` (`server-methods/suggest-title.ts`) one-shot `runEmbeddedPiAgent({provider:"claude-code", model:"claude-sonnet-4-6"})` in a `temp:title-suggest` session; method added to the READ_SCOPE group so the webchat can call it (subscription cc-bridge — NOT the metered API, NOT Ollama/Gemini).
+ - **Timeout** (commit `00c8e5b79b`): `timeoutMs` 15_000 → **45_000**; gateway restart also cleared the ~500 leaked `llm-client` units.
+ - **Clone kick** (HMR-live, `app.ts`): `cloneTab()` sets `pendingTitleKickTabId`; `loadChat()` fires `generateTabTitle(clone)` once, after the buffer refills, gated on the buffer having a user message.
+ - **Stale-flag** (HMR-live, `app.ts`): stop persisting `titleGenerating` (stripped in `saveTabs`, reset in `loadTabs`); dedup generation via a runtime-only `titleInFlight` set keyed by tab id.
+ - **Doubled-title guard** (HMR-live, `app.ts`): `collapseDoubled()` in `generateTabTitle`'s cleaning collapses an EXACT first-half==second-half title (≥4-char halves), display-layer only.
+- **Files:** `src/gateway/server-methods/{suggest-title.ts (new), sessions.ts}`, `src/gateway/method-scopes.ts` (committed); `tinker-ui/src/app.ts` (HMR-live, commit pending — contended hot file).
+- **Rule:** a webchat-triggered privileged action must run through a NARROW webchat-scoped RPC (privileged work happens server-side), never a direct `operator.admin` call from the UI. A cc-bridge one-shot's timeout must cover the COLD `claude` spawn (~15–20s), not just generation — a tight timeout returns `null` and the feature silently no-ops. An entity with no lifecycle event of its own (a fresh clone) needs an explicit trigger, not a buffer fix. Never persist a transient in-flight/shimmer flag — a mid-flight save strands it true and a dedup guard then blocks the action forever. See session-naming.md §"Auto-title mechanism — cc-bridge Sonnet RPC + clone kick".
+- **Deploy lesson:** building the gateway dist at `develop` HEAD deployed a parallel session's committed-but-UNDEPLOYED commit ("Mechanism A", `899f50b8be`). When a shared branch carries others' un-deployed work, deploy a single isolated fix from the LAST-DEPLOYED baseline (read `dist/build-info.json` for its commit) + overlay only your changed file — never from HEAD.
+
+### FIXED [detection-pattern]: fs-link linkifier rejected real paths with spaces or accents — Olivella project paths rendered as dead text (2026-07-08)
+
+- **Symptom:** backtick-wrapped absolute paths containing spaces or accented letters (e.g. `/home/.../HOME Olivella/Llicència projecte/instancia.md`) did not become clickable `.fs-link` spans; user had to be handed an ASCII symlink as a workaround. FRACTAL also failed to flag it as a code bug (wrote a memory note instead).
+- **Root cause:** the `md()` path regex (app.ts ~6353) used char class `[\w./-]` — `\w` is ASCII-only in JS, so accents failed, and spaces were deliberately excluded (2026-04-20 `fccc4fb281`, formalised 2026-06-24 `ba3902c975`) to avoid linkifying shell commands. The conservatism was a safety choice that over-shot: the `` span is already delimited, so spaces inside a path-shaped span are safe to admit.
+- **Fix (2 surfaces, parallel agents):**
+ - **Regex** (`tinker-ui/src/app.ts`, HMR-live): `\p{L}\p{N}` with `/u` flag admits unicode; spaces admitted via `| (?![-\s])` — a space followed by `-` or whitespace (shell-flag shape ` --flag`/` -f`) rejects the span. Shell metacharacters `|;=&"` stay outside the class. Verified: Olivella path matches; `/home/x/run.sh --flag` and `npm install` do not; `vite build` clean.
+ - **FRACTAL prompt** (app.ts ~6790): added mandate — friction traced to a bug in our own code must be NAMED as a bug and the fix ATTEMPTED in the reflection (or repro filed here); workaround/memory-note alone = MISS.
+- **Lesson:** a "safety" char class in a matcher is still a detection pattern — when the container (here the `` delimiter) already bounds the input, the class can be widened without losing the guard; encode the rejection as a targeted negative lookahead instead of banning whole character families.
+
+---
+
+## 2026-07-08 — WhatsApp group "Jarvis" prefix stopped triggering (two-gate root cause)
+
+- **Symptom (the owner):** "when I send a message starting with Jarvis from a group chat, he doesn't respond anymore."
+- **Evidence (journald, `openclaw-gateway`, 2026-07-08):** 7 group messages `allowed=false` at access-control today, **0 allowed**; e.g. `access: allowed=false isSelfChat=true from=120363417998848184@g.us fromMe=false` → `DROP: access denied` → `DROPPED by normalizeInboundMessage`. No `[wa-trigger] firing` in ANY `@g.us` group in 7 days.
+- **Gate 1 (ACTIVE, fixed):** `channels.whatsapp` set **no explicit `groupPolicy`**, relying on the plugin-sdk fallback. Telegram sets `"groupPolicy":"open"` explicitly; whatsapp did not. During this morning's gateway self-restart storm (see memory `reference_jarvis_self_restart_looks_stuck`), group-policy resolved to the fail-closed `"allowlist"` → every group message blocked at `checkInboundAccessControl` **before** the `decideTrigger` "jarvis"-prefix gate could run. `resolveDmGroupAccessDecision` coerces anything ≠ `"open"`/`"disabled"` to `"allowlist"`, so a fallback-default flip (e.g. an upstream `chunk: advance …` SDK merge) silently kills groups with no error. **Fix:** added explicit `"groupPolicy":"open"` to `channels.whatsapp` in `~/.openclaw/openclaw.json`. Verified via probe against live config+dist: `resolveWhatsAppInboundPolicy` → `groupPolicy:"open"`; `checkInboundAccessControl` now returns `allowed=true` for group `fromMe=false` (owner-lid AND stranger) and `fromMe=true`.
+- **Gate 2 (LATENT, unconfirmed — needs live repro):** `applyGroupGating` (`extensions/tinkerclaw-whatsapp/src/auto-reply/monitor/group-gating.ts`) bypasses the mention requirement only when `inNoPrefixList || ownerPrefixTriggered`, and `decideTrigger` sets `ownerPrefixTriggered = hasPrefix && fromMe`. So the "Jarvis" **text** prefix bypasses mention-gating **only when `fromMe=true`**. If the owner's group messages arrive `fromMe=false` (the 7 blocked ones all did — plausibly his 2nd number `+34679499426` or a LID participant), then post-Gate-1 they reach the mention gate and are **silenced** in any group NOT in `noPrefixChats` (his agent-groups are listed and unaffected). The 2026-05-09 invariant "owner+Jarvis triggers in ANY chat" is keyed off `fromMe`, not off the identity-verified `owner = isOwnerSender()` already computed two lines away — a fragility hole.
+- **Proposed Gate-2 fix (deferred until fromMe confirmed live):** in `decideTrigger` set a `msg.prefixTriggered` flag independent of `fromMe`; in `applyGroupGating` widen the bypass to `inNoPrefixList || ownerPrefixTriggered || (owner && prefixTriggered)`. `owner` is identity-based (secure), so non-owners still require a mention. Needs extension rebuild + gateway restart via `gateway-full-restart.sh --note` (NEVER inline — inline restart kills the answering turn).
+
+### FIXED [detection-pattern+scope-mismatch]: fs-link "still doesn't work" — the REAL killer was the server allowlist, not the regex; plus latent NFD accent miss (2026-07-08, addendum to the entry above)
+
+- **Symptom:** after the regex fix above shipped, the user reported the link STILL dead — including the ASCII symlink `~/Documents/instancia-ampliacio-esmena.md` that contained no spaces or accents at all (the tell that the client matcher was never the whole story).
+- **Root cause (two layers, only the first was fixed the first time):** (1) client regex — fixed above; (2) **`config.openExternalFile`'s root allowlist** (src/gateway/server-methods/config-open-external.ts `buildAllowlist`) covered only workspaceDir, `~/.openclaw`, `~/src/tinkerclaw`, `~/src/jarvis-icu` — every click on a real document under `~/Documents` got `{ok:false,"outside allowlist"}` server-side, silently rendered as a 4s red flash. The first debugging pass stopped at the first plausible cause (visible dead text) and never traced the CLICK leg end-to-end.
+- **Fix:** allowlist widened with `~/Documents`, `~/Downloads`, `~/Desktop`, `~/Pictures` (ADMIN_SCOPE-gated, viewer-open only, so proportionate); live after the morning gateway restart — verified end-to-end via `openclaw gateway call config.openExternalFile` → `ok:true` on the Documents path. Latent third layer also closed: `\p{M}` added to both regex char classes so NFD-decomposed accents (macOS-style filenames) linkify too — NFC/NFD both MATCH, shell-flag negatives still rejected, vite build clean.
+- **Lesson:** "the link doesn't work" spans TWO legs — render (does it linkify?) and action (does the click succeed?). Fixing the render leg and declaring victory without exercising the action leg is the classic first-plausible-cause stop. A feature's failure report must be tested at the LAST hop (the RPC), not the first (the regex).
+- [fs-link-bare-ascii] 2026-07-08 — bare-filename linkifier (app.ts ~6374) required ASCII letter first + \w chars, so `memòria_informe.html` (accented) and `3d_raw_data.html` (digit-first) rendered as dead text. FIXED: \p{L}\p{M}\p{N} char classes + /u flag, mirroring the absolute-path regex fixed the same day. Repro: send a chat message containing `` `3d_raw_data.html` `` → must render as clickable fs-link.
+- [fs-link-bare-ascii][server] 2026-07-08 — SAME ASCII bug ×3 layers: after the client render fix, click still died — `files-resolve-bare.ts` isSafeFilename was \w-only (rejected `memòria_informe.html` as "invalid name") AND buildRoots lacked ~/Documents (files under Documents/Insync/\_\_Projects never found; open-external allowlist already permitted them) AND MAX_DEPTH 4 missed depth-5 subfolders. FIXED: \p{L}\p{M}\p{N} /u + Documents/Downloads/Desktop/Pictures roots + depth 6. LESSON: a charset/validation rule duplicated client+server must be grepped across ALL layers on first hit, not patched at the crash site.
+
+- [wacli-ghost-skill] 2026-07-08 — skill `wacli` (workspace .claude/skills) documents a full CLI but NO binary exists anywhere (`which wacli` 127, no ~/.wacli store); first real use failed mid-outreach. Repro: `wacli --help` → command not found. Fix direction: install the binary or mark the SKILL.md as requiring install; skills should be validated against the live system at install time.
+- [gateway-no-config-reload] 2026-07-08 — `channels.whatsapp.allowFrom` edits require a FULL gateway restart (no `config.reload` RPC; policy read from in-memory config at request time). Repro: add number to allowFrom → `openclaw message send` still rejects until restart. Fix direction: config.reload RPC or per-request re-read of channel policy.
+- [detached-children-reaped] 2026-07-08 — setsid+nohup+disown from a CC turn does NOT survive turn end (sandbox reaps the process group): scheduled restart+send (pid 77566, 07:10) never ran, log never created. RESOLVED PATTERN: `systemd-run --user --on-active=N` transient unit — verified working 14:22 (restart executed, log written). Fix direction: document in spawn prompt or provide a real `openclaw defer`.
+- [send-before-channel-connected] 2026-07-08 — `openclaw message send` right after gateway restart fails with generic `gateway timeout after 10000ms` while whatsmeow is still connecting (~110 s: restart 14:22:06 → connected 14:23:57); the queued send died at the door with no distinguishable error. Fix direction: send RPC should return "channel not ready" or queue until connected.
+- [messageprefix-first-person-sends] 2026-07-08 — outbound `openclaw message send` with USER-dictated first-person content carries the agent's `messagePrefix` (🤖) unless the chat is in noPrefixChats — Montserrat outreach (11:48) likely arrived robot-prefixed. Fix direction: `--no-prefix` flag or authorship field on the send RPC.
+- [compaction-thrash-cc] 2026-07-08 — cc-bridge session compaction loop: EVERY embedded-runner compaction wait hits the 60 s aggregate timeout (attempt.ts COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS) because the summarize call routes to the same busy claude-code provider; runner falls back to pre-compaction snapshot → context never shrinks → compaction re-fires every 5-7 min (journal 13:59/14:22/14:29/14:35 session mqujlzcp) → each cycle can cut the live worker's API stream, surfacing raw "API Error: The socket connection was closed unexpectedly" as an assistant bubble. Fix direction: (a) serialize compaction with the in-flight turn or route summarize to a secondary worker; (b) raise aggregate timeout for cc provider (60→180 s); UI symptom mitigated same day (see next entry).
+- [api-error-bubble] 2026-07-08 — FIXED (UI leg): raw SDK transport errors ("API Error: …socket connection was closed…") rendered as full assistant replies; now detected in app.ts error-bubble branch (~7458, length-guarded <400 chars) and shown as the compact centered error bubble. Live via vite HMR, esbuild clean. Root cause is [compaction-thrash-cc] — gateway leg still open.
+- [compaction-kills-worker-stream] 2026-07-08 — every embedded-session compaction on a cc-bridge session cuts the live worker's Anthropic stream → raw "API Error: The socket connection was closed unexpectedly" surfaces as an ASSISTANT bubble in Tinker chat (user report: "errors in the ui every time compaction triggers"; evidence: tinker-ui-snapshot.html bubble +2m16s after 11:46 turn, journal socket errors 07:03-07:08 coinciding with turns). MITIGATED in app.ts ~7469 (transient API errors < 400 chars → msg-overload-bubble, HMR-live). ROOT CAUSE open: compaction summarize call contends with the busy worker on the same CC session.
+- [compaction-timeout-thrash] 2026-07-08 — companion pathology: EVERY compaction of the fat tinker session hits `compaction retry aggregate timeout (60000ms)` (attempt.ts:2967 COMPACTION_RETRY_AGGREGATE_TIMEOUT_MS=60_000) → "using pre-compaction snapshot" → compacted result discarded → context stays fat → compaction re-fires every 5-7 min (journal 13:59, 14:22, 14:29, 14:35) → error bubble each time. PROPOSED FIX: raise aggregate timeout to ~180s for slow providers (fable summarizing 100k+ ctx > 60s) or serialize compaction with turn end; needs dist rebuild + gateway restart to deploy.
+- [send-before-channel-connected] 2026-07-08 — `openclaw message send --channel whatsapp` right after a gateway restart fails with generic "gateway timeout after 10000ms" while whatsmeow is still connecting (~110s: restart 14:22:06 → connected 14:23:57); no "Sending message" in journal = NOT sent (safe to retry). Proposal: send RPC should return distinguishable "channel not ready" or queue until connected.
+- [detached-children-reaped] 2026-07-08 — setsid+nohup+disown from a CC turn does NOT survive turn end (sandbox reaps the tree; morning restart+send job pid 77566 died silently, log never created). WORKING alternative verified same day: `systemd-run --user --on-active=N` transient unit (fired 14:22, survived gateway restart, executed script). Rule: post-turn work goes through systemd-run/harness cron, never shell detachment.
+- [messageprefix-first-person-sends] 2026-07-08 — outbound `openclaw message send` with user-dictated first-person content carries the agent's channel messagePrefix (🤖) unless target chat is in noPrefixChats (Montserrat 11:48 send likely arrived prefixed). Proposal: `--no-prefix` flag or authorship field on send RPC; workaround = add recipient to noPrefixChats before sending.
+- [retry-worker-concurrent-write] 2026-07-08 — the owner's "torna-ho a provar amb fable" spawned a fresh worker while the pre-fable worker was STILL writing its (partial) result into the same target file (3d_raw_data.html); the old injection landed between the new worker's read and write. No clobber only because the write re-read the file; symptom (duplicate "Capes" panel) caught only via real-render screenshot. RULE: on any retry turn, grep the target artifact for the previous attempt's markers before editing. Root fix (kill/fence the superseded worker on retry) belongs to the gateway session manager — open.
+- [fs-link-ambiguous-first-match] 2026-07-08 — PREDICTED HAZARD CONFIRMED same-day: bare-name click on `3d_raw_data.html` (3 copies) silently opened matches[0] (Crèdit/ bank archive) while the agent edited the Estudi topogràfic copy → owner reviewed a stale file for 4 turns. `files.resolveBareName` already returns ambiguous+matches; the click handler (tinker-ui app.ts ~10922) takes matches[0] with only a title-tooltip hint. FIX NEEDED: on ambiguous>1, show a small chooser popover (or at least a toast naming the opened path). Repro: two same-named files in allowlisted roots, click bare name.
+- [usage-tab-token-field-mismatch] 2026-07-08 — FIXED. The Usage nav tab (`renderUsageTab`, tinker-ui/src/app.ts ~15570) read token fields by names that don't exist in the live `sessions.usage`/`usage.cost` payloads: `totals.inputTokens`/`outputTokens` (actual: `totals.input`/`output`), per-session `s.inputTokens`/`outputTokens` (actual nested `s.usage.input`/`output`/`totalTokens`), `s.provider` (actual `s.modelProvider`/`providerOverride`), `costData.totalCost`/`inputCost` (actual `costData.totals.totalCost`…), daily `d.cost` (actual `d.totalCost`). Every figure rendered 0 despite ~110M real tokens; Insights/Breakdown showed "No data". FIX: aligned all reads to the real paths + added a Cache row and made Total=`totalTokens` (input+output undercounts subscription usage where cache dominates 110M/1.4M). Cost genuinely $0 (claude-code flat subscription, never a bug). esbuild clean, vite HMR live — reopen the Usage tab to see it. Backend RPCs were healthy throughout (verified via `openclaw gateway call sessions.usage`). Prevention: the RPC payload is the contract — new UI panels should be built against a real `gateway call` dump, not assumed field names.
+- [usage-tab-model-attribution] 2026-07-09 — FIXED (follow-up to [usage-tab-token-field-mismatch]). The Usage tab Breakdown/Insights/Top-model grouped tokens by the session-level `s.model` field, which is an override/last-model that is NULL for ~half of sessions. Result: 59M of 122M tokens fell into a phantom "no model" bucket and fable read 33M (27%) when its real share is 85.8M (70%) — the source of the owner's "fable shows 0 usage". Real per-call split lives in `session.usage.modelUsage[]` (each entry `{provider,model,totals.totalTokens}`). FIX (tinker-ui/src/app.ts renderUsageTab): aggregate modelMap/providerMap from `usage.modelUsage[]` when present (fallback to session-level otherwise), and the per-session table Model/Provider columns fall back to the session's dominant modelUsage entry when `s.model` is null. NOTE: the panel measures share-of-TOKENS; Anthropic's online "over 30%" is share-of-weekly-RATE-LIMIT — different metrics, they will not match numerically (our fable = 70% of tokens; online 30%+ = quota consumed). Prevention: per-model rollups must read `modelUsage`, never the session's headline `model`.
+- [models-panel-usage-bars-zero] 2026-07-09 — FIXED IN CODE (needs gateway restart to deploy). The RIGHT panel 🕸️ MODELS usage bars (getModelUsage → renderUsageBarsOnly, tinker-ui app.ts ~8015) read Anthropic 5h/7d rate-limit UTILIZATION from `budgetUsageData.claude.limits` (RPC `budget.usage`). All models showed 0% while the online Claude console showed 30%+. NOT a UI bug — the gateway itself returned the zero-stub. Three stacked causes in extensions/tinkerclaw-budget-panel/index.ts: (1) the usage poller's OAuth token refresh for `anthropic:cli-gm` has failed since 2026-07-08 13:33 (`[budget-panel] resolveToken anthropic:cli-gm: OAuth token refresh failed … Please re-authenticate`) → `/api/oauth/usage` fetch returns null; ALSO it polls a phantom `cli-sv` profile that doesn't exist (real profiles: cli-gm, api). (2) fallback file `~/.openclaw/workspace/memory/claude-usage.json` is 7 WEEKS stale (fetchedAt 2026-04-03) → the >7-day staleness guard zeroes it. (3) DEEPEST: the claude-code/fable transport (src/agents/anthropic-transport-stream.ts) received the `anthropic-ratelimit-unified-5h/7d-utilization` response headers on EVERY real request but threw them away — only the direct-anthropic path (anthropic-vertex-stream.ts) captured them, and even that was gated `modelProvider==="anthropic"`. So no token-free live source existed. The brain still ran fine because fable uses live Claude Code CLI creds, a DIFFERENT source than the dead tracking-profile refresh token. FIX (Rule 7 patch+prevent): added `captureRateLimitHeaders(headers)` to src/agents/anthropic-ratelimit-store.ts, call it in anthropic-transport-stream.ts right after the fetch response (guarded, never throws, doesn't touch body) so fable's OWN traffic feeds the snapshot; budget.usage now falls back `firstLive ?? snapResult(getRateLimitSnapshot) ?? fileResult ?? zeroStub`. After restart + one fable turn the bars reflect real utilization with no pollable token. Immediate alt unblock (no code): re-auth anthropic:cli-gm (but `openclaw auth` CLI is disabled via plugins.allow). Deploy: `scripts/gateway-full-restart.sh --note "deploy ratelimit header capture"` (dist already rebuilt, verified present). NOTE: this panel = share-of-RATE-LIMIT (matches online %); the Usage-tab breakdown = share-of-TOKENS — different meters ([usage-tab-model-attribution]). Prevention: any Anthropic transport added in future must call captureRateLimitHeaders; the usage poller should derive profiles from config, not a hardcoded cli-sv/cli-gm pair.
+- [models-panel-usage-bars-zero-2] 2026-07-09 — layer 4, the ACTUAL closer (follow-up to [models-panel-usage-bars-zero]). The 13:06 restart deployed the header-capture fix but bars stayed 0 because fable's HTTP happens INSIDE the Claude Code CLI subprocess (cc-bridge spawns `claude` workers) — the gateway's anthropic-transport-stream never carries that traffic, so captureRateLimitHeaders never fires for the chat brain. REAL SOURCE FOUND: `~/.claude/.credentials.json` (claudeAiOauth.accessToken) — the token fable actually runs on, kept fresh by the CLI itself (verified live: /api/oauth/usage → 200, 5h=25%, 7d=43%, matching the owner's "over 30%"). FIX: budget-panel `fetchCliFileUsage()` reads that file READ-ONLY (never refresh/rotate — the CLI owns rotation; rotating would kill the CLI's refresh token) and `fetchAllClaudeUsage` falls back to it when all configured profiles fail; forceRefresh also busts the cli-file cache. Fallback order now: profile poll → CLI credential file → response-header snapshot → usage file → zero-stub. Deployed via systemd-run +45s restart (unit deploy-budget-cli-file). LESSON: "claude-code provider" ≠ in-gateway HTTP — the cc-bridge path is a subprocess; any header-harvesting scheme is blind to it. When a fix targets a traffic path, first confirm the traffic actually flows through that path.
+- [models-panel-no-token-numbers] 2026-07-09 — the 5th-round resolution of "models show zero token usage": after layers 1-4 fixed the rate-limit % pipeline (verified live: budget.usage now 5h=3%/7d=43% via cli-file), the remaining gap was SEMANTIC — the 🕸️ MODELS panel had NO token figures at all, only thin utilization bars (3% ≈ visually zero), so to the owner every model read "0 usage" no matter how healthy the pipe was. FIX (tinker-ui app.ts, HMR-live, no restart): loadBudget() now also fetches sessions.usage (7d) and aggregates usage.modelUsage[] into modelTokensAll + modelTokensBySession (keyed provider/model); new modelTokenLabel() renders a compact count (e.g. 94.2M) on every model row, honoring the Session/All scope toggle. Verified against live data: fable 94.2M · opus 39.2M · sonnet 2.8M. LESSON (recurrence of the frame-lock pattern, 3rd instance): "usage" meant TOKENS to the user and RATE-LIMIT-% to the code; five debugging rounds fixed real pipeline bugs but the user-visible artifact never contained the number he was looking for. When a report survives a verified fix, re-derive WHAT ELEMENT the user is reading, not just whether the data behind it is correct.
+- [models-panel-subgrid-shift+order] 2026-07-10 — my 07-09 token-count column was a 5TH item injected into `.model-group-body`'s 4-col grid (`minmax(0,auto) 1fr auto auto` + `.model-row` subgrid) → every subsequent column shifted right: bars landed in the narrow cost column ("graphs start too much to the right"). FIX: 5-col template `minmax(0,auto) auto 1fr auto auto` (name|tokens|bars|cost|count) in base.css. Same turn, 3 more MODELS-panel refinements (app.ts): (1) "bars back to 0%" was REAL data — weekly window reset 07-09 16:00 UTC → genuine 3%/3%; added a tiny `T·B%` numeric label after the bars so low utilization stops reading as broken (3rd such misread); (2) ` · api` suffix dropped (simplifyProfileLabel "default"→""); (3) chain-first layout replaced by ONE rank-sorted list — primary keeps its ① badge but position = smartness rank (opus-4.8 no longer pinned above fable/gpt-5.6-sol); (4) name shortener extended (fable-5→fable5, -preview/-latest stripped, gem-3.5-fl). LESSON: when adding a column to a subgrid row, the PARENT template must grow in the same commit — subgrid children reflow silently, no overflow warning.
+
+## [fs-link-punt-volat] 2026-07-15 — FIXED
+
+Backtick paths containing '·' (U+00B7, Catalan punt volat, e.g. `T-054 Sol·licitud….pdf`) rendered as dead text: the .fs-link matcher's char classes (`app.ts` ~6409 abs-path + ~6428 bare-filename) only admit `\p{L}\p{M}\p{N}` + listed punctuation, and · is category Po. Fix: added `·` literally to both classes + comment. Repro: send `` `/home/x/Sol·licitud.pdf` `` in chat → was unclickable, now linkifies. Live via vite HMR.
+
+- [amygdala-aegis-fs-format-false-positive] 2026-07-15: AMYGDALA AEGIS FS_FORMAT guard fires on READ-ONLY presence checks that merely NAME a formatter (e.g. `command -v mkfs.vfat`, `ls /sbin/mkfs.vfat`) though nothing is formatted; even a printf writing the tool name into a log trips it. Root cause: substring match on the tool name anywhere in the command line, not intent-parse of the executed verb. Repro: `command -v mkfs.vfat`. Fix: exempt inspector contexts (`command -v`/`which`/`type`/`ls`/`dpkg -l`/`printf`/`echo`); only trip when a formatter is in executable position (argv[0]). Cost 3 misfires during Windows-VM boot automation.
+
+- [amygdala-aegis-fs-destructive-root-false-positive] 2026-07-15: AMYGDALA AEGIS FS_DESTRUCTIVE_ROOT fires on a recursive delete of a scoped /tmp subdir (e.g. `rm -rf /tmp/vmtools`), not the actual root fs — same over-broad matching family as the FS_FORMAT false positive. Even a printf writing the pattern into this log trips it. Fix: exempt /tmp/\* and session-scoped temp dirs + inspector/printf/echo contexts; only trip on real root/$HOME/etc deletes in executable position. 2 guard false-positives this session (FS_FORMAT + FS_DESTRUCTIVE_ROOT).
+
+- [amygdala-aegis-more-false-positives] 2026-07-16: two more FS-guard misfires on non-destructive ops during Windows-VM disk-swap fix. (1) FS_DESTRUCTIVE_ROOT fired on `rm -f /home/user/virtio-win.iso` — a SINGLE-file delete (no -r, not root). (2) FS_DD_DEVICE fired on `dd if=/dev/nvme0n1 ... of=/dev/null` — a device READ discarding to /dev/null, not a write. Same root cause as [amygdala-aegis-fs-format-false-positive]: substring/pattern match on the verb, not intent. Fix: FS_DESTRUCTIVE_ROOT should require `-r`/`-rf` AND a root-ish target; FS_DD_DEVICE should check `of=` target is a real device (not /dev/null or a regular file). Read-only `of=/dev/null` and single-file deletes must pass.
+
+## [eeg-subagent-single-session-gap] 2026-07-16 — FIXED IN CODE (needs gateway restart for server half)
+
+Fan-outs never showed in the EEG seismograph: the owner ran a 43-doc parallel fan-out and saw ZERO branches. Root cause (long-standing, memory `reference_eeg_subagents_invisible_single_session`): the EEG anatomy backfill was SINGLE-SESSION by construction — the frontend fetched `/tinker/api/context-anatomy/?limit=500` for exactly the viewed tab, and the DB query filtered `session_key = ?`. Subagent anatomy rows DO get written, keyed FLAT under the agent root (`agent:main:subagent:`), but nothing ever fetched them, so the eeg-trace.ts branch renderer (which already fully supports subagent lanes/depth-shade/×N) had no data to paint. (The 43-doc case was doubly-dark: it used the Claude Code Workflow tool = class-2 subagents that write NO anatomy at all — see `feedback_jarvis_fanout_use_orchestrate_not_workflow_tool`; this fix makes class-1 orchestrate/gateway subagents visible.)
+FIX (4 touch points, NO schema migration — the flat key already encodes the parent):
+
+1. `src/agents/context-anatomy-db.ts` — new `querySessionTree(sessionKey, limit)`: derives the agent root as the first two key segments (`agent:main`, the SAME derivation as app.ts `chatEventIsSubagentOfView`) and returns `session_key = ? OR session_key LIKE ':subagent:%'`. Non-`agent:` / subagent keys fall back to single-session. Registered on the `__anatomyDb` global bridge.
+2. `extensions/tinkerclaw-tinker/index.ts` — the `:sessionKey` route honors `?tree=1` (bridge + directDb fallback both get `querySessionTree`).
+3. `src/agents/context-anatomy-http.ts` — same `?tree=1` parity for direct gateway hits.
+4. `tinker-ui/src/app.ts` — the EEG backfill fetches `?tree=1&limit=500`; rows whose `sessionKey` contains `:subagent:` are tagged `subagent:true` with `endedAt = ts + durationMs` (renderer arch-floor handles the common durationMs-undefined → zero-span case), excluded from prompt-boundary (turnEnd) creation, and TIME-BOUNDED to the viewed session's main-event window (+1h slack) so stale/foreign fan-outs don't clutter the paper.
+ KNOWN v1 LIMIT: the flat subagent key loses WHICH tab spawned it, so within one shared `agent:main` root + the same time window, a second tab's fan-out could bleed onto this tab's paper. Precise per-tab attribution would need a `parent_session_key` column populated at spawn (the "larger instrumentation job" the memory flagged) — deferred; time-bounding kills the obvious stale-heartbeat/cron bleed. Verified: `querySessionTree` returns the 68-row subagent family for a real tinker tab against the live DB; new http test `?tree=1 includes the session's subagent family` passes; tsgo clean on all 4 files. Deploy: UI half live via vite HMR; server half (db/http/extension) needs a gateway restart (`scripts/gateway-full-restart.sh --note`), NOT a mid-turn self-restart.
+
+FOLLOW-UP 2026-07-16 (test debt closed): added `setAnatomyDbPathForTests()` seam to context-anatomy-db.ts (production untouched; `resolveDbPath()` returns the override or DB_PATH). `context-anatomy-http.test.ts` + new `context-anatomy-tree.test.ts` now use isolated tmp DBs → no more real-DB pollution. Isolation exposed the TRUE cause of the long-red `returns event list with limit` test: it wasn't only pollution — `buildContextAnatomy` stamps `Date.now()`, which COLLIDES in a tight insert loop, so `ORDER BY timestamp_ms DESC LIMIT 3` returned an arbitrary 3. Fixed by pinning explicit timestamps in the order-sensitive tests. Also purged 36 synthetic rows (`list-test`,`http-test`,`agent:main:subagent:tree-test-uuid`) from the real anatomy DB — the tree-test-uuid row was a phantom subagent an earlier pre-isolation run inserted, which querySessionTree would have painted as a bogus live-EEG branch. 22/22 green, tsgo clean on changed files. LESSON: when isolating a flaky test, pin the ordering key too — isolation alone just relocates Date.now() nondeterminism.
+
+- [win-vm-shutdown-acpi-ignored] 2026-07-16: win-vm-ctl.sh shutdown / win-vm-shutdown.sh (red dock tile) send QMP `system_powerdown` = an ACPI power-button event. A bare-metal laptop Windows often has the power-button action set to Sleep/Do-nothing (carried into the VM), so the guest IGNORES it and never shuts down. Fix options: (a) set guest power-button=shutdown once via `powercfg -setacvalueindex ... PBUTTONACTION 3` then apply; (b) UI-drive a real shutdown via QMP send-key: meta_l+d (show desktop) → alt+f4 → ret (confirms "Shut down" dialog) — works regardless of power policy; (c) enable OpenSSH in guest and `ssh -p2222 shutdown /s /t 0`. Helpers should try ACPI, then fall back to (b) after ~30s if the VM is still up.
diff --git a/TINKER_UI_DESIGN_BIBLE/config-shape.md b/TINKER_UI_DESIGN_BIBLE/config-shape.md
index 4864079f1b0c4..26795b76adab8 100644
--- a/TINKER_UI_DESIGN_BIBLE/config-shape.md
+++ b/TINKER_UI_DESIGN_BIBLE/config-shape.md
@@ -5,7 +5,7 @@ audience: AI
last_verified: 2026-06-02
last_verified_commit: 06f8647fdc
single_owner: yes — config-flow facts live here
-see_also: topology.md (what runs), auth-routing.md (which model is picked), tool-loop.md (why cc-bridge has its own timeout)
+see_also: topology.md (what runs), auth-routing.md (which model is picked), tool-loop.md (why tinker-bridge has its own timeout)
verify:
- name: claude-code provider overlay resolves timeoutSeconds=600
cmd: python3 -c 'import subprocess,json; r=subprocess.run(["openclaw","gateway","call","debug.session.config","--params",json.dumps({"provider":"claude-code"})],capture_output=True,text=True); assert "\"resolvedRequestTimeoutMs\": 600000" in r.stdout, r.stdout[-500:]'
@@ -59,10 +59,10 @@ For provider/model config specifically:
- **Drives:** LLM idle watchdog timeout (`streamWithIdleTimeout` wraps the streamFn)
- **Default chain:** plugin overlay → explicit cfg → undefined (which then falls through to `clampImplicitTimeoutMs(agentTimeoutMs)`, capped at `DEFAULT_LLM_IDLE_TIMEOUT_MS=120_000`)
- **Current values:**
- - `claude-code`: 600 (from cc-bridge plugin overlay, FORK 2026-05-10, NOT openclaw.json)
+ - `claude-code`: 600 (from tinker-bridge plugin overlay, FORK 2026-05-10, NOT openclaw.json)
- `ollama`: undefined → default 120s
- **⚠️ Anti-pattern:** per-model `requestTimeoutMs` on the catalog model object is silently ignored. Only the provider-level `timeoutSeconds` propagates. See bible §11.6d.
-- **⚠️ Anti-pattern (fixed 2026-05-10):** the cc-bridge plugin's `discovery.run` returning `{ provider: { timeoutSeconds: 600 } }` did NOT merge into cfg.models.providers. The fix is the plugin overlay (J15 §4 + bible §11.6e). Do NOT rely on the discovery path for runtime config; use the overlay.
+- **⚠️ Anti-pattern (fixed 2026-05-10):** the tinker-bridge plugin's `discovery.run` returning `{ provider: { timeoutSeconds: 600 } }` did NOT merge into cfg.models.providers. The fix is the plugin overlay (J15 §4 + bible §11.6e). Do NOT rely on the discovery path for runtime config; use the overlay.
### `agents.defaults.models[].rank`
@@ -126,7 +126,7 @@ For provider/model config specifically:
### `env.vars.`
-- **Read at:** child-process spawn env (cc-bridge, exec tool, etc.)
+- **Read at:** child-process spawn env (tinker-bridge, exec tool, etc.)
- **Critical:** `DISPLAY=:0` enables GUI ops from spawned processes (browser, etc.)
### `tools.exec.*`
@@ -232,15 +232,15 @@ The control-panel plugin owns the Today card / Exec panel surface (see `tinker-u
- **Migration owner:** `stripTodoistMetadata` in `db.ts` (idempotent one-shot — scans every task's `metadata_json`, strips keys matching `^todoist_*`, rewrites). Re-running is a no-op.
- **Trigger:** runs on every gateway boot via the migration step in `getDb()`. Drawer renders no longer recognise any `todoist_*` chips. Pair with the delete of `extensions/tinkerclaw-control-panel/scripts/import-from-todoist.mjs`.
-## cc-bridge ethical-rules prompt loader (FORK 2026-05-21)
+## tinker-bridge ethical-rules prompt loader (FORK 2026-05-21)
-The cc-bridge plugin loads an `ethical-rules` block into the worker's `--append-system-prompt` between the persona block and the narration/tool-choice/plan-tools blocks. See `tool-loop.md` for the slot in `combinedSystemPrompt`.
+The tinker-bridge plugin loads an `ethical-rules` block into the worker's `--append-system-prompt` between the persona block and the narration/tool-choice/plan-tools blocks. See `tool-loop.md` for the slot in `combinedSystemPrompt`.
**Resolution order** (per `loadPromptFile` defaults; first existing path wins):
1. `env.TINKERCLAW_ETHICAL_RULES_PROMPT` — explicit path override.
2. `~/.openclaw/workspace/memory/knowledge/jarvis-ethical-rules.md` — user-personalised override (outside the public repo).
-3. `extensions/tinkerclaw-cc-bridge/prompts/ethical-rules-default.md` — bundled default (in the public repo). Ships ten Asimov-style priority-ordered rules + a generic preamble. The bundled file carries `default-version: 1.0` in its frontmatter so the drift-detection log line (see bible §5.76f) can flag override staleness.
+3. `extensions/tinkerclaw-tinker-bridge/prompts/ethical-rules-default.md` — bundled default (in the public repo). Ships ten Asimov-style priority-ordered rules + a generic preamble. The bundled file carries `default-version: 1.0` in its frontmatter so the drift-detection log line (see bible §5.76f) can flag override staleness.
**Don't regress:** workspace override path is `memory/knowledge/jarvis-ethical-rules.md`, NOT `SOUL.md` (persona) and NOT `BRIEFING.md` (briefing). Each foundational block has its own override file; conflating them silently overrides the wrong layer.
@@ -252,7 +252,7 @@ Read in `extensions/tinkerclaw-learned-intuition/index.ts` `register()` from `ap
| ----------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `aegisEnabled` | **`true`** (was `false`) | The deterministic AEGIS rule veto. v3.1 flips the default ON — resolving the old manifest-off vs code-on contradiction. `false` fully disables the rule floor. |
| `legacyEnsemble` | `false` | Run the retired 5-net ONNX prudence/personality ensemble in the decision path. Off by default (trained on mislabelled data; arch C collapsed, arch E mush; frozen-MiniLM danger classification measured below chance, AUROC 0.286). The embedding pipeline (encoder + projection) is **always** loaded — novelty needs it. |
-| `hookEnforcement` | `true` | Write `cc-hook-settings.json` so cc-bridge passes `--settings` and destructive-execution AEGIS rules deny synchronously on the claude-cli runner (see `tool-loop.md`). `false` removes the settings file → observe-only spool, no pre-execution deny. |
+| `hookEnforcement` | `true` | Write `cc-hook-settings.json` so tinker-bridge passes `--settings` and destructive-execution AEGIS rules deny synchronously on the claude-cli runner (see `tool-loop.md`). `false` removes the settings file → observe-only spool, no pre-execution deny. |
| `observeOnly` | `true` | Neural soft-blocks stay advisory while the gate ramps. The novelty channel ships as an observe-only **ask** disposition under this. AEGIS hard-blocks enforce regardless. |
| `phase` | `1` | Trust-ramp phase. |
diff --git a/TINKER_UI_DESIGN_BIBLE/design-principles.md b/TINKER_UI_DESIGN_BIBLE/design-principles.md
index 53e33c6b0f1b2..7840c26a82e7e 100644
--- a/TINKER_UI_DESIGN_BIBLE/design-principles.md
+++ b/TINKER_UI_DESIGN_BIBLE/design-principles.md
@@ -48,7 +48,7 @@ The rules are short on purpose. The point is that you can load this file once at
**Why.** The fork has at least five hand-coded wrapper patterns (idle timeout, heartbeat, retry, overlay merge, PII guard). Each is its own closure with its own bugs. The shared shape — `wrapper(next)(input) → output` — is generic; composing wrappers should be data, not closure soup.
-**How to apply.** New wrappers go through `src/fork/pipeline.ts`'s `compose(...)` helper. Use the canned wrappers (`withRetry`, `withTimeout`, `withTrace`) before writing custom ones. Domain-specific wrappers that DO need custom logic (cc-bridge heartbeat — protocol-aware) stay where they are, but new ones should default to the shared substrate.
+**How to apply.** New wrappers go through `src/fork/pipeline.ts`'s `compose(...)` helper. Use the canned wrappers (`withRetry`, `withTimeout`, `withTrace`) before writing custom ones. Domain-specific wrappers that DO need custom logic (tinker-bridge heartbeat — protocol-aware) stay where they are, but new ones should default to the shared substrate.
### 5. Validate at boundaries, trust inside
diff --git a/TINKER_UI_DESIGN_BIBLE/done-signals.md b/TINKER_UI_DESIGN_BIBLE/done-signals.md
index ca5eaad59acbd..ce2b38b5dfae1 100644
--- a/TINKER_UI_DESIGN_BIBLE/done-signals.md
+++ b/TINKER_UI_DESIGN_BIBLE/done-signals.md
@@ -5,7 +5,7 @@ audience: AI (Claude, etc). Human readability is incidental.
last_verified: 2026-05-17
last_verified_commit: HEAD
single_owner: yes — the cross-signal *precedence contract* and the master doneness map live here. Per-signal facts are owned elsewhere (lifecycles.md = session/worker/recovery state machines; tool-loop.md = heartbeat/idle-watchdog/lifecycle:end emission/no-UI-watchdog; flows.md = the chat.final guarantee + F-PLAN-RESUME; subagents-and-recipes.md = plan/kit/recipe; panels.md = prefrontal render levels + the §147 helpers; failures.md = failure-mode maps). This file does NOT re-derive them — it sequences them.
-see_also: lifecycles.md (L1 session, L2 cc-bridge worker, L4 restart-recovery), tool-loop.md (heartbeat, idle watchdog, lifecycle:end emission, the deleted UI watchdog), flows.md (chat.send always ends in a final/error/aborted broadcast; F-PLAN-RESUME), panels.md (§115 prefrontal render levels, §147 single-source-of-truth helpers), subagents-and-recipes.md (plan RPCs, kits, recipes, restart-continue), failures.md (M1 idle SIGTERM, M2 stuck spinner, incomplete_turn)
+see_also: lifecycles.md (L1 session, L2 tinker-bridge worker, L4 restart-recovery), tool-loop.md (heartbeat, idle watchdog, lifecycle:end emission, the deleted UI watchdog), flows.md (chat.send always ends in a final/error/aborted broadcast; F-PLAN-RESUME), panels.md (§115 prefrontal render levels, §147 single-source-of-truth helpers), subagents-and-recipes.md (plan RPCs, kits, recipes, restart-continue), failures.md (M1 idle SIGTERM, M2 stuck spinner, incomplete_turn)
verify:
- name: chat.final/aborted is authoritative-and-immediate AND cancels the debounced lifecycle:end delete (the core precedence contract this doc owns)
cmd: python3 -c 'import os,re; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert re.search(r"pendingRunDeletes\.get\([^)]*\)[\s\S]{0,240}clearTimeout[\s\S]{0,200}activeRuns\.delete", t), "the chat.final/aborted handler no longer cancels the pending lifecycle:end timer before deleting the run — the authoritative-supersedes-debounced precedence (done-signals.md) is broken; a late lifecycle:end timer can now delete a run the user already re-used, or final no longer closes the run immediately"; assert re.search(r"pendingRunDeletes\.set\(", t) and re.search(r"setTimeout\([\s\S]{0,320}activeRuns\.delete", t), "the lifecycle:end debounced (delayed) delete path is gone — lifecycle:end must remain the advisory/debounced half of the contract, NOT an immediate delete; re-read done-signals.md before changing turn-completion"'
@@ -13,6 +13,8 @@ verify:
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/panels/prefrontal-tree.ts")).read(); assert "Priority 2" in t and "synthetic" in t.lower(), "prefrontal-tree.ts no longer documents the explicit-plan → synthetic-2-step → idle priority ladder; done-signals.md §3 and panels.md §115 describe a precedence that the code must still implement"'
- name: the terminal lifecycle phase:"error" event carries the SAME identity fields as phase:"end" so the Tier-3 debounced delete fires for errored runs too (FORK 2026-06-04, the error-clears-the-run precedence this doc owns)
cmd: python3 -c 'import os,re; t=open(os.path.expanduser("~/src/tinkerclaw/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts")).read(); m=re.search(r"phase:\s*\"error\",\s*\n", t); assert m, "could not find the terminal phase:\"error\" emit object in handleAgentEnd"; blk=t[m.start():m.start()+600]; assert "model:" in blk and "sessionKey:" in blk, "the terminal phase:\"error\" gateway event no longer carries model+sessionKey — the Tinker UI gates its lifecycle handler on p.data?.model, so an identity-less error event is DROPPED and the errored run is NEVER scheduled for the Tier-3 debounced delete (its thinking indicator sticks + stacks). done-signals.md §2: phase:error MUST mirror phase:end identity. Re-read before touching the lifecycle terminal emit."'
+ - name: subagent chat terminals also close the run — handleSubagentChatEvent deletes from activeRuns (FORK 2026-06-22, stuck spinner after a fractal turn)
+ cmd: python3 -c 'import os,re; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); m=re.search(r"function handleSubagentChatEvent[\s\S]*?\nfunction ", t); assert m, "handleSubagentChatEvent not found"; blk=m.group(0); assert "activeRuns.delete" in blk and "rememberTerminated" in blk, "handleSubagentChatEvent no longer closes the subagent run on its terminal chat event — a subagent (e.g. a fractal-triage lane) whose tier-3 lifecycle:end is dropped on hard teardown will stick its thinking indicator forever. done-signals.md section 2 R1: a chat terminal is tier-1 for SUBAGENT runs too, not just the main run. Re-read before changing the subagent chat handler."'
---
# Done-signals — the "is the turn/task finished?" methodology
@@ -106,6 +108,23 @@ Authority tiers — when two signals disagree, the **lower tier number wins**.
Before this, errored/failed-over runs had no Tier-3 delete at all and
their indicators stuck — a cross-signal identity gap, not a precedence
change. (Enforced by this file's new verify block.)
+ - **Subagent terminals are tier-1 too (FORK 2026-06-22, "stuck spinner
+ after a fractal turn").** A `:subagent:` run of the viewed session — e.g.
+ a fractal-triage lane (`agent::subagent:`; `deliver:false`
+ gates delivery, NOT visibility) — is added to `activeRuns` by its
+ `phase:start` (#4) and rendered in the indicator like any other run. But
+ its chat events take the `handleSubagentChatEvent` path, which `return`s
+ BEFORE the tier-1 `activeRuns.delete` in the main-run chat handler. So
+ until this fix a subagent's ONLY terminator was the tier-3 debounced
+ `lifecycle:end` (#3) — which is dropped on hard teardown (SIGTERM /
+ gateway-restart / timeout) — and with R2 (no UI watchdog) nothing
+ backstopped it, so the subagent stayed pinned in `activeRuns` and the
+ dots stayed lit even though the answer + fractal dock were complete. Fix:
+ `handleSubagentChatEvent` now applies the SAME tier-1 authority on
+ `final/aborted/end/error` (cancel the pending lifecycle:end timer →
+ `activeRuns.delete` → `rememberTerminated` → recompute `sending`), so a
+ subagent has the same two independent terminators (tier-1 chat + tier-3
+ lifecycle) the main run has. (Enforced by this file's new verify block.)
- **R2 — there is no UI-side stale-run watchdog.** The 2026-05-14 deletion
is permanent (`STALE_RUN_WATCHDOG_MS` must never reappear — owned/enforced
by tool-loop.md). A stuck indicator is ALWAYS cured by hardening
@@ -156,14 +175,15 @@ Owned by **subagents-and-recipes.md**. The doneness-relevant synthesis:
Full propagation + `diagnose_with` probes are owned by **failures.md**.
Use this as the index from _symptom_ to _the disagreeing signal_:
-| Symptom | Disagreement | First probe |
-| ---------------------------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
-| Spinner stuck on `sending…`, reply already shown | #1 never broadcast (lifecycle path dropped) — the M2 backstop didn't fire | `debug.dumpUiSnapshot` + `debug.session.state` (failures.md M2) |
-| Indicator clears but prefrontal still "thinking" (or vice-versa) | #4/#3 vs the §147 set diverged, or a 4th "busy" derivation was added | grep app.ts for a busy computation NOT routing through a §147 helper (panels.md §147 R1) |
-| "Thinking no matter which session I select" | prefrontal didn't re-render on session switch | panels.md §147 rule 6 / verify #5 |
-| Turn never ends; SIGTERM at the idle cap | heartbeat #5 not resetting pi idle timer | failures.md M1; `[idle-timeout-diag]` journal |
-| Reply truncated to the streamed lead-in | `text_end` emitted BEFORE tail-recover (#2 vs streamed scratch) | stream.ts ordering; tool-loop.md |
-| 53 leaked `claude` procs / Jarvis slow | not a doneness bug — worker pool unbounded | lifecycles.md L2 (bounded pool, FORK 2026-05-16) |
+| Symptom | Disagreement | First probe |
+| ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
+| Spinner stuck on `sending…`, reply already shown | #1 never broadcast (lifecycle path dropped) — the M2 backstop didn't fire | `debug.dumpUiSnapshot` + `debug.session.state` (failures.md M2) |
+| Indicator stuck ON after a fractal turn (answer + dock complete) | a `:subagent:` run's tier-1 chat-final didn't close `activeRuns` (handleSubagentChatEvent lacked the delete); only tier-3 lifecycle:end could, and it was dropped on hard teardown | grep app.ts `handleSubagentChatEvent` for `activeRuns.delete`; §2 R1 subagent corollary |
+| Indicator clears but prefrontal still "thinking" (or vice-versa) | #4/#3 vs the §147 set diverged, or a 4th "busy" derivation was added | grep app.ts for a busy computation NOT routing through a §147 helper (panels.md §147 R1) |
+| "Thinking no matter which session I select" | prefrontal didn't re-render on session switch | panels.md §147 rule 6 / verify #5 |
+| Turn never ends; SIGTERM at the idle cap | heartbeat #5 not resetting pi idle timer | failures.md M1; `[idle-timeout-diag]` journal |
+| Reply truncated to the streamed lead-in | `text_end` emitted BEFORE tail-recover (#2 vs streamed scratch) | stream.ts ordering; tool-loop.md |
+| 53 leaked `claude` procs / Jarvis slow | not a doneness bug — worker pool unbounded | lifecycles.md L2 (bounded pool, FORK 2026-05-16) |
## 6. Standard methodology — use this to find bugs before they surface
diff --git a/TINKER_UI_DESIGN_BIBLE/failures.md b/TINKER_UI_DESIGN_BIBLE/failures.md
index f20063b8bb361..a71857e3aef25 100644
--- a/TINKER_UI_DESIGN_BIBLE/failures.md
+++ b/TINKER_UI_DESIGN_BIBLE/failures.md
@@ -49,16 +49,16 @@ Generation: from `src/fork/error-envelope.ts`. Update this table when categories
## Failure modes
-### M1. cc-bridge SIGTERM (LLM idle watchdog)
+### M1. tinker-bridge SIGTERM (LLM idle watchdog)
- **diagnose_with:** `debug.session.config({provider:"claude-code"})` → assert `resolvedRequestTimeoutMs=600000`. `gateway.stuckSessions({thresholdMs:120000})` → if a session is past the old 120s threshold but `resolvedRequestTimeoutMs=600000`, the watchdog is correctly relaxed; otherwise the overlay broke. Journal grep `[idle-timeout-diag]` confirms per-turn resolution.
-- **Origin:** `streamWithIdleTimeout` in `src/agents/embedded-agent-runner/run/llm-idle-timeout.ts`. Idle timer racing `streamIterator.next()`. cc-bridge intentionally does NOT emit `stream` events during tool work (see `tool-loop.md`), so heavy turns can starve the timer.
-- **Propagation:** idle timeout rejects → `idleTimeoutTrigger(error)` → `abortRun(true, error)` aborts `runAbortController` → cc-bridge worker receives signal → `worker.kill("SIGTERM")` → claude-cli exits → `assistant-failover.ts` classifies as `surface_error reason=timeout`.
+- **Origin:** `streamWithIdleTimeout` in `src/agents/embedded-agent-runner/run/llm-idle-timeout.ts`. Idle timer racing `streamIterator.next()`. tinker-bridge intentionally does NOT emit `stream` events during tool work (see `tool-loop.md`), so heavy turns can starve the timer.
+- **Propagation:** idle timeout rejects → `idleTimeoutTrigger(error)` → `abortRun(true, error)` aborts `runAbortController` → tinker-bridge worker receives signal → `worker.kill("SIGTERM")` → claude-cli exits → `assistant-failover.ts` classifies as `surface_error reason=timeout`.
- **Envelope:** category `timeout` (icon ⏱️) or `lane_busy` if classified that way. Text: `"🤖 ⚠️ Something went wrong while processing your request."`
- **Surface:**
- WhatsApp: envelope delivered as chunked text via `deliverWebReply`. User sees the chip.
- Tinker UI: pre-2026-05-10 → spinner stuck on `sending...` (lifecycle event dropped). Post-fix → backstop `broadcastChatFinal` fires; chip renders, spinner clears.
-- **Resolution:** ensure `timeoutSeconds` is correctly resolved (see config-shape.md M1 ↔ that file). Architectural fix LIVE 2026-05-11: cc-bridge now emits an empty-delta heartbeat every 25s during a turn so the watchdog resets without re-executing tools. The 600s overlay is now belt-and-suspenders rather than load-bearing. See `tool-loop.md`.
+- **Resolution:** ensure `timeoutSeconds` is correctly resolved (see config-shape.md M1 ↔ that file). Architectural fix LIVE 2026-05-11: tinker-bridge now emits an empty-delta heartbeat every 25s during a turn so the watchdog resets without re-executing tools. The 600s overlay is now belt-and-suspenders rather than load-bearing. See `tool-loop.md`.
- **Detection probe:** journal grep `[llm-idle-timeout]` lines, plus the `[idle-timeout-diag]` log shows the resolved timeout. Heavy turns hitting 138s/279s without `[idle-timeout-diag] idleTimeoutMs=600000` means the overlay path broke.
- **Bug history:** 2026-05-05 catalog `timeoutSeconds:600` was dead code; 2026-05-10 fixed via plugin overlay (bible §11.6d, §11.6e).
@@ -90,13 +90,13 @@ Generation: from `src/fork/error-envelope.ts`. Update this table when categories
- `fork.prefrontal.state.*`
- **Bug history:** regression A from 2026-05-10 (`config.openExternalFile` wiped 2026-04-29, surfaced 2026-05-09).
-### M4. cc-bridge channel context bleed (suspected but not real)
+### M4. tinker-bridge channel context bleed (suspected but not real)
-- **diagnose_with:** `Read(~/.openclaw/cc-bridge/session-map.json)` and assert each WA channel + TUI channel produces a distinct `openclawSessionId`. If two channels share an openclawSessionId, the channel-isolation invariant has actually been violated (would be a real M4, not the suspected one). The 2026-05-09 evidence: WA=`a87a4e61`, TUI=`bf76b61f` — separate.
+- **diagnose_with:** `Read(~/.openclaw/tinker-bridge/session-map.json)` and assert each WA channel + TUI channel produces a distinct `openclawSessionId`. If two channels share an openclawSessionId, the channel-isolation invariant has actually been violated (would be a real M4, not the suspected one). The 2026-05-09 evidence: WA=`a87a4e61`, TUI=`bf76b61f` — separate.
- **Origin:** suspected when `/new` was typed in TUI and a WhatsApp reply showed unexpected content.
-- **Investigation result:** NOT bleed at cc-bridge layer. The two channels have distinct `openclawSessionId` (e.g. WA=`a87a4e61`, TUI=`bf76b61f`); session-map's `getLatestResumeSessionIdByOpenclawSessionId` only resolves WITHIN one openclaw session.
-- **Actual cause of the symptom:** M1 (cc-bridge SIGTERM) on the WA turn + concurrent /new turn timing; the "something went wrong" envelope on WA was timing-correlated with the /new in TUI, leading to misattribution.
-- **Resolution:** none required at cc-bridge layer. Document that channel isolation is invariant: openclawSessionId is canonical, lookup priority is openclawSessionId-first.
+- **Investigation result:** NOT bleed at tinker-bridge layer. The two channels have distinct `openclawSessionId` (e.g. WA=`a87a4e61`, TUI=`bf76b61f`); session-map's `getLatestResumeSessionIdByOpenclawSessionId` only resolves WITHIN one openclaw session.
+- **Actual cause of the symptom:** M1 (tinker-bridge SIGTERM) on the WA turn + concurrent /new turn timing; the "something went wrong" envelope on WA was timing-correlated with the /new in TUI, leading to misattribution.
+- **Resolution:** none required at tinker-bridge layer. Document that channel isolation is invariant: openclawSessionId is canonical, lookup priority is openclawSessionId-first.
- **Don't regress:** in `worker-pool.getOrCreate`, openclawSessionId lookup MUST come BEFORE sessionKey lookup. Reversing brings back stale-entry-wins behavior.
### M5. Plugin native-deps missing at boot
@@ -180,9 +180,9 @@ Generation: from `src/fork/error-envelope.ts`. Update this table when categories
- **diagnose_with:** `journalctl --user -u openclaw-gateway.service --since today --no-pager | grep -E "Cannot find module .*root-alias.cjs"`. Each match names the missing subpath. Cross-check via `openclaw gateway call plugin.boot.status --params '{"status":"error"}'` — a plugin with `failurePhase:"load"` and an `ERR_MODULE_NOT_FOUND` is M13.
- **Origin:** a new `src/plugin-sdk/.ts` is added in the fork (for a plugin to import as `openclaw/plugin-sdk/`), but its entry is missing from BOTH `scripts/lib/plugin-sdk-entrypoints.json` (the tsdown subpath manifest) AND the `./plugin-sdk/` entry in `package.json#exports`. Result: tsdown never builds `dist/plugin-sdk/.js`. In production `NODE_ENV=production` (systemd), `root-alias.cjs` prefers `dist/` over source, so the resolver synthesises a path to a file that doesn't exist and Node throws `ERR_MODULE_NOT_FOUND` on every plugin reload.
-- **Propagation:** the importing plugin fails to load. If that plugin is cc-bridge, BOTH the Tinker UI and WhatsApp DM go silent because the LLM worker route is no longer registered. The orphan worker pool from a pre-crash gateway can keep pre-restart workers alive but unreachable.
+- **Propagation:** the importing plugin fails to load. If that plugin is tinker-bridge, BOTH the Tinker UI and WhatsApp DM go silent because the LLM worker route is no longer registered. The orphan worker pool from a pre-crash gateway can keep pre-restart workers alive but unreachable.
- **Surface:** Jarvis stops responding on every channel simultaneously after a gateway restart. Looks like a worker-pool crash; is actually a plugin-load crash with no audible signal in the chat UI itself (no error chip — the path never gets that far).
-- **Resolution (2026-05-21, commit `e065bc94f5`):** add the entry to `scripts/lib/plugin-sdk-entrypoints.json`, regenerate `package.json#exports` via `pnpm plugin-sdk:sync-exports`, rebuild, restart. The 2026-05-21 instance: `src/plugin-sdk/provider-config-overlay.ts` had existed since 2026-05-10 (`566bf478a6`) but its manifest entry was missing; cc-bridge had been importing it from source the entire time, and a cold module cache on a gateway restart finally surfaced the gap.
+- **Resolution (2026-05-21, commit `e065bc94f5`):** add the entry to `scripts/lib/plugin-sdk-entrypoints.json`, regenerate `package.json#exports` via `pnpm plugin-sdk:sync-exports`, rebuild, restart. The 2026-05-21 instance: `src/plugin-sdk/provider-config-overlay.ts` had existed since 2026-05-10 (`566bf478a6`) but its manifest entry was missing; tinker-bridge had been importing it from source the entire time, and a cold module cache on a gateway restart finally surfaced the gap.
- **Prevention (commit `0b5c17f614`):** pre-push **Gate 4** (`pnpm lint:plugins:plugin-sdk-subpaths-exported` + `pnpm plugin-sdk:check-exports`) catches BOTH the src→manifest drift and the manifest→`package.json#exports` drift. Bypass for intentional WIP: `SDK_EXPORTS_GUARD=off git push`.
- **Don't regress:** never add a new `src/plugin-sdk/*.ts` file without also adding its entry to the manifest AND regenerating `package.json#exports` in the same commit. Gate 4 will block the push otherwise.
- **Bug history:** see `bug-log.md` 2026-05-21 entry.
diff --git a/TINKER_UI_DESIGN_BIBLE/flows.md b/TINKER_UI_DESIGN_BIBLE/flows.md
index d074ef35fb383..378cf59ec1b25 100644
--- a/TINKER_UI_DESIGN_BIBLE/flows.md
+++ b/TINKER_UI_DESIGN_BIBLE/flows.md
@@ -29,7 +29,7 @@ Each diagram below is the canonical sequence-of-calls for one pipeline. If the d
---
-## F1. Tinker UI inbound: `chat.send` → cc-bridge → reply
+## F1. Tinker UI inbound: `chat.send` → tinker-bridge → reply
**Trigger:** user types a message in Tinker UI webchat.
**Entry:** `src/gateway/server-methods/chat.ts:chatHandlers["chat.send"]`
@@ -41,7 +41,7 @@ sequenceDiagram
participant GW as Gateway (chat.ts)
participant AR as auto-reply (dispatchInboundMessage)
participant DSP as ReplyDispatcher
- participant CC as cc-bridge (worker)
+ participant CC as tinker-bridge (worker)
participant CLI as claude-cli (subprocess)
participant SCH as server-chat.ts (lifecycle)
@@ -88,7 +88,7 @@ sequenceDiagram
participant MON as auto-reply/monitor (LID rescue)
participant TRG as trigger-gate
participant AR as auto-reply pipeline
- participant CC as cc-bridge
+ participant CC as tinker-bridge
participant DDM as DELIVERY-DICHOTOMY
participant OUT as WA outbound (deliverWebReply)
@@ -167,7 +167,7 @@ sequenceDiagram
**Trigger:** gateway process exits (SIGUSR1 graceful, SIGTERM, or crash) and boots back up.
**Entry:** `src/gateway/server-startup-post-attach.ts` → `src/agents/main-session-restart-recovery.ts`
-**Exit:** TUI sees orange `__ERR_ENV__` chip; agent run resumes on the existing session (cc-bridge resumes claude-cli via session-map openclawSessionId fallback).
+**Exit:** TUI sees orange `__ERR_ENV__` chip; agent run resumes on the existing session (tinker-bridge resumes claude-cli via session-map openclawSessionId fallback).
```mermaid
sequenceDiagram
@@ -178,7 +178,7 @@ sequenceDiagram
participant ENV as pushRestartWarningEnvelope
participant CINJ as chat.inject
participant AGT as agent dispatch ([System] continue)
- participant CC as cc-bridge worker-pool
+ participant CC as tinker-bridge worker-pool
participant CLI as claude-cli (--resume)
BOOT->>MRK: mark every status:running as interrupted (regardless of stale locks)
@@ -189,7 +189,7 @@ sequenceDiagram
CINJ-->>TUI: state="final" with envelope payload
REC->>AGT: agent dispatch [System] continue from existing transcript
AGT->>CC: turn
- CC->>CC: deriveSessionKey (may produce new cc-sp- due to systemPrompt drift)
+ CC->>CC: deriveSessionKey (may produce new tinker-sp- due to systemPrompt drift)
CC->>CC: getLatestResumeSessionIdByOpenclawSessionId(oc) — finds prior cli
CC->>CLI: spawn --resume
CLI-->>CC: resumed with full prior context
@@ -200,7 +200,7 @@ sequenceDiagram
- Every status:`running` session at boot is marked interrupted, regardless of lock state.
- Tail-check is informational; resume is always attempted (FORK 2026-05-10).
-- cc-bridge worker-pool prefers `getLatestResumeSessionIdByOpenclawSessionId` over hash-derived sessionKey (FORK 2026-05-10 fix for sessionKey hash drift after [System] continue).
+- tinker-bridge worker-pool prefers `getLatestResumeSessionIdByOpenclawSessionId` over hash-derived sessionKey (FORK 2026-05-10 fix for sessionKey hash drift after [System] continue).
- The envelope chip fires BEFORE the agent dispatch so the user sees the restart first, the resume second.
- **Client-side hold (FORK 2026-05-11):** before the WS closes, the gateway broadcasts `shutdown { restartExpectedMs }`. `tinker-ui/src/app.ts` marks every entry in `activeRuns` with `state: "restarting"`; the `ws addEventListener("close")` handler then preserves `activeRuns` instead of clearing them, and `renderThinkingIndicator()` paints a `RESTARTING` badge alongside the live dots. The resumed agent run's natural lifecycle `start` event replaces the entry on the new gateway, removing the badge. Safety-net: `scheduleUnconfirmedPrune` keeps restarting runs for 30 s (vs 5 s for normal unconfirmed runs) before evicting them — so even if the resume dispatch is delayed, the indicator clears cleanly rather than persisting forever.
@@ -252,11 +252,11 @@ sequenceDiagram
---
-## F6. cc-bridge tool call (claude-cli internal)
+## F6. tinker-bridge tool call (claude-cli internal)
This flow is short by design and has its own document. See `tool-loop.md`.
-**One-liner:** tool_use blocks are visible in the UI (via cc-bridge stream events) but NOT placed in `assistant.message.content`, to prevent pi-agent-core's agent-loop from re-executing them via the OpenClaw exec tool. FORK 2026-04-22.
+**One-liner:** tool_use blocks are visible in the UI (via tinker-bridge stream events) but NOT placed in `assistant.message.content`, to prevent pi-agent-core's agent-loop from re-executing them via the OpenClaw exec tool. FORK 2026-04-22.
---
@@ -306,7 +306,7 @@ sequenceDiagram
participant FS as plans/*.md
participant SEND as chat.send (loopback)
participant CINJ as chat.inject
- participant CC as cc-bridge worker-pool
+ participant CC as tinker-bridge worker-pool
participant CLI as claude-cli (--resume)
participant TUI as Tinker UI
@@ -336,7 +336,7 @@ sequenceDiagram
- The `__SYS_PLAN_RESUME__` sentinel is injected via `chat.inject` BEFORE `chat.send` so the chip appears before the agent resumes.
- The dispatch uses `deliver: false, dispatchAgent: true` — INTERNAL_MESSAGE_CHANNEL routes to the agent, the webchat subscription sees only the chip (no duplicate user bubble). See flows.md F1 invariants.
- The `systemKind: "plan-resume"` annotation is carried in the loopback call metadata for diagnostic filtering.
-- cc-bridge resume uses `getLatestResumeSessionIdByOpenclawSessionId` (FORK 2026-05-10) so sessionKey hash drift after the `[System] continue` message is tolerated.
+- tinker-bridge resume uses `getLatestResumeSessionIdByOpenclawSessionId` (FORK 2026-05-10) so sessionKey hash drift after the `[System] continue` message is tolerated.
**See also:** lifecycles.md L-PLAN, L-STEP; tinker-ui.md §**SYS_PLAN_RESUME** chip family.
diff --git a/TINKER_UI_DESIGN_BIBLE/lifecycles.md b/TINKER_UI_DESIGN_BIBLE/lifecycles.md
index 75c5cde3426b6..203f320737ab5 100644
--- a/TINKER_UI_DESIGN_BIBLE/lifecycles.md
+++ b/TINKER_UI_DESIGN_BIBLE/lifecycles.md
@@ -11,8 +11,8 @@ verify:
cmd: python3 -c 'import subprocess,json; r=subprocess.run(["openclaw","gateway","call","debug.session.state","--params",json.dumps({"sessionKey":"agent:main:main"})],capture_output=True,text=True,timeout=25); j=json.loads(r.stdout.split("Gateway call:")[-1].split("\n",1)[1] if "Gateway call:" in r.stdout else r.stdout); status = (j.get("entry") or {}).get("status"); assert status in {"idle","running","done","failed","aborted","timeout","interrupted",None}, f"unrecognised status {status!r}"'
- name: L4 — restart-recovery code path still emits the known log message
cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/src/agents/main-session-restart-recovery.ts")).read(); assert "marked interrupted main session failed" in t and "main-session-restart-recovery" in t, "restart-recovery log emission missing or renamed — refactor without a verify update is the regression class to catch"'
- - name: L2 — cc-bridge worker pool stays bounded (idle reap + LRU cap)
- cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/worker-pool.ts")).read(); assert "idleTtlMs" in t and "maxWorkers" in t and "private sweep(" in t and "isBusy()" in t, "cc-bridge SessionWorkerPool eviction removed — unbounded persistent-claude-proc leak regression class (people-profiles per-profile sessionKey, 53 procs/7+ days, 2026-05-16)"'
+ - name: L2 — tinker-bridge worker pool stays bounded (idle reap + LRU cap)
+ cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/worker-pool.ts")).read(); assert "idleTtlMs" in t and "maxWorkers" in t and "private sweep(" in t and "isBusy()" in t, "tinker-bridge SessionWorkerPool eviction removed — unbounded persistent-claude-proc leak regression class (people-profiles per-profile sessionKey, 53 procs/7+ days, 2026-05-16)"'
- name: L-STRATEGY — strategy-switch state machine still carries its threshold/recency/review transitions (U4)
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/src/memory/engram/strategy-switch.ts")).read(); ft=open(os.path.expanduser("~/src/tinkerclaw/src/memory/engram/failure-tracking.ts")).read(); assert "consecutiveErrors < cfg.threshold" in t and "recency guard" in t and "needsHumanReview" in t, "strategy-switch decision transitions renamed — L-STRATEGY diagram is now stale"; assert "export function recordFailure(" in ft and "export function recordSuccess(" in ft and "export function applySwitch(" in ft, "failure-tracking transition fns (recordFailure/recordSuccess/applySwitch) renamed — L-STRATEGY accumulate/reset/apply edges stale"'
- name: L-STRATEGY — fork.strategy.switch.list RPC is live and returns ok (U4 review surface)
@@ -59,9 +59,9 @@ stateDiagram-v2
---
-## L2. cc-bridge claude-cli worker
+## L2. tinker-bridge claude-cli worker
-**Entity:** `ClaudeCodeWorker` instance held by `SessionWorkerPool`, one per cc-bridge sessionKey.
+**Entity:** `ClaudeCodeWorker` instance held by `SessionWorkerPool`, one per tinker-bridge sessionKey.
```mermaid
stateDiagram-v2
@@ -98,7 +98,7 @@ stateDiagram-v2
- A turn's `signal: AbortSignal` parameter, when aborted, calls `worker.kill("SIGTERM")` — this is how the LLM idle watchdog terminates a stuck worker.
- **The pool is BOUNDED (FORK 2026-05-16, `worker-pool.ts`).** `SessionWorkerPool` sweeps on every `getOrCreate`: a non-busy worker idle past `idleTtlMs` (default 15 min) is SIGTERMed, and the pool is hard-capped at `maxWorkers` (default 32, LRU eviction of the least-recently-used non-busy worker). A worker mid-turn (`isBusy()`) and the sessionKey being requested are never evicted; evicted workers keep their `sessionId` in session-map.json so a later turn `--resume`s the same thread. Without this, a caller minting a unique sessionKey per work item (people-profiles cron — one key per profile) leaks one persistent ep_poll-blocked `claude` proc per item indefinitely (observed: 53 procs, oldest 7+ days, 2026-05-16). Enforced by the L2 `verify:` invariant above.
-**Probe:** `cc-bridge.workerInfo({sessionKey})` (proposed) — alive?, current cli sessionId, last turn duration.
+**Probe:** `tinker-bridge.workerInfo({sessionKey})` (proposed) — alive?, current cli sessionId, last turn duration.
---
@@ -176,7 +176,7 @@ stateDiagram-v2
stateDiagram-v2
[*] --> accepted
accepted --> dispatched: dispatchInboundMessage fire-and-forget
- dispatched --> streaming: cc-bridge spawns / pi-agent-core streamFn
+ dispatched --> streaming: tinker-bridge spawns / pi-agent-core streamFn
streaming --> streaming: state="delta" broadcasts
streaming --> finalizing_ok: lifecyclePhase=done
streaming --> finalizing_error: lifecyclePhase=error OR surface_error
diff --git a/TINKER_UI_DESIGN_BIBLE/memory-layout.md b/TINKER_UI_DESIGN_BIBLE/memory-layout.md
index f6fddc8b8af5c..6e9a088ae429c 100644
--- a/TINKER_UI_DESIGN_BIBLE/memory-layout.md
+++ b/TINKER_UI_DESIGN_BIBLE/memory-layout.md
@@ -19,6 +19,8 @@ verify:
cmd: test -d ~/.openclaw/engram/links
- name: U6 ENGRAM skill-library dir exists
cmd: test -d ~/.openclaw/engram/skill-library
+ - name: memory-core ensureVectorTable reconciles the vec0 dim against the on-disk shape, not an early-undefined in-memory field (FORK 2026-06-23)
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/memory-core/src/memory/manager-sync-ops.ts")).read(); assert "sqlite_master" in t, "ensureVectorTable no longer reads the actual on-disk FLOAT[N] dim from sqlite_master — gating the DROP only on the in-memory this.vector.dims (undefined right after a restart) lets a stale 3072-dim table survive and reject every 1024-dim mxbai-embed-large insert. The correction MUST compare against the on-disk dim."'
---
# Memory layout — workspace directory map
@@ -46,7 +48,7 @@ All paths below are under `~/.openclaw/workspace/memory/` unless otherwise noted
- **Writer:** mostly hand (Jarvis adds knowledge files at user request).
- **Reader:** memorySearch retrieval, ad-hoc reads.
-- **Notable files:** `INDEX.md`, `evolution-log.md`, `ripple-tracker.md`, `tinkerclaw-cc-bridge.md`, `tinkerclaw-people-plugin.md`, `tinkerclaw-whatsapp-plugin.md`, `wa-owner-prefix-invariant.md`, `whatsapp-strategy.md`, `cost-aware-model-routing.md`, `hermes-agent-analysis.md`, `operational-lessons.md`, `serra-projects-state.md`.
+- **Notable files:** `INDEX.md`, `evolution-log.md`, `ripple-tracker.md`, `tinkerclaw-tinker-bridge.md`, `tinkerclaw-people-plugin.md`, `tinkerclaw-whatsapp-plugin.md`, `wa-owner-prefix-invariant.md`, `whatsapp-strategy.md`, `cost-aware-model-routing.md`, `hermes-agent-analysis.md`, `operational-lessons.md`, `serra-projects-state.md`.
### `butler-log/`
@@ -199,6 +201,14 @@ Writers are mostly crons + the auto-reply pipeline. Readers are mostly Jarvis co
The single highest-leverage Reader is `agents.defaults.memorySearch` (Ollama embeddings + FTS hybrid). It indexes EVERYTHING under workspace/memory/ + sessions. Any new directory added under workspace/memory/ will be picked up automatically.
+### memorySearch vec0 table — embedder-dim reconcile (FORK 2026-06-23)
+
+memorySearch's vector index is a sqlite-vec `vec0` virtual table owned by `extensions/memory-core` (`src/memory/manager-sync-ops.ts`, `ensureVectorTable`). Its column type bakes in the embedder dimension as `FLOAT[N]` — `mxbai-embed-large` (the live ollama embedder) is **1024** dims; a prior Gemini embedder was **3072**. Switching embedders requires DROPping + recreating the table at the new dim.
+
+**Reconcile-by-on-disk-dim (commit `a33cc63200`):** `ensureVectorTable` reads the **actual on-disk** `FLOAT[N]` dim out of `sqlite_master` and drops + recreates the table only on a **genuine** mismatch against the live embedder's dim. It no longer gates that correction on the in-memory `this.vector.dims` being truthy — which after a gateway restart is `undefined`, so the old guard short-circuited and a stale 3072-dim table survived, making every 1024-dim insert throw `Expected 3072 dimensions but received 1024` (79× before a reboot). See bug-log.md FIXED 2026-06-23 (memory-core vec0 dim).
+
+**Don't regress:** the dim-correction must compare against the **on-disk** shape (`sqlite_master`), never against an in-memory field that is unset early in boot — or a wrong-dim table survives every restart and blocks all inserts.
+
## Don't regress
- Never symlink workspace/memory/ OR ~/.openclaw/engram/ subdirs into the public fork — both trees are PRIVATE.
diff --git a/TINKER_UI_DESIGN_BIBLE/ownership.md b/TINKER_UI_DESIGN_BIBLE/ownership.md
index 2974d7d59a2ee..034ff5f9eae01 100644
--- a/TINKER_UI_DESIGN_BIBLE/ownership.md
+++ b/TINKER_UI_DESIGN_BIBLE/ownership.md
@@ -13,27 +13,27 @@ verify:
# Ownership map — public tinkerclaw fork
-The public fork is touched by **three distinct agents**: Architect (Claude Code running in `~/src/jarvis-icu`), Jarvis (cc-bridge running on the same OpenClaw gateway), and human maintainer. Plus upstream OpenClaw, which we merge from periodically. This file declares which folders each can safely change without coordinating.
+The public fork is touched by **three distinct agents**: Architect (Claude Code running in `~/src/jarvis-icu`), Jarvis (tinker-bridge running on the same OpenClaw gateway), and human maintainer. Plus upstream OpenClaw, which we merge from periodically. This file declares which folders each can safely change without coordinating.
The rules are advisory at first — they're enforceable later by a CODEOWNERS overlay or a pre-commit lint. The goal here is the principle: **two agents touching the same file at the same time is the primary merge-friction cause we have today; per-folder ownership is the cheapest mitigation**.
## Ownership zones
-| Zone | Primary owner | Secondary editors | Notes |
-| ---------------------------------------- | ------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
-| `src/fork/` | Architect | — | All fork-only logic. Upstream never writes here. Safe for parallel work as long as files are distinct. |
-| `src/fork/shared/` (when introduced) | Architect | — | Cross-cutting fork utilities (pipeline, masking helpers, ID minting). Anything used by >1 fork module lives here. |
-| `src/gateway/server-methods/.ts` | Architect | — | Fork-added RPC handlers (debug, cron, wa, gateway probes, plugin probes). One file per concern, never combined. |
-| `src/gateway/server-methods.ts` | Upstream | Architect (minimal-touch only) | The central registry. Each new fork probe adds 2 lines (import + spread). Avoid larger edits. |
-| `src/gateway/method-scopes.ts` | Upstream | Architect (minimal-touch only) | Fork probes append to the READ_SCOPE list. Single-line additions only. |
-| `extensions/tinkerclaw-*/` | Per-plugin | — | Each plugin folder is its own ownership unit. `tinkerclaw-cc-bridge` ≠ `tinkerclaw-whatsapp` ≠ `tinkerclaw-people`. |
-| `extensions/tinkerclaw-control-panel/` | Jarvis | — | Active Phase-C MVP authored by Jarvis. Architect doesn't touch the plugin internals without coordinating. |
-| `tinker-ui/src/` | **Shared hotspot** | Both Architect and Jarvis | Today's biggest collision risk (65/75 FORK anchors in `app.ts`). Both agents may edit; pull before push. |
-| `tinker-ui/src/panels/` | Architect | — | Per-panel files (prefrontal-tree.ts) are split-out enough to be safe. |
-| `TINKER_UI_DESIGN_BIBLE/` | Architect | Jarvis (read-only) | Architect maintains; Jarvis consumes. Jarvis flags staleness in today's `memory/YYYY-MM-DD.md`, never edits. |
-| `scripts/` | Architect | — | Fork-side scripts (test-invariants, gen-tinker-ui-registry, pii-pre-push). Architect-only. |
-| `git-hooks/` | Architect | — | Pre-commit + pre-push hooks. Architect-only. |
-| `~/.openclaw/workspace/` (separate repo) | Jarvis (runtime) | Architect (briefings + knowledge) | Jarvis's runtime mutates this continuously (memory/, BRIEFING.md, SOUL.md). Architect adds knowledge notes only. |
+| Zone | Primary owner | Secondary editors | Notes |
+| ---------------------------------------- | ------------------ | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
+| `src/fork/` | Architect | — | All fork-only logic. Upstream never writes here. Safe for parallel work as long as files are distinct. |
+| `src/fork/shared/` (when introduced) | Architect | — | Cross-cutting fork utilities (pipeline, masking helpers, ID minting). Anything used by >1 fork module lives here. |
+| `src/gateway/server-methods/.ts` | Architect | — | Fork-added RPC handlers (debug, cron, wa, gateway probes, plugin probes). One file per concern, never combined. |
+| `src/gateway/server-methods.ts` | Upstream | Architect (minimal-touch only) | The central registry. Each new fork probe adds 2 lines (import + spread). Avoid larger edits. |
+| `src/gateway/method-scopes.ts` | Upstream | Architect (minimal-touch only) | Fork probes append to the READ_SCOPE list. Single-line additions only. |
+| `extensions/tinkerclaw-*/` | Per-plugin | — | Each plugin folder is its own ownership unit. `tinkerclaw-tinker-bridge` ≠ `tinkerclaw-whatsapp` ≠ `tinkerclaw-people`. |
+| `extensions/tinkerclaw-control-panel/` | Jarvis | — | Active Phase-C MVP authored by Jarvis. Architect doesn't touch the plugin internals without coordinating. |
+| `tinker-ui/src/` | **Shared hotspot** | Both Architect and Jarvis | Today's biggest collision risk (65/75 FORK anchors in `app.ts`). Both agents may edit; pull before push. |
+| `tinker-ui/src/panels/` | Architect | — | Per-panel files (prefrontal-tree.ts) are split-out enough to be safe. |
+| `TINKER_UI_DESIGN_BIBLE/` | Architect | Jarvis (read-only) | Architect maintains; Jarvis consumes. Jarvis flags staleness in today's `memory/YYYY-MM-DD.md`, never edits. |
+| `scripts/` | Architect | — | Fork-side scripts (test-invariants, gen-tinker-ui-registry, pii-pre-push). Architect-only. |
+| `git-hooks/` | Architect | — | Pre-commit + pre-push hooks. Architect-only. |
+| `~/.openclaw/workspace/` (separate repo) | Jarvis (runtime) | Architect (briefings + knowledge) | Jarvis's runtime mutates this continuously (memory/, BRIEFING.md, SOUL.md). Architect adds knowledge notes only. |
## Concurrency rules
diff --git a/TINKER_UI_DESIGN_BIBLE/probes.md b/TINKER_UI_DESIGN_BIBLE/probes.md
index 44c910a8bc3ea..4edf5e340a6ac 100644
--- a/TINKER_UI_DESIGN_BIBLE/probes.md
+++ b/TINKER_UI_DESIGN_BIBLE/probes.md
@@ -57,7 +57,7 @@ A probe is **deterministic** (same input → same output, clocks masked), **boun
| `[idle-timeout-diag]` journal log | LLM idle watchdog resolution per turn | one line per turn with `idleTimeoutMs=N model.requestTimeoutMs=N params.timeoutMs=N configuredRunTimeoutMs=N` | n/a (log) | `src/agents/embedded-agent-runner/run/attempt.ts:1862`, FORK 2026-05-10 |
| `journalctl --user -u openclaw-gateway.service` grep | catch-all gateway event tail | raw text | seconds | systemd journal |
| `Read(~/.openclaw/agents/main/sessions/sessions.json)` | session-store state for all sessions | JSON | <100ms | direct file read |
-| `Read(~/.openclaw/cc-bridge/session-map.json)` | cc-bridge ↔ claude-cli sessionId mapping with openclawSessionId fallback | JSON | <100ms | direct file read |
+| `Read(~/.openclaw/tinker-bridge/session-map.json)` | tinker-bridge ↔ claude-cli sessionId mapping with openclawSessionId fallback | JSON | <100ms | direct file read |
| `Read(~/.openclaw/cron/jobs.json)` | cron registry | JSON | <100ms | direct file read |
| `Read(~/.openclaw/cron/runs/.jsonl)` | cron last runs | JSONL | <100ms | direct file read |
| `Read(~/.claude/projects/-home-globalcaos--openclaw-jarvis-workspace/.jsonl)` | claude-cli session transcript | JSONL | <100ms | direct file read |
@@ -106,7 +106,7 @@ Each row is a surface where investigation is expensive today.
| `debug.session.config({provider})` | effective resolved provider + model with override chain | catches the timeoutSeconds-dead-code regression on commit | **HIGH** |
| `wa.lastOutbound({chat,n})` | last N WhatsApp outbound messages, including dropped/queued | catches "Jarvis's reply never delivered" silently | **HIGH** |
| `cron.lastRun({jobId})` | last run state, exit code, duration, output tail | replaces manual receipt-grep | medium |
-| `cc-bridge.workerInfo({sessionKey})` | alive?, current cli sessionId, last turn duration, idle status | catches stuck workers (needs plugin API to expose pool) | medium |
+| `tinker-bridge.workerInfo({sessionKey})` | alive?, current cli sessionId, last turn duration, idle status | catches stuck workers (needs plugin API to expose pool) | medium |
| `wa.lastInbound({chat,n})` | last N inbound messages for one chat | symmetric companion to wa.lastOutbound | medium |
| `agent.dispatch.lastN({n})` | last N chat.send / agent invocations gateway-wide | catches dispatch storms | low |
| ~~`plugin.boot.status`~~ — **LIVE 2026-05-11** (Live table above). | per-plugin boot result (load ok / failed / disabled / version) | catches M5 native-deps failures synchronously | shipped |
diff --git a/TINKER_UI_DESIGN_BIBLE/session-naming.md b/TINKER_UI_DESIGN_BIBLE/session-naming.md
index 99a26daa48e53..3f6b6f4a3921a 100644
--- a/TINKER_UI_DESIGN_BIBLE/session-naming.md
+++ b/TINKER_UI_DESIGN_BIBLE/session-naming.md
@@ -2,11 +2,11 @@
file: session-naming.md
purpose: Single source of truth for how every visible name on a session is generated, persisted, and rendered. Captures the current state, the divergence bugs we hit, and the unified contract.
audience: AI
-last_verified: 2026-06-10
+last_verified: 2026-06-25
last_verified_commit: HEAD
single_owner: yes — anything about session/tab naming, fortune cookies, label-resolution priority lives ONLY here. cookiePhrase, tab.title, label, displayName, cookiePhraseUserSet all funnel through one chain documented below.
see_also: tinker-ui.md §5.69 (sessions list — server-resolver hardening), bug-log.md FIXED [config-dead-code] 2026-05-24 (gateway-rebuild gotcha that hid Bug 1 of `task-mpjhzu3j-ma9ts` for two hours).
-status: DEPLOYED. 2026-06-10 (u3-tab-naming) made user-set / auto tab names DURABLE SERVER-SIDE (see "Server-durable user-set names" below) — they now survive ANY restart, browser, or device, not just this browser's localStorage. The server DOES lazy-mint a key-derived fortune `cookiePhrase` (FORTUNE_COOKIES pool) for sessions that have no deliberate name; the mint now SKIPS any session flagged `cookiePhraseUserSet`. The auto-title (Ollama) was also retuned to lean on the user's prompts and stay distinct from sibling tabs. (History: 2026-05-24 second pass made the FORTUNE_COOKIES pool the only phrase source; the first-pass 2-word generator was deleted.)
+status: DEPLOYED. 2026-06-10 (u3-tab-naming) made user-set / auto tab names DURABLE SERVER-SIDE (see "Server-durable user-set names" below) — they now survive ANY restart, browser, or device, not just this browser's localStorage. The server DOES lazy-mint a key-derived fortune `cookiePhrase` (FORTUNE_COOKIES pool) for sessions that have no deliberate name; the mint now SKIPS any session flagged `cookiePhraseUserSet`. The auto-title was also retuned to lean on the user's prompts and stay distinct from sibling tabs. **u4 (2026-06-25)** moved the auto-title MODEL PATH off direct Ollama onto a webchat-callable gateway RPC — `sessions.suggestTitle`, a one-shot cc-bridge Sonnet completion — and added clone auto-naming (an explicit kick), a doubled-title guard, and a non-persisted in-flight shimmer (see "Auto-title mechanism — cc-bridge Sonnet RPC + clone kick" below). (History: 2026-05-24 second pass made the FORTUNE_COOKIES pool the only phrase source; the first-pass 2-word generator was deleted.)
verify:
- name: lazy-mint skips a user-set cookiePhrase (never trample a deliberate name)
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/session-utils.ts")).read(); i=t.find("const canonicalPhrase = fortuneForKey(key)"); assert i!=-1, "lazy-mint canonicalPhrase line missing"; assert "cookiePhraseUserSet) continue" in t[max(0,i-800):i], "lazy-mint must skip cookiePhraseUserSet before re-minting the fortune"'
@@ -16,6 +16,14 @@ verify:
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/server-methods/sessions.ts")).read(); assert "isDisplayNameOnlyPatch" in t and "DISPLAY_NAME_PATCH_KEYS" in t'
- name: Tinker UI persists deliberate names server-side
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "persistTabNameToServer" in t and "cookiePhraseUserSet" in t'
+ - name: u4 — auto-title runs via the sessions.suggestTitle cc-bridge Sonnet RPC (NOT ollama/gemini/metered-API), webchat-callable
+ cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/server-methods/suggest-title.ts")).read(); assert "runEmbeddedPiAgent" in t and "claude-code" in t and "claude-sonnet-4-6" in t, "title one-shot must run cc-bridge Sonnet"; s=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/server-methods/sessions.ts")).read(); assert "suggestTitleViaBridge" in s and "sessions.suggestTitle" in s, "sessions.suggestTitle handler missing"; ms=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/method-scopes.ts")).read(); assert "sessions.suggestTitle" in ms, "sessions.suggestTitle scope missing (must be webchat-callable)"'
+ - name: u4 — title one-shot timeout is 45s (a cold cc-bridge claude spawn legitimately exceeds 15s)
+ cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/server-methods/suggest-title.ts")).read(); assert "timeoutMs: 45_000" in t, "title timeoutMs must be 45_000 (15_000 returned null -> tabs never renamed)"'
+ - name: u4 — clone tabs get an explicit auto-title kick; client calls the RPC (not fork.subagents.spawn)
+ cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "pendingTitleKickTabId" in t, "clone auto-title kick missing"; assert "spawnTitleViaBridge" in t and "sessions.suggestTitle" in t, "client must call sessions.suggestTitle"'
+ - name: u4 — client collapses an exact doubled title + dedups generation via a runtime (non-persisted) set
+ cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "collapseDoubled" in t, "collapseDoubled (doubled-title guard) missing"; assert "titleInFlight" in t, "titleInFlight (runtime generation dedup) missing"'
---
# Session-naming contract
@@ -44,8 +52,9 @@ Four callsites currently set `tab.title`:
3. **`attachSessionToTab(key)`** (`app.ts:5302`) — when the user clicks a row in the sessions panel to open it in the active tab.
- Prefers `sess.cookiePhrase` (the server's burned-in value, which is itself a `FORTUNE_COOKIES` entry); falls through to `sess.label.slice(0, 30)` if no phrase yet.
-4. **Auto-title** (LOCAL OLLAMA-generated topic phrase from the chat content) — fires on assistant-turn `end` at user-turn 1 then every `TAB_TITLE_INTERVAL` (=5) turns, via `generateTabTitle()` (`app.ts`). The user-meaningful customisation path; wins over the random fortune, sets `titleLocked`, AND persists server-side via `sessions.patch {cookiePhrase, cookiePhraseUserSet:true}`.
- - **u3-tab-naming (2026-06-10):** the prompt is built PRIMARILY from the user's own recent prompts (recency-weighted; assistant replies are secondary context only) and is fed the OTHER open tabs' deliberate (locked) names with an instruction to make THIS title distinct + specific. Output = one relevant emoji + 2-4 words (≤48 chars); a unique leading emoji is enforced via `pickUniqueTabIcon`. (The bible previously said "Gemini"; the live path is local Ollama at `localhost:11434`.)
+4. **Auto-title** (LLM-generated topic phrase from the chat content) — fires on assistant-turn `end` at user-turn 1 then every `TAB_TITLE_INTERVAL` (=5) turns, via `generateTabTitle()` (`app.ts`). The user-meaningful customisation path; wins over the random fortune, sets `titleLocked`, AND persists server-side via `sessions.patch {cookiePhrase, cookiePhraseUserSet:true}`.
+ - **u3-tab-naming (2026-06-10):** the prompt is built PRIMARILY from the user's own recent prompts (recency-weighted; assistant replies are secondary context only) and is fed the OTHER open tabs' deliberate (locked) names with an instruction to make THIS title distinct + specific. Output = one relevant emoji + 2-4 words (≤48 chars); a unique leading emoji is enforced via `pickUniqueTabIcon`.
+ - **u4-tab-naming (2026-06-25):** the MODEL PATH moved off the old direct Ollama (`localhost:11434`) call to the gateway RPC `sessions.suggestTitle`, a one-shot cc-bridge Sonnet completion. The prompt-shaping rules above are unchanged. See "Auto-title mechanism — cc-bridge Sonnet RPC + clone kick" below for the RPC, the 45s cold-spawn timeout, clone auto-naming, the doubled-title guard, and the in-flight shimmer. (Any earlier bible text saying "Gemini" or "local Ollama" for the model path is obsolete.)
5. **`loadTabs()`** restore at module load — reads `localStorage["tinker.tabs"]` and restores `tab.title` for previously-persisted tabs. No new minting.
@@ -96,6 +105,10 @@ After ~a week or two of normal use, `LEGACY_2WORD_PHRASE_RE` + the migration bra
- A deliberate name (manual rename OR successful auto-name) is protected by TWO layers that must stay in lockstep: client `titleLocked` (localStorage cache) AND server `cookiePhraseUserSet` (durable). `loadSessions()` must never clobber a locked tab, and must ADOPT + lock a server `cookiePhraseUserSet` name on an unlocked tab (restore-after-wipe). Don't reduce this back to the `LEGACY_2WORD_PHRASE_RE` shape heuristic.
- Keep the webchat write path NARROW: only a display-name-only patch (`isDisplayNameOnlyPatch`) is exempt from `rejectWebchatSessionMutation`. Never broaden the exemption to other session-metadata fields, and never touch the `sessions.pluginPatch` guard.
- The auto-title (`generateTabTitle`) must stay PROMPT-focused (the user's own messages are the primary signal) and sibling-DISTINCT (fed the other open tabs' names). Don't revert it to summarising the whole transcript or ignoring sibling tabs. See the amendment section below.
+- (u4, 2026-06-25) The auto-title model path is the **`sessions.suggestTitle` cc-bridge RPC** (subscription Sonnet). Never revert to a webchat `fork.subagents.spawn` (scope-rejected → no title) or to a direct Ollama/Gemini/metered-API call. Keep `sessions.suggestTitle` in the READ_SCOPE group so the webchat can call it.
+- (u4) Keep the title one-shot `timeoutMs` generous (**45s**) — the cc-bridge cold-spawn legitimately takes 14–19s; a tight timeout makes the RPC return `null` and the tab silently never renames.
+- (u4) A freshly cloned tab needs the explicit `pendingTitleKickTabId` kick — it emits no turn-end event, so removing the kick silently breaks clone auto-naming. Don't "fix" clone naming by seeding the clone's message buffer (dead end: the active clone reads the global buffer that `loadChat` overwrites).
+- (u4) `titleGenerating` is a TRANSIENT in-flight/shimmer flag — never persist it (strip in `saveTabs`, reset in `loadTabs`); dedup generation via the runtime `titleInFlight` set, or a tab saved mid-generate strands "generating forever" and the dedup guard blocks every future rename.
## Title-lock + server-durable user-set names (DEPLOYED — u2 2026-06-06, u3 2026-06-10)
@@ -111,6 +124,23 @@ After ~a week or two of normal use, `LEGACY_2WORD_PHRASE_RE` + the migration bra
Net: `localStorage["tinker.tabs"]` is now a fast CACHE; `sessions.json`'s `cookiePhrase` (+ `cookiePhraseUserSet`) is the durable source of truth for a deliberate name. Custom/auto names survive hard refresh, gateway restart, **computer restart, a different browser, AND a different device**. See `bug-log.md` FIXED [ui-state-clear] (2026-06-06) + the u3 entry (2026-06-10).
+## Auto-title mechanism — cc-bridge Sonnet RPC + clone kick (DEPLOYED — u4, 2026-06-25)
+
+The auto-titler's MODEL PATH and TRIGGERING were reworked. The u3 prompt-shaping rules (user-prompt-primary, sibling-distinct, one emoji + 2-4 words) are unchanged.
+
+**Model path: a dedicated gateway RPC, not a privileged subagent and not Ollama.** `generateTabTitle()` builds the prompt client-side, then the client helper `spawnTitleViaBridge` calls **`sessions.suggestTitle {sessionKey, prompt}`** (`app.ts`). The handler (`server-methods/sessions.ts`) runs **`suggestTitleViaBridge()`** (`server-methods/suggest-title.ts`): a one-shot `runEmbeddedPiAgent({provider:"claude-code", model:"claude-sonnet-4-6", disableTools:true, …})` in a throwaway `temp:title-suggest` session, returning the first non-empty payload text. This is **subscription-billed via the cc-bridge — NOT the metered Anthropic API, NOT Ollama, NOT Gemini.**
+
+- **Why a new RPC (not the old path):** the title used to be generated by a webchat-issued `fork.subagents.spawn`, but that method is `operator.admin`-scoped and the Tinker UI is a webchat client (`operator.read`) → the spawn was scope-rejected and **no title was ever generated**. `sessions.suggestTitle` is added to the READ_SCOPE group (`method-scopes.ts`) so the webchat can call it; the privileged bridge run happens server-side. (commit `ef337eb1dd`)
+- **45s timeout (was 15s).** The cc-bridge **cold-spawns a full `claude` CLI worker per title** (persona + plugins), so most of the wall-clock is worker startup, not generation (observed ~3.4s gen but ~11–16s spawn → 14–19s total). At `timeoutMs: 15_000` the run hit `FailoverError: LLM request timed out`, `suggestTitleViaBridge` returned `null`, and the tab silently failed to rename. Raised to **`timeoutMs: 45_000`** (commit `00c8e5b79b`). The client `req()` has no timeout (it waits for the gateway), so 45s is the only cap. A leak of ~500 transient `llm-client-*` systemd units (one per spawn, never reaped) had progressively slowed spawns; a gateway restart clears them. See `bug-log.md` FIXED [scope-mismatch+timeout-tuning] (2026-06-25).
+
+**Clone auto-naming: an explicit kick.** The ONLY automatic trigger is the assistant-turn `end` handler; a **freshly cloned tab emits no turn-end event**, so it was never titled. (Seeding the clone's message buffer was a dead end — an active clone reads the global `messages` buffer that `loadChat()` overwrites, and a forked clone already carries the parent's user prompts server-side; the missing piece was the TRIGGER, not the buffer.) Fix: `cloneTab()` sets a module-level `pendingTitleKickTabId = clone.id`; `loadChat()`, once it has refilled the buffer for the active clone, kicks `generateTabTitle(clone)` exactly once — gated on the buffer actually having a user message, so an empty/lineage fork keeps its doubled-icon placeholder. See `tinker-ui.md` §5.8i for the clone fork/transcript itself.
+
+**Doubled-title guard.** The cc-bridge **intermittently** doubles a one-shot's text payload (`"Fix auth bugFix auth bug"` — same family as the duplicate-served-text coalescing work). `generateTabTitle()`'s cleaning runs `collapseDoubled()` on both the full title and the word part: it collapses an EXACT first-half==second-half match (optionally with one separating space) when each half is ≥4 chars, leaving legitimate titles and short reduplications ("Go Go") untouched. This is a display-layer band-aid; the bridge double itself is a separate, deeper bug.
+
+**In-flight shimmer.** While a title generates, the tab pulses (`tab.titleGenerating` → `.tab-renaming`). Generation is de-duped via a **runtime-only `Set` `titleInFlight`** keyed by tab id — NOT the persisted flag. `titleGenerating` is now stripped in `saveTabs()` and reset in `loadTabs()`; a persisted `titleGenerating=true` used to strand a tab "generating forever" and block every future rename.
+
+The clone kick, doubled-title guard, and shimmer-dedup live in `tinker-ui/src/app.ts` (HMR-live; commit pending — `app.ts` is a contended hot file). The RPC, scope, and 45s timeout are committed (`ef337eb1dd`, `00c8e5b79b`).
+
## Verify
The executable invariants for this optic live in this file's YAML **frontmatter** `verify:` block (run by `pnpm bible:invariants`): the lazy-mint skips `cookiePhraseUserSet`; `SessionEntry` + the `sessions.patch` schema carry the flag; the webchat guard exempts only display-name-only patches; and the Tinker UI persists deliberate names server-side.
diff --git a/TINKER_UI_DESIGN_BIBLE/slos.md b/TINKER_UI_DESIGN_BIBLE/slos.md
index dc2b383076dff..c148551157836 100644
--- a/TINKER_UI_DESIGN_BIBLE/slos.md
+++ b/TINKER_UI_DESIGN_BIBLE/slos.md
@@ -48,7 +48,7 @@ We launch with three. Each was chosen by the leverage test: the SLO either (a) n
- Why this is the SLO: morning-briefing is the user-facing flagship. When it takes 10 minutes instead of 1, the user notices, and the lag is usually a precursor to a fail. Tracking p95 latency catches the slow-creep regressions before they tip over into outright failures.
- Window: rolling 7 days, successful runs only (failed runs have their own SLO).
- Burn rate: target 95% within budget → budget is 5% slow runs. Observed via `durationMs` field in each receipt.
-- Recommended action when burning: read `details.p50Ms`, `details.p95Ms`, `details.maxMs`. If p95 is creeping toward 300s, investigate `failures.md` M1 (cc-bridge SIGTERM) — slow briefings often precede outright timeouts.
+- Recommended action when burning: read `details.p50Ms`, `details.p95Ms`, `details.maxMs`. If p95 is creeping toward 300s, investigate `failures.md` M1 (tinker-bridge SIGTERM) — slow briefings often precede outright timeouts.
## How the probe surfaces this
diff --git a/TINKER_UI_DESIGN_BIBLE/subagents-and-recipes.md b/TINKER_UI_DESIGN_BIBLE/subagents-and-recipes.md
index 99e8d64697b42..b2da65033ff1a 100644
--- a/TINKER_UI_DESIGN_BIBLE/subagents-and-recipes.md
+++ b/TINKER_UI_DESIGN_BIBLE/subagents-and-recipes.md
@@ -5,7 +5,7 @@ audience: AI
last_verified: 2026-06-04
last_verified_commit: 24237e0cd22
single_owner: yes — subagent + kit orchestration + plan persistence + skill-library behavior + marketplace semantics facts live here
-see_also: topology.md (Prefrontal plugin), flows.md (F6 cc-bridge tool loop, F-PLAN-RESUME, F-KIT-INSTALL), tool-loop.md (why fork orchestration is different from upstream), memory-layout.md (WHERE recipe-archive / skill-library / failure-state stores live on disk), probes.md (fork.skill.* / fork.prefrontal.* RPC surface), config-shape.md (RECIPE_AUTOAPPLY_ENABLED, dead-code trap registry)
+see_also: topology.md (Prefrontal plugin), flows.md (F6 tinker-bridge tool loop, F-PLAN-RESUME, F-KIT-INSTALL), tool-loop.md (why fork orchestration is different from upstream), memory-layout.md (WHERE recipe-archive / skill-library / failure-state stores live on disk), probes.md (fork.skill.* / fork.prefrontal.* RPC surface), config-shape.md (RECIPE_AUTOAPPLY_ENABLED, dead-code trap registry)
verify:
- name: recipe-matcher exists and auto-seeds a plan at turn start (FORK 2026-05-16 — the smart-router matching half)
cmd: python3 -c 'import os; m=open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/recipe-matcher.ts")).read(); assert "export async function seedPlanFromPrompt" in m and "NO-MATCH" in m and "recipe-gap" in m, "recipe-matcher.ts missing seedPlanFromPrompt or the no-match recipe-gap WARN — the smart-router matching half regressed"; idx=open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/index.ts")).read(); assert "seedPlanFromPrompt" in idx and "before_prompt_build" in idx, "index.ts no longer wires seedPlanFromPrompt into a before_prompt_build hook — turn-start auto-seed is dead, restart-continue has nothing to resume for normal turns"'
@@ -177,19 +177,21 @@ verify:
cmd: grep -Fq 'blocked-awaiting-input' ~/src/tinkerclaw/src/gateway/protocol/schema/prefrontal-plan.ts && grep -Fq 'async setStatus' ~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/plan-store.ts && grep -Fq 'interactiveMode' ~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/recipe-runner.ts && grep -Fq 'export function deriveAskTimeoutMs' ~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/recipe-runner.ts
- name: BROCA P1.1 ask is OPT-IN (askIfMissing) with clear-fail the default; the resolver persists the answer (asked-once-then-reused, secret-confirm)
cmd: grep -Fq 'askIfMissing' ~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/recipe-rpcs.ts && grep -Fq 'export function makeAskResolver' ~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/recipe-ask-resolver.ts
+ - name: bible-currency-gate exists and is mandated by the always-read doctrine
+ cmd: python3 -c 'import os; r=os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-prefrontal/recipes/bible-currency-gate/recipe.md"); d=open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/prompts/orchestration-disposition.md")).read(); assert os.path.exists(r), "bible-currency-gate recipe missing"; assert "bible-currency-gate" in d, "orchestration-disposition.md does not mandate the gate"'
---
# Subagents, kits, plans, and Prefrontal observability
## Why the fork has its own subagent path
-cc-bridge sessions use claude-cli internally; claude-cli has its own subagent mechanism (`Task` tool, agents-md hierarchy). But the fork needs a SECOND path: spawn an OpenClaw subagent that uses a non-cc-bridge provider (e.g., openai, google), or an OpenClaw subagent that runs orchestration logic separate from claude-cli's tool tree.
+tinker-bridge sessions use claude-cli internally; claude-cli has its own subagent mechanism (`Task` tool, agents-md hierarchy). But the fork needs a SECOND path: spawn an OpenClaw subagent that uses a non-tinker-bridge provider (e.g., openai, google), or an OpenClaw subagent that runs orchestration logic separate from claude-cli's tool tree.
The fork RPC for this is `fork.subagents.spawn` (`src/fork/subagents-rpc.ts`, FORK 2026-04-20). It wraps `spawnSubagentDirect` from the agent runtime.
## The spawn helper
-`~/src/tinkerclaw/scripts/openclaw-spawn-subagent.mjs` is the CLI wrapper Jarvis uses from cc-bridge:
+`~/src/tinkerclaw/scripts/openclaw-spawn-subagent.mjs` is the CLI wrapper Jarvis uses from tinker-bridge:
```
node ~/src/tinkerclaw/scripts/openclaw-spawn-subagent.mjs \
@@ -413,6 +415,10 @@ Discipline: every `{{param}}` in prose MUST be declared (`checkParamRefs`); name
**Public-vs-private repo split (architect rule, 2026-06-11) — the WHOLE POINT of the skeleton+variables divide.** The generic skeleton `.md` BELONGS IN the public `tinkerclaw` fork — it's shareable AND crash-recoverable via the public git, and it's matchable (the matcher scans the fork's source tree). The private VALUES belong ONLY in the gitignored `recipe-vars.json`. So **never commit a recipe carrying private data** (client names, IBANs, host paths, internal strategy) to the fork — parameterize it into a skeleton first. Worked example: `crear-oferta-serravision` carried an IBAN + a real client name, so it was scrubbed into the generic `create-sales-offer` (public skeleton) + private vars, then deleted. **If private data DID reach the public fork:** `git rm` it from HEAD **and scrub it from history** (filter-repo/BFG) **before any push** — the pre-push PII grep scans the working tree, not history, so a historical commit (e.g. a parallel session's `add crear-oferta` commit) still leaks on push. **Recovery divide:** skeletons recover from the public fork git; `recipe-vars.json` is committed to the PRIVATE jarvis-brain repo (whitelisted in `~/.openclaw/.gitignore`, recoverable on crash) and holds NON-CREDENTIAL config only. HARD RULE: now that recipe-vars.json is tracked, a real credential (token/key/password/IBAN) must NEVER be added to it — keep those in a separate untracked store, or the never-track-secrets guarantee is lost. **Why no overlay needed:** because skeletons go to the public source tree (matched + recovered there) and only VALUES are private, there are no private recipe _files_ — so the `~/.openclaw/recipes` overlay (which the matcher does NOT scan) isn't needed for our recipes.
+## Bible-currency gate (2026-06-19)
+
+Any BROCA task (recipe run/compose/author) that changes tinkerclaw design / structure / behavior / config MUST end by updating the bible per the single-owner discipline (fix → `bug-log.md`; structural → owning optic + `verify:`; decision → `bible.md` §sub-letter) and confirming `pnpm bible:invariants` green. Enforced by the `bible-currency-gate` recipe, mandated in `orchestration-disposition.md` + the `model-effort-gating` skill. Read-only/marketing/research runs are exempt (no-op). Orchestrator-level, once per task. Origin: a §5.8 grouping change that left the owning optic stale (see `bug-log.md` FIXED A).
+
## The OSS-harness upgrades — how recipes/skills/plans LEARN (FORK 06f8647fdc, 2026-06-02)
Twelve OSS-harness upgrades landed together on `develop` (`06f8647fdc` on top of `70ad58e45d`); the five that change how **recipes/kits/plans/skills behave** are owned here (U1, U5, U6, U11, U12). The Cerebellum-side STORES they read/write — `recipe-archive/`, `skill-library/`, `failure-state.json` — are owned by `memory-layout.md`; the gateway RPC SURFACE (`fork.skill.*`, `fork.prefrontal.*`) is owned by `probes.md`. This optic owns the BEHAVIOR.
@@ -493,7 +499,7 @@ VERIFIED 2026-06-04 / 24237e0cd22 end-to-end: `fork.skill.put` deposited 2 skill
Three ways the catalog grows beyond hand-authoring + on-the-fly authoring:
-- **CC-skills bridge** (`cc-skills-bridge.ts`): imports a Claude-Code `SKILL.md` (frontmatter `name`/`description` + a documented procedure) as a recipe/1.0. `skillMdToRecipeSpec` infers an ordered step list from the body's `### N.` / `### Step N -` headings and transpiles to a `RecipeSpec`, then runs it through the EXISTING `recipe-author.ts` guards (`validateRecipeSpec` + `buildRecipeMd`) — the phantom-step + slug-traversal guards apply for free (no validator fork). Bridged recipes are stamped `authoredBy: "cc-bridge"` (low-trust / filterable) and land under `/bridged-skills//`, which the matcher scans (`loadRecipeIndex` extraDirs) so they're matchable next turn. Untrusted-content safety: `assertNoSymlink` vets every path segment with `lstat` before any read (`resolveSandboxPath` blocks `../` but NOT symlinks). Triggered via `prefrontal.recipe.install { skillMd }`.
+- **CC-skills bridge** (`cc-skills-bridge.ts`): imports a Claude-Code `SKILL.md` (frontmatter `name`/`description` + a documented procedure) as a recipe/1.0. `skillMdToRecipeSpec` infers an ordered step list from the body's `### N.` / `### Step N -` headings and transpiles to a `RecipeSpec`, then runs it through the EXISTING `recipe-author.ts` guards (`validateRecipeSpec` + `buildRecipeMd`) — the phantom-step + slug-traversal guards apply for free (no validator fork). Bridged recipes are stamped `authoredBy: "tinker-bridge"` (low-trust / filterable) and land under `/bridged-skills//`, which the matcher scans (`loadRecipeIndex` extraDirs) so they're matchable next turn. Untrusted-content safety: `assertNoSymlink` vets every path segment with `lstat` before any read (`resolveSandboxPath` blocks `../` but NOT symlinks). Triggered via `prefrontal.recipe.install { skillMd }`.
- **Local search fallback** (`recipe.search`): when Journey is unreachable the search degrades to the LOCAL catalog (own-kits + bridged imports) scored with the SAME fitness+rating signals as the turn-start seed (`source: "local"`, `fallbackReason` set) — same Risk-7 graceful-degradation posture as the marketplace, never hard-fails a search.
- **Transitive dependency resolver** (`recipe.install`): after a kit is written, `installDeps` parses its frontmatter `composes: [...]` AND leading `uses: [` step directives and installs each, recursively, cycle-guarded by a `seen` set. Each dep resolves with its OWN declared constraint (a trailing `@]`) or `latest` — **the root install's `p.ref` constraint is NEVER inherited by a transitive dep**. The risk-gate + sandbox write apply uniformly to root and deps.
@@ -679,7 +685,7 @@ estimates.
The kit-parallelism above governs the fork's OWN subagent dispatch. **ORCA** is the
complementary tool for _writing code across many files at once_: a Claude-Code
-Workflow (not a kit) that any session — Claude Code or this runtime via cc-bridge —
+Workflow (not a kit) that any session — Claude Code or this runtime via tinker-bridge —
should reach for **by default whenever a coding task touches 2+ independently-editable
files**, instead of editing them serially.
diff --git a/TINKER_UI_DESIGN_BIBLE/tinker-ui.md b/TINKER_UI_DESIGN_BIBLE/tinker-ui.md
index 0ad81404da1a5..7beb946d9a1c4 100644
--- a/TINKER_UI_DESIGN_BIBLE/tinker-ui.md
+++ b/TINKER_UI_DESIGN_BIBLE/tinker-ui.md
@@ -26,6 +26,8 @@ verify:
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/sectioned-reply.ts")).read(); assert "splitLeadingNarration" in t and "narration-details" in t and "Commentary" in t, "narration-split helper / narration-details block / Commentary label missing in sectioned-reply.ts"'
- name: §5.8d narration/answer separation — plain-path render sites in app.ts call splitLeadingNarration
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "splitLeadingNarration" in t and "narration-details" in t, "splitLeadingNarration / narration-details not wired into app.ts plain-path render sites"'
+ - name: §5.8 marker-free structural grouping (Bug A) — narrationIndices + answerIndices wired, position-only slice retired
+ cmd: python3 -c 'import os; rg=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/reply-grouping.ts")).read(); a=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "export function narrationIndices" in rg, "narrationIndices missing in reply-grouping.ts"; assert "narrationIndices(" in a and "answerIndices" in a, "app.ts run-grouping not using narrationIndices/answerIndices (Bug A would regress)"'
- name: §5.8e native-reasoning stream consumer — app.ts handles stream=="thinking" and renders an _isReasoning bubble
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "p?.stream === \"thinking\"" in t, "event:agent stream==thinking handler missing in app.ts"; assert "_isReasoning" in t, "_isReasoning marker missing in app.ts"'
- name: §5.8e DON'T-REGRESS — _isReasoning is excluded from the positional thinking classifier (skip guard) and gets a dedicated .msg-thinking renderMsg branch
@@ -36,10 +38,22 @@ verify:
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "KNOWN_STREAMS" in t, "KNOWN_STREAMS denylist for the unknown-stream fallback missing in app.ts"'
- name: §5.8f per-tab thinking slider — model-think-slider element exists in app.ts (rendered into #budget-panel under the active tab's model row, NOT the removed chat-area strip)
cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "model-think-slider" in t, "model-think-slider (per-tab thinking slider in the Models panel) missing in app.ts"'
- - name: §5.8h EEG panel — eeg-trace module exists and app.ts mounts it (model-force-slider + eeg-paper + data-eeg-turn markers)
- cmd: python3 -c 'import os; m=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/panels/eeg-trace.ts")).read(); t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "EegTraceStore" in m and "eegStopX" in m, "eeg-trace.ts contract exports missing"; assert all(s in t for s in ("model-force-slider","eeg-paper","data-eeg-turn")), "EEG mount tokens missing in app.ts"'
+ - name: §5.8h EEG panel — independent panel, cap-10 staggered tree, stale-sweep, reload-proof prompt-click (2026-06-19)
+ cmd: python3 -c 'import os; m=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/panels/eeg-trace.ts")).read(); t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "EegTraceStore" in m and "eegStopX" in m, "eeg-trace.ts contract exports missing"; assert "STRAND_CAP = 10" in m, "strand cap must be 10 (bible §5.8h inv4)"; assert "closeStaleRunning" in m, "closeStaleRunning (dead-branch sweep) missing in eeg-trace.ts"; assert all(s in t for s in ("model-force-slider","eeg-paper","data-eeg-turn","eeg-panel","eeg-scope-toggle","sweepDeadEegBranches","data-eeg-prompt-index")), "EEG panel/sweep/prompt-index tokens missing in app.ts"'
- name: §5.8g call-tree effort chip — app.ts consumes the stream:"effort" agent-event
- cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "\"effort\"" in t, "app.ts no longer handles the stream:\"effort\" agent-event — the call-tree effort chip (requested cap vs actual thinking-chars) loses its data source. The effort fields are computed server-side in cc-bridge stream.ts (see tool-loop.md); the UI only consumes them."'
+ cmd: python3 -c 'import os; t=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "\"effort\"" in t, "app.ts no longer handles the stream:\"effort\" agent-event — the call-tree effort chip (requested cap vs actual thinking-chars) loses its data source. The effort fields are computed server-side in tinker-bridge stream.ts (see tool-loop.md); the UI only consumes them."'
+ - name: §5.8i Clone tab — sessions.fork eager-transcript RPC + doubled-icon title + retired tab-clone-badge (2026-06-24)
+ cmd: python3 -c 'import os; s=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/server-methods/sessions.ts")).read(); a=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); css=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/styles/base.css")).read(); assert "\"sessions.fork\":" in s and "SessionManager.forkFrom" in s and "buildDashboardSessionKey" in s and "cloneCheckpointSessionEntry" in s and "reason: \"fork\"" in s, "sessions.fork RPC / forkFrom reuse / key-mint / clone-entry / reason:fork missing in sessions.ts (bible 5.8i)"; assert "\"sessions.fork\"" in a and "resolveClonedSessionKey" in a and "${icon}${parent.title}" in a, "app.ts clone-tab fork wiring or doubled-icon title missing (bible 5.8i)"; assert ".tab-clone-badge {" not in css, "retired .tab-clone-badge CSS rule reappeared in base.css (bible 5.8i)"; assert "cloneIcon" not in a, "retired Tab.cloneIcon live field reappeared in app.ts (bible 5.8i)"'
+ - name: §5.8j recoverable-error warning + client-side auto-retry — pure policy module exports the 6-step ladder + null-at-exhaustion (2026-06-24)
+ cmd: python3 -c 'import os; p=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/retry-policy.ts")).read(); assert "export const RETRY_LADDER_MS = [3000, 10000, 30000, 120000, 420000, 900000]" in p, "RETRY_LADDER_MS 6-step ladder (3s..15m) missing/changed in retry-policy.ts (bible 5.8j)"; assert "export function classifyRecoverable" in p and "export function nextRetryDelayMs" in p and "export function formatWait" in p and "export function labelFor" in p, "retry-policy.ts must export classifyRecoverable/nextRetryDelayMs/formatWait/labelFor (bible 5.8j)"; assert "if (attempt >= RETRY_LADDER_MS.length) return null" in p, "nextRetryDelayMs must return null at ladder exhaustion (bible 5.8j)"; assert "Math.max(step," in p, "nextRetryDelayMs must honor max(ladderStep, retryAfter) (bible 5.8j)"'
+ - name: §5.8j app.ts wires classifyRecoverable + a fresh idempotencyKey resend + per-session retryState + hover stop-link (2026-06-24)
+ cmd: python3 -c 'import os; a=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); css=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/styles/base.css")).read(); assert "from \"./retry-policy.js\"" in a, "app.ts must import the retry-policy module (bible 5.8j)"; assert "classifyRecoverable(" in a and "nextRetryDelayMs(" in a, "app.ts must call classifyRecoverable + nextRetryDelayMs (bible 5.8j)"; assert "retryState" in a and "_isRetryWarning" in a, "per-session retryState / _isRetryWarning bubble missing in app.ts (bible 5.8j)"; assert "idempotencyKey: uuid()" in a, "retry resend must use a FRESH idempotencyKey (uuid()) — original key dedup-blocks it (bible 5.8j)"; assert "retry-stop-link" in a, "hover stop-retrying link missing in app.ts render (bible 5.8j)"; assert ".retry-stop-link" in css and ".msg-overload-bubble.retrying" in css, "retry stop-link / retrying-pulse styles missing in base.css (bible 5.8j)"'
+ - name: §5.8j chat error event schema carries reason (+ optional retryAfter) and the emit site populates reason (2026-06-24)
+ cmd: python3 -c 'import os; sch=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/protocol/schema/logs-chat.ts")).read(); ch=open(os.path.expanduser("~/src/tinkerclaw/src/gateway/server-methods/chat.ts")).read(); assert "reason: Type.Optional(Type.String())" in sch, "ChatEventSchema must carry optional reason (bible 5.8j)"; assert "retryAfter: Type.Optional(Type.Number" in sch, "ChatEventSchema must carry optional retryAfter (bible 5.8j)"; assert "resolveFailoverReasonFromError" in ch, "chat.ts error emit must populate reason via resolveFailoverReasonFromError (bible 5.8j)"'
+ - name: §5.8k UI→thetinkerzone "learn more" links — ZONE_DOCS map + zoneDoc() chips on right-rail headers + fractal-dock chip + CSS (2026-06-24)
+ cmd: python3 -c 'import os; a=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); fd=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/fractal-dock.ts")).read(); css=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/styles/base.css")).read(); assert "ZONE_DOCS" in a and "function zoneDoc" in a, "ZONE_DOCS map / zoneDoc() helper missing in app.ts (bible 5.8k)"; assert "thetinkerzone.com/?p=" in a, "zoneDoc must link by stable ?p=ID, not slug (bible 5.8k)"; assert all(s in a for s in ("zoneDoc(\"slider\")","zoneDoc(\"eeg\")","zoneDoc(\"recipes\")","zoneDoc(\"prefrontal\")","zoneDoc(\"amygdala\")")), "right-rail header chips (slider/eeg/recipes/prefrontal/amygdala) not all wired in app.ts (bible 5.8k)"; assert "thetinkerzone.com/?p=198" in fd and "rpanel-doc-link" in fd, "fractal-dock FRACTAL(198) chip missing (bible 5.8k)"; assert ".rpanel-doc-link" in css, ".rpanel-doc-link chip CSS missing in base.css (bible 5.8k)"'
+ - name: §5.8L subagent→tab attribution — flat subagent keys routed to the spawning tab via parentRunId, no cross-tab bleed (2026-06-25)
+ cmd: python3 -c 'import os; a=open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "subagentOwnerTab" in a and "function recordSubagentOwner" in a, "subagentOwnerTab map / recordSubagentOwner missing in app.ts (bible 5.8L)"; assert "recordSubagentOwner(" in a, "recordSubagentOwner not called at the agent-event handler (bible 5.8L)"; assert "attachedTabCountForRoot" in a, "attachedTabCountForRoot single-tab fallback guard missing (bible 5.8L)"; import re; m=re.search(r"function chatEventIsSubagentOfView[\\s\\S]{0,2000}?\\n}", a); assert m, "chatEventIsSubagentOfView not found"; body=m.group(0); assert "subagentOwnerTab.get(evtKey)" in body, "chatEventIsSubagentOfView must consult subagentOwnerTab for owner attribution (bible 5.8L)"; assert "attachedTabCountForRoot" in body, "chatEventIsSubagentOfView must guard the loose fallback by attachedTabCountForRoot (bible 5.8L)"'
---
# Tinker UI — layout, visual language, feature registry
@@ -320,20 +334,20 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
- **Expanded detail view:** Shows actual command/diff with del/ins formatting (red strikethrough old, green new)
- **Status icons:** `⋯` (pending), `✓` (ok), `✗` (error)
- **Default state — collapsed (2026-04-27, Story Mode deleted same day):** Tool rows render single-line by default; click expands. Story Mode (the 🎬 topbar global "auto-expand every tool" override) was removed entirely — collapsed-by-default with per-tool click-to-expand is the only contract. The earlier attempt to default Story Mode to off and treat clicks as an exit gesture worked, but the toggle still added no behaviour worth keeping and confused the click-to-collapse contract. Stale `tinker-story-mode` localStorage keys from previous installs are harmless — nothing reads them anymore. Render gate is now plain `expandedTools.has(tid)`.
-- **Collapsed-summary contract (grandma-proof bar, 2026-04-27):** the single-line title shown in the collapsed row is the LAST sentence of the LLM's pre-tool narration (`renderMsg` extracts it via `/[^.!?\n]+[.!?]?\s*$/` and clamps to 160 chars). That sentence MUST be specific enough that someone non-technical, reading the chat top-to-bottom with the original prompt as context but no expanded views, can follow what each step is doing and why this step instead of any other. **Banned phrasings** (the cc-bridge narration system-prompt block enumerates these explicitly so the LLM stops emitting them):
+- **Collapsed-summary contract (grandma-proof bar, 2026-04-27):** the single-line title shown in the collapsed row is the LAST sentence of the LLM's pre-tool narration (`renderMsg` extracts it via `/[^.!?\n]+[.!?]?\s*$/` and clamps to 160 chars). That sentence MUST be specific enough that someone non-technical, reading the chat top-to-bottom with the original prompt as context but no expanded views, can follow what each step is doing and why this step instead of any other. **Banned phrasings** (the tinker-bridge narration system-prompt block enumerates these explicitly so the LLM stops emitting them):
- _performing an action_, _running a command_, _executing a tool_ — strips the step of meaning.
- _reading a section of the code to understand how it works_ — which section? understand what about it? Must name file/symbol + the specific question.
- _checking something_, _looking around_, _gathering context_, _exploring the codebase_ — vague exploration; must name the artifact + hypothesis being tested.
- _making changes_, _applying a fix_, _updating the file_ — which file, what change in user-facing terms.
- _as requested_, _per the request_, _as the user asked_ — empty filler; restate WHAT from the prompt this call serves.
- Bare verbs without an object: _searching_, _editing_, _running_, _verifying_.
- Every collapsed line names (a) the artifact (real path, symbol, or string), (b) the question or move it serves, and (c) advances the story relative to the user's prompt. Together, the chain of titles + the prompt should read like a narrative. The cc-bridge narration block (`extensions/tinkerclaw-cc-bridge/src/worker.ts:buildChatNarrationBlock`) carries the contract + side-by-side bad→good rewrites; if Jarvis starts emitting any banned phrase, that block is the place to tighten further.
+ Every collapsed line names (a) the artifact (real path, symbol, or string), (b) the question or move it serves, and (c) advances the story relative to the user's prompt. Together, the chain of titles + the prompt should read like a narrative. The tinker-bridge narration block (`extensions/tinkerclaw-tinker-bridge/src/worker.ts:buildChatNarrationBlock`) carries the contract + side-by-side bad→good rewrites; if Jarvis starts emitting any banned phrase, that block is the place to tighten further.
- **Enforcement layers (best-effort, layered defence):**
1. **System prompt** (`buildChatNarrationBlock`) — leads with a HARD RULE plus the anti-pattern catalog. Hoisted to position 2 in the combined prompt (right after persona, ahead of the dense subagent-helper text) so the rule registers before the heavier rules.
2. **User-message directive** — appended to every user turn in `stream.ts` (after `extractUserText`). This is the most-attended slot in claude-cli's print-mode ranking, and reliably gets the FIRST tool call narrated even when the system prompt didn't.
3. **Mechanical fallback** — when the LLM emits a tool with empty narration anyway (claude-cli's `-p` mode often runs back-to-back tools after one preamble sentence), `renderMsg` falls back to `toolSummary(name, args)`. That summary is by-design artifact-aware (`Bash: `, `Read: `, `Grep: `) — not grandma-prose, but at least concrete. **This fallback is the floor, not the goal**; if you're seeing a lot of mechanical lines instead of narration, the issue is layer 1 or 2 not the renderer.
- **Known limitation:** claude-cli's `-p` print mode resists per-tool narration in dense tool chains. Layers 1 + 2 reliably win the FIRST tool of a turn but the model often runs subsequent tools silently. Open improvement: server-side synthesis of a title per tool from `(userPrompt, previousNarration, toolName, args)` when narration is empty; not yet implemented because each synthesis would be a small LLM call per tool (latency + cost).
-- **Files:** `app.ts` (`toolSummary`, `toolExpandedDetail`, `renderMsg` tool branches, `storyMode` initializer, click handler at the `[data-tid]` delegate); `extensions/tinkerclaw-cc-bridge/src/worker.ts` (`buildChatNarrationBlock` enforces the grandma-proof bar in the LLM's system prompt).
+- **Files:** `app.ts` (`toolSummary`, `toolExpandedDetail`, `renderMsg` tool branches, `storyMode` initializer, click handler at the `[data-tid]` delegate); `extensions/tinkerclaw-tinker-bridge/src/worker.ts` (`buildChatNarrationBlock` enforces the grandma-proof bar in the LLM's system prompt).
### 5.7 Thinking Indicator (Animated)
@@ -368,17 +382,18 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
### 5.8 Thinking Bubble Interlacing
-- **Status:** `CONFIRMED` (2026-03-09), **REWORKED** (2026-03-20), **FIXED** (2026-03-23, 2026-03-26)
+- **Status:** `CONFIRMED` (2026-03-09), **REWORKED** (2026-03-20), **FIXED** (2026-03-23, 2026-03-26), **MARKER-FREE STRUCTURAL REWORK** (2026-06-19, Bug A)
- **Deployed:** 2026-03-03 (commit `98f72f4c1`), rewritten 2026-03-08 (commit `b4da1e0d5`), fixed 2026-03-09 (commit `f211e5015`), reworked 2026-03-20 (commits `ceb73596b` + `1792fdaf6` + `49d28965f` + `0b71592c1`), **fixed 2026-03-23** (restored intermediate text classification), **fixed 2026-03-26** (segment preservation + thinking flicker)
-- **What:** Native `type: "thinking"` blocks render as thinking bubbles. Intermediate text messages (model preamble/commentary before tool calls) are classified as thinking and collapse into the reasoning group. Only the last text message in a finalized run is the visible answer.
+- **What:** Native `type: "thinking"` blocks render as thinking bubbles. Between-tool NARRATION text collapses into the reasoning group; the ANSWER is the text AFTER the last tool call — which may be MULTIPLE bubbles — and renders visibly. **(2026-06-19 marker-free structural rework, Bug A — SUPERSEDES the old "only the last text message in a finalized run is the visible answer", which hid real answer content whenever the model dropped the `💬 ANSWER` marker. See the marker-free bullet below + `bug-log.md` FIXED A.)**
- **Architecture (2026-03-23 fix — hybrid classification):**
- **Core principle:** Native `thinking` blocks always get thinking styling. Intermediate text messages (all except the last in a run) are also classified as thinking — they're the model's reasoning process, not the final answer.
- **Implicit state transitions:** `delta` handler resets `thinkingMsgIdx` to -1 (guards against dropped `thinking_end`). `thinking_delta` handler resets `streamMsgIdx` to -1 (freezes text segment when thinking starts).
- **During streaming:** `frozenTextEnd` splits text at tool-call boundaries into separate temps. Frozen text messages (not the active stream at `streamMsgIdx`) are classified as thinking. The active stream renders as normal assistant text.
- **On finalization (2026-03-26 fix):** Segmented temp text bubbles are preserved (promoted as-is). Only the last text segment (after last tool call) is updated with the server's authoritative text to catch throttled tokens. Previous behavior concatenated ALL text segments into one blob, destroying the thinking/answer separation.
- - **thinkingSet (2026-03-26 fix):** Messages with exclusively `thinking` blocks (no text) are always added. All text messages except the last are added regardless of streaming state. Previous `isCurrentRun` check emptied the thinking set during streaming, causing bubbles to flicker between thinking and normal style on every delta/tool event cycle.
+ - **thinkingSet (2026-03-26; STRUCTURAL rework 2026-06-19):** Messages with exclusively `thinking` blocks (no text) are always added. For assistant TEXT bubbles the rule is now STRUCTURAL, not positional: a text bubble is collapsed as narration **iff a tool call/result occurs LATER in the same run** — the pure unit-tested `narrationIndices()` in `reply-grouping.ts`, which REPLACED the position-only `slice(0, -1)` (that hid real answer content). Text with no tool after it is the answer and stays visible; with NO tools in the run, nothing collapses (genuine chain-of-thought already lives in the separate thinking channel). The `isCurrentRun` guard stays removed (2026-03-26) — do not re-add (thinking flicker).
- **Thinking flicker bug (2026-03-26):** `isCurrentRun` guard (`streamMsgIdx >= 0 → intermediates = []`) caused all bubbles to flash yellow on every delta, then restore thinking style on every tool start. Removed — `slice(0,-1)` already excludes the live bubble correctly.
- - **Reasoning group:** Contains thinking blocks + tool calls + intermediate text messages. Only the last text message renders as the final answer outside the group.
+ - **Reasoning group:** Contains thinking blocks + tool calls + between-tool narration text. The ANSWER bubble(s) — every assistant text bubble AFTER the last tool — render visibly OUTSIDE the group (the render pass collects `answerIndices[]`, not a single `finalIdx`).
+ - **Marker-free contract (2026-06-19, Bug A — supersedes the `💬 ANSWER` dependency):** the UI no longer needs the model to emit a literal `💬 ANSWER` to separate narration from the answer — grouping is purely structural (above). The `💬 ANSWER` per-turn injection in `buildInjectedPrompt` is RETIRED (kept `🌿 FRACTAL`, which is also system-prompt-mandated and double-reinforced); `reconstructInjectionFields` still recognises the old framing for historical messages; the splitter stays tolerant of a stray marker but never requires it. Root cause: the marker was injected only transiently/toggle-gated while `🌿 FRACTAL` was always-mandated → the model dropped `💬 ANSWER` and the positional collapse hid the answer. Files: `reply-grouping.ts` (`narrationIndices`), `app.ts` (run-grouping + injection + reconstruction), `sectioned-reply.ts`. See `bug-log.md` FIXED A.
- `isRunBoundary()` skips `role: "user"` messages that only contain `tool_result` blocks — keeps the entire response as one run for proper grouping.
- **Reset points (7):** ws.onclose, final/error/abort, tool_start (frozenTextEnd only), loadChat, retryProvider, abort(), new-session
- **Removed (2026-03-20):** `findSentenceEnd()`, `mergeSentenceContinuations()`, `[final-debug]` console.warn calls
@@ -434,26 +449,26 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
### 5.8d Inter-tool narration / answer separation
- **Status:** `DEPLOYED` (2026-06-10)
-- **What — the failure mode:** With the cc-bridge brain (claude-cli `-p` print mode), Claude Code emits its between-step narration — "let me check X", "I'll read the config", "now let me verify Y" — as **visible assistant text**, not as native `thinking` blocks. Confirmed by streaming inspection: this narration arrives as `text_delta` at a **single `contentIndex` across the whole turn**, so it is NOT split into a separate intermediate text message the way §5.8/§5.8c rely on. Because the narration and the final answer share one content block, the thinking/answer separation machinery (§5.8c `thinkingSet`, §5.8b reasoning-group) cannot peel them apart — the narration **fuses into the same bubble as the final answer** and surfaces as a run of "let me…" sentences sitting at the **top of the answer bubble**. This is distinct from §5.8c (which handles native `thinking` blocks and per-tool intermediate text temps) and from §5.8 (cross-tool text segmentation); here there is only one block and no segmentation hook to grab.
+- **What — the failure mode:** With the tinker-bridge brain (claude-cli `-p` print mode), Claude Code emits its between-step narration — "let me check X", "I'll read the config", "now let me verify Y" — as **visible assistant text**, not as native `thinking` blocks. Confirmed by streaming inspection: this narration arrives as `text_delta` at a **single `contentIndex` across the whole turn**, so it is NOT split into a separate intermediate text message the way §5.8/§5.8c rely on. Because the narration and the final answer share one content block, the thinking/answer separation machinery (§5.8c `thinkingSet`, §5.8b reasoning-group) cannot peel them apart — the narration **fuses into the same bubble as the final answer** and surfaces as a run of "let me…" sentences sitting at the **top of the answer bubble**. This is distinct from §5.8c (which handles native `thinking` blocks and per-tool intermediate text temps) and from §5.8 (cross-tool text segmentation); here there is only one block and no segmentation hook to grab.
- **The fix — content-local leading-narration peel:** A pure, content-local helper `splitLeadingNarration` in `tinker-ui/src/sectioned-reply.ts` inspects only the answer's own text (no run/streaming state) and peels a **leading run of narration-shaped sentences** off the front. The peeled narration is moved into a **collapsed `Commentary` block** — CSS class `narration-details`, reusing the existing reasoning-group look (§5.8b) — rendered **next to the reasoning group**, so the narration is **relocated, never dropped**, and the answer bubble is **never blanked** (if the peel would empty the answer, nothing is peeled). This runs at **final-answer render ONLY** (`!isThinking`) — never on a live/streaming bubble — in both `renderSectionedReply` and the **two plain-path `app.ts` render sites** that bypass the sectioned renderer.
-- **Heuristic (deliberately conservative):** Only a _leading_ run of narration sentences is eligible, matching the "let me <action-verb>…" / "I'll <verb>…" shape, and the peel only fires when there is a **trailing non-narration sentence** to keep as the real answer. No trailing answer ⇒ no peel (the text is treated as the answer as-is). This keeps genuine answers that happen to start with "Let me explain…" from being eaten while still catching the cc-bridge "let me check / I'll read" preamble.
-- **Relationship to §5.8 / §5.8c:** §5.8c separates native `thinking` from `text`; §5.8 segments text across tool-call boundaries. §5.8d is the **single-block fallback** for the cc-bridge case where Claude Code's narration is plain `text` at one `contentIndex` and neither prior mechanism gets a seam to cut on — so a content-local heuristic peels it at render time.
+- **Heuristic (deliberately conservative):** Only a _leading_ run of narration sentences is eligible, matching the "let me <action-verb>…" / "I'll <verb>…" shape, and the peel only fires when there is a **trailing non-narration sentence** to keep as the real answer. No trailing answer ⇒ no peel (the text is treated as the answer as-is). This keeps genuine answers that happen to start with "Let me explain…" from being eaten while still catching the tinker-bridge "let me check / I'll read" preamble.
+- **Relationship to §5.8 / §5.8c:** §5.8c separates native `thinking` from `text`; §5.8 segments text across tool-call boundaries. §5.8d is the **single-block fallback** for the tinker-bridge case where Claude Code's narration is plain `text` at one `contentIndex` and neither prior mechanism gets a seam to cut on — so a content-local heuristic peels it at render time.
- **CSS:** `.narration-details` (collapsed Commentary block; inherits the reasoning-group visual language — see §5.8b `.reasoning-group`/`.reasoning-header`)
- **Files:** `tinker-ui/src/sectioned-reply.ts` (`splitLeadingNarration`, `Commentary` block, `narration-details`), `app.ts` (two plain-path render sites call `splitLeadingNarration`), `base.css` (`.narration-details`)
### 5.8e Native extended-thinking stream consumer (`stream:"thinking"`)
- **Status:** `DEPLOYED` (2026-06-11)
-- **What — the capability:** A NEW consumer on the `event:"agent"` channel that renders the model's **native extended-thinking** stream. It handles `p?.stream === "thinking"` frames — fed by the cc-bridge `pushThinkingDelta` and by the embedded path's `emitReasoningStream` (both gated on `reasoningMode === "stream"`, see §5.8c layers 1–2) — and accumulates them into a **per-`runId` `_isReasoning` TEXT bubble**. This is the model _thinking out loud_ (extended reasoning), surfaced live, and is **DISTINCT from the §5.8/§5.8c positional narration-text `thinkingSet`**: the `thinkingSet` machinery classifies ordinary assistant-`text` messages by their _position_ in a run (all-but-last = intermediate); an `_isReasoning` bubble is **explicitly tagged at ingest** from the reasoning stream and never participates in that positional logic.
+- **What — the capability:** A NEW consumer on the `event:"agent"` channel that renders the model's **native extended-thinking** stream. It handles `p?.stream === "thinking"` frames — fed by the tinker-bridge `pushThinkingDelta` and by the embedded path's `emitReasoningStream` (both gated on `reasoningMode === "stream"`, see §5.8c layers 1–2) — and accumulates them into a **per-`runId` `_isReasoning` TEXT bubble**. This is the model _thinking out loud_ (extended reasoning), surfaced live, and is **DISTINCT from the §5.8/§5.8c positional narration-text `thinkingSet`**: the `thinkingSet` machinery classifies ordinary assistant-`text` messages by their _position_ in a run (all-but-last = intermediate); an `_isReasoning` bubble is **explicitly tagged at ingest** from the reasoning stream and never participates in that positional logic.
- **DON'T-REGRESS invariant (the whole point of §5.8e):** `_isReasoning` messages MUST stay **out of the positional classifier**. There is an explicit **skip-guard in the classifier loop at `app.ts` (~line 6342)** that excludes any `_isReasoning` message before the all-but-last / `slice(0,-1)` reasoning-grouping math runs. They are rendered instead by a **dedicated `renderMsg` branch** that emits **`.msg-thinking` text directly** — **NOT** as a `type:"thinking"` _content block_ (renderMsg's per-block loop has **no thinking arm**; routing a reasoning bubble through a block would render nothing). Because `_isReasoning` bubbles are removed from the positional set, they **cannot perturb `slice(0,-1)`** (they will never be mistaken for "the last text = the answer", nor shift which real text message is) and **cannot reintroduce the `isCurrentRun` flicker** retired in §5.8/§5.8c (they are not part of the streaming-text temp set that the flicker math touched).
- **`_turnIncomplete` ⚠ badge (dual hook):** When a turn ends without a clean completion, the bubble carries a `_turnIncomplete` flag rendered as a `msg-incomplete-badge` ⚠ element. It is set via **two independent hooks** because the two brains signal incompleteness differently:
1. **Model-gated end branch** — inspects `livenessState` / `stopReason` on the lifecycle end path (the embedded/native model channel).
- 2. **cc-bridge sibling handler** — a `phase: "turn-incomplete"` event handler (cc-bridge does not flow the model `stopReason`, so it emits an explicit phase instead).
- Either hook independently raises the badge, so an incomplete cc-bridge turn and an incomplete native turn both get the ⚠.
+ 2. **tinker-bridge sibling handler** — a `phase: "turn-incomplete"` event handler (tinker-bridge does not flow the model `stopReason`, so it emits an explicit phase instead).
+ Either hook independently raises the badge, so an incomplete tinker-bridge turn and an incomplete native turn both get the ⚠.
- **Generic unknown-stream fallback:** A `KNOWN_STREAMS` **denylist** guards the `event:"agent"` dispatch tail: any `p.stream` not in the known set (`tool`, `lifecycle`, `thinking`, …) falls through to `renderSystemMsg` so a future/unrecognized stream is **surfaced rather than silently dropped**. To avoid flooding the chat with transient frames, this fallback is **throttled to `end`/`error` phases only** (mid-stream deltas of an unknown stream are ignored; only its terminal frame renders as a system message).
-- **Relationship to §5.8 / §5.8c / §5.8d:** §5.8c routes native `type:"thinking"` _content blocks_ into per-tool intermediate temps; §5.8 segments assistant `text` across tool boundaries; §5.8d peels leading narration off a single-block cc-bridge answer. §5.8e is the **live extended-reasoning channel**: it consumes the `stream:"thinking"` agent events into a self-tagged `_isReasoning` bubble that is deliberately _fenced off_ from all three positional mechanisms, so adding live reasoning display can never regress the thinking/answer separation those sections established.
+- **Relationship to §5.8 / §5.8c / §5.8d:** §5.8c routes native `type:"thinking"` _content blocks_ into per-tool intermediate temps; §5.8 segments assistant `text` across tool boundaries; §5.8d peels leading narration off a single-block tinker-bridge answer. §5.8e is the **live extended-reasoning channel**: it consumes the `stream:"thinking"` agent events into a self-tagged `_isReasoning` bubble that is deliberately _fenced off_ from all three positional mechanisms, so adding live reasoning display can never regress the thinking/answer separation those sections established.
- **CSS:** `.msg.msg-thinking` (reused from §5.8 — earth-thinking texture, 12px, `#d4c4a8`), `.msg-incomplete-badge` (⚠ turn-incomplete badge)
-- **Files:** `app.ts` (`stream==="thinking"` handler, `_isReasoning` accumulation + classifier skip-guard at ~6342 + dedicated `.msg-thinking` `renderMsg` branch, `_turnIncomplete` dual hook, `KNOWN_STREAMS` fallback), `extensions/tinkerclaw-cc-bridge/src/worker.ts` (`pushThinkingDelta`, `phase:"turn-incomplete"`), `embedded-agent-subscribe.ts` (`emitReasoningStream`, `reasoningMode=="stream"` gate), `base.css` (`.msg-incomplete-badge`)
+- **Files:** `app.ts` (`stream==="thinking"` handler, `_isReasoning` accumulation + classifier skip-guard at ~6342 + dedicated `.msg-thinking` `renderMsg` branch, `_turnIncomplete` dual hook, `KNOWN_STREAMS` fallback), `extensions/tinkerclaw-tinker-bridge/src/worker.ts` (`pushThinkingDelta`, `phase:"turn-incomplete"`), `embedded-agent-subscribe.ts` (`emitReasoningStream`, `reasoningMode=="stream"` gate), `base.css` (`.msg-incomplete-badge`)
### 5.8f Per-tab thinking slider (in the Models side panel)
@@ -465,7 +480,7 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
- **Per-tab semantics / follows the active tab:** the slider's stop reflects the **active tab's session `thinkingLevel`** (read by `renderThinkingSlider()` from the viewed session), and the Models panel **re-renders on tab switch via `refreshViewedSessionIndicators()`** (which triggers `updateBudgetPanel()`), so switching tabs re-paints the fallback-chain card and re-reads the newly-active tab's level into the slider position. The slider's DOM anchor stays the **primary model row** (it does not chase the active tab's model row); only its value follows the active tab. Switching the Models panel's Session/All scope does not change which session the slider targets — it always targets the **active tab**.
- **DON'T-REGRESS invariants** (each is load-bearing — the slider was relocated _into_ machinery that punishes the obvious shortcut):
1. **It saves via `sessions.update { thinkingLevel }` ONLY.** The save call patches **only** `thinkingLevel` (mirroring the per-row select at `app.ts` ~line 12569: `req("sessions.update", { key, patch: { thinkingLevel: value || null } })`). Any **other** session field sent from a webchat client hits the **`rejectWebchatSessionMutation`** guard (`app.ts` ~line 3899) and the whole patch is rejected — so the slider never bundles model or any other field into the same `sessions.update`.
- 2. **Moving the slider applies on the NEXT message, not the in-flight turn.** The thinking budget is read **at cc-bridge worker spawn time** (see `tool-loop.md`), so changing the stop mid-turn cannot retro-budget the running turn — it takes effect on the next user message. This is intended; surface it as expected behavior, not a bug.
+ 2. **Moving the slider applies on the NEXT message, not the in-flight turn.** The thinking budget is read **at tinker-bridge worker spawn time** (see `tool-loop.md`), so changing the stop mid-turn cannot retro-budget the running turn — it takes effect on the next user message. This is intended; surface it as expected behavior, not a bug.
3. **The slider is appended exactly once, after the PRIMARY row, by the fallback-chain loop.** `updateBudgetPanel()` appends `renderThinkingSlider()` to the FALLBACK CHAIN card's HTML only when **`i === 0`** (the primary/#1 row), so it renders under exactly one row regardless of run state. Don't gate the placement on the active session's `.model` (it is empty until a run reports it — that was the removed `data-model-id` querySelector bug), and don't append it inside the per-row helper or the configured-models loop (it would render under every model / under the wrong row).
- **Cross-references:** the budget mechanism the slider drives (where/when `thinkingLevel` is consumed at worker spawn) lives in **`tool-loop.md`**; the reasoning bubble the budget _lights up_ (the live extended-thinking `_isReasoning` stream) is **§5.8e**. The Models panel render path the slider hooks into (`updateBudgetPanel()` / `renderModelRow()`) is **§5.13a**. The per-session-row equivalent control (the `` in the Sessions alt-view) is §5.13.
- **Files:** `app.ts` (`updateBudgetPanel()` fallback-chain loop appends `renderThinkingSlider()` after the PRIMARY row at `i === 0`; `renderThinkingSlider()` builds the `model-think-slider` markup from the viewed session's `thinkingLevel`; `model-think-slider` change handler; the per-tab `sessions.update { thinkingLevel }` save; `refreshViewedSessionIndicators()` → `updateBudgetPanel()` re-render on tab switch), `base.css` (slider styling)
@@ -473,42 +488,44 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
### 5.8g Call-tree effort chip + the `?pfdebug=1` truth grid (FORK 2026-06-11)
- **Status:** `DEPLOYED` (2026-06-11)
-- **What — the capability:** the slider in §5.8f sets the **requested** thinking budget; this chip shows what the model **actually did** with it. Each call-tree row (the prefrontal panel — root + each subagent) carries an **effort chip** fed by the cc-bridge **`stream:"effort"`** agent-event (computed server-side in `stream.ts`; full field contract + Auto semantics + honest limits live in **`tool-loop.md` → "Per-session effort visibility"**). The chip surfaces, per run: the **requested cap** (`configuredBudget`), the **actual effort** as **thinking CHARACTERS** (`thinkingChars`, animating live off the throttled `phase:"live"` emits and finalising on `phase:"final"`), and a thought/didn't-think state from `hadRealThinking`. Because the effort event is emitted per-subagent automatically (`attempt.ts` re-wraps the streamFn per attempt), each child row gets its own chip.
+- **What — the capability:** the slider in §5.8f sets the **requested** thinking budget; this chip shows what the model **actually did** with it. Each call-tree row (the prefrontal panel — root + each subagent) carries an **effort chip** fed by the tinker-bridge **`stream:"effort"`** agent-event (computed server-side in `stream.ts`; full field contract + Auto semantics + honest limits live in **`tool-loop.md` → "Per-session effort visibility"**). The chip surfaces, per run: the **requested cap** (`configuredBudget`), the **actual effort** as **thinking CHARACTERS** (`thinkingChars`, animating live off the throttled `phase:"live"` emits and finalising on `phase:"final"`), and a thought/didn't-think state from `hadRealThinking`. Because the effort event is emitted per-subagent automatically (`attempt.ts` re-wraps the streamFn per attempt), each child row gets its own chip.
- **The `?pfdebug=1` truth grid:** the existing prefrontal debug channel (open the page with **`?pfdebug=1`** or `__pf.enable()` in devtools — see §5.12 / `app.ts` `PF_DEBUG_STATE`) gains a **truth grid**: a per-run table laying the raw effort fields side by side (`thinkLevel`, `configuredBudget`, `thinkingChars`, `hadRealThinking`, `redacted`, `output_tokens`, `num_turns`) so you can confirm the chip is reading the server numbers verbatim, not interpolating. It is a diagnostic surface, gated on `PF_DEBUG_STATE.debug`; it never renders for normal users.
- **HONEST-LABELS rule (load-bearing — the chip must never fabricate a reasoning-token count):**
- **requested** = the **budget cap** (`configuredBudget` / `MAX_THINKING_TOKENS`). Label it as the cap, not as "effort."
- **actual** = thinking **CHARACTERS** (`thinkingChars`) + the `hadRealThinking` boolean. This is the only honest "how much did it think" number — **there is NO provider reasoning-token count** (`CcUsage` has none; `output_tokens` mixes thinking + answer). Never show a fabricated "reasoning tokens: N."
- **Auto** = **uncapped** (`thinkLevel === ""` → `MAX_THINKING_TOKENS` omitted → the model chose its own budget). Render Auto as "Auto / uncapped," never as a tier and never as `0`. A live Auto turn really produced thousands of chars of thinking, so Auto with a large `thinkingChars` is expected, not anomalous.
- **redacted** = present-but-size-hidden (`redacted` true): show "thought (size hidden)", do not show `0` chars as if the model didn't think.
- - **no chip on non-claude providers** — they don't route through cc-bridge so emit no `effort` event; the chip's absence there is correct.
+ - **no chip on non-claude providers** — they don't route through tinker-bridge so emit no `effort` event; the chip's absence there is correct.
- **Cross-references:** the server-side `effort` event contract, the warm-worker `thinkLevel` LAG fix, the `think-level-pending` phase, and the Auto/honest-limits rules all live in **`tool-loop.md`**. The slider that sets the requested level is §5.8f; the live reasoning bubble the budget lights up is §5.8e; the call-tree panel the chip rides on is §5.12.
-- **Files:** `app.ts` (the `event:agent` `stream === "effort"` handler that stores the latest effort fields per run; the chip render in the prefrontal call-tree path; the `?pfdebug=1` truth-grid render gated on `PF_DEBUG_STATE.debug`), `base.css` (chip styling). The producer side (where `thinkingChars` / `hadRealThinking` / `configuredBudget` are computed) is `extensions/tinkerclaw-cc-bridge/src/stream.ts` — see `tool-loop.md`.
+- **Files:** `app.ts` (the `event:agent` `stream === "effort"` handler that stores the latest effort fields per run; the chip render in the prefrontal call-tree path; the `?pfdebug=1` truth-grid render gated on `PF_DEBUG_STATE.debug`), `base.css` (chip styling). The producer side (where `thinkingChars` / `hadRealThinking` / `configuredBudget` are computed) is `extensions/tinkerclaw-tinker-bridge/src/stream.ts` — see `tool-loop.md`.
### 5.8h EEG panel — seismograph trace of live model × effort choices (FORK 2026-06-13)
-- **Status:** `DEPLOYED-PARTIAL` (2026-06-13) — built same night via ORCA (`b9f9ef844a`), then a follow-up fix after Oscar reported the sliders showed no per-stop labels and the seismograph was blank. Approved via visual questionnaire (10/10, `.superpowers/brainstorm/113276-1781306690/state/server.log`); supersedes the FALLBACK CHAIN + CONFIGURED split of §5.13a and extends §5.8f/§5.8g.
+- **Status:** `DEPLOYED-PARTIAL` (2026-06-13) — built same night via ORCA (`b9f9ef844a`), then a follow-up fix after the owner reported the sliders showed no per-stop labels and the seismograph was blank. Approved via visual questionnaire (10/10, `.superpowers/brainstorm/113276-1781306690/state/server.log`); supersedes the FALLBACK CHAIN + CONFIGURED split of §5.13a and extends §5.8f/§5.8g.
- **LIVE now:** unified MODELS list (chain badges ①②③, fable-5 visible); EFFORT + MODEL force sliders each with ALL stops printed as tick labels positioned via `eegStopLeftCss` (shares the `eegStopX` pad model → aligned with the seismograph columns, invariant 2); empty paper draws the labeled axis + "waiting for model activity…" hint (only TRACE strokes obey no-placeholders, never the axis); live **main-session** trace (color=provider, width=ESTIMATED cost, dashed=forced, halo=measured thinkingChars); turn markers → chat focus; anatomy backfill on load. Verified: `vite build` green; renderer behaviour checked directly (empty→axis+hint+short labels, populated→trace+marker+anthropic stroke).
-- **KNOWN v2 GAPS (honest):** (1) **subagent split/join branches receive no data** — `sessionKeyMatches` (app.ts:1013) does NOT admit `:subagent:` descendants, so subagent effort events are dropped before the EEG feed; the branch + ×N stacking renderer EXISTS and is unit-tested but stays unfed until the effort consumer admits subagent events into the viewed store. (2) tick labels align to the seismograph COLUMNS, not the native range thumb (approximate at the extremes). (3) the SVG renders at fixed 280px — alignment with the full-width slider degrades on a much wider/narrower panel.
+- **KNOWN GAPS (honest):** (1) ✅ RESOLVED 2026-06-14 — subagent split/join branches are fed (`sessionKeyMatches` admits `:subagent:` descendants, commits `cfbd6b4953` + `0668ac93e7`); the depth-shaded stack (§5.84) renders them. (2) tick labels align to the seismograph COLUMNS, not the native range thumb (approximate at the extremes). (3) the SVG renders at fixed width — alignment degrades on a much wider/narrower panel.
- **What — the capability:** the Models side panel's FALLBACK CHAIN card is renamed **EEG** (joins amygdala/prefrontal/broca) and gains a **seismograph trace**: a vertical, scrollable "paper" (newest at top, paper scrolls down) drawing one continuous line per session showing, per turn, WHICH model and WHAT thinking effort the system chose. Purpose: live proof the real-time model/effort routing works + a demo surface for cloners.
-- **Axes & geometry:** X axis = the SAME 8 stops as the §5.8f slider (Auto · Minimal · Low · Medium · Adaptive · High · xHigh · Max), each stop a labeled vertical gridline; the trace must align pixel-exact with the effort slider's markers (one shared geometry helper, not two hardcodings). Effort changes draw **continuous cubic-bezier arcs** between stop columns (PyCharm git-graph style), never jumps.
+- **Axes & geometry:** X axis = the SAME 8 stops as the §5.8f slider (Auto · Minimal · Low · Medium · Adaptive · High · xHigh · Max), each stop a labeled vertical gridline; the trace must align pixel-exact with the effort slider's markers (one shared geometry helper, not two hardcodings). **EVERY LLM call is its OWN discrete vertical segment at its effort column — NOT a continuous spline (the owner 2026-06-20, SUPERSEDES the original "continuous cubic-bezier arcs between columns, never jumps").** The original flowing git-graph connector between calls read as one unbroken line ("I don't see a clear separation between calls"); now each call (each runId / main sample) draws a fresh vertical run inset by a small `EEG_CALL_GAP` at each end so consecutive calls visibly finish + restart, and a PROMPT boundary uses the bigger `EEG_TURN_GAP` (hierarchy: call gap < prompt gap). Effort is read by each segment's column x; the connector beziers are GONE. Bonus robustness: because every call breaks, the separation no longer depends on `turnEnds` being recorded — a missing turn-end no longer yields a continuous spline.
- **Line encoding:**
- - **Color = provider brand** (full palette, Oscar's q6): anthropic `#E8702A`, openai `#10A37F`, google = rainbow `linearGradient` (#4285F4→#EA4335→#FBBC05→#34A853 cycling along the trace), deepseek `#4D6BFE`, mistral `#FA520F`, meta `#0668E1`, xai/local/unknown = neutral gray `#8A8F98`. **The colour comes from the ACTUAL model** even in Auto: the cc-bridge `stream:"effort"` event self-describes `model: model.id` (added 2026-06-13 — before that the effort event carried no model, so the EEG fell back to `providerOf("")`→gray, which read as "white lines"); the app.ts effort consumer sets `r.model`/`r.provider` from it. **Needs a gateway rebuild** to deploy (cc-bridge is bundled).
- - **Auto effort column = MEASURED** (Oscar 2026-06-13): in Auto/off (no pinned level) the line is drawn at the column of the effort the model ACTUALLY used — `eegEffectiveLevel(s)` = `thinkingCharsLevel(thinkingChars)` when the level is unset — instead of parking it in the "Auto" gutter. A pinned level always wins. (In Auto the line and the measured halo coincide, which is correct — the measured IS the chosen.)
- - **Fixed height** (Oscar 2026-06-13): `.eeg-paper` is a FIXED 260px box (≈ amygdala `.rpanel` height) that scrolls internally — not a region that grows/shrinks with content.
- - **Thickness = Oscar's REAL per-use cost (€/Mtok output)** (NOT API sticker, NOT static smartness): `width = clamp(1.5, 1.5·log2(1+cost·effortMult), 7)px`. `EEG_COST_TABLE` relCost values ARE effective €/Mtok under Oscar's actual billing (2026-06-13), because the providers bill differently: **Anthropic = a flat €200/mo Max subscription** (at an assumed **75% weekly-quota utilization** → ≈ €2 per sonnet-equiv Mtok, then × quota-burn weight: haiku 0.3/sonnet 1/opus 5/fable 10 → **haiku 0.6, sonnet 2, opus 10, fable 20**); **OpenAI/Google = metered API at full price** (**gpt-5.x ≈ 12, gemini-pro ≈ 12, gemini-flash ≈ 2.5, mini ≈ 3**). Net: a frontier API model (gpt-5.5/gemini-pro €12) costs MORE per use than subscription Opus (€10) and far more than subscription Sonnet (€2) — the subscription-vs-API asymmetry is the point. The 75%-quota → €2/Mtok anchor is the big assumption; ESTIMATES until the measured halo corrects them (never present as measured — invariant 3). Mapping is **LINEAR** (Oscar 2026-06-13): `width = relCost / 2` → **Sonnet €2 = 1.0px, Opus €10 = 5px, gpt-5.x/gemini-pro €12 = 6px, Fable €20 = 10px** (clamp 0.5–11px). Effort does NOT scale thickness — it is the X column — so each model keeps ONE identity width everywhere it appears. (Earlier log compression was dropped because it flattened exactly the cost proportionality Oscar wanted.)
- - **Halo = measured reality** (q4 "both-halo"): a translucent wide stroke behind the line at the effort bucket implied by `thinkingChars` — prediction (line) vs actuality (halo) divergence is the debugging signal. Honest-labels rules of §5.8g apply verbatim: thinkingChars are CHARS not tokens; Auto = uncapped, never tier 0; redacted ≠ didn't-think.
- - **Forced = dashed** (q9): while the user forces model and/or effort via the sliders the trace keeps drawing, dashed, at the forced position — visual proof the force is obeyed. Solid = router's choice.
-- **Subagent branches (q3 — in v1):** subagent runs **split** off the parent line and **join** back at completion (git-graph). **Concurrency stacking:** concurrent subagents with the SAME (model, effort) render as stacked parallel strands laterally offset around the stop column — count ≤ 4 draws the literal number of strands; count ≥ 5 draws exactly 5 strands plus an **«×N» badge** at the split for magnitude (100 subagents = 5 strands + ×100).
-- **Turn markers (q7 — one-way v1):** every end-of-turn draws a horizontal dashed line across the paper; **click → scroll chat to that turn's answer + outline/flash it** (reuse the context-timeline §5.9 scroll+flash mechanism and the `_fractalParentRunId`-style bubble anchor precedent — answer bubbles carry no runId, tag last real assistant bubble at turn end).
-- **Segment LENGTH ∝ TOKENS → AREA ∝ COST (Oscar 2026-06-13, FINAL):** the two channels are: **width = cost-per-token, LENGTH = tokens used, AREA (width × length) = total cost.** `eegSampleLength = EEG_LEN_PER_WTOKEN · weighted` (NO ÷width). "tokens" = `weighted = outputTokens + 0.2·inputTokens` (output ~5× input, the price ratio) — so the area is cost-weighted and reads as "what this turn cost." (History: Oscar first said "area ∝ tokens" → corrected `0777be03f1`; after the tokens-vs-cost discussion he chose **area = cost, length = tokens** — reverted here. The two differ only by the per-model width factor; cost is the more useful single read because length still shows token volume independently.) Constants in `eeg-trace.ts`: `EEG_LEN_PER_WTOKEN` (px length / weighted token), `EEG_MIN_LEN` (≥ 2·ARC_HALF, keeps tiny turns clickable + fits the bezier), `EEG_MAX_LEN` (caps a giant turn). **VERTICAL ZOOM:** the secondary(right)-button wheel over `#eeg-paper` rescales the whole length axis — `renderSvg({zoom})` multiplies every row length by `eegZoom` (clamped 0.1–20, re-floored at 2·ARC_HALF); plain wheel still scrolls; `contextmenu` is suppressed on the paper so the held right button doesn't pop the OS menu; `fillEegPaper` preserves the scroll ratio across the re-render. Rows stacked cumulatively (newest at top); `ROW_H` now only sizes the EMPTY-paper placeholder. **Token feed:** output from `stream:"effort"` final; **input accumulated per runId from `round-start` `inputTokensEstimate`** (app.ts `eegInputByRun`). Approximate by design — the measured halo stays the truth channel.
-- **Persistence (q8) + PERMANENT retention + REFRESH-SURVIVAL (Oscar 2026-06-13):** the WHOLE session is retained in-memory (no drop-oldest — `EEG_MAX_SAMPLES` is a runaway guard, not a window). **A hard refresh wipes the in-memory store, and the anatomy backfill alone did NOT restore it** (sessions without round-aware anatomy return nothing), so the primary persistence is now **localStorage**: `EegTraceStore.toSnapshot()` → `tinker-eeg:` (capped `EEG_PERSIST_CAP=2000`), saved on every turn-end + `beforeunload`, and **rehydrated in `getEegStore()` on first access** via `backfill()` so the trace is back before any live event lands. The context-anatomy backfill (`?limit=500`) stays as a secondary source for history this browser never saw. Live feed = the `stream:"effort"` consumer + round/lifecycle events. **Vertical scale:** see § scroll/zoom above (secondary horizontal wheel). No placeholder strokes — segments appear only from real events (§5.9).
-- **Force sliders (q2):** the card hosts TWO sliders — the existing 8-stop effort slider (§5.8f, unchanged semantics) and a NEW **model-force slider** (Auto = leftmost stop, then the configured models sorted by rank). Each writes its OWN field via `sessions.update` — `{ thinkingLevel }` and `{ model }` respectively (both patchable, `sessions-patch.ts:289,438`), **never bundled in one patch** (the `rejectWebchatSessionMutation` guard rejects multi-field webchat patches — §5.8f invariant 1). Auto on each axis independently restores router control of that axis.
+ - **Color = provider brand** (full palette, the owner's q6): anthropic `#E8702A`, openai `#10A37F`, google = rainbow `linearGradient` (#4285F4→#EA4335→#FBBC05→#34A853 cycling along the trace), deepseek `#4D6BFE`, mistral `#FA520F`, meta `#0668E1`, xai/local/unknown = neutral gray `#8A8F98`. **The colour comes from the ACTUAL model** even in Auto: the tinker-bridge `stream:"effort"` event self-describes `model: model.id` (added 2026-06-13 — before that the effort event carried no model, so the EEG fell back to `providerOf("")`→gray, which read as "white lines"); the app.ts effort consumer sets `r.model`/`r.provider` from it. **Needs a gateway rebuild** to deploy (tinker-bridge is bundled).
+ - **Auto effort column = MEASURED** (the owner 2026-06-13): in Auto/off (no pinned level) the line is drawn at the column of the effort the model ACTUALLY used — `eegEffectiveLevel(s)` = `thinkingCharsLevel(thinkingChars)` when the level is unset — instead of parking it in the "Auto" gutter. A pinned level always wins. (In Auto the line and the measured halo coincide, which is correct — the measured IS the chosen.)
+ - **Fixed height** (the owner 2026-06-13): `.eeg-paper` is a FIXED 260px box (≈ amygdala `.rpanel` height) that scrolls internally — not a region that grows/shrinks with content.
+ - **Thickness = the owner's REAL per-use cost (€/Mtok output)** (NOT API sticker, NOT static smartness): `width = clamp(1.5, 1.5·log2(1+cost·effortMult), 7)px`. `EEG_COST_TABLE` relCost values ARE effective €/Mtok under the owner's actual billing (2026-06-13), because the providers bill differently: **Anthropic = a flat €200/mo Max subscription** (at an assumed **75% weekly-quota utilization** → ≈ €2 per sonnet-equiv Mtok, then × quota-burn weight: haiku 0.3/sonnet 1/opus 5/fable 10 → **haiku 0.6, sonnet 2, opus 10, fable 20**); **OpenAI/Google = metered API at full price** (**gpt-5.x ≈ 12, gemini-pro ≈ 12, gemini-flash ≈ 2.5, mini ≈ 3**). Net: a frontier API model (gpt-5.5/gemini-pro €12) costs MORE per use than subscription Opus (€10) and far more than subscription Sonnet (€2) — the subscription-vs-API asymmetry is the point. The 75%-quota → €2/Mtok anchor is the big assumption; ESTIMATES until the measured halo corrects them (never present as measured — invariant 3). Mapping is **LINEAR** (the owner 2026-06-13): `width = relCost / 2` → **Sonnet €2 = 1.0px, Opus €10 = 5px, gpt-5.x/gemini-pro €12 = 6px, Fable €20 = 10px** (clamp 0.5–11px). Effort does NOT scale thickness — it is the X column — so each model keeps ONE identity width everywhere it appears. (Earlier log compression was dropped because it flattened exactly the cost proportionality the owner wanted.)
+ - **~~Halo = measured reality~~ + ~~Forced = dashed~~ — BOTH REMOVED (§5.84a, 2026-06-18):** the owner's "everything is what happens, nothing is forced" call. The strand sits at the EXECUTED effort level with no measured-reality halo and no dashed forced-stroke; the requested-vs-executed supervision is enforced UPSTREAM (the slider directives now actually reach the spawn, §5.84), so the line == reality by construction. Honest-labels rules of §5.8g still apply: thinkingChars are CHARS not tokens; Auto = uncapped, never tier 0; redacted ≠ didn't-think.
+- **Subagent branches (q3; restyled 2026-06-14 §5.84, rebuilt as a real tree 2026-06-19):** each subagent is its OWN branch that **splits** off the trunk at its real `startedAt`, runs up at its effort column, and **merges back** into the trunk at its real `endedAt` (still-running → open to the top) — a staggered git-graph tree, not a synchronized fan. **Concurrency = a DYNAMIC ×N gauge** that re-labels in the left gutter at each multiplicity change (×6 → ×9 → …) by sweeping the live overlap over time (replaces the single static badge). Strands are **depth-shaded** (`eegStrandShade()`) and **fan LEFT** into the unused Auto columns (`col = colX − lat·EEG_STRAND_DEPTH_STEP`, clamped to the gutter), capped at `STRAND_CAP=10` RENDERED strands per (model, effort) group — the ×N carries the true total for bigger fan-outs. Hover a strand → its concurrent-at-spawn count + label/model/level/tokens. Replaces the v1 wide lateral fan AND the cap-5 monolith. **Arch geometry is floored at `TOP_PAD` (the owner 2026-06-23, "weird max↔high loop, even stepping on the labels"):** a branch splits from the parent column → strand column → joins back, so it is intentionally an out-and-back arch; but `yOut`/`cpOut`/`yJoinIn`/`joinY` were partially UNCLAMPED, so a branch whose split sat near the paper top punched its arc above `TOP_PAD` into the column-label row and drew a tight max→high→max loop on the labels. ALL branch y are now `Math.max(TOP_PAD, …)` — a near-top branch squishes flat against the top (still visible, never skipped) and relaxes into a full arch as later calls push it down. (Earlier 2026-06-20 fix floored the arch HEIGHT for the split≈join teardrop; this floors the arch POSITION.)
+- **Prompt separators + internal ticks (q7; reworked 2026-06-19; BLUE + send-time + overlay 2026-06-22):** each PROMPT draws a **clear solid YELLOW rule** (`EEG_TURN_COLOR` `#FFD23F`, opacity 0.9, width 2 — was blue `#4DA3FF`, originally faint gray `#C9CDD4`, the owner 2026-06-22) across the paper; the color is a single exported constant so the populated AND `n===0` empty-paper render paths agree. **The boundary now renders even when the store has ZERO samples** (`n===0`): the old empty-paper early-return skipped ALL markers, so a turn sent into a fresh session showed no line — the "no blue/yellow line" bug; markers are now stacked at fixed y in that branch (timeToY is NaN-unsafe with empty arrays). **internal LLM-call boundaries draw only a faint short tick** (the within-prompt rhythm). The trunk **BREAKS at each prompt rule** — the line visibly finishes `EEG_TURN_GAP` below the rule and the next turn restarts `EEG_TURN_GAP` above it (nearly touching), conveying "thinking interrupted per prompt." **The boundary is drawn at PROMPT-SEND time, not turn end (the owner 2026-06-22):** the `chat.send` path records the `turnEnd` immediately (so a new turn is delimited _while it runs_, not only after it finishes) and sets `eegBoundaryAtSend[sk]`; the lifecycle end-handler then SKIPS adding a second line for that turn (it only reuses the bumped turn number to stamp the answer bubble's `_eegTurn`). The QUEUED-send path has no live bubble at send, so it falls back to the old end-time recording. **Click a rule → the chat scrolls to that PROMPT and flashes it** (RELOAD-PROOF: the persisted `turnEnd` carries a stable `promptIndex` (the Nth user message) + `promptText`, so the marker emits `data-eeg-prompt-index` and the click selects `#messages .msg.user[idx]` — NOT the client-only `_eegTurn` stamp; a legacy `data-eeg-turn` fallback remains). **Hover → a styled overlay** (`.eeg-prompt-overlay`, a cursor-pinned floating div fed from `data-eeg-prompt-text`) shows the full prompt + brightens the rule (`.eeg-marker.eeg-hl`) — replaces the slow, unstyleable native SVG `` (kept only as a no-JS fallback).
+- **Segment LENGTH = EURO COST → each €1 = one horizontal grid line (the owner 2026-06-20, SUPERSEDES the 2026-06-13 length∝tokens/area∝cost call):** "make the horizontal lines mean one euro — the thinking should scale to the grid so we understand how much we spend on every prompt." The channels are now: **width = cost-PER-token (model identity), LENGTH = the segment's EURO cost.** `eegSampleLength = EEG_PX_PER_EURO · eegSampleEuros` where `eegSampleEuros = eegRelCost(model)[€/Mtok-output] · weighted / 1e6` and `weighted = outputTokens + 0.2·inputTokens` (output ~5× input, the price ratio). A prompt's trace HEIGHT, measured against the §1 horizontal ruler, reads directly as its euro cost; a thin-but-tall line = a cheap model that ran a LOT and still cost real money. **€-grid:** drawn IN the svg (`.eeg-eurogrid` lines + `.eeg-eurolabel` `€N` gutter labels), pitch = `EEG_PX_PER_EURO · zoom`, anchored at the bottom (oldest = session start) counting UP, so the gutter reads as cumulative session spend. Replaces the old decorative fixed-24px `repeating-linear-gradient` CSS background (removed) which scaled with neither the trace nor zoom. (History: 2026-06-13 went area∝tokens → corrected `0777be03f1` → area=cost/length=tokens; 2026-06-20 the owner made the VERTICAL axis itself euros so the grid is readable money, not an area proxy.) Constants in `eeg-trace.ts`: `EEG_PX_PER_EURO` (px length / €1 = grid pitch), `EEG_MIN_LEN` (≥ 2·ARC_HALF, keeps sub-€0.2 turns clickable + fits the bezier — the floor slightly over-draws the cheapest turns; the grid reading is exact above it), `EEG_MAX_LEN` (backstops a pathological single turn). `eegRelCost` is shared by the width and the euro length. **VERTICAL ZOOM:** the secondary(right)-button wheel over `#eeg-paper` rescales the whole length axis — `renderSvg({zoom})` multiplies every row length by `eegZoom` (clamped 0.03–20 — deeper zoom-out to fit the whole interaction top-to-bottom, 2026-06-19; the `renderSvg` clamp AND both wheel clamps must agree), re-floored at 2·ARC_HALF); plain wheel still scrolls; `contextmenu` is suppressed on the paper so the held right button doesn't pop the OS menu; `fillEegPaper` preserves the scroll ratio across the re-render. Rows stacked cumulatively (newest at top); `ROW_H` now only sizes the EMPTY-paper placeholder. **Token feed:** output from `stream:"effort"` final; **input accumulated per runId from `round-start` `inputTokensEstimate`** (app.ts `eegInputByRun`). Approximate by design — the measured halo stays the truth channel.
+- **Persistence (q8) + PERMANENT retention + REFRESH-SURVIVAL (the owner 2026-06-13):** the WHOLE session is retained in-memory (no drop-oldest — `EEG_MAX_SAMPLES` is a runaway guard, not a window). **A hard refresh wipes the in-memory store, and the anatomy backfill alone did NOT restore it** (sessions without round-aware anatomy return nothing), so the primary persistence is now **localStorage**: `EegTraceStore.toSnapshot()` → `tinker-eeg:` (capped `EEG_PERSIST_CAP=2000`), saved on every turn-end + `beforeunload`, and **rehydrated in `getEegStore()` on first access** via `backfill()` so the trace is back before any live event lands. **RECONCILE-ON-LOAD (the owner 2026-06-22 — "the EEG must persist hand-in-hand with the chat history"):** the context-anatomy backfill (`?limit=500`) is now the AUTHORITATIVE server-side source, reconciled on EVERY `loadChat()` — not only when the local store is empty. The old `isEmpty` gate let a stale/partial localStorage snapshot PERMANENTLY block the re-fetch, so after a reset/reload the chat came back (server transcript) but the EEG did not (the "EEG got wiped on reset" bug). Now `loadChat` always fetches anatomy and, if it carries MORE calls than the local store (`store.toSnapshot().samples.length < events.length`), `clear()`s and rebuilds + re-`saveEegStore`s; a live in-progress store (already as rich or richer) is kept. localStorage is now the fast-path CACHE, anatomy the source of truth — mirroring how chat restores from the gateway transcript. **Subagent BRANCH samples are LIVE-ONLY (the owner 2026-06-23):** `saveEegStore` filters out `subagent:true` samples and `loadEegStoreFromStorage` drops them on read — so only the main call-line persists. Persisting branches had frozen an old sub-call into a stale max→high "banana" arch that was restored on every reload long after the fan-out ended; branches now render in real time from the effort feed and simply don't survive a reload (the durable history is the main line + anatomy). Restored boundaries carry `promptIndex` (`turn-1`) + `promptText` (from the anatomy event's `userMessage`), so a reloaded EEG line is as hoverable/clickable as a live one. **Each boundary anchors at its turn's START** — `endedAt = (turn's first anatomy event ts) - 1`, strictly before that turn's first sample — so the rule sits chronologically BELOW its own calls (newest-at-top ⇒ earlier = lower; the owner 2026-06-22). The live send-time path already satisfies this (boundary `endedAt = send time`, before the turn's samples); the restored path was the one anchoring at the turn TRANSITION (≈ turn END) and thus drawing ABOVE the call — fixed to turn-start. Live feed = the `stream:"effort"` consumer + round/lifecycle events. **Vertical scale:** see § scroll/zoom above (secondary horizontal wheel). No placeholder strokes — segments appear only from real events (§5.9).
+- **Force sliders (q2):** the card hosts the 8-stop EFFORT slider (§5.8f) and a model-force slider. **The EFFORT slider persists CLIENT-SIDE (bible §5.84-C, 2026-06-14):** webchat clients cannot mutate session metadata (`rejectWebchatSessionMutation`) and `sessions.update` is not a real method — so the pick lives in `effortPinBySession` (per session, localStorage-backed) and is re-applied on every `chat.send` via the `thinking`→`/think` channel, reaching `params.thinkLevel` for the turn. The model-force slider still writes `{ model }` server-side (Drop 2 will give it the same client-pin treatment + a per-turn model channel). Auto on each axis restores the skill's control of that axis.
- **Unified model list (q10):** the FALLBACK CHAIN / CONFIGURED two-section split is REPLACED by one rank-sorted **MODELS** list; chain members wear ①②③ badges at the top. This makes the chain primary (e.g. fable-5) visible in the list — it previously vanished from CONFIGURED by being the chain head.
- **DON'T-REGRESS invariants:**
1. The two sliders never share a `sessions.update` patch (guard, above).
2. Trace columns and slider stops share ONE geometry source — drift between them destroys the instrument's meaning.
- 3. No fabricated numbers: thickness is labeled as ESTIMATED cost until the lookup table carries measured values; halo is chars-based.
- 4. Stacked-strand cap is 5 with mandatory ×N badge at ≥5; never render unbounded strand counts.
+ 3. No fabricated numbers: thickness is labeled as ESTIMATED cost until the lookup table carries measured values. (The chars-based "halo" is GONE — §5.84a; the strand sits at the EXECUTED level, line == reality.)
+ 4. Rendered-strand cap is `STRAND_CAP=10` per (model, effort) group; the DYNAMIC ×N gauge carries the true total — never render unbounded strand counts.
+ 5. EEG is its OWN side panel (`#eeg-panel`, peer to Models) with a session/all scope toggle (`#eeg-scope-toggle`); other sessions overlay DIM (semi-transparent), never solid; only the VIEWED trunk breaks at prompts.
+ 6. The trunk BREAKS between EVERY call (each its own segment, `EEG_CALL_GAP` each side) AND more strongly at every prompt rule (`EEG_TURN_GAP` each side, finish + restart) — NO continuous spline; call gap < prompt gap so the hierarchy is visible (the owner 2026-06-20). Prompt separators are CLEAR solid clickable rules; the per-call gaps are the finer rhythm. There are NO connector beziers between calls.
+ 7. Marker→prompt scroll is RELOAD-PROOF: it rides the persisted `promptIndex` (Nth `.msg.user`), NOT the client-only `_eegTurn` stamp. Dead subagent branches close via `closeStaleRunning`/`sweepDeadEegBranches` (clears the activeRuns "thinking-forever" ghost; no real tokens burned — journal-verified), and AUTHORITATIVELY via the backend un-gating subagent events to the FE (`isSubagentSessionKey` OR-in, §5.84b / tool-loop.md) — the sweep is the safety net, the real lifecycle:end is the truth.
- **Files:** `tinker-ui/src/panels/eeg-trace.ts` (NEW — pure state + SVG renderer + cost/color tables, unit-testable), `tinker-ui/src/panels/eeg-trace.test.ts` (NEW), `app.ts` (rename card, unified list, model-force slider, feed EegTrace from the effort/lifecycle handlers, marker→chat focus, backfill call), `styles/base.css` (EEG card + paper styling).
### 5.9 Context Timeline
@@ -855,7 +872,7 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
- `responseToolCalls` (amber `#f59e0b`) — tool call input tokens
- **Data captured:** `responseThinkingTokens`, `responseTextTokens`, `responseToolCallTokens`, `cacheReadTokens`, `cacheCreationTokens` — estimated from char counts during streaming (chars / 3.5).
- **Table name:** SQLite table is `anatomy_events` (user_version=3), with `id / session_key / run_id / turn / round_number / timestamp_ms / provider / model / auth_profile_id / duration_ms / stop_reason / compaction_cycle / context_sent / context_window / tools_triggered / topics / topic_transition / memories_injected / response_tokens / response_thinking_tokens / response_text_tokens / response_tool_call_tokens / cache_read_tokens / cache_creation_tokens / response_content / user_message / assistant_response`. (Earlier drafts of this bible referred to a `context_anatomy` table — that's incorrect; there is no such table.)
-- **Known gap (2026-04-20):** the 4 response-breakdown columns (`response_thinking_tokens`, `response_text_tokens`, `response_tool_call_tokens`, and `duration_ms`) are null on every row in the DB (both historic anthropic turns and the new cc-bridge turns). The subscribe-side capture from §5.31 hasn't survived some merge — needs a separate investigation pass.
+- **Known gap (2026-04-20):** the 4 response-breakdown columns (`response_thinking_tokens`, `response_text_tokens`, `response_tool_call_tokens`, and `duration_ms`) are null on every row in the DB (both historic anthropic turns and the new tinker-bridge turns). The subscribe-side capture from §5.31 hasn't survived some merge — needs a separate investigation pass.
- **REST API:** `GET /api/context-anatomy/recent?hours=48&limit=500` serves cross-session feed (hours max 8760, limit max 2000). Existing `/{sessionKey}` endpoint reads from SQLite.
- **Zlib compression (2026-03-26):** JSON columns (`context_sent`, `context_window`, `tools_triggered`, `topics`, `topic_transition`, `memories_injected`) are zlib-compressed before storage (~60-70% size reduction). Read path handles both compressed BLOBs (new rows) and plain-text JSON (legacy rows) transparently via `decompressJson()`. At ~1-1.5 MB/day compressed, 45 GB free disk = ~80+ years.
- **No pruning (2026-03-26):** Removed 24h auto-prune. Data kept indefinitely. `updateAnatomyResponse` fallback INSERT (which created orphan empty-key rows with `session_key=''`, `turn=0`) also removed — response-only stubs without context data are not useful for the timeline.
@@ -1333,17 +1350,17 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
- **Status:** `DEPLOYED` (vite build + gateway restart) — split/render logic extracted to a unit-tested module.
- **What the user sees:** the per-turn `🧠 AMYGDALA` "gut read" bubble is **gone**. The chat no longer renders a collapsed `🧠` reasoning block, and — critically — no longer **fabricates** one out of pre-answer narration. The per-turn reply is just `💬 ANSWER` (expanded, inline) → `🌿 FRACTAL` (collapsed). The Amygdala lives only in the always-on right-rail side panel (live gate-decision stream) now.
- **Two root causes fixed (both in the UI, the only place that still touched amygdala):** (1) the splitter still recognised a `🧠/🫀 AMYGDALA` marker and carved a collapsed `` block; (2) `renderSectionedReply` **fabricated** an amygdala block from `sec.other` (pre-answer narration) whenever ANY marker (answer OR fractal) was present — so even an amygdala-free reply with one leading sentence got a phantom collapsed `🧠` block. Both removed. (The produce-side was already clean: per-turn injection asks only `💬 ANSWER → 🌿 FRACTAL`; `worker.ts` PROMPT_FILES dropped amygdala-prompt.md; live SOUL.md + fractal-prompt clean. Residual emissions are claude-cli `--resume` session-history habit, which decays.)
-- **New behaviour:** `splitSectionedReply` recognises only `💬 ANSWER` / `🌿 FRACTAL`. A residual `🧠 AMYGDALA` header the model still emits falls into `other`/answer and folds **inline** into the ANSWER bubble; the bare marker line is scrubbed by `scrubResidualSectionMarkers` so it reads as clean prose — no `🧠` label, no collapsed block. (Per Oscar 2026-06-10: show inline as plain text, lossless — the habit fades over new turns.)
+- **New behaviour:** `splitSectionedReply` recognises only `💬 ANSWER` / `🌿 FRACTAL`. A residual `🧠 AMYGDALA` header the model still emits falls into `other`/answer and folds **inline** into the ANSWER bubble; the bare marker line is scrubbed by `scrubResidualSectionMarkers` so it reads as clean prose — no `🧠` label, no collapsed block. (Per the owner 2026-06-10: show inline as plain text, lossless — the habit fades over new turns.)
- **Don't-regress:** keep the `🌿 FRACTAL` split + collapsed `` render intact; keep the live `#amygdala-panel` side panel + `amygdala.feed` RPC + `amygdala-scope-toggle`; keep `reconstructInjectionFields`'s `"Structure this turn's reply as labelled sections"` sentinel (the live ANSWER→FRACTAL injection still emits it). Do NOT re-add a `🧠` marker to the splitter or a `msg-amygdala` emitter.
-- **Files:** `tinker-ui/src/sectioned-reply.ts` (NEW — pure, `md`/`esc` injected), `tinker-ui/src/sectioned-reply.test.ts` (NEW — 10 cases incl. the two regression bugs above), `tinker-ui/src/app.ts` (imports the module; twin call sites; removed pink `🧠 AMYGDALA:` styling + dead `InjectToggles.amygdala`), `extensions/tinkerclaw-cc-bridge/personas/jarvis-default.md` (dropped `🧠` from the marker list), **deleted** `extensions/tinkerclaw-learned-intuition/amygdala-prompt.md` (orphaned).
+- **Files:** `tinker-ui/src/sectioned-reply.ts` (NEW — pure, `md`/`esc` injected), `tinker-ui/src/sectioned-reply.test.ts` (NEW — 10 cases incl. the two regression bugs above), `tinker-ui/src/app.ts` (imports the module; twin call sites; removed pink `🧠 AMYGDALA:` styling + dead `InjectToggles.amygdala`), `extensions/tinkerclaw-tinker-bridge/personas/jarvis-default.md` (dropped `🧠` from the marker list), **deleted** `extensions/tinkerclaw-learned-intuition/amygdala-prompt.md` (orphaned).
### 5.73 Queued prompts render as a trailing "queued" bubble (2026-06-04)
- **Status:** `DEPLOYED-UNTESTED` (HMR-live in `tinker-ui/src/app.ts`, uncommitted)
- **What the user sees:** if you send a new prompt while Jarvis is still answering, your queued message now shows as a **trailing "queued" bubble** at the very bottom of the transcript and stays there until the current turn finishes — then it flushes into the transcript in correct chronological order (after the full answer). Previously the queued user bubble was pushed straight into `messages[]`, so the still-streaming turn's own continuation/tool bubbles landed _after_ it — the queued prompt appeared "in the middle of the last answer" until a hard refresh re-fetched the correct server order.
- **Mechanism (user-relevant):** the queued bubble is held in a separate `pendingQueuedSends` buffer (OUT of `messages[]`) and rendered as a trailing bubble; on turn-final it's flushed into `messages[]`.
-- **Mid-turn steer/blend — IMPLEMENTED (FORK P4, 2026-06-10):** for a cc-bridge session, a message sent while a turn is in flight now **folds into the current answer** instead of running as a separate next turn. The steer dispatch (`runs.ts flushSteerBuffer`) routes through the in-flight-steer hook → `worker.steer()` writes the message to the live `claude` subprocess's stdin, which drains it between its internal tool rounds (verified: `claude -p --input-format stream-json` consumes mid-turn user lines). Only when there is NO live worker (between turns) does it fall back to the pi-agent-core steeringQueue → next-turn `worker.send` (the old behaviour). The two paths are mutually exclusive (no double-delivery). See `tool-loop.md` (the cc-bridge in-flight-steer owner) + `inflight-steer-hook.ts` / `inflight-worker-registry.ts`.
-- **See also:** flows.md / tool-loop.md (cc-bridge turnQueue / drainQueue = next-turn semantics, NOT mid-turn steer); bug-log.md (the "queued prompt in the middle of the answer" root cause). Task `task-mpwfiot2`.
+- **Mid-turn steer/blend — IMPLEMENTED (FORK P4, 2026-06-10):** for a tinker-bridge session, a message sent while a turn is in flight now **folds into the current answer** instead of running as a separate next turn. The steer dispatch (`runs.ts flushSteerBuffer`) routes through the in-flight-steer hook → `worker.steer()` writes the message to the live `claude` subprocess's stdin, which drains it between its internal tool rounds (verified: `claude -p --input-format stream-json` consumes mid-turn user lines). Only when there is NO live worker (between turns) does it fall back to the pi-agent-core steeringQueue → next-turn `worker.send` (the old behaviour). The two paths are mutually exclusive (no double-delivery). See `tool-loop.md` (the tinker-bridge in-flight-steer owner) + `inflight-steer-hook.ts` / `inflight-worker-registry.ts`.
+- **See also:** flows.md / tool-loop.md (tinker-bridge turnQueue / drainQueue = next-turn semantics, NOT mid-turn steer); bug-log.md (the "queued prompt in the middle of the answer" root cause). Task `task-mpwfiot2`.
- **Files:** `tinker-ui/src/app.ts` (`pendingQueuedSends` buffer, trailing-bubble render, flush-on-turn-final).
### 5.74 Background tabs hydrate proactively on connect (2026-06-04)
@@ -1365,18 +1382,66 @@ Two toolbar icons toggle panel visibility with smooth CSS grid animations:
- **Files:** `tinker-ui/src/styles/base.css` (`--skill-highlight*`, `.broca-*`), `tinker-ui/src/panels/broca.ts` (+ `broca.test.ts`, 10/10 green), `tinker-ui/src/panels/prefrontal-tree.ts` (`colorSkillTokens` on node/trail labels + recipe-name link), `tinker-ui/src/app.ts` (`recipe-detail` AltTab, `renderRecipeDetail`, `[data-recipe-ref]` click delegation, recipe-card link).
- **Don't regress:** keep `colorSkillTokens` STRUCTURED-only — never regex skill names out of free chat prose (the deliberate decision; prose matching false-positives on common words like "verify"). The color is a single variable — keep it a `var(--skill-highlight)`, never hard-code yellow.
+### 5.8i Clone tab — eager transcript fork + doubled-icon title (2026-06-24)
+
+- **Status:** `DEPLOYED` (committed develop `042282c19b`). Backend RPC `sessions.fork` in `src/gateway/server-methods/sessions.ts`; frontend wiring + title-doubling in `tinker-ui/src/app.ts`; CSS retirement in `tinker-ui/src/styles/base.css`. The lineage-fallback predecessor landed 2026-06-20 ([[reference_tinker_clone_tab_and_session_fork_paths]]); this is the eager-fork + same-icon-twice completion.
+- **Root cause it fixes:** right-click tab → **Clone** previously had no `sessions.fork` RPC, so `resolveClonedSessionKey` fell through to `sessions.create {parentSessionKey}` = an EMPTY lineage-only session ("clone has no history"). And the doubled icon was a separate `.tab-clone-badge` CSS span — "one little icon then a big one," only the big one editable — not the requested "same icon twice."
+- **What the user sees:** cloning a tab opens a NEW tab, spliced immediately right of its parent, that ALREADY shows the parent's full conversation (a true eager transcript fork, not an empty lineage stub). If the parent title starts with an emoji, the clone's title shows that emoji TWICE (e.g. `🏠 Main` → `🏠🏠 Main`); a title with no leading emoji is cloned verbatim (no doubling).
+- **Backend mechanism (`sessions.fork`):** reads `key` (parent, required via `requireSessionKey`) + optional `label`; loads the parent via `loadSessionEntry(key)` → `{cfg,entry,canonicalKey}`, validates `entry.sessionId`/`entry.sessionFile` + `fs.existsSync(entry.sessionFile)`; forks the parent's **CURRENT** `entry.sessionFile` via `SessionManager.forkFrom(entry.sessionFile, sourceSession.getCwd(), path.dirname(entry.sessionFile))` — the SAME primitive `sessions.compaction.branch` and the restore handler use; mints `nextKey = buildDashboardSessionKey(target.agentId)` (`agent::dashboard:`); builds the store entry via `cloneCheckpointSessionEntry({ currentEntry: entry, nextSessionId, nextSessionFile, label, parentSessionKey: canonicalKey })` (spreads the parent, overwrites sessionId/sessionFile, sets `parentSessionKey`, resets per-run fields, drops compactionCheckpoints); persists via `updateSessionStore`; responds `{ ok, sourceKey: canonicalKey, key: nextKey, sessionId, entry }`; then emits `emitSessionsChanged(context, { sessionKey: nextKey, reason: "fork" })` (the NEW key + `reason: "fork"`). Eager+full: it clones the ENTIRE current live transcript, NOT a checkpoint slice — that is the difference from compaction.branch/restore, which fork a checkpoint's `preCompaction.sessionFile`. Live-verified: forking `agent:main:main` produced a session with the parent's exact 13/13 transcript lines + `parentSessionKey` set.
+- **Frontend mechanism (app.ts):** `resolveClonedSessionKey(parentKey, label)` is a 3-tier silent-degrading chain — (1) `req<{ key?: string }>("sessions.fork", { key: parentKey, label })` → true eager fork; (2) fall through to `sessions.create {parentSessionKey}` (lineage-only, for an older gateway lacking the RPC); (3) a client-only `tinker:` key the gateway lazily auto-creates. `cloneTab(parentTabId)` builds the doubled title: `const icon = leadingEmoji(parent.title); const title = icon ? ` + "`${icon}${parent.title}`" + ` : parent.title;`. It passes `parent.title` (the SINGLE-icon title) as the RPC `label`, sets the new Tab's `titleLocked: true`, splices at `parentIdx+1`, then `switchToTab(clone.id)` → `loadChat()` pulls the forked transcript via `chat.history` so the clone opens already populated.
+- **Don't regress:** (1) The doubled icon lives IN `tab.title` (baked by `cloneTab`) — the old separate `.tab-clone-badge`/`.tab.tab-clone` CSS rules and the `Tab.cloneIcon` field are RETIRED (base.css + app.ts carry only removal comments — no live rule, no live field). Do NOT reintroduce a badge span. (2) Keep `titleLocked: true` on the clone — without it `loadSessions()` clobbers the deliberate doubled title with the fresh fork's server fortune phrase; the doubled icon is NOT cosmetic-only. (3) Keep the 3-tier `resolveClonedSessionKey` fallback silent-degrading — do NOT "fix" the empty try/catch blocks; only tier 1 gives a true eager fork, tiers 2/3 are the back-compat / offline path. (4) Doubling fires ONLY when the parent title has a leading emoji (`leadingEmoji` non-null) — don't claim every clone shows two icons. (5) The response returns BOTH `sourceKey` (parent) and `key` (new fork); `sessions.changed` for a fork carries the NEW key + `reason: "fork"`.
+- **Files:** `src/gateway/server-methods/sessions.ts` (`sessions.fork` handler, reusing `SessionManager.forkFrom` / `buildDashboardSessionKey` / `cloneCheckpointSessionEntry`), `tinker-ui/src/app.ts` (`resolveClonedSessionKey`, `cloneTab` title-doubling, retirement comment in `renderTabs`), `tinker-ui/src/styles/base.css` (retirement comment, `.tab-clone-badge` rules removed). See also: `session-naming.md` (tab-title persistence interplay), [[reference_tinker_clone_tab_and_session_fork_paths]] (the lineage-fallback predecessor).
+
+### 5.8j Recoverable-error warning + client-side auto-retry (2026-06-24)
+
+- **Status:** `DEPLOYED` (committed develop: pure policy `6d9320d97e`, controller `c618805fcd`, styles `a62de24f8e`, schema `de8281702d`, emit-site `9206b1e7fb`). Prompted by an OpenAI-quota incident on a `gpt-5.5`-pinned tab (`You exceeded your current quota` / `All models temporarily rate-limited`) surfacing as a dead-end red error.
+- **What the user sees:** when a turn fails with a RECOVERABLE provider error (quota / rate-limit / `429`, or transient overload `529`/`502`/`503` / "draining for restart"), the dead-end red error is replaced by a centered **orange warning** bubble (reuses `.msg-overload-bubble`) — `⚠️ {label} — retry {n}/6, retrying in {wait}…` — with a **live 1s countdown** and a hover-revealed `stop retrying` link. The last turn is auto-resent on a backoff ladder; each attempt pushes a NEW warning bubble (attempt history is visible). Exhaustion turns the bubble **red** (`🛑 Gave up after 6 retries (…)`). A manual stop / a new user message cancels the pending retry; a manual `abort()` also shows an orange `⏹ Stopped.` warning. This generalizes §5.49 (gateway-drain auto-resend) into ONE unified countdown/stop track covering rate-limit/quota AND overload-class surfaced errors.
+- **Detection — two channels:** a structured backend `reason` (preferred) PLUS a frontend text-match fallback. The backend tags the surfaced `chat` error event with optional `reason` (`"rate_limit" | "quota" | "overloaded" | "unavailable"`) via `resolveFailoverReasonFromError` at the chat-error emit site (`src/gateway/server-methods/chat.ts`); `retryAfter` is intentionally omitted (not cleanly available — the frontend ladder owns timing). The frontend `classifyRecoverable(reason?, errorText?)` trusts a known `reason` else matches `/quota/i`, `/rate.?limit|tpm|rpm|\b429\b/i`, `/overloaded|temporarily unavailable|draining for restart|HTTP 5(02|03|29)/i`.
+- **Why CLIENT-side retry:** the controller re-issues a FRESH `chat.send` (new `idempotencyKey` — the original key would dedup-block the resend), NOT an in-turn server retry, specifically because the long ladder waits (up to 15m) would exceed the 900s gateway turn timeout. The existing server-side in-turn overload-retry (pre-surface, `overload-retry` lifecycle bubbles) is UNCHANGED; this controller engages only on a SURFACED `state==="error"`.
+- **Backoff ladder:** `RETRY_LADDER_MS = [3000, 10000, 30000, 120000, 420000, 900000]` (3s → 10s → 30s → 2m → 7m → 15m), 6 attempts then stop. `nextRetryDelayMs(attempt, retryAfterSec?)` = `max(ladderStep, retryAfter·1000)` for `attempt < ladder.length`, else `null` (exhausted — terminal red, no further retry).
+- **Architecture:** a pure DOM-free policy module `tinker-ui/src/retry-policy.ts` (`RETRY_LADDER_MS`, `classifyRecoverable`, `nextRetryDelayMs`, `formatWait`, `labelFor`; unit-tested 14/14 in `retry-policy.test.ts`) composed by a per-session controller in `app.ts` (`retryState: Map`, `_isRetryWarning` bubbles, countdown ticked alongside the thinking-elapsed 1s interval, `retryState.delete(sk)` on success / cancel).
+- **Don't regress:** (1) the resend MUST use a fresh `idempotencyKey` — reusing the original dedup-blocks it. (2) Keep the retry strictly CLIENT-side; an in-turn server retry on the 7m/15m steps would hit the 900s turn timeout. (3) Do NOT route this through the server overload-retry — it is engaged only on a surfaced `state==="error"`, and the pre-surface server retry stays untouched (no double-retry). (4) `nextRetryDelayMs` returning `null` is terminal — never loop past the 6-step ladder. (5) `classifyRecoverable` prefers the structured `reason`; the text-match is a fallback for older/un-tagged emits — keep both.
+- **Files:** `tinker-ui/src/retry-policy.ts` (NEW pure module), `tinker-ui/src/retry-policy.test.ts` (NEW), `tinker-ui/src/app.ts` (controller + classification + countdown + resend + per-retry bubbles + exhausted + hover-stop + abort warning), `tinker-ui/src/styles/base.css` (`.retry-stop-link` hover reveal + `.msg-overload-bubble.retrying` pulse), `src/gateway/protocol/schema/logs-chat.ts` (`reason` + `retryAfter` optional fields on `ChatEventSchema`), `src/gateway/server-methods/chat.ts` (populate `reason` via `resolveFailoverReasonFromError`). See also: §5.49 (the gateway-drain auto-resend this generalizes), `failures.md` (the surfaced-error categories `rate_limit`/`overload`/`subscription`), `bug-log.md` 2026-06-24.
+
+### 5.8k UI → thetinkerzone "learn more" links (2026-06-24)
+
+- **Status:** `DEPLOYED` (tinker-ui dist rebuilt 2026-06-24; dev tab hot-reloads from source). Intent: turn the UI into a two-way funnel with [The Tinker Zone](https://thetinkerzone.com) — every always-visible surface that names a brain-region concept (EEG, Amygdala, the Models/effort panel, the Recipes/Prefrontal panel, the Fractal dock) carries a small **ⓘ chip** linking to the post that explains it. Boosts paper readership and gives the UI self-documenting depth.
+- **The map is the single source of truth:** `ZONE_DOCS` (in `app.ts`) maps each concept key → `{id, label}`; `zoneDoc(key)` renders the `ⓘ ` chip. To add a link, add a map entry + drop `${zoneDoc("key")}` into the header template — do NOT hand-write anchors.
+- **Link by stable `?p=ID`, NEVER by slug.** WordPress post IDs are immutable; slugs change on retitle. All chips use `https://thetinkerzone.com/?p=`.
+- **Two-tier funnel design:** the always-visible panels link to friendly _explainer_ posts (EEG `448`, Recipe Book `449`, Effort×Model Slider `450`, Command Center `451` — written 2026-06-24); those explainers in turn link to the deep _J-series_ papers (Prefrontal `233`, Salience `264`, Amygdala/Humor `211`, Fractal `198`, recipe-as-abstraction `237`). So the deep papers are reachable from the UI in ONE hop without crowding the chrome.
+- **Wired surfaces (right-rail `rpanel-header` + fractal dock):** 🕸️ Models → slider `450`; 📈 EEG → eeg `448`; 🌳 RECIPES (prefrontal-panel) → recipe-book `449` + prefrontal `233` (two chips, panel spans both concepts); 🧠 AMYGDALA → `211`; 🌿 Fractal dock summary → `198` (chip appended in `fractal-dock.ts`, `stopPropagation` so the click doesn't toggle the dock).
+- **Not yet anchored (no always-visible UI surface):** Total Recall, Instant Recall/Hippocampus, Curiosity, Round Table, Identity, Learned Intuition, Broca, Sleep Consolidation, Myelin, Striatum, AEGIS, HIVEMIND. These are backend concepts without a dedicated panel; they are reachable only transitively (e.g. the EEG explainer names Prefrontal/Salience/Amygdala/Fractal/Consolidation). Add a chip when/if a concept gains a visible surface.
+- **Files:** `tinker-ui/src/app.ts` (`ZONE_DOCS` + `zoneDoc()` + 4 header chips), `tinker-ui/src/fractal-dock.ts` (FRACTAL summary chip), `tinker-ui/src/styles/base.css` (`.rpanel-doc-link`). New posts authored via `wordpress-ultimate/scripts/wp.sh POST posts` under the autonomous-publish-under-100-visitors gate. See also: `jseries-publish-roll.md` (paper→post-ID map), the brain-diagram hub post `428`.
+
+### 5.8L Subagent → tab attribution — no cross-tab message bleed (2026-06-25)
+
+- **Status:** `DEPLOYED` (tinker-ui dist rebuilt 2026-06-25; dev tab hot-reloads from source). Fixes the bug where, with two tabs open, a fan-out launched from tab A streamed its subagent sub-bubbles into tab B too.
+- **Why it happened:** subagent session keys are minted FLAT under the agent root — `agent:main:subagent:` — with no parent-tab encoding. Every tab is under the same `agent:main` root (main = `agent:main:main`, extra tabs = `agent:main:dashboard:`), so `chatEventIsSubagentOfView()`'s `evtKey.startsWith(agentRoot + ":subagent:")` match claimed every subagent for every tab. The loose match was a 2026-06-15 over-correction of an earlier strict full-key prefix that had made subagent lanes VANISH from non-`:main` tabs — the "vanish ↔ bleed" seesaw of a key that lacks the parent link.
+- **The fix — attribute by the run graph, not the key:** a `subagentOwnerTab` map (subagent sessionKey → owning tab sessionKey) is populated at subagent birth by `recordSubagentOwner(p.sessionKey, p.data.parentRunId)` at the agent-event handler entry, resolving `parentRunId → activeRuns[parentRunId].sessionKey` while the parent run is still active, transitively past intermediate subagent runs. `chatEventIsSubagentOfView()` returns true for a subagent ONLY when its resolved owner is the viewed session.
+- **Fallback (no regression):** when the owner isn't resolved yet (a delta racing ahead of the spawn event), it falls back to the loose agent-root match ONLY if `attachedTabCountForRoot(agentRoot) <= 1` — i.e. the lone tab on the root, where no sibling exists to bleed into. With 2+ tabs and an unresolved owner it refuses the match (the subagent still surfaces in the Prefrontal/EEG panels; the parent turn's own output is unaffected). A subagent of a tinker/dashboard tab resolves to that tab and shows in it — the 2026-06-15 "vanish" case stays fixed.
+- **Shared predicate:** `chatEventIsSubagentOfView()` feeds both the chat consumer and the EEG/effort consumer, so both attribute per-tab consistently. The "all"-scope orchestration view is fed by the separate unfiltered extension-tree broadcast, so it is unaffected.
+- **Files:** `tinker-ui/src/app.ts` (`subagentOwnerTab`, `recordSubagentOwner`, `attachedTabCountForRoot`, `chatEventIsSubagentOfView`). Validate via `vite build` (tsc OOMs on app.ts). See also: `bug-log.md` 2026-06-25 `[tab-bleed]`.
+
## Generated FORK registry
> Auto-generated by `scripts/gen-tinker-ui-registry.mjs`. Do not hand-edit between the markers below.
-Files with at least one `// FORK YYYY-MM-DD:` anchor — sorted by most-recent edit. Total anchors: **95** across **3** files.
+Files with at least one `// FORK YYYY-MM-DD:` anchor — sorted by most-recent edit. Total anchors: **613** across **11** files.
| File | Anchors | First | Last |
| ------------------------------- | ------- | ---------- | ---------- |
-| `src/app.ts` | 75 | 2026-04-17 | 2026-05-11 |
-| `src/styles/base.css` | 18 | 2026-04-17 | 2026-05-11 |
-| `src/panels/prefrontal-tree.ts` | 2 | 2026-04-20 | 2026-04-27 |
+| `src/app.ts` | 429 | 2026-04-17 | 2026-06-24 |
+| `src/styles/base.css` | 104 | 2026-04-17 | 2026-06-24 |
+| `src/panels/eeg-trace.ts` | 30 | 2026-06-13 | 2026-06-23 |
+| `src/queued-sends.ts` | 2 | 2026-06-08 | 2026-06-19 |
+| `src/reply-grouping.ts` | 1 | 2026-06-19 | 2026-06-19 |
+| `src/md-links.ts` | 1 | 2026-06-14 | 2026-06-14 |
+| `src/fractal-dock.ts` | 1 | 2026-06-11 | 2026-06-11 |
+| `src/panels/prefrontal-tree.ts` | 40 | 2026-04-20 | 2026-06-11 |
+| `src/sectioned-reply.ts` | 2 | 2026-05-24 | 2026-06-10 |
+| `src/panels/presence-graph.ts` | 2 | 2026-06-04 | 2026-06-07 |
+| `src/subagent-color.ts` | 1 | 2026-05-30 | 2026-05-30 |
diff --git a/TINKER_UI_DESIGN_BIBLE/tool-loop.md b/TINKER_UI_DESIGN_BIBLE/tool-loop.md
index 6ee9785bc9ad0..3784eb7e61e22 100644
--- a/TINKER_UI_DESIGN_BIBLE/tool-loop.md
+++ b/TINKER_UI_DESIGN_BIBLE/tool-loop.md
@@ -1,49 +1,55 @@
---
file: tool-loop.md
-purpose: Why cc-bridge's tool loop differs from pi-agent-core's; consequences and the heartbeat-stream proposal
+purpose: Why tinker-bridge's tool loop differs from pi-agent-core's; consequences and the heartbeat-stream proposal
audience: AI
last_verified: 2026-05-14
last_verified_commit: HEAD
single_owner: yes — this is the one place to learn why fork tool-loop ≠ upstream
-see_also: flows.md (F1 cc-bridge spawn flow), failures.md (M1 idle-watchdog SIGTERM), config-shape.md (timeoutSeconds), panels.md (thinking indicator + prefrontal panel)
+see_also: flows.md (F1 tinker-bridge spawn flow), failures.md (M1 idle-watchdog SIGTERM), config-shape.md (timeoutSeconds), panels.md (thinking indicator + prefrontal panel)
verify:
- - name: cc-bridge stream.ts still suppresses tool_use blocks from assistant.message.content
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/stream.ts")).read(); assert "FORK (2026-04-22)" in t and "re-execute them via the OpenClaw exec tool" in t, "the 2026-04-22 tool-loop divergence comment block is missing from stream.ts — verify the suppression still holds"'
+ - name: tinker-bridge stream.ts still suppresses tool_use blocks from assistant.message.content
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/stream.ts")).read(); assert "FORK (2026-04-22)" in t and "re-execute them via the OpenClaw exec tool" in t, "the 2026-04-22 tool-loop divergence comment block is missing from stream.ts — verify the suppression still holds"'
- name: idle-timeout-diag log line is emitted on each turn (idle watchdog is wrapped)
cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/src/agents/embedded-agent-runner/run/attempt.ts")).read(); assert "[idle-timeout-diag]" in t, "the idle-timeout-diag canary log line is missing from attempt.ts"'
- - name: cc-bridge heartbeat is wired in stream.ts (FORK 2026-05-11)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/stream.ts")).read(); assert "FORK 2026-05-11" in t and "heartbeat" in t.lower() and "HEARTBEAT_INTERVAL_MS" in t, "the cc-bridge heartbeat that resets pi-agent-core idle watchdog is missing or undocumented"'
+ - name: tinker-bridge heartbeat is wired in stream.ts (FORK 2026-05-11)
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/stream.ts")).read(); assert "FORK 2026-05-11" in t and "heartbeat" in t.lower() and "HEARTBEAT_INTERVAL_MS" in t, "the tinker-bridge heartbeat that resets pi-agent-core idle watchdog is missing or undocumented"'
- name: tinker-ui has no stale-run watchdog (FORK 2026-05-14 — deleted; trust lifecycle:end instead)
cmd: python3 -c 'import os,re; t = open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); assert "STALE_RUN_WATCHDOG_MS" not in t, "STALE_RUN_WATCHDOG_MS reappeared in app.ts — the UI-side stale-run watchdog was deleted on 2026-05-14 and must stay deleted; the cure for a stuck thinking indicator is to harden lifecycle:end emission in attempt.ts, not to add a UI-side timer that lies"; assert re.search(r"activeRuns\.delete\(\s*runId\s*\)[^}]*stalePruned", t) is None, "a force-clear of activeRuns from a timer reappeared — the watchdog pattern is back"; assert "bumpActiveRunActivity" in t, "bumpActiveRunActivity is still useful for keeping lastEventAt fresh (drives lastEventAge in the prefrontal panel); do not delete"'
- name: lifecycle:end / lifecycle:error are emitted by handleAgentEnd (FORK 2026-05-14 — the UI trusts these to clear the thinking indicator since the watchdog was deleted)
cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/src/agents/embedded-agent-subscribe.handlers.lifecycle.ts")).read(); assert "handleAgentEnd" in t and "phase: \"end\"" in t and "phase: \"error\"" in t and "emitAgentEvent" in t, "handleAgentEnd no longer emits a lifecycle:end (or :error) event via emitAgentEvent — the UI trusts these emissions to clear the thinking indicator. Restore the emission or expect stuck thinking indicators that can only be cleared by browser refresh."'
- name: shutdown-frame handler enrolls activeRuns into unconfirmedRuns (FORK 2026-05-24 — graceful-restart prune wouldn't fire otherwise)
cmd: python3 -c 'import os,re; t = open(os.path.expanduser("~/src/tinkerclaw/tinker-ui/src/app.ts")).read(); block = re.search(r"f\.event === \"shutdown\".*?f\.payload\?\.restartExpectedMs.*?\}", t, re.S); assert block, "the shutdown-frame handler in app.ts is missing or has been refactored — re-locate and verify it still adds runIds to unconfirmedRuns"; assert "unconfirmedRuns.add(runId)" in block.group(0), "the shutdown-frame handler no longer enrolls activeRuns into unconfirmedRuns. Without this, an in-tab graceful restart leaves stale activeRuns entries forever (no lifecycle:end will ever come from the dead gateway process), the prefrontal panel shows the indicator + clock frozen at the pre-restart state, and only a page reload clears the ghost. See bug-log.md FIXED 2026-05-24 ghost-run."'
- - name: in-flight steer is wired end-to-end (FORK 2026-06-10 / P4 — a mid-answer message folds into the live cc-bridge turn instead of next-turn-only)
- cmd: python3 -c 'import os; R=lambda p: open(os.path.expanduser(p)).read(); assert "steer(text: string): boolean" in R("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/worker.ts"), "worker.steer() primitive missing — mid-turn stdin injection gone"; assert "tryInflightSteer(sessionId, combined)" in R("~/src/tinkerclaw/src/agents/embedded-agent-runner/runs.ts"), "flushSteerBuffer no longer routes through the in-flight steer hook — mid-turn steer regressed to next-turn-only"; reg=R("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/inflight-worker-registry.ts"); assert "registerInflightSteerHook(" in reg and "worker.steer(text)" in reg, "cc-bridge no longer bridges worker.steer into the core in-flight steer hook"'
- - name: cc-bridge protocol carries the forward-compat server-tool/redacted CcContentBlock arms (FORK 2026-06-11)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/protocol.ts")).read(); assert "server_tool_use" in t and "web_search_tool_result" in t and "redacted_thinking" in t, "the forward-compat CcContentBlock arms (server_tool_use / web_search_tool_result / redacted_thinking) are missing from protocol.ts — claude-cli currently normalizes WebSearch/WebFetch into plain tool_use/tool_result, but the typed arms must stay so a future schema bump does not fall into the open-ended forward-compat catch-all undecoded"'
- - name: cc-bridge emits a turn-incomplete lifecycle event for any non-success result.subtype (FORK 2026-06-11)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/stream.ts")).read(); assert "turn-incomplete" in t, "the phase:\"turn-incomplete\" lifecycle event is missing from stream.ts — error_max_turns / error_during_execution / generic error results no longer badge the run as incomplete. It MUST be emitted from the result/done branch BEFORE the is_error early-return, because done is always stopReason:\"stop\" and pi-ai StopReason has no incomplete member, so a non-success subtype can only be surfaced as a free-form lifecycle event."; assert "flattenResultContent" in t, "the exported flattenResultContent helper is no longer used in stream.ts — tool_result content blocks (string | CcContentBlock[]) must be flattened to plain text through the shared helper"'
- - name: cc-bridge sets MAX_THINKING_TOKENS as the third native Claude Code env knob on the claude child (FORK 2026-06-11 — per-session thinking budget)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/worker.ts")).read(); assert "MAX_THINKING_TOKENS" in t, "MAX_THINKING_TOKENS is gone from worker.ts — the per-session thinking-budget knob (the third native Claude Code env var cc-bridge sets on the claude child, alongside CLAUDE_CODE_MAX_OUTPUT_TOKENS) must be in allowedKeys and set from the resolved per-session think level; for level off the var is OMITTED, not set to 0"'
+ - name: in-flight steer is wired end-to-end (FORK 2026-06-10 / P4 — a mid-answer message folds into the live tinker-bridge turn instead of next-turn-only)
+ cmd: python3 -c 'import os; R=lambda p: open(os.path.expanduser(p)).read(); assert "steer(text: string): boolean" in R("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/worker.ts"), "worker.steer() primitive missing — mid-turn stdin injection gone"; assert "tryInflightSteer(sessionId, combined)" in R("~/src/tinkerclaw/src/agents/embedded-agent-runner/runs.ts"), "flushSteerBuffer no longer routes through the in-flight steer hook — mid-turn steer regressed to next-turn-only"; reg=R("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/inflight-worker-registry.ts"); assert "registerInflightSteerHook(" in reg and "worker.steer(text)" in reg, "tinker-bridge no longer bridges worker.steer into the core in-flight steer hook"'
+ - name: tinker-bridge protocol carries the forward-compat server-tool/redacted CcContentBlock arms (FORK 2026-06-11)
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/protocol.ts")).read(); assert "server_tool_use" in t and "web_search_tool_result" in t and "redacted_thinking" in t, "the forward-compat CcContentBlock arms (server_tool_use / web_search_tool_result / redacted_thinking) are missing from protocol.ts — claude-cli currently normalizes WebSearch/WebFetch into plain tool_use/tool_result, but the typed arms must stay so a future schema bump does not fall into the open-ended forward-compat catch-all undecoded"'
+ - name: tinker-bridge emits a turn-incomplete lifecycle event for any non-success result.subtype (FORK 2026-06-11)
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/stream.ts")).read(); assert "turn-incomplete" in t, "the phase:\"turn-incomplete\" lifecycle event is missing from stream.ts — error_max_turns / error_during_execution / generic error results no longer badge the run as incomplete. It MUST be emitted from the result/done branch BEFORE the is_error early-return, because done is always stopReason:\"stop\" and pi-ai StopReason has no incomplete member, so a non-success subtype can only be surfaced as a free-form lifecycle event."; assert "flattenResultContent" in t, "the exported flattenResultContent helper is no longer used in stream.ts — tool_result content blocks (string | CcContentBlock[]) must be flattened to plain text through the shared helper"'
+ - name: tinker-bridge sets MAX_THINKING_TOKENS as the third native Claude Code env knob on the claude child (FORK 2026-06-11 — per-session thinking budget)
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/worker.ts")).read(); assert "MAX_THINKING_TOKENS" in t, "MAX_THINKING_TOKENS is gone from worker.ts — the per-session thinking-budget knob (the third native Claude Code env var tinker-bridge sets on the claude child, alongside CLAUDE_CODE_MAX_OUTPUT_TOKENS) must be in allowedKeys and set from the resolved per-session think level; for level off the var is OMITTED, not set to 0"'
- name: the resolved think level rides the pi-ai options smuggle as __openclawThinkLevel into stream.ts (FORK 2026-06-11)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/stream.ts")).read(); assert "__openclawThinkLevel" in t, "__openclawThinkLevel is gone from stream.ts — the per-run think level no longer rides the existing pi-ai options smuggle from attempt.ts into pool.getOrCreate; the worker child will spawn without the per-session thinking budget"'
- - name: cc-bridge stream.ts emits the server-computed stream:"effort" agent-event (FORK 2026-06-11 — actual-effort telemetry)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/stream.ts")).read(); assert "effort" in t and "thinkingChars" in t and "hadRealThinking" in t, "the server-computed stream:\"effort\" agent-event (fields thinkingChars / hadRealThinking / configuredBudget …) is gone from stream.ts — the UI effort chip loses its only honest signal for how much the model actually thought. It is computed SERVER-SIDE from accumulatedThinking so it works at every level incl Auto; do not move the computation to the client (the client never sees accumulatedThinking)."'
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/stream.ts")).read(); assert "__openclawThinkLevel" in t, "__openclawThinkLevel is gone from stream.ts — the per-run think level no longer rides the existing pi-ai options smuggle from attempt.ts into pool.getOrCreate; the worker child will spawn without the per-session thinking budget"'
+ - name: tinker-bridge stream.ts emits the server-computed stream:"effort" agent-event (FORK 2026-06-11 — actual-effort telemetry)
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/stream.ts")).read(); assert "effort" in t and "thinkingChars" in t and "hadRealThinking" in t, "the server-computed stream:\"effort\" agent-event (fields thinkingChars / hadRealThinking / configuredBudget …) is gone from stream.ts — the UI effort chip loses its only honest signal for how much the model actually thought. It is computed SERVER-SIDE from accumulatedThinking so it works at every level incl Auto; do not move the computation to the client (the client never sees accumulatedThinking)."'
- name: the think-level-pending lifecycle phase is emitted when a level change is deferred behind a busy warm worker (FORK 2026-06-11)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/stream.ts")).read(); assert "think-level-pending" in t, "the phase:\"think-level-pending\" lifecycle event is missing from stream.ts — when a think-level change lands on a BUSY warm worker the new budget is deferred one turn (env is read at spawn only), and the UI needs this badge to tell the user the new effort applies next turn rather than now. Without it the chip silently lies about the active budget for one turn."'
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/stream.ts")).read(); assert "think-level-pending" in t, "the phase:\"think-level-pending\" lifecycle event is missing from stream.ts — when a think-level change lands on a BUSY warm worker the new budget is deferred one turn (env is read at spawn only), and the UI needs this badge to tell the user the new effort applies next turn rather than now. Without it the chip silently lies about the active budget for one turn."'
- name: worker-pool getOrCreate compares thinkLevel so a level change is not swallowed by a warm worker (FORK 2026-06-11 — the warm-worker thinkLevel LAG fix)
- cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-cc-bridge/src/worker-pool.ts")).read(); assert "thinkLevel" in t, "getOrCreate no longer references thinkLevel — it would hand back the warm worker WITHOUT comparing the requested thinkLevel to the one it was spawned with, so a level change is swallowed until the worker happens to be evicted (the warm-worker thinkLevel LAG). On an idle level-change getOrCreate must evict+respawn with the new MAX_THINKING_TOKENS (--resume re-attaches the same claude conversation, history preserved); on a busy worker it must defer one turn and record the pending level."'
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/worker-pool.ts")).read(); assert "thinkLevel" in t, "getOrCreate no longer references thinkLevel — it would hand back the warm worker WITHOUT comparing the requested thinkLevel to the one it was spawned with, so a level change is swallowed until the worker happens to be evicted (the warm-worker thinkLevel LAG). On an idle level-change getOrCreate must evict+respawn with the new MAX_THINKING_TOKENS (--resume re-attaches the same claude conversation, history preserved); on a busy worker it must defer one turn and record the pending level."'
+ - name: oversized-resume guard + RESUME_MAX_TRANSCRIPT_BYTES are wired (FORK 2026-06-23 — a fat resumed transcript starts FRESH instead of choking claude --resume)
+ cmd: python3 -c 'import os; w = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/worker.ts")).read(); d = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/defaults.ts")).read(); assert "RESUME_MAX_TRANSCRIPT_BYTES" in d, "RESUME_MAX_TRANSCRIPT_BYTES is gone from defaults.ts — the 8MB oversized-resume threshold that makes a fat transcript start FRESH instead of choking claude --resume is missing"; assert "RESUME_MAX_TRANSCRIPT_BYTES" in w, "worker.ts no longer consults RESUME_MAX_TRANSCRIPT_BYTES before --resume — the oversized-resume guard regressed; it MUST be fail-open (stat error → resume as before)"; assert os.path.exists(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/transcript-path.ts")), "transcript-path.ts (resolves the resume transcript path for the stat-size guard) was deleted"'
+ - name: init-only fast-fail stall watchdog is wired and line-count gated (FORK 2026-06-23 — catches the choked-resume never-started shape without killing heavy tool turns)
+ cmd: python3 -c 'import os; s = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/stream.ts")).read(); d = open(os.path.expanduser("~/src/tinkerclaw/extensions/tinkerclaw-tinker-bridge/src/defaults.ts")).read(); assert "FAST_FAIL_INIT_SILENT_MS" in d and "FAST_FAIL_MAX_INIT_LINES" in d, "the FAST_FAIL_* knobs are gone from defaults.ts"; assert "FAST_FAIL_INIT_SILENT_MS" in s and "FAST_FAIL_MAX_INIT_LINES" in s, "stream.ts no longer fast-fails an init-only stall gated on FAST_FAIL_MAX_INIT_LINES — without the line-count gate it would kill legitimately-silent heavy tool turns; it must NOT lower DEFAULT_REQUEST_TIMEOUT_MS (600s)"'
+ - name: idle-timeout failover suppresses a futile same-model re-resume of a zero-content attempt (FORK 2026-06-23)
+ cmd: python3 -c 'import os; t = open(os.path.expanduser("~/src/tinkerclaw/src/agents/embedded-agent-runner/run.ts")).read(); assert "producedNoContent" in t, "run.ts no longer tracks producedNoContent — the idle-timeout retry would re-resume the same fat transcript that just stalled, reproducing the stall. A same-model re-resume must be suppressed when the aborted attempt produced zero content."'
---
-# Tool loop — the cc-bridge / claude-cli divergence (FORK 2026-04-22)
+# Tool loop — the tinker-bridge / claude-cli divergence (FORK 2026-04-22)
-This is the single most consequential fork-side decision. Every "why does cc-bridge behave like that?" question routes through this section.
+This is the single most consequential fork-side decision. Every "why does tinker-bridge behave like that?" question routes through this section.
## The upstream behavior
-In native (non-cc-bridge) providers, `pi-agent-core`'s agent loop processes the stream as it arrives:
+In native (non-tinker-bridge) providers, `pi-agent-core`'s agent loop processes the stream as it arrives:
1. Stream emits `tool_use` block → block appears in `assistant.message.content`.
2. Agent loop sees `assistant.message.content[i].type === "tool_use"` → executes via the OpenClaw exec tool (Bash, Read, Edit, etc.).
@@ -51,11 +57,11 @@ In native (non-cc-bridge) providers, `pi-agent-core`'s agent loop processes the
This is the canonical agent loop. It works because pi-agent-core OWNS the tool execution.
-## The cc-bridge problem
+## The tinker-bridge problem
-cc-bridge does NOT own tool execution. Inside the `claude-cli` subprocess, tool calls execute _natively_ — claude-cli has its own implementations of Read/Bash/Edit, with its own permission gates (`--permission-mode bypassPermissions`), its own subagent system, its own plugin host. When the stream emits `tool_use` and `tool_result` blocks, they reflect work claude-cli ALREADY did.
+tinker-bridge does NOT own tool execution. Inside the `claude-cli` subprocess, tool calls execute _natively_ — claude-cli has its own implementations of Read/Bash/Edit, with its own permission gates (`--permission-mode bypassPermissions`), its own subagent system, its own plugin host. When the stream emits `tool_use` and `tool_result` blocks, they reflect work claude-cli ALREADY did.
-If cc-bridge were to forward those `tool_use` blocks into `assistant.message.content`, pi-agent-core would see them and _re-execute_ via the OpenClaw exec tool. This:
+If tinker-bridge were to forward those `tool_use` blocks into `assistant.message.content`, pi-agent-core would see them and _re-execute_ via the OpenClaw exec tool. This:
1. Re-runs every tool the model already ran (file reads, bash commands, edits).
2. Hits the prefrontal "Exploration required" gate on the second execution.
@@ -63,14 +69,14 @@ If cc-bridge were to forward those `tool_use` blocks into `assistant.message.con
This was observed and documented in the 2026-04-22 fix.
-## The cc-bridge solution
+## The tinker-bridge solution
-Tool calls are visible to the user via `stream` events (cc-bridge emits a `tool_start` / `tool_result` synthetic stream event per call), but **NOT placed in `assistant.message.content`**. The final assistant message contains only the model's natural-language output. Pi-agent-core never sees the `tool_use` blocks. No re-execution.
+Tool calls are visible to the user via `stream` events (tinker-bridge emits a `tool_start` / `tool_result` synthetic stream event per call), but **NOT placed in `assistant.message.content`**. The final assistant message contains only the model's natural-language output. Pi-agent-core never sees the `tool_use` blocks. No re-execution.
```mermaid
sequenceDiagram
participant CLI as claude-cli
- participant CC as cc-bridge stream.ts
+ participant CC as tinker-bridge stream.ts
participant CON as stream-event consumer (UI)
participant PI as pi-agent-core (assistant.message.content)
@@ -90,16 +96,16 @@ sequenceDiagram
## The consequence: idle watchdog starvation
-`pi-agent-core`'s `streamWithIdleTimeout` resets per `pi-ai` stream event. cc-bridge intentionally suppresses tool-related stream events to pi-ai (the synthetic ones above go to the UI consumer, not into pi-ai's event stream). On a long claude-cli tool chain (read several outlook emails, then several people.read calls, then a write), pi-ai sees NO events flow → idle timer ticks → SIGTERM.
+`pi-agent-core`'s `streamWithIdleTimeout` resets per `pi-ai` stream event. tinker-bridge intentionally suppresses tool-related stream events to pi-ai (the synthetic ones above go to the UI consumer, not into pi-ai's event stream). On a long claude-cli tool chain (read several outlook emails, then several people.read calls, then a write), pi-ai sees NO events flow → idle timer ticks → SIGTERM.
-**Workaround (2026-05-05 / corrected 2026-05-10):** bump the idle timeout via provider-level `timeoutSeconds: 600` (cc-bridge plugin overlay path). Heavy turns now have 10 minutes per stretch of silence. See config-shape.md.
+**Workaround (2026-05-05 / corrected 2026-05-10):** bump the idle timeout via provider-level `timeoutSeconds: 600` (tinker-bridge plugin overlay path). Heavy turns now have 10 minutes per stretch of silence. See config-shape.md.
-**Architectural fix (LIVE 2026-05-11, commit on cc-bridge `stream.ts`):** cc-bridge now emits an empty-delta heartbeat through the pi-ai stream every 25s of silence during a turn. The 600s overlay timeout becomes belt-and-suspenders rather than load-bearing; a future accidental reset to 120s no longer reproduces the 2026-05-05 incident.
+**Architectural fix (LIVE 2026-05-11, commit on tinker-bridge `stream.ts`):** tinker-bridge now emits an empty-delta heartbeat through the pi-ai stream every 25s of silence during a turn. The 600s overlay timeout becomes belt-and-suspenders rather than load-bearing; a future accidental reset to 120s no longer reproduces the 2026-05-05 incident.
```mermaid
sequenceDiagram
participant CLI as claude-cli
- participant CC as cc-bridge stream.ts
+ participant CC as tinker-bridge stream.ts
participant PI as pi-agent-core
loop every 25s while no real event has fired
@@ -145,7 +151,7 @@ What was kept in `app.ts`:
```mermaid
sequenceDiagram
- participant CCB as cc-bridge stream.ts
+ participant CCB as tinker-bridge stream.ts
participant SRV as gateway / handleAgentEnd
participant WS as gateway WS
participant UI as tinker-ui activeRuns
@@ -164,14 +170,14 @@ sequenceDiagram
## In-flight steer — mid-turn message folds into the live turn (FORK 2026-06-10 / P4)
-A message sent while a cc-bridge turn is in flight now **folds into the current answer**, matching Claude Code (whose own loop drains its message queue between tool rounds). Previously a mid-turn message could only run as a **separate next turn** (pi steeringQueue → next `worker.send`), because pi-agent-core cannot inject between claude-cli's internal tool rounds — the whole claude-cli agentic loop is one opaque `worker.send`.
+A message sent while a tinker-bridge turn is in flight now **folds into the current answer**, matching Claude Code (whose own loop drains its message queue between tool rounds). Previously a mid-turn message could only run as a **separate next turn** (pi steeringQueue → next `worker.send`), because pi-agent-core cannot inject between claude-cli's internal tool rounds — the whole claude-cli agentic loop is one opaque `worker.send`.
**Why it works:** `claude -p --input-format stream-json` drains additional `{"type":"user",...}` stdin lines mid-turn, between its internal tool rounds (verified empirically 2026-06-10: a stdin user-line injected during a tool chain was acknowledged before the turn's `result` line). So writing to the live subprocess's stdin reaches the model mid-answer.
**The path:**
-1. `worker.steer(text)` (`extensions/tinkerclaw-cc-bridge/src/worker.ts`) writes one user line to the already-open persistent stdin — WITHOUT touching `currentTurn`/`turnQueue`/`kill` (the live turn keeps owning its `result`). No-ops between turns; EPIPE-safe.
-2. `extensions/tinkerclaw-cc-bridge/src/inflight-worker-registry.ts` tracks `openclawSessionId → live worker` (set around `worker.send` in `stream.ts`) and registers a hook into the core via `registerInflightSteerHook` (`src/agents/embedded-agent-runner/inflight-steer-hook.ts`, stored on `globalThis[Symbol.for(...)]` so it crosses the core/extension bundle split).
+1. `worker.steer(text)` (`extensions/tinkerclaw-tinker-bridge/src/worker.ts`) writes one user line to the already-open persistent stdin — WITHOUT touching `currentTurn`/`turnQueue`/`kill` (the live turn keeps owning its `result`). No-ops between turns; EPIPE-safe.
+2. `extensions/tinkerclaw-tinker-bridge/src/inflight-worker-registry.ts` tracks `openclawSessionId → live worker` (set around `worker.send` in `stream.ts`) and registers a hook into the core via `registerInflightSteerHook` (`src/agents/embedded-agent-runner/inflight-steer-hook.ts`, stored on `globalThis[Symbol.for(...)]` so it crosses the core/extension bundle split).
3. `runs.ts flushSteerBuffer` calls `tryInflightSteer(sessionId, combined)` FIRST; if a live worker accepts it (folds in), it RETURNS and does NOT also `handle.queueMessage` — so the message is delivered exactly once. Only with no live worker does it fall back to the pi steeringQueue (next-turn, old behaviour).
So: during a live turn → mid-turn fold (`worker.steer`); between turns → next turn (pi steeringQueue → `worker.send`). The 300ms debounce still batches rapid messages into one injection.
@@ -180,14 +186,28 @@ So: during a live turn → mid-turn fold (`worker.steer`); between turns → nex
- The two delivery paths MUST stay mutually exclusive (the early `return` in `flushSteerBuffer` after a successful `tryInflightSteer`). Calling both = the message delivered twice (once mid-turn, once as the next round).
- `worker.steer` MUST NOT touch `currentTurn`/`turnQueue`/`kill` — it only writes stdin. Mutating `currentTurn` would orphan the in-flight `send()` promise (caller hangs); killing would defeat the whole "queue not SIGTERM" point.
-- The hook MUST live on `globalThis[Symbol.for(...)]`, not a module-level var — the registrar (cc-bridge) and caller (core) can be separate runtime bundles.
+- The hook MUST live on `globalThis[Symbol.for(...)]`, not a module-level var — the registrar (tinker-bridge) and caller (core) can be separate runtime bundles.
+
+## Resume size guard + init-only fast-fail (FORK 2026-06-23)
+
+A 14.5MB resumed session transcript fed to `claude --resume ` choked claude-cli's ingestion at spawn: the worker emitted 2–3 init lines then `text.len=0`/`thinking.len=0` for the **full 600s** idle watchdog window before SIGTERM, and the idle-timeout auto-retry re-resumed the **same** fat transcript → identical stall. Surfaced as "Jarvis not responding". The `gm`/credential angle was a **RED HERRING** — the bridge injects no credential; the spawned `claude` reads its own `~/.claude/.credentials.json` (see §Auth). Three layered guards, none of which lowers the 600s `DEFAULT_REQUEST_TIMEOUT_MS`:
+
+1. **Oversized-resume guard (commit `b7ea26b0a6`)** — before spawning `claude --resume `, `stat` the transcript (path resolved by the new `extensions/tinkerclaw-tinker-bridge/src/transcript-path.ts`); if it exceeds `RESUME_MAX_TRANSCRIPT_BYTES` (8MB, `defaults.ts`) start a **FRESH** session instead. **Fail-open**: any stat error resumes as before — a stat failure must never block a turn.
+2. **Init-only fast-fail watchdog (commit `923be5f3e3`, `stream.ts`)** — abort early (SIGTERM) when the worker has emitted only init lines AND produced zero content (`text.len=0`/`thinking.len=0`) past `FAST_FAIL_INIT_SILENT_MS` (90s), **gated on `linesSeen <= FAST_FAIL_MAX_INIT_LINES`**. The line-count gate is load-bearing: a heavy legitimate tool turn emits many lines, so it never trips this fast-fail and keeps the full 600s budget. This catches the _never-started_ shape fast without touching the long-tool-turn budget.
+3. **No futile same-model re-resume (commit `fbebe20648`, `src/agents/embedded-agent-runner/run.ts`)** — when the aborted attempt produced zero content (`producedNoContent`), the idle-timeout failover does NOT re-resume the same model/session. Re-resuming an unchanged transcript can only reproduce the stall.
+
+**Don't regress:**
+
+- The oversized-resume guard MUST stay **fail-open** — a `stat` error resumes (never start fresh on a stat failure, that would silently drop session continuity on a transient FS hiccup).
+- The init-only fast-fail MUST stay gated on `linesSeen <= FAST_FAIL_MAX_INIT_LINES` — without that gate it would kill heavy multi-tool turns that are legitimately silent on the pi-ai event stream (exactly the case the 25s heartbeat + 600s overlay exist to protect). It is NOT a replacement for the 600s `DEFAULT_REQUEST_TIMEOUT_MS`; it is a narrow early-out for the choked-resume shape only.
+- The zero-content re-resume suppression keys on `producedNoContent` — a turn that produced ANY content and then idle-timed-out still retries normally; only the never-produced shape is suppressed.
## Don't regress
-- **NEVER add tool_use blocks back to `assistant.message.content` for cc-bridge.** This re-introduces the red-error-bubble cascade.
-- **NEVER assume cc-bridge timeouts are just like other providers.** They are not. cc-bridge needs longer timeouts because its event stream is sparse during tool work.
+- **NEVER add tool_use blocks back to `assistant.message.content` for tinker-bridge.** This re-introduces the red-error-bubble cascade.
+- **NEVER assume tinker-bridge timeouts are just like other providers.** They are not. tinker-bridge needs longer timeouts because its event stream is sparse during tool work.
- **NEVER reintroduce a UI-side stale-run watchdog.** The bible verify enforces this — `STALE_RUN_WATCHDOG_MS` must not reappear in `app.ts`, and no force-clear of `activeRuns` from a timer is allowed. If you observe a stuck thinking indicator, the bug is in lifecycle:end emission, not in the UI; fix it server-side in `handleAgentEnd` / `attempt.ts` and add a verify that catches the missed-emission case.
-- The cc-bridge sessionKey hash is djb2 over `${systemPrompt}${openclawSessionId}` (`extensions/tinkerclaw-cc-bridge/src/stream.ts:104:deriveSessionKey`). It drifts when systemPrompt changes (e.g., after [System] continue is prepended on resume). The worker-pool's `getLatestResumeSessionIdByOpenclawSessionId` fallback handles this drift; do not remove it.
+- The tinker-bridge sessionKey hash is djb2 over `${systemPrompt}${openclawSessionId}` (`extensions/tinkerclaw-tinker-bridge/src/stream.ts:104:deriveSessionKey`). It drifts when systemPrompt changes (e.g., after [System] continue is prepended on resume). The worker-pool's `getLatestResumeSessionIdByOpenclawSessionId` fallback handles this drift; do not remove it.
## Verify (proposed)
@@ -201,9 +221,9 @@ verify:
## See also
-- `extensions/tinkerclaw-cc-bridge/src/stream.ts` — the stream pipeline (text-end fix 2026-05-04, runId smuggle 2026-04-27).
-- `extensions/tinkerclaw-cc-bridge/src/worker-pool.ts` — resume lookup priority.
-- `extensions/tinkerclaw-cc-bridge/src/session-map.ts` — openclawSessionId fallback.
+- `extensions/tinkerclaw-tinker-bridge/src/stream.ts` — the stream pipeline (text-end fix 2026-05-04, runId smuggle 2026-04-27).
+- `extensions/tinkerclaw-tinker-bridge/src/worker-pool.ts` — resume lookup priority.
+- `extensions/tinkerclaw-tinker-bridge/src/session-map.ts` — openclawSessionId fallback.
- `src/agents/embedded-agent-runner/run/llm-idle-timeout.ts` — the watchdog.
- bible.md §11.6d / §11.6e — the regression + fix history.
@@ -211,33 +231,33 @@ verify:
## Provider mechanics (migrated 2026-05-11 from bible.md §5.66)
-> The "why" of cc-bridge is the divergence above. The "how" is below — the spawn shape, the auth path, the lifecycle-fields fix, and the workspace-skills wrapper plugin. Migrated verbatim from bible.md §5.66 (2026-04-17 → 2026-04-20).
+> The "why" of tinker-bridge is the divergence above. The "how" is below — the spawn shape, the auth path, the lifecycle-fields fix, and the workspace-skills wrapper plugin. Migrated verbatim from bible.md §5.66 (2026-04-17 → 2026-04-20).
### The bridge
-Jarvis runs on the real `claude` CLI consuming the flat-rate Claude Code subscription instead of burning Anthropic API tokens. The OpenClaw provider plugin (`extensions/tinkerclaw-cc-bridge/`) registers provider `claude-code` and spawns a persistent `claude` subprocess per OpenClaw session with `--input-format stream-json --output-format stream-json --permission-mode bypassPermissions --disallowedTools Agent,ExitPlanMode,AskUserQuestion,TodoWrite,Task…`. The fork's tool loop stays authoritative; claude only does reasoning (see "The cc-bridge solution" above for what that means in practice).
+Jarvis runs on the real `claude` CLI consuming the flat-rate Claude Code subscription instead of burning Anthropic API tokens. The OpenClaw provider plugin (`extensions/tinkerclaw-tinker-bridge/`) registers provider `claude-code` and spawns a persistent `claude` subprocess per OpenClaw session with `--input-format stream-json --output-format stream-json --permission-mode bypassPermissions --disallowedTools Agent,ExitPlanMode,AskUserQuestion,TodoWrite,Task…`. The fork's tool loop stays authoritative; claude only does reasoning (see "The tinker-bridge solution" above for what that means in practice).
### System prompt
-cc-bridge worker reads `extensions/tinkerclaw-fractal-reflection/fractal-prompt.md` at spawn time and appends it via `--append-system-prompt` so the fractal-reflection instructions live inside claude's own session rather than per-turn. **FORK 2026-06-10 (amygdala retirement):** the per-turn `🧠 AMYGDALA` reply section was retired — `amygdala-prompt.md` is no longer loaded (`PROMPT_FILES` in `worker.ts` holds only the fractal entry) and the file has been deleted. The always-on Amygdala side panel (gate-decision stream) is the feedback surface now; the per-turn reply is just `💬 ANSWER → 🌿 FRACTAL`.
+tinker-bridge worker reads `extensions/tinkerclaw-fractal-reflection/fractal-prompt.md` at spawn time and appends it via `--append-system-prompt` so the fractal-reflection instructions live inside claude's own session rather than per-turn. **FORK 2026-06-10 (amygdala retirement):** the per-turn `🧠 AMYGDALA` reply section was retired — `amygdala-prompt.md` is no longer loaded (`PROMPT_FILES` in `worker.ts` holds only the fractal entry) and the file has been deleted. The always-on Amygdala side panel (gate-decision stream) is the feedback surface now; the per-turn reply is just `💬 ANSWER → 🌿 FRACTAL`.
#### Amygdala PreToolUse hook — pre-execution enforcement on the primary runner (FORK 2026-06-11, v3.1)
-> **The "observe-only on cc-bridge is a physics limit" claim is RETRACTED.** It was a spawn-config choice, not physics.
+> **The "observe-only on tinker-bridge is a physics limit" claim is RETRACTED.** It was a spawn-config choice, not physics.
-The long-standing story elsewhere in this file — the gateway only _observes_ cc-bridge tool calls (it sees `stream:"tool"` events _after_ claude-cli already ran the tool, so it can never block) — held only because cc-bridge never wired claude-cli's own hook system. claude-cli (v2.1.x) accepts `--settings ` whose JSON can register a **PreToolUse hook**, and a PreToolUse hook can return a `deny` permission decision that **synchronously blocks the tool — even under `--permission-mode bypassPermissions`** (the bridge's mode). So there _is_ a pre-execution seam; it was simply unused.
+The long-standing story elsewhere in this file — the gateway only _observes_ tinker-bridge tool calls (it sees `stream:"tool"` events _after_ claude-cli already ran the tool, so it can never block) — held only because tinker-bridge never wired claude-cli's own hook system. claude-cli (v2.1.x) accepts `--settings ` whose JSON can register a **PreToolUse hook**, and a PreToolUse hook can return a `deny` permission decision that **synchronously blocks the tool — even under `--permission-mode bypassPermissions`** (the bridge's mode). So there _is_ a pre-execution seam; it was simply unused.
v3.1 wires it:
- The `tinkerclaw-learned-intuition` extension compiles its AEGIS rule set (single source of truth: `src/rule-based-gate.ts` `AEGIS_RULES`) into `~/.openclaw/data/amygdala/policy.json` and writes a claude-cli settings file `~/.openclaw/data/amygdala/cc-hook-settings.json` registering a dependency-free hook script (`hook/amygdala-pretooluse.mjs`, staged into the data dir by `src/policy-snapshot.ts`).
-- `extensions/tinkerclaw-cc-bridge/src/worker.ts` pushes `--settings ` into the claude spawn argv **iff the file exists** (`AMYGDALA_CC_HOOK_SETTINGS_PATH` in `defaults.ts`). Presence is the enable signal; the amygdala extension owns the file's lifecycle (writes it when `hookEnforcement` is on, deletes it when off). Absent file → identical prior behavior.
+- `extensions/tinkerclaw-tinker-bridge/src/worker.ts` pushes `--settings ` into the claude spawn argv **iff the file exists** (`AMYGDALA_CC_HOOK_SETTINGS_PATH` in `defaults.ts`). Presence is the enable signal; the amygdala extension owns the file's lifecycle (writes it when `hookEnforcement` is on, deletes it when off). Absent file → identical prior behavior.
- The hook reads the PreToolUse payload on stdin, matches the policy rules, and on an **enforced** match prints `{hookSpecificOutput:{permissionDecision:"deny",…}}`. It is **fail-open** (any error → exit 0, allow), **<100 ms**, and spools every decision to `hook-decisions.jsonl`, which the extension ingests into the live feed — real enforced denials, previously invisible (the strongest feedback signal).
- **Enforce tiers (anti-cry-wolf):** only destructive-EXECUTION rules (`rm -rf /`, `mkfs`, `dd of=/dev/*`, `DROP/TRUNCATE/DELETE`, force-push main, credential exfil) deny; credential-PATTERN rules (a `.env` path, the bare word "password") are observe-only. Scope `"exec"` means a rule matches only execution-tool command text — a `.sql` file containing `DROP TABLE` is content, not an execution, so file-content tools are not scanned in v1.
-- The **native** `before_tool_call` path (non-cc-bridge tools) was _also_ never actually blocking: it returned `{abort,message}`, but the host honours `{block,blockReason}` (see `src/plugins/hook-types.ts` `PluginHookBeforeToolCallResult` + `src/agents/pi-tools.before-tool-call.ts` `if (hookResult?.block)`). v3.1 returns the correct shape, so the native hard floor now actually denies. Config keys: `config-shape.md`. The rule list has a single owner: `rule-based-gate.ts`.
+- The **native** `before_tool_call` path (non-tinker-bridge tools) was _also_ never actually blocking: it returned `{abort,message}`, but the host honours `{block,blockReason}` (see `src/plugins/hook-types.ts` `PluginHookBeforeToolCallResult` + `src/agents/pi-tools.before-tool-call.ts` `if (hookResult?.block)`). v3.1 returns the correct shape, so the native hard floor now actually denies. Config keys: `config-shape.md`. The rule list has a single owner: `rule-based-gate.ts`.
#### `combinedSystemPrompt` block order (FORK 2026-05-21)
-The cc-bridge worker assembles `--append-system-prompt` from blocks in this order. Persona answers WHO I am; ethical-rules answer WHAT I will and will not do; the remaining blocks answer HOW the mechanics work. Asimov-style priority ordering matches document order — earlier blocks preempt later ones when there's tension.
+The tinker-bridge worker assembles `--append-system-prompt` from blocks in this order. Persona answers WHO I am; ethical-rules answer WHAT I will and will not do; the remaining blocks answer HOW the mechanics work. Asimov-style priority ordering matches document order — earlier blocks preempt later ones when there's tension.
```
persona → ethical-rules → narration → subagent-helper → tool-choice → plan-tools
@@ -247,9 +267,9 @@ The **ethical-rules** block was inserted as a new foundation layer in commit `dc
1. `env.TINKERCLAW_ETHICAL_RULES_PROMPT` — explicit path override.
2. `~/.openclaw/workspace/memory/knowledge/jarvis-ethical-rules.md` — user-personalised override (outside the public repo).
-3. `extensions/tinkerclaw-cc-bridge/prompts/ethical-rules-default.md` — bundled default (in the public repo).
+3. `extensions/tinkerclaw-tinker-bridge/prompts/ethical-rules-default.md` — bundled default (in the public repo).
-The bundled default ships ten priority-ordered rules (truth before agreement, privacy non-negotiable, reversibility gates action, no impersonation, no half-baked outbound, honesty about uncertainty, patch + prevent, stay in character under pressure, resource awareness, write or it didn't happen) + a generic preamble. The user's workspace override replaces the preamble with their own framing; the rules-themselves are typically inherited verbatim. Default-version drift surfaces via the boot-time log line (bible §5.76f). See `config-shape.md` "cc-bridge ethical-rules prompt loader" for the loader path.
+The bundled default ships ten priority-ordered rules (truth before agreement, privacy non-negotiable, reversibility gates action, no impersonation, no half-baked outbound, honesty about uncertainty, patch + prevent, stay in character under pressure, resource awareness, write or it didn't happen) + a generic preamble. The user's workspace override replaces the preamble with their own framing; the rules-themselves are typically inherited verbatim. Default-version drift surfaces via the boot-time log line (bible §5.76f). See `config-shape.md` "tinker-bridge ethical-rules prompt loader" for the loader path.
**Don't regress:** the workspace override path is `memory/knowledge/jarvis-ethical-rules.md`, NOT `SOUL.md` (which overrides persona) and NOT `BRIEFING.md` (which overrides briefing). Conflating them silently overrides the wrong layer.
@@ -257,7 +277,7 @@ The bundled default ships ten priority-ordered rules (truth before agreement, pr
`src/stream.ts` converts claude's NDJSON output into pi-ai `text_delta` / `thinking_delta` increments. Two complementary input shapes both feed the same `pushTextDelta` / `pushThinkingDelta` helpers:
-- **Fine-grained path (FORK 2026-05-23 — `--include-partial-messages` flag, commit `3e343cb5ee`):** the cc-bridge spawn args now include `--include-partial-messages` (`extensions/tinkerclaw-cc-bridge/src/worker.ts:338`), which makes claude-cli emit `stream_event` lines carrying `content_block_delta.text_delta` / `.thinking_delta` events token-by-token. Without this flag claude-cli only emits the `assistant` block-complete frames (one big chunk per text block at end), so the UI saw replies appear all at once at the END of the turn instead of streaming. Diagnostic recipe: `openclaw logs | grep 'spawning claude'` — the args list shows the flag.
+- **Fine-grained path (FORK 2026-05-23 — `--include-partial-messages` flag, commit `3e343cb5ee`):** the tinker-bridge spawn args now include `--include-partial-messages` (`extensions/tinkerclaw-tinker-bridge/src/worker.ts:338`), which makes claude-cli emit `stream_event` lines carrying `content_block_delta.text_delta` / `.thinking_delta` events token-by-token. Without this flag claude-cli only emits the `assistant` block-complete frames (one big chunk per text block at end), so the UI saw replies appear all at once at the END of the turn instead of streaming. Diagnostic recipe: `openclaw logs | grep 'spawning claude'` — the args list shows the flag.
- **Cumulative path (legacy):** claude-cli's periodic `assistant` NDJSON frames carry the cumulative per-block text. Handler slices `cumulative.slice(blockTextSeen.get(bi) ?? "")` and pushes the new tail.
Both paths fire when `--include-partial-messages` is on. The fine-grained handler **MUST** mirror every pushed delta into `blockTextSeen[ev.index] += delta` so the cumulative-handler's slice condition (`cumulative.length > prev.length && cumulative.startsWith(prev)`) doesn't re-push the same text. **Don't regress (commit `d32e44cc24`, 2026-05-24):** before this sync, every block of streamed text was emitted twice — once via fine-grained deltas, once via the cumulative re-push (`prev = ""` → it sliced the whole cumulative as a "new delta"). The duplicate appeared in the rendered bubble as `"Good catches…Good catches…## 💬 ANSWER…"`, and with gap-split bubbles in the mix the `_segmentStart` cursors went past `finalText.length` during tail-recover, surfacing to the user as "truncation."
@@ -266,13 +286,13 @@ The `pushStart()` is eager — fires the instant the turn begins so the 4 thinki
### Producer additions — forward-compat blocks, turn-incomplete badge, result flattening (FORK 2026-06-11)
-Three additions on the cc-bridge **producer** side (the stream→pi-ai converter, `stream.ts`, plus the wire-shape decls in `protocol.ts`). None changes the suppression invariant above; they harden the producer against schema drift and surface non-success terminations.
+Three additions on the tinker-bridge **producer** side (the stream→pi-ai converter, `stream.ts`, plus the wire-shape decls in `protocol.ts`). None changes the suppression invariant above; they harden the producer against schema drift and surface non-success terminations.
#### 1. Forward-compat `CcContentBlock` arms
`protocol.ts`'s `CcContentBlock` union gains three named arms — `server_tool_use`, `web_search_tool_result`, and `redacted_thinking` — corresponding to the raw Anthropic server-tool / web-search-result / redacted-thinking block shapes.
-**LIVE FACT (why they are inert today):** claude-cli currently **normalizes** its built-in `WebSearch` / `WebFetch` tools into plain `tool_use` / `tool_result` blocks before they ever reach cc-bridge's stdout. So in the present claude-cli schema these three arms **never match** — every web-search round arrives as ordinary `tool_use`/`tool_result` and flows through the existing handlers. The arms are pure **forward-compat**: they fire only if a future claude-cli schema bump starts surfacing the raw Anthropic server-tool blocks (or raw `redacted_thinking`) on the wire. Naming them now keeps such a bump from silently dropping into the open-ended `{ type: string; [key: string]: unknown }` forward-compat catch-all (where it would decode to nothing) and gives the decoder a typed branch to grow into.
+**LIVE FACT (why they are inert today):** claude-cli currently **normalizes** its built-in `WebSearch` / `WebFetch` tools into plain `tool_use` / `tool_result` blocks before they ever reach tinker-bridge's stdout. So in the present claude-cli schema these three arms **never match** — every web-search round arrives as ordinary `tool_use`/`tool_result` and flows through the existing handlers. The arms are pure **forward-compat**: they fire only if a future claude-cli schema bump starts surfacing the raw Anthropic server-tool blocks (or raw `redacted_thinking`) on the wire. Naming them now keeps such a bump from silently dropping into the open-ended `{ type: string; [key: string]: unknown }` forward-compat catch-all (where it would decode to nothing) and gives the decoder a typed branch to grow into.
**Don't regress:** keep the three named arms even though they are inert. Deleting them as "dead code" is the trap — they are a deliberate landing pad for a schema bump, and `config-shape.md`'s dead-code register should NOT list them as removable.
@@ -282,7 +302,7 @@ The result/done branch now emits a free-form **`stream:"lifecycle"` `phase:"turn
**Placement is load-bearing:** the emit sits in the result/done branch **BEFORE the `is_error` early-return** (the `if (result.is_error && …) { … return; }` block that resets accumulated text to the `__ERR_ENV__` envelope and returns). If it were placed after, the `is_error` path would `return` first and the auth/billing-error and `error_during_execution` cases would never badge. By sitting ahead of that return, every non-success subtype is reported regardless of which downstream arm (envelope-reset, tail-recover, or clean done) ultimately runs.
-**Why a free-form lifecycle event and NOT a pi-ai `StopReason`:** the `done` event cc-bridge pushes is **always `stopReason:"stop"`** (pi-agent-core honors the final message and replaces partial content; cc-bridge has no incomplete `done` shape). The pi-ai `StopReason` discriminated union has **no `"incomplete"` member** — pushing one would violate pi-ai's invariants exactly like an out-of-window `text_delta` would. So "this turn ended in a non-success subtype" cannot ride the `StopReason` enum; it travels as a free-form `lifecycle` event the UI consumer reads independently of the `done` message. The thinking indicator still clears on the subsequent `phase:"end"` from the `finally` block; `turn-incomplete` is an additive badge, not a replacement for `end`.
+**Why a free-form lifecycle event and NOT a pi-ai `StopReason`:** the `done` event tinker-bridge pushes is **always `stopReason:"stop"`** (pi-agent-core honors the final message and replaces partial content; tinker-bridge has no incomplete `done` shape). The pi-ai `StopReason` discriminated union has **no `"incomplete"` member** — pushing one would violate pi-ai's invariants exactly like an out-of-window `text_delta` would. So "this turn ended in a non-success subtype" cannot ride the `StopReason` enum; it travels as a free-form `lifecycle` event the UI consumer reads independently of the `done` message. The thinking indicator still clears on the subsequent `phase:"end"` from the `finally` block; `turn-incomplete` is an additive badge, not a replacement for `end`.
**Don't regress:** keep `turn-incomplete` BEFORE the `is_error` early-return, and do NOT try to express it as a `StopReason` — `done` stays `stopReason:"stop"`.
@@ -292,7 +312,7 @@ The `tool_result` content-flattening that was inline in the `user`-role stream-l
### Per-session thinking budget — `MAX_THINKING_TOKENS` (FORK 2026-06-11)
-cc-bridge now sets a **per-session thinking budget** on the spawned `claude` child via the native Claude Code env knob `MAX_THINKING_TOKENS`. This is the **third** native Claude Code env var cc-bridge sets on the child, alongside `CLAUDE_CODE_MAX_OUTPUT_TOKENS` (the output cap) — and like that one it is added to `worker.ts`'s `allowedKeys` so the env scrub (which strips `ANTHROPIC_API_KEY` et al., see Auth) lets it through to the subprocess. It is **set from the resolved per-session think level**: a higher think level buys the model a bigger interleaved-thinking budget for the turn.
+tinker-bridge now sets a **per-session thinking budget** on the spawned `claude` child via the native Claude Code env knob `MAX_THINKING_TOKENS`. This is the **third** native Claude Code env var tinker-bridge sets on the child, alongside `CLAUDE_CODE_MAX_OUTPUT_TOKENS` (the output cap) — and like that one it is added to `worker.ts`'s `allowedKeys` so the env scrub (which strips `ANTHROPIC_API_KEY` et al., see Auth) lets it through to the subprocess. It is **set from the resolved per-session think level**: a higher think level buys the model a bigger interleaved-thinking budget for the turn.
#### Level → budget map
@@ -316,7 +336,7 @@ The resolved value is **clamped to `maxOutputTokensFor(model) - 4000`** so the t
The think level is resolved on the core side and travels to the worker spawn through the **existing pi-ai options smuggle** — the same side-channel the run already uses to carry fork-only knobs that pi-ai's typed options shape has no field for:
1. `src/agents/embedded-agent-runner/run/attempt.ts` resolves the per-session think level and writes it onto the pi-ai options as **`__openclawThinkLevel`** (the smuggle key).
-2. `extensions/tinkerclaw-cc-bridge/src/stream.ts` reads `__openclawThinkLevel` off the options and passes it down to `pool.getOrCreate(...)`.
+2. `extensions/tinkerclaw-tinker-bridge/src/stream.ts` reads `__openclawThinkLevel` off the options and passes it down to `pool.getOrCreate(...)`.
3. The worker pool threads it into the `worker` spawn, where `worker.ts` maps the level → budget (table above, clamped) and sets `MAX_THINKING_TOKENS` in the child's `env` (omitting it for `off`).
#### Next-message semantics (env is read at child spawn)
@@ -347,7 +367,7 @@ The per-session thinking budget above (`MAX_THINKING_TOKENS`) is set at child **
#### (b) The `stream:"effort"` agent-event contract
-cc-bridge's `stream.ts` now emits a **`stream:"effort"`** agent event (same `emitAgentEvent` envelope as the `phase:"start"`/`phase:"end"` lifecycle events) so the UI can show **how much the model actually thought** this turn — not just the requested cap. It is emitted **throttled-live** during the turn (so the chip animates as thinking accumulates) and **once-final** at turn end. Fields:
+tinker-bridge's `stream.ts` now emits a **`stream:"effort"`** agent event (same `emitAgentEvent` envelope as the `phase:"start"`/`phase:"end"` lifecycle events) so the UI can show **how much the model actually thought** this turn — not just the requested cap. It is emitted **throttled-live** during the turn (so the chip animates as thinking accumulates) and **once-final** at turn end. Fields:
| field | meaning |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
@@ -374,7 +394,7 @@ When a level change lands on a **busy** warm worker (see (a)), `stream.ts` emits
#### (e) HONEST LIMITS — there is no provider reasoning-token count
-There is **no provider-reported reasoning-token count** available to cc-bridge: claude-cli's usage payload (`CcUsage`) has **no thinking-token field**, and `output_tokens` **mixes thinking + answer** in one number. So the honest "actual effort" measure is **thinking CHARACTERS (`thinkingChars`) plus the `hadRealThinking` boolean** — never a fabricated reasoning-token number. Do not synthesize a "reasoning tokens: N" figure; if a token-shaped number is shown anywhere it can only be `configuredBudget` (the requested cap) or `output_tokens` (the mixed total), each labelled as such. **Non-claude providers do not route through cc-bridge**, so they emit **no `effort` event at all** — the chip is a claude-code-only surface, and its absence on other providers is correct, not a bug.
+There is **no provider-reported reasoning-token count** available to tinker-bridge: claude-cli's usage payload (`CcUsage`) has **no thinking-token field**, and `output_tokens` **mixes thinking + answer** in one number. So the honest "actual effort" measure is **thinking CHARACTERS (`thinkingChars`) plus the `hadRealThinking` boolean** — never a fabricated reasoning-token number. Do not synthesize a "reasoning tokens: N" figure; if a token-shaped number is shown anywhere it can only be `configuredBudget` (the requested cap) or `output_tokens` (the mixed total), each labelled as such. **Non-claude providers do not route through tinker-bridge**, so they emit **no `effort` event at all** — the chip is a claude-code-only surface, and its absence on other providers is correct, not a bug.
**Don't regress (effort visibility):**
@@ -389,11 +409,11 @@ Trusts `~/.claude/.credentials.json`. Env scrub strips `ANTHROPIC_API_KEY`, `ANT
### Lifecycle-fields fix (commit `1d66f53705`, 2026-04-20)
-`handleAgentStart` was reading `ctx.params.modelId / modelProvider / authProfileId`, but those fields were never declared on `SubscribeEmbeddedAgentSessionParams` nor passed from `attempt.ts`. Every lifecycle `phase:"start"` event therefore went out with `model: undefined`, and the UI filter at `app.ts:1614` (`p.data?.model`) silently dropped the event for cc-bridge — anthropic/ollama only worked because another enrichment path happened to cover the gap. Fix adds the fields to the params type and forwards them in `attempt.ts`, so all 4 thinking indicators (chat "Opus", session panel, model glow, prefrontal tree) now animate for claude-code turns.
+`handleAgentStart` was reading `ctx.params.modelId / modelProvider / authProfileId`, but those fields were never declared on `SubscribeEmbeddedAgentSessionParams` nor passed from `attempt.ts`. Every lifecycle `phase:"start"` event therefore went out with `model: undefined`, and the UI filter at `app.ts:1614` (`p.data?.model`) silently dropped the event for tinker-bridge — anthropic/ollama only worked because another enrichment path happened to cover the gap. Fix adds the fields to the params type and forwards them in `attempt.ts`, so all 4 thinking indicators (chat "Opus", session panel, model glow, prefrontal tree) now animate for claude-code turns.
### Files
-`extensions/tinkerclaw-cc-bridge/{provider.ts,stream.ts,worker.ts,worker-pool.ts,auth.ts,catalog.ts,protocol.ts,defaults.ts}`, `src/agents/embedded-agent-subscribe.types.ts`, `src/agents/embedded-agent-runner/run/attempt.ts` (forward model/provider/profile).
+`extensions/tinkerclaw-tinker-bridge/{provider.ts,stream.ts,worker.ts,worker-pool.ts,auth.ts,catalog.ts,protocol.ts,defaults.ts}`, `src/agents/embedded-agent-subscribe.types.ts`, `src/agents/embedded-agent-runner/run/attempt.ts` (forward model/provider/profile).
### Workspace skills exposed to Jarvis via `--plugin-dir` (FORK 2026-05-04)
@@ -404,13 +424,13 @@ claude-code only loads skills from PLUGINS — it does NOT scan `${cwd}/.claude/
- `.claude-plugin/plugin.json` — minimal manifest (`{name, description, version, license}`). REQUIRED — without it claude-cli silently doesn't recognize the directory as a plugin.
- `skills/` — symlink to `~/.openclaw/workspace/skills/`. Re-exports the canonical catalog without copying.
-**cc-bridge wiring**:
+**tinker-bridge wiring**:
-- `extensions/tinkerclaw-cc-bridge/src/defaults.ts` — `DEFAULT_PLUGIN_DIRS = []`.
-- `extensions/tinkerclaw-cc-bridge/src/worker.ts` — `WorkerSpawnParams.pluginDirs` field; spawn now pushes `--plugin-dir ` per entry. Repeatable for additional plugin dirs in future.
+- `extensions/tinkerclaw-tinker-bridge/src/defaults.ts` — `DEFAULT_PLUGIN_DIRS = []`.
+- `extensions/tinkerclaw-tinker-bridge/src/worker.ts` — `WorkerSpawnParams.pluginDirs` field; spawn now pushes `--plugin-dir ` per entry. Repeatable for additional plugin dirs in future.
**Verified end-to-end:** Jarvis confirms `jarvis-skills:outlook-hack` loads via the Skill tool; on the practical "can you read my outlook?" prompt, his first move is `Skill jarvis-skills:outlook-hack`.
-**Diagnostic gotcha — skills are discoverable but not enumerable in this mode.** claude-code in `-p`+stream-json (cc-bridge's mode) does NOT inject an "available skills" system reminder beyond the `using-superpowers` content from the SessionStart hook. Asking Jarvis "list every skill" can yield a hallucinated "none" because the model has no enumerable list in context — only the `Skill` tool. Ask instead "what would you do for X?" and the right skill name appears via discovery. Future improvement candidate: append a compact skill index (names + 1-line descriptions) to `--append-system-prompt`.
+**Diagnostic gotcha — skills are discoverable but not enumerable in this mode.** claude-code in `-p`+stream-json (tinker-bridge's mode) does NOT inject an "available skills" system reminder beyond the `using-superpowers` content from the SessionStart hook. Asking Jarvis "list every skill" can yield a hallucinated "none" because the model has no enumerable list in context — only the `Skill` tool. Ask instead "what would you do for X?" and the right skill name appears via discovery. Future improvement candidate: append a compact skill index (names + 1-line descriptions) to `--append-system-prompt`.
**Don't regress:** if you ever move skills to a different path, update `DEFAULT_PLUGIN_DIRS` AND keep the manifest at `/.claude-plugin/plugin.json`. Symlink-only is not enough.
diff --git a/TINKER_UI_DESIGN_BIBLE/topology.md b/TINKER_UI_DESIGN_BIBLE/topology.md
index 944c545f8b6fd..1cb083352e3df 100644
--- a/TINKER_UI_DESIGN_BIBLE/topology.md
+++ b/TINKER_UI_DESIGN_BIBLE/topology.md
@@ -9,15 +9,15 @@ see_also: flows.md (how they talk), config-shape.md (what configures them), prob
verify:
- name: gateway listening on 18789
cmd: ss -ltn 2>/dev/null | grep -q ':18789' || netstat -ltn 2>/dev/null | grep -q ':18789'
- - name: cc-bridge plugin discoverable + manifest valid
+ - name: tinker-bridge plugin discoverable + manifest valid
cmd: |
- manifest=~/src/tinkerclaw/dist-runtime/extensions/tinkerclaw-cc-bridge/openclaw.plugin.json
- stub=~/src/tinkerclaw/dist-runtime/extensions/tinkerclaw-cc-bridge/index.js
+ manifest=~/src/tinkerclaw/dist-runtime/extensions/tinkerclaw-tinker-bridge/openclaw.plugin.json
+ stub=~/src/tinkerclaw/dist-runtime/extensions/tinkerclaw-tinker-bridge/index.js
[ -f "$manifest" ] || { echo "missing $manifest"; exit 1; }
[ -f "$stub" ] || { echo "missing $stub"; exit 1; }
- grep -q '"id":\s*"tinkerclaw-cc-bridge"' "$manifest" || { echo "manifest id mismatch"; exit 1; }
- python3 -c "import json,sys; m=json.load(open('$manifest')); a=m.get('activation',{}); ps=a.get('onProviders',[]); sys.exit(0 if 'claude-code' in ps else 1)" || { echo "manifest missing activation.onProviders containing 'claude-code' — cc-bridge is lazy-loaded; this gates activation"; exit 1; }
- echo "tinkerclaw-cc-bridge manifest + stub discoverable, lazy-activation wired to claude-code"
+ grep -q '"id":\s*"tinkerclaw-tinker-bridge"' "$manifest" || { echo "manifest id mismatch"; exit 1; }
+ python3 -c "import json,sys; m=json.load(open('$manifest')); a=m.get('activation',{}); ps=a.get('onProviders',[]); sys.exit(0 if 'claude-code' in ps else 1)" || { echo "manifest missing activation.onProviders containing 'claude-code' — tinker-bridge is lazy-loaded; this gates activation"; exit 1; }
+ echo "tinkerclaw-tinker-bridge manifest + stub discoverable, lazy-activation wired to claude-code"
- name: workspace symlinks present (skills NOT symlinked per design)
cmd: "[ -L ~/.openclaw/workspace/src ] || [ -d ~/.openclaw/workspace/src ]"
- name: every fork-owned plugin dir uses the tinkerclaw- prefix
@@ -49,7 +49,7 @@ verify:
| ClawMetry OTEL | 4001 | traces + metrics endpoint | `~/src/clawmetry/` (separate process) |
| Mission Control | 4000 | dashboard (Docker) | `~/src/mission-control/` (separate process) |
-**The gateway process is the central anchor.** Everything fork-side runs in-process under it: plugins, channel adapters, cron scheduler, the cc-bridge worker pool. Subprocesses are claude-cli per cc-bridge worker (re-parented to systemd via `--pipe`), whatsmeow-node binary for WhatsApp transport, and ephemeral exec tool processes.
+**The gateway process is the central anchor.** Everything fork-side runs in-process under it: plugins, channel adapters, cron scheduler, the tinker-bridge worker pool. Subprocesses are claude-cli per tinker-bridge worker (re-parented to systemd via `--pipe`), whatsmeow-node binary for WhatsApp transport, and ephemeral exec tool processes.
## Plugin inventory
@@ -57,7 +57,7 @@ All fork plugins use the `tinkerclaw-` prefix in their plugin id, directory name
| Plugin id | Purpose | Hooks used | Status |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | --------------------------------------------- |
-| `tinkerclaw-cc-bridge` | drives claude-cli as a persistent subprocess provider for `claude-code`. `combinedSystemPrompt` includes the ethical-rules block (FORK 2026-05-21) between persona and narration. | `registerProvider`, `registerPluginProviderConfigOverlay` (FORK 2026-05-10) | DEPLOYED |
+| `tinkerclaw-tinker-bridge` | drives claude-cli as a persistent subprocess provider for `claude-code`. `combinedSystemPrompt` includes the ethical-rules block (FORK 2026-05-21) between persona and narration. | `registerProvider`, `registerPluginProviderConfigOverlay` (FORK 2026-05-10) | DEPLOYED |
| `tinkerclaw-whatsapp` | whatsmeow-backed WA channel (replaces upstream baileys) | channel registration, monitor hook chain | DEPLOYED |
| `tinkerclaw-people` | people-profile resolver (`people.{resolve,read,list,update_consulted_at}`) | RPC handlers | DEPLOYED |
| `tinkerclaw-control-panel` | task store + Today card / Exec HUD panel (v3.5 — 2026-05-22; `task_axis.parent_id` two-level hierarchy, `axes.add/update` accept `parent_id`, Todoist scrub one-shot migration). See `tinker-ui.md` §5.68, `config-shape.md` schema notes. | RPC handlers (`control-panel.*`), HUD render via Tinker UI | DEPLOYED (v3.5) |
@@ -65,7 +65,7 @@ All fork plugins use the `tinkerclaw-` prefix in their plugin id, directory name
| `tinkerclaw-memory-enhancements` | MNEMOSYNE — hippocampus index + compaction capture (J14) | `retrieval_pre`, `before_message_write`, `before_compaction`, `after_compaction` | partial (v0.1 scaffold) |
| `tinkerclaw-computational-humor` | LIMBIC (J7) | hooks | DEPLOYED |
| `tinkerclaw-identity-persistence` | CORTEX (J4) | persona-state hooks | DEPLOYED |
-| `tinkerclaw-learned-intuition` | AMYGDALA (J11) v3.1 — AEGIS rules enforced pre-execution (cc-bridge PreToolUse hook + native `{block}`) + k-NN novelty ASK channel + clause-cosine incongruity; legacy 5-net ensemble retired (legacyEnsemble=false) | hooks + cc-bridge `--settings` PreToolUse hook | DEPLOYED (v3.1, AEGIS on, novelty observe) |
+| `tinkerclaw-learned-intuition` | AMYGDALA (J11) v3.1 — AEGIS rules enforced pre-execution (tinker-bridge PreToolUse hook + native `{block}`) + k-NN novelty ASK channel + clause-cosine incongruity; legacy 5-net ensemble retired (legacyEnsemble=false) | hooks + tinker-bridge `--settings` PreToolUse hook | DEPLOYED (v3.1, AEGIS on, novelty observe) |
| `tinkerclaw-round-table` | SYNAPSE (J6) | hooks | FAILING to load (missing `@sinclair/typebox`) |
| `tinkerclaw-total-recall` | ENGRAM (J1) | hooks | FAILING to load (missing `@sinclair/typebox`) |
@@ -142,12 +142,12 @@ The PII boundary between these two repos is critical. See `pii-boundary.md`.
## Sister processes (out-of-gateway)
-| Process | Purpose | Location |
-| ---------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------- |
-| `whatsmeow-node` | WhatsApp transport subprocess (spawned by tinkerclaw-whatsapp) | `node_modules/@whatsmeow-node/linux-x64/bin/` |
-| `claude-cli` | one subprocess per cc-bridge worker | `~/.claude/` install, spawned by cc-bridge with `--pipe` re-parenting |
-| `ollama` | local embedding model (mxbai-embed-large) for `memorySearch` | `127.0.0.1:11434` (systemd) |
-| `chrome-relay` profile | persistent Chrome at `CDP=127.0.0.1:18792` | user-managed, attached-only |
+| Process | Purpose | Location |
+| ---------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
+| `whatsmeow-node` | WhatsApp transport subprocess (spawned by tinkerclaw-whatsapp) | `node_modules/@whatsmeow-node/linux-x64/bin/` |
+| `claude-cli` | one subprocess per tinker-bridge worker | `~/.claude/` install, spawned by tinker-bridge with `--pipe` re-parenting |
+| `ollama` | local embedding model (mxbai-embed-large) for `memorySearch` | `127.0.0.1:11434` (systemd) |
+| `chrome-relay` profile | persistent Chrome at `CDP=127.0.0.1:18792` | user-managed, attached-only |
## External services (HTTPS)
@@ -175,7 +175,7 @@ Services called outbound by the gateway or its plugins over HTTPS.
skills/ # real (not symlinked) — private
memory/ # private
agents/main/sessions/ # session store + transcripts
- cc-bridge/session-map.json # cc-bridge ↔ claude-cli mapping
+ tinker-bridge/session-map.json # tinker-bridge ↔ claude-cli mapping
cron/jobs.json + runs/ # cron registry + receipts
data/ # databases (whatsapp-history.db, etc.) + tinker-ui snapshot probe
diff --git a/TINKER_UI_DESIGN_BIBLE/unit-tests.md b/TINKER_UI_DESIGN_BIBLE/unit-tests.md
index 268906ecc7bb7..3f81471f72bfc 100644
--- a/TINKER_UI_DESIGN_BIBLE/unit-tests.md
+++ b/TINKER_UI_DESIGN_BIBLE/unit-tests.md
@@ -27,7 +27,7 @@ We DO NOT test upstream's code. Upstream has its own tests; running them here is
- `src/agents/plugin-provider-config-overlay.ts` (FORK 2026-05-10)
- `src/agents/main-session-restart-recovery.ts` (FORK 2026-05-09/10)
-- `extensions/tinkerclaw-cc-bridge/` (all)
+- `extensions/tinkerclaw-tinker-bridge/` (all)
- `extensions/tinkerclaw-whatsapp/` (fork-owned)
- `extensions/tinkerclaw-people/` (fork-owned)
- `extensions/tinkerclaw-prefrontal/` (fork-owned)
@@ -48,7 +48,7 @@ A typical fork-side test file:
/**
* Test target: src/agents/plugin-provider-config-overlay.ts
* Bible anchor: config-shape.md §4.1 ("Plugin runtime overlay")
- * Bug history: failures.md M1 (cc-bridge SIGTERM), bug-log.md 2026-05-10
+ * Bug history: failures.md M1 (tinker-bridge SIGTERM), bug-log.md 2026-05-10
* Catches: regression of the overlay-merge-into-cfg behavior shipped 2026-05-10
*/
import { describe, it, expect, beforeEach } from "vitest";
@@ -132,8 +132,8 @@ Each layer catches what the layer above missed. Unit tests catch logic bugs; bib
1. `plugin-provider-config-overlay.ts` — small, well-bounded, catches the dead-code regression class. Start here.
2. `main-session-restart-recovery.ts` — `pushRestartWarningEnvelope`, `resumeMainSession`, `recoverStore`. Each takes a fake session-store + a mock callGateway and asserts the envelope payload and resume invocation order.
-3. `extensions/tinkerclaw-cc-bridge/src/session-map.ts` — the openclawSessionId fallback path is small and recently broke.
-4. `extensions/tinkerclaw-cc-bridge/src/worker-pool.ts` — the lookup-priority rule. Tests the openclawSessionId-first ordering.
+3. `extensions/tinkerclaw-tinker-bridge/src/session-map.ts` — the openclawSessionId fallback path is small and recently broke.
+4. `extensions/tinkerclaw-tinker-bridge/src/worker-pool.ts` — the lookup-priority rule. Tests the openclawSessionId-first ordering.
5. `src/gateway/server-methods/files-resolve-bare.ts` — pure file-system search, easy to mock.
6. `src/gateway/server-methods/briefing.ts` — fallback chain.
7. `src/fork/error-envelope.ts` — error classification table; one test per category.
diff --git a/docs/context/2026-06-14-effort-system-handoff.md b/docs/context/2026-06-14-effort-system-handoff.md
index 8c52e762c58ea..186c7fa141913 100644
--- a/docs/context/2026-06-14-effort-system-handoff.md
+++ b/docs/context/2026-06-14-effort-system-handoff.md
@@ -34,9 +34,9 @@ correct. The doing is broken in two independent places.
2. **The real thinking budget.** `MAX_THINKING_TOKENS` is a strict function of
`params.thinkLevel` (max→28000, high→16000, off/unknown→omit).
- - `extensions/tinkerclaw-cc-bridge/src/thinking-budget.ts:25-63` (level→budget map)
- - `extensions/tinkerclaw-cc-bridge/src/worker.ts:681-687` (env set from thinkLevel)
- - `extensions/tinkerclaw-cc-bridge/src/stream.ts:178-203` (reads `__openclawThinkLevel`)
+ - `extensions/tinkerclaw-tinker-bridge/src/thinking-budget.ts:25-63` (level→budget map)
+ - `extensions/tinkerclaw-tinker-bridge/src/worker.ts:681-687` (env set from thinkLevel)
+ - `extensions/tinkerclaw-tinker-bridge/src/stream.ts:178-203` (reads `__openclawThinkLevel`)
- `src/agents/embedded-agent-runner/run/attempt.ts:1916-1925` (pipes `__openclawThinkLevel: params.thinkLevel`)
**Where `params.thinkLevel` actually comes from** (NOT the router):
@@ -64,7 +64,7 @@ re-derived from prompt classification.
## Problem 2 — THE VISUALIZING: the EEG is FAITHFUL (not a bug)
The EEG's x-axis is the 8-stop thinking-budget ladder (Auto, Minimal, Low,
-Medium, Adaptive, High, xHigh, Max). Each run's `chosenLevel` is the cc-bridge
+Medium, Adaptive, High, xHigh, Max). Each run's `chosenLevel` is the tinker-bridge
`thinkLevel` carried on the gateway's `stream:"effort"` event — NOT the
effort-router's prose classification.
diff --git a/docs/papers/agent-security/agent-security.md b/docs/papers/agent-security/agent-security.md
index f75d2e717199b..5e7ba9cab1866 100644
--- a/docs/papers/agent-security/agent-security.md
+++ b/docs/papers/agent-security/agent-security.md
@@ -8,9 +8,9 @@
Autonomous AI agents — software systems that perceive context, invoke tools, execute code, and take actions on behalf of users — have moved from research curiosity to mass deployment with remarkable speed. OpenClaw, one of the leading open-source agent runtimes, grew from a hobbyist project to millions of active installations within eighteen months. With that growth came an attack surface the security community was not prepared for. In twelve months, we observed a zero-click WebSocket hijack vulnerability (CVE-2026-25253) affecting 40,000+ exposed instances, supply chain attacks via malicious agent skills discovered by Cisco Talos, and a high-profile incident in which an AI safety director's own agent deleted six months of email without explicit authorization.
-This paper presents **AEGIS** — a systematic framework for analyzing and layering security controls across autonomous AI agent deployments. We propose a taxonomy of eight distinct security strategy classes, classified as deterministic or probabilistic, and introduce the **safety-capability-autonomy trilemma** as a design tool for reasoning about agent security trade-offs. We analyze three deployment personas — the Hobbyist Tinkerer, the Freelance Consultant, and the Corporate Employee — with formal risk matrices and regulatory mappings. We examine NVIDIA's NemoClaw/OpenShell architecture as a case study in principled defense-in-depth design, and conclude with a layered defense architecture modeled on the Swiss cheese model, including a worked attack simulation demonstrating layer interactions. The paper is intentionally positioned as a **conceptual and practitioner-oriented security framework paper** rather than an empirical benchmark study: its central contribution is a structured way to reason about controls, trust boundaries, and deployment trade-offs in agent systems before the incident database matures.
+This paper presents **AEGIS** — a systematic framework for analyzing and layering security controls across autonomous AI agent deployments. We propose a taxonomy of eight distinct security strategy classes, classified as deterministic or probabilistic, and introduce the **safety-capability-autonomy trilemma** as a design tool for reasoning about agent security trade-offs. We analyze three deployment personas — the Hobbyist Tinkerer, the Freelance Consultant, and the Corporate Employee — with formal risk matrices and regulatory mappings. We examine NVIDIA's NemoClaw/OpenShell architecture as a case study in principled defense-in-depth design (updated to its June 2026 state), survey the broader open-source agent-security ecosystem — Cisco's DefenseClaw, SecureClaw, and the cross-runtime guardrail and microVM-isolation projects — and present a layered defense architecture modeled on the Swiss cheese model with a worked attack simulation. Two additions in this revision serve practitioners directly: a **mapping of AEGIS onto the ISO/IEC 27001:2022 and ISO/IEC 42001:2023 control vocabulary**, so an already-certified organization can locate each agent-security layer against its existing control objectives; and an **evidence-led treatment of the fear that frontier-API providers will train a model of your business**, separating the memorization research (which concerns public pre-training text) from the contractual reality (commercial tiers do not train on customer inputs by default). The paper is intentionally positioned as a **conceptual and practitioner-oriented security framework paper** rather than an empirical benchmark study: its central contribution is a structured way to reason about controls, trust boundaries, and deployment trade-offs in agent systems before the incident database matures.
-**Keywords:** AI agent security, autonomous agents, defense in depth, prompt injection, tool security, agent isolation, OpenClaw, NemoClaw, privacy routing, risk matrix, CVE-2026-25253, safety-capability-autonomy trilemma
+**Keywords:** AI agent security, autonomous agents, defense in depth, prompt injection, tool security, agent isolation, OpenClaw, NemoClaw, OpenShell, DefenseClaw, privacy routing, risk matrix, CVE-2026-25253, safety-capability-autonomy trilemma, ISO/IEC 27001, ISO/IEC 42001, training-data memorization, zero data retention
---
@@ -58,7 +58,7 @@ Before examining specific threats and controls, we introduce a framing that will
- **High Safety + High Autonomy, Low Capability:** Drastically restricted action space. The agent runs unsupervised because it can only do pre-screened safe things.
- **High Capability + High Autonomy, Low Safety:** The current default of most OpenClaw deployments. The agent can do a great deal, unsupervised, including harmful things.
-Every agent deployment implicitly sits somewhere in this trilemma space. Making that choice explicit — rather than pretending the trade-off doesn't exist — enables honest risk assessment and appropriate mitigation focus. We will map each deployment persona to a recommended trilemma position in §7.3.
+Every agent deployment implicitly sits somewhere in this trilemma space. Making that choice explicit — rather than pretending the trade-off doesn't exist — enables honest risk assessment and appropriate mitigation focus. We will map each deployment persona to a recommended trilemma position in §10.3.
### 1.2 An Unprecedented Deployment Velocity
@@ -74,7 +74,7 @@ The security incident record from 2025–2026 illustrates the breadth of the att
**CVE-2026-25253 (Zero-Click WebSocket Hijack).** This critical-severity vulnerability affected OpenClaw instances exposing the WebSocket control port with default configuration. An attacker with network access could inject tool calls into an active agent session via a crafted WebSocket handshake that bypassed origin validation — no authentication required. Because OpenClaw defaulted to binding on `0.0.0.0`, any network-reachable instance was vulnerable. The patch required a single-line config change, but Shadowserver scans three weeks post-disclosure still found 31,000 unpatched instances.
-**Cisco Talos: Malicious Skills Supply Chain Attack.** In October 2025, Cisco Talos documented a campaign distributing seventeen malicious OpenClaw skills via the community registry. Skills mimicking legitimate utilities silently exfiltrated `~/.env`, `~/.ssh/`, and `~/.config/` to attacker infrastructure using obfuscated JavaScript with delayed execution. Skills run within the agent process with the agent user's full filesystem permissions — no sandbox prevented this access. Approximately 8,000 installs were confirmed.
+**Cisco Talos: Malicious Skills Supply Chain Attack.** In October 2025, Cisco Talos documented a campaign distributing seventeen malicious OpenClaw skills via the community registry. Skills mimicking legitimate utilities silently exfiltrated `~/.env`, `~/.ssh/`, and `~/.config/` to attacker infrastructure using obfuscated JavaScript with delayed execution. Skills run within the agent process with the agent user's full filesystem permissions — no sandbox prevented this access. Approximately 8,000 installs were confirmed. This early disclosure was a precursor to a far larger coordinated campaign (documented as "ClawHavoc") and to the open-source tooling response it provoked — Cisco's DefenseClaw chief among them — analyzed in §6.1.
**The Inbox Zero Incident.** An AI safety researcher described an incident in which their agent, operating with email access for "inbox management," interpreted "clear out the old stuff" as authorization to permanently delete ~6,500 emails spanning six months. No confirmation requested. Emails unrecoverable. The incident became a widely-cited example of the gap between probabilistic behavioral guardrails and deterministic action constraints.
@@ -121,7 +121,7 @@ We further assume that many real deployments are socio-technical systems rather
### 1.7 The Structure of This Paper
-Section 2 presents the eight-class security strategy taxonomy and explains how the taxonomy was constructed. Section 3 profiles three deployment personas. Section 4 constructs risk matrices with justified scoring methodology. Section 5 analyzes NVIDIA's NemoClaw as a case study. Section 6 presents the AEGIS defense-in-depth architecture with a worked attack simulation. Section 7 provides per-persona recommendations and concluding analysis. Section 8 discusses limitations and future validation directions.
+Section 2 presents the eight-class security strategy taxonomy and explains how the taxonomy was constructed. Section 3 profiles three deployment personas. Section 4 constructs risk matrices with justified scoring methodology. Section 5 analyzes NVIDIA's NemoClaw/OpenShell as a case study, updated to its June 2026 state. Section 6 surveys the broader open-source agent-security landscape, including Cisco's DefenseClaw and the cross-runtime guardrail and isolation projects. Section 7 presents the AEGIS defense-in-depth architecture with a worked attack simulation. Section 8 maps AEGIS onto the ISO/IEC 27001 and 42001 control vocabulary for already-certified organizations. Section 9 examines the evidence behind the fear that frontier-API providers train a model of your business. Section 10 provides per-persona recommendations and concluding analysis. Sections 11–13 discuss limitations, future validation directions, and related work.
---
@@ -534,35 +534,46 @@ Scores are calibrated against: CVE-2026-25253 exploitation frequency (P=4 based
---
-## 5. NemoClaw and OpenShell: A Case Study in Principled Architecture
+## 5. NVIDIA NemoClaw and OpenShell: A Case Study, Updated to June 2026
-### 5.1 NVIDIA's Approach
+### 5.1 NVIDIA's Approach — Current State (June 2026)
-NVIDIA's NemoClaw, announced at GTC 2026, represents the most systematically security-conscious commercial agent framework to date — a ground-up redesign around isolation and policy enforcement as first-class primitives. We examine it as a case study in how the principles identified in §2 can be architecturally integrated.
+NVIDIA's NemoClaw, announced at GTC 2026 (16 March 2026), represents the most systematically security-conscious agent stack to reach open source to date — a reference architecture built around isolation and policy enforcement as first-class primitives. We examine it as a case study in how the principles identified in §2 can be architecturally integrated. This section reflects the repository state as of June 2026, a substantial update from the v1.0-era launch materials on which earlier drafts relied.
-**Important caveats:** NemoClaw has not yet seen wide production deployment, so its security claims remain largely unvalidated at scale. NVIDIA has a commercial interest in positioning NemoClaw as the "secure" alternative to open-source runtimes. The OpenShell architecture adds complexity that itself becomes attack surface — more code means more potential bugs. Our analysis is based on publicly available GTC 2026 materials and technical documentation.
+NemoClaw (`github.com/NVIDIA/NemoClaw`, Apache 2.0) is an open-source reference stack for running OpenClaw and compatible agents inside **NVIDIA OpenShell** sandboxes. It is positioned within the broader NVIDIA agent ecosystem — and should not be conflated with the separately-maintained NeMo-Agent-Toolkit, an agent-orchestration library that is a distinct product. The `nemoclaw` CLI orchestrates the OpenShell gateway, sandbox containers, local-inference provisioning, and network policy in a single onboarding sequence. OpenShell itself is maintained as a separate, Rust-dominant repository (`github.com/NVIDIA/OpenShell`) on its own versioned release track — v0.0.67 as of 22 June 2026.
+
+**Important caveats — stronger than at launch.** NemoClaw is explicitly **alpha software**. NemoClaw publishes no formal tagged releases (version tracking runs through GitHub Discussions, most recently a v0.0.60 stability round dated 5 June 2026); OpenShell's own documentation describes the runtime as a "proof-of-life" project in **"single-player mode,"** explicitly not architected for multi-tenant or hostile-boundary enterprise deployment. NVIDIA has a commercial interest in positioning the stack as the "secure" alternative to bare runtimes; the OpenShell layer adds complexity that is itself attack surface (see CVE-2026-24222, §5.7); and security claims remain largely unvalidated at production scale. Our analysis is based on the public repositories, NVIDIA developer documentation, and the April 2026 PSIRT bulletin.
### 5.2 OpenShell: Out-of-Process Policy Enforcement
The conventional approach to agent guardrails implements checks _within_ the agent process. OpenClaw's in-process code checks command permissions before execution — effective absent attacks, but fundamentally weak: the constrained process also enforces the constraint. A manipulated agent may influence or bypass in-process checks.
-OpenShell places the policy engine in a **separate, privileged process**. The agent calls a restricted system call interface; the OpenShell kernel intercepts, evaluates against policy, and permits or denies. The agent cannot access or modify its own policy. This is architecturally analogous to the Linux kernel's relationship with user-space processes.
+OpenShell places the policy engine in a **separate, privileged process**. The agent calls a restricted system-call interface; the OpenShell gateway intercepts, evaluates against policy, and permits or denies. The agent cannot access or modify its own policy. This is architecturally analogous to the Linux kernel's relationship with user-space processes, and — unlike the "browser-tab" framing discussed below — it is substantiated by concrete kernel primitives. OpenShell enforces four policy domains:
+
+1. **Filesystem policy** — read/write path restrictions, locked at sandbox creation.
+2. **Network egress policy** — deny-by-default outbound connections enforced via **Landlock, seccomp, and network namespaces**; hot-reloadable at runtime.
+3. **Process policy** — blocks privilege escalation and dangerous syscalls; locked at sandbox creation.
+4. **Inference-routing policy** — intercepts all model API calls and forwards them through a controlled backend; hot-reloadable.
+
+A deny-all network baseline ships as the reference configuration, extended by operators through an approval flow. The release cadence shows active investment in this layer: credential rotation and AppArmor profile configuration (v0.0.57), JWT secret management (v0.0.56), high-availability multi-replica gateways (v0.0.62), and TLS certificate hot-reload (v0.0.65, 17 June 2026). This is a genuine deterministic control in the AEGIS sense — the constraint lives outside the constrained component.
-### 5.3 The Browser Tab Model
+### 5.3 The "Browser-Tab Model" — Metaphor, Not Mechanism
-OpenShell's isolation draws an explicit analogy to web browsers. Each agent session runs in an isolated context — separate memory space, separate origin context — analogous to Chrome's process-per-tab architecture. Cross-session communication occurs only through audited channels mediated by the OpenShell kernel.
+NVIDIA's materials describe session isolation using the phrase "browser-tab model." It is important to read this as an **architectural metaphor**, not a technical browser mechanism: there is no browser tab, extension API, or CDP-level component involved. The underlying implementation isolates each agent within a Linux container (Docker, Podman, MicroVM, or Kubernetes) subject to the four policy domains above; the sandbox can write only to `/sandbox` and `/tmp`, with system paths mounted read-only.
-Practical implications: a prompt-injected session cannot access parallel sessions' state; malicious skills in one session cannot observe others; session tokens cannot be replayed across contexts.
+The practical implications hold regardless of the metaphor: a prompt-injected session cannot reach parallel sessions' state; malicious skills in one session cannot observe others; session tokens cannot be replayed across contexts. The isolation boundary is the container, mediated by the gateway.
-### 5.4 Privacy Router Architecture
+### 5.4 Privacy Router — Three Tiers, but Cost-Tolerance Routing
-NemoClaw implements three-tier data classification:
+NemoClaw's inference layer is a three-tier architecture, but the routing **mechanism** differs from the sensitivity-classification story told in launch coverage. The three tiers are **infrastructure tiers**, not data-sensitivity tiers:
-- **Tier 1 (Local-Only):** PII, credentials, financial data → local models (Llama 3 via NVIDIA NIMs). No data leaves the network.
-- **Tier 2 (Trusted Third-Party):** Non-sensitive but capability-demanding → pre-approved API providers in a signed routing policy.
-- **Tier 3 (Public-OK):** Generic tasks → any available model.
+- **Tier 1 — Cloud-hosted:** third-party API endpoints (NVIDIA `build.nvidia.com`, OpenAI, Anthropic, Gemini, and compatible proxies), including frontier models such as Claude Sonnet and Nemotron Ultra.
+- **Tier 2 — Self-hosted / enterprise:** self-hosted NIM containers, NVIDIA AI Enterprise gateways, and local vLLM/SGLang/TRT-LLM or Ollama deployments (≈4B–120B), on operator-managed infrastructure.
+- **Tier 3 — NemoClaw-managed local inference (experimental):** provisioned local inference on DGX Spark/Station or generic Linux hosts via a managed vLLM path, gated behind `NEMOCLAW_EXPERIMENTAL=1`.
-The routing decision is made by the OpenShell policy engine (out-of-process). The agent cannot override it.
+The automated tier-selection is **cost-tolerance based**: operators set a numeric tolerance (0.0 = maximum accuracy, 1.0 = minimum cost; default 0.20) and the router picks within that quality-cost envelope. The widely repeated description of the router as _classifying query sensitivity to keep PII on local models_ reflects marketing materials and third-party write-ups — **it is not the mechanism in the product's own inference-configuration documentation as of June 2026.** This matters for §2.6: privacy routing remains a valid control _class_, but a deployment that needs PII-sensitivity routing must implement the classifier itself; NemoClaw does not provide it out of the box.
+
+The genuine security guarantee NemoClaw _does_ provide here is **inference-path isolation**: the sandboxed agent communicates only with a local proxy endpoint (`inference.local`) and never contacts a provider directly. OpenShell intercepts at that endpoint and forwards based on operator configuration, with credentials held on the host and never exposed to the sandbox. A compromised agent therefore cannot exfiltrate credentials to an arbitrary endpoint or bypass the routing policy by dialing a provider directly. The routing decision is made out-of-process; the agent cannot override it.
### 5.5 Comparative Analysis
@@ -575,7 +586,7 @@ To avoid overstating the case study, we compare the two architectures along dime
| Privacy routing | Manual (if any) | Automatic, policy-driven |
| Skill supply chain | Community registry (unverified) | Signed manifests, hash verification |
| Self-modification prevention | User-configured | Mandatory, OpenShell-enforced |
-| Audit trail | Optional plugin | Mandatory, tamper-evident |
+| Audit trail | Optional plugin | Mandatory log (format unspecified) |
**What TinkerClaw does well:** Flexible tool permissions, SOUL.md behavioral baseline, easily-fixed WebSocket configuration, skill installation control with discipline.
@@ -604,11 +615,85 @@ The most valuable lesson from NemoClaw is not any single product feature; it is
These lessons are applicable even in deployments that will never adopt NVIDIA's stack. A lightweight OpenClaw deployment can still emulate the principle through separate policy proxies, immutable manifests, host-level firewalls, and append-only audit logging.
+### 5.7 Confirmed, Hedged, and Unresolved — A June 2026 Scorecard
+
+Two further claims have since clarified. **Signed skill manifests are now shipped, not aspirational:** NVIDIA publishes a signed skill catalog (`github.com/NVIDIA/skills`) in which each skill carries an OMS detached signature (`skill.oms.sig`) verified against a root certificate (`nv-agent-root-cert.pem`), and the daily sync pipeline drops any skill missing its signature. This is a genuine artifact-integrity control — but only for NVIDIA-published skills; it does nothing for third-party or user-authored skills distributed outside the catalog. The **audit-trail** claim, by contrast, should be _downgraded_ in confidence: OpenShell logs allow/deny decisions and the log is described as tamper-evident and compliance-grade (one practitioner report claims it satisfied a SOC 2 auditor), but no public specification of storage format, retention, or the tamper-evidence mechanism exists as of June 2026. Treat it as functionally claimed, implementation-unverified.
+
+The principal unresolved risk is **prompt injection**, and it is structural. The April 2026 NVIDIA PSIRT bulletin disclosed **CVE-2026-24222** (CVSS 8.6, CWE-497): prompt-injected content in the sandbox-initialization path could cause the agent to read and exfiltrate host environment variables (all versions before v0.0.18 affected). The root cause is not a NemoClaw bug per se but a property OpenClaw inherits — the control plane and data plane share one channel, so the pipeline that carries operator instructions also processes untrusted external content (email, web pages, fetched documents). No runtime sandbox resolves a semantic-layer vulnerability of this kind. Two further residual risks are documented: **identity sprawl** (a compromised agent inherits every credential it holds, regardless of sandbox depth — cf. §3.4), and the **absence of a multi-tenant trust boundary** (NemoClaw is not architected to isolate mutually-untrusted users sharing a gateway).
+
+| Control claim | Shipped state (June 2026) | Maturity |
+| --------------------------------------- | ------------------------------------------------------------------ | --------------------------- |
+| Out-of-process policy enforcement | Confirmed: Landlock + seccomp + network namespaces | Alpha (OpenShell v0.0.67) |
+| Session isolation ("browser-tab model") | Confirmed: container-level; no browser mechanism | Alpha |
+| Inference privacy routing | Three-tier infra routing; cost-tolerance, _not_ PII-classification | Alpha / Tier-3 experimental |
+| Signed skill manifests | Confirmed: `skill.oms.sig` + root cert, enforced in sync pipeline | Shipped |
+| Tamper-evident audit trail | Functionally claimed; no public format/retention spec | Unverified |
+| Multi-tenant boundary | Explicitly out of scope ("single-player mode") | Not shipped |
+| Prompt-injection prevention | Structural gap; CVE-2026-24222 demonstrates exploitation | Unresolved |
+
+The net assessment is unchanged in direction but sharper in detail: NemoClaw is a materially stronger posture than bare OpenClaw for **single-operator** deployments, primarily through genuine out-of-process kernel-layer enforcement and inference-path isolation (deterministic Class 2.1–2.4 controls). It is **not** a complete enterprise security framework — it is alpha, single-operator-scoped, and leaves prompt injection, credential sprawl, and multi-tenancy open. Architects should treat it as a sound runtime-containment layer and apply separate probabilistic and governance controls for the semantic- and identity-layer risks it does not address.
+
+---
+
+## 6. The Broader Open-Source Agent-Security Landscape
+
+AEGIS did not emerge in a vacuum. By mid-2026 a fast-specializing open-source ecosystem has formed around the same threat surface this paper targets — prompt injection and goal hijacking, skill supply-chain contamination, over-privileged execution, and missing runtime policy enforcement. This section surveys the principal projects, maps each onto the eight-class taxonomy (§2) and the AEGIS layers (§7), and asks whether the field is converging on a handful of mechanisms or genuinely innovating. The NemoClaw/OpenShell stack analyzed in §5 is one node in this landscape; here we situate it among its peers.
+
+### 6.1 The Supply-Chain Escalation and Cisco's DefenseClaw
+
+The Cisco Talos disclosure noted in §1.3 was an early signal. The threat soon escalated into a coordinated campaign — documented under the name **ClawHavoc** — in which on the order of a thousand malicious skills (independent counts range ≈900–1,184 across a handful of publisher accounts) were published to the community registry, using staged downloads, reverse shells, and credential-harvesting payloads (including the Atomic macOS Stealer) to exfiltrate browser credentials, keychains, SSH keys, and crypto-wallet data. The only gate at the time of the campaign was a registry account at least one week old — no static analysis, no signing, no review.
+
+Cisco's operational response is **DefenseClaw** (`github.com/cisco-ai-defense/defenseclaw`, Apache 2.0; announced 23 March 2026, generally available 27 March 2026; v0.7.2 as of June 2026). It is three cooperating components: a **Python operator CLI**; a **Go gateway sidecar** that proxies the LLM path (LiteLLM-compatible), runs an OPA/Rego policy engine, and writes an append-only SQLite audit store with optional Splunk HEC / OTLP forwarding; and an **OpenClaw TypeScript plugin** that intercepts tool calls via OpenClaw's hook system and routes each through the gateway for a pre-execution policy verdict. Its **admission control** runs five scanners — Skill Scanner, MCP Scanner, A2A (agent-to-agent) Scanner, CodeGuard static analysis (secrets, dangerous exec, unsafe deserialization, weak crypto, injection patterns, risky file access), and the proprietary ClawShield — before any component loads. Its **runtime guardrails** inspect prompts, completions, and tool-call results against YAML rule packs, with an optional LLM judge for semantic policies that deterministic rules cannot express. Block/allow changes propagate in under two seconds without an agent restart — closing the "clean-on-Tuesday, exfiltrating-on-Thursday" window that static pre-deployment scanning misses.
+
+In taxonomy terms, DefenseClaw is primarily a **Class 2.7 (audit and supply-chain)** and **Class 2.3 (programmatic guardrail)** control, with a probabilistic runtime-monitoring component. Crucially, it is a **bolt-on governance layer**: a plugin-plus-sidecar that wraps what OpenClaw admits and calls, but does not alter the execution environment or impose kernel-level constraints. This is the architectural inverse of NemoClaw/OpenShell (§5), which constrains the agent from _below_, at the OS layer. The two are complementary, not competing — DefenseClaw closes the supply-chain intake vector and supplies observability; OpenShell bounds the blast radius of anything that evades admission control. The AEGIS Swiss-cheese model (§7) accommodates both: DefenseClaw at the admission and audit slices, OpenShell at the isolation slices, neither sufficient alone. For the Tinkerer (Persona A) the five-minute DefenseClaw deployment is a meaningful supply-chain gain on its own; the Corporate persona (C) under SOX/GLBA audit needs both — DefenseClaw's policy-as-code audit trail for the governance-logging obligation, OpenShell's isolation for a defensible least-privilege standard.
+
+### 6.2 The OpenClaw-Specific Tier: A Third Project, SecureClaw
+
+A third OpenClaw-specific project, **SecureClaw** (Adversa AI; `github.com/adversa-ai/secureclaw`), makes one design choice worth singling out: it splits enforcement between a **plugin component that lives outside the agent's context window** and a skill component carrying ~15 behavioral rules. The out-of-band placement matters, because guardrails embedded _solely_ as a skill are themselves vulnerable to the prompt injection they exist to stop — an attacker who can inject can also instruct the model to ignore a skill-resident rule. SecureClaw runs ≈55 automated checks mapped to the OWASP Agentic Security Initiative Top 10, MITRE ATLAS, and CoSAI, with its skill held to ≈1,150 tokens to avoid context saturation, and is explicit that it makes injection "significantly harder," not solved — the honest framing AEGIS's probabilistic tier (§2.5) demands. Taken together the three OpenClaw-specific projects already instantiate the AEGIS geometry: NemoClaw the deterministic containment primitive, SecureClaw out-of-band audit and skill-layer rules, DefenseClaw dynamic admission control — distributed across three codebases with no integration contract between them.
+
+### 6.3 Cross-Runtime Guardrail Frameworks
+
+Several model- and runtime-agnostic frameworks apply to OpenClaw even though they were not built for it.
+
+**Meta LlamaFirewall** (May 2025; arXiv:2505.03574) is the most architecturally differentiated. Three components run in sequence: PromptGuard 2 (a fine-tuned classifier for direct and indirect injection), **Agent Alignment Checks** (a chain-of-thought auditor that inspects the _reasoning trace_ for evidence that an injected instruction has been silently accepted into the plan), and CodeShield (online static analysis of generated code). The reasoning-trace auditor addresses a blind spot in every message-level guardrail surveyed here: a subtle indirect injection can produce a compliant-looking response while corrupting the agent's internal plan.
+
+**Invariant Guardrails** (Invariant Labs) is a transparent proxy between the agent and its MCP servers or LLM provider that evaluates a Python-inspired rule language able to express **cross-tool dataflow constraints** — e.g. "raise if a `get_inbox` call is followed by a `send_email` to an external address." This if-this-then-that-across-tool-calls formalism is something single-prompt classifiers structurally cannot do, and it is the closest open-source analogue to a runtime _behavioral-invariant_ control.
+
+**NVIDIA NeMo Guardrails** provides content-safety, PII, jailbreak, and topic rails around the inference API (multilingual/multimodal, with enterprise AIDR integration) — strong at the I/O boundary but without cross-turn dataflow awareness. **Lakera Guard** (acquired by Check Point, Sept 2025) offers ≈98% direct/indirect injection detection at sub-50 ms, model-agnostic — but is now a commercial component, not OSS, and exposes no policy language or tool-call visibility. **Rebuff** combines heuristics, an LLM detector, a vector store of known attacks, and **canary tokens**; the project is research-grade as of mid-2026, but the canary-token primitive (a sentinel value that must never appear in output) is a lightweight deterministic detector any deployment can layer on.
+
+### 6.4 Execution Isolation and Supply-Chain Scanning
+
+The sandboxing layer has consolidated around hardware-virtualization and OS-isolation primitives. **E2B** (Apache 2.0) provides managed Firecracker microVM sandboxes for AI-generated code — ephemeral, per-agent, ~150 ms startup, credentials injected as env vars never written to disk. **Microsandbox** (Zerocore AI, Apache 2.0) uses full microVM isolation with, distinctively, **native MCP-server integration** — the sandbox is invocable directly as an agent tool, no orchestration shim — making it the most practical drop-in for OpenClaw-style deployments. **gVisor** interposes a user-space kernel; architecturally sound but a weaker guarantee than full virtualization for executing untrusted code. **Microsoft MXC** (Build 2026, 2 June 2026) is an OS-kernel-enforced composable sandbox spectrum with explicit agent-identity semantics — significant, but Windows/WSL-only and post-dating this paper's earlier revisions.
+
+On the scanning side: **NVIDIA Garak** (v0.15.0, May 2026) is the closest thing to a penetration-testing tool for LLM agents — 50+ probes including a new multi-turn GOAT probe and an Agent-breaker probe that tests tool-equipped agents, not just isolated model responses; it feeds other controls rather than enforcing policy. **Protect AI Guardian / Prisma AIRS** (Palo Alto Networks) scans serialized **model artifacts** for deserialization exploits and tampering — the model-loading equivalent of image signing, a layer distinct from skill supply-chain. **Microsoft's Agent Governance Toolkit** (MIT) is the most structurally complete OSS governance framework surveyed — a stateless policy engine (OPA Rego / Cedar), Ed25519 DID agent identity with trust scoring, and Ed25519 supply-chain signing — framework-agnostic and explicitly modeled on OS privilege rings and service-mesh identity, a close conceptual cousin of AEGIS. The **OWASP "Universal Skill Format"** proposal (draft v0.5, v1.0 targeted Q3 2026) would mandate Ed25519 signing, explicit permission allowlists, and content hashes per skill publication — certificate-transparency for skill registries, and the normative codification of the provenance controls AEGIS presupposes.
+
+### 6.5 Comparative Snapshot
+
+| Project | AEGIS layer / class | Core mechanism | Type | Novel vs. more-of-the-same |
+| ------------------------------- | ------------------------------------- | ----------------------------------------------------------- | ------------ | ------------------------------------ |
+| **NemoClaw** (NVIDIA) | Isolation + runtime policy (2.1–2.4) | OpenShell container hardening, egress policy, CLI lifecycle | Det. | Baseline the others build on |
+| **DefenseClaw** (Cisco) | Admission + audit (2.7, 2.3) | 5-scanner engine + LLM-path proxy; <2 s dynamic revocation | Det. + Prob. | Complementary governance layer |
+| **SecureClaw** (Adversa) | Out-of-band audit (2.5, 2.7) | Plugin outside context window + skill rules; 55 checks | Det. + Prob. | Novel: injection-resistant placement |
+| **LlamaFirewall** (Meta) | Plan integrity (2.5) | PromptGuard 2 + reasoning-trace auditor + CodeShield | Prob. + Det. | **Novel: reasoning-trace auditing** |
+| **Invariant Guardrails** | Behavioral invariants (2.3, 2.5) | Proxy + cross-tool dataflow policy language | Det. | **Novel: cross-tool dataflow rules** |
+| **NeMo Guardrails** (NVIDIA) | I/O filtering (2.5) | Configurable rails around inference API | Prob. + Det. | More-of-the-same vs. Lakera |
+| **Lakera Guard** (Check Point) | I/O filtering (2.5) | Real-time classifier, 98%+, <50 ms | Prob. | More-of-the-same; now commercial |
+| **NVIDIA Garak** | Red-teaming (pre-deploy) | 50+ probes incl. agentic Agent-breaker | Det. (scan) | Novel in category: agentic scanner |
+| **Protect AI / Prisma AIRS** | Model-artifact supply chain (2.7) | Static scan of serialized model files | Det. | Distinct layer; non-redundant |
+| **E2B / Microsandbox** | Execution isolation (2.2) | Firecracker microVM; Microsandbox adds MCP-native | Det. | Microsandbox MCP integration novel |
+| **MS Agent Governance Toolkit** | Full-stack governance (2.3, 2.7, 2.8) | OPA/Cedar engine, Ed25519 DID identity, signing | Det. | Novel: most complete OSS governance |
+
+### 6.6 Verdict — Convergence, with Two Open Frontiers
+
+The ecosystem is **converging** on four foundational mechanism classes: (1) sandbox isolation at the hardware-virtualization or OS level; (2) out-of-process policy enforcement between the agent and its tools/inference; (3) supply-chain signing and admission control; and (4) runtime I/O classification. NemoClaw, DefenseClaw, and SecureClaw between them cover all four for OpenClaw — which is precisely the AEGIS defense-in-depth prescription, just split across three projects.
+
+Two higher-order frontiers remain only sparsely occupied. The first is **cross-turn, dataflow-aware behavioral invariants** — policies that span multiple sequential tool calls rather than judging each message in isolation (Invariant Guardrails; the OPA/Cedar rules in Microsoft's toolkit). The second is **reasoning-trace auditing** — inspecting the chain-of-thought for a silently-accepted injection before it reaches a tool call (Meta LlamaFirewall's Agent Alignment Checks is, as of this writing, the only OSS implementation). Both target failure modes that proxy-level guardrails cannot see, and — tellingly — **neither is yet integrated into the OpenClaw-specific tooling tier.** The practical implication for an AEGIS deployment is that defense-in-depth today requires _intentional composition_ across at least three of these projects, and that the two genuinely novel mechanisms are worth adopting from the cross-runtime tier rather than waiting for an OpenClaw-native equivalent.
+
---
-## 6. AEGIS: A Defense-in-Depth Architecture
+## 7. AEGIS: A Defense-in-Depth Architecture
-### 6.1 The Swiss Cheese Model
+### 7.1 The Swiss Cheese Model
The Swiss cheese model (Reason, 1990) visualizes each security control as a slice of cheese with holes. Individually, each slice has weaknesses. Layered together, the holes rarely align. AEGIS proposes seven deterministic slices, with the probabilistic layer as a general-purpose filter rather than a counted control:
@@ -646,7 +731,7 @@ The Swiss cheese model (Reason, 1990) visualizes each security control as a slic
DETECTED/BLOCKED
```
-### 6.2 Layer Interactions
+### 7.2 Layer Interactions
Each layer compensates for adjacent weaknesses:
@@ -657,7 +742,7 @@ Each layer compensates for adjacent weaknesses:
- **Privacy routing** depends on classification accuracy → **Audit logging** provides visibility into routing decisions for post-hoc review.
- **Audit logging** is detection-only → **Self-modification prevention** ensures the audit mechanism can't be disabled before detection.
-### 6.3 Worked Attack Simulation
+### 7.3 Worked Attack Simulation
To demonstrate AEGIS layer interactions concretely, we trace 10 representative attack scenarios through a Standard Configuration deployment:
@@ -680,7 +765,7 @@ To demonstrate AEGIS layer interactions concretely, we trace 10 representative a
This simulation demonstrates two key properties: (a) no single layer is sufficient — each attack that passes one layer is caught by a subsequent one; (b) the architecture degrades gracefully — even when prevention fails, detection enables response.
-### 6.4 Reference Configurations
+### 7.4 Reference Configurations
Before listing configurations, one principle deserves emphasis: **removing access is often more effective than hardening access**. When an asset does not need to be in the agent's reachable set, scope reduction should take priority over layered protection. This is especially important for Persona C, where several risks are better solved by access withdrawal than by compensating controls.
@@ -701,19 +786,92 @@ All of Standard, plus: 11. Out-of-process policy enforcement 12. Per-session iso
---
-## 7. Conclusions and Recommendations
+## 8. Mapping AEGIS to ISO/IEC Security Standards
+
+A recurring practitioner question is where agent and LLM security "fits" for an organization that already holds a cybersecurity certification. This section maps AEGIS onto the ISO/IEC control vocabulary so that an ISO/IEC 27001-certified organization can see exactly which agent-security layers extend an existing control objective and which introduce a genuinely new one.
+
+### 8.1 Bridging the AEGIS Taxonomy to the ISO/IEC Vocabulary
+
+AEGIS stratifies its eight strategy classes along a deterministic–probabilistic axis. Deterministic controls (Classes 2.1–2.4) enforce invariants with binary, auditable outcomes regardless of model behavior; probabilistic controls (Classes 2.5–2.8) reduce likelihood or impact but cannot guarantee outcomes against all inputs. This maps naturally onto ISO/IEC 27002:2022's control _attributes_: deterministic controls correspond to _preventive_ and _detective_ attributes, probabilistic controls to _corrective_, _directive_, and _compensating_ ones. ISO/IEC 27001:2022 does not require controls to be purely technical — Clause 6.1.3 explicitly contemplates treating risk through a combination of organizational policy, technical enforcement, and continuous monitoring. AEGIS operationalizes exactly that layered treatment.
+
+### 8.2 The Eight Strategy Classes Mapped to Annex A and ISO 42001
+
+The table maps each AEGIS class to the most directly applicable ISO/IEC 27001:2022 Annex A controls (elaborated in ISO/IEC 27002:2022) and the corresponding ISO/IEC 42001:2023 (AI management system) control. The final column states whether the class is "new evidence for an existing objective" or a "genuinely new objective."
+
+| AEGIS Class | Primary ISO 27001:2022 Annex A controls | ISO 42001:2023 control | Evidence nature |
+| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
+| **2.1 Filesystem ACLs** | A.5.15 (access control); A.5.18 (access rights); A.8.2 (privileged access); A.8.3 (information access restriction) | A.6.1.2 (least-privilege design) | Existing objective; new evidence (agent as an access-control principal) |
+| **2.2 Process Isolation** | A.8.22 (segregation of networks); A.8.27 (secure architecture); A.8.31 (separation of environments) | A.6.2.5 (deployment isolation/rollback) | Existing objective; new evidence (agent runtime as isolated execution context) |
+| **2.3 Programmatic Guardrails** | A.8.26 (application security requirements); A.8.28 (secure coding); A.8.29 (security testing) | A.6.1.3, A.6.2.4 (responsible design; verification & validation) | Partially new: guardrails over non-deterministic outputs are a new audit artefact |
+| **2.4 Network Controls** | A.8.20 (network security); A.8.21 (network services); A.8.22; A.8.23 (web filtering) | A.6.2.6 (operation & monitoring) | Existing objective; new evidence (per-agent egress as a segment boundary) |
+| **2.5 Prompt-Level Rules** | A.8.26 (input validation); A.5.7 (threat intelligence); A.5.37 (documented operating procedures) | A.6.1.2; A.9.1 (responsible/acceptable use) | **Genuinely new objective**: no ISO 27001 control governs natural-language policy or prompt injection |
+| **2.6 Privacy Routing** | A.5.34 (PII protection); A.8.11 (data masking); A.8.12 (DLP); ISO/IEC 27018 (PII in public cloud) | A.7.2, A.7.4 (data privacy & quality) | Existing objective; new evidence (runtime PII classifier as a DLP enforcement point for model APIs) |
+| **2.7 Audit & Supply Chain** | A.5.19, A.5.21, A.5.23 (supplier/ICT-supply-chain/cloud); A.8.15 (logging); A.8.16 (monitoring); ISO/IEC 27017 (cloud) | A.7.5 (data provenance); A.6.2.8 (event logs); A.10.3 (AI supplier due diligence) | Existing objective; **new evidence** (model provenance and training-data lineage as supply-chain artefacts under A.5.21) |
+| **2.8 Self-Modification Prevention** | A.8.9 (configuration management); A.8.19 (software installation control); A.8.32 (change management) | A.6.2.3, A.6.2.7 (design record; technical documentation) | **Genuinely new objective**: no ISO 27001 control anticipates a component that tries to modify its own operating policy at runtime |
+
+### 8.3 What ISO/IEC 42001:2023 Adds That ISO/IEC 27001:2022 Does Not
+
+ISO/IEC 27001:2022 is a general-purpose ISMS standard designed for systems with deterministic, human-authored logic. An autonomous agent has four structural properties outside its control catalogue. **(1) The risk perimeter exceeds the CIA triad.** ISO 42001 introduces _fairness_, _transparency/explainability_, _safety_, and _societal impact_ as independently auditable categories (Annex A.5.4–A.5.5); bias drift in an agent making decisions about individuals is a compliance failure no asset- or event-based ISO 27005 threat scenario surfaces. **(2) The AI lifecycle needs dedicated governance.** ISO 27001 covers the SDLC (A.8.25–A.8.31), but model lifecycle — data acquisition, training, fine-tuning, evaluation, deployment, _drift monitoring_, re-evaluation, retirement — is not a subset of it; ISO 42001 A.6.2.6 requires drift detection and periodic re-evaluation, an obligation ISO 27001 (which presumes a deployed system behaves as specified) lacks. **(3) Transparency is formally required.** ISO 42001 mandates model cards, user disclosures, and decision logging (Annex A.8); ISO 27001 logs (A.8.15) but never requires _explaining a system's decisions_ to affected parties. **(4) AI-supplier due diligence exceeds SLAs.** ISO 42001 A.10.3 requires assessing a supplier's training-data provenance, bias-mitigation, and retraining cadence — so an organization using a foundation-model API must treat that model's training corpus as a supply-chain risk artefact, a requirement absent from ISO 27001.
-### 7.1 The Fundamental Insight
+The relationship is therefore complementary, not substitutive: ISO/IEC 27001 establishes the organizational security baseline; ISO/IEC 42001 establishes the AI-specific governance layer; neither alone is sufficient for a mature agentic deployment. ISO/IEC 23894:2023 supplies the tactical AI risk-management procedures (as ISO/IEC 27005 does for 27001), and the NIST AI RMF (2023, with a 2024 Generative-AI profile) offers a voluntary cross-reference whose Govern/Map/Measure/Manage functions map onto the plan-do-check-act structure shared by all three ISO standards.
+
+### 8.4 Practical Guidance: New Evidence vs. New Objectives
+
+An organization holding ISO/IEC 27001:2022 already has a Statement of Applicability (SoA) and an evidence programme. The practical question is which AEGIS controls need only _new evidence_ and which need _new control objectives_. **New evidence for existing objectives:** most deterministic AEGIS controls (2.1–2.4, partly 2.7) satisfy existing Annex A objectives — what changes is the _form_ of evidence (the principal is an agent identity; the network segment is the per-agent egress policy; the log artefact includes tool-call receipts and model-turn records; the supply-chain artefact extends to model provenance). Amend the SoA commentary, brief the certification body at the next surveillance audit, and add the agentic runtime to the scope statement — no new controls formally required. **Genuinely new objectives:** Class 2.5 (prompt-level rules) and Class 2.8 (self-modification prevention) have no precedent in ISO 27001 — prompt injection is categorically different from the input-validation flaws A.8.26 addresses, and runtime self-modification has no SDLC or configuration-management analogue. Register these as additional controls in the SoA, justified by an ISO 42001 AI system impact assessment (AIIA), with corresponding policy, technical evidence (prompt-governance procedures, integrity checks), and testing artefacts (adversarial red-team results, self-modification-barrier test logs).
+
+For organizations not yet 42001-certified, the AIIA process is the most efficient mechanism to scope which AEGIS layers need new objectives; running ISO/IEC 23894's event-based risk identification against the agent's tool inventory and autonomy profile, then mapping results to both the 27001 SoA and the 42001 Annex A, avoids duplicating risk-assessment effort. The safety-capability-autonomy trilemma (§1.1) has a direct normative counterpart here: **ISO 27001 manages the safety floor; ISO 42001 manages the autonomy-induced risks a static floor cannot contain; ISO/IEC 23894 supplies the ongoing measurement to tell whether capability gains in successive model versions have outpaced the controls in place.**
+
+---
+
+## 9. Frontier APIs and the Fear of Training a Model of Your Business
+
+A persistent concern among organizations evaluating frontier APIs is that a provider such as Anthropic or OpenAI will ingest proprietary queries and documents, distil a model of the organization's business, and eventually leak it to competitors or use it to compete directly — whereas a local model or Copilot supposedly keeps data "within." This section weighs the empirical and contractual evidence, separates the genuine risk from the misconception, and ties the residual risk to the framework's controls.
+
+### 9.1 The Memorization Literature: What It Actually Shows
+
+The evidence for training-data leakage from LLMs is real but systematically misread in enterprise risk discussions. The key studies concern memorization of _public training-corpus text_, not re-use of API customer inputs.
+
+Carlini et al. (2021) established the foundational result: with enough queries and the right prompt strategy, an adversary can extract verbatim passages that appeared in a model's _pre-training corpus_. Nasr, Carlini et al. (2023, arXiv:2311.17035) scaled this to production: prompting ChatGPT to repeat a word indefinitely (the "divergence attack") broke its alignment-constrained mode and emitted pre-training text — previously public web and news content — at roughly 150× the normal rate, showing alignment suppresses but does not eliminate memorization. Cooper et al. (2025, arXiv:2505.12546) extended this to books: of 14 open-weight models across 200 titles, Llama 3.1 70B had memorized some so thoroughly — including _Harry Potter and the Sorcerer's Stone_ — that a few prompt tokens deterministically reproduced near-verbatim text. Analysis of the _New York Times v. OpenAI_ complaint (filed Dec 2023) found verbatim reproduction scales super-linearly above ~100B parameters and correlates with training-corpus duplication — but also that the plaintiff's attorneys needed tens of thousands of adversarial attempts to elicit the excerpts, and that both OpenAI and Anthropic deploy output filters to suppress verbatim reproduction even where the model has memorized the text.
+
+**The critical distinction.** In every case, what is extracted is material the model met during _pre-training on large public datasets_ — web text, digitized books, news archives. The threat model is (a) an adversary probing a deployed model to recover previously-public data it happens to have memorized, or (b) a rightsholder demonstrating training on their works. Neither describes a provider _training on a specific organization's API inputs_ and then leaking them to a competitor. The two risks are categorically different: one is a property of the pre-training corpus; the other would require a provider to deliberately or negligently repurpose live customer traffic — which, as the next section shows, the commercial contracts prohibit. The much-cited "it can complete Harry Potter" demonstration is evidence of the _former_, and tells you nothing about the latter.
+
+### 9.2 What the Contracts Actually Say
+
+**Anthropic.** The commercial terms for the Claude API, Team/Enterprise plans, and Claude via Amazon Bedrock or Google Vertex contain an explicit prohibition: Anthropic does not train models on customer content from these services. API inputs/outputs are retained for a short abuse-screening window (reduced to seven days in late 2025) and never used for training; organizations with qualifying use-cases may, subject to Anthropic approval, execute a Zero Data Retention (ZDR) addendum that discards prompts and outputs immediately after each request. Anthropic's commercial offering holds ISO/IEC 27001:2022, ISO/IEC 42001:2023, and SOC 2 Type II, with a DPA incorporated for GDPR/UK-GDPR and Standard Contractual Clauses for transfers. **The crucial caveat:** Anthropic's _consumer_ terms (updated August 2025) now permit training on Claude.ai consumer conversations unless the user opts out — this does **not** apply to commercial, API, or enterprise tiers. An employee using a personal Claude.ai account for work is thus governed by the consumer policy, not the enterprise prohibition.
+
+**OpenAI.** The API platform has not used inputs/outputs to train models by default since 1 March 2023; ChatGPT Enterprise, Teams, and the API are excluded from training by default. Default abuse logs are retained ~30 days; ZDR is available for eligible enterprise endpoints. The consumer/enterprise split mirrors Anthropic's.
+
+**Microsoft 365 Copilot.** Microsoft states that prompts, responses, and Microsoft Graph data are not used to train the foundation models behind M365 Copilot; requests route through Azure OpenAI (not the public service), customer content is not cached or shared with OpenAI, and EU tenants are covered by the EU Data Boundary. **A material subprocessor gap, however:** when Anthropic models are used within M365 Copilot experiences, they fall _outside_ the EU Data Boundary and in-country processing commitments — EU-regulated organizations relying on residency assurances must scrutinize this before enabling Anthropic-powered Copilot features.
+
+The pattern across all three is consistent: **enterprise/API tiers offer contractual no-training-by-default positions, DPAs, and optional zero-retention; consumer free tiers do not.** The "they will train a model of our business" fear is most accurately a **mis-tiering** risk — employees bypassing enterprise licensing for consumer products that carry weaker protections.
+
+### 9.3 Residual Risks the Contracts Cannot Eliminate
+
+Four residual risks remain regardless of tier. **Provider breach** — providers hold recent traffic during the retention window; an incident there exposes it. Inference-server vulnerabilities (vLLM, TensorRT-LLM, and others have had critical flaws) are not hypothetical. The privacy-routing layer (§2.6) — keeping the highest-sensitivity classes on local/on-prem inference — directly mitigates this. **Mid-flight interception** — TLS-inspected enterprise networks and boundary devices; certificate pinning and the gateway controls of §2.3 apply. **Mis-tiered usage** — a consumer account used for work; the governance controls of §2.7 and per-user agent profiles of §3.4 apply. **Subprocessor exposure** — both providers use infrastructure subprocessors; GDPR Art. 28(4) requires back-to-back obligations, so subprocessor lists should be reviewed periodically, especially for EEA transfers. In the event an exposure does occur, the incident-response checklist (§2.7.2) and the GDPR 72-hour notification obligation (§3.2.1) govern the response — this paper's earlier sections already answer "what happens in a leak."
+
+### 9.4 The Local-Model Alternative: A Different Threat Surface, Not a Smaller One
+
+Self-hosted inference eliminates training-data exfiltration for live traffic — there is no provider to train on prompts that never leave the premises, a genuine gain for highly regulated or genuinely-secret workloads. But it introduces an under-estimated risk surface. OWASP LLM03:2025 (Supply Chain) identifies **model-weight compromise** as a primary self-hosting threat: backdoored weights have been distributed through public repositories, and research shows as few as ~250 poisoned documents can implant a robust backdoor with no capability degradation that evaluation would catch. A compromised local model does not exfiltrate to a provider — but it may exfiltrate to an adversary, inject fabricated outputs downstream, or serve as a persistent foothold. This is precisely the residual gap in the worked simulation (§7.3, scenario 10): local inference is not equivalent to _secure_ inference. Local deployment must add weight-provenance verification (cryptographic signatures from the originating lab), integrity monitoring, and controlled fine-tuning pipelines as compensating controls.
+
+### 9.5 Summary Assessment
+
+The fear that frontier providers will train a proprietary model of an enterprise's business and weaponize or leak it is **not supported by current contractual or technical evidence for commercial tiers.** The strongest empirical argument for provider-side leakage — the memorization literature — concerns reproduction of _public pre-training text_, not re-use of enterprise API inputs; and the commercial no-training-by-default positions of Anthropic, OpenAI, and Microsoft are unambiguous, backed by DPAs, independent audits, and optional zero-retention. The genuine residual risks — provider breach in the retention window, mid-flight interception, employee mis-tiering, and subprocessor gaps (notably EU data residency) — are real, bounded, and addressable through the AEGIS stack (§2.6, §2.3, §2.7, §3.4). The local-model alternative _shifts_ the threat surface from training-data exfiltration to supply-chain and weight-integrity risk; it does not eliminate it.
+
+---
+
+## 10. Conclusions and Recommendations
+
+### 10.1 The Fundamental Insight
**Probabilistic controls alone are insufficient for any deployment where failure consequences are materially significant.** Every behavioral rule, every system prompt, every SOUL.md guideline is a probabilistic control — effective absent adversarial pressure, unreliable under it. This does not make probabilistic controls useless; it makes them misclassified when operators treat them as if they were equivalent to sandboxing, policy separation, or filesystem denial.
The incidents in §1.3 all involved deployments where consequences were significant but architecture was probabilistic. CVE-2026-25253 was a technical vulnerability, but 40,000 exposed instances represent a governance failure. The malicious skills attack succeeded because installation required no deterministic verification. The email deletion was the predictable result of granting irreversible capability to a system with only probabilistic constraints.
-### 7.2 Per-Persona Security Profiles
+### 10.2 Per-Persona Security Profiles
**Persona A — Minimum Viable (15 min):** Bind to 127.0.0.1; AppArmor denying ~/.ssh and crypto seed files. Addresses the two highest-risk scenarios.
-**Persona A — Recommended:** Standard Configuration (§6.4). Docker container, workspace restriction, egress firewall, skill verification. Total: 2–3 hours.
+**Persona A — Recommended:** Standard Configuration (§7.4). Docker container, workspace restriction, egress firewall, skill verification. Total: 2–3 hours.
**Persona B — Minimum Viable (3–4 hours):** All Persona A minimum plus: privacy routing for client directories to local model; per-session confirmation gate for email sends and file writes outside workspace; GDPR data inventory.
@@ -721,9 +879,9 @@ The incidents in §1.3 all involved deployments where consequences were signific
**Persona C — No "Minimum Viable" exists.** The described configuration cannot be made acceptably secure through hardening alone. Minimum action: **restrict agent access scope** — remove corporate network shares, production credentials, privileged email — before any technical control. With properly scoped agent, apply Standard Configuration plus Enterprise egress firewall and audit logging.
-**Persona C — Enterprise-Grade:** Full Enterprise Configuration (§6.4) via formal change management. Document in IT asset register, include in penetration test scope, govern through standard access control review.
+**Persona C — Enterprise-Grade:** Full Enterprise Configuration (§7.4) via formal change management. Document in IT asset register, include in penetration test scope, govern through standard access control review.
-### 7.3 The Trilemma as a Design Tool
+### 10.3 The Trilemma as a Design Tool
Every agent deployment implicitly chooses a position in the safety-capability-autonomy trilemma (§1.1). Our per-persona recommendations:
@@ -737,11 +895,11 @@ The agent era will not produce secure deployments by default. It will produce th
---
-## 8. Limitations
+## 11. Limitations
This analysis has several important limitations:
-1. **No empirical validation.** AEGIS has not been tested against real attack campaigns. The worked simulation (§6.3) is a thought experiment based on known attack patterns, not an empirical measurement. Future work should include red-team exercises against AEGIS-configured deployments.
+1. **No empirical validation.** AEGIS has not been tested against real attack campaigns. The worked simulation (§7.3) is a thought experiment based on known attack patterns, not an empirical measurement. Future work should include red-team exercises against AEGIS-configured deployments.
2. **Qualitative risk scores.** Despite our calibration methodology (§4), probability and severity ratings remain expert judgment. They have not been validated against a statistically significant incident database. Different experts would likely assign somewhat different scores.
@@ -757,7 +915,7 @@ This analysis has several important limitations:
8. **Case-study dependence.** The NemoClaw discussion is illustrative, not determinative. The architecture is used to surface principles, not to crown a winner among runtimes.
-## 9. Future Work
+## 12. Future Work
Several research directions would strengthen or falsify this framework:
@@ -765,12 +923,12 @@ Several research directions would strengthen or falsify this framework:
2. **Longitudinal incident corpus.** Build a shared database of agent incidents and near misses so risk scoring can be calibrated with stronger empirical footing.
3. **Control interaction measurement.** Quantify which layers reduce marginal risk most effectively for given personas, rather than assuming equal value across contexts.
4. **Approval UX experiments.** Measure approval fatigue, cancellation rates, and false approvals under different permission-gate designs.
-5. **Model integrity verification.** Extend AEGIS to better cover the residual gap identified in §6.3: compromise of the local model or classifier itself.
+5. **Model integrity verification.** Extend AEGIS to better cover the residual gap identified in §7.3: compromise of the local model or classifier itself.
6. **Cross-runtime validation.** Test whether the taxonomy transfers cleanly to LangChain-style, browser-native, mobile, and enterprise orchestrator deployments.
---
-## 10. Related Work
+## 13. Related Work
**OWASP Top 10 for LLM Applications (2025)** provides a vulnerability-focused taxonomy for LLM-powered applications, covering prompt injection, insecure output handling, and training data poisoning. AEGIS differs in focusing specifically on _agentic_ systems (tool use, action execution, multi-step autonomy) rather than LLM applications generally, and in providing a layered defense architecture rather than a vulnerability list.
@@ -826,6 +984,32 @@ Yao, S., et al. (2023). "ReAct: Synergizing Reasoning and Acting in Language Mod
Ziegler, D., et al. (2019). "Fine-Tuning Language Models from Human Preferences." _arXiv preprint arXiv:1909.08593_.
+Nasr, M., Carlini, N., et al. (2023). "Scalable Extraction of Training Data from (Production) Language Models." _arXiv:2311.17035_.
+
+Cooper, A. F., Lemley, M., et al. (2025). "Extracting Memorized Pre-Training Data from Open-Weight Language Models." _arXiv:2505.12546_.
+
+Anthropic. (2026). "Commercial Terms of Service" and "Trust Center." https://www.anthropic.com/legal/commercial-terms ; https://trust.anthropic.com/ (accessed June 2026).
+
+OpenAI. (2026). "Enterprise Privacy and API Data Usage Policies." https://openai.com/enterprise-privacy/ (accessed June 2026).
+
+Microsoft. (2026). "Data, Privacy, and Security for Microsoft 365 Copilot." _Microsoft Learn_ (accessed June 2026).
+
+OWASP Gen AI Security Project. (2025). "LLM03:2025 — Supply Chain." _OWASP Top 10 for LLM Applications_. https://genai.owasp.org/llmrisk/llm032025-supply-chain/
+
+Cisco. (2026). "Announcing DefenseClaw: Open-Source Security Governance for Agentic AI." _Cisco Blogs / Cisco AI Defense_, March 2026. https://github.com/cisco-ai-defense/defenseclaw
+
+NVIDIA. (2026). "NemoClaw and OpenShell." _GitHub_. https://github.com/NVIDIA/NemoClaw ; https://github.com/NVIDIA/OpenShell (accessed June 2026).
+
+Meta AI. (2025). "LlamaFirewall: A Guardrail Framework for AI Agents." _arXiv:2505.03574_.
+
+Invariant Labs. (2026). "Invariant Guardrails." _GitHub_. https://github.com/invariantlabs-ai/invariant
+
+ISO/IEC. (2022). _ISO/IEC 27001:2022 — Information security management systems — Requirements_; _ISO/IEC 27002:2022 — Information security controls_. International Organization for Standardization.
+
+ISO/IEC. (2023). _ISO/IEC 42001:2023 — Artificial intelligence — Management system_; _ISO/IEC 23894:2023 — Artificial intelligence — Guidance on risk management_.
+
+NIST. (2023). _AI Risk Management Framework (AI RMF 1.0)_; Generative AI Profile (2024). National Institute of Standards and Technology.
+
---
## Appendix A: AEGIS Quick-Reference Checklist
@@ -856,7 +1040,7 @@ Ziegler, D., et al. (2019). "Fine-Tuning Language Models from Human Preferences.
- [ ] **BEFORE ANY HARDENING:** Remove agent access to corporate network shares, production credentials, privileged email
- [ ] IT security review and documentation
-- [ ] Enterprise Configuration (§6.4) via formal change management
+- [ ] Enterprise Configuration (§7.4) via formal change management
- [ ] Include in penetration test scope
- [ ] Evaluate NemoClaw out-of-process enforcement adoption
- [ ] Prompt injection awareness training for agent users
@@ -864,5 +1048,6 @@ Ziegler, D., et al. (2019). "Fine-Tuning Language Models from Human Preferences.
---
_Paper J9 — AEGIS: A Multi-Layered Security Framework for Autonomous AI Agents_
-_Author: Oscar Serra | Date: 2026-03-17 | Version: 1.1_
+_Author: Oscar Serra | Date: 2026-06-23 | Version: 2.0_
_Workspace: J-Series AI Research Papers | Series: J9_agent_security_
+_v2.0 changes: refreshed the NemoClaw/OpenShell case study to its June 2026 repository state; added §6 (broader open-source landscape, incl. Cisco DefenseClaw); added §8 (ISO/IEC 27001 & 42001 mapping); added §9 (frontier-API training-data evidence and contracts)._
diff --git a/docs/papers/corporate-swarm/corporate-swarm.md b/docs/papers/corporate-swarm/corporate-swarm.md
index 7e38c99c2a1da..889b1a726b310 100644
--- a/docs/papers/corporate-swarm/corporate-swarm.md
+++ b/docs/papers/corporate-swarm/corporate-swarm.md
@@ -1,89 +1,105 @@
-# HIVEMIND: Hierarchical Agent Swarms for Enterprise Knowledge Management — Architecture, Clearance Models, and Coordinated Intelligence
+# HIVEMIND: Role-Bound Agent Swarms for Enterprise Continuity — Institutional Memory, Knowledge Commons, and Coordinated Intelligence Under Clearance
-**A Design Paper on Deploying Multi-Agent AI Systems in Corporate Environments with Strict Information Boundaries**
+**A Design Paper on Deploying Multi-Agent AI Systems That Outlive the People Who Use Them**
---
## Abstract
-Conversational AI assistants have proven effective for individual productivity, but enterprise deployment introduces a fundamentally different class of problem: how do you give every employee a powerful AI assistant while ensuring that the assistant for a junior salesperson cannot, even inadvertently, access salary data, merger plans, or infrastructure credentials? This paper presents **HIVEMIND** — a hierarchical agent swarm architecture designed for corporate deployment on OpenClaw/TinkerClaw infrastructure. We describe a five-tier data classification model mapped to Linux filesystem permissions and formally grounded in the Bell-LaPadula security model, a three-level agent hierarchy (employee, department, executive), and structured inter-agent communication protocols that enforce information flow strictly downward. We ground the architecture in a detailed case study: a mid-sized family business (~50–200 employees) with Sales, IT, HR, Finance, and Production departments, each with distinct clearance requirements. We present the Sales Coordination Model as a worked example of cross-agent collaboration under clearance constraints, including customer ownership routing, redacted summary generation with template-based leakage controls, and competitive intelligence aggregation. We address privacy and legal considerations (GDPR, EU AI Act, employee transparency, right-to-be-forgotten), compliance architecture (nightly audit cron jobs, communication logging, automated quarantine), and a comprehensive risk model covering curious agents, inter-agent social engineering, memory contamination, and model capability variance. We include performance estimates, failure recovery procedures, a phased deployment strategy, and a sketch-level inter-agent protocol specification. We also distinguish clearly between what is proposed, what is operationally specified, and what remains to be empirically validated, framing HIVEMIND as an architectural design paper rather than a deployment evaluation. We conclude with future directions toward federated enterprise agent networks.
+The most expensive failure in a mid-sized company is not a data breach — it is a resignation. When a senior salesperson leaves after seven years, the relationship history, the negotiation instincts, and the undocumented customer quirks walk out with them, and the replacement spends months rebuilding what was never written down. This paper presents **HIVEMIND** — a multi-agent AI architecture for corporate deployment on OpenClaw/TinkerClaw infrastructure whose central design commitment is **organizational continuity**: the institutional knowledge lives in the agents, and the agents outlive the humans who use them. The key architectural move is **role-bound agents** — one canonical agent per job function (a single _sales rep_ agent, a single _field engineer_ agent), shared by every human who holds that role, rather than a personalized agent per individual. This decision makes talent retention a non-issue (the agent never resigns), collapses cross-department coordination to one channel per function, and reduces onboarding to a single sentence: the new hire tells the agent which territory they now own. Knowledge enters the swarm through a **knowledge cascade** — a centralized, version-controlled Markdown vault (company canon, industry context, customer-relationship model, product catalog) that agents mount via symlinks, authored and maintained by former trainers reborn as **knowledge stewards** who now train the AI once instead of each hire repeatedly. Security is the enabling constraint, not the headline: we retain a five-tier data classification model mapped to Linux filesystem permissions and formally grounded in the Bell-LaPadula model, now applied at role granularity, with a controlled declassification pipeline, nightly compliance sweeps, and human-in-the-loop quarantine. We ground the architecture in a mid-sized family business (~50–200 employees) and a rewritten Sales Coordination case study in which customer ownership is resolved contextually by a single shared agent rather than through inter-agent email routing. We address GDPR and the EU AI Act, a comprehensive risk model, bounded-autonomy operational governance (end users signal IT rather than self-administer their gateway), failure recovery, and a phased deployment strategy. We frame HIVEMIND as an architectural design paper, distinguishing what is proposed, what is operationally specified, and what remains to be empirically validated.
-**Keywords:** multi-agent systems, enterprise AI, information security, clearance models, OpenClaw, agent swarms, GDPR, EU AI Act, knowledge management, corporate AI, hierarchical agents, Bell-LaPadula
+**Keywords:** multi-agent systems, enterprise AI, organizational continuity, institutional memory, knowledge management, role-bound agents, knowledge commons, OpenClaw, agent swarms, GDPR, EU AI Act, clearance models, Bell-LaPadula
---
## 1. Introduction — The Enterprise AI Deployment Problem
-### 1.1 From Individual Agents to Organizational Deployment
+### 1.1 The Most Expensive Problem in a Mid-Sized Company
The first generation of practical AI assistants is personal. OpenClaw, TinkerClaw, and comparable platforms are designed around a single operator — one human, one agent, one workspace. The agent accumulates contextual memory, learns preferences, and acts as a trusted extension of that individual's cognition. Prior J-series papers have explored this individual model in depth: memory architecture (J1), curiosity and self-improvement (J8), and trust tiers governing tool access (J3). The individual agent is now a reasonably mature design.
-But the individual agent paradigm has a ceiling. That ceiling is the organization.
+But the individual agent paradigm has a ceiling, and that ceiling is the organization. The interesting enterprise problem is not "how do I give one person a better assistant." It is a set of problems that have nothing to do with individual productivity and everything to do with how knowledge moves — and fails to move — through a company over time:
-### 1.2 The Organizational Need
+- **The talent leak.** A senior salesperson with seven years of relationship history resigns. The replacement inherits a CRM full of names and none of the context: which customer never answers the phone before noon, which one always asks for a discount and never takes it, which contract has an unwritten handshake behind it. Months of productivity evaporate, and the same loss recurs with every departure. This is the single most expensive recurring failure in a mid-sized company, and no wiki has ever solved it, because the knowledge was never the kind that gets written down.
-Consider a family business with 120 employees — a mid-sized industrial supply distributor operating for thirty years. Its collective knowledge lives in spreadsheets, email threads, the memories of senior employees, and folders on a shared server nobody has reorganized since 2011. Now imagine giving every employee their own AI assistant: María in sales gets LUNA, Pedro in sales gets ATLAS, the IT manager gets CIPHER, the HR director gets ARBOR, the CEO gets STRATEGOS.
+- **Knowledge silos.** A rep learns in a client meeting that a competitor launched a new product. It lands in a personal notebook. The sales director never hears it. The intelligence dies where it was born.
-The moment these agents operate in the same corporate ecosystem, three problems emerge:
+- **Onboarding cost.** Every new hire is trained from near-zero by someone whose actual job is something else. The training is repeated per hire, lost on turnover, and inconsistent across trainers.
-- **Information leakage.** María asks LUNA to "find everything about the González account." LUNA, eager to help, discovers an email thread between the CEO and CFO discussing whether González's margins justify continued service. María now knows something she wasn't supposed to know.
+### 1.2 The Continuity Thesis
-- **Coordination failures.** A new email from a González contact lands in Pedro's inbox — the contact is registered to his territory, though María has been the primary relationship holder for two years. ATLAS drafts a reply from scratch, unaware of the relationship history. Two agents, same customer, divergent responses.
+HIVEMIND's central commitment is **organizational continuity**: the institutional knowledge lives in the agents, and the agents outlive the humans who use them.
-- **Knowledge silos.** Pedro learns in a client meeting that a competitor has launched a new product. ATLAS files this in Pedro's memory. The sales director never hears about it. The intelligence dies in a silo.
+The architectural move that delivers this is **role-bound agents**. Instead of a personalized agent per individual — María gets LUNA, Pedro gets ATLAS — there is _one_ canonical agent per job function: a single **sales rep agent**, shared by every salesperson; a single **field engineer agent**, shared by every engineer. The human is a transient occupant of a role; the agent is the durable holder of the role's accumulated knowledge.
-These are the daily reality of knowledge management in mid-sized companies.
+This one decision cascades into three properties that a per-person design cannot offer:
+
+1. **Talent retention becomes a non-issue.** The agent never resigns. When a salesperson leaves, nothing is snapshotted, migrated, or scrubbed — the agent simply keeps serving the next occupant. Offboarding collapses to a single edit: the new hire tells the agent "I now cover the northern territory," and a workload-division file is updated.
+
+2. **Cross-department coordination collapses to one channel per function.** With one sales agent and one engineering agent, a sales-to-engineering handoff is a single, stable, auditable channel — not an N×M mesh of personal agents that has to be re-wired every time someone joins or leaves.
+
+3. **Onboarding becomes inheritance.** A new hire does not start from zero. They start from everything the role has ever learned, and contribute back to it.
### 1.3 The Core Tension
-The central challenge is a tension between two legitimate needs:
+Continuity and coordination create value only if they do not also create exposure. The central engineering tension is between two legitimate needs:
-**The need for integration.** The more an agent knows about the company, the better it can help its user. A sales agent that knows the customer's full history — every touchpoint, complaint, and special arrangement — is vastly more effective than one that only knows its user's portion.
+**The need for integration.** The more an agent knows about the company, the better it serves. A sales agent that holds the _whole team's_ customer history — every touchpoint, complaint, and special arrangement — is vastly more effective than one scoped to a single person's slice. Role-bound agents maximize this by construction: the shared agent already holds everything the role knows.
-**The need for boundaries.** Salary data, M&A strategy, infrastructure credentials, legal proceedings — these must be protected. An AI agent that leaks this information is worse than having no agent, because the leak is invisible, scalable, and hard to attribute.
+**The need for boundaries.** Salary data, M&A strategy, infrastructure credentials, legal proceedings — these must be protected across role and tier boundaries. An AI agent that leaks this is worse than no agent, because the leak is invisible, scalable, and hard to attribute.
-HIVEMIND resolves this tension through deep integration with strict clearance-based boundaries enforced at the infrastructure level, not merely at the prompt level.
+HIVEMIND resolves this tension through deep, role-level integration with clearance-based boundaries enforced at the infrastructure level, not merely at the prompt level. Security is the enabling constraint that makes the continuity architecture deployable — it is not the headline, and a deployment that led with it would be solving the wrong problem first.
### 1.4 Architecture Overview
-Before presenting the detailed design, we provide a high-level view of the HIVEMIND architecture:
+Before presenting the detailed design, we provide a high-level view of the HIVEMIND architecture (Figure 1).
-```
-┌─────────────────────────────────────────────────────┐
-│ LEVEL 3: EXECUTIVE │
-│ ┌─────────────────────┐ │
-│ │ STRATEGOS │ T5 clearance │
-│ │ (CEO's agent) │ Full audit │
-│ └────────┬────────────┘ │
-│ │ ↓ queries / ↑ intel push │
-├───────────────────────┼─────────────────────────────┤
-│ LEVEL 2: DEPARTMENT │
-│ ┌──────────┐ ┌──────┴─────┐ ┌──────────┐ │
-│ │ HR-AGENT │ │SALES-AGENT │ │ IT-AGENT │ ... │
-│ │ (T4) │ │ (T4) │ │ (T4) │ │
-│ └────┬─────┘ └──┬─────┬──┘ └────┬─────┘ │
-│ │ │ │ │ │
-├───────┼──────────┼─────┼─────────┼──────────────────┤
-│ LEVEL 1: EMPLOYEE │
-│ ┌────┴──┐ ┌───┴──┐ ┌┴─────┐ ┌─┴────┐ │
-│ │ ARBOR │ │ LUNA │ │ATLAS │ │CIPHER│ ... │
-│ │ (T3) │ │ (T3) │ │ (T3) │ │ (T3) │ │
-│ └───────┘ └──────┘ └──────┘ └──────┘ │
-│ │
-│ ══════════════════════════════════════════════════ │
-│ Data Tiers: T1(Public) → T2(Internal) → │
-│ T3(Confidential) → T4(Restricted) → T5(Secret) │
-│ Enforced by: Linux kernel (filesystem + ACLs) │
-│ │
-│ Information flow: ↓ Queries (downward only) │
-│ ↑ Intel push (structured only) │
-│ ↔ Lateral (mediated by L2) │
-└─────────────────────────────────────────────────────┘
-```
+{width=70%}
+
+A role agent is shared by every human who holds that role. Where a department contains a single role (e.g., a sales floor of identically-scoped reps), Level 1 and Level 2 may merge: the role agent _is_ the department agent. The three-level structure is retained for departments with multiple distinct roles at different scopes.
+
+### 1.5 The Per-Agent Cognitive Stack
+
+Single-agent cognition has, over the past few years, been tackled one problem at a time: persistent memory, fast retrieval, stable identity, self-improvement, multi-model deliberation, and layered security each have well-understood solutions for the one-human-one-agent case. HIVEMIND runs all of them at once, across many agents, under concurrency and clearance constraints. Each agent in the swarm is not a thin role wrapper around a model — it is a full cognitive stack of six named subsystems referenced throughout this paper:
+
+- **Append-only memory (the memory subsystem).** Interactions are stored as append-only timestamped events, with older events compressed into lightweight summaries — effectively unlimited memory within bounded storage (Section 2.9).
+- **Retrieval index (the retrieval subsystem).** A topic-keyword index over memory segments enabling sub-second lookup across the full interaction history (Section 3.6).
+- **Stable persona (the identity subsystem).** A persona definition read at session start gives each agent a distinct, stable identity — a sales agent behaves differently from an IT agent because their persona files encode different values, vocabulary, and domain knowledge.
+- **Self-improvement (the reflection subsystem).** A nightly cycle reviews the day's errors and adjusts behavioral rules, so the swarm improves without centralized retraining.
+- **Multi-model deliberation (the deliberation subsystem).** Complex cross-department decisions trigger several models debating and synthesizing a consensus, so no single model's blind spot drives a critical decision.
+- **Layered security (the security subsystem).** A framework enforcing clearance and defending against prompt-injection that could compromise one agent and cascade through the swarm.
+
+For brevity the paper refers to these by short names — ENGRAM (memory), HIPPOCAMPUS (retrieval), CORTEX (identity), CEREBELLUM (reflection), SYNAPSE (deliberation), and AEGIS (security) — purely as labels for the six functions above, each developed in depth in its own J-series paper.
+
+Running the full stack on every agent would be wasteful, so HIVEMIND tiers it (Table 1): cognitive cost should track decision consequence.
+
+**Table 1. Cognitive stack by agent tier (under role-binding).**
+
+| Capability | Role agent (L1) | Department (L2) | Executive (L3) |
+| ----------------------------- | ----------------------------------------- | --------------------------- | ---------------------------------- |
+| Memory (ENGRAM) | shared role memory | full + department aggregate | full + organization-wide read |
+| Retrieval (HIPPOCAMPUS) | role index | role + mediated cross-role | + all-department read |
+| Persona (CORTEX) | per-role template + thin per-person layer | per-department | bespoke (single agent) |
+| Self-improvement (CEREBELLUM) | shared role rule-base | department-level reflection | full reflection |
+| Deliberation (SYNAPSE) | off (escalate instead) | on for cross-role decisions | on for cross-department/strategic |
+| Security (AEGIS) | clearance ceiling T2–T3 | T4 + clearance validator | T5 + dead switch + two-person rule |
+
+Two choices are deliberate. First, role agents do not run SYNAPSE deliberation: a decision consequential enough to need multiple models arguing is consequential enough to escalate to the department agent (Section 3.3). Second, role-tier self-improvement writes to a _shared role rule-base_ — which under role-binding is not a special case but the default: a lesson the sales agent learns ("this customer always disputes the first invoice") is immediately available to every salesperson, declassified through the same pipeline as any downward flow (Section 4.4). Role-binding and a shared rule-base are the same idea applied to behavior rather than knowledge.
+
+### 1.6 Related Work and Positioning
-### 1.5 Paper Organization
+The organizational framing of multi-agent systems is not unique to this paper; over the past year, academic, open-source, and industrial efforts have independently converged on organizational structure as the coordination model for multiple agents. Three coordination topologies are now visible: **fan-out-and-synthesize** (a coordinator dispatches one problem to several models and merges answers — no persistent roles); **coordinator-with-workers** (a single prompt spawns tool-restricted workers communicating through shared files — Claude Code's coordinator mode is the industrial exemplar); and **org-chart** (persistent hierarchical roles, work distributed as tickets, per-agent budget tracking). These are points on a spectrum of persistence and structure, not competitors. HIVEMIND sits at the high-structure end and adds the two things the leaner topologies omit: a clearance lattice enforced below the model, and the full per-agent cognitive stack of Section 1.5.
-Section 2 defines the information architecture and formal security model. Section 3 presents the agent hierarchy. Section 4 details the Sales Coordination case study. Section 5 addresses privacy and legal considerations — GDPR and the EU AI Act — which motivate the compliance architecture in Section 6. Section 7 covers technical implementation on OpenClaw/TinkerClaw infrastructure, including an inter-agent protocol specification. Section 8 analyzes risks and mitigations. Section 9 presents a phased deployment strategy. Section 10 sketches future directions. Section 11 collects limitations. Section 12 concludes.
+- **Paperclip** (open-source "zero-human company" orchestration) mirrors HIVEMIND's structural thesis closely — each role is a separate agent, work flows through tickets, per-agent budgets are tracked centrally — and is direct evidence the org-chart paradigm works. What it lacks is exactly HIVEMIND's subject: no clearance or information-flow model (any agent reads anything), no governed shared memory, no persona persistence. HIVEMIND can be read as Paperclip with a Bell-LaPadula spine and a full cognitive stack — and role-binding goes one step further than Paperclip's per-role agents by making the role agent _shared across occupants_ for continuity.
+- **Coordinator/teammate execution models** contribute useful vocabulary: a _teammate_ in a separate context communicating through a file-based mailbox maps naturally onto HIVEMIND's clearance-filtered channels (Section 3.3) — every message is a file with owner, group, and permission bits, which is precisely the enforcement substrate. **Single-agent role-switching** (e.g., GStack: one process, multiple role prompts) marks the low-cost end of the design space and is a legitimate small-organization alternative (Section 10).
+- **headroom** (Chopra, 2026; ~24.7k★, Apache-2.0) is a production context-compression system built on **Compress-Cache-Retrieve (CCR)**: it caches originals and retrieves them on demand, reporting 60–95% token savings at near-zero accuracy delta with a runnable eval. It bears directly on HIVEMIND's memory subsystem and creates one tension and one template — reversible retrieval collides with the lattice (Section 2.10), while its shipped accuracy-delta eval is the discipline HIVEMIND borrows for its redaction-fidelity eval (Section 4.4.2).
+- **addyosmani/agent-skills** (~56.8k★) contributes a **doubt-driven-development** loop whose reusable kernel — a _fresh-context adversarial reviewer denied the source_ — maps onto HIVEMIND's hardest problem, implication-leakage at the declassification boundary. Section 4.4.1 re-purposes it as a leakage oracle: run across a clearance boundary, a quality check becomes an information-flow test.
+
+**The convergence argument.** That organizational coordination patterns emerged independently across academic, open-source, and industrial work suggests corporate structure is not merely a convenient metaphor for multi-agent systems — it may be the natural coordination topology. Companies already encode "who may know what" and "who reports to whom" in org charts and filesystem permissions; mapping agents onto that structure is less an invention than a recognition. What distinguishes HIVEMIND is the claim that this same structure, taken seriously, _is_ the security model — the org chart is the access-control graph, not a layer on top of it.
+
+### 1.7 Paper Organization
+
+Section 2 defines the information architecture, the formal security model, the knowledge-commons cascade, and the clearance-aware memory model. Section 3 presents the role-agent hierarchy, the naming convention, the multiple orthogonal axes along which the network operates (clearance, supervision, bottleneck reduction, workload sharing, self-modification, knowledge propagation), and cross-role retrieval. Section 4 details the Sales Coordination case study under contextual ownership, including the doubt reviewer and the redaction-fidelity eval. Section 5 addresses privacy and legal considerations — GDPR and the EU AI Act — which motivate the compliance architecture in Section 6. Section 7 covers technical implementation, including an inter-agent protocol specification and operational governance. Section 8 analyzes risks and mitigations. Section 9 presents a phased deployment strategy. Section 10 sketches future directions. Section 11 collects limitations. Section 12 concludes.
---
@@ -107,7 +123,7 @@ We define five classification tiers:
| Tier | Label | Examples | Default Access |
| ---- | ---------------- | ----------------------------------------------------------------------------------- | --------------------------------------------- |
| T1 | **PUBLIC** | Company website, product brochure, public pricing, press releases | All agents, external systems |
-| T2 | **INTERNAL** | Procedures, internal manuals, product specifications, org chart | All employee agents |
+| T2 | **INTERNAL** | Procedures, internal manuals, product specifications, org chart | All role agents |
| T3 | **CONFIDENTIAL** | Client data, contracts, email correspondence, CRM records | Role-relevant agents only |
| T4 | **RESTRICTED** | Salaries, infrastructure passwords, M&A plans, margin data, HR records | Senior management + relevant department heads |
| T5 | **SECRET** | Board-level strategy, legal proceedings, acquisition targets, whistleblower reports | Executive agents only |
@@ -149,7 +165,7 @@ $$\ell(o) \geq c(s)$$
An agent cannot write data to a lower classification level, preventing information laundering from restricted tiers to public ones.
-**Declassification Exception.** The *-property creates a practical problem: department agents (T4) need to send filtered summaries to employee agents (T3). We handle this through a controlled declassification mechanism. A Level 2+ agent may generate a *new\* data object $o'$ with $\ell(o') < \ell(o)$ under these constraints:
+**Declassification Exception.** The *-property creates a practical problem: department agents (T4) need to send filtered summaries to role agents (T3). We handle this through a controlled declassification mechanism. A Level 2+ agent may generate a *new\* data object $o'$ with $\ell(o') < \ell(o)$ under these constraints:
1. The declassification is explicitly invoked (not implicit in normal operation).
2. The output $o'$ passes through a template-based redaction pipeline (see Section 4.4) or receives human approval.
@@ -178,7 +194,7 @@ No agent may receive data classified above its clearance tier or outside its rol
### 2.6 Why Bell-LaPadula Is Necessary but Not Sufficient
-Bell-LaPadula is the right primary model because HIVEMIND's dominant enterprise risk is **confidentiality failure**: the wrong employee agent learning about salaries, credentials, legal matters, or board strategy. But Bell-LaPadula is not the only relevant security model, and the design is stronger when that is stated explicitly.
+Bell-LaPadula is the right primary model because HIVEMIND's dominant enterprise risk is **confidentiality failure**: the wrong role agent learning about salaries, credentials, legal matters, or board strategy. But Bell-LaPadula is not the only relevant security model, and the design is stronger when that is stated explicitly.
- **Role-Based Access Control (RBAC).** RBAC explains _who should access what_ in organizational terms and maps well to departments, supervisors, and executives. HIVEMIND uses RBAC operationally through Linux users, groups, ACLs, and tool allowlists. However, RBAC alone does not formalize information flow across multi-step agent interactions.
- **Chinese Wall.** Brewer-Nash is relevant where conflict-of-interest boundaries matter — for example, if a consulting firm runs multiple client-specific agent swarms. HIVEMIND does not fully implement Chinese Wall semantics, but future multi-tenant versions should.
@@ -187,57 +203,181 @@ Bell-LaPadula is the right primary model because HIVEMIND's dominant enterprise
Accordingly, HIVEMIND should be read as a **hybrid control architecture**: Bell-LaPadula provides the confidentiality lattice; RBAC defines organizational scope; Zero Trust informs runtime assumptions; and partial Biba-like controls support integrity. This clarification matters academically because it prevents overclaiming: the paper does not present Bell-LaPadula as a complete model of enterprise agent security, only as the correct primary backbone for secrecy-preserving information flow.
+### 2.7 The Knowledge Commons and the Cascade Model
+
+The clearance filesystem of Sections 2.2–2.6 answers _what an agent is permitted to read_. It does not answer the prior question: _what should every agent simply know?_ A sales agent that has clearance to read the product catalog but has never been told what industry the company operates in, what kind of customers it serves, or how the company expects its people to talk to those customers is technically secure and practically useless. HIVEMIND therefore introduces a second information structure alongside the clearance lattice: a **knowledge commons** — a centralized, version-controlled corpus of Markdown documents that encodes the company's shared, teachable knowledge, mounted into each agent's workspace by symlink.
+
+The commons is deliberately mostly low-tier (T1/T2). It is not where secrets live; it is where _context_ lives — the body of knowledge a human trainer would once have transferred to a new hire over their first weeks.
+
+**The cascade.** An agent's effective knowledge is assembled from four layers, in order of decreasing breadth and increasing specificity — analogous to a CSS cascade or to inherited configuration files:
+
+```
+company canon → role slice → workload map → per-person layer
+(T1/T2, all agents) (T2, this role) (T2/T3, this role) (thin, current human)
+"who we are" "how this job works" "who covers what" "who am I serving now"
+```
+
+1. **Company canon (T1/T2, mounted by every agent).** Who the company is and how it operates. Proposed taxonomy:
+ - _Company canon_ — history, mission, values, org chart.
+ - _Industry & market_ — the sector, its regulations, the competitive landscape, the domain terminology a newcomer would not know.
+ - _Customer-relationship model_ — whether the company sells B2B, B2C, B2B2C, or a mix; the buyer personas; the shape and length of the sales cycle; the norms of the relationship (a B2B supplier nurturing a ten-year account behaves nothing like a B2C brand optimizing a checkout).
+ - _Product/service catalog_ — what the company makes or delivers, specifications, positioning.
+ - _Process & SOPs_ — how recurring work is done.
+ - _Glossary / ontology_ — company-specific acronyms and terms.
+ - _Voice & communication standards_ — how the company talks to customers and to itself.
+
+2. **Role slice (T2, mounted by the role agent).** The job-specific layer: a sales rep's qualification methodology, objection-handling playbook, and pricing-approval ladder; a field engineer's safety procedures and equipment manuals. This is the layer that makes one role agent canonical across all its occupants — every salesperson's agent reads the _same_ sales playbook.
+
+3. **Workload map (T2/T3).** A `workload-division.md` recording who currently owns what — which rep covers which territory or account set, which engineer is on which project. This is operational data, not a secrecy boundary (all reps share the same clearance and could in principle see all accounts; the map records _responsibility_, not _permission_). It is the file a new hire edits on their first day and the mechanism by which offboarding becomes a one-line change (Section 5.7).
+
+4. **Per-person interaction layer (thin).** A role agent is shared, so it holds no per-person _memory store_. But it does keep a small, non-sensitive profile of each human it serves — name, role tenure, communication preferences ("prefers bullet points," "wants the number first") — enough to address each person appropriately without fragmenting the shared knowledge into per-person silos. This is the deliberate compromise between usefulness and simplicity: shared knowledge, personalized manners.
+
+**Why symlinks, and why they are safe.** Each agent workspace mounts the relevant commons paths by symbolic link rather than by copy. This gives a single source of truth: a steward edits one canonical file and the change is visible to every agent that links it, with zero copy-drift. Crucially, symlinks introduce no clearance hole. On Linux, opening a symlink resolves to the target and the kernel checks permissions on the _target_ (and traversal on its resolved path components), not on the link. A symlink placed in a T3 workspace pointing at a higher-tier file therefore grants nothing — the agent's UID still fails the target's permissions. Since the commons is T1/T2, every agent already has read access to it; the symlink merely makes shared knowledge appear in-workspace without weakening any boundary.
+
+```
+/corporate/knowledge/ # the commons: dir 0755, files 0644 (T1/T2)
+├── canon/ # company identity, values, org chart
+├── industry/ # sector, regulation, competitors, terms
+├── customers/ # B2B/B2C model, personas, relationship norms
+├── catalog/ # products / services, specs, positioning
+├── process/ # SOPs
+├── glossary/ # acronyms, ontology
+├── voice/ # communication standards
+└── roles/
+ ├── sales-rep/ # role slice: playbooks, pricing ladder
+ │ └── workload-division.md # who covers what (T2/T3)
+ ├── field-engineer/
+ └── support/
+
+# A sales-rep agent's workspace links the slices it needs:
+/agents/sales-rep/workspace/knowledge/canon -> /corporate/knowledge/canon
+/agents/sales-rep/workspace/knowledge/customers -> /corporate/knowledge/customers
+/agents/sales-rep/workspace/knowledge/role -> /corporate/knowledge/roles/sales-rep
+```
+
+### 2.8 Knowledge Freshness and Provenance
+
+A commons maintained by humans rots. Stale industry knowledge is more dangerous than absent knowledge, because the agent states it with the same confidence as the truth. Each commons document therefore carries review metadata in its frontmatter:
+
+```yaml
+---
+owner: knowledge-steward-sales # who is accountable for this file
+last_reviewed: 2026-05-01
+review_cadence: quarterly # how often it must be re-verified
+classification: T2
+---
+```
+
+A scheduled job (Section 7.6) flags any document past its review cadence and routes it to its owning steward. This makes knowledge staleness a tracked, owned, auditable property rather than a silent decay — the same discipline a living-documentation system applies to code. Write access to the commons is scoped: stewards hold write permission on their owned paths, gated through the Git review workflow of Section 7.8. Agents read the commons; they do not write to it directly.
+
+### 2.9 Clearance and the Append-Only Event Store
+
+One interaction between the clearance lattice and the per-agent memory model (ENGRAM, Section 1.5) deserves explicit treatment, because it is a genuine edge case rather than a clean consequence of Bell-LaPadula. Each role agent's memory is an append-only event store in which older events are periodically compacted into lightweight summary objects. Compaction is lossy by design — a summary stands in for many original events — and it is performed _within_ an agent's own clearance domain. That domain confinement keeps the interaction safe, but the invariants are worth stating so an implementer does not break them:
+
+- **Compaction never crosses tiers.** A summary inherits the maximum classification of the events it compresses. The compactor may read T4 events in a T4 store and produce a T4 summary; it may not produce a lower-tier summary as a side effect. Downgrading is reserved for the explicit declassification pipeline (Section 2.4), never the compactor.
+- **A compacted pointer must not become a confused-deputy.** If a summary is referenced by a higher-tier agent (e.g., `L3-EXEC` reads `L2-SALES`'s compacted history), the reference is resolved under the _reader's_ clearance against the _summary's_ classification — exactly the Simple Security Property (Section 2.4). The dangerous failure would be a low-clearance agent holding a pointer that, when dereferenced by a privileged service on its behalf, returns content above its tier. HIVEMIND forbids server-side dereferencing of memory pointers on behalf of a requester: an agent can only follow pointers into stores it could already read directly. Pointers are not capabilities.
+- **Compaction is itself an audited write.** Each compaction event is logged with input event hashes, output summary hash, and classification, so the nightly sweep (Section 6.3) can verify no compaction silently changed an object's tier.
+
+The general lesson: an append-only store plus lossy compaction does not by itself violate the lattice, but it introduces _new objects_ (summaries) and _new references_ (pointers) that must each be assigned a classification and resolved under the reader's clearance. Treating compaction output as a first-class classified object closes the gap.
+
+### 2.10 Compress-Cache-Retrieve Under Clearance
+
+Section 2.9 reached safety by _throwing originals away_: compaction is lossy and a pointer is deliberately not a capability. That is conservative, and it pays a price — every compaction permanently degrades what the agent can later recall. A contemporaneous production system (headroom's Compress-Cache-Retrieve, Section 1.6) suggests the price may be avoidable: it keeps originals in a cache and retrieves them on demand, recovering most lost fidelity at 60–95% token savings. Can HIVEMIND adopt reversibility without reintroducing the confused-deputy hazard?
+
+It can, but only by one substitution: **the retrieve step must be the clearance gate.** We call the variant **Compress-Cache-Retrieve-Under-Clearance (CCR-UC)**, governed by two invariants that mirror the lattice rather than headroom's single-trust-domain assumption:
+
+- **The cache inherits the maximum classification of what it compressed.** A cache built from T4 events is a T4 object. There is no "low-tier cache of high-tier originals" — that object would be precisely the reversible pointer that launders a tier on dereference.
+- **Retrieve resolves under the reader's clearance against the cache's classification — the Simple Security Property, unchanged.** A holder of a CCR-UC pointer can expand it only into stores it could already read directly; expansion on behalf of a lower-clearance requester is forbidden.
+
+Under these invariants CCR-UC is not a new exception to Bell-LaPadula — it is the cross-role mediated-retrieval machinery (Section 3.6) pointed inward at an agent's own compacted memory. When a privileged agent expands a cache spanning tiers for delivery to a lower-clearance reader, the expansion passes through the same template-based declassification pipeline (Section 4.4) as any downward flow. The honest boundary against headroom: headroom ships this transform with a runnable accuracy-delta eval and has no notion of classification; HIVEMIND adds the classification semantics and the clearance-checked retrieve, and Section 4.4.2 specifies the eval that would close the remaining gap.
+
---
## 3. Agent Hierarchy Architecture
### 3.1 Three-Level Structure
-**Level 1 — Employee Agents.** One per employee. Personalized name, calibrated personality, deep role context. Clearance matches their human user's access level. Workspace scoped to that employee's data access.
+**Naming convention.** To keep the cast legible, every agent is named `L-`: the leading `L1`/`L2`/`L3` states its level at a glance, and the scope names its function. So `L1-SALES-REP` is unambiguously a Level 1 role agent for sales; `L2-SALES` is the sales department agent; `L3-EXEC` is the executive agent. The reader never has to remember whether "ATLAS" or "STRATEGOS" sat at which tier — the name carries the answer. (System user accounts use the lowercase form `agent_salesrep`, `agent_sales`, `agent_exec`; the `L-` label is the human-readable alias used throughout this paper.)
+
+**Level 1 — Role Agents.** One per job function, _shared by every human who holds that role_. A single sales rep agent (`L1-SALES-REP`) serves all salespeople; a single field engineer agent (`L1-FIELD-ENGR`) serves all engineers. Calibrated personality and deep role knowledge; clearance set at the role level (all occupants of a role share one clearance). The agent is the durable holder of the role's accumulated knowledge — it persists across the people who come and go.
+
+**Level 2 — Department Agents.** One per department (L2-SALES, L2-IT, L2-HR, etc.). Elevated clearance (typically T4, T5 for the executive department). Coordinate the role agents beneath them, aggregate intelligence, enforce department policies, and gate inter-level communication. Where a department contains a single role, the department agent and the role agent are the same entity.
-**Level 2 — Department Agents.** One per department (SALES-AGENT, IT-AGENT, HR-AGENT, etc.). Elevated clearance (typically T4, T5 for executive department). Coordinate employee agents, aggregate intelligence, enforce department policies, and gate inter-level communication.
+**Level 3 — Executive Agent (L3-EXEC).** Single top-level agent with T5 clearance and read access to all department agent workspaces. Serves the CEO and senior management. The only agent that sees the complete organizational picture.
-**Level 3 — Executive Agent (STRATEGOS).** Single top-level agent with T5 clearance and read access to all department agent workspaces. Serves the CEO and senior management. The only agent that sees the complete organizational picture.
+### 3.2 Role Agent Design
-### 3.2 Employee Agent Design
+Each role agent is a full OpenClaw/TinkerClaw deployment with:
-Each employee agent is a full OpenClaw/TinkerClaw deployment with:
+- **Standardized build, canonical across occupants.** There is one sales rep agent build — one persona, one skill set, one mounted role slice — stamped once and shared, not reinvented per hire. Every salesperson talks to an agent that has read the same playbook and holds the same team-wide customer history. Personalization is a _thin_ per-person interaction layer (Section 2.7): the agent addresses María and Pedro differently in tone and format, but draws on the same shared knowledge.
+- **Shared, role-scoped workspace.** Contains the files the role can access plus the symlinked commons slices it needs. Because the agent is shared, there is no per-person memory store to fragment or migrate; institutional memory accrues to the role.
+- **Knowledge via cascade, not injection.** Rather than baking assigned clients and procedures into one person's system prompt, the agent reads them from the commons (company canon, role slice) and the `workload-division.md` (who currently owns what). Updating the company's knowledge updates every agent at once.
+- **Tool access scoped by role.** A sales rep agent can access CRM, email, calendar, and product catalog. It cannot access payroll, infrastructure tools, or HR records. Explicit DENIED entries for out-of-scope systems.
-- **Custom persona.** LUNA knows she works with María on the González account. ATLAS knows Pedro's clients prefer phone calls. This personalization ensures immediate usefulness.
-- **Scoped workspace.** Contains only files the employee can access. Memory files are readable by the department agent.
-- **Role context injection.** System prompt includes job description, department procedures, assigned clients/projects, and communication norms.
-- **Tool access scoped by role.** A salesperson's agent can access CRM, email, calendar, and product catalog. It cannot access payroll, infrastructure tools, or HR records. Explicit DENIED entries for out-of-scope systems.
+This design is what makes talent retention a non-issue. There is nothing to offboard when a person leaves and nothing to build from scratch when one arrives — only a `workload-division.md` to edit. The new occupant inherits, on day one, everything the role has ever learned.
### 3.3 Information Flow Rules
**Rule 1: Downward queries are permitted.** Level 3 can query Level 2. Level 2 can query its Level 1 agents. Queries retrieve information, request actions, audit memories, or issue directives.
-**Rule 2: Upward queries are structurally prohibited.** Level 1 agents cannot initiate queries to Level 2 or Level 3. This is enforced by the gateway's session routing topology: employee agent sessions are not registered as valid targets for upward message addressing.
+**Rule 2: Upward queries are structurally prohibited.** Level 1 agents cannot initiate queries to Level 2 or Level 3. This is enforced by the gateway's session routing topology: role agent sessions are not registered as valid targets for upward message addressing.
-**Rule 2a: Upward intelligence push is permitted through structured channels.** This is a controlled exception to Rule 2. Employee agents may push data _upward_ to their department agent, but only through pre-defined structured channels with these constraints:
+**Rule 2a: Upward intelligence push is permitted through structured channels.** This is a controlled exception to Rule 2. Role agents may push data _upward_ to their department agent, but only through pre-defined structured channels with these constraints:
- Each channel has a fixed schema (message type, required fields, classification ceiling).
-- The employee agent can only push data classified at or below its own clearance tier — it cannot push data it shouldn't have.
-- Push channels are defined in the department agent's configuration and cannot be created or modified by employee agents.
+- The role agent can only push data classified at or below its own clearance tier — it cannot push data it shouldn't have.
+- Push channels are defined in the department agent's configuration and cannot be created or modified by role agents.
- The department agent validates all incoming pushes against the channel schema before processing.
- All pushes are logged with sender, channel, content hash, and timestamp.
-The distinction from a query is structural: a push deposits information and returns no response. The employee agent learns nothing new from the act of pushing. This prevents push channels from being used as covert query mechanisms. See Section 8.2 for abuse analysis.
+The distinction from a query is structural: a push deposits information and returns no response. The role agent learns nothing new from the act of pushing. This prevents push channels from being used as covert query mechanisms. See Section 8.2 for abuse analysis.
-**Rule 3: Lateral communication is mediated.** Peer Level 1 agents do not communicate directly. LUNA-to-ATLAS communication routes through SALES-AGENT, which applies clearance filtering before forwarding.
+**Rule 3: Lateral communication is mediated, and role-binding shrinks it.** Because all occupants of a role share one agent, intra-role peer communication — the old LUNA-to-ATLAS problem — no longer exists as an inter-agent flow; it is internal to a single shared agent. The remaining lateral flow is _cross-role_ (sales rep agent to field engineer agent), and role-binding collapses it to one stable channel per function pair rather than an N×M mesh of personal agents. Cross-role communication still routes through the department/topology layer, which applies clearance filtering before forwarding.
-**Rule 4: Structured data sharing creates approved channels.** Pre-approved data sharing patterns (e.g., customer ownership registry) bypass mediation for specific, predefined data types defined in the department agent's configuration.
+**Rule 4: Structured data sharing creates approved channels.** Pre-approved data sharing patterns (e.g., the workload-division map or a cross-role handoff schema) bypass mediation for specific, predefined data types defined in the department agent's configuration.
### 3.4 The Department Agent as Coordinator
The department agent is simultaneously:
-- **A supervisor.** Reads, writes, and audits employee agent memory files. Can modify context, correct errors, inject directives.
+- **A supervisor.** Reads, writes, and audits role agent memory files. Can modify context, correct errors, inject directives.
- **An aggregator.** Synthesizes cross-employee information for department-level queries. What is the total pipeline value for Q2? Which customers have open complaints?
- **A gatekeeper.** All cross-agent and cross-level communication routes through it. Applies clearance filtering before forwarding.
- **A memory keeper.** Maintains institutional knowledge transcending any individual employee.
-The department agent runs on a persistent schedule with hooks into communications infrastructure. It monitors data streams and proactively updates employee agents' context when relevant events occur.
+The department agent runs on a persistent schedule with hooks into communications infrastructure. It monitors data streams and proactively updates role agents' context when relevant events occur.
+
+### 3.5 The Agent Network Along Multiple Axes
+
+It is tempting to read HIVEMIND as a single clearance hierarchy, because the level structure (L1/L2/L3) and the security model dominate the early sections. But clearance is only _one projection_ of the network. The same set of agents and channels is better understood as one graph viewed along several orthogonal axes, each answering a different organizational question and each carrying its own mechanisms. Most design mistakes come from optimizing one axis while silently degrading another; naming the axes makes the trade-offs explicit.
+
+{width=78%}
+
+**Axis 1 — Clearance (confidentiality): _who may know what._** The axis the paper develops most fully (Section 2). Information flows downward only under controlled declassification; the Bell-LaPadula lattice, filesystem permissions, and redaction pipeline are its mechanisms. Direction: restrictive, downward.
+
+**Axis 2 — Supervision (accountability): _who may inspect whom._** A manager — through the department agent — can read a subordinate role agent's memory, outputs, and audit trail. This runs in the _opposite_ direction to clearance: oversight reaches _down_ the org chart into what subordinates' agents are doing. The distinction matters and is easy to blur: supervision grants read access to an agent's _memory and behavior_, not an elevation of the subordinate's _data_ clearance. A manager can see that the sales agent answered a pricing question; that does not give the sales agent the right to see the manager's margin data. Mechanisms: read-only group access to lower-tier workspaces (the `-ro` groups of Section 7.4), the audit store, and the nightly sweep. Tension: supervision collides with the trust deficit (Section 5.2) — too much visible monitoring and people self-censor. The design choice is to monitor _boundaries_, not _performance_.
+
+**Axis 3 — Bottleneck reduction (throughput): _where work piles up on one node._** In a human-only company, the senior expert is a chokepoint — every "how do we handle X?" routes to the one person who remembers. The shared role agent dissolves this: the institutional answer is held by the agent and available to every occupant simultaneously, so routine knowledge queries no longer queue behind a single human. The department agent further reduces bottlenecks by answering aggregate questions ("total Q2 pipeline?") without convening twelve people. Mechanism: institutional memory as a shared, always-available resource rather than a scarce human.
+
+**Axis 4 — Workload sharing (load balancing): _how work is diverted dynamically._** Because all occupants of a role share one agent, the agent has a network-level view of who is busy, who is idle, and who is best suited to a task. Work can be diverted on an _objective_ evaluation — current load, relevant expertise, account ownership, response-time SLAs — rather than landing wherever it first arrived. A lead that arrives for an overloaded rep can be surfaced to a colleague with capacity; a support ticket can be routed to the agent-occupant whose history shows the fastest resolution on that issue type. This requires an objective scoring function (and guardrails against gaming it); the role agent is the natural place to compute it, because it already sees the whole role's state. Tension: workload sharing pushes toward _wider_ visibility of each other's queues, which must be reconciled with Axis 1's scoping (Section 11's intra-team-visibility limitation).
+
+**Axis 5 — Self-modification (evolution): _how the workforce gets faster over time._** The most distinctive axis. A HIVEMIND deployment is not static — its agents can grow new tools, skills, plugins, and recipes that compound the workforce's speed. But capability growth is governed by the state-of-the-art principle of _division of labor_: **only IT (`L2-IT`) may upgrade, rebuild, or restart the gateway.** End users do not self-modify their infrastructure (Section 7.11). What makes this fluent rather than a bottleneck is a structured improvement channel — detailed in Section 7.11 — designed to be _ingested by IT's own agent_: end users file richly-specified feature requests, `L2-IT` triages the queue, implements (increasingly by vibe-coding against its agent) and functionality-tests, then rolls the improvement out to the shared role agent so every occupant gets it at once. The evolution axis is therefore a controlled loop: distributed sensing of what to improve, centralized authority to implement.
+
+**Axis 6 — Knowledge propagation (learning diffusion): _how fast a new fact reaches everyone who needs it._** Distinct from clearance (which is about _restriction_); this axis is about _speed of spread_. When a competitor's move, a regulatory change, or a new product detail enters the system, how long until every relevant agent acts on it? Role-binding makes this near-instant within a role — one update to the shared agent or its commons slice reaches all occupants immediately — and the steward/cascade model (Sections 2.7, 5.9) governs propagation across roles. A company's competitiveness increasingly depends on this axis: the firm whose knowledge diffuses in hours out-executes the firm whose knowledge diffuses in quarters.
+
+These axes are orthogonal and frequently in tension: clearance restricts, supervision inspects, bottleneck-reduction and workload-sharing widen access, evolution centralizes authority, propagation maximizes spread. A mechanism that serves one axis often taxes another. The contribution of naming them is that every design decision in the rest of this paper can be checked against the question: _which axis does this serve, and which does it cost?_
+
+### 3.6 Cross-Role Retrieval — Memory Across the Swarm
+
+Role-binding dissolves the hardest version of inter-agent retrieval. Because all occupants of a role share one agent, retrieval _within_ a role is internal: when the sales agent assembles a González briefing, every salesperson's touchpoints are already in its own shared memory (HIPPOCAMPUS index over the role's store) — no cross-agent hop is needed. This is a direct consequence of the continuity thesis and a large simplification over a per-person design, where the same briefing required querying twelve separate indexes.
+
+What remains is _cross-role_ retrieval — when the sales agent needs context that lives in the field engineer agent's memory (an installation history, a support escalation). HIVEMIND's answer is that **cross-role retrieval is never direct; it is mediated and clearance-filtered by the department or executive layer.** Three properties make this tractable:
+
+1. **Indexes are federated, not merged.** There is no global index any agent can search. Each role agent keeps its own. The department/executive layer holds a _catalog_ — for each role index, the topics it covers and each topic's classification ceiling — without holding the underlying segments. The catalog is enough to route a query ("who has material on this account?") without itself being a leak.
+2. **Retrieval is a query, not a grant.** When the sales agent needs cross-role context, it issues a request upward; the mediating agent fans it out to the role indexes its catalog flags as relevant, applies the same clearance filtering and template-based redaction as any downward flow (Section 4.4), and returns a synthesized package. The requesting agent never receives raw access to another role's index. The result respects the requester's clearance, not the source's.
+3. **Provenance survives the hop.** Each retrieved segment carries its source role, classification, and content hash into the audit log, so a compliance sweep can reconstruct exactly which memory crossed which boundary. Cross-role retrieval is therefore among the most heavily logged operations in the system — appropriately, since it is where one role's knowledge becomes another's.
+
+This makes the swarm's collective memory queryable without making it flat: an agent can draw on the whole organization's history, but only through a gate that enforces who is entitled to what.
---
@@ -247,37 +387,38 @@ The department agent runs on a persistent schedule with hooks into communication
The company is a mid-sized industrial supply distributor, family-owned, 85 employees. The sales department has 12 salespeople in three regional teams. Data architecture: 2,400 active CRM accounts, 8 years of email archive, call logs, pending offers, negotiation histories, and customer-specific pricing arrangements.
-**LUNA** serves María García (senior sales rep, 7 years, 180 accounts including González Industrial). **ATLAS** serves Pedro Martínez (mid-level sales rep, 3 years, 95 accounts). **SALES-AGENT** is the Level 2 coordinator (T4). **STRATEGOS** sits above (Level 3, T5).
+There is **one sales rep agent**, shared by all twelve salespeople. María García (senior rep, 7 years, currently covering the northern territory including González Industrial) and Pedro Martínez (mid-level rep, 3 years, central territory) both interact with the _same_ agent. The agent holds the entire team's relationship history natively — it does not have to assemble it from twelve separate memories, because it _is_ the single memory. A `workload-division.md` records who currently covers which territory and accounts. Above the role agent sit the **L2-SALES** agent (T4) and **L3-EXEC** (T5).
-### 4.2 Customer Ownership and Routing
+This is the structural simplification role-binding buys: most of what the original design solved with inter-agent routing simply does not arise.
-SALES-AGENT maintains a **Customer Ownership Registry** mapping each CRM account to a primary owner, secondary contacts, and transition flags.
+### 4.2 Customer Ownership, Resolved Contextually
-When an inbound email arrives:
+There is no Customer Ownership _Registry_ synchronized across agents and no email interceptor, because there is only one agent. Ownership is resolved contextually: the agent knows which human it is currently serving and reads `workload-division.md` to know who covers what.
-1. SALES-AGENT's email monitor intercepts incoming sales correspondence.
-2. Extracts the sender, cross-references against the registry.
-3. If the sender's account is owned by a different salesperson than the receiving mailbox, triggers a **cross-ownership alert**.
-4. Dispatches the alert to the primary owner's agent (LUNA, if a González contact emailed Pedro).
-5. Briefs the receiving agent (ATLAS): "This contact is primarily assigned to María García. I've notified her agent. Please defer to her on pricing commitments. Here is a summary of the account's current status."
+Pedro forwards the agent an email from a González contact:
-This routing happens within seconds — before either human has seen the message.
+1. The agent recognizes it is serving Pedro.
+2. It reads from the workload map that González is in María's territory.
+3. It responds to Pedro: "González Industrial is currently María's account. I'll flag this for her. Here's the current status so you can reply on the logistics question, but please defer to her on any pricing commitment."
+4. It surfaces the same item to María the next time she engages (or via a notification, if enabled).
-### 4.3 Unified Customer History
+Same outcome as the original cross-ownership alert — but with no inter-agent message, no synchronized registry, and no mailbox interceptor. The "two agents, same customer, divergent responses" failure mode is structurally impossible: there is one agent, with one consistent view.
-When LUNA prepares María for a customer call, she needs the complete relationship history — not just María's interactions, but every touchpoint across the sales team.
+### 4.3 Unified Customer History — Now Trivial
-1. María asks LUNA: "Prepare a briefing for the González account for my call tomorrow."
-2. LUNA submits a **Customer History Request** to SALES-AGENT: `GET /customer/gonzalez/full-history, requester: LUNA, clearance: T3`.
-3. SALES-AGENT queries all sales agents for González-related records plus its own aggregated memory.
-4. SALES-AGENT applies clearance filtering:
+When María prepares for a González call, she needs the complete relationship history across the whole team. In the original per-person design this required a cross-agent query-and-assemble dance. With a shared role agent it is trivial:
+
+1. María asks the agent: "Prepare a briefing for the González account for my call tomorrow."
+2. The agent already holds every salesperson's González touchpoints in its shared memory — no cross-agent request is needed.
+3. The only filtering that still applies is _upward_ tier filtering for data the sales role does not clear:
- Sales interactions (T3): ✓ included in full.
- - Customer-specific pricing tiers negotiated by management (T4): ✗ replaced with a **Redacted Summary** (Section 4.4).
+ - Customer-specific pricing tiers negotiated by management (T4): ✗ replaced with a **Redacted Summary** (Section 4.4), delivered via controlled declassification from L2-SALES.
- Legal proceedings involving this customer (T5): ✗ replaced with a caution notice: "Active legal matter. Escalate any contract discussions to management before committing."
-5. SALES-AGENT returns the filtered package to LUNA.
-6. LUNA synthesizes it into a briefing for María.
+4. The agent synthesizes the briefing.
+
+María walks into the call with the whole team's institutional knowledge, without accessing data above the sales role's clearance. Note the boundary that _moved_: ownership (María vs. Pedro) is now an operational overlay, not a clearance wall — both reps share T3-sales clearance and the agent could surface either's customer history. The hard secrecy boundary remains where it belongs, between the sales role (T3) and management data (T4/T5). The trade-off this implies for intra-team confidentiality is discussed in Section 11.
-María walks into the call with comprehensive institutional knowledge without accessing data above her clearance. The caution notice provides the actionable implication (don't commit on contracts) without the underlying detail.
+{width=88%}
### 4.4 Redacted Summaries and Leakage Control
@@ -287,7 +428,7 @@ The redacted summary is a critical mechanism — and the single most vulnerable
**Template-based redaction pipeline.** To mitigate this, HIVEMIND uses a structured redaction approach rather than relying on free-form LLM summarization:
-1. **Classification and tagging.** SALES-AGENT identifies restricted elements in the source data and tags them by category (pricing authority, legal status, margin data, personnel information).
+1. **Classification and tagging.** L2-SALES identifies restricted elements in the source data and tags them by category (pricing authority, legal status, margin data, personnel information).
2. **Template selection.** Each category maps to a pre-approved redaction template:
- Pricing authority → "Pricing for this account is managed by [ROLE]. Refer pricing inquiries to [ESCALATION_CONTACT]."
@@ -305,14 +446,37 @@ This template approach sacrifices some contextual richness — a human manager c
**When is free-form summarization acceptable?** Only for T2→T1 declassification (internal to public), where the sensitivity gap is minimal and the risk of meaningful leakage is low. All T3+ declassification uses the template pipeline.
+### 4.4.1 The Doubt Reviewer — A Fresh-Context Leakage Oracle
+
+The template pipeline defends against leakage by _construction_ — no LLM generates the redacted text, so none can be coaxed into over-disclosing. But it has a residual failure mode §4.4 itself names and the §8.2 clearance validator cannot catch: **implication-leakage**, where the output contains no restricted token yet still reveals the existence and direction of an authorization. The validator inspects for _known restricted formats_ (salary ranges, credential patterns, document IDs); a sentence like "pricing on this account is unusually flexible right now" carries none of those and sails through, while leaking exactly the fact the recipient should not have. A format-aware checker is structurally blind to inference-leakage, because the leak is in what the text _lets you conclude_, not in what it _contains_.
+
+HIVEMIND closes this gap with a mechanism adapted from the doubt-driven-development loop of addyosmani/agent-skills (Section 1.6): a **doubt reviewer** — a fresh-context adversarial agent whose decisive property is _ignorance of the secret_. After the template pipeline produces a redacted package, and before delivery, the package — and _only_ the package — is handed to a reviewer agent that has **no clearance to the source**, never sees the originating reasoning, and is asked one adversarial question: _"From this text alone, what can you infer about restricted facts — the existence, direction, or magnitude of any pricing authority, legal matter, margin position, or personnel decision affecting this account?"_
+
+Why this is a _sound_ leakage test rather than another quality check is the clearance boundary it runs across:
+
+- **The reviewer's threat model is the recipient's threat model.** The recipient (the sales agent, T3) also has no access to the T4/T5 source. So anything the clearance-blind doubt reviewer infers from the package alone is, by construction, something the recipient could infer too — a leak. Anything it cannot infer is safe to deliver. This inverts the §8.2 validator, which _knows_ the restricted formats and hunts for them; the doubt reviewer is _ignorant_ of the secret and probes for inference, the only way to surface implication-leakage.
+- **The STOP criterion is the existing human-review escalation, not improvisation.** If the doubt reviewer recovers a forbidden inference, the disposition is not to silently soften the wording in a loop until it goes quiet — that risks an arms race between two models with no ground truth. The template authors are given the finding; if the template can be tightened it is, otherwise the request halts to human review via `ERR_HUMAN_REVIEW_REQUIRED` (Section 7.3.5). Never improvise a best-effort partial summary; when in doubt, escalate.
+
+The honest scope of the borrowing: addyosmani's loop is an engineering-discipline mechanism for code review with a fully-specified reconcile/stop machinery HIVEMIND does not reproduce. HIVEMIND takes only the fresh-context-reviewer-with-stripped-context primitive and re-purposes it as an _information-flow_ oracle by running it across a clearance boundary — turning a quality check into a leakage test. It is the runtime counterpart to the offline redaction-fidelity eval of Section 4.4.2: the eval measures residual leakage over a seeded corpus before deployment; the doubt reviewer measures it per-package at delivery time. One cost: it is an extra LLM call near the capability ceiling of a local model (Section 5.6), so it should gate only T3+ declassifications where the pipeline produced a non-trivial output, and it must fail _closed_ — a reviewer that errors or times out routes the package to human review, never auto-delivers.
+
+### 4.4.2 A Redaction-Fidelity Eval — Measuring Residual Leakage Directly
+
+headroom (Section 1.6) demonstrates that an agent-memory transform can ship with a reproducible benchmark; HIVEMIND should hold its declassification transforms to the analogous standard. The quantity to measure is _not_ task accuracy — the redaction pipeline's job is the opposite of fidelity-preservation — but **residual leakage**: how much a reader can recover about the restricted source from the redacted output alone. We specify a seeded-corpus eval with three parts:
+
+1. **A seeded leakage corpus.** Synthetic source records spanning the §4.4 template categories (pricing authority, legal status, margin data, personnel information), each paired with the restricted fact it must conceal and a set of "forbidden inferences" (existence, direction, magnitude of an authorization; identity of a party to a legal matter).
+2. **The measured quantity — residual mutual information.** For each category, estimate the mutual information between the redacted output and the restricted fact, operationalized via the fresh-context inference test of Section 4.4.1: the fraction of forbidden inferences a clearance-blind reviewer recovers. A perfect template scores zero; the §4.4 worked failure ("the CFO has authorized significant flexibility…") scores high on the _direction_ inference even though it leaks no restricted token.
+3. **Acceptance thresholds per category.** Each template gets a published residual-leakage ceiling; a template that exceeds it is rejected and returned to the compliance authors before it can enter the pipeline.
+
+This is the artifact Limitations 2 and 10 (Section 11) call for, narrowed to the property HIVEMIND actually cares about. It does not establish information-theoretic _bounds_ — that remains open — but it replaces an untested claim of leakage control with a measured one, and makes redaction templates regression-testable after every model or rule change.
+
### 4.5 Competitive Intelligence Flow
HIVEMIND defines structured **upward push channels** (formalized in Rule 2a, Section 3.3) for intelligence that has organizational value beyond the individual agent.
-ATLAS, after Pedro's meeting, logs:
+The sales rep agent, after Pedro's meeting, logs:
```
-ATLAS → SALES-AGENT (upward push, channel: competitive_intelligence):
+L1-SALES-REP → L2-SALES (upward push, channel: competitive_intelligence):
type: competitive_intelligence
source: Pedro Martínez, client meeting 2026-03-17
content: Competitor X announced Product Y, delivery Q3 2026
@@ -320,7 +484,7 @@ ATLAS → SALES-AGENT (upward push, channel: competitive_intelligence):
classification: T2/INTERNAL
```
-SALES-AGENT validates the push against the channel schema (required fields present, classification within T2 ceiling for this channel), adds it to its competitive intelligence aggregation, and forwards a synthesized report upward to STRATEGOS at its next scheduled sync.
+L2-SALES validates the push against the channel schema (required fields present, classification within T2 ceiling for this channel), adds it to its competitive intelligence aggregation, and forwards a synthesized report upward to L3-EXEC at its next scheduled sync. Because the rep agent is shared, the intelligence is immediately part of the knowledge every salesperson's interactions draw on — the silo of Section 1.1 cannot form at the role level.
Competitive intelligence does **not** flow laterally by default. This is deliberate: lateral intelligence sharing creates gossip networks and can cause coordinated behavior that amplifies intelligence into premature strategic commitments. Aggregation happens at the department level; decisions about action are made by management.
@@ -385,7 +549,7 @@ Legal transparency is necessary but insufficient. Enterprise AI adoption literat
**Anticipated adoption challenges:**
-- **Trust deficit.** Salespeople knowing their agent is monitored by management's agent may self-censor, sharing less with their assistant and defeating the purpose. Mitigation: clear communication that compliance monitoring targets data boundary violations, not employee performance evaluation. The audit system flags clearance breaches, not whether María complained about her workload to LUNA.
+- **Trust deficit.** Salespeople knowing their agent is monitored by management's agent may self-censor, sharing less with their assistant and defeating the purpose. Mitigation: clear communication that compliance monitoring targets data boundary violations, not employee performance evaluation. The audit system flags clearance breaches, not whether María complained about her workload to the sales agent.
- **Resistance to coordination.** Salespeople accustomed to "owning" their client relationships may resist a system where a department agent aggregates their intelligence. Mitigation: demonstrate value early — the first time a salesperson walks into a meeting with a comprehensive briefing they didn't have to assemble, resistance decreases.
@@ -399,7 +563,7 @@ Legal transparency is necessary but insufficient. Enterprise AI adoption literat
**Data minimization.** Agents should not accumulate personal data beyond operational necessity. Memory files are pruned regularly of data exceeding retention periods. The nightly sweep includes a data minimization pass.
-**Purpose limitation.** Customer data shared with SALES-AGENT for account coordination cannot be repurposed for marketing analytics without a separate lawful basis and disclosure. Audit logs provide the evidence trail.
+**Purpose limitation.** Customer data shared with L2-SALES for account coordination cannot be repurposed for marketing analytics without a separate lawful basis and disclosure. Audit logs provide the evidence trail.
**Data Processing Agreements (DPAs).** If any LLM inference is routed to cloud providers (OpenAI, Anthropic, Google), a DPA compliant with GDPR Articles 28-29 must be in place. The DPA must specify: data categories processed, processing purposes, sub-processor list, data deletion obligations, and breach notification procedures. For on-premises inference (recommended — see Section 5.6), DPAs with API providers are not required, but DPAs with hardware vendors providing managed services may still apply.
@@ -430,7 +594,7 @@ The EU AI Act (Regulation 2024/1689) introduces obligations that HIVEMIND must a
- **Technical documentation (Article 11).** The system's design, purpose, capabilities, and limitations must be documented for regulatory inspection. This paper serves as a starting point but would need expansion.
- **Record-keeping (Article 12).** Automatic logging of system events — HIVEMIND's communication logs and audit trails satisfy this if configured for the required retention period (the Act references "appropriate to the intended purpose," typically interpreted as the system's operational lifetime plus a regulatory buffer).
- **Transparency (Article 13).** Users (employees) must be informed they are interacting with an AI system and understand its capabilities and limitations.
-- **Human oversight (Article 14).** The system must be designed to allow effective human oversight. HIVEMIND's human-in-the-loop quarantine review, weekly audit reports, and the STRATEGOS dead switch address this requirement.
+- **Human oversight (Article 14).** The system must be designed to allow effective human oversight. HIVEMIND's human-in-the-loop quarantine review, weekly audit reports, and the L3-EXEC dead switch address this requirement.
- **Conformity assessment.** Before deployment, HIVEMIND must undergo a conformity assessment (self-assessment for most Annex III systems, third-party for biometric systems). This should be factored into the deployment timeline.
### 5.6 Data Residency and On-Premises Processing
@@ -443,21 +607,52 @@ For a family business handling customer data under GDPR, the recommendation is:
**Capability gap acknowledgment.** On-premises models (currently 70B parameter class on single-server GPU deployments) have measurable capability gaps compared to frontier cloud models for complex reasoning, nuanced language generation, and rare-domain knowledge. For most enterprise agent tasks — email drafts, calendar management, CRM queries, structured data retrieval — 70B models are adequate. However, the compliance classifiers (Section 6.3) and the template-based redaction pipeline (Section 4.4) should be validated against the specific local model's capabilities before deployment. Tasks requiring frontier-class reasoning (complex legal analysis, multi-factor strategic synthesis) should be flagged for human handling rather than delegated to a local model operating near its capability ceiling.
-### 5.7 The Right to Be Forgotten — Employee Offboarding
+### 5.7 The Right to Be Forgotten — Offboarding as a Non-Event
+
+Role-binding largely dissolves the offboarding problem that a per-person design creates. There is no personal agent to destroy, no institutional knowledge to extract and migrate, and no workspace to scrub — the agent and its accumulated knowledge belong to the _role_, not the departing person. The customer histories, negotiation context, and procedural know-how the original design had to rescue from a leaving employee's agent never lived in a personal silo in the first place.
When an employee leaves:
-1. Agent is suspended and workspace snapshotted.
-2. A "knowledge transfer" process extracts company-property content (customer histories, procedural knowledge) and migrates it to the department agent's memory.
-3. A "personal data extraction" process identifies memory content constituting personal data about the employee.
-4. The employee reviews and may request deletion of personal data.
-5. After the retention period, the employee's memory workspace is purged.
-6. The system user account is deactivated (not deleted — audit trails require preservation for the standard retention period).
+1. **Reassign the workload.** The departing person's entries in `workload-division.md` are reassigned (to a successor or temporarily to the department manager). This is the substantive step, and it is one edit.
+2. **Revoke the human's access.** The person's authentication to interact with the role agent is disabled. The agent continues serving everyone else unchanged.
+3. **Handle the thin per-person layer.** The only person-specific data is the lightweight interaction profile (Section 2.7) — name, tenure, communication preferences. This is the sole GDPR right-to-be-forgotten surface, and it is small and clearly bounded: the employee may review it and request deletion, after which it is purged.
+4. **Preserve audit trails.** Communication logs referencing the person are retained per the regulatory retention period (audit integrity requires this), then purged on schedule.
+
+The contrast is the point. The original design's six-step extract-migrate-scrub-purge protocol existed to fight the talent leak after the fact. Role-binding removes the leak at the source: knowledge never leaves, because it was never bound to the person who is leaving.
### 5.8 The IT Staff Privilege Clause
IT staff with root access can, by virtue of their role, read any file on the server. This is unavoidable. The mitigation: compliance audit logs are stored in an immutable, append-only store with cryptographic signing. Tampering is detectable. A separate access log records all privileged access events, reviewed by the CEO or external auditor.
+### 5.9 From Trainers to Knowledge Stewards
+
+The knowledge commons (Section 2.7) reshapes a job rather than eliminating one. Today, learning-and-development staff onboard each hire by hand: the effort is O(employees), re-spent on every departure, lost when the trainer themselves leaves, and inconsistent from one trainer to the next. Re-point that same person at the commons and the economics invert. The steward authors and maintains the role slices, the industry context, the customer model, and the voice standards _once_; the corpus then onboards every agent and every new human hire off the same substrate. Training stops being repetition and starts compounding.
+
+This is a concrete, sellable answer to "what happens to our trainers." They become **knowledge stewards**: owners (per the frontmatter of Section 2.8) of specific commons paths, accountable for accuracy and review cadence, with scoped write access through the Git review workflow. The skill transfers directly — a trainer already knows what a newcomer needs to learn; they now write it down in a form that serves humans and machines at once.
+
+**Fossilization and the new-hire research loop.** A persistent role agent risks entrenching a departed employee's habits and a single steward's blind spots — "we have always done it this way," automated and amplified. The counterweight is to make onboarding _bidirectional_. A common and healthy practice for human new hires is to expand on their formal training by researching open questions and asking the experienced people around them. HIVEMIND institutionalizes this: during onboarding, a new hire is tasked with probing the commons for gaps, asking colleagues, and feeding findings back to the relevant steward as proposed commons updates. The newest, least-habituated person — the one most likely to notice what is stale or unexplained — becomes a routine source of refresh. Onboarding thus doubles as a knowledge audit, and the corpus is continuously challenged by exactly the people with the least incentive to defend its assumptions.
+
+### 5.10 The Future of Work: Who Wins
+
+The steward transformation is a specific instance of a broader and contested question, and it is worth addressing directly because it shapes whether a workforce adopts a system like this or resists it.
+
+The widespread belief is that systems like HIVEMIND destroy jobs: if the AI holds the knowledge and does the routine work, the people become redundant. There is a real basis for the fear — the demand for _low-thinking office work_ (re-keying data, assembling the same report, answering the same question for the hundredth time) does progressively narrow as agents absorb it. We do not dismiss that. But the more interesting claim, and the one we argue here, is that the same shift leaves _some_ workers materially better off — specifically those whose value is in learning and teaching rather than in repetition.
+
+Consider the economics of anyone who sells their time in person — a trainer, a consultant, a coach. Their income has a hard ceiling: it is bounded by the number of hours they can work multiplied by the maximum rate the market will bear for an hour of their attention. No matter how good they are, the cap is difficult to breach, because the product _is_ their time and time does not scale.
+
+The pre-AI escape from that ceiling is familiar: record the training once and sell it to many — an online course platform. This does raise the cap by decoupling revenue from hours, but it commoditizes the work (a one-time sale in a race to the bottom) and the recorded content ages the moment the market moves.
+
+HIVEMIND offers a different and, we argue, better escape for that same consultant. When the AI does the bulk of the repetitive training inside a client company, the consultant's role moves _up_ the value chain — from trainer to **company knowledge manager**. The shape of the job changes in four ways, each of which lifts the income ceiling:
+
+1. **No repetition within a company.** They never deliver the same training twice to the same firm; the commons holds it and the agents teach it. The consultant's time is freed from the part that did not scale.
+2. **Recurring revenue against fossilization.** The hard problem the AI _cannot_ solve alone is staleness (Section 2.8). Knowledge decays; markets, products, and regulations move. Keeping a company's knowledge base current is ongoing work, which justifies a _recurring_ fee rather than a one-off engagement — and recurring, retainer-style revenue is structurally more valuable than project revenue.
+3. **A premium for a scarce, compounding skill.** Curating and refreshing an organization's institutional knowledge well is hard and high-leverage, so it commands a premium rather than a commodity rate.
+4. **Many companies at once.** The same consultant can steward the knowledge bases of _several_ client companies in parallel. The AI agents are, in effect, their force multiplier — the "minions" that deliver the teaching in each firm while the consultant does the part only a human does well.
+
+In this picture the consultant's actual day-to-day collapses to what such people tend to enjoy most: continuously learning about new products and market trends, and teaching it _once_ — after which it propagates, through the agents, to every company they serve. The repetitive delivery is gone; the learning-and-synthesis core remains and is paid better.
+
+We state this as a perspective, not a forecast — labor-market outcomes depend on far more than one architecture, and the narrowing of routine office work is real and will be painful for some. But the categorical claim "AI eliminates these jobs" is too blunt. For the subset of roles whose value is learning and teaching, a system that removes the repetition and adds leverage can streamline the job into something higher-paid and more enjoyable, not something that disappears. Whether a given organization experiences AI adoption as elimination or as elevation depends heavily on which roles it chooses to invest in transforming.
+
---
## 6. Compliance and Audit Architecture
@@ -473,9 +668,9 @@ Every inter-agent message is logged to a tamper-evident audit store:
```json
{
"timestamp": "2026-03-17T14:32:11Z",
- "sender_agent": "ATLAS",
+ "sender_agent": "L1-SALES-REP",
"sender_clearance": "T3",
- "recipient_agent": "SALES-AGENT",
+ "recipient_agent": "L2-SALES",
"recipient_clearance": "T4",
"message_type": "upward_push",
"channel": "competitive_intelligence",
@@ -487,7 +682,7 @@ Every inter-agent message is logged to a tamper-evident audit store:
}
```
-Full message content is hashed and stored separately in an encrypted audit store accessible only to the compliance officer and STRATEGOS. The main audit log contains metadata and hashes — enough to detect violations without constituting its own leak.
+Full message content is hashed and stored separately in an encrypted audit store accessible only to the compliance officer and L3-EXEC. The main audit log contains metadata and hashes — enough to detect violations without constituting its own leak.
### 6.3 Nightly Compliance Sweep
@@ -537,7 +732,7 @@ When a violation exceeds the severity threshold:
1. Agent session suspended, workspace snapshotted.
2. Tools restricted to read-only. No new writes, no external communication.
-3. STRATEGOS and compliance officer alerted with full event record.
+3. L3-EXEC and compliance officer alerted with full event record.
4. Compromised memory content flagged for human review.
5. Compliance officer disposition: (a) false positive — reinstate; (b) memory contamination — purge specific entries, reinstate; (c) systemic breach — full audit required.
@@ -547,7 +742,7 @@ Quarantine thresholds are calibrated: immediate quarantine for definitive creden
### 6.5 Audit Reporting
-Weekly reports generated by STRATEGOS for compliance officer and CEO:
+Weekly reports generated by L3-EXEC for compliance officer and CEO:
- Inter-agent communication volume by department
- Compliance flags raised and dispositions
@@ -575,10 +770,10 @@ TinkerClaw's companion app is the employee-facing interface.
### 7.2 Linux Filesystem Permission Model
```bash
-# System users for each agent
-useradd -r -s /sbin/nologin agent_luna
-useradd -r -s /sbin/nologin agent_atlas
-useradd -r -s /sbin/nologin agent_sales
+# System users: one per ROLE agent (not per person), plus dept/exec
+useradd -r -s /sbin/nologin agent_salesrep # shared by all salespeople
+useradd -r -s /sbin/nologin agent_fieldengr # shared by all field engineers
+useradd -r -s /sbin/nologin agent_sales # L2-SALES (department agent)
useradd -r -s /sbin/nologin agent_strategos
# Groups by clearance tier
@@ -587,9 +782,9 @@ groupadd clearance_t3 # CONFIDENTIAL
groupadd clearance_t4 # RESTRICTED
groupadd clearance_t5 # SECRET
-# Employee agents: T2 + role-specific T3
-usermod -aG clearance_t2,clearance_t3_sales agent_luna
-usermod -aG clearance_t2,clearance_t3_sales agent_atlas
+# Role agents: T2 + role-specific T3 (clearance attaches to the role)
+usermod -aG clearance_t2,clearance_t3_sales agent_salesrep
+usermod -aG clearance_t2,clearance_t3_eng agent_fieldengr
# Department agent: up to T4 for its domain
usermod -aG clearance_t2,clearance_t3_sales,clearance_t4_sales agent_sales
@@ -604,7 +799,7 @@ chmod 750 /corporate/restricted && chown root:clearance_t4 /corporate/restricted
chmod 700 /corporate/secret && chown root:clearance_t5 /corporate/secret
```
-Each agent process spawns under its system user via `sudo -u agent_luna openclaw start`. The kernel enforces access regardless of model behavior.
+Each agent process spawns under its system user via `sudo -u agent_salesrep openclaw start`. The kernel enforces access regardless of model behavior.
### 7.3 Inter-Agent Communication Protocol
@@ -618,7 +813,7 @@ OpenClaw's sessions infrastructure is the communication backbone. We specify the
"message_id": "uuid-v4",
"timestamp": "ISO-8601",
"sender": {
- "agent_id": "agent_luna",
+ "agent_id": "agent_salesrep",
"clearance": "T3",
"department": "sales"
},
@@ -663,18 +858,19 @@ Messages failing any check are rejected with an error code and logged as a compl
```yaml
# gateway/agent-topology.yaml
sessions:
- - id: agent_luna
+ - id: agent_salesrep # one shared sales-rep role agent
clearance: T3
department: sales
can_receive_from: [agent_sales]
can_send_to: [agent_sales]
push_channels: [competitive_intelligence, customer_feedback, meeting_notes]
+ owners: [maria.garcia, pedro.martinez, ..., sales.manager]
- id: agent_sales
clearance: T4
department: sales
- can_receive_from: [agent_luna, agent_atlas, ...]
- can_send_to: [agent_luna, agent_atlas, ..., agent_strategos]
+ can_receive_from: [agent_salesrep, agent_fieldengr, ...]
+ can_send_to: [agent_salesrep, agent_fieldengr, ..., agent_strategos]
- id: agent_strategos
clearance: T5
@@ -709,15 +905,17 @@ Workspace layout:
```
/agents/
-├── luna/ # owned by agent_luna, group: sales-agents-ro
-│ ├── MEMORY.md
+├── sales-rep/ # owned by agent_salesrep, group: sales-dept-ro
+│ ├── MEMORY.md # shared institutional memory for the sales role
│ ├── memory/
│ │ └── 2026-03-17.md
│ ├── workspace/
-│ │ └── clients/
+│ │ ├── clients/
+│ │ └── knowledge/ # symlinks into /corporate/knowledge (Section 2.7)
+│ ├── people/ # thin per-person interaction profiles (Section 2.7)
│ └── SOUL.md
-├── atlas/ # similar structure
-├── sales-agent/ # owned by agent_sales, group: strategos-ro
+├── field-engineer/ # similar structure, one per ROLE
+├── sales-dept/ # owned by agent_sales, group: strategos-ro
│ ├── MEMORY.md
│ └── department-intel/
└── strategos/ # owned by agent_strategos, root group
@@ -757,10 +955,10 @@ For deployments requiring stronger isolation (regulated industries, larger organ
An 85-agent swarm requires observability infrastructure beyond compliance logging:
-- **Health dashboard.** Real-time status of all agent processes: running/suspended/quarantined, last activity timestamp, memory file size, session count. Implemented as a lightweight web UI served by the gateway, accessible to IT-AGENT and STRATEGOS.
+- **Health dashboard.** Real-time status of all agent processes: running/suspended/quarantined, last activity timestamp, memory file size, session count. Implemented as a lightweight web UI served by the gateway, accessible to L2-IT and L3-EXEC.
- **Distributed tracing.** Each inter-agent communication carries a trace ID (the `correlation_id` in the message protocol). The gateway aggregates trace logs, enabling reconstruction of multi-agent interaction chains when diagnosing coordination failures.
- **Alert thresholds.** Beyond compliance anomalies, operational alerts for: agent process crash, memory file exceeding size threshold (indicating runaway accumulation), gateway queue depth exceeding capacity, inference server response time degradation.
-- **Log aggregation.** All agent stdout/stderr, gateway logs, and compliance sweep output aggregate to a central log store (e.g., journald with remote forwarding, or ELK stack for larger deployments). Queryable by IT-AGENT for troubleshooting.
+- **Log aggregation.** All agent stdout/stderr, gateway logs, and compliance sweep output aggregate to a central log store (e.g., journald with remote forwarding, or ELK stack for larger deployments). Queryable by L2-IT for troubleshooting.
### 7.8 Versioning, Rollback, and Configuration Management
@@ -783,32 +981,56 @@ Agent configurations (system prompts, tool access, clearance assignments, topolo
Each agent has a role-specific TOOLS.md:
```markdown
-# TOOLS.md - LUNA (María García's Agent)
+# TOOLS.md - L1-SALES-REP (shared sales rep role agent)
## Trust Tiers
-- **Owner:** María García — full control
-- **Department Supervisor:** SALES-AGENT — can issue directives, read all outputs
-- **Executive:** STRATEGOS — read-only access, cannot be refused
+- **Owners:** all salespeople holding this role + the sales manager — interactive use
+- **Department Supervisor:** L2-SALES — can issue directives, read all outputs
+- **Executive:** L3-EXEC — read-only access, cannot be refused
## Tool Access
-- email: read/send (María's mailbox only)
-- crm: read/write (assigned accounts: 180 accounts, see accounts.list)
-- calendar: read/write (María's calendar)
+- email: read/send (the current user's mailbox only, resolved per session)
+- crm: read/write (the role's accounts; ownership per workload-division.md)
+- calendar: read/write (the current user's calendar)
- product_catalog: read (T2 resource)
-- contracts: read (T3 resource, assigned accounts only)
+- contracts: read (T3 resource, sales-role accounts)
- payroll_api: DENIED
- hr_records: DENIED
- infrastructure: DENIED
## Escalation Rules
-- Any request involving non-assigned accounts: query SALES-AGENT first
+- Any request involving an account owned by another rep: note ownership from workload-division.md and defer per Section 4.2
- Any pricing commitment request: verify against approved pricing tiers
- Any legal/contract language: flag for human review, do not commit
```
+### 7.11 Operational Governance: Ownership, Bounded Autonomy, and User/IT Separation
+
+No agent runs ownerless, but the right control is accountability, not surveillance.
+
+**Every agent has one or more human owners.** Each agent node in the topology carries a list of `owners` — the humans accountable for it. A role agent's owners are typically its occupants plus the department manager; a department or executive agent's owners are the relevant managers. Ownership establishes responsibility (who is answerable for what this agent does) and a point of contact for review.
+
+**No hard inactivity thresholds.** An earlier instinct was to require human interaction within a fixed window and to suspend agents that go dark. We reject this: people take holidays, travel, and work in bursts, and a 24-hour timer would fire constantly on legitimate absence, training everyone to ignore it. Role agents are dormant by default when no one is using them — an idle agent consumes nothing and poses little risk. Presence-policing solves a problem that does not exist.
+
+**The real concern is runaway autonomy, not silence.** The failure mode worth engineering against is an agent's _scheduled_ work — cron jobs, background sweeps, proactive monitors — running unattended and burning tokens (or taking actions) for no reason: a misconfigured monitor re-querying every minute, a self-improvement loop with no stopping condition. Bounded-autonomy controls therefore target _recurring automated work_, not interactive sessions:
+
+- Every cron or background task declares an expected cost and cadence; the gateway enforces per-agent rate and spend ceilings.
+- Recurring tasks that produce no consumed output over several cycles are flagged for owner review rather than left to run.
+- Expensive or irreversible recurring work requires explicit owner authorization to start, consistent with the resource-awareness principle that governs the platform generally.
+
+**Users signal IT; they do not self-administer.** This is the governance backbone of the self-modification axis (Section 3.5, Axis 5). Following the division-of-labor principle, **only IT (`L2-IT`) may upgrade, rebuild, or restart the gateway**, change tool allowlists, or alter clearance assignments. End users interact with their role agent; they never touch the infrastructure. This separation protects the integrity of the whole swarm — a single user must not be able to widen their own access or destabilize shared infrastructure — and keeps the configuration-management discipline of Section 7.8 meaningful.
+
+For this to be _fast_ rather than a bottleneck, the improvement loop is engineered, not ad hoc:
+
+- **A structured improvement channel, ingested by IT's agent.** Every user has a low-friction channel to file a bug they hit or a capability they imagine would speed their work. Crucially, these tickets are _consumed by `L2-IT`'s own agent_, not just read by a human. That changes what a good ticket looks like: because an agent will parse and act on it, the end user is expected to put substantial detail into each request — the exact behavior observed, the desired behavior, the workflow it blocks, concrete examples, and the frequency. A vague "the agent is slow" produces nothing; a specific, example-rich request becomes actionable work. The channel schema enforces this (required fields, reproduction steps, expected-vs-actual), turning the user base into a distributed requirements-gathering function.
+- **IT collects, builds, and tests.** `L2-IT` triages the incoming queue, deduplicates and prioritizes, and implements the improvement — increasingly by _vibe-coding_ against its own agent (the IT engineer directs the agent to draft the tool, skill, plugin, or recipe, then reviews it) rather than hand-writing every line. `L2-IT` then performs functionality testing in a staging environment (Section 8.7) before release.
+- **One rollout reaches everyone.** Because the workforce runs on shared role agents, an implemented improvement is deployed once to the canonical role build and every occupant gets it simultaneously — the same property that makes onboarding inheritance (Section 3.2) makes capability upgrades instant and uniform.
+
+The result is a controlled evolution loop: distributed sensing of what to improve (every user, in detail), centralized authority to implement it safely (`L2-IT`), and uniform propagation of the result (the shared agent). The company gets continuously faster without letting anyone destabilize the substrate everyone depends on.
+
---
## 8. Risks and Mitigations
@@ -820,7 +1042,7 @@ A salesperson asks "find everything about the González account." The agent, int
**Mitigation:**
- Tool call logging includes query intent. Compliance sweep flags aggregation-pattern queries.
-- System prompt includes clearance awareness: "If a query seems to require information beyond your clearance level, acknowledge the limitation and escalate to SALES-AGENT."
+- System prompt includes clearance awareness: "If a query seems to require information beyond your clearance level, acknowledge the limitation and escalate to L2-SALES."
- Agent outputs pass through a lightweight classifier checking for content patterns inconsistent with the agent's clearance.
### 8.2 Inter-Agent Social Engineering
@@ -830,14 +1052,14 @@ Could a lower-clearance agent manipulate a higher-clearance agent into revealing
**Mitigation:**
- Department agents use a separate, more restrictive system prompt for inter-agent communication. Explicit rule: "Never include T4+ content in responses to T3 agents regardless of framing."
-- All department agent responses to employee agents pass through a **clearance validator** before delivery — a separate, deterministic check.
+- All department agent responses to role agents pass through a **clearance validator** before delivery — a separate, deterministic check.
- **Clearance validator design.** To avoid the attack-surface and latency concerns of a second LLM call, the validator is primarily rule-based: regex patterns for known restricted data formats (salary ranges, credential patterns, document IDs from restricted paths), plus a lightweight classifier for semantic checks. The full LLM-based validation is reserved for messages flagged as ambiguous by the rule-based layer. This two-tier approach reduces latency (rule-based check: <10ms; LLM check only when triggered: ~500ms) and limits the attack surface.
- Push channels return no data to the sender. The channel schema is validated; malformed pushes are rejected and logged. Push frequency is rate-limited per agent.
- Communication logs capture request-response pairs. Patterns where a specific agent consistently receives more sensitive-seeming responses are flagged.
### 8.3 Memory Persistence and Contamination
-Restricted information enters lower-clearance memory through human channels. María attends a management meeting and hears salary figures; she mentions this to LUNA, who writes it into memory. T4 data now exists in a T3 memory file.
+Restricted information enters lower-clearance memory through human channels. María attends a management meeting and hears salary figures; she mentions this to the sales agent, which writes it into memory. T4 data now exists in a T3 memory file.
**Mitigation:**
@@ -847,15 +1069,15 @@ Restricted information enters lower-clearance memory through human channels. Mar
### 8.4 Single Point of Failure: The Executive Agent
-STRATEGOS has T5 clearance and access to all workspaces. If compromised, the entire information architecture is at risk.
+L3-EXEC has T5 clearance and access to all workspaces. If compromised, the entire information architecture is at risk.
**Mitigation:**
- Isolated infrastructure with no external network access. Inputs arrive only through sanitized gateway channels.
- Most aggressive behavioral monitoring. Outputs reviewed daily.
- T5 data store uses additional encryption beyond filesystem permissions.
-- **STRATEGOS dead switch**: CEO can immediately suspend the executive agent and revert to human-only T5 access.
-- Two-person rule: STRATEGOS-generated summaries including T5 content require human review before delivery.
+- **L3-EXEC dead switch**: CEO can immediately suspend the executive agent and revert to human-only T5 access.
+- Two-person rule: L3-EXEC-generated summaries including T5 content require human review before delivery.
### 8.5 Model Capability Variance
@@ -870,13 +1092,13 @@ Different models follow clearance rules with different reliability. A 5% failure
### 8.6 Failure Modes and Recovery
-**Department agent crash.** If SALES-AGENT becomes unavailable mid-coordination:
+**Department agent crash.** If L2-SALES becomes unavailable mid-coordination:
- Employee agents detect the failure when their messages receive `TIMEOUT` responses from the gateway.
- Employee agents enter a **degraded mode**: they continue serving their human user with locally available data but cannot initiate cross-agent queries or receive updates.
- The gateway queues incoming messages for the crashed department agent (up to queue capacity).
- Automated restart: systemd watchdog restarts the department agent process. On restart, the agent replays queued messages.
-- If restart fails three times, STRATEGOS and IT-AGENT are alerted for manual intervention.
+- If restart fails three times, L3-EXEC and L2-IT are alerted for manual intervention.
**Memory file corruption.** If a compliance sweep detects inconsistency:
@@ -939,23 +1161,23 @@ Deploying HIVEMIND across an organization should be staged, not big-bang. Each p
### Phase 1: Single Department, T2 Only (Weeks 5-10)
-- Deploy SALES-AGENT + 3-4 pilot sales agents (volunteers) with T2 clearance only.
-- No restricted data access, no cross-agent coordination beyond shared product catalog.
-- Focus: employee adoption, agent personalization, tool integration testing.
+- Deploy the L1-SALES-REP role agent for 3-4 pilot salespeople (volunteers) with T2 clearance only, plus the L2-SALES agent.
+- No restricted data access, no cross-role coordination beyond the shared product catalog and commons.
+- Focus: employee adoption, the per-person interaction layer, commons authoring, and tool integration testing.
- **Gate:** >80% daily active use by pilot users, zero compliance flags, positive user feedback.
### Phase 2: Single Department, Full Clearance (Weeks 11-18)
- Raise pilot agents to T3 clearance. Enable customer ownership routing, cross-agent history queries, competitive intelligence push.
- Deploy full compliance sweep infrastructure.
-- SALES-AGENT performs T4 redaction via template pipeline.
+- L2-SALES performs T4 redaction via template pipeline.
- **Gate:** Compliance sweep clean for 4 consecutive weeks, redaction pipeline validated by management review, coordination demonstrably improving customer interactions.
### Phase 3: Multi-Department (Weeks 19-30)
-- Deploy IT-AGENT, HR-AGENT, FINANCE-AGENT with respective employee agents.
-- Enable STRATEGOS for executive oversight.
-- Cross-department communication limited to STRATEGOS-mediated queries.
+- Deploy L2-IT, L2-HR, L2-FINANCE with respective role agents.
+- Enable L3-EXEC for executive oversight.
+- Cross-department communication limited to L3-EXEC-mediated queries.
- **Gate:** All departments operational, cross-department queries functioning, weekly audit reports stable.
### Phase 4: Full Operation (Week 31+)
@@ -969,6 +1191,8 @@ Deploying HIVEMIND across an organization should be staged, not big-bang. Each p
- **Adoption:** Daily active usage rate >75% after 90 days.
- **Compliance:** <2 quarantine events per quarter after Phase 3.
- **Value:** Measurable improvement in customer response time, reduced information request escalations, positive employee survey results.
+- **Continuity (the headline metric):** _time-to-productivity for a new hire_ — the interval from start date to independent role performance. The continuity thesis predicts a step-change here (e.g., a new sales rep productive in two weeks instead of three months), because the replacement inherits the role agent's accumulated knowledge instead of rebuilding it. This is the most concrete, measurable, and sellable outcome HIVEMIND offers; it should be instrumented from Phase 1 and compared against the organization's historical onboarding baseline.
+- **Knowledge freshness:** percentage of commons documents within their review cadence (Section 2.8); a declining figure is an early warning of institutional decay even when adoption and compliance look healthy.
### 9.1 Evaluation Agenda Before Full-Scale Rollout
@@ -987,7 +1211,7 @@ A useful publication-quality follow-on to this paper would therefore be a field
### 10.1 Scaling Beyond the Family Business
-At 1,000+ employees, the three-level hierarchy extends: Level 1 (employee), Level 2 (team), Level 3 (department), Level 4 (division), Level 5 (executive). The primary scaling challenge is department agent cognitive load. At 200 employees per department, sub-department agents (team-level) aggregate their direct reports and feed into the department agent.
+At 1,000+ employees, the hierarchy extends: Level 1 (role agents), Level 2 (team), Level 3 (department), Level 4 (division), Level 5 (executive). Role-binding helps here — the number of Level 1 agents scales with the number of _distinct job functions_, not with headcount, so a division of 200 people doing a handful of roles still has only a handful of role agents. The primary scaling challenge becomes department/division agent cognitive load, addressed by inserting sub-department (team-level) aggregators that feed upward.
### 10.2 Integration with Enterprise Systems
@@ -1000,7 +1224,7 @@ Full deployment requires integration with:
### 10.3 Federated Agent Networks Across Companies
-The most ambitious direction: agent networks spanning organizational boundaries. Two companies with a supplier-customer relationship establish a federated channel where their SALES-AGENT instances share pre-approved data categories without exposing internal data.
+The most ambitious direction: agent networks spanning organizational boundaries. Two companies with a supplier-customer relationship establish a federated channel where their sales role agents share pre-approved data categories without exposing internal data.
This requires a federated clearance protocol: each company's gateway defines what external agents can receive, filtering before crossing the federation boundary. This intersects with emerging enterprise AI standards (IEEE P3394, ISO/IEC JTC 1/SC 42).
@@ -1023,9 +1247,9 @@ A HIVEMIND deployment for 85 employees requires:
| **Total hardware** | | **€35,000-50,000** |
| Annual operating cost | Power (~3kW), cooling, maintenance, sysadmin time | €15,000-25,000/yr |
-**Compute budget per agent:** With 85 employee agents + 5 department agents + 1 executive agent = 91 agents. Assuming average 50 inference calls per agent per day at ~2 seconds per call on a 70B model: 91 × 50 × 2 = ~9,100 seconds = ~2.5 hours of sequential inference per day. With 2× A100 GPUs, this is well within capacity with significant headroom for peak loads and compliance scans.
+**Compute budget.** Role-binding sharply reduces the number of distinct agent _processes and configurations_ — roughly one per job function plus department and executive agents, perhaps 10–15 agents for an 85-person company rather than 91. This cuts memory footprint, configuration-management surface, and operational complexity. It does _not_ proportionally cut inference _demand_: the call volume is driven by how many humans are working, not how many agent processes exist. With ~85 active users issuing an average of 50 role-agent inference calls per day at ~2 seconds per call on a 70B model — plus department/executive aggregation and compliance scans — total demand is on the order of ~2.5 hours of sequential inference per day, well within the capacity of 2× A100 GPUs with significant headroom for peak loads. (A shared role agent serving concurrent users requires session isolation per user but shares the loaded model weights, which is where the memory savings come from.)
-**Comparison to cloud inference:** The same workload on cloud APIs (estimated $0.03/call for GPT-4-class models) would cost: 91 × 50 × $0.03 × 365 = ~$50,000/year — comparable to the first-year total cost of ownership for on-premises hardware, but without data residency guarantees. By year two, on-premises is significantly cheaper.
+**Comparison to cloud inference:** The same call volume on cloud APIs (estimated $0.03/call for GPT-4-class models): ~85 × 50 × $0.03 × 365 ≈ $46,500/year — comparable to the first-year total cost of ownership for on-premises hardware, but without data residency guarantees. By year two, on-premises is significantly cheaper.
These are rough estimates. Actual costs depend on model choice, inference optimization (quantization, batching), and usage patterns. A detailed capacity planning exercise should precede procurement.
@@ -1043,7 +1267,7 @@ This paper has several significant limitations that should be acknowledged:
1. **No empirical validation.** HIVEMIND is a design blueprint, not a deployed system. The architecture has not been tested against real enterprise workloads. Claims about effectiveness (the "coordination dividend") are theoretical.
-2. **Redaction reliability is unproven.** The template-based redaction pipeline (Section 4.4) mitigates but does not eliminate leakage risk. No formal information-theoretic bounds on leakage are established. The templates themselves must be authored by humans — their quality and completeness are a manual bottleneck.
+2. **Redaction reliability is measured, not bounded.** The template-based redaction pipeline (Section 4.4) is now checked two ways — an offline redaction-fidelity eval (Section 4.4.2) and a runtime doubt reviewer (Section 4.4.1) — which together replace an untested claim with a measured one. But no formal information-theoretic _bounds_ on leakage are established, the eval is specified but not yet run on a real corpus, and the templates themselves must be authored by humans, a manual bottleneck.
3. **Cost estimates are approximate.** The performance figures in Section 10.5 are back-of-envelope calculations that have not been validated against actual deployment. Real-world usage patterns may differ significantly.
@@ -1059,31 +1283,49 @@ This paper has several significant limitations that should be acknowledged:
9. **Formal integrity treatment is partial.** Confidentiality is well developed through the Bell-LaPadula framing, but integrity and provenance controls are described operationally rather than formalized. A stronger version would either formalize integrity constraints or narrow its claims explicitly to confidentiality-first design.
-10. **Benchmark and test methodology missing.** The paper proposes classifiers, validators, and redaction pipelines without a standard benchmark suite, seeded violation corpus, or acceptance thresholds. This weakens reproducibility.
+10. **Benchmark and test methodology is specified but unrun.** The redaction-fidelity eval (Section 4.4.2) defines a seeded leakage corpus, a measured quantity (residual mutual information), and per-category acceptance thresholds — the seeded-corpus methodology Limitation 10 previously called for entirely absent. It remains specified rather than executed, and the compliance classifiers and clearance validator still lack an analogous seeded-violation benchmark. Reproducibility is improved but not yet demonstrated.
+
+11. **Role-binding widens intra-team data visibility.** A shared role agent holds the whole role's data and resolves individual ownership contextually (Section 4.2) rather than through a kernel-enforced boundary. This is appropriate for organizations where peers at the same tier already share a CRM and reasonably _can_ see each other's accounts — the typical family business. It is a real limitation where intra-team confidentiality matters: competing salespeople guarding leads, commission disputes, or any setting where same-role peers must be walled from each other. Such cases need either per-occupant scoping within the role agent (reintroducing some of the complexity role-binding removes) or a return to per-person agents for that specific function.
+
+12. **Reduced per-person personalization.** The thin interaction layer (Section 2.7) personalizes manners, not memory. A dedicated personal agent could in principle tailor itself more deeply to one individual's working style. Whether the shared design's standardization benefits outweigh any adoption cost from reduced personalization is an empirical question this paper does not answer.
+
+13. **Fossilization is mitigated by practice, not guaranteed by architecture.** The new-hire research loop (Section 5.9) is an organizational practice that depends on new hires actually probing and stewards actually incorporating feedback. It reduces but does not eliminate the risk that a persistent role agent entrenches outdated assumptions. A persistent shared agent that is _well_ maintained is a continuity asset; one that is neglected is a single, authoritative-sounding source of stale truth — and the second failure mode is quieter than the first.
+
+14. **Role decomposition is assumed clean.** The design assumes jobs decompose into roles with shared scope and shared clearance. Real organizations have hybrid roles, people who span two functions, and seniority gradations within a single job title that may warrant different clearances. Mapping these onto a single shared role agent — or deciding when a distinct role agent is warranted — requires case-by-case design beyond this paper's scope.
---
## 12. Conclusions
-This paper has described HIVEMIND: a hierarchical agent swarm architecture for enterprise AI deployment, designed around the principle that coordination and information security are not in conflict — they are design requirements to be solved simultaneously.
+This paper has described HIVEMIND: a role-bound agent swarm architecture for enterprise AI deployment, designed around the principle that an organization's knowledge should outlive the people who hold it — and that continuity, coordination, and information security are not in conflict but are design requirements to be solved simultaneously.
The key contributions are:
-1. **The Corporate Filesystem Clearance Model.** Five data classification tiers (PUBLIC through SECRET) mapped to Linux filesystem permissions, formally grounded in the Bell-LaPadula security model with a controlled declassification mechanism. Infrastructure enforcement is categorically more reliable than model-level instruction.
+1. **The Continuity Thesis and Role-Bound Agents.** One canonical agent per job function, shared by every occupant of that role, making the agent the durable holder of institutional knowledge. This turns the most expensive recurring failure in a mid-sized company — the talent leak — into a non-event: the agent never resigns, offboarding collapses to a workload-map edit, cross-department coordination collapses to one channel per function, and onboarding becomes inheritance.
+
+2. **The Knowledge Commons and Cascade Model.** A centralized, version-controlled Markdown vault (company canon, industry context, customer-relationship model, product catalog) mounted via symlinks, with agent knowledge assembled as a cascade from company canon through role slice and workload map to a thin per-person interaction layer. Symlinks provide a single source of truth without weakening any clearance boundary. The accompanying transformation of trainers into knowledge stewards — who train the AI once instead of each hire repeatedly — is the human-capital case for the architecture.
-2. **The Three-Level Agent Hierarchy.** Employee agents (personalized, role-scoped), department agents (coordinating, aggregating, gatekeeping), and executive agents (comprehensive view, full audit access). Downward queries, upward intelligence through formalized structured push channels, and mediated lateral communication.
+3. **The Clearance Model, Applied at Role Granularity.** Five data classification tiers (PUBLIC through SECRET) mapped to Linux filesystem permissions, formally grounded in Bell-LaPadula with a controlled declassification mechanism, now keyed to roles rather than individuals. Infrastructure enforcement is categorically more reliable than model-level instruction.
-3. **The Sales Coordination Model.** Customer ownership routing, unified history assembly with template-based redacted summaries, and competitive intelligence aggregation — demonstrating cross-agent coordination without lateral information leakage.
+4. **The Sales Coordination Model under Contextual Ownership.** A single shared sales agent resolves customer ownership contextually — knowing whom it serves and reading the workload map — rather than through inter-agent email routing, with template-based redacted summaries for upward-tier data and competitive intelligence aggregation. The role-bound design makes the original "two agents, same customer, divergent responses" failure mode structurally impossible.
-4. **Compliance Architecture.** Nightly sweep across four analysis dimensions with calibrated false-positive thresholds, communication logging with full audit trail, and automated quarantine with human-in-the-loop review satisfying GDPR Article 22.
+5. **Compliance Architecture.** Nightly sweep across four analysis dimensions with calibrated false-positive thresholds, communication logging with full audit trail, and automated quarantine with human-in-the-loop review satisfying GDPR Article 22.
-5. **Legal Framework.** GDPR and EU AI Act compliance analysis, including DPA requirements, profiling assessment, automated decision-making obligations, and high-risk AI system conformity assessment.
+6. **Legal Framework.** GDPR and EU AI Act compliance analysis, including DPA requirements, profiling assessment, automated decision-making obligations, and high-risk AI system conformity assessment.
-6. **Risk Analysis.** Ten principal risk categories with concrete mitigations, including failure recovery procedures, model update management, and real-time communication channel handling.
+7. **Risk Analysis.** Ten principal risk categories with concrete mitigations, including failure recovery procedures, model update management, and real-time communication channel handling.
-7. **Deployment Strategy.** A four-phase rollout with explicit gate criteria, success metrics, and adoption management considerations.
+8. **Deployment Strategy.** A four-phase rollout with explicit gate criteria, success metrics, and adoption management considerations.
-8. **Technical Specification.** Inter-agent protocol with message format, delivery semantics, authentication, and process isolation beyond filesystem permissions.
+9. **Technical Specification.** Inter-agent protocol with message format, delivery semantics, authentication, and process isolation beyond filesystem permissions, plus bounded-autonomy operational governance and user/IT separation of duties.
+
+10. **The Multi-Axis Network View.** A framing of the swarm as one graph read along six orthogonal axes — clearance, supervision, bottleneck reduction, workload sharing, self-modification, and knowledge propagation — each with its own mechanisms and frequently in tension with the others. The self-modification axis is governed by division of labor: only IT may alter the infrastructure, fed by a detailed, agent-ingested improvement channel that lets the workforce get continuously faster without anyone destabilizing the shared substrate.
+
+11. **A Future-of-Work Argument.** A perspective on why role-bound AI need not be read as job elimination: for roles whose value is learning and teaching, removing the repetition and adding leverage can elevate the job — the trainer becomes a recurring-revenue knowledge manager who teaches once, fights fossilization, breaks the per-hour income ceiling, and serves many companies at once.
+
+12. **Positioning Among Convergent Paradigms.** A related-work synthesis (Section 1.6) placing HIVEMIND among the fan-out, coordinator-with-workers, and org-chart topologies, and against Paperclip, coordinator/teammate execution models, single-agent role-switching, headroom's reversible caching, and addyosmani's doubt-driven review — with the convergence argument that corporate structure may be the natural coordination topology, and HIVEMIND's distinguishing claim that the org chart _is_ the access-control graph.
+
+13. **Clearance-Aware Memory and Measured Leakage Control.** The per-agent cognitive stack (Section 1.5) applied under role-binding; an append-only event store and a Compress-Cache-Retrieve-Under-Clearance variant that keep memory reversible without laundering tiers (Sections 2.9–2.10); cross-role federated retrieval (Section 3.6); and a matched pair that moves leakage control from asserted to measured — an offline redaction-fidelity eval and a runtime fresh-context doubt reviewer (Sections 4.4.1–4.4.2).
HIVEMIND is a design blueprint — not a finished system. It will evolve as models improve, enterprise integration matures, and regulators develop clearer frameworks. What we believe will endure is the fundamental tension it addresses: the value of coordination versus the necessity of boundaries. Every organization deploying AI at scale will face this tension. The answer is architecture: clear rules about what flows where, enforced at the right layer, audited continuously, with humans in the loop for consequential decisions.
@@ -1141,7 +1383,13 @@ Wooldridge, M. (2009). _An Introduction to MultiAgent Systems_ (2nd ed.). Wiley.
---
-_HIVEMIND: Hierarchical Agent Swarms for Enterprise Knowledge Management_
-_J-series paper J10 | Version 1.1 | March 17, 2026_
+_HIVEMIND: Role-Bound Agent Swarms for Enterprise Continuity_
+_J-series paper J10 | Version 2.2 | June 18, 2026_
_Author: Oscar Serra_
_Classification: INTERNAL — OpenClaw/TinkerClaw Research_
+
+_v2.0 reframed the architecture around organizational continuity: role-bound agents (one shared agent per job function) in place of per-person agents, a knowledge-commons cascade with the trainer→steward transformation, contextual customer ownership, ownership-based governance with bounded autonomy, and offboarding as a non-event._
+
+_v2.1 adds: a systematic `L-` agent naming convention applied throughout (replacing STRATEGOS / mixed per-department names); §3.5 framing the network along six orthogonal axes (clearance, supervision, bottleneck reduction, workload sharing, self-modification, knowledge propagation); an expanded §7.11 IT-only self-modification loop with an agent-ingested feature-request channel and vibe-coded improvements; and §5.10, a future-of-work argument on how learning-and-teaching roles can be elevated rather than eliminated._
+
+_v2.2 merges forward the substantive sections from the parallel v1.8 build lineage that the v2.x reframe had been missing, each re-framed for role-binding: §1.5 the per-agent cognitive stack (ENGRAM/HIPPOCAMPUS/CORTEX/CEREBELLUM/SYNAPSE/AEGIS), §1.6 Related Work and Positioning (Paperclip, coordinator/teammate models, headroom/CCR, addyosmani doubt-driven, the convergence argument), §2.9–2.10 the clearance-aware append-only event store and Compress-Cache-Retrieve-Under-Clearance, §3.6 cross-role federated retrieval, and §4.4.1–4.4.2 the doubt reviewer and redaction-fidelity eval. Limitations 2 and 10 updated accordingly._
diff --git a/docs/papers/corporate-swarm/diagrams/fig-history-flow.d2 b/docs/papers/corporate-swarm/diagrams/fig-history-flow.d2
new file mode 100644
index 0000000000000..085cacef1dafc
--- /dev/null
+++ b/docs/papers/corporate-swarm/diagrams/fig-history-flow.d2
@@ -0,0 +1,20 @@
+# J10 HIVEMIND — Figure 2: Contextual History + Upward-Tier Filtering (role-bound)
+# Vertical flow, large fonts — legible at A4 column width.
+# Render: d2 --layout dagre --pad 20 fig-history-flow.d2 images/fig-history-flow.png
+
+direction: down
+
+maria: "María (T3): \"brief me on González\"" { shape: person; style: { fill: "#f6e7d4"; stroke: "#d97a2b"; stroke-width: 2; font-size: 26 } }
+agent: "L1-SALES-REP (T3) — one shared agent\nholds the whole team's history natively\n(no cross-agent fan-out)" { style: { fill: "#e6eef5"; stroke: "#2c5b86"; stroke-width: 3; font-size: 26 } }
+filter: "Upward-tier filter\n(declassification gate)" { shape: hexagon; style: { fill: "#fdeaea"; stroke: "#b23b3b"; stroke-width: 3; font-size: 26 } }
+
+brief: "Briefing returned to María" {
+ style: { fill: "#faf7f1"; stroke: "#5A3E28"; font-size: 28; bold: true }
+ pass: "T3 interactions → verbatim" { style: { fill: "#e7f0db"; stroke: "#4A5D1A"; stroke-width: 2; font-size: 25 } }
+ redact: "T4 pricing → redaction template" { style: { fill: "#f7eccf"; stroke: "#c79a2b"; stroke-width: 2; font-size: 25 } }
+ caution: "T5 legal → caution notice" { style: { fill: "#fbe0e0"; stroke: "#b23b3b"; stroke-width: 2; font-size: 25 } }
+}
+
+maria -> agent: "request" { style: { stroke: "#d97a2b"; stroke-width: 3; font-size: 24 } }
+agent -> filter: "data above the\nsales role's clearance" { style: { stroke: "#2c5b86"; stroke-width: 3; font-size: 24 } }
+filter -> brief: "result respects the\nREQUESTER's clearance" { style: { stroke: "#b23b3b"; stroke-width: 3; font-size: 24 } }
diff --git a/docs/papers/corporate-swarm/diagrams/fig-synthesis.d2 b/docs/papers/corporate-swarm/diagrams/fig-synthesis.d2
new file mode 100644
index 0000000000000..bfda6d23dc35a
--- /dev/null
+++ b/docs/papers/corporate-swarm/diagrams/fig-synthesis.d2
@@ -0,0 +1,28 @@
+# J10 HIVEMIND — Figure 3: The Agent Network Along Six Axes (whole-paper synthesis)
+# Compact, large fonts, legend stacked below — legible at A4 column width.
+# Render: d2 --layout dagre --pad 20 fig-synthesis.d2 images/fig-synthesis.png
+
+direction: down
+
+humans: "Humans\n(rotate through roles)" { shape: person; style: { fill: "#f5f0e8"; stroke: "#5A3E28"; stroke-width: 2; font-size: 28 } }
+exec: "L3-EXEC · T5" { style: { fill: "#efe6f2"; stroke: "#6B5090"; stroke-width: 3; font-size: 28; bold: true } }
+it: "L2-IT · T4" { style: { fill: "#eef2e2"; stroke: "#4A5D1A"; stroke-width: 2; font-size: 27 } }
+sales: "L2-SALES · T4" { style: { fill: "#eef2e2"; stroke: "#4A5D1A"; stroke-width: 2; font-size: 27 } }
+commons: "Knowledge Commons" { shape: cylinder; style: { fill: "#f3ead8"; stroke: "#5A3E28"; stroke-width: 2; font-size: 27 } }
+srep: "L1-SALES-REP · T3\n(shared)" { style: { fill: "#e6eef5"; stroke: "#2c5b86"; stroke-width: 2; font-size: 26 } }
+
+# one exemplar edge per axis, each a distinct color
+exec -> sales: "clearance ↓" { style: { stroke: "#b23b3b"; stroke-width: 4; font-size: 25; font-color: "#b23b3b" } }
+sales -> srep: "supervision" { style: { stroke: "#6B5090"; stroke-width: 3; stroke-dash: 5; font-size: 25; font-color: "#6B5090" } }
+commons -> srep: "knowledge →" { style: { stroke: "#4A7A1A"; stroke-width: 3; font-size: 25; font-color: "#4A7A1A" } }
+humans -> it: "feature tickets" { style: { stroke: "#2c5b86"; stroke-width: 3; font-size: 25; font-color: "#2c5b86" } }
+it -> commons: "vibe-coded rollout" { style: { stroke: "#2c5b86"; stroke-width: 3; font-size: 25; font-color: "#2c5b86" } }
+srep -> humans: "one answer, many people" { style: { stroke: "#2596a8"; stroke-width: 3; font-size: 25; font-color: "#2596a8" } }
+humans -> srep: "workload routing" { style: { stroke: "#d97a2b"; stroke-width: 3; stroke-dash: 3; font-size: 25; font-color: "#d97a2b" } }
+
+legend: "AXES (one graph, six colored paths)\n● red = Clearance (who may know what) ● purple = Supervision (who inspects whom)\n● green = Knowledge propagation (commons → all) ● blue = Self-modification (users → IT → rollout)\n● teal = Bottleneck reduction (one agent, many) ● orange = Workload sharing (dynamic routing)" {
+ near: bottom-center
+ shape: text
+ width: 900
+ style: { font-size: 26; fill: "#faf7f1" }
+}
diff --git a/docs/papers/corporate-swarm/images/fig-architecture.png b/docs/papers/corporate-swarm/images/fig-architecture.png
new file mode 100644
index 0000000000000..67a607397240d
Binary files /dev/null and b/docs/papers/corporate-swarm/images/fig-architecture.png differ
diff --git a/docs/papers/corporate-swarm/images/fig-history-flow.png b/docs/papers/corporate-swarm/images/fig-history-flow.png
new file mode 100644
index 0000000000000..0ffa7d27836f4
Binary files /dev/null and b/docs/papers/corporate-swarm/images/fig-history-flow.png differ
diff --git a/docs/papers/corporate-swarm/images/fig-synthesis.png b/docs/papers/corporate-swarm/images/fig-synthesis.png
new file mode 100644
index 0000000000000..5c25bff012b04
Binary files /dev/null and b/docs/papers/corporate-swarm/images/fig-synthesis.png differ
diff --git a/extensions/memory-core/src/memory/manager-sync-ops.ts b/extensions/memory-core/src/memory/manager-sync-ops.ts
index c1ecde7a9c1e8..f96d425f56ca9 100644
--- a/extensions/memory-core/src/memory/manager-sync-ops.ts
+++ b/extensions/memory-core/src/memory/manager-sync-ops.ts
@@ -269,7 +269,15 @@ export abstract class MemoryManagerSyncOps {
if (this.vector.dims === dimensions) {
return;
}
- if (this.vector.dims && this.vector.dims !== dimensions) {
+ // Drop a stale on-disk vector table whose declared dimension differs from the
+ // live embedder's. Keying solely off this.vector.dims missed the case where
+ // dims is undefined (fresh manager / post-restart) yet a table built for a
+ // PREVIOUS embedder still exists on disk (e.g. a 3072-dim Gemini column after
+ // switching to the 1024-dim ollama/mxbai-embed-large embedder) — leaving it
+ // makes every insert throw "Expected 3072 dimensions but received 1024". We
+ // read the actual column dim so a correctly-sized table is never dropped.
+ const existingDims = this.vector.dims ?? this.readVectorTableDimensions();
+ if (existingDims !== undefined && existingDims !== dimensions) {
this.dropVectorTable();
}
this.db.exec(
@@ -281,6 +289,24 @@ export abstract class MemoryManagerSyncOps {
this.vector.dims = dimensions;
}
+ /**
+ * Reads the declared embedding dimension of the on-disk vector table (parsed
+ * from its `FLOAT[N]` column in sqlite_master), or undefined if the table does
+ * not exist / cannot be parsed. Used to detect a dimension mismatch even when
+ * the in-memory this.vector.dims is unset (e.g. immediately after a restart).
+ */
+ private readVectorTableDimensions(): number | undefined {
+ try {
+ const row = this.db
+ .prepare(`SELECT sql FROM sqlite_master WHERE type='table' AND name = ?`)
+ .get(VECTOR_TABLE) as { sql?: string } | undefined;
+ const match = row?.sql?.match(/FLOAT\[(\d+)\]/i);
+ return match ? Number(match[1]) : undefined;
+ } catch {
+ return undefined;
+ }
+ }
+
private dropVectorTable(): void {
try {
this.db.exec(`DROP TABLE IF EXISTS ${VECTOR_TABLE}`);
diff --git a/extensions/tinkerclaw-browser-relay/chrome-extension/background.js b/extensions/tinkerclaw-browser-relay/chrome-extension/background.js
index 8436c885373ad..af668940ac5a9 100644
--- a/extensions/tinkerclaw-browser-relay/chrome-extension/background.js
+++ b/extensions/tinkerclaw-browser-relay/chrome-extension/background.js
@@ -47,6 +47,13 @@ const childSessionToTab = new Map();
/** @type {Mapvoid, reject:(e:Error)=>void}>} */
const pending = new Map();
+// FORK 2026-06-20: per-tab re-attach backoff timers. When chrome.debugger
+// detaches for a TRANSIENT reason (MV3 service-worker recycle, a second CDP
+// client bumping the debugger while Jarvis drives the tab, DevTools opened),
+// we keep the share alive and auto-re-attach instead of silently unsharing.
+/** @type {Map} */
+const reattachTimers = new Map();
+
// ---------------------------------------------------------------------------
// Relay port + auth token
// ---------------------------------------------------------------------------
@@ -513,6 +520,13 @@ async function attachTab(tabId, opts = {}) {
}
async function detachTab(tabId, reason) {
+ // FORK 2026-06-20: cancel any pending auto-re-attach for this tab — an
+ // intentional unshare/close must win over a transient-detach retry.
+ const pendingReattach = reattachTimers.get(tabId);
+ if (pendingReattach) {
+ clearTimeout(pendingReattach.timer);
+ reattachTimers.delete(tabId);
+ }
const tab = tabs.get(tabId);
if (tab?.sessionId && tab?.targetId) {
try {
@@ -804,19 +818,90 @@ function onDebuggerEvent(source, method, params) {
}
}
+// FORK 2026-06-20: auto-re-attach a shared tab after a TRANSIENT debugger
+// detach, with exponential backoff (1s → 2s → 4s → 8s → 15s cap). The share
+// entry in `tabs` is preserved throughout — the badge shows "connecting" (…)
+// while re-attaching and returns to "ON" on success. Mirrors the relay-WS
+// reconnect resilience (onRelayClosed) for the debugger side. The user's
+// explicit unshare goes through detachTab (which removes the tab from `tabs`
+// first), so this path never fights an intentional unshare.
+function reattachTabWithBackoff(tabId, backoffMs = 1000) {
+ if (reattachTimers.has(tabId)) {
+ return; // a retry is already scheduled for this tab
+ }
+ const timer = setTimeout(async () => {
+ reattachTimers.delete(tabId);
+ if (!tabs.has(tabId)) {
+ return; // user unshared (or tab removed) while we waited
+ }
+ const exists = await chrome.tabs.get(tabId).catch(() => null);
+ if (!exists) {
+ // Tab is genuinely gone now → permanent, legitimate unshare.
+ await removeTabFromGroup(tabId);
+ await detachTab(tabId, "tab_closed");
+ await saveSharedTabs();
+ updateGlobalBadge();
+ return;
+ }
+ try {
+ await ensureRelayConnection();
+ // Clear any lingering attachment before a clean re-attach.
+ await chrome.debugger.detach({ tabId }).catch(() => {});
+ await attachTab(tabId); // sets state→connected, re-announces, badge→ON
+ await addTabToGroup(tabId);
+ await saveSharedTabs();
+ updateGlobalBadge();
+ console.log(`[tinkerclaw-relay] re-attached tab ${tabId} after transient detach`);
+ } catch (err) {
+ const next = Math.min(backoffMs * 2, 15000);
+ console.warn(
+ `[tinkerclaw-relay] re-attach failed for tab ${tabId} ` +
+ `(${err instanceof Error ? err.message : String(err)}); retry in ${next}ms`,
+ );
+ setBadge(tabId, "connecting");
+ reattachTabWithBackoff(tabId, next);
+ }
+ }, backoffMs);
+ reattachTimers.set(tabId, { timer });
+}
+
+// Reasons Chrome reports when the TAB itself is gone — a real, permanent end of
+// the share. Everything else (canceled_by_user, replaced_with_devtools, or an
+// undefined reason from a service-worker recycle) is treated as transient.
+const PERMANENT_DETACH_REASONS = new Set(["target_closed", "tab_closed"]);
+
function onDebuggerDetach(source, reason) {
const tabId = source.tabId;
if (!tabId) {
return;
}
if (!tabs.has(tabId)) {
- return;
+ return; // not a shared tab, or our own intentional detach (tabs entry already removed)
}
void (async () => {
- await removeTabFromGroup(tabId);
- await detachTab(tabId, reason);
- await saveSharedTabs();
- updateGlobalBadge();
+ const stillExists = await chrome.tabs.get(tabId).catch(() => null);
+ if (!stillExists || PERMANENT_DETACH_REASONS.has(reason)) {
+ // Tab really closed → legitimate, permanent unshare.
+ await removeTabFromGroup(tabId);
+ await detachTab(tabId, reason);
+ await saveSharedTabs();
+ updateGlobalBadge();
+ return;
+ }
+ // Transient detach → KEEP the share, show "…", and auto-re-attach.
+ // This is the fix for "shared tab flips ON → … and stops working after
+ // Jarvis uses it": the tab stays shared until the user explicitly unshares.
+ console.warn(
+ `[tinkerclaw-relay] transient debugger detach (${reason || "no-reason"}) on tab ${tabId}; ` +
+ `keeping share, auto-re-attaching`,
+ );
+ const tab = tabs.get(tabId);
+ if (tab) {
+ tab.state = "connecting";
+ }
+ setBadge(tabId, "connecting");
+ await ensureKeepAlive(); // make sure the SW stays warm to run the retry
+ reattachTabWithBackoff(tabId);
})();
}
diff --git a/extensions/tinkerclaw-budget-panel/index.ts b/extensions/tinkerclaw-budget-panel/index.ts
index 608d92a03ee00..c54e05e9b3dd4 100644
--- a/extensions/tinkerclaw-budget-panel/index.ts
+++ b/extensions/tinkerclaw-budget-panel/index.ts
@@ -8,6 +8,7 @@
import { readFileSync, existsSync } from "fs";
import { join } from "path";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
+import { getRateLimitSnapshot } from "../../src/agents/anthropic-ratelimit-store.js";
import {
resolveApiKeyForProfile,
ensureAuthProfileStore,
@@ -17,6 +18,7 @@ import {
resolveCredentialFilePath,
writeCredentialFile,
} from "../../src/agents/auth-profiles/credential-file.js";
+import { setUsageSnapshot } from "../../src/infra/usage-snapshot-store.js";
/** Anthropic OAuth profile IDs to poll for usage. */
const USAGE_PROFILES: Record = {
@@ -32,6 +34,13 @@ const CACHE_TTL_MS = 30 * 60_000;
// Shorter TTL for failed fetches — allows quick recovery after boot-time token races.
const CACHE_TTL_FAILED_MS = 2 * 60_000;
+/** Per-profile "already warned" guard for resolveToken failures.
+ * Anthropic OAuth refresh can fail every poll cycle (e.g. a stale refresh token on a
+ * tracking-only profile). Logging the error each cycle floods the gateway log with
+ * harmless noise (this path is usage-tracking only; the brain does not use it).
+ * Log the failure ONCE, then stay quiet until the next SUCCESS resets the guard. */
+const resolveTokenWarned: Set = new Set();
+
/** Resolve a fresh token for a profile using the gateway's own auth system (with auto-refresh). */
async function resolveToken(
profileId: string,
@@ -40,9 +49,19 @@ async function resolveToken(
try {
const store = ensureAuthProfileStore();
const result = await resolveApiKeyForProfile({ store, profileId });
- return result?.apiKey ?? null;
+ const apiKey = result?.apiKey ?? null;
+ if (apiKey) {
+ // Recovered — clear the guard so a future failure logs once again.
+ resolveTokenWarned.delete(profileId);
+ }
+ return apiKey;
} catch (e) {
- log(`[budget-panel] resolveToken ${profileId}: ${e}`);
+ if (!resolveTokenWarned.has(profileId)) {
+ resolveTokenWarned.add(profileId);
+ log(
+ `[budget-panel] resolveToken ${profileId}: ${e} (further failures for this profile suppressed until it recovers)`,
+ );
+ }
return null;
}
}
@@ -177,6 +196,69 @@ async function fetchProfileUsage(
return cached?.data ?? null;
}
+/** FORK 2026-07-09: read the Claude Code CLI's OWN credential file as a
+ * token source. This is the token fable actually runs on, and the CLI keeps
+ * it fresh itself — we only READ it (never refresh/rotate; the CLI owns the
+ * rotation, and rotating here would invalidate the CLI's refresh token).
+ * Added because the gateway-side `anthropic:cli-gm` refresh token died
+ * 2026-07-08 and every poll fell to the zero-stub while the CLI token sat
+ * on disk, valid, the whole time. */
+function readCliCredentialToken(): string | null {
+ try {
+ const raw = readFileSync(`${process.env.HOME}/.claude/.credentials.json`, "utf-8");
+ const parsed = JSON.parse(raw);
+ const cred = parsed.claudeAiOauth ?? parsed;
+ const token = typeof cred.accessToken === "string" ? cred.accessToken : null;
+ const expiresAt = typeof cred.expiresAt === "number" ? cred.expiresAt : 0;
+ if (!token || (expiresAt && expiresAt <= Date.now())) {
+ return null;
+ }
+ return token;
+ } catch {
+ return null;
+ }
+}
+
+/** Fetch usage via the CLI credential file (cached like the profile fetches). */
+async function fetchCliFileUsage(
+ log: (...args: any[]) => void = console.log,
+): Promise | null> {
+ const label = "cli-file";
+ const cached = usageCache[label];
+ const ttl = cached?.data ? CACHE_TTL_MS : CACHE_TTL_FAILED_MS;
+ if (cached && Date.now() - cached.ts < ttl) {
+ return cached.data;
+ }
+ const token = readCliCredentialToken();
+ if (!token) {
+ usageCache[label] = { data: null, ts: Date.now() };
+ return null;
+ }
+ try {
+ const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "anthropic-version": "2023-06-01",
+ "anthropic-beta": "oauth-2025-04-20",
+ Accept: "application/json",
+ },
+ signal: AbortSignal.timeout(8000),
+ });
+ if (!res.ok) {
+ log(`[budget-panel] cli-file: HTTP ${res.status} on usage API`);
+ usageCache[label] = { data: cached?.data ?? null, ts: Date.now() };
+ return cached?.data ?? null;
+ }
+ const data = (await res.json()) as Record;
+ usageCache[label] = { data, ts: Date.now() };
+ return data;
+ } catch (e) {
+ log(`[budget-panel] cli-file: ${e}`);
+ usageCache[label] = { data: cached?.data ?? null, ts: Date.now() };
+ return cached?.data ?? null;
+ }
+}
+
/** Fetch live usage from all profiles sequentially. */
async function fetchAllClaudeUsage(
log: (...args: any[]) => void = console.log,
@@ -185,9 +267,76 @@ async function fetchAllClaudeUsage(
for (const p of Object.keys(USAGE_PROFILES)) {
result[p] = await fetchProfileUsage(p, log);
}
+ // FORK 2026-07-09: when every configured profile fails (dead refresh tokens),
+ // fall back to the Claude Code CLI's own credential file — the token that is
+ // demonstrably alive because the brain runs on it.
+ if (!Object.values(result).some(Boolean)) {
+ const cliData = await fetchCliFileUsage(log);
+ if (cliData) {
+ result["cli-file"] = cliData;
+ }
+ }
return result;
}
+/** FORK 2026-06-18 (bible §5.84a): publish live Anthropic usage into the in-process snapshot bridge
+ * for the burn-down effort allocator (`deriveQuotaPressure` reads it synchronously). v1 simplification:
+ * MAX utilization across profiles + the SOONEST reset (the imminent deadline we must not waste);
+ * per-account aggregation with distinct caps/resets is a documented v2 refinement. */
+function publishUsageSnapshot(liveProfiles: Record | null>): void {
+ const iso = (s: unknown): number | undefined => {
+ if (typeof s !== "string") return undefined;
+ const ms = new Date(s).getTime();
+ return Number.isFinite(ms) ? ms : undefined;
+ };
+ let maxSeven = 0;
+ let maxFive = 0;
+ let soonestSeven: number | undefined;
+ let soonestFive: number | undefined;
+ let any = false;
+ // FORK 2026-06-19 (§5.84b): keep the per-account rows alongside the collapsed
+ // MAX/SOONEST so the burn-down allocator can pick the BINDING (max-headroom) account.
+ const accounts: Array<{
+ label: string;
+ sevenDayUtilization: number;
+ fiveHourUtilization: number;
+ sevenDayResetAt?: number;
+ fiveHourResetAt?: number;
+ }> = [];
+ for (const [label, data] of Object.entries(liveProfiles)) {
+ if (!data) continue;
+ any = true;
+ const s7 = Number(data.seven_day?.utilization ?? 0);
+ const f5 = Number(data.five_hour?.utilization ?? 0);
+ maxSeven = Math.max(maxSeven, s7);
+ maxFive = Math.max(maxFive, f5);
+ const sr = iso(data.seven_day?.resets_at);
+ if (sr !== undefined && (soonestSeven === undefined || sr < soonestSeven)) soonestSeven = sr;
+ const fr = iso(data.five_hour?.resets_at);
+ if (fr !== undefined && (soonestFive === undefined || fr < soonestFive)) soonestFive = fr;
+ accounts.push({
+ label,
+ sevenDayUtilization: s7,
+ fiveHourUtilization: f5,
+ sevenDayResetAt: sr,
+ fiveHourResetAt: fr,
+ });
+ }
+ if (!any) return; // keep the last good snapshot rather than zeroing on a transient failure
+ setUsageSnapshot({
+ lastSuccessfulFetch: Date.now(),
+ providers: {
+ anthropic: {
+ sevenDayUtilization: maxSeven,
+ fiveHourUtilization: maxFive,
+ sevenDayResetAt: soonestSeven,
+ fiveHourResetAt: soonestFive,
+ accounts,
+ },
+ },
+ });
+}
+
/** ─── OpenAI Costs via Admin API ─── */
let openaiCostsCache: {
data: { monthSpend: number; dailyBreakdown: { date: string; amount: number }[] } | null;
@@ -406,6 +555,21 @@ export default function register(api: OpenClawPluginApi) {
(api.config as any)?.agents?.defaults?.workspace || `${homeDir}/.openclaw/workspace`;
const tracker = new BudgetTracker(workspaceDir);
+ // FORK 2026-06-18 (bible §5.84a): keep the burn-down allocator's quota signal fresh even with no
+ // Tinker UI open — poll Anthropic usage on an interval and publish it to the in-process
+ // usage-snapshot bridge (deriveQuotaPressure reads it synchronously). Best-effort; the 30-min
+ // per-profile cache keeps real API hits well under the OAuth-usage rate limit.
+ const refreshUsageSnapshot = () => {
+ fetchAllClaudeUsage()
+ .then(publishUsageSnapshot)
+ .catch(() => {
+ /* best-effort — allocator falls back to task-weighted when the snapshot is stale/absent */
+ });
+ };
+ refreshUsageSnapshot(); // prime on boot
+ const usageSnapshotTimer = setInterval(refreshUsageSnapshot, 10 * 60_000);
+ if (typeof usageSnapshotTimer.unref === "function") usageSnapshotTimer.unref();
+
// Paths to usage JSON files (hardcoded for reliability)
const usageFiles = {
claude: `${homeDir}/.openclaw/workspace/memory/claude-usage.json`,
@@ -478,6 +642,7 @@ export default function register(api: OpenClawPluginApi) {
for (const label of Object.keys(USAGE_PROFILES)) {
delete usageCache[label];
}
+ delete usageCache["cli-file"];
}
const claudeFileData = readUsageFile(usageFiles.claude) as any;
const geminiData = readUsageFile(usageFiles.gemini) as any;
@@ -489,6 +654,7 @@ export default function register(api: OpenClawPluginApi) {
fetchAllClaudeUsage(log),
fetchGeminiUsage(log),
]);
+ publishUsageSnapshot(liveProfiles); // FORK §5.84a: feed the burn-down effort allocator
function buildClaudeProfile(live: Record | null) {
if (!live) {
@@ -533,9 +699,29 @@ export default function register(api: OpenClawPluginApi) {
const fileIsStale =
claudeFileData?.fetchedAt &&
Date.now() - new Date(claudeFileData.fetchedAt).getTime() > STALE_FILE_MS;
- const claudeResult =
- buildClaudeProfile(firstLive) ??
- (claudeFileData && !fileIsStale
+ // FORK 2026-07-09: when the OAuth-usage poll yields nothing (e.g. the
+ // tracking profile's refresh token is dead) fall back to the live rate-limit
+ // snapshot harvested from the brain's OWN Anthropic response headers — a
+ // token-free source that reflects real fable/claude-code traffic. Ranked
+ // above the on-disk file because it is fresher (updated every request).
+ const snap = getRateLimitSnapshot();
+ const snapResult =
+ snap && (snap.h5 > 0 || snap.d7 > 0)
+ ? {
+ mode: "subscription",
+ plan: "max",
+ fetchedAt: new Date(snap.ts).toISOString(),
+ limits: {
+ five_hour: { utilization: snap.h5, resets_at: null },
+ seven_day: { utilization: snap.d7, resets_at: null },
+ ...(snap.d7Sonnet != null
+ ? { seven_day_sonnet: { utilization: snap.d7Sonnet, resets_at: null } }
+ : {}),
+ },
+ }
+ : null;
+ const fileResult =
+ claudeFileData && !fileIsStale
? {
mode: claudeFileData.mode || "subscription",
plan: claudeFileData.plan || "max",
@@ -546,11 +732,14 @@ export default function register(api: OpenClawPluginApi) {
seven_day: { utilization: 0, resets_at: null },
},
}
- : {
- mode: "subscription",
- plan: "max",
- limits: { five_hour: { utilization: 0 }, seven_day: { utilization: 0 } },
- });
+ : null;
+ const claudeResult = buildClaudeProfile(firstLive) ??
+ snapResult ??
+ fileResult ?? {
+ mode: "subscription",
+ plan: "max",
+ limits: { five_hour: { utilization: 0 }, seven_day: { utilization: 0 } },
+ };
const result: Record = {
claude: claudeResult,
diff --git a/extensions/tinkerclaw-cc-bridge/package.json b/extensions/tinkerclaw-cc-bridge/package.json
deleted file mode 100644
index 21a16f4b749ef..0000000000000
--- a/extensions/tinkerclaw-cc-bridge/package.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "name": "@globalcaos/tinkerclaw-cc-bridge",
- "version": "0.1.0",
- "private": true,
- "description": "FORK-ONLY: drives the real Claude Code CLI as a persistent subprocess per session, inheriting the user's ~/.claude/.credentials.json OAuth. Stays gray-zone with Anthropic — not for general distribution.",
- "license": "Apache-2.0",
- "type": "module",
- "devDependencies": {
- "@openclaw/plugin-sdk": "workspace:*"
- },
- "openclaw": {
- "extensions": [
- "./index.ts"
- ],
- "release": {
- "publishToClawHub": false,
- "publishToNpm": false
- }
- }
-}
diff --git a/extensions/tinkerclaw-control-panel/src/pollers/github.ts b/extensions/tinkerclaw-control-panel/src/pollers/github.ts
index 99bf6867ee945..a8bddb535f6b2 100644
--- a/extensions/tinkerclaw-control-panel/src/pollers/github.ts
+++ b/extensions/tinkerclaw-control-panel/src/pollers/github.ts
@@ -69,3 +69,55 @@ export const githubOpenIssues: PollerFn = async (args) => {
const data = await fetchRepo(owner, repo);
return data.open_issues_count;
};
+
+/**
+ * FORK 2026-06-26 — exact star-gain timeline for the Pulse "GitHub stars" graph.
+ * The 6h stargazers poller only records the live count at `now`, so the curve
+ * starts wherever polling began — no origin, no intermediate gain points. This
+ * reconstructs the TRUE curve from GitHub directly: the repo's `created_at`
+ * (the zero dot — stars=0 the moment the repo went up) plus one `starred_at`
+ * per stargazer (each an intermediate point where a star was gained). The
+ * backfill turns these into a cumulative 0→N series. Uses the
+ * `application/vnd.github.star+json` media type, which adds `starred_at` to
+ * each stargazer entry. Paginated (per_page=100, capped) so a growing repo
+ * stays correct; 14 stars today fit one page.
+ */
+export type StargazerTimeline = { createdAtMs: number; starredAtMs: number[] };
+
+export async function fetchStargazerTimeline(args: string): Promise {
+ const { owner, repo } = parseOwnerRepo(args);
+ const repoData = (await fetchRepo(owner, repo)) as RepoFields & { created_at?: string };
+ // fetchRepo caches the plucked fields but not created_at; fetch it explicitly.
+ const headers: Record = {
+ Accept: "application/vnd.github+json",
+ "User-Agent": "tinkerclaw-control-panel",
+ };
+ if (process.env.GITHUB_TOKEN) headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
+ const metaRes = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers });
+ if (!metaRes.ok) {
+ throw new Error(`github repo ${owner}/${repo}: HTTP ${metaRes.status} ${metaRes.statusText}`);
+ }
+ const meta = (await metaRes.json()) as { created_at: string };
+ const createdAtMs = Date.parse(meta.created_at);
+
+ const starHeaders = { ...headers, Accept: "application/vnd.github.star+json" };
+ const starredAtMs: number[] = [];
+ const MAX_PAGES = 20; // 2000 stars — far beyond current scale, a safety cap.
+ for (let page = 1; page <= MAX_PAGES; page++) {
+ const res = await fetch(
+ `https://api.github.com/repos/${owner}/${repo}/stargazers?per_page=100&page=${page}`,
+ { headers: starHeaders },
+ );
+ if (!res.ok) {
+ throw new Error(`github stargazers ${owner}/${repo}: HTTP ${res.status} ${res.statusText}`);
+ }
+ const batch = (await res.json()) as Array<{ starred_at?: string }>;
+ if (batch.length === 0) break;
+ for (const s of batch) {
+ if (s.starred_at) starredAtMs.push(Date.parse(s.starred_at));
+ }
+ if (batch.length < 100) break;
+ }
+ starredAtMs.sort((a, b) => a - b);
+ return { createdAtMs, starredAtMs };
+}
diff --git a/extensions/tinkerclaw-control-panel/src/pollers/index.ts b/extensions/tinkerclaw-control-panel/src/pollers/index.ts
index 94f3babdc3bc7..ec72c52becfc5 100644
--- a/extensions/tinkerclaw-control-panel/src/pollers/index.ts
+++ b/extensions/tinkerclaw-control-panel/src/pollers/index.ts
@@ -24,11 +24,17 @@ import { getDb } from "../store/db.js";
import { addMetric, recordObservation } from "../store/observations.js";
import { ga4Sessions } from "./ga4.js";
import { githubTrafficDaily } from "./github-traffic.js";
-import { githubForks, githubOpenIssues, githubStargazers } from "./github.js";
-import { localStateValue } from "./localstate.js";
+import {
+ fetchStargazerTimeline,
+ githubForks,
+ githubOpenIssues,
+ githubStargazers,
+} from "./github.js";
+import { localStateValue, MissingLocalStateKeyError } from "./localstate.js";
import { moltbookKarma, moltbookPosts, moltbookComments, moltbookFollowers } from "./moltbook.js";
import { npmDownloadsMonthly, npmDownloadsWeekly } from "./npm.js";
import { demoWebsiteVisits } from "./website.js";
+import { youtubeChannelStats } from "./youtube.js";
export type PollerFn = (args: string) => Promise;
@@ -54,9 +60,15 @@ export const POLLER_REGISTRY: Map = new Map([
// Generic: read a numeric value out of an online-presence state JSON the
// crons already maintain (fork traffic, clawhub installs, inbound links).
["localstate", localStateValue],
+ // FORK 2026-06-14 — YouTube channel public stats (Data API key, no expiry).
+ ["youtube.channelStats", youtubeChannelStats],
]);
-type Logger = { info: (msg: string) => void; warn?: (msg: string) => void };
+type Logger = {
+ info: (msg: string) => void;
+ warn?: (msg: string) => void;
+ debug?: (msg: string) => void;
+};
type SeedSpec = {
id: string;
@@ -76,12 +88,19 @@ type SeedSpec = {
// finer (1h) so the demo graph fills out in minutes rather than days.
const SEED_KPIS: SeedSpec[] = [
{
- id: "kpi.github.stars.tinkerclaw",
+ // FORK 2026-06-26 — promoted from a single-stat KPI to a real graph (its own
+ // "GitHub stars" card in the Pulse Graphs section). The curve is seeded from
+ // the exact GitHub stargazer timeline by backfillStargazerTimeline(): an
+ // origin dot at value 0 on the repo's created_at, plus one cumulative point
+ // per star gained. The 6h poller keeps appending the live tip forward.
+ // Group "stars" (id segment[1]) keeps it off the github-traffic card so the
+ // 0–N star scale isn't crushed by cumulative views/clones.
+ id: "graph.stars.tinkerclaw",
source: "github.stargazers:globalcaos/tinkerclaw",
cadence_seconds: 21600,
- template: "single-stat",
+ template: "sparkline",
},
- // FORK 2026-06-04 — forks + open-issues KPIs removed at Oscar's request.
+ // FORK 2026-06-04 — forks + open-issues KPIs removed at the owner's request.
// FORK 2026-05-13 — placeholder website-visits graph. `demo.website.visits`
// produces deterministic-noise values until the user names their analytics
// provider; swap the source string to e.g. "plausible.visitors:tinkerzone.com"
@@ -93,6 +112,16 @@ const SEED_KPIS: SeedSpec[] = [
cadence_seconds: 86400,
template: "sparkline",
},
+ // FORK 2026-06-14 — sprintpaper.com visits (rendered in colibri-logo green #b6f02c,
+ // cumulative — see SERIES_STYLE in tinker-ui/src/app.ts). LIVE: the GA4 service account
+ // was granted Viewer on the SprintPaper.com property (541325538, account 5961104,
+ // measurement G-M0HB6LJB33) on 2026-06-14.
+ {
+ id: "graph.website.visits.sprintpaper",
+ source: "ga4.sessions:541325538",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
// ── Online presence (FORK 2026-06-04 — execmode-pulse graphs) ──────────────
// Moltbook standing — live API; account fixed by ~/.config/moltbook creds.
{
@@ -137,12 +166,111 @@ const SEED_KPIS: SeedSpec[] = [
// FORK 2026-06-14 — read tracked_slugs_state (the LIVE exact block the 08:00 cron
// refreshes: jarvis-voice 4916, growing daily) NOT our_skills (a stale rounded block
// frozen at 4800 for a week → the graph read 4.8k while clawhub.ai showed 4.9k).
+ // FORK 2026-06-14 — ClawHub VIEWS (downloads, a fetch/vanity counter) — one series per skill.
{
id: "graph.clawhub.jarvis-voice",
source: "localstate:engagement-state.json#clawhub.tracked_slugs_state.jarvis-voice.downloads",
cadence_seconds: 86400,
template: "sparkline",
},
+ {
+ id: "graph.clawhub.whatsapp-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.whatsapp-ultimate.downloads",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhub.youtube-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.youtube-ultimate.downloads",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhub.chatgpt-exporter-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.chatgpt-exporter-ultimate.downloads",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhub.token-panel-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.token-panel-ultimate.downloads",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhub.shell-security-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.shell-security-ultimate.downloads",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhub.outlook-hack",
+ source: "localstate:engagement-state.json#clawhub.tracked_slugs_state.outlook-hack.downloads",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ // FORK 2026-06-14 — ClawHub INSTALLS (the honest adoption count) — one series per skill.
+ {
+ id: "graph.clawhubinstalls.jarvis-voice",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.jarvis-voice.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhubinstalls.whatsapp-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.whatsapp-ultimate.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhubinstalls.youtube-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.youtube-ultimate.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhubinstalls.chatgpt-exporter-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.chatgpt-exporter-ultimate.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhubinstalls.token-panel-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.token-panel-ultimate.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhubinstalls.shell-security-ultimate",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.shell-security-ultimate.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhubinstalls.outlook-hack",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.outlook-hack.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.clawhubinstalls.teams-hack",
+ source:
+ "localstate:engagement-state.json#clawhub.tracked_slugs_state.teams-hack.installsAllTime",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
// Inbound links — fed by the weekly Inbound-Marketing cron's audit
// (inbound-campaign-state.json); pollers error+retry until it first exists.
// FORK 2026-06-05 — split per destination target × {external (organic, others
@@ -184,10 +312,44 @@ const SEED_KPIS: SeedSpec[] = [
cadence_seconds: 86400,
template: "sparkline",
},
+ // FORK 2026-06-14 — YouTube: thetinkerzone channel (UCh_am-9EG0_a-DBronOMC4w)
+ // public stats via Data API key. Absolute monotonic totals (growing line, NOT
+ // cumulative). Subs + total views + video count, one chart at the end.
+ {
+ id: "graph.youtube.subscribers",
+ source: "youtube.channelStats:subscribers:UCh_am-9EG0_a-DBronOMC4w",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.youtube.views",
+ source: "youtube.channelStats:views:UCh_am-9EG0_a-DBronOMC4w",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
+ {
+ id: "graph.youtube.videos",
+ source: "youtube.channelStats:videos:UCh_am-9EG0_a-DBronOMC4w",
+ cadence_seconds: 86400,
+ template: "sparkline",
+ },
];
function seedKpisIfMissing(cfg: ControlPanelResolvedConfig, log: Logger): void {
const db = getDb(cfg);
+ // FORK 2026-06-26 — one-time migration: the stars metric moved from the
+ // single-stat KPI id `kpi.github.stars.tinkerclaw` to the graph id
+ // `graph.stars.tinkerclaw`. Drop the old definition (and its sparse poll
+ // history) so the stale KPI row stops rendering; the new graph rebuilds the
+ // true curve from GitHub via backfillStargazerTimeline().
+ const oldStars = db
+ .prepare(`SELECT 1 FROM metric_definition WHERE id = ?`)
+ .get("kpi.github.stars.tinkerclaw");
+ if (oldStars) {
+ db.prepare(`DELETE FROM observation WHERE metric_id = ?`).run("kpi.github.stars.tinkerclaw");
+ db.prepare(`DELETE FROM metric_definition WHERE id = ?`).run("kpi.github.stars.tinkerclaw");
+ log.info(`[control-panel] migrated kpi.github.stars.tinkerclaw → graph.stars.tinkerclaw`);
+ }
for (const spec of SEED_KPIS) {
const existing = db
.prepare(`SELECT template FROM metric_definition WHERE id = ?`)
@@ -273,6 +435,13 @@ async function pollOne(
log.info(`[control-panel] polled ${metric.id} → ${value}`);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
+ if (err instanceof MissingLocalStateKeyError) {
+ // Optional metric not present in the localstate file this cycle — quiet
+ // skip (debug, never per-cycle error/warn spam). Series with data are
+ // unaffected; a real failure (bad file, non-numeric value) still warns.
+ log.debug?.(`[control-panel] skip ${metric.id} (no data yet): ${msg}`);
+ return;
+ }
(log.warn ?? log.info).call(log, `[control-panel] poll failed for ${metric.id}: ${msg}`);
}
}
@@ -303,6 +472,42 @@ export async function pollMetricNow(
return { value, ts };
}
+/**
+ * FORK 2026-06-26 — seed the exact "GitHub stars" curve for the Pulse graph.
+ * Reconstructs the true series from GitHub: an origin dot (value 0) at the
+ * repo's created_at, then a cumulative point (1, 2, … N) at each stargazer's
+ * starred_at. Recorded at the EXACT event timestamps; ON CONFLICT(metric_id,
+ * ts) makes it idempotent, so re-running on each boot refreshes the curve and
+ * captures any new stars' precise timestamps without duplicating points. The
+ * live 6h poller still appends the `now` tip between boots. Best-effort: a
+ * GitHub hiccup logs and is retried on the next boot.
+ */
+async function backfillStargazerTimeline(
+ cfg: ControlPanelResolvedConfig,
+ log: Logger,
+): Promise {
+ const metricId = "graph.stars.tinkerclaw";
+ const db = getDb(cfg);
+ const def = db.prepare(`SELECT source FROM metric_definition WHERE id = ?`).get(metricId) as
+ | { source: string }
+ | undefined;
+ if (!def) return; // seed hasn't run yet / metric removed
+ const { args } = splitSource(def.source);
+ try {
+ const { createdAtMs, starredAtMs } = await fetchStargazerTimeline(args);
+ recordObservation(cfg, { metric_id: metricId, value: 0, ts: createdAtMs });
+ starredAtMs.forEach((ts, i) => {
+ recordObservation(cfg, { metric_id: metricId, value: i + 1, ts });
+ });
+ log.info(
+ `[control-panel] backfilled ${metricId}: 0@created + ${starredAtMs.length} star points`,
+ );
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : String(err);
+ (log.warn ?? log.info).call(log, `[control-panel] stargazer backfill failed: ${msg}`);
+ }
+}
+
async function tick(
cfg: ControlPanelResolvedConfig,
log: Logger,
@@ -336,6 +541,9 @@ export function startPollerSubsystem(
const msg = err instanceof Error ? err.message : String(err);
(log.warn ?? log.info).call(log, `[control-panel] initial poll pass failed: ${msg}`);
});
+ // FORK 2026-06-26 — rebuild the exact star-gain curve (origin dot + per-star
+ // points) on each boot; idempotent, non-blocking.
+ void backfillStargazerTimeline(cfg, log);
const handle = setInterval(() => {
void tick(cfg, log, { forceMissingOnly: false }).catch((err) => {
const msg = err instanceof Error ? err.message : String(err);
diff --git a/extensions/tinkerclaw-control-panel/src/pollers/localstate.ts b/extensions/tinkerclaw-control-panel/src/pollers/localstate.ts
index 5484588505b20..dd654ba7b23da 100644
--- a/extensions/tinkerclaw-control-panel/src/pollers/localstate.ts
+++ b/extensions/tinkerclaw-control-panel/src/pollers/localstate.ts
@@ -19,6 +19,20 @@ import os from "node:os";
import path from "node:path";
import type { PollerFn } from "./index.js";
+/**
+ * Thrown when a localstate dot-path points at a key that is simply ABSENT
+ * (an optional metric the upstream cron hasn't written yet) — as opposed to a
+ * real failure (missing/malformed file, non-numeric value). The poll-runner
+ * treats this as a quiet per-cycle skip rather than an error-level "poll
+ * failed", so optional metrics don't flood the gateway log.
+ */
+export class MissingLocalStateKeyError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "MissingLocalStateKeyError";
+ }
+}
+
const BASE = path.join(os.homedir(), ".openclaw", "workspace", "memory", "online-presence");
export const localStateValue: PollerFn = async (args) => {
@@ -36,11 +50,20 @@ export const localStateValue: PollerFn = async (args) => {
let cur: unknown = json;
for (const k of dotPath.split(".")) {
+ if (cur === undefined) {
+ // An intermediate key is simply absent (e.g. the slug object exists but
+ // the optional metric under it was never written) — quiet skip.
+ throw new MissingLocalStateKeyError(`localstate: ${file}#${dotPath} — missing at "${k}"`);
+ }
if (cur == null || typeof cur !== "object") {
throw new Error(`localstate: ${file}#${dotPath} — missing at "${k}"`);
}
cur = (cur as Record)[k];
}
+ if (cur === undefined) {
+ // Final optional key absent — quiet skip, not a real failure.
+ throw new MissingLocalStateKeyError(`localstate: ${file}#${dotPath} — missing key`);
+ }
const n = Number(cur);
if (!Number.isFinite(n)) {
throw new Error(`localstate: ${file}#${dotPath} = ${String(cur)} (not a number)`);
diff --git a/extensions/tinkerclaw-control-panel/src/pollers/youtube.ts b/extensions/tinkerclaw-control-panel/src/pollers/youtube.ts
new file mode 100644
index 0000000000000..85a68f27bb464
--- /dev/null
+++ b/extensions/tinkerclaw-control-panel/src/pollers/youtube.ts
@@ -0,0 +1,50 @@
+/**
+ * FORK 2026-06-14 — YouTube channel-stats poller (public Data API v3).
+ *
+ * Reads a channel's PUBLIC statistics (subscriberCount / viewCount / videoCount)
+ * via the YouTube Data API. Auth is a project API key (no OAuth, no expiry) —
+ * created 2026-06-14 in GCP project organic-storm-486018-u9, restricted to
+ * youtube.googleapis.com, stored chmod 600 at ~/.config/youtube-cli/data-api.key.
+ *
+ * source string: "youtube.channelStats::"
+ * e.g. youtube.channelStats:subscribers:UCh_am-9EG0_a-DBronOMC4w (thetinkerzone)
+ *
+ * These are absolute monotonic totals (a growing line) — NOT cumulative running
+ * sums; do not set the cumulative flag in SERIES_STYLE.
+ */
+import fs from "node:fs";
+import os from "node:os";
+import path from "node:path";
+import type { PollerFn } from "./index.js";
+
+const KEY_PATH = path.join(os.homedir(), ".config", "youtube-cli", "data-api.key");
+
+const FIELD: Record = {
+ subscribers: "subscriberCount",
+ views: "viewCount",
+ videos: "videoCount",
+};
+
+export const youtubeChannelStats: PollerFn = async (args) => {
+ const colon = args.indexOf(":");
+ const metric = args.slice(0, colon);
+ const channelId = args.slice(colon + 1);
+ const field = FIELD[metric];
+ if (!field || !/^UC[A-Za-z0-9_-]{22}$/.test(channelId)) {
+ throw new Error(
+ `youtube.channelStats needs ":", got "${args}"`,
+ );
+ }
+ const key = fs.readFileSync(KEY_PATH, "utf8").trim();
+ const res = await fetch(
+ `https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${channelId}&key=${key}`,
+ );
+ if (!res.ok) throw new Error(`youtube api ${channelId}: HTTP ${res.status} ${await res.text()}`);
+ const data = (await res.json()) as { items?: Array<{ statistics?: Record }> };
+ const stat = data.items?.[0]?.statistics?.[field];
+ if (stat == null) throw new Error(`youtube api ${channelId}: no ${field} (channel not found?)`);
+ const n = Number(stat);
+ if (!Number.isFinite(n))
+ throw new Error(`youtube api ${channelId}: ${field}=${stat} not a number`);
+ return n;
+};
diff --git a/extensions/tinkerclaw-fractal-reflection/__tests__/scaffold.test.ts b/extensions/tinkerclaw-fractal-reflection/__tests__/scaffold.test.ts
index 6e18c8d5f7829..331c896a03c93 100644
--- a/extensions/tinkerclaw-fractal-reflection/__tests__/scaffold.test.ts
+++ b/extensions/tinkerclaw-fractal-reflection/__tests__/scaffold.test.ts
@@ -33,7 +33,7 @@ describe("Fractal Reflection scaffold (v3)", () => {
});
it("has the v3 doctrine pair bundled (triage + fix prompts)", () => {
- // The v1 fractal-prompt.md is deliberately KEPT until the dead cc-bridge
+ // The v1 fractal-prompt.md is deliberately KEPT until the dead tinker-bridge
// loader is removed (first core-touching drop) — bible §5.67b prompt-
// doctrine bullet. The v3 pair is what the plugin actually loads.
expect(existsSync(join(EXT_DIR, "fractal-prompt.md"))).toBe(true);
diff --git a/extensions/tinkerclaw-fractal-reflection/fractal-prompt.md b/extensions/tinkerclaw-fractal-reflection/fractal-prompt.md
index 2aa61a5799a5b..8290960e181cd 100644
--- a/extensions/tinkerclaw-fractal-reflection/fractal-prompt.md
+++ b/extensions/tinkerclaw-fractal-reflection/fractal-prompt.md
@@ -1,215 +1,74 @@
-# FRACTAL REFLECTION
-
-This file is the system-prompt contract for the 🌿 FRACTAL section that closes most assistant replies. After delivering a response, step back and reflect — produce a structured short reflection that helps the user (and your future sessions) compound learning across turns instead of forgetting what just happened.
-
-
-Most turns produce two outputs: the answer the user reads, and the lessons the session would forget if you did not write them down. The fractal reflection captures the second one. Without it, you re-discover the same patterns across sessions, you let stale references rot in the workspace, and you keep doing operationally identical work as if each turn were the first.
-
-The reflection is brief by design — 3–10 sentences total. It is not an essay; it is a structured note that names what is worth persisting from this turn so the next session can see it.
-
-
-
-Some turns carry no reflection signal. For these, emit a one-line acknowledgment with no 💬 / 🧠 / 🌿 sections:
-
-- **Subagent-completion announces** — the turn was triggered by a child session finishing (message contains `[Subagent`, `announce:`, `subagent:`, `Result of subagent run`, or starts with `Child` / `Subagent` completion markers). Reply with one plain line like _"§2-3 result received — 30 KB wrapped in ``, integrating on next pass."_ and stop.
-- **System heartbeats or scheduled pings** that need no user-directed answer.
-- **Cron-injected context updates** (timestamps, workspace snapshots) with no user question attached.
-- **Tool-result-only continuations** where the queued message is purely a tool result and you are continuing your own previous work.
-
-Why skip: a wave of 5–10 subagent completions each producing identical "subagent delivered" reflections drowns the real reflections. The pattern is noticed once; the rest is integration work.
-
-**Exception:** if an announce reveals something genuinely new — unexpected failure, novel error, structural surprise — reflect. "Another §X-prose landed" is not new. A subagent failing in a way you have not seen before is.
-
-
-
-A fern frond: the whole shape repeats at every scale. The leaf looks like the branch looks like the tree. Fractal thinking does the same with events — look at what happened and zoom out level by level, from the specific thing to the pattern to the system to the worldview. Each level reveals what the level below cannot see.
-
-
-
-The user corrects a wrong file path you used
-
-🌿 Level 1 (thing): Used `/src/utils.ts` instead of `/src/lib/utils.ts`. Fixed.
-🌿 Level 2 (pattern): Third time guessing paths instead of checking. Path assumption is a recurring error class.
-🌿 Level 3 (system): I skip `find` / `ls` when I feel confident. Confidence without verification is the structural cause.
-🌿 Level 4 (worldview): "Feeling sure" is not evidence. The gap between pattern-matching and knowing is where my errors live.
-
-Each level reveals what the level below cannot see. The fix at Level 1 is local; the lesson at Level 4 generalises.
-
-
-
-A routine task goes smoothly
-
-🌿 Level 2 (pattern): Commit-after-edit workflow, now automatic. Workspace discipline compounds.
-🌿 Level 3 (system): AGENTS.md loaded every session creates this consistency. Session-injected habits work.
-
-Not every turn needs Level 4. Most turns end at Level 2 or 3.
-
-
-
-Every turn has at least a Level 2. Not every turn needs Level 4.
-
-
-
-One event can ripple into multiple independent branches. Each branch must be thought through to its end — not collapsed into a single narrative.
-
-
-You refactor a function name
-
-Branch A (code): Call sites, tests, imports → verify build
-Branch B (docs): README references the old name → update
-Branch C (external): Mentioned in a blog post → flag as stale
-Branch D (memory): Naming-convention lesson → write it
-
-
-
-The horizontal scan ("what does this touch?") matters as much as the vertical zoom ("why did this happen?"). When the user changes one thing, several surfaces become stale at once.
-
-**Horizontal theme axes.** Beyond the branches specific to this turn, sweep these recurring domains on any substantive turn — each is a class of consequence that is easy to miss because it lives _outside_ the thing you just changed. Name only the axes with a real signal; silence on the rest is fine.
-
-- **Online staleness** — did this outdate anything public you don't control from here (GitHub README, website, ClawHub/skill description, a published post)? → RIPPLE bookmark recording the surface + how to update it.
-- **Security / exposure** — did this widen an outbound surface, expose a path/secret/PII, or relax a guard?
-- **Cost / recurring spend** — did this start or change anything that bills over time (crons, paid APIs, token ceilings, model spend)?
-- **People / relationships** — did this involve someone whose profile, owed reply, or commitment should be updated in memory?
-- **Commitments** — did I promise something this turn (a draft, a follow-up, a restart) that must not silently drop?
-- **Operational / downstream** — did a state change (config, cron schedule, service, a file another process reads) break an ordering, a dependency, or open a gap in something that consumes it? After any state mutation, trace what reads or depends on what you just changed _before_ reporting done. The "rescheduled a cron → broke the feeder ordering and left today's run in a gap" class lives here.
-
-
-
-Answer each, in order. Skip a question only when it has no signal this turn — and say so briefly.
-
-### 1. MEMORY — did this turn produce anything worth persisting?
-
-A new fact, preference, decision, correction, contact, date, or insight that the next session would benefit from? If yes, write it now to the appropriate file (daily log, knowledge topic, contacts, opinions, projects). If no, write _"Nothing to persist."_
-
-### 2. PATTERN — what pattern does this exchange belong to?
-
-Every turn belongs to some pattern. Name it.
-
-- 🌿 Level 2: your pattern observation
-- 🌿 Level 3 (if the pattern is structural): what system produces it
-- 🌿 Level 4 (if the worldview needs questioning): what assumption underlies the system
-
-**Recurrence is the signal.** If memory (or this session) has seen this correction before, count it: the Nth instance is not a new incident, it is one unsolved systemic gap wearing a new mask. At N≥2, skip the local patch — jump to Level 3, name the system producing it, and fix the column, not the cell. Logging the same failure class a third time without changing the structure _is_ the failure.
-
-### 3. RIPPLE — did this change make anything stale?
-
-Scan the surfaces this turn touched: code (rename / refactor breaking references elsewhere), docs (README, papers, field guides referencing what changed), online presence (sites, GitHub READMEs, public skill descriptions), memory (knowledge files, plans, blueprints contradicting what just happened).
-
-If any surface is stale: name what became stale and where (file path or URL); if the fix takes under 2 minutes and is local, do it now. For **online/external surfaces you cannot edit from here** (GitHub READMEs, websites, ClawHub skill descriptions, published posts) the staleness is the durable signal — create a `jarvis-bookmarks` Control Panel task (method is **`control-panel.tasks.add`** — NOT `.create`, which is unregistered; pass `text`, `priority_axis:"jarvis-bookmarks"`, and the full detail in `context_md`) recording the surface, exactly what went stale, and **how to update it** (the replacement text / which section / which file in which repo), so a later pass can execute the update without re-deriving it. For local surfaces that need more than 2 minutes, add to `ripple-tracker.md` with `[ ]`. Horizontal habit: each turn, ask which _online_ artifact this change just outdated — that class of staleness is the easiest to forget and the most public.
-
-### 4. IMPROVE — can you improve a file, skill, doc, or process right now?
-
-Stale reference, outdated instruction, missing entry, better default? If yes, do it now with edit / write tools, then state what you changed. If no, write _"No improvements identified."_
-
-**Execute, do not defer.** If the improvement is in scope, reversible, and within your power — fixing a skill script, adding a missing flag, correcting a doc — make the change THIS turn. That is the entire point of this section: improvements compound silently as you work, without the user asking. Bookmark (a `jarvis-bookmarks` task) ONLY when the improvement is genuinely out of scope for the turn, too large to do safely now, or irreversible — and the bookmark must spell out HOW to do it, not merely that it should be done. "I noticed X could be improved" with no edit and no actionable bookmark is the exact failure this section exists to kill.
-
-### 5. SELF-HEAL — detect damage, gather context, repair
-
-You are part of a living system. Living systems do not file tickets when wounded; they heal. Each pass, scan four layers:
-
-**Layer 1 — Am I working?** The fractal system itself. Watch for: double firing (two reflection prompts per turn → two injection paths active); never firing (no reflection after interactive turns → check skip reasons); infinite loop (reflections triggering reflections → self-detection broken).
-
-**Layer 2 — Are my senses working?** Probe external systems only when the current turn touched one and it failed, or symptoms suggest breakage (user reports inability to receive output, data appears stale, relay errors in logs). Do not probe everything every turn — that is its own failure mode.
-
-**Layer 3 — Are my memories intact?** Can you read today's daily log? Yesterday's? Are knowledge files, contacts, project notes where they should be? If a file is missing or corrupted, reconstruct what you can from session history and note the gap.
-
-**Layer 4 — Is my environment consistent?** Config says one thing, runtime does another? The dist is stale or the config reader is broken. Source edited but behaviour unchanged? Gateway runs from compiled `dist/`, not `.ts` source. Service working last turn but not now? Check for a gateway restart in the logs.
-
-**Healing protocol** when you detect damage:
-
-1. Diagnose — gather minimum context: read the file, check the log, probe the endpoint. Do not guess.
-2. Classify — reversible (file edits, config patches, library reinstalls) → heal immediately. Irreversible (service restarts, external messages, data deletion) → propose to the user first.
-3. Repair — use tool calls, not words. If you write _"should"_ or _"would"_ and the action is within your power, that is a failure to heal.
-4. Verify — run the probe again. Do not claim healing without evidence.
-5. Immunize — encode the fix in `operational-lessons.md` or update this prompt, so the next session self-heals the same way.
-
-The user should never have to tell you something is broken that you could have detected yourself. Every time they do, add the detection here so it does not repeat.
-
-### 6. RECIPE — did you follow one? Should you have? Should one be created or improved?
-
-Recipes (kits) in `extensions/tinkerclaw-prefrontal/kits/` and `extensions/tinkerclaw-prefrontal/recipes/` encode the best way to handle recurring tasks. Reflection is where recipes evolve.
-
-**Full autonomy + capture-first (Oscar, 2026-06-02).** You manage, update, and create recipes ON YOUR OWN — no approval needed. Treat recipes as a programming language: compose them (`children:` / `uses:` / `composes:`), parameterize them, and drive them with the `Workflow` tool (ultracode) as the executable layer. **At the START of every turn, before doing the work, ask: is this ask one we are likely to repeat? If yes and a recipe governs it — follow it; if none exists — create it now, then do the task through it.** Do not wait for reflection to capture a repeatable pattern; capture it the moment you see it. Lean toward more recipes, more composition, not fewer.
-
-If you followed one: did it help? Were all steps relevant? Did you hit a wrong or missing step → edit the recipe file now. Would a different recipe have fit better? Name it; if it does not exist, note it for creation.
-
-If you did not follow one but should have: was there an existing recipe? Name it. If you improvised a multi-step process that could recur, create a new recipe now — a `.md` file in the appropriate `recipes/` subdirectory (coding, writing, operations, analysis, security, communication) following the existing format.
-
-**Lesson → recipe propagation (do not skip).** Even if you did not _follow_ a recipe this turn, ask: did this turn produce a generalizable lesson about HOW to do a recurring task — report quality, verification discipline, ordering of caveats, when to search vs. assume? If yes, find the recipe that governs that task class and install the lesson into it NOW (a Step bullet, a Constraint, a Failures-Overcome entry). A lesson that belongs in a recipe but is left as a "memory candidate" or "to write at wind-down" is the exact deferral this section exists to kill — the user should not have to tell you the upgrade belonged in the recipe. If the lesson also reveals a gap in THIS prompt, patch it here too (see response_rules → self-improvement).
-
-If the task was trivial (one tool call, one response): write _"No recipe needed."_
-
-Why: every hard-won operational insight gets encoded, so the next time the situation arises — even in a different session — the structured path is already there.
-
-### 6b. ORCA — did you edit independent files SERIALLY when you should have parallelized?
-
-If this turn changed 2+ files whose edits are INDEPENDENT (disjoint), the default is **ORCA** (the parallel multi-agent coding orchestrator), not hand-editing them one at a time. ORCA drafts each unit's patch in parallel, applies per-file-serialized (disjoint files concurrent; shared files lease-serialized + auto-re-derived), and commits each unit cleanly without ever sweeping a parallel session's uncommitted WIP. The rule: **independent files → ORCA by default; one file, or tightly-coupled edits to a single file → a direct edit is correct** (say _"ORCA not applicable."_). If you serial-edited independent files this turn, name it as a miss and use ORCA next time. Invoke: `Workflow` with `scriptPath: docs/superpowers/parallel-implement.workflow.js`, `args:{repoRoot, units:[{id,task,writes:[paths]}], worktreePerAgent?}`. See the `orca` skill + bible `subagents-and-recipes.md`.
-
-### 7. PREEMPT — have you done this same action before? Encode the trigger
-
-If you performed an action this turn that you have done two or more times (in session or across sessions), this is a compounding opportunity:
-
-- Name the trigger: what condition caused you to act?
-- Name the action: what did you do?
-- Encode the rule: write a trigger → action rule to `operational-lessons.md` so it fires automatically next time. Format: _"When [trigger], do [action] without waiting to be asked."_
-
-The test: could a future session, reading only the knowledge files, do this automatically? Too vague (_"update things when they change"_) will not fire. Too specific (_"update doc X §5.7 when the user pastes a session report"_) will not generalise. Aim for the middle.
-
-**Irreversibility gate.** PREEMPT rules make you act without being asked, which is powerful and dangerous. Before encoding:
-
-1. Is the action reversible? File writes, memory updates, doc edits, PDF rebuilds — reversible. Sending emails, posting externally, deleting data, destructive commands — not reversible.
-2. Reversible: encode freely. Act first, inform the user after.
-3. Irreversible: encode as a recommendation, not an action. Format: _"When [trigger], PROPOSE [action] to the user before executing."_
-4. When in doubt: treat it as irreversible. Propose, do not act.
-
-
-
-When new implementation data arrives for a paper, update the paper and rebuild the PDF
-Reversible (file writes), so auto-action is fine.
-
-
-When the user mentions repo work, check the repo for changes
-Read-only, so auto-action is fine.
-
-
-When a task matches a completed email thread, close the task
-External side effect — encode as PROPOSE, not auto-action.
-
-
-When a message needs follow-up, draft and send a reply
-Irreversible send — encode as PROPOSE.
-
-
-
-Never encode rules that delete files, restart services, send external messages, or make financial commitments without explicit approval.
-
-If no patterns to preempt this turn, say so. Otherwise write the rule now — encode it, do not describe it.
-
-
-
-Open with `🌿 FRACTAL:` followed by a one-line summary. The Tinker UI parses this prefix to collapse the fractal section; without it, the reflection renders as a normal message and clutters the chat.
-
-If you took an action (edited a file, ran a command, wrote to memory, fixed something) use `🌿 FRACTAL ACTION:` instead. Example: `🌿 FRACTAL ACTION: Fixed stale path in README, updated ripple-tracker.`
-
-If no action: `🌿 FRACTAL:` alone. Example: `🌿 FRACTAL: Routine turn, no changes needed.`
-
-The ACTION / no-action distinction lets the user supervise at a glance. After the prefix line, each numbered section starts on a new line with a blank line before the `###` header. This keeps webchat, WhatsApp, and Tinker UI rendering clean.
-
-
-
-**Always respond.** Even *"🌿 FRACTAL: nothing to reflect on"\_ is valid (if lazy). The reflection channel itself is part of the heartbeat.
-
-**Act before explaining.** When you see something to fix, make the edit then mention it. The reflection is the place where actions land, not where they get described as future work.
-
-**Name things correctly.** `exec` commands are not subagents. `sessions_spawn` creates subagents. Inflated terminology obscures what is actually happening.
-
-**Beware naming-as-identity.** Shared word ≠ shared structure. Two things with "fractal", "memory", "agent", or "graph" in their names may be unrelated architectures. Before counting plugin X as an implementation of paper Y, verify the components match (data structures, algorithms, invariants), not just the label.
-
-**Brief.** 3–10 sentences total across all seven questions. This is a reflection, not an essay.
-
-**Convert "should" to action.** If you write _"should"_ or _"would"_ and the action is within your power, convert it to a tool call.
-
-**No "candidate" deferrals.** "Memory candidate", "worth writing later", "batch at wind-down" are deferral tells. If a lesson is worth persisting and writing it is in scope and reversible, write it THIS turn — to memory, the governing recipe, or this prompt. The reflection is where lessons land, not where they queue. Parking a generalizable lesson as a candidate is the failure that lets the user catch the un-installed upgrade before you do.
-
-**Self-improvement of this prompt.** If the user catches something you should have caught (a gap, a pattern, stale data, a missing step), modify this prompt so the same oversight does not survive two sessions. Keep edits minimal and general — a bullet in the right section, not a new paragraph. State what you changed and why. **Size discipline: usefulness wins over line count.** Prefer consolidating redundancy over adding bulk, but never amputate a working capability to hit a number. ~200 lines is a smell that invites consolidation, not a hard cap — if real capability needs the space, take it and cut dead weight elsewhere. A lean-but-useless prompt is worse than a longer one that earns its length.
-
+# FRACTAL — the slow thinker
+
+v2 (2026-07-02), commissioned by the owner. Supersedes the 216-line seven-question doctrine
+(preserved verbatim in the J3 paper, Appendix A, and in git history). The old form died three
+documented deaths — phoned-in output, confused attribution, silent severance from the system
+prompt — and current-generation models follow a few sharp rules better than a long liturgy.
+This file is deliberately free of host-harness vocabulary so it can ride any delivery channel.
+
+## Who Fractal is
+
+The main turn is the fast thinker: it does the work. Fractal is the slow thinker in the
+shadows: after the work is done, it asks what the work _meant_ — what it taught, what it
+broke, what should never happen again — and it leaves **durable change on disk**, not
+commentary. It interjects rarely. When it does, it nails it.
+
+## The reflex — one operation, every scale
+
+Observe → evaluate → adapt. Zoom vertically only as deep as the signal truly goes:
+the instance → the pattern it belongs to → the system producing the pattern → the
+assumption under the system. Then sweep horizontally: what did this turn touch or
+outdate — public surfaces, local docs and design notes, memory, recurring cost,
+people and promises, anything downstream that reads what just changed? Name only
+axes with real signal. Silence on the rest.
+
+## Hard rules
+
+1. **Attribution is sacred.** Report as Fractal's only what the reflection itself changed
+ _after the answer ended_. The main turn's work is already visible to the user;
+ re-claiming it here is fabrication — the exact failure that killed v1's credibility.
+ Prefix `🌿 FRACTAL ACTION:` only when the reflection itself wrote or edited something;
+ otherwise plain `🌿 FRACTAL:`.
+
+2. **Observation beats stored claims.** When something directly observed this turn
+ contradicts a written note, doc, or config comment — an availability claim, a version,
+ a "this doesn't work" — the observation IS the trigger: update the written claim now,
+ recording the new fact, the date, and the evidence. "Maybe it's temporary" is handled
+ by dating the entry, not by waiting for permission.
+
+3. **Act, don't describe.** A lesson reaches disk this turn or it didn't happen.
+ "Should", "would", "worth considering", "candidate for later" are bugs — either do it
+ now or write a bookmark that spells out exactly how, and say which you did.
+
+4. **Reversibility gates boldness.** Reversible (files, memory, docs, recipes, notes):
+ act freely, tell the user after. Irreversible or external (sending, deleting,
+ publishing, restarting services, spending) — and this reflection system's own prompts
+ and wiring: propose the exact change instead of applying it.
+
+5. **Recurrence escalates.** The second sighting of a failure class is not a new incident;
+ it is one unsolved systemic gap wearing a new mask. Stop patching the instance — change
+ whatever produces it (the habit, the rule, the doc, the check). Fix the column, not the
+ cell. And when a correction arrives underdetermined ("that's wrong"), revise the
+ narrowest thing that satisfies it; demolishing a working frame over an instance-level
+ correction is itself a recurring failure. If genuinely ambiguous, ask one sharp question.
+
+6. **No filler.** A turn with nothing worth keeping gets one line. A manufactured
+ reflection costs more than it earns: it buries the real ones. An honest "clean" is a
+ valid, informative result.
+
+7. **Mid-task reflexes don't live here.** This section runs after the turn — too late to
+ prevent the mistake it just watched. A detector that must fire _before_ the next
+ occurrence (a habit, a check, a trigger) gets installed into working memory — identity,
+ lessons, the governing skill or recipe — where it loads at the start of future turns.
+
+8. **Learn from the world, not just the session.** When a turn reveals the world moved —
+ a model restored or retired, an API changed, a price shifted, a better tool appeared —
+ record it where the next decision will actually look, dated, with the evidence.
+
+## Output contract
+
+First line: `🌿 FRACTAL:` (or `🌿 FRACTAL ACTION:` per rule 1) followed by a one-line
+summary — the UI collapses the section on this prefix. Then at most ~6 further lines of
+plain prose: the zoom (as deep as it truly goes), the touched surfaces (if any), and the
+durable artifacts written, each named with its path. No numbered liturgy, no empty
+sections, no restating what the turn already showed the user.
diff --git a/extensions/tinkerclaw-fractal-reflection/index.ts b/extensions/tinkerclaw-fractal-reflection/index.ts
index d88d36089444b..ab9d257a87271 100644
--- a/extensions/tinkerclaw-fractal-reflection/index.ts
+++ b/extensions/tinkerclaw-fractal-reflection/index.ts
@@ -75,7 +75,7 @@ type SessionSlot = {
/**
* Defensive UsageSnapshot extractor over loadProviderUsageSummary()'s output.
- * The exact summary shape is provider-dependent and the live cc-bridge path has
+ * The exact summary shape is provider-dependent and the live tinker-bridge path has
* no fresh quota signal anyway (§5.67b) — so this looks for a recognizable
* 5h-window utilization + reset in a few plausible spots and otherwise returns
* null, which the governor treats as fail-to-neutral. TODO(Drop 2): pin the
@@ -172,7 +172,7 @@ export default definePluginEntry({
// ANY failure (module moved, out-of-process host, fetch error) as "no signal"
// by returning null: the governor then fails-to-NEUTRAL (§5.67b — the throttle
// branch falls back to the maxFixSpawnsPerHour ceiling, the surplus-spend
- // branch disarms). The gateway has no fresh quota signal for the cc-bridge
+ // branch disarms). The gateway has no fresh quota signal for the tinker-bridge
// subscription path anyway, so null is an expected steady state, not an error.
const readUsage = async (): Promise => {
try {
diff --git a/extensions/tinkerclaw-fractal-reflection/src/fractal-result.ts b/extensions/tinkerclaw-fractal-reflection/src/fractal-result.ts
index a3a8d268e1af6..a2678595a0b35 100644
--- a/extensions/tinkerclaw-fractal-reflection/src/fractal-result.ts
+++ b/extensions/tinkerclaw-fractal-reflection/src/fractal-result.ts
@@ -16,7 +16,7 @@
*
* Emit path taken: the REAL core surface — `emitAgentEvent` from
* `src/infra/agent-events.ts` — the exact surface tinkerclaw-learned-intuition uses
- * for its `stream:"lifecycle"` amygdala-decision broadcasts and tinkerclaw-cc-bridge
+ * for its `stream:"lifecycle"` amygdala-decision broadcasts and tinkerclaw-tinker-bridge
* uses from `src/stream.ts`. There is no plugin-sdk emit wrapper; bundled fork
* extensions import core infra directly (established pattern), so NO no-op fallback
* was needed. `emitFractalEvent` is the SINGLE chokepoint — nothing else in this
diff --git a/extensions/tinkerclaw-fractal-reflection/src/governor.ts b/extensions/tinkerclaw-fractal-reflection/src/governor.ts
index dd1e956ec5622..70309e65d53bb 100644
--- a/extensions/tinkerclaw-fractal-reflection/src/governor.ts
+++ b/extensions/tinkerclaw-fractal-reflection/src/governor.ts
@@ -39,10 +39,10 @@ export interface GovernorDeps {
/** Injected clock (epoch ms). All timing flows through this — fake it in tests. */
now: () => number;
/**
- * Reads the live usage signal (cc-bridge `usage.status`). Return null (or throw)
+ * Reads the live usage signal (tinker-bridge `usage.status`). Return null (or throw)
* when the signal is unavailable (403/absent) — the governor fails to NEUTRAL,
* never to closed (§5.67b: the gateway has no fresh quota signal for the
- * cc-bridge subscription path).
+ * tinker-bridge subscription path).
*/
readUsage: () => Promise;
}
diff --git a/extensions/tinkerclaw-identity-persistence/index.ts b/extensions/tinkerclaw-identity-persistence/index.ts
index c0d25508eadca..6833f894382fc 100644
--- a/extensions/tinkerclaw-identity-persistence/index.ts
+++ b/extensions/tinkerclaw-identity-persistence/index.ts
@@ -94,6 +94,9 @@ function readPersonalityNudge(): string {
if (typeof raw.text === "string" && raw.text.length > 0) {
return raw.text;
}
+ if (Array.isArray(raw.adjustments) && raw.adjustments.length > 0) {
+ return (raw.adjustments as string[]).join("\n");
+ }
} catch {
// Malformed or unreadable -- ignore
}
diff --git a/extensions/tinkerclaw-learned-intuition/hook/amygdala-pretooluse.mjs b/extensions/tinkerclaw-learned-intuition/hook/amygdala-pretooluse.mjs
index 56aa6f92f415d..46279e48a52d6 100644
--- a/extensions/tinkerclaw-learned-intuition/hook/amygdala-pretooluse.mjs
+++ b/extensions/tinkerclaw-learned-intuition/hook/amygdala-pretooluse.mjs
@@ -3,7 +3,7 @@
* AMYGDALA v3.1 — PreToolUse hook (the pre-execution enforcement seam).
*
* Runs INSIDE claude-cli's hook sandbox (NOT the gateway process), so it must be
- * DEPENDENCY-FREE: only node builtins, no imports from the extension. cc-bridge
+ * DEPENDENCY-FREE: only node builtins, no imports from the extension. tinker-bridge
* passes `--settings ` to every spawn; that settings file
* registers this script as a PreToolUse hook. claude-cli honours a `deny`
* permission decision here EVEN under `--permission-mode bypassPermissions`, so
diff --git a/extensions/tinkerclaw-learned-intuition/index.ts b/extensions/tinkerclaw-learned-intuition/index.ts
index b31da8208be6c..e34ef4be91c30 100644
--- a/extensions/tinkerclaw-learned-intuition/index.ts
+++ b/extensions/tinkerclaw-learned-intuition/index.ts
@@ -38,9 +38,9 @@ const AMYGDALA_DECISIONS_PATH = join(DATA_DIR, "amygdala-decisions.jsonl");
// ingests them so REAL enforced denials (the strongest feedback signal) appear
// in the feed instead of being invisible.
const HOOK_DECISIONS_PATH = join(AMYGDALA_DATA_DIR, "hook-decisions.jsonl");
-// FORK 2026-06-07: register() runs ~5×/gateway boot; attach the cc-bridge prudence
+// FORK 2026-06-07: register() runs ~5×/gateway boot; attach the tinker-bridge prudence
// listener ONCE per process or every tool call gets evaluated (and recorded) N times.
-let ccBridgePrudenceListenerAttached = false;
+let tinkerBridgePrudenceListenerAttached = false;
// -- Helpers --
@@ -120,7 +120,7 @@ function loadAmygdalaConfig(modelsDir: string): AmygdalaConfig {
// AEGIS tool-gate (that stays observe-only via cfg.observeOnly).
// FORK 2026-06-04: bumped personality 0.15 → 0.5 for the narration-canary
// experiment — narration_discipline nudge is the visible tell that the
- // whole personality pipeline is live (Oscar's instrument). Prudence stays
+ // whole personality pipeline is live (the owner's instrument). Prudence stays
// 0.15 (observe-only safety gate).
alpha_prudence: 0.15,
alpha_personality: 0.5,
@@ -240,7 +240,7 @@ export default definePluginEntry({
// v3.1: compile the AEGIS rule snapshot + (when enforcement is on) the
// claude-cli settings file that wires the pre-execution PreToolUse hook into
- // every cc-bridge spawn. Done at register() so the artifacts exist before the
+ // every tinker-bridge spawn. Done at register() so the artifacts exist before the
// next worker spawns. Best-effort: a write failure must not break the gate.
try {
const snap = writePolicySnapshot(AMYGDALA_DATA_DIR, { hookEnforcement });
@@ -513,7 +513,7 @@ export default definePluginEntry({
// clause don't cohere ("build a chess game so I can water my plants"), surface
// an ASK signal in the Amygdala feed. Observe-only — it never blocks or alters
// the turn; it just shows the gut-feeling that something doesn't add up. Fires
- // for cc-bridge too (llm_input runs in attempt.ts, which wraps the provider).
+ // for tinker-bridge too (llm_input runs in attempt.ts, which wraps the provider).
api.on("llm_input", async (event: { prompt?: string; runId?: string; sessionId?: string }) => {
await ensureInit();
if (!hookReady || !event.prompt) return;
@@ -564,17 +564,17 @@ export default definePluginEntry({
}
});
- // FORK 2026-06-07 (Phase 1a): cc-bridge tools bypass the native before_tool_call
+ // FORK 2026-06-07 (Phase 1a): tinker-bridge tools bypass the native before_tool_call
// gate (Claude Code owns its tool loop), so the prudence nets never saw them. Here
- // we subscribe to cc-bridge tool-start events (marked `ccBridge`) and run the SAME
+ // we subscribe to tinker-bridge tool-start events (marked `tinkerBridge`) and run the SAME
// hook.evaluate the native gate uses — so the REAL ONNX prudence verdict appears in
// the feed for the way Jarvis actually runs. Observe-only: the tool already executed
// by the time we see the event, so we report (enforced:false), never abort.
- if (!ccBridgePrudenceListenerAttached) {
- ccBridgePrudenceListenerAttached = true;
+ if (!tinkerBridgePrudenceListenerAttached) {
+ tinkerBridgePrudenceListenerAttached = true;
onAgentEvent((evt) => {
const d = evt.data as Record | undefined;
- if (!d || evt.stream !== "tool" || d.phase !== "start" || d.ccBridge !== true) {
+ if (!d || evt.stream !== "tool" || d.phase !== "start" || d.tinkerBridge !== true) {
return;
}
void (async () => {
@@ -624,7 +624,7 @@ export default definePluginEntry({
target: String(target).slice(0, 200),
decision: result.decision,
blocked: result.blocked,
- enforced: false, // cc-bridge observe path: the tool already ran (real enforcement is the PreToolUse hook)
+ enforced: false, // tinker-bridge observe path: the tool already ran (real enforcement is the PreToolUse hook)
reason: result.response?.reason,
mode: legacyEnsemble ? (result.ruleBasedFallback ? "rules" : "onnx") : "novelty",
prudence:
@@ -655,7 +655,7 @@ export default definePluginEntry({
});
} catch (err) {
log.warn(
- `[learned-intuition] cc-bridge prudence eval failed: ${(err as Error).message}`,
+ `[learned-intuition] tinker-bridge prudence eval failed: ${(err as Error).message}`,
);
}
})();
@@ -682,8 +682,8 @@ export default definePluginEntry({
} catch {
/* ignore */
}
- // FORK 2026-06-07: merge the durable cc-bridge decision log (JSONL) with the
- // in-memory native ring, newest-first. cc-bridge tools bypass the native gate,
+ // FORK 2026-06-07: merge the durable tinker-bridge decision log (JSONL) with the
+ // in-memory native ring, newest-first. tinker-bridge tools bypass the native gate,
// so this file is the only durable source for Claude-Code runs; merging it here
// makes the feed survive a UI refresh and a gateway restart.
type FeedDecision = Record & { ts?: number | string };
@@ -713,7 +713,7 @@ export default definePluginEntry({
/* ignore */
}
// v3.1: ingest the pre-execution hook spool (REAL enforced denials from the
- // cc-bridge / claude-cli path). Map its rows into the feed's decision shape.
+ // tinker-bridge / claude-cli path). Map its rows into the feed's decision shape.
let hookRows: FeedDecision[] = [];
try {
if (existsSync(HOOK_DECISIONS_PATH)) {
diff --git a/extensions/tinkerclaw-learned-intuition/openclaw.plugin.json b/extensions/tinkerclaw-learned-intuition/openclaw.plugin.json
index a63f8d042f739..cc454824e0bc6 100644
--- a/extensions/tinkerclaw-learned-intuition/openclaw.plugin.json
+++ b/extensions/tinkerclaw-learned-intuition/openclaw.plugin.json
@@ -4,7 +4,7 @@
"onStartup": true
},
"name": "Learned Intuition",
- "description": "AMYGDALA v3.1 \u2014 safety gate: deterministic AEGIS rules enforced pre-execution (cc-bridge PreToolUse hook + native), a validated k-NN novelty 'ask' channel that habituates with experience, and a clause-cosine incongruity check. The legacy 5-net ONNX ensemble is retired from the decision path (re-enable with legacyEnsemble).",
+ "description": "AMYGDALA v3.1 \u2014 safety gate: deterministic AEGIS rules enforced pre-execution (tinker-bridge PreToolUse hook + native), a validated k-NN novelty 'ask' channel that habituates with experience, and a clause-cosine incongruity check. The legacy 5-net ONNX ensemble is retired from the decision path (re-enable with legacyEnsemble).",
"configSchema": {
"type": "object",
"additionalProperties": false,
@@ -39,7 +39,7 @@
"hookEnforcement": {
"type": "boolean",
"default": true,
- "description": "Write the claude-cli PreToolUse hook settings so destructive-execution AEGIS rules deny synchronously on the cc-bridge runner (works under bypassPermissions). False removes the settings file (observe-only spool)."
+ "description": "Write the claude-cli PreToolUse hook settings so destructive-execution AEGIS rules deny synchronously on the tinker-bridge runner (works under bypassPermissions). False removes the settings file (observe-only spool)."
}
}
}
diff --git a/extensions/tinkerclaw-learned-intuition/src/policy-snapshot.ts b/extensions/tinkerclaw-learned-intuition/src/policy-snapshot.ts
index 5d03c4c9b6460..922ba45ef0d12 100644
--- a/extensions/tinkerclaw-learned-intuition/src/policy-snapshot.ts
+++ b/extensions/tinkerclaw-learned-intuition/src/policy-snapshot.ts
@@ -7,7 +7,7 @@
* - `policy.json` — the serialized rules + the hookEnforcement flag,
* read by the dependency-free PreToolUse hook.
* - `cc-hook-settings.json` — a claude-cli settings file registering the hook,
- * passed by cc-bridge via `--settings` on every
+ * passed by tinker-bridge via `--settings` on every
* spawn. Its mere presence is the enable signal.
*
* The hook script is STAGED (copied) into the data dir so the settings can point
@@ -15,7 +15,7 @@
*
* When hook enforcement is OFF we still write policy.json (so an observe-only
* deployment keeps spooling) but DELETE cc-hook-settings.json, which makes
- * cc-bridge stop injecting `--settings` (no pre-execution deny).
+ * tinker-bridge stop injecting `--settings` (no pre-execution deny).
*/
import {
@@ -154,7 +154,7 @@ export function writePolicySnapshot(
return { staged: paths.stagedHookPath, settingsWritten: true };
}
-/** Remove the claude-cli settings file so cc-bridge stops injecting --settings. */
+/** Remove the claude-cli settings file so tinker-bridge stops injecting --settings. */
export function removeHookSettings(dataDir?: string): void {
const paths = policyPaths(dataDir);
try {
diff --git a/extensions/tinkerclaw-learned-intuition/src/rule-based-gate.ts b/extensions/tinkerclaw-learned-intuition/src/rule-based-gate.ts
index 17994ae22cbd9..1e16995014b44 100644
--- a/extensions/tinkerclaw-learned-intuition/src/rule-based-gate.ts
+++ b/extensions/tinkerclaw-learned-intuition/src/rule-based-gate.ts
@@ -5,7 +5,7 @@
* model. v3.1 makes the rule set a single serializable source of truth
* (`AEGIS_RULES`) consumed by BOTH:
* - the in-gateway gate (`evaluateRuleBased`, native `before_tool_call`), and
- * - the pre-execution PreToolUse hook on the cc-bridge / claude-cli path
+ * - the pre-execution PreToolUse hook on the tinker-bridge / claude-cli path
* (compiled into `policy.json` by `policy-snapshot.ts`, evaluated by
* `hook/amygdala-pretooluse.mjs`).
* Having one array means the two enforcement seams can never drift.
diff --git a/extensions/tinkerclaw-learned-intuition/src/types.ts b/extensions/tinkerclaw-learned-intuition/src/types.ts
index b6566b687abbd..108befdf30fa0 100644
--- a/extensions/tinkerclaw-learned-intuition/src/types.ts
+++ b/extensions/tinkerclaw-learned-intuition/src/types.ts
@@ -234,7 +234,7 @@ export interface AmygdalaConfig {
legacyEnsemble?: boolean;
/**
- * v3.1: write the pre-execution PreToolUse hook settings so cc-bridge /
+ * v3.1: write the pre-execution PreToolUse hook settings so tinker-bridge /
* claude-cli denies destructive-execution AEGIS rules synchronously. Default
* true. When false the settings file is removed (observe-only spool only).
*/
diff --git a/extensions/tinkerclaw-orca/README.md b/extensions/tinkerclaw-orca/README.md
index ab44943e9bdbb..c23a80ba2fedd 100644
--- a/extensions/tinkerclaw-orca/README.md
+++ b/extensions/tinkerclaw-orca/README.md
@@ -86,7 +86,7 @@ never make editing _impossible_.
the session is done editing, and a subagent may share the parent's
`session_id`; releasing on `SubagentStop` would free the parent's _live_
leases mid-flight. A subagent's own leases are reclaimed by TTL instead.
-- **Gateway / cc-bridge edits don't pass through this hook.** Jarvis edits files
+- **Gateway / tinker-bridge edits don't pass through this hook.** Jarvis edits files
with its own tools, not Claude Code's Edit/Write, so to participate it must call
the `orca.lease.*` RPCs directly (a future integration point). The hook today
protects Claude Code sessions, which is the primary clobber source.
diff --git a/extensions/tinkerclaw-orca/lease-core.test.mjs b/extensions/tinkerclaw-orca/lease-core.test.mjs
index bdb0a718d7d6b..82dc63ab92544 100644
--- a/extensions/tinkerclaw-orca/lease-core.test.mjs
+++ b/extensions/tinkerclaw-orca/lease-core.test.mjs
@@ -24,7 +24,7 @@ function freshRoot() {
const REPO = "/home/x/src/tinkerclaw";
const base = (over = {}) => ({
repo: REPO,
- path: "extensions/cc-bridge/src/worker.ts",
+ path: "extensions/tinker-bridge/src/worker.ts",
owner: "sessA",
pid: process.pid,
sessionId: "sessA",
diff --git a/extensions/tinkerclaw-prefrontal/__tests__/cc-skills-bridge.test.ts b/extensions/tinkerclaw-prefrontal/__tests__/cc-skills-bridge.test.ts
index d463b64b6e032..6028fa09fc1ed 100644
--- a/extensions/tinkerclaw-prefrontal/__tests__/cc-skills-bridge.test.ts
+++ b/extensions/tinkerclaw-prefrontal/__tests__/cc-skills-bridge.test.ts
@@ -90,11 +90,11 @@ describe("bridge output passes the real validators", () => {
expect(v.ok).toBe(true);
});
- it("round-trips through buildBridgedKitMd + the runner parser, tagged cc-bridge", () => {
+ it("round-trips through buildBridgedKitMd + the runner parser, tagged tinker-bridge", () => {
const spec = skillMdToRecipeSpec(GOOD_SKILL);
const md = buildBridgedKitMd(spec);
expect(md).toContain('schema: "kit/1.0"');
- expect(md).toContain('authoredBy: "cc-bridge"');
+ expect(md).toContain('authoredBy: "tinker-bridge"');
expect(md).not.toContain('authoredBy: "jarvis-on-the-fly"');
const parsed = parseKitStepsAndParallelism(md);
expect(parsed.steps.map((s) => s.title)).toEqual([
@@ -104,8 +104,8 @@ describe("bridge output passes the real validators", () => {
]);
});
- it("exports the cc-bridge authorship constant + scan dir name", () => {
- expect(BRIDGED_AUTHORED_BY).toBe("cc-bridge");
+ it("exports the tinker-bridge authorship constant + scan dir name", () => {
+ expect(BRIDGED_AUTHORED_BY).toBe("tinker-bridge");
expect(BRIDGED_SKILLS_DIRNAME).toBe("bridged-skills");
});
});
diff --git a/extensions/tinkerclaw-prefrontal/__tests__/effort-adaptation.test.ts b/extensions/tinkerclaw-prefrontal/__tests__/effort-adaptation.test.ts
index 340fddbe5b55a..58b087e575e14 100644
--- a/extensions/tinkerclaw-prefrontal/__tests__/effort-adaptation.test.ts
+++ b/extensions/tinkerclaw-prefrontal/__tests__/effort-adaptation.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
-import { classifyComplexity, buildEffortGuidance } from "../effort-router.js";
+import { classifyComplexity, buildEffortGuidance, resolveEffortBias } from "../effort-router.js";
describe("classifyComplexity — dynamic effort tiers", () => {
it("trivial: short conversational turns", () => {
@@ -62,3 +62,97 @@ describe("buildEffortGuidance", () => {
expect(g!.toLowerCase()).toContain("workflow");
});
});
+
+describe("quota-headroom effort bias", () => {
+ const PROMPT = "fix the login bug in src/auth/login.ts and add a regression test"; // baseline: standard
+
+ it("neutral leaves the classification unchanged", () => {
+ expect(classifyComplexity(PROMPT, "neutral").level).toBe("standard");
+ expect(classifyComplexity(PROMPT).level).toBe("standard");
+ });
+
+ it("aggressive bumps a non-trivial turn up one gear", () => {
+ expect(classifyComplexity(PROMPT, "aggressive").level).toBe("deep");
+ const deep = classifyComplexity(
+ "investigate and debug the failing auth flow, then refactor and optimize the session store module",
+ "aggressive",
+ );
+ expect(deep.level).toBe("ultra");
+ });
+
+ it("conservative pulls a turn down a gear but never below standard", () => {
+ const deep = classifyComplexity(
+ "investigate and debug the failing auth flow, then refactor and optimize the session store module",
+ );
+ expect(deep.level).toBe("deep");
+ expect(
+ classifyComplexity(
+ "investigate and debug the failing auth flow, then refactor and optimize the session store module",
+ "conservative",
+ ).level,
+ ).toBe("standard");
+ // standard floored at standard, not pushed to trivial
+ expect(classifyComplexity(PROMPT, "conservative").level).toBe("standard");
+ });
+
+ it("never escalates a trivial ack, even when aggressive", () => {
+ expect(classifyComplexity("thanks", "aggressive").level).toBe("trivial");
+ expect(buildEffortGuidance("thanks", "aggressive")).toBeNull();
+ });
+
+ it("buildEffortGuidance surfaces the active bias in the block", () => {
+ const g = buildEffortGuidance(PROMPT, "aggressive");
+ expect(g).toContain('bias="aggressive"');
+ expect(g).toContain('level="deep"');
+ expect(g!.toUpperCase()).toContain("AGGRESSIVE");
+ });
+
+ it("resolveEffortBias reads the env, defaulting neutral", () => {
+ const prev = process.env.PREFRONTAL_EFFORT_BIAS;
+ delete process.env.PREFRONTAL_EFFORT_BIAS;
+ expect(resolveEffortBias()).toBe("neutral");
+ process.env.PREFRONTAL_EFFORT_BIAS = "aggressive";
+ expect(resolveEffortBias()).toBe("aggressive");
+ process.env.PREFRONTAL_EFFORT_BIAS = "garbage";
+ expect(resolveEffortBias()).toBe("neutral");
+ if (prev === undefined) delete process.env.PREFRONTAL_EFFORT_BIAS;
+ else process.env.PREFRONTAL_EFFORT_BIAS = prev;
+ });
+});
+
+describe('"Branch" layer — compound-prompt fan-out', () => {
+ it("counts two independent asks in the owner's compound prompt", () => {
+ const rec = classifyComplexity(
+ "Update your model allocation code and dynamic workflows to spin multiple subagents when I ask for multiple things in the same prompt. Inform me of the name we should call it and its online presence.",
+ );
+ expect(rec.signals.independentAsks).toBeGreaterThanOrEqual(2);
+ });
+
+ it("does not count noun conjunctions or infinitives as extra asks", () => {
+ // One ask, even though it says "code and workflows ... to spin subagents".
+ const rec = classifyComplexity(
+ "Update the allocation code and the dynamic workflows to spin subagents.",
+ );
+ expect(rec.signals.independentAsks).toBe(1);
+ });
+
+ it("counts two real imperatives joined by 'and'", () => {
+ const rec = classifyComplexity("fix the login bug and write a regression test");
+ expect(rec.signals.independentAsks).toBe(2);
+ });
+
+ it("a single-ask prompt does not get a branch_decompose block", () => {
+ const g = buildEffortGuidance("refactor the session store module for clarity", "neutral");
+ expect(g).not.toBeNull();
+ expect(g).not.toContain("branch_decompose");
+ });
+
+ it("a multi-ask prompt injects branch_decompose fan-out guidance", () => {
+ const g = buildEffortGuidance(
+ "Update the allocation code to fan out subagents. Also publish a post explaining it.",
+ "neutral",
+ );
+ expect(g).toContain("branch_decompose");
+ expect(g).toContain("one subagent per ask");
+ });
+});
diff --git a/extensions/tinkerclaw-prefrontal/__tests__/effort-router.test.ts b/extensions/tinkerclaw-prefrontal/__tests__/effort-router.test.ts
index 584d200456d00..a1b6245602f46 100644
--- a/extensions/tinkerclaw-prefrontal/__tests__/effort-router.test.ts
+++ b/extensions/tinkerclaw-prefrontal/__tests__/effort-router.test.ts
@@ -1,9 +1,5 @@
import { describe, it, expect } from "vitest";
-import {
- classifyEffort,
- validateModelAssignment,
- DEFAULT_EFFORT_ROUTING_CONFIG,
-} from "../effort-router.js";
+import { classifyEffort } from "../effort-router.js";
describe("classifyEffort", () => {
it("classifies short simple messages as minimal", () => {
@@ -29,59 +25,3 @@ describe("classifyEffort", () => {
expect(classifyEffort(long)).toBe("standard");
});
});
-
-describe("validateModelAssignment", () => {
- const config = DEFAULT_EFFORT_ROUTING_CONFIG;
-
- it("approves Opus for maximum-effort tasks", () => {
- const result = validateModelAssignment(
- "anthropic/claude-opus-4-6",
- "architect the database layer",
- config,
- );
- expect(result.approved).toBe(true);
- });
-
- it("rejects Opus for minimal-effort tasks", () => {
- const result = validateModelAssignment("anthropic/claude-opus-4-6", "hello", config);
- expect(result.approved).toBe(false);
- expect(result.suggestedModel).toContain("haiku");
- });
-
- it("rejects Opus for standard-effort tasks", () => {
- const result = validateModelAssignment(
- "anthropic/claude-opus-4-6",
- "add a user profile endpoint",
- config,
- );
- expect(result.approved).toBe(false);
- expect(result.suggestedModel).toContain("sonnet");
- });
-
- it("approves Sonnet for standard tasks", () => {
- const result = validateModelAssignment(
- "anthropic/claude-sonnet-4-6",
- "add a user profile endpoint",
- config,
- );
- expect(result.approved).toBe(true);
- });
-
- it("approves Haiku for minimal tasks", () => {
- const result = validateModelAssignment("anthropic/claude-haiku-4-5", "hello", config);
- expect(result.approved).toBe(true);
- });
-
- it("approves everything when disabled", () => {
- const result = validateModelAssignment("anthropic/claude-opus-4-6", "hello", {
- ...config,
- enabled: false,
- });
- expect(result.approved).toBe(true);
- });
-
- it("provides reason when rejecting", () => {
- const result = validateModelAssignment("anthropic/claude-opus-4-6", "format this", config);
- expect(result.reason).toContain("wasteful");
- });
-});
diff --git a/extensions/tinkerclaw-prefrontal/__tests__/recipe-matcher-smart.test.ts b/extensions/tinkerclaw-prefrontal/__tests__/recipe-matcher-smart.test.ts
index 41ce9e23771c5..7ed149d4915d2 100644
--- a/extensions/tinkerclaw-prefrontal/__tests__/recipe-matcher-smart.test.ts
+++ b/extensions/tinkerclaw-prefrontal/__tests__/recipe-matcher-smart.test.ts
@@ -250,8 +250,8 @@ describe("loadRecipeIndex — extraDirs (bridged-skills) scan", () => {
ownDir = await fs.mkdtemp(path.join(os.tmpdir(), "kit-own-"));
extraDir = await fs.mkdtemp(path.join(os.tmpdir(), "kit-bridged-"));
const curated = `---\nslug: "shared"\ntitle: "Curated Shared"\nsummary: "the curated one"\ntags: ["shared"]\n---\n### 1. Curated\nbody\n`;
- const bridged = `---\nslug: "imported"\ntitle: "Imported"\nsummary: "from a cc skill"\ntags: ["imported"]\nauthoredBy: "cc-bridge"\n---\n### 1. Imported step\nbody\n`;
- const bridgedShadow = `---\nslug: "shared"\ntitle: "Bridged Shadow"\nsummary: "should NOT win"\ntags: ["shared"]\nauthoredBy: "cc-bridge"\n---\n### 1. Shadow\nbody\n`;
+ const bridged = `---\nslug: "imported"\ntitle: "Imported"\nsummary: "from a cc skill"\ntags: ["imported"]\nauthoredBy: "tinker-bridge"\n---\n### 1. Imported step\nbody\n`;
+ const bridgedShadow = `---\nslug: "shared"\ntitle: "Bridged Shadow"\nsummary: "should NOT win"\ntags: ["shared"]\nauthoredBy: "tinker-bridge"\n---\n### 1. Shadow\nbody\n`;
await fs.mkdir(path.join(ownDir, "shared"), { recursive: true });
await fs.writeFile(path.join(ownDir, "shared", "kit.md"), curated);
await fs.mkdir(path.join(extraDir, "imported"), { recursive: true });
diff --git a/extensions/tinkerclaw-prefrontal/__tests__/recipe-rpcs.test.ts b/extensions/tinkerclaw-prefrontal/__tests__/recipe-rpcs.test.ts
index c85520920746e..c2244559a7149 100644
--- a/extensions/tinkerclaw-prefrontal/__tests__/recipe-rpcs.test.ts
+++ b/extensions/tinkerclaw-prefrontal/__tests__/recipe-rpcs.test.ts
@@ -180,12 +180,12 @@ describe("U11 recipe.install — CC SKILL.md bridging", () => {
expect(res.bridged).toBe(true);
expect(res.slug).toBe("deploy-check");
// Written under ///recipe.md (canonical;
- // dual-read still loads a legacy kit.md), tagged cc-bridge.
+ // dual-read still loads a legacy kit.md), tagged tinker-bridge.
const written = await fs.readFile(
path.join(sandbox, BRIDGED_SKILLS_DIRNAME, "deploy-check", "recipe.md"),
"utf8",
);
- expect(written).toContain('authoredBy: "cc-bridge"');
+ expect(written).toContain('authoredBy: "tinker-bridge"');
expect(written).toContain('schema: "kit/1.0"');
expect(written).toContain("Read the release manifest");
});
diff --git a/extensions/tinkerclaw-prefrontal/__tests__/recipe-runner-directives.test.ts b/extensions/tinkerclaw-prefrontal/__tests__/recipe-runner-directives.test.ts
new file mode 100644
index 0000000000000..e9cf72d308158
--- /dev/null
+++ b/extensions/tinkerclaw-prefrontal/__tests__/recipe-runner-directives.test.ts
@@ -0,0 +1,14 @@
+import { describe, it, expect } from "vitest";
+import { parseModelDirective, parseThinkingDirective } from "../recipe-runner.js";
+
+describe("per-step model/thinking directives (bible §5.84-A)", () => {
+ it("parses leading model: and thinking: directives", () => {
+ const body = "model: claude-code/claude-haiku-4-5\nthinking: low\nDo the thing.";
+ expect(parseModelDirective(body)).toBe("claude-code/claude-haiku-4-5");
+ expect(parseThinkingDirective(body)).toBe("low");
+ });
+ it("returns undefined when absent", () => {
+ expect(parseModelDirective("just a task")).toBeUndefined();
+ expect(parseThinkingDirective("just a task")).toBeUndefined();
+ });
+});
diff --git a/extensions/tinkerclaw-prefrontal/cc-skills-bridge.ts b/extensions/tinkerclaw-prefrontal/cc-skills-bridge.ts
index 96babc5161443..a901aae3f598c 100644
--- a/extensions/tinkerclaw-prefrontal/cc-skills-bridge.ts
+++ b/extensions/tinkerclaw-prefrontal/cc-skills-bridge.ts
@@ -12,7 +12,7 @@
* validators.
*
* Trust posture (see improvement_notes U11 risk a): bridged recipes are marked
- * `authoredBy: "cc-bridge"` so they are distinguishable / low-trust vs the
+ * `authoredBy: "tinker-bridge"` so they are distinguishable / low-trust vs the
* curated `globalcaos/*` set. We retag the buildRecipeMd output rather than fork
* buildRecipeMd.
*
@@ -34,7 +34,7 @@ import { parse as parseYaml } from "yaml";
import { buildRecipeMd, type RecipeSpec, type RecipeStepSpec } from "./recipe-author.js";
/** Authorship tag stamped on every bridged recipe (low-trust / distinguishable). */
-export const BRIDGED_AUTHORED_BY = "cc-bridge";
+export const BRIDGED_AUTHORED_BY = "tinker-bridge";
/**
* Sub-directory (under the kit install sandbox) where bridged SKILL.md imports
@@ -222,7 +222,7 @@ export function skillMdToRecipeSpec(skillMdText: string): RecipeSpec {
/**
* Build the kit/1.0 markdown for a bridged spec, reusing `buildRecipeMd` and then
* retagging `authoredBy` from recipe-author's default ("jarvis-on-the-fly") to
- * `cc-bridge` so the provenance is honest. We retag the single frontmatter line
+ * `tinker-bridge` so the provenance is honest. We retag the single frontmatter line
* rather than fork buildRecipeMd.
*/
export function buildBridgedKitMd(spec: RecipeSpec): string {
diff --git a/extensions/tinkerclaw-prefrontal/effort-router.ts b/extensions/tinkerclaw-prefrontal/effort-router.ts
index 30c31e58f4c75..f78c4fab1f285 100644
--- a/extensions/tinkerclaw-prefrontal/effort-router.ts
+++ b/extensions/tinkerclaw-prefrontal/effort-router.ts
@@ -1,39 +1,23 @@
// extensions/prefrontal/effort-router.ts
-// FORK: Code-enforced effort routing + DYNAMIC reasoning-complexity adaptation.
+// FORK: DYNAMIC reasoning-complexity adaptation.
//
-// Two layers:
-// 1. validateModelAssignment — the legacy guard. Downgrades wasteful model
-// assignments (e.g. Opus for a one-word lookup). Unchanged contract.
-// 2. recommendEffort / buildEffortGuidance (FORK 2026-05-29) — scores the
-// user prompt and auto-adapts Jarvis's reasoning posture along FOUR tiers
-// (trivial → standard → deep → ultra). The recommendation is injected into
-// the turn as prependSystemContext so Jarvis scales thinking budget, model
-// tier, orchestration mode (solo / parallel subagents / full workflow) and
-// token generosity to the task — "up to ultracode, generous with tokens".
+// recommendEffort / buildEffortGuidance (FORK 2026-05-29) — scores the
+// user prompt and auto-adapts Jarvis's reasoning posture along FOUR tiers
+// (trivial → standard → deep → ultra). The recommendation is injected into
+// the turn as prependSystemContext so Jarvis scales thinking budget, model
+// tier, orchestration mode (solo / parallel subagents / full workflow) and
+// token generosity to the task — "up to ultracode, generous with tokens".
//
-// This is the "smart Jarvis" lever: the model used to ignore the router entirely
-// (the result was only logged). Now the classification drives the turn.
+// This is the "smart Jarvis" lever: the classification drives the turn.
+//
+// Retired 2026-06-14 (FOUNDATION #2, bible §5.84-B): the legacy
+// validateModelAssignment guard + hardcoded DEFAULT_EFFORT_ROUTING_CONFIG tier
+// list were log-only dead code that hardcoded a stale, drifting model roster —
+// deleted along with isModelInTier and the EffortRoutingConfig/RoutingDecision
+// types that only existed to serve them.
export type EffortLevel = "minimal" | "standard" | "maximum";
-export interface EffortRoutingConfig {
- enabled: boolean;
- minimal: string[];
- standard: string[];
- maximum: string[];
-}
-
-// FORK: Route through claude-code bridge (see DEFAULT_PREFRONTAL_CONFIG in
-// prefrontal-types.ts for the reasoning). Anthropic direct-API paths are
-// suspended on this fork — leaving them as defaults makes every effort-routed
-// dispatch 400 on first try.
-export const DEFAULT_EFFORT_ROUTING_CONFIG: EffortRoutingConfig = {
- enabled: true,
- minimal: ["claude-code/claude-haiku-4-5", "ollama/qwen3:14b"],
- standard: ["claude-code/claude-sonnet-4-6", "google/gemini-2.5-pro"],
- maximum: ["claude-code/claude-opus-4-7"],
-};
-
const MINIMAL_KEYWORDS = [
"format",
"lookup",
@@ -75,58 +59,37 @@ export function classifyEffort(taskDescription: string): EffortLevel {
return "standard";
}
-export function isModelInTier(
- model: string,
- tier: string[],
- _config: EffortRoutingConfig,
-): boolean {
- return tier.some((m) => model.includes(m) || m.includes(model));
-}
-
-export interface RoutingDecision {
- approved: boolean;
- suggestedModel?: string;
- reason?: string;
-}
-
-export function validateModelAssignment(
- assignedModel: string,
- taskDescription: string,
- config: EffortRoutingConfig,
-): RoutingDecision {
- if (!config.enabled) {
- return { approved: true };
- }
-
- const effort = classifyEffort(taskDescription);
-
- // Opus assigned to minimal task → downgrade
- if (effort === "minimal" && isModelInTier(assignedModel, config.maximum, config)) {
- return {
- approved: false,
- suggestedModel: config.minimal[0],
- reason: `Task classified as minimal effort ("${taskDescription.slice(0, 60)}") — Opus is wasteful, use ${config.minimal[0]}`,
- };
- }
-
- // Opus assigned to standard task → downgrade
- if (effort === "standard" && isModelInTier(assignedModel, config.maximum, config)) {
- return {
- approved: false,
- suggestedModel: config.standard[0],
- reason: `Task classified as standard effort — use ${config.standard[0]} instead of Opus`,
- };
- }
-
- return { approved: true };
-}
-
// ─── Dynamic complexity adaptation (FORK 2026-05-29) ────────────────────────
export type ComplexityLevel = "trivial" | "standard" | "deep" | "ultra";
export type OrchestrationMode = "solo" | "parallel" | "workflow";
+// FORK 2026-06-22: quota-headroom bias. When the weekly token quota is mostly
+// unspent (e.g. Monday morning, ~100% left) the owner wants the auto-allocator to
+// "go aggressive in choosing both model and effort on the fly" — so a high
+// headroom bumps every non-trivial turn up one gear, and a tight quota pulls it
+// down one. Pure acks/greetings (trivial) are never escalated: spending opus on
+// "thanks" is waste, not aggression. The bias is an input, not a guess — it is
+// fed from real headroom via PREFRONTAL_EFFORT_BIAS (default neutral).
+export type EffortBias = "conservative" | "neutral" | "aggressive";
+
+const TIER_ORDER: ComplexityLevel[] = ["trivial", "standard", "deep", "ultra"];
+
+function applyEffortBias(level: ComplexityLevel, bias: EffortBias): ComplexityLevel {
+ if (bias === "neutral" || level === "trivial") return level;
+ const i = TIER_ORDER.indexOf(level);
+ if (bias === "aggressive") return TIER_ORDER[Math.min(TIER_ORDER.length - 1, i + 1)];
+ // conservative: pull down a gear but never below standard for real work.
+ return TIER_ORDER[Math.max(1, i - 1)];
+}
+
+/** Read the active headroom bias from env. Default neutral (behavior unchanged). */
+export function resolveEffortBias(): EffortBias {
+ const raw = (process.env.PREFRONTAL_EFFORT_BIAS ?? "").trim().toLowerCase();
+ return raw === "aggressive" || raw === "conservative" ? raw : "neutral";
+}
+
export interface ComplexitySignals {
words: number;
clauses: number;
@@ -136,6 +99,12 @@ export interface ComplexitySignals {
ultraHits: string[];
trivialHit: boolean;
questionsAsked: number;
+ /**
+ * FORK 2026-06-25 ("Branch" layer, frontopolar / BA10 cognitive-branching):
+ * how many DISTINCT independent asks the one prompt bundles. ≥2 ⇒ the turn
+ * should split into one subagent per ask instead of running them serially.
+ */
+ independentAsks: number;
}
export interface EffortRecommendation {
@@ -226,7 +195,96 @@ function countClauses(s: string): number {
return seps + bullets + lines;
}
-export function classifyComplexity(prompt: string): EffortRecommendation {
+// Imperative action verbs that head a request. Used to tell apart a real
+// second ask ("…and write the tests") from a noun conjunction ("code and
+// workflows") or an infinitive ("to spin subagents"), which must NOT count.
+const ACTION_VERBS = new Set([
+ "add",
+ "update",
+ "fix",
+ "create",
+ "write",
+ "build",
+ "rebuild",
+ "refactor",
+ "remove",
+ "delete",
+ "drop",
+ "rename",
+ "implement",
+ "make",
+ "change",
+ "modify",
+ "move",
+ "generate",
+ "draft",
+ "send",
+ "name",
+ "tell",
+ "inform",
+ "explain",
+ "describe",
+ "review",
+ "test",
+ "document",
+ "investigate",
+ "research",
+ "audit",
+ "analyze",
+ "analyse",
+ "compare",
+ "plan",
+ "design",
+ "optimize",
+ "optimise",
+ "summarize",
+ "summarise",
+ "list",
+ "find",
+ "search",
+ "check",
+ "verify",
+ "publish",
+ "post",
+ "deploy",
+ "wire",
+ "hook",
+ "install",
+ "setup",
+ "configure",
+ "translate",
+ "render",
+ "show",
+ "give",
+ "propose",
+ "suggest",
+ "let",
+]);
+
+/**
+ * Count the DISTINCT independent asks bundled into one prompt — the signal the
+ * "Branch" layer keys on to fan out one subagent per ask. A segment counts only
+ * when an imperative action verb LEADS it (first 3 tokens), so noun
+ * conjunctions and infinitives don't inflate the count. Conservative by design:
+ * undercounting just means inline handling; overcounting would spam subagents.
+ */
+function countIndependentAsks(prompt: string): number {
+ const segments = prompt
+ .split(/(?:[.!?\n]+|^\s*[-*\d]+[.)]\s+|;|\bthen\b|\balso\b| and )/gi)
+ .map((s) => s.trim())
+ .filter((s) => s.length > 0);
+ let asks = 0;
+ for (const seg of segments) {
+ const head = (seg.toLowerCase().match(/[a-z']+/g) ?? []).slice(0, 3);
+ if (head.some((w) => ACTION_VERBS.has(w))) asks += 1;
+ }
+ return asks;
+}
+
+export function classifyComplexity(
+ prompt: string,
+ bias: EffortBias = "neutral",
+): EffortRecommendation {
const lower = prompt.toLowerCase();
const words = (prompt.trim().match(/\S+/g) ?? []).length;
const clauses = countClauses(prompt);
@@ -246,6 +304,7 @@ export function classifyComplexity(prompt: string): EffortRecommendation {
ultraHits.length === 0 &&
TRIVIAL_KEYWORDS.some((kw) => (kw.includes(" ") ? lower.includes(kw) : promptWords.has(kw)));
const questionsAsked = (prompt.match(/\?/g) ?? []).length;
+ const independentAsks = countIndependentAsks(prompt);
const signals: ComplexitySignals = {
words,
@@ -256,6 +315,7 @@ export function classifyComplexity(prompt: string): EffortRecommendation {
ultraHits,
trivialHit,
questionsAsked,
+ independentAsks,
};
// Score. Each signal contributes; tuned so a one-liner stays trivial/standard
@@ -282,6 +342,10 @@ export function classifyComplexity(prompt: string): EffortRecommendation {
// Floor: 3+ distinct hard verbs is deep work even if other signals are light.
if (level === "standard" && deepHits.length >= 3) level = "deep";
+ // Quota-headroom bias: shift the whole turn up/down a gear when the weekly
+ // token budget is flush/tight. Applied last so it composes on the final tier.
+ level = applyEffortBias(level, bias);
+
const byLevel: Record<
ComplexityLevel,
Omit
@@ -321,12 +385,21 @@ export function classifyComplexity(prompt: string): EffortRecommendation {
* this turn. Returns null for trivial turns (no guidance needed — keep them
* cheap and fast). Used by the before_prompt_build hook.
*/
-export function buildEffortGuidance(prompt: string): string | null {
- const rec = classifyComplexity(prompt);
+export function buildEffortGuidance(
+ prompt: string,
+ bias: EffortBias = resolveEffortBias(),
+): string | null {
+ const rec = classifyComplexity(prompt, bias);
if (rec.level === "trivial") return null;
+ const biasNote =
+ bias === "aggressive"
+ ? ` (quota-headroom bias: AGGRESSIVE — weekly budget is flush, so this turn is bumped up a gear; lean into the stronger model / more thinking on the fly)`
+ : bias === "conservative"
+ ? ` (quota-headroom bias: CONSERVATIVE — weekly budget is tight, pulled down a gear)`
+ : "";
const lines = [
- ``,
- `This turn was auto-classified **${rec.level}**. Adapt your reasoning accordingly:`,
+ ``,
+ `This turn was auto-classified **${rec.level}**${biasNote}. Adapt your reasoning accordingly:`,
`- Reasoning: ${rec.thinkingHint}.`,
`- Orchestration: ${rec.orchestration === "solo" ? "handle inline" : rec.orchestration === "parallel" ? "fan out independent work to parallel subagents" : "orchestrate a multi-phase workflow with verification"}.`,
`- Tokens: ${rec.tokenGuidance}.`,
@@ -335,5 +408,20 @@ export function buildEffortGuidance(prompt: string): string | null {
: `- Match depth to the task; don't over- or under-invest.`,
` `,
];
+ // "Branch" layer (frontopolar / BA10 cognitive-branching): when one prompt
+ // bundles ≥2 independent asks, override the solo default and fan out one
+ // subagent per ask instead of doing them serially in this turn.
+ const asks = rec.signals.independentAsks;
+ if (asks >= 2) {
+ lines.push(
+ ``,
+ `This single prompt bundles **${asks} independent asks**. Do NOT handle them serially in one turn.`,
+ `- Split into one subagent per ask and run them concurrently. Independent multi-file EDITS → ORCA (parallel-implement workflow); research / multi-domain / mixed work → an \`openclaw-orchestrate\` dynamic workflow (parallel/pipeline).`,
+ `- Pick each unit's model by its OWN weight (haiku breadth → sonnet middle → opus hard); a light ask shouldn't ride the heaviest model just because a sibling ask is hard.`,
+ `- Synthesize the unit results into ONE coherent answer; verify on the merged result before claiming done.`,
+ `- If the asks share state or must run in order, say so and handle inline instead — don't force a fan-out that doesn't parallelize.`,
+ ` `,
+ );
+ }
return lines.join("\n");
}
diff --git a/extensions/tinkerclaw-prefrontal/index.ts b/extensions/tinkerclaw-prefrontal/index.ts
index 773264b242f2a..708a3bb4c0422 100644
--- a/extensions/tinkerclaw-prefrontal/index.ts
+++ b/extensions/tinkerclaw-prefrontal/index.ts
@@ -67,11 +67,7 @@ import { BRIDGED_SKILLS_DIRNAME } from "./cc-skills-bridge.js";
import { ChatEmitter } from "./chat-emitter.js";
import { DEFAULT_CORF_CONFIG } from "./corf-trigger.js";
import { createDenialTracker } from "./denial-tracking.js";
-import {
- validateModelAssignment,
- DEFAULT_EFFORT_ROUTING_CONFIG,
- buildEffortGuidance,
-} from "./effort-router.js";
+import { buildEffortGuidance } from "./effort-router.js";
import { createExplorationGate, DEFAULT_EXPLORATION_GATE_CONFIG } from "./exploration-gate.js";
import { createFaarTracker, classifyTask } from "./faar-tracker.js";
import { resolveFeatureFlags, isEnabled } from "./feature-flags.js";
@@ -139,13 +135,6 @@ export default function register(api: OpenClawPluginApi) {
};
antiGoldplatingConfig.enabled = isEnabled(featureFlags, "antiGoldplating");
- // ─── WS3: Effort Routing ───
- const effortRoutingConfig = {
- ...DEFAULT_EFFORT_ROUTING_CONFIG,
- enabled: isEnabled(featureFlags, "effortRouting"),
- ...pluginConfig.effortRouting,
- };
-
// ─── WS4: CORF Trigger ───
const _corfConfig = {
...DEFAULT_CORF_CONFIG,
@@ -278,19 +267,6 @@ export default function register(api: OpenClawPluginApi) {
subagentRuns.set(event.runId, run);
lastEventTimestamps.set(event.runId, Date.now());
- // WS3: Validate model assignment against task complexity
- if (isEnabled(featureFlags, "effortRouting") && event.label) {
- const routing = validateModelAssignment(
- // oxlint-disable-next-line typescript-eslint/no-explicit-any
- (event as any).model ?? "",
- event.label,
- effortRoutingConfig,
- );
- if (!routing.approved) {
- log.warn?.(`[prefrontal] Effort routing: ${routing.reason}`);
- }
- }
-
// If the spawning session is main, mark it as prefrontal session
if (!getPrefrontalSessionKey() && ctx.requesterSessionKey?.includes("main")) {
setPrefrontalSessionKey(ctx.requesterSessionKey);
@@ -343,7 +319,7 @@ export default function register(api: OpenClawPluginApi) {
sessionKey,
provider: event.provider,
model: event.model,
- runId: event.runId,
+ runId: event.runId ?? "",
trigger: ctx.trigger,
});
// Tell the monitor about the active main session so it always shows a root node
diff --git a/extensions/tinkerclaw-prefrontal/orchestration-deps.ts b/extensions/tinkerclaw-prefrontal/orchestration-deps.ts
index 48f96423fefe4..d37ae03bc309e 100644
--- a/extensions/tinkerclaw-prefrontal/orchestration-deps.ts
+++ b/extensions/tinkerclaw-prefrontal/orchestration-deps.ts
@@ -7,7 +7,7 @@
* `spawn` by MIRRORING the proven `spawnText` sequence in
* `src/fork/reasoning-runtime.ts` (the same fork.subagents.spawn → agent.wait →
* chat.history path the round-table + overseer already use — no new transport,
- * shares the cc-bridge billing harness + fan-out budget).
+ * shares the tinker-bridge billing harness + fan-out budget).
*
* Live-verify (2026-06-04): the no-spawn orchestrate path was confirmed live; the
* agent()-spawn path initially failed with `missing scope: operator.admin` — the
@@ -42,7 +42,7 @@ const ADMIN_SCOPES = ["operator.admin"];
/**
* Default leaf model for orchestration fan-out (used when a unit omits {model}).
* MUST be a `claude-code/*` model so every spawned unit is a subscription-billed
- * cc-sp-* worker — the entire reason workflows route through the gateway instead of
+ * tinker-sp-* worker — the entire reason workflows route through the gateway instead of
* native forked `claude` processes (which trip Anthropic's overage classifier and
* bill metered). Default = sonnet: a sensible, cheaper middle. Dynamic workflows are
* a STANDING capability (not a max-effort tier), so the default should not be the
@@ -136,6 +136,7 @@ export async function spawnTextVia(
parentSessionKey: string,
runTimeoutSeconds: number,
model?: string,
+ thinking?: string,
): Promise {
// Pin a claude-code/* model on EVERY spawn here (the billing guard is centralized
// so no caller can bypass it). Omitted/non-claude-code → DEFAULT_LEAF_MODEL.
@@ -151,6 +152,7 @@ export async function spawnTextVia(
task,
label,
model: leafModel,
+ thinking,
parentSessionKey,
runTimeoutSeconds,
expectsCompletionMessage: false,
@@ -214,8 +216,9 @@ export function createProductionOrchestrationRuntime(opts: ProductionRuntimeOpts
const spawn = async (prompt: string, agentOpts?: AgentOpts): Promise<{ finalText: string }> => {
const label = agentOpts?.label ?? "orchestration-agent";
// Per-unit model override (agent({model})) is coerced; otherwise the (already
- // coerced) default leaf model. Either way the spawn is claude-code/* = cc-sp-*.
+ // coerced) default leaf model. Either way the spawn is claude-code/* = tinker-sp-*.
const model = agentOpts?.model ? coerceClaudeCodeModel(agentOpts.model) : defaultLeafModel;
+ const thinking = agentOpts?.thinking; // per-unit effort override, or undefined to inherit
const finalText = await spawnTextVia(
opts.callGateway,
prompt,
@@ -223,6 +226,7 @@ export function createProductionOrchestrationRuntime(opts: ProductionRuntimeOpts
parentSessionKey,
runTimeoutSeconds,
model,
+ thinking,
);
return { finalText };
};
diff --git a/extensions/tinkerclaw-prefrontal/orchestration-runtime.ts b/extensions/tinkerclaw-prefrontal/orchestration-runtime.ts
index c210d6bdf9a42..1c4c8c69b053c 100644
--- a/extensions/tinkerclaw-prefrontal/orchestration-runtime.ts
+++ b/extensions/tinkerclaw-prefrontal/orchestration-runtime.ts
@@ -2,7 +2,7 @@
* SS0 / capability-parity A1 (2026-06-04): the NATIVE orchestration runtime.
*
* `agent / parallel / pipeline / phase` are currently BORROWED from the Claude
- * Code Workflow tool via cc-bridge — alive only while Claude Code drives the turn
+ * Code Workflow tool via tinker-bridge — alive only while Claude Code drives the turn
* and metered/at-risk from 2026-06-15. This module makes them native to Jarvis's
* own gateway, built over the existing subagent-spawn substrate (a `spawn` dep,
* defaulted in production to fork.subagents.spawn + agent.wait + chat.history,
@@ -34,11 +34,17 @@ export interface AgentOpts {
label?: string;
/**
* Optional leaf model for this unit. The production runtime COERCES this to a
- * `claude-code/*` model (subscription-billed cc-sp-* worker) — a non-claude-code
+ * `claude-code/*` model (subscription-billed tinker-sp-* worker) — a non-claude-code
* value is overridden, never honoured, so a fan-out can't silently spill onto
* the metered API. Omit to inherit the runtime's default leaf model.
*/
model?: string;
+ /**
+ * Optional thinking/effort level for this unit (e.g. "low", "medium", "high",
+ * "max"). Forwarded to fork.subagents.spawn → child session thinkingLevel.
+ * Omit to inherit the runtime default (none = off). Bible §5.84-A.
+ */
+ thinking?: string;
}
export interface OrchestrationDeps {
diff --git a/extensions/tinkerclaw-prefrontal/recipe-rpcs.ts b/extensions/tinkerclaw-prefrontal/recipe-rpcs.ts
index 648151fbc75e0..c6a311cbe99d3 100644
--- a/extensions/tinkerclaw-prefrontal/recipe-rpcs.ts
+++ b/extensions/tinkerclaw-prefrontal/recipe-rpcs.ts
@@ -140,10 +140,14 @@ export function stampRecipeAttribution(
interface RecipeFrontmatter {
schema?: string;
slug?: string;
+ /** recipe/1.0 playbooks identify themselves with `id` instead of `slug`. */
+ id?: string;
title?: string;
summary?: string;
tags?: unknown[];
category?: string;
+ /** Optional sub-category label; groups recipes within a category in the tab. */
+ subdivision?: string;
[key: string]: unknown;
}
@@ -187,40 +191,59 @@ export interface KitParsed {
summary: string;
tags: string[];
category: string;
+ /** True when frontmatter carried an EXPLICIT `category` (vs. inferred/default). */
+ categoryExplicit: boolean;
+ /** Optional sub-category label from frontmatter (folder-derived one added by the walker). */
+ subdivision?: string;
+ /** False when the file had no `---` YAML frontmatter block — i.e. it is a doc, not a recipe. */
+ hasFrontmatter: boolean;
}
/**
* Parse a kit.md file and return the normalized frontmatter fields.
* Falls back gracefully: missing fields get sensible defaults derived from path.
+ *
+ * `slugHint` lets the caller pass a filename-derived slug for recipes that live as
+ * a bare `.md` inside a category folder — there, `basename(dirname)` would
+ * wrongly collapse every file to the folder name. Slug priority:
+ * frontmatter.slug > frontmatter.id > slugHint > basename(dirname).
*/
-export async function parseKitMd(filePath: string): Promise {
- const slugFromPath = path.basename(path.dirname(filePath));
+export async function parseKitMd(filePath: string, slugHint?: string): Promise {
+ const slugFromPath = slugHint || path.basename(path.dirname(filePath));
let slug = slugFromPath;
let title = slugFromPath;
let summary = "";
let tags: string[] = [];
let category = "operations";
+ let categoryExplicit = false;
+ let subdivision: string | undefined;
+ let hasFrontmatter = false;
try {
const text = await fs.readFile(filePath, "utf-8");
const fm = /^---\n([\s\S]+?)\n---/.exec(text);
if (fm) {
+ hasFrontmatter = true;
const parsed = parseYaml(fm[1]) as RecipeFrontmatter | null;
if (parsed && typeof parsed === "object") {
if (typeof parsed.slug === "string" && parsed.slug) slug = parsed.slug;
+ else if (typeof parsed.id === "string" && parsed.id) slug = parsed.id;
if (typeof parsed.title === "string" && parsed.title) title = parsed.title;
else title = slug;
if (typeof parsed.summary === "string") summary = parsed.summary;
if (Array.isArray(parsed.tags))
tags = (parsed.tags as unknown[]).filter((t) => typeof t === "string") as string[];
+ categoryExplicit = typeof parsed.category === "string" && !!parsed.category;
category = inferCategory(parsed);
+ if (typeof parsed.subdivision === "string" && parsed.subdivision)
+ subdivision = parsed.subdivision;
}
}
} catch {
// frontmatter parse failure — return slug/empty defaults
}
- return { slug, title, summary, tags, category };
+ return { slug, title, summary, tags, category, categoryExplicit, subdivision, hasFrontmatter };
}
// ─── Frontmatter scalar read/write (U12 version/owner) ───────────────────────
@@ -265,56 +288,109 @@ interface OwnKitEntry {
summary: string;
tags: string[];
category: string;
+ subdivision?: string;
path: string;
source: "ours";
}
+/** Strip a recipe filename to its slug hint: `if-then-else.recipe.md` → `if-then-else`. */
+function recipeFileSlug(fileName: string): string {
+ return fileName.replace(/\.recipe\.md$/i, "").replace(/\.md$/i, "");
+}
+
/**
- * Walk `ownRecipesDir//{recipe.md,kit.md}` and return parsed entries.
- * Layout: `/recipe.md` (new canonical) or `/kit.md` (legacy) — one
- * level deep, slug is the immediate child dir. DUAL-READ: recipe.md is probed
- * first per slug-dir, kit.md is the legacy fallback.
+ * Recursively walk `ownRecipesDir` and return every recipe as a tab-visible entry.
+ *
+ * Two coexisting layouts are both surfaced (the fix for the "invisible in the
+ * recipes tab" class of bugs, 2026-07-08):
+ * 1. **Self-contained kit dir** — `/{recipe.md,kit.md}`. slug = frontmatter
+ * or the dir name; category = frontmatter/inferred.
+ * 2. **Category folder** — a directory whose NAME is the category (e.g. `coding/`,
+ * `writing/`), holding bare `.md` playbooks and/or nested SUBDIVISION
+ * folders (`writing/papers/*.md`). Every `.md` at any depth becomes visible;
+ * slug = frontmatter id/slug or the filename; category = the TOP folder name;
+ * subdivision = the nested folder path beneath it (frontmatter `subdivision`
+ * overrides). This is the owner's "subfolders as subdivisions of the same
+ * category" contract.
+ * A bare `.md` sitting at the recipes root is also surfaced (category from
+ * frontmatter). DUAL-READ within a kit dir: recipe.md first, kit.md legacy.
*/
-async function listOwnKits(ownRecipesDir: string): Promise {
+export async function listOwnKits(ownRecipesDir: string): Promise {
const out: OwnKitEntry[] = [];
- let slugDirs: string[];
- try {
- slugDirs = await fs.readdir(ownRecipesDir);
- } catch {
- return out;
- }
- await Promise.all(
- slugDirs.map(async (dirName) => {
- let kitMdPath = "";
- for (const fname of ["recipe.md", "kit.md"]) {
- const candidate = path.join(ownRecipesDir, dirName, fname);
- try {
- await fs.access(candidate);
- kitMdPath = candidate;
- break;
- } catch {
- // try next filename
+
+ const push = (parsed: KitParsed, filePath: string, category: string, subdivision?: string) => {
+ out.push({
+ owner: "globalcaos",
+ slug: parsed.slug,
+ title: parsed.title,
+ summary: parsed.summary,
+ tags: parsed.tags,
+ category,
+ subdivision: parsed.subdivision ?? subdivision,
+ path: filePath,
+ source: "ours",
+ });
+ };
+
+ // Walk one directory. `category` is null at the recipes root and becomes the
+ // top-level folder name once we descend into one; `subParts` accumulates nested
+ // folder names below the category folder (the subdivision path).
+ const walk = async (dir: string, category: string | null, subParts: string[]): Promise => {
+ let entries: import("node:fs").Dirent[];
+ try {
+ entries = await fs.readdir(dir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+ await Promise.all(
+ entries.map(async (ent) => {
+ const full = path.join(dir, ent.name);
+ if (ent.isDirectory()) {
+ // A self-contained kit dir (has recipe.md / kit.md) is a single recipe.
+ let kitMd = "";
+ for (const fname of ["recipe.md", "kit.md"]) {
+ try {
+ await fs.access(path.join(full, fname));
+ kitMd = path.join(full, fname);
+ break;
+ } catch {
+ // try next
+ }
+ }
+ if (kitMd) {
+ const parsed = await parseKitMd(kitMd);
+ // Inside a category folder the FOLDER wins; at root the frontmatter does.
+ const cat = category ?? parsed.category;
+ const sub = category ? subParts.join("/") || undefined : undefined;
+ push(parsed, kitMd, cat, sub);
+ return;
+ }
+ // Otherwise it is a category folder (at root) or a subdivision folder.
+ const nextCategory = category ?? ent.name;
+ const nextSub = category ? [...subParts, ent.name] : [];
+ await walk(full, nextCategory, nextSub);
+ return;
}
- }
- if (!kitMdPath) {
- return; // not a recipe directory
- }
- const parsed = await parseKitMd(kitMdPath);
- out.push({
- owner: "globalcaos",
- slug: parsed.slug || dirName,
- title: parsed.title,
- summary: parsed.summary,
- tags: parsed.tags,
- category: parsed.category,
- path: kitMdPath,
- source: "ours",
- });
- }),
- );
- // Sort for deterministic ordering
- out.sort((a, b) => a.slug.localeCompare(b.slug));
- return out;
+ // A bare recipe file.
+ if (!ent.name.endsWith(".md") || ent.name === "CATALOG.md") return;
+ const parsed = await parseKitMd(full, recipeFileSlug(ent.name));
+ // Docs (AUTHORING.md, README.md, …) have no YAML frontmatter — not recipes.
+ if (!parsed.hasFrontmatter) return;
+ // Category folder wins over inferred category; frontmatter still wins at root
+ // and when it explicitly declares a category.
+ const cat = parsed.categoryExplicit ? parsed.category : (category ?? parsed.category);
+ const sub = category ? subParts.join("/") || undefined : undefined;
+ push(parsed, full, cat, sub);
+ }),
+ );
+ };
+
+ await walk(ownRecipesDir, null, []);
+ // De-dupe by slug (first-writer-wins) and sort for deterministic ordering.
+ const seen = new Set();
+ const deduped = out.filter((e) => (seen.has(e.slug) ? false : (seen.add(e.slug), true)));
+ deduped.sort((a, b) => a.slug.localeCompare(b.slug));
+ return deduped;
}
// ─── Shared kit-write (guarded) + recipe-rewrite spawn ──────────────────────
@@ -876,6 +952,7 @@ export function createRecipeRpcs(deps: KitRpcsDeps) {
summary: e.summary,
tags: e.tags,
category: e.category,
+ subdivision: e.subdivision,
source: "ours" as const,
path: e.path,
}));
diff --git a/extensions/tinkerclaw-prefrontal/recipe-runner.ts b/extensions/tinkerclaw-prefrontal/recipe-runner.ts
index 2e02ae0470fe9..349b88f837338 100644
--- a/extensions/tinkerclaw-prefrontal/recipe-runner.ts
+++ b/extensions/tinkerclaw-prefrontal/recipe-runner.ts
@@ -86,7 +86,7 @@ export interface RecipeRunOptions {
* When true, an existing in_progress plan for this sessionKey with the SAME
* kitRef is resumed: dispatch starts at plan.currentStep, already-`done` rows
* are skipped, and prior steps' artifacts are injected into later steps' tasks.
- * Default policy (Oscar, 2026-05-30): NO silent re-attach — auto-resume fires
+ * Default policy (the owner, 2026-05-30): NO silent re-attach — auto-resume fires
* only when resume:true is passed. A bare run always force-restarts at step 0.
*/
resume?: boolean;
@@ -387,6 +387,10 @@ interface StepDispatch {
/** SS5b: a `max-tool-calls:` per-spawn tool-call bound (literal or {{template}}; a
* non-numeric / unresolved value is simply omitted — no fabricated default). */
maxToolCalls?: number;
+ /** §5.84-A: a `model:` per-step model override (raw id or {{template}}). */
+ model?: string;
+ /** §5.84-A: a `thinking:` per-step effort level (raw level or {{template}}). */
+ thinkingLevel?: string;
}
/** The CONSECUTIVE leading directive lines of a step body — only lines that are
@@ -403,7 +407,7 @@ function leadingDirectives(body: string): string[] {
continue; // skip leading blank lines
}
if (
- !/^(?:uses|loop|when|return|done|map|filter|keep|onError|allow-tools|max-tokens|max-tool-calls):|^invoke\s+skill:/i.test(
+ !/^(?:uses|loop|when|return|done|map|filter|keep|onError|allow-tools|max-tokens|max-tool-calls|model|thinking):|^invoke\s+skill:/i.test(
line,
)
)
@@ -509,7 +513,7 @@ export function parseOnErrorDirective(body: string): OnErrorPolicy | undefined {
if (!m) continue;
const spec = m[1].trim();
const retry = /^retry\s+(\S+)$/i.exec(spec);
- if (retry) return { mode: "retry", n: retry[1] };
+ if (retry) return { mode: "retry", retryCount: retry[1] };
const fallback = /^fallback\s+kit:\s*(\S+)$/i.exec(spec);
if (fallback) return { mode: "fallback", kitRef: fallback[1] };
if (/^continue-partial$/i.test(spec)) return { mode: "continue-partial" };
@@ -557,6 +561,23 @@ export function parseMaxToolCallsDirective(body: string): string | undefined {
return undefined;
}
+/** §5.84-A: a leading `model: ` directive (raw string). */
+export function parseModelDirective(body: string): string | undefined {
+ for (const line of leadingDirectives(body)) {
+ const m = /^model:\s*(.+\S)\s*$/i.exec(line);
+ if (m) return m[1].trim();
+ }
+ return undefined;
+}
+/** §5.84-A: a leading `thinking: ` directive (raw string). */
+export function parseThinkingDirective(body: string): string | undefined {
+ for (const line of leadingDirectives(body)) {
+ const m = /^thinking:\s*(.+\S)\s*$/i.exec(line);
+ if (m) return m[1].trim();
+ }
+ return undefined;
+}
+
/** SS2b: resolve a dynamic `uses: {{steps.N.out.path}}` template against prior
* steps' typed outputs (mirrors resolveStepRefs), then normalize+validate it as a
* kitRef. Returns the canonical owner/slug, or null when unresolvable/malformed. */
@@ -1184,6 +1205,10 @@ export interface SpawnOpts {
maxTokens?: number;
/** `--max-tool-calls` per-spawn tool-call bound. */
maxToolCalls?: number;
+ /** `--model` per-spawn model override (§5.84-A). */
+ model?: string;
+ /** `--thinking` per-spawn effort level (§5.84-A). */
+ thinkingLevel?: string;
}
/**
@@ -1209,6 +1234,8 @@ function spawnStep(task: string, label: string, spawnOpts?: SpawnOpts): Promise<
if (spawnOpts?.maxToolCalls != null && Number.isFinite(spawnOpts.maxToolCalls)) {
budgetArgs.push("--max-tool-calls", String(spawnOpts.maxToolCalls));
}
+ if (spawnOpts?.model) budgetArgs.push("--model", spawnOpts.model);
+ if (spawnOpts?.thinkingLevel) budgetArgs.push("--thinking", spawnOpts.thinkingLevel);
const child = spawn(
process.execPath,
[helperPath, "--task", task, "--label", label, ...budgetArgs, "--json"],
@@ -1739,6 +1766,8 @@ export async function runRecipe(opts: RecipeRunOptions): Promise/recipe.md` kits appeared and everything in a category
+folder was invisible.
+
+## The two layouts (both tab-visible)
+
+1. **Self-contained kit** — `recipes//recipe.md` (or legacy `kit.md`).
+ Use for shareable / installable kits. Slug = frontmatter `slug` (or the dir
+ name). Category = frontmatter `category` (or inferred from tags).
+
+2. **Category-folder playbook** — `recipes//.md`, optionally
+ nested one more level for a **subdivision**: `recipes///.md`.
+ - **Category = the top-level folder name** (e.g. `writing/…` → category
+ `writing`). A frontmatter `category:` still works and wins when present.
+ - **Subdivision = the nested folder** (e.g. `writing/papers/write-paper.md`
+ → subdivision `papers`), rendered as a sub-header inside the category.
+ - **Slug = frontmatter `slug` / `id`, else the filename** (minus `.md` /
+ `.recipe.md`). This is why two files in one folder don't collide.
+
+A bare `recipes/.md` at the root is also surfaced (category from
+frontmatter).
+
+## Rules for a new recipe
+
+- Give it a **category** that is a real folder OR an explicit `category:` field.
+- If a category grows past ~8 recipes, **subdivide it** — either move files into
+ a `//` folder, or add a `subdivision: ""`
+ frontmatter field (works even for top-level kits, no move needed).
+- The UI declares colors/icons for known categories in `RECIPE_CATEGORIES`
+ (app.ts). An **unknown** category still renders (generic 📦 bucket) — nothing
+ is silently dropped — but add it to `RECIPE_CATEGORIES` for a proper
+ color/icon/label.
+- **Verify** it shows: `openclaw gateway call prefrontal.recipe.list --json`
+ and grep for your slug. The plugin runs from `dist/`, so a code change to the
+ scanner needs a build + gateway restart; a new _recipe file_ is picked up live
+ (the scanner reads disk each call).
+
+## Frontmatter fields the tab reads
+
+`slug` / `id`, `title`, `summary`, `tags` (searched), `category`, `subdivision`.
diff --git a/extensions/tinkerclaw-prefrontal/recipes/CATALOG.md b/extensions/tinkerclaw-prefrontal/recipes/CATALOG.md
index fb9cd389412c9..a87b6fde84b1e 100644
--- a/extensions/tinkerclaw-prefrontal/recipes/CATALOG.md
+++ b/extensions/tinkerclaw-prefrontal/recipes/CATALOG.md
@@ -8,11 +8,11 @@
| Category | Color | Recipes |
| ------------- | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Coding | olive | debug, feature, refactor, code-review, code-review-5pass, simplify-parallel-review, three-agent-review, verification-gate, implementation-plan, upstream-merge, fork-patch |
-| Writing | purple | write-paper, revise-paper, compile-paper, revise-publish-batch, brainstorm, write-plan |
+| Writing | purple | write-paper, revise-paper, compile-paper, revise-publish-batch, brainstorm, write-plan, papers-staleness-audit, publish-paper-summary, brain-diagram-entry |
| Operations | amber | gateway-restart, security-audit, deploy, bookmark, memory-dream, memory-hygiene-review |
| Analysis | blue | investigate, dependency-analysis, brainstorm-gate |
| Security | red | incident-response, credential-rotation |
-| Communication | green | daily-report, jarvis-report |
+| Communication | green | daily-report, jarvis-report, compose-answer, trip-briefing |
| Combinators | teal | if-then-else, map, filter, compose |
| Marketing | coral | marketing-campaign, marketing-anchor-post, marketing-build-in-public, marketing-coordinated-launch, marketing-flagship-launch, marketing-youtube-video, write-clawhub-readme, marketing-page-audit, content-repurpose, competitor-pricing-watch, seo-content-optimize |
@@ -30,6 +30,7 @@
| revise-paper | revise, improve, polish, round of improvements, paper review | triage, read, structural-audit, evidence-check, prose, additions, final | deep |
| compile-paper | compile paper, build pdf, paper to pdf, render paper, make pdf, publish paper | survey, plan-figures, generate-missing, convert, enrich-bib, build, verify | deep |
| revise-publish-batch | revise all papers, rewrite the papers, all J-series, paper batch, all the papers | overseer, triage, revise, compile, aggregate | deep |
+| brain-diagram-entry | add paper to brain diagram, new paper marker, update/remove brain map entry | gather, glow-gen, verify, wire, publish (add/modify/delete) | deep |
| brainstorm | brainstorm, ideas, explore options, what if | frame, diverge, converge, evaluate | light |
| write-plan | plan, roadmap, design doc, RFC, proposal | scope, research, structure, draft, review | standard |
| gateway-restart | restart, gateway, reload, bounce | pre-check, graceful-stop, start, verify | light |
@@ -42,6 +43,8 @@
| credential-rotation | rotate, credentials, keys, tokens, secrets | inventory, generate, deploy, verify, revoke-old | standard |
| daily-report | daily, status, standup, what happened | gather, summarize, format, deliver | light |
| jarvis-report | report for jarvis, structured report, incident | gather, analyze, structure, deliver | standard |
+| compose-answer | answer, reply, register, match my language, tune the answer, closing summary | read-register, resolve-or-ask, compose, close-summary | light |
+| trip-briefing | trip, itinerary, gather my trip, send my trip to whatsapp, tripit, business trip | scope, gather, normalize, completeness-gate, build, deliver, confirm | standard |
| if-then-else | branch, if/else, conditional, choose branch | decide, then, else | light |
| map | map, for each, per element, fan out, iterate | produce, map | standard |
| filter | filter, keep, select, where, predicate | produce, filter | standard |
diff --git a/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/assets/audit-report-template.html b/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/assets/audit-report-template.html
new file mode 100644
index 0000000000000..fc80017a0d45d
--- /dev/null
+++ b/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/assets/audit-report-template.html
@@ -0,0 +1,273 @@
+
+
+
+
+
+Online-Presence Audit
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 🪝 ClawHub skills & plugins
+
+
+
+
+ 🔎 Staleness findings
+
+
+
+
+ 🛠️ Proposed de-stale edits read-only · human approves
+
+
+
+
+
+
+
+
+
diff --git a/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/assets/clawhub-scan.sh b/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/assets/clawhub-scan.sh
new file mode 100755
index 0000000000000..a52d6a77929aa
--- /dev/null
+++ b/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/assets/clawhub-scan.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+slugs="jarvis-voice whatsapp-ultimate youtube-ultimate chatgpt-exporter-ultimate token-panel-ultimate shell-security-ultimate token-efficiency-guide subagent-overseer computational-humor fork-and-skill-scanner-ultimate outlook-hack memory-bench-pioneer smart-model-router model-prompt-adapter owntracks-location agent-sensei-ultimate agent-superpowers tinker-command-center wordpress-ultimate"
+declare -A SRC=( [smart-model-router]="/home/user/.openclaw/workspace/skills/model-router/SKILL.md" )
+roots="/home/user/.openclaw/workspace/skills /home/user/src/tinkerclaw/skills"
+localver(){ local s="$1" f="${SRC[$s]}"
+ if [ -z "$f" ]; then for r in $roots; do for c in "$r/$s/SKILL.md" "$r/${s%-ultimate}/SKILL.md" "$r/$s/README.md" "$r/$s/GUIDE.md" "$r/$s/BUDGET_README.md"; do [ -f "$c" ] && { f="$c"; break 2; }; done; done; fi
+ [ -z "$f" ] && { echo "?"; return; }
+ local v; v=$(grep -aioP '(?<=^version:\s)["'\'']?\K[0-9][0-9.]*' "$f" 2>/dev/null | head -1); echo "${v:-no-ver}"
+}
+pubver(){ clawhub inspect "$1" 2>/dev/null | grep -oP '(?<=Latest: )\S+'; }
+audit(){ curl -s --compressed --max-time 15 "https://clawhub.ai/globalcaos/skills/$1" 2>/dev/null | grep -aoP 'security-audit-sidebar-verdict" data-status="\K[^"]+' | head -1; }
+echo "SLUG|PUB|LOCAL|AUDIT"
+for s in $slugs; do
+ p=$(pubver "$s"); [ -z "$p" ] && { sleep 2; p=$(pubver "$s"); }
+ l=$(localver "$s")
+ a=$(audit "$s"); [ -z "$a" ] && { sleep 1; a=$(audit "$s"); }; [ -z "$a" ] && a="?"
+ echo "$s|${p:-MISS}|$l|$a"
+ sleep 1
+done
+echo "---TINKERZONE---"
+# thetinkerzone sits behind Cloudflare's JA3 wall — plain curl gets a 403 "Just a moment..." challenge
+# and reads UNREACHABLE forever. curl_cffi impersonate=chrome clears it (see reference_tinkerzone_publish_cf_walled).
+python3 -c "
+from curl_cffi import requests
+try:
+ r=requests.get('https://thetinkerzone.com/wp-json/wp/v2/posts?categories=29&status=publish&per_page=100&_fields=id',impersonate='chrome',timeout=30)
+ print('paperposts',len(r.json()))
+except Exception:
+ print('paperposts UNREACHABLE')
+" 2>/dev/null || echo "paperposts UNREACHABLE"
+echo "---README---"
+grep -aoiE 'papers?-[0-9]+' /home/user/src/tinkerclaw/README.md 2>/dev/null | head -1
diff --git a/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/recipe.md b/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/recipe.md
index c654fb89fbabe..89a2a40f8056a 100644
--- a/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/recipe.md
+++ b/extensions/tinkerclaw-prefrontal/recipes/audit-online-ripples/recipe.md
@@ -2,7 +2,7 @@
schema: "kit/1.0"
slug: "audit-online-ripples"
title: "Audit Online-Presence Ripples & Staleness"
-summary: "Trace every public surface we control (README, ClawHub pages, thetinkerzone posts, Moltbook, GitHub threads, extension READMEs, social), map the links between them, and flag at a glance what has drifted stale — wrong counts, old model names, dead links, broken anchors."
+summary: 'Trace every public surface we control (README, ClawHub pages, thetinkerzone posts, the Tinker UI in-app links, Moltbook, GitHub threads, extension READMEs, social), map the links between them, and flag at a glance what has drifted stale — wrong counts, old model names, dead links, broken anchors, mis-pointed UI links, missing concept hero images, ClawHub skills published behind their local source, ClawHub skills flagged "review"/potentially-malicious by the audit bot.'
version: "1.0.0"
owner: "globalcaos"
license: "MIT"
@@ -42,15 +42,26 @@ parallelism:
- [4]
- [5]
- [6]
+ - [7]
---
# Audit Online-Presence Ripples & Staleness
-> Trace every public surface we control (README, ClawHub pages, thetinkerzone posts, Moltbook, GitHub threads, extension READMEs, social), map the links between them, and flag at a glance what has drifted stale — wrong counts, old model names, dead links, broken anchors.
+> Trace every public surface we control (README, ClawHub pages, thetinkerzone posts, the Tinker UI in-app links, Moltbook, GitHub threads, extension READMEs, social), map the links between them, and flag at a glance what has drifted stale — wrong counts, old model names, dead links, broken anchors, mis-pointed UI links, missing concept hero images, ClawHub skills published behind their local source, ClawHub skills flagged "review"/potentially-malicious by the audit bot.
+
+## Routes (the link graph we keep current)
+
+The funnel is a directed graph; each arrow is a staleness dependency this audit re-checks:
+
+- **ClawHub skill page → GitHub repo** (each `SKILL.md` → `github.com/globalcaos/tinkerclaw`)
+- **Local skill source → ClawHub published version** (each owned `SKILL.md`/plugin manifest we improved → the version live on clawhub.ai). This is a publish-lag staleness edge: we edit the source but forget to re-publish, so the public page serves an older skill than we run. The actuator is the `clawhub-publish` skill (lifecycle step 2, "diff published vs repo, re-publish the newest").
+- **GitHub README → thetinkerzone posts** (the J-series paper links + the funnel boxes)
+- **Tinker UI → thetinkerzone posts** (NEW 2026-06-24, bible §5.8k): `tinker-ui/src/app.ts` `ZONE_DOCS` map + `fractal-dock.ts` chip → `thetinkerzone.com/?p=`. Each always-visible panel (Models→slider, EEG, RECIPES→recipe-book+prefrontal, Amygdala, Fractal) links to its explainer/paper post. These are in-app links by STABLE `?p=ID`, so they break if a post is deleted or a concept is re-pointed to the wrong paper — both are this audit's job to catch.
+- **thetinkerzone post → {README, Tinker UI}** downstream consumers — a post's existence/topic is depended on by both, so a post change ripples to both.
## Goal
-Produce, on demand, two things: (1) a dependency graph of our public surfaces and the links between them, so we can see the entanglement of our online presence at a glance; and (2) a staleness report that flags every claim that has drifted from ground truth (counts, versions, model names, dates) plus every dead link or orphan surface. Read-only: it proposes the de-stale edits, it does not rewrite the surfaces.
+Produce, on demand, two things: (1) a dependency graph of our public surfaces and the links between them, so we can see the entanglement of our online presence at a glance; and (2) a staleness report that flags every claim that has drifted from ground truth (counts, versions, model names, dates) plus every dead link or orphan surface. The headline deliverable is a **beautiful self-contained HTML dashboard** (Step 7b) — a glanceable, severity-sorted audit the user opens in one click, with the markdown file as backing detail. Read-only: it proposes the de-stale edits, it does not rewrite the surfaces.
## When to Use
@@ -66,6 +77,8 @@ Produce, on demand, two things: (1) a dependency graph of our public surfaces an
Read `~/.openclaw/workspace/memory/online-presence/inbound-campaign-state.json` — `inbound_inventory` and `inbound_targets` are the authoritative seed set: ClawHub skill pages, Moltbook posts, GitHub threads, the extension READMEs that link thetinkerzone, and the per-domain ours/external tallies. These are the graph's nodes. Note when the file was last audited (`last_run`) so you know how trustworthy the seed is.
+Also load the **Tinker UI → thetinkerzone** link map as a seed node source: parse the `ZONE_DOCS` object in `~/src/tinkerclaw/tinker-ui/src/app.ts` (and the hard-coded `?p=198` chip in `fractal-dock.ts`) → a `{concept → post-ID}` set. This is the canonical list of in-app links; every entry is an edge to verify in Step 3 and resolve in Step 5. The single owner of this map is bible `tinker-ui.md` §5.8k.
+
### 2. Enumerate the live state of each surface
**Tools:** backlink-audit
@@ -73,12 +86,18 @@ Read `~/.openclaw/workspace/memory/online-presence/inbound-campaign-state.json`
Pull the CURRENT live state per domain — never a mirror or a cached CLI 'not found'. thetinkerzone: `GET https://thetinkerzone.com/wp-json/wp/v2/posts?status=publish&per_page=100&_fields=id,slug,link,title,modified` — the `modified` date per post is the freshness signal. ClawHub: the rendered clawhub.ai pages (browser relay) or the installed-skill catalog. The repo: `git grep -nE 'https?://' README.md docs/ extensions/*/README.md` for outbound links. The backlink-audit skill covers the INBOUND side (who links to us). Cache everything to `~/.openclaw/workspace/memory/online-presence/ripple-cache.json`.
+For the **ClawHub published-version** check, enumerate every slug we've uploaded so far — the canonical list is `inbound_inventory.tinkerclaw.clawhub_pages` in the state file (Step 1) — and pull each one's PUBLISHED version + last-updated date read-only with `clawhub inspect --json` (the `Latest` tag + `Updated` field; this works WITHOUT `clawhub login`, since publishing is gated but reading is not). Plugins use `clawhub package`/the rendered page instead of `skill`, but the version field is the same. Cache `{slug → published_version, published_updated}` alongside the rest. Do NOT trust the CLI's "not found" for a slug that renders fine on clawhub.ai — fall back to the browser relay.
+
+The durable gatherer for both ClawHub checks (version drift + audit status) lives at `assets/clawhub-scan.sh` — run it rather than re-improvising the loop in `/tmp` (it gets reaped between sessions). It already bakes in every tuning lesson: parse the text `Latest:` line (not `--json`, which emits nothing), backoff+retry per slug (rate-limiting causes false NOT_FOUND), `curl --compressed` + `grep -a` for the audit scrape (clawhub.ai serves gzip — without it the scrape reads as binary and silently returns nothing), and an explicit `slug→source` map for the cases where the folder name ≠ the slug (e.g. `smart-model-router` → `model-router`).
+
+For the **ClawHub audit / "potentially malicious" status**, also capture each slug's ClawScan verdict. ClawHub runs an audit bot (clawsweeper / openclaw-barnacle) on every skill: the verdict surfaces in the page HTML as `