# VisionClaw Agent — Revision History

Detailed release-by-release notes. The README's revision history shows one-liners; this file is the deeper play-by-play for anyone who wants to know exactly what changed and why.

Newest first.

---

## Round 98.20 — May 5, 2026

**CI Concurrency Group — killed the cancellation-email storm.** When the Auto Git Push workflow commits multiple files in rapid succession during a heavy editing session (the public-mirror routinely batches home/landing/updates/sidebar/about edits within a 90s quiet window), GitHub used to spawn one full CI run per commit and email the maintainer for every cancelled run as the next push superseded the previous — often 6+ emails per editing session. Added a `concurrency` block to `.github/workflows/ci.yml` that groups runs by branch (`ci-${{ github.workflow }}-${{ github.ref }}`) with `cancel-in-progress: true`, so only the latest commit on `main` runs CI to completion; older runs cancel at the workflow level (one cancellation, not one-per-job).

---

## Round 98.19 + 98.19+sec — May 5, 2026

**Memory v2 (deer-flow Nuggets 1-4) + Whole-App Code Review Sweep.** Two rounds shipped same day.

**R98.19 Memory v2** layered four complementary mechanics onto the agent memory subsystem (additive, backward-compatible, 1 new column + 1 new background queue, no schema break):
1. **Confidence-scored facts** — every memory write carries a 0.0-1.0 `confidence` plus a `confidence_source` enum; recall ranks by `confidence × recency × access-frequency` so a high-confidence fact from a verified tool result beats a low-confidence one inferred from chat.
2. **Debounced write queue** — 30-second in-process queue dedupes identical writes within the window so a 5-tool-call burst persists ONE row not five.
3. **Synthesis-time dedup** — substring + Jaccard ≥0.8 match against existing facts in the same scope before write; higher-confidence write wins.
4. **Token cap on recall context** — hard-capped at 8K tokens so memory pulls never blow out the chat budget on long sessions.

**R98.19+sec Whole-App Code Review Sweep** — three architect rounds, six real bugs closed. The big finding: a recurring bug class showed up across five separate hardening passes — historical code used CommonJS-style `require()` inside `try/catch` blocks under ESM module mode, so every `require()` threw "require is not defined" at runtime and the catch silently swallowed it. Net effect: five different security primitives were quietly degraded for as long as those files have been deployed. All five fixed in one session: provider-error secret redaction (was leaking unredacted), `gate_command` untrusted-stdout fence (silently degrading), `wrapAsData` fence builder, presenter constant-time HMAC compare (was hard-blocking every legitimate request with 403), and the Claude-agent GitHub importer's prompt-injection scanner (the most serious — imported personas could carry "ignore previous instructions" + exfil-curl payloads straight into a durable system prompt). The scanner catch was also tightened from "false fail-closed" (the comment claimed fail-closed, the code was actually fail-open) to true fail-closed quarantine. tsc clean across all three rounds.

---

## Round 94 — May 3, 2026

**Tenant Cost-Attribution Integrity.** Every authenticated path — vc_ API key, session token, Replit OIDC, glasses gateway, MCP server (global + per-tenant derived keys), background job worker, nightly tool-sommelier cron — now wraps its handler in `runWithTenant()` from a brand-new `server/lib/tenant-context.ts`. AsyncLocalStorage propagates the binding through every downstream `await`, so even the singleton `replitOpenai` chat / embeddings / `audio.speech` / `audio.transcriptions` calls bill the correct tenant without anyone having to thread a `_tenantId` parameter through six layers of helpers. `createMeteredOpenAIClient` `resolveTenant()` runs an explicit priority chain — explicit `opts.tenantId` → AsyncLocalStorage current → ADMIN with warn-once-per-stack-trace — so misses are loud, not silent. Public chat passes `convTenantId` explicitly to `getClientForModel`.

**The bug Felix HVAC caught.** The original implementation used a CommonJS `require("./lib/tenant-context")` inside the resolver. Under tsx/ESM dev mode that throws `require is not defined` and the entire cost lookup silently fell back to ADMIN — every per-tenant cost was being attributed to platform overhead. A live end-to-end Felix HVAC tenant test surfaced the warning immediately. Fix: promoted to a top-of-file static `import { currentTenantId } from "./lib/tenant-context"` (no circular risk; the helper only depends on `node:async_hooks`). Three parallel architect reviews + a clean-boot Felix HVAC re-run confirmed zero `require is not defined` warnings, zero `cost-track failed` warnings under normal traffic.

**Same window, 9 distinct High-severity findings closed.** `truncateWithSummary` now merges its summary into the first kept user turn when needed — Anthropic strict-alternation safe. `scan_file` gains lstat symlink rejection + realpath re-validation (parity with read_file/write_file). Persona create/update routes scan `soul`/`agentsDoc`/`heartbeatDoc`/`brandVoiceDoc`/`identity`/`tools` for prompt injection BEFORE persistence; mind soul scanned identically; the claude-code-importer scans every imported persona body the same way. `write_file` shares a single blocklist with the path-jail and walks every parent component for symlinks. Per-tenant hourly escalation quota (20/hr) means a single noisy tenant can't drain the platform escalation budget. Metered OpenAI client cache keys are now SHA256 (collision-resistant, periodic prune). `truncateWithSummary` demotes the summary from system to user role — history summaries are untrusted reference data, not policy, closing a prompt-injection privilege-escalation path. `cleanedFullResponse` strips `<tool_call>` tags. A disconnect-persistence guard means client-aborted streams no longer trigger title-gen + hook side effects.

---

## Rounds 83-93 — May 2-3, 2026

**R95.c — TIER-1 outbound-gate completeness pass (same window):** Eight surgical fixes closing every remaining outbound surface that bypassed the R95 redaction gate. (1) `server/hooks.ts` webhook-relay added SSRF deny-list (private/loopback/link-local/metadata IPs) + R95 gate on the relayed body. (2) `server/email.ts` `sendEmail` now runs the gate centrally at the provider chokepoint instead of relying on every caller. (3) `server/mcp-server.ts` gates every tool response before serializing back to the MCP client. (4) `server/glasses-gateway.ts` gates the response payload on `/v1/glasses/execute`. (5) `server/exec-tool.ts` adds `lstat` + `realpath` checks on the working directory (parity with `read_file`/`write_file`/`scan_file`). (6) `server/tools.ts` removed the `||1` tenant fail-open in the Felix proposal-spec verifier. (7) `recurring-messages` list/pause routes now require explicit tenant context. (8) Three pre-existing `runWithTenant` TS errors fixed in passing. **Plus CI hardening:** `tests/security/sql-raw-callsite-allowlist.test.ts` refactored to a content-only `textsOnly()` comparator that ignores line-number drift but still hard-fails on any new `sql.raw` callsite — CVE guard semantics preserved, line shifts no longer red CI. Full security+storage suite **111/111 PASS**.

**R95.v2 hardening (same window):** Two-round architect review hardened the gate against Unicode/zero-width bypass (`AKIA\u200B...` and fullwidth digits now caught via NFKC canonicalization), killed credit-card false positives via Luhn validation, moved noisy 9-digit-SSN-shape pattern behind an opt-in flag, made the block-message generic so it cannot be probed as an oracle, hoisted the gate into `messaging-gateway.deliverMessage` so the cron scheduler and every other caller are automatically covered (SMS/WhatsApp force strict mode), and fixed a pre-existing cross-tenant bug in scheduled-message create/cancel surfaced during review. Test suite expanded to 22 cases including bypass scenarios — all pass.

**R95 — Outbound Sensitive-Data Redaction Gate (May 3 2026).** Direct response to "Agents of Chaos" (Northeastern et al., Feb 2026) Case Study #3, where an OpenClaw-architecture agent refused a direct request for an SSN then disclosed it via a forwarded email thread. New deterministic pattern-based egress gate (`server/lib/outbound-redaction.ts`) scans every outbound payload — email body/subject/html, inter-agent message, agent-callable preflight — for tenant env-var values (live registry refreshed every 5min), private keys (PEM/OpenSSH), live cloud/payment provider keys (AWS, Stripe `sk_live`, OpenAI, Anthropic, Google, GitHub, Slack), JWTs, Bearer tokens, env-var-shape assignments, credit-card numbers, and US SSNs. Three verdicts (`clean`/`redact`/`block`) wired into `send_email`, `sessions_send`, and a new `verify_outbound_safety` tool. Critical findings refuse to transmit and instruct the agent to escalate via `request_approval` if the recipient is genuinely authorized. Same-window bonus: `server/providers.ts` resolver had the same R94-class CommonJS-require-in-ESM silent-throw bug, fixed.

**Comprehensive 24h security sweep.** Nine R-bundles closing every defect surfaced by three parallel architect reviews of the entire app, all shipped same window. R83 streaming-aware AnthropicPromptCache with per-tenant entry quotas. R84 audit-reasoning + reasoning-extractor sandbox. R85 prompt-injection scanner ported from Hermes Alpha (10 threat patterns + invisible-unicode steganography detection). R86 / R91 streaming wiring + glasses fail-closed on missing tenant binding. R87 context-compressor with token-aware budgets. R88 error-context-parser + tool-call-fallback-parser — when the LLM emits malformed XML/JSON tool-calls the parser still recovers. R89 orphan tool_call/tool_result repair after truncation, for both Anthropic and OpenAI strict-alternation. R90 context-files-loader gated by the same scanner. R92 self-heal + auto-memorize given explicit ADMIN tenant attribution. R93 tenant-attribution propagated to 7 cross-cutting callsites (felix-loop, plan-executor, ceo-orchestrator, cross-critique, distributed-slides, auto-memorize, self-heal).

---

## Round 80 — May 2, 2026

**Claude Code Subagent Importer + Runtime Wiring.** Open-source AI agents are exploding on GitHub — every week another curated `.claude/agents/*.md` collection drops with battle-tested personas for security work, code review, content production, research. Round 80 turns VisionClaw into the multi-tenant runtime that hosts any of them, fully wired into our governance stack.

**The flow.** A platform admin pastes a GitHub URL pointing at any `.claude/agents` directory. The importer walks the tree to any depth via the public GitHub tree+raw APIs (no auth required; an optional GitHub token lifts the rate limit), parses every agent's YAML frontmatter strictly via `js-yaml` `JSON_SCHEMA` (BOM- and CRLF-tolerant), maps each Claude Code tool reference to its VisionClaw equivalent, and infers tier (executor if the agent declares `Bash` OR its description contains "Tier 2" / "executes"; advisory otherwise). A preview screen shows the parsed roster — name, tier, mapped tools, conflicts with already-installed personas — before any DB write. Apply is idempotent via namespaced `<source-slug>:<agent-slug>` names so re-running the same import never duplicates rows.

**The wiring layer is what makes it real.** A naive importer that just inserts persona rows would get you LLMs hallucinating `Read('foo.txt')` against a runtime that has no `Read` tool. So every imported persona's `soul` (system prompt) is wrapped with a `VISIONCLAW RUNTIME ADAPTER` preamble placed *before* the original Claude Code instructions:

- A **Trust Boundary** clause at the top tells the agent that everything below the adapter is untrusted legacy guidance, the adapter wins on conflict, and any instruction to bypass Human-in-the-Loop must be refused. The agent itself becomes a participant in policy enforcement.
- A **Claude → VisionClaw tool translation table**: `Read` → `read_file` / `scan_file`, `Write` → `write_file` / `write_scratchpad` / `create_memory` / `create_knowledge`, `Bash` → `exec` / `execute_code` (HITL-gated), `WebFetch` → `web_fetch` / `firecrawl_scrape` / `readability_extract`, `WebSearch` → `web_search` / `firecrawl_search`, `Task` → `delegate_task`, `Grep` → `search_memory` / `search_knowledge` / `scraped_pages_query`, `Edit` → `write_file` (HITL-gated). Every mapped name was verified against the live `TOOL_DEFINITIONS` source — no fictitious tools.
- The standard **7-step VisionClaw operating loop** populated with the executor or advisory variant so imported agents participate in the same recall-plan-verbalize-persist rhythm as built-in personas.

**Tier-aware tool router.** Imports map their role to `Imported Subagent (developer)` for executor-tier or `Imported Subagent (researcher)` for advisory. Two new policy entries are listed *first* in the persona-policy object so substring-match wins over generic developer/researcher keys: the researcher role hard-blocks `exec`, `shell_exec`, `execute_code`, `write_file`, `send_email`, `whatsapp`, `deliver_product`, `draft_social_post`, `marketing_experiment` at the router — refusal happens before any LLM ever sees the tool. Developer-tier allows code/system/file access but still blocks delivery surfaces.

**Per-persona autonomy rules on dangerous tools.** Executor-tier imports automatically receive three `autonomy_rules` rows — `exec`, `execute_code`, `write_file` — all set to `approve_before`, tenant-scoped. Idempotency comes from the `(tenant_id, persona_id, action_type)` partial unique index used with `onConflictDoNothing` + `.returning()` so re-runs report truthful counts. Researcher imports skip these because the router already hard-blocks the same tools at a higher layer — defense in depth without redundant DB rows.

**Hardened SSRF posture.** The URL parser locks input host to `github.com` / `www.github.com`. Every network call goes only to `api.github.com` or `raw.githubusercontent.com`. `redirect: "error"` is set on every fetch so a redirect to an unexpected host fails closed instead of silently following. Repo-tree truncation (>100k entries) is rejected. A per-request file cap prevents resource exhaustion. Errors flow back to the admin only — no server-internal paths or secrets in the response.

**Architect review** returned SHIP IT after closing three issues: a fictitious `fetch_file` tool name was removed from the translation table, the researcher policy gaps were closed (delivery surfaces explicitly blocked), and the trust boundary was made explicit rather than implied. 43/43 importer tests + 62/62 security tests + tsc clean.

**Why this matters.** Community agent collections are now a viable distribution channel for specialized AI labor. A VisionClaw operator can bolt on a 30-agent pentest crew or a 12-agent content team in under a minute, and the runtime adapter + tier-aware policies + autonomy rules ensure the new arrivals can't escape governance. Imports are first-class citizens — they show up in `/api/personas`, can be set as a conversation persona via the AI Team page, can be `delegate_task`'d to by Felix and other built-ins, and persist findings via `create_memory` / `create_knowledge`.

---

## Round 79.3 — May 2, 2026

A same-session four-round push closing the full Human-in-the-Loop approval surface and killing a multi-day red CI gate. The round-by-round play-by-play follows.

**HITL self-loop fix (R79.3c):** The agent was emailing the owner asking permission to email the owner. The `send_email` tool is correctly classified as high-risk, but it was firing HITL approval even when the recipient *was* the configured owner alert address — Felix sending a deliverable to `huskyauto@gmail.com` triggered an approval email to `huskyauto@gmail.com` asking permission for the original send. Added `isOwnerSelfEmail()` to `server/policy-engine.ts` that auto-approves `send_email` when **all** recipients (to+cc+bcc unioned, fail-closed on any mixed-recipient case) are owner-controlled. The owner set is sourced from the `OWNER_ALERT_EMAIL` / `SITE_OWNER_EMAIL` / `SITE_CONTACT_EMAIL` / `OUTPUT_DELIVERY_OVERRIDE_EMAIL` env vars plus hardcoded fallback contacts, and the matcher handles every recipient encoding shape (string, array, comma-list, `{name,email}` object).

**One-click approve/deny in HITL emails (R79.3d):** Escalation emails arrived with just a confirmation ID and "reply via WhatsApp or open the workspace" — no clickable buttons, every approval cost a context switch. New `server/hitl-tokens.ts` issues HMAC-SHA256 signed JSON tokens `{cid, decision, tid, exp}` packed as `<base64url-payload>.<base64url-signature>` with a 24h TTL. Secret resolution is layered (`HITL_TOKEN_SECRET → SESSION_SECRET → CRON_SECRET → ephemeral random with warning log`); signature compare is `crypto.timingSafeEqual` to defang timing oracles. The escalation email now ships both a plaintext block (for clients that strip HTML) and a styled HTML body with green Approve / red Deny buttons. `/api/hitl/approve` and `/api/hitl/deny` register **before** the `/api` auth middleware so a clicker doesn't need a session cookie, and use a prefetcher-safe split: GET renders an idempotent confirmation page (Outlook Safe Links and Gmail link-previewers can fetch it freely without resolving anything), POST is the only path that actually mutates. `/api/hitl/` is in the CSRF skip list — the HMAC token is strictly stronger here. Replay attacks are blocked by the one-time consume of the pending entry.

**Multi-day red CI gate closed (R79.3e):** The `Security & Tenant-Isolation Tests (hard gate)` job had been failing red for ~5 commits while local always ran 191/191 green — the worst kind of failure to debug. Pulled the actual job log via the GitHub Actions API and dissected: out of 191 sub-tests, exactly 9 were failing in two distinct buckets. **Bucket 1** — the `sql.raw` / `sql.identifier` callsite-snapshot security tests shelled out to `execSync("rg -n ... --type ts server/ || true")`. On the CI runner image, `rg` returned nothing (PATH or version mismatch). The `|| true` swallowed the failure, the walker returned an empty map, the snapshot mismatched the populated baseline. Rewrote as a pure-Node `fs.readdirSync` walker plus a JS regex — no shell dependency, deterministic across local + CI. **Bucket 2** — 7 storefront-checkout tests died with `Catalog file does not exist on disk: project-assets/...`. The catalog has an `existsSync` gate that 500s if the static product file is missing; `project-assets/` is gitignored because real payloads ship via Drive, not git, so a fresh CI checkout has zero catalog files. New `tests/fixtures/seed-catalog-files.ts` regex-extracts every `filePath:` entry from `server/product-catalog.ts` and writes a small type-aware stub (real HTML/CSV/MD/PDF headers) at each path; idempotent — skips files that already exist, so it's a no-op locally where real assets live. Wired into `.github/workflows/ci.yml` as a step between the Stripe schema bootstrap and the test runner. **Bonus** — postgres logs were spamming `malformed array literal: "1"` from `attachCitationMetadata` because Drizzle's tagged template doesn't auto-encode JS arrays as a postgres `int[]` literal. Rewrote as a properly-parameterized `IN (?, ?, ...)` list with each id bound as its own scalar.

**Post-review hardening (R79.3f):** Architect review of the three-round arc found one Medium-rated security defect and one Low. **Medium:** `resolveToolConfirmation()` in `server/tool-mutation.ts` only enforced tenant equality when `pending.conversationId != null`. For confirmations created from non-conversation contexts — background jobs, scheduled tool calls — the tenant check was skipped entirely; an attacker who could obtain or guess a `confirmationId` could resolve another tenant's pending HITL approval. Fixed by removing the `conversationId` condition: tenant equality is now enforced whenever both the caller and the pending record know their tenants. The internal auto-deny path (disconnect handler) intentionally calls without a `requesterTenantId` so it preserves correct behavior. 4 new regression tests pin the corrected behavior across (a) no conversationId, (b) with conversationId, (c) internal trusted caller, (d) unknown cid. **Low:** the catalog-stub seeder used `abs.startsWith(ROOT)` as path-traversal defense — fooled by sibling-prefix paths. Replaced with `path.relative(ROOT, abs)` + `..` / absolute reject + upfront `path.isAbsolute(rel)` check.

**195/195 hard-gate green** locally and on CI (run `25254489085`, all 5 jobs PASS on commit `b0f994b`). The architect explicitly cleared the rest of the surface: HITL one-click email design (HMAC strong, prefetcher-safe split, replay-blocked), CSRF skip for `/api/hitl/*`, `isOwnerSelfEmail()` recipient-union logic, the IN-list rewrite, and the JS-native snapshot guard. Net additions: 1 new module (`server/hitl-tokens.ts`), 1 new CI fixture (`tests/fixtures/seed-catalog-files.ts`), 1 new regression test file (`tests/security/hitl-cross-tenant-resolve.test.ts`).

---

## Round 74.13y — April 28, 2026

A four-day push covering autonomy, verification, and a thorough hardening sweep. Highlights below; the round-by-round play-by-play follows.

**Felix Autonomous Loop (R74.13w, dry-run mode through 2026-05-12):** A scheduled "Daily Operating Loop" cron now wakes the CEO persona every 4 hours during waking hours to read his inbox, scan recent conversations for skill-worthy patterns, and decide whether to draft a proactive action, invoke `synthesize_skill`, delegate to a specialist, or report "nothing to do." Five new operator-facing tools (`felix_loop_status`, `list/approve/reject_felix_proposal`, `felix_loop_run_now`). Safety rails: dry-run for 14 days (logs intent but doesn't auto-send), per-project opt-in, monthly cost cap, hard "ask first" for anything touching customers/finances/external channels.

**Felix Verification Rail (R74.13x, SWD-inspired):** Every Felix proposal now declares an `expected_post_state` — a small machine-verifiable spec describing the post-execution world. New `server/felix-verify.ts` defines a per-kind `VERIFIER_REGISTRY` (whitelisted table + filterable columns + content columns) and a `captureState`/`verifyDelta` pair. **All SQL is parameterized via Drizzle's `sql` template — LLM output never reaches `sql.raw`.** Mismatch flips status to `verification_failed`, requires manual re-approval. Two new Bob-only tools: `verify_felix_proposal_spec` (read-only schema check) and `execute_felix_proposal` (verify-around).

**Borrowed-best-practice pass (R74.13v, SocratiCode-inspired):** No AGPL code copied — only well-known patterns. MCP plugin manifests for Claude Code, Cursor, and OpenAI Codex CLI; "search before reading" discipline preamble injected into core operating loops; resumable code-health scanner with JSON checkpoint sidecar; new `AGENTS.md` companion to `replit.md` for non-Replit coding hosts; landing page gains "The Agentic Edge" section.

**Two-pass architect review day (R74.13u-sec / R74.13u-2):** Morning security sweep — admin routes hard-gated to `requirePlatformAdmin`; Stripe Checkout validates `priceId` against live `stripe.prices`+`stripe.products`; `decryptApiKey` throws `DecryptionError` on failure (no more silent ciphertext leak); Drive backup scoped by `tenant_id`; CSRF tokens keyed per-session. Afternoon webhook reliability rebuild — Stripe + Coinbase dedupe rebuilt as a CLAIM-then-COMMIT model in `webhook-dedupe.ts`. The previous "mark seen on receive" pattern silently dropped events on transient processing failure — that class of revenue-loss bug is closed. 6h GC sweep preserves in-flight (uncommitted) claims.

**"Fix anything that needs to be fixed" sweep (R74.13y):** Eight surgical code-level fixes — added 8 missing operator tools to the registry; replaced a CommonJS `require()` in an ESM module that was silently disabling the very_slow rate-limit coverage check; added `cross_critique` and `video_transcribe_words` to `EXPENSIVE_TOOLS` (1/min vs default 10/min); fixed a wording bug that conflated rate-limit warnings with missing-tool warnings; repaired a broken `auto-tuner.ts` query referencing the wrong table (now joins correctly so the 24h tuning cycle actually executes); replaced a magic-number `tenantId: 1` with a named `PLATFORM_OWNER_TENANT_ID` constant; un-swallowed a `.catch(()=>{})` on proposal status updates; normalized the checkout idempotency key (trim + lowercase email) so case-variant resubmissions hit the same lane. Plus a tenant-row cleanup: 7 stale test tenants and their 8 trivial dependent rows deleted in a single atomic transaction, leaving the clean 2-tenant baseline. Architect final review: zero HIGH or MEDIUM findings.
