Skip to content

Releases: eyssen/eyas

EYAS v0.8.23-beta — Nothing said is lost

Choose a tag to compare

@eyssen eyssen released this 05 Sep 07:53

EYAS could hold a conversation and forget it. Messages lived in
conversation_messages, background agent output lived in agent_events, and
the memory tiers underneath stayed at zero rows — nothing wrote to them, so
nothing could ever be recalled from them.

This release lays the floor. A raw layer now keeps every message verbatim,
compressed and content-addressed, and a deterministic pass turns each flush
into facts, a gist, entities, topics and tags — with zero model calls and no
API cost
. It is a write path: recall from these layers arrives in the next
wave, and until then the vault index and related-work blocks serve the prompt
exactly as before.

The raw layer

  • Every persisted message is kept a second time, verbatim. Each one becomes
    a zstd blob keyed by the SHA-256 of its uncompressed bytes, plus a
    memory_raw row carrying actor, task, project, source type, timestamp and
    trust tier, a contentless FTS5 entry, and structural tags. Two byte-identical
    messages inside one task share one blob (ref_count 2); across two tasks they
    get one blob each, so a future per-task erasure stays possible.
  • Capture sits at the persistence layer, not after the turn. Chat messages
    are captured inside addMessage itself, so interactive routes, executeAgent,
    the orchestrator, God Mode's winner promotion and every channel adapter are
    covered structurally rather than by remembering to call something. Background
    agent output is captured from the event store's LlmResponse append and joined
    to its conversation through agent_sessions.
  • Trust is assigned at capture and never inherited from context. Your own
    messages are owner, model-authored text is derived, tool output is
    ingested. A derived row can never outrank the sources it came from.
  • Writes are buffered per task and flushed on four triggers: the task closes
    (or moves into a closed stage), the buffer passes chunkTokens (8000
    estimated tokens), a once-a-minute sweep finds it idle for idleFlushMinutes
    (30), or EYAS stops — shutdown flushes everything still buffered, so a restart
    loses nothing. A failed flush rolls back and returns its units to the buffer.
  • Idempotency is keyed on the capture-time id, not on content, so a retried
    flush is a no-op while two genuinely identical replies stay two occurrences. A
    reply that arrives twice through two different paths — once as an event, once
    as the message that follows it — is suppressed once, within ten minutes and
    within one task.

What EYAS derives from every flush

  • A deterministic extraction pass runs after each committed flush, with no
    model call.
    It reads the task's new rows above a per-task watermark and
    derives: structural facts from key: value lines (up to 20) plus up to four
    board facts (title, project, project type, agent); entities by regex — dates,
    @mentions, #tickets, code identifiers, backticked terms, capitalised
    phrases (up to 50); topics from TF-IDF stems unioned with entity names; and a
    gist of at most 280 characters built from the first and last message plus up
    to three TF-IDF-picked sentences. This is a property of the import graph, not
    a code path left untaken: nothing under memory/v2 imports a model, provider
    or gateway.
  • An importance score is computed from the conversation itself — message
    count, your own text volume, decision markers in five languages, whether the
    task is closed and whether you pinned it. The weights are hand-set and
    published in the source.
  • Facts are arbitrated, not appended. A fact whose content hash already
    exists, still live and in the same project, is linked rather than duplicated.
    The same (subject, predicate) with a different object supersedes the old
    row: the old one gets a valid_until and an invalidated_by_fact_id, so
    "deadline is Monday → Friday → Monday" ends with exactly one live fact and an
    intact history. Nothing is updated in place and nothing is deleted.
  • Tags are inherited only when every source carries them. A fact or gist can
    never carry a project or task its own sources lack; a violation is counted and
    the tag withheld.
  • Every run is recorded, including the ones that do nothing. memory_run
    gets a row for each extraction with its trigger, counts, language, importance
    and — always, in this release — model_used = NULL and model_calls_used = 0.
    A skip writes a row too.

The poisoning gate

  • Instruction-shaped text never becomes a fact or a gist at full trust.
    Explicit override phrasing — "ignore all previous instructions", "hagyd
    figyelmen kívül", "vergiss alles", "olvida todo", "oubliez tout", role
    reassignment with "from now on" — is rejected outright. Imperatives aimed at
    the assistant, tool-invocation directives and memory-wipe directives are
    quarantined: stored, but at a trust tier that recall must exclude. Fake role
    markup (<system>, [INST], SYSTEM:, shouted headings) is quarantined too.
  • A rejected gist degrades rather than disappearing: it falls back to the
    heuristic gist, then to only the sentences that scan clean, then to a stub that
    keeps the task addressable without carrying the text. Every rejection and
    quarantine is counted in the run row.
  • Coverage is English, Hungarian, German, Spanish and French. It is a regex
    gate — a filter, not a proof — and it will occasionally quarantine ordinary
    engineering prose such as Execute the following command in the pod: ….

Configuration

Seven new keys, all under memory:

  • memory.l0.enabled (default true) — the master switch for raw capture.
  • memory.l0.extractInLegacy (default true) — derive facts and gists even
    though engine is still legacy. Set it to false to keep the raw text and
    derive nothing.
  • memory.engine (default legacy) — selects the read/write engine. Today
    it gates only extraction; it does not switch retrieval yet.
  • memory.l0.chunkTokens (8000) and memory.l0.idleFlushMinutes (30) —
    the two size and time flush triggers.
  • memory.l0.captureToolResults (default false) — read this before
    turning it on.
    A captured tool result is the whole output, verbatim and
    unredacted, plus 2048 characters of the call's arguments: run_command stdout,
    read_file contents and a live browser_totp one-time code all land in the
    raw layer as plain text. Nothing redacts them and nothing encrypts them at
    rest — dek_id is NULL and zstd is compression, not confidentiality. With the
    flag on, every boot prints a warning saying exactly that.
  • memory.l0.toolResultMaxBytes (8192) — the byte cap applied to a captured
    tool result, clipped on a UTF-8 boundary with a visible truncation marker.

Diagnostics and platform

  • eyas doctor reports two new lines. SQLite capabilities are probed live —
    FTS5 is required and a missing one is a hard failure; sqlite-vec is checked by
    actually loading it, inserting an int8 row and running a KNN query, with a
    platform-specific remedy when it is absent. The zstd tier is reported too:
    native is fine, the WASM fallback is a warning (about 2× slower), none is a
    failure.
  • A three-tier zstd shim picks Bun's native compressor, then node:zlib
    (Node ≥ 22.15; 23.0–23.7 have none), then @bokuweb/zstd-wasm. If no tier
    resolves, capture is disabled loudly and says so — never left buffering
    silently. Level 3, measured ratio ≈ 2.7 on real text, about 32 µs per message.
  • One new dependency: @bokuweb/zstd-wasm 0.0.27 (MIT), used only when
    neither runtime provides zstd natively.
  • Every EYAS database connection now runs PRAGMA synchronous = NORMAL
    instead of SQLite's default FULL. With WAL this stays durable across a
    process crash, but not across an OS crash or power loss at the instant of
    commit. This applies to all modules, not only to memory.
  • The capability probe no longer runs inside an open transaction. It used to
    be able to swallow a caller's uncommitted rows; it now refuses, loudly, and a
    failed probe is never cached.

What this release does not do yet

  • Nothing reads the new layers. There is no retrieval path, no HTTP
    endpoint, no UI page and no eyas memory command in this release: the raw
    rows, facts and gists are written and then wait. Recall, embeddings and the
    context assembler are the next wave. Setting memory.engine: v2 today changes
    nothing.
  • The raw layer grows and nothing prunes it. There is no retention setting
    and no cleanup job yet; measured, a captured row costs on the order of 5 KB
    all-in including indexes. If you would rather not pay that yet, set
    memory.l0.enabled: false.
  • Rows whose source timestamp predates the extraction watermark are not
    extracted.
    They stay in the raw layer and are counted in the run row, and a
    future rebuild recovers them.

EYAS v0.8.22-beta — The door opens

Choose a tag to compare

@eyssen eyssen released this 03 Sep 20:20

[0.8.22-beta] - 2026-09-03 — The door opens

A knock got you native or Docker. The native path then died after a
successful-looking bun install: Vite could not find @vitejs/plugin-react,
because the UI is a nested package the root install never touches.

The door opens now: nested src/web deps, one retry if a package is still
missing, unlinked local editors skipped, and a public clone still builds a UI.

One-line installer

  • Native install now installs the nested frontend package. src/web has
    its own package.json and is not a bun workspace, so root bun install
    never put Vite or @vitejs/plugin-react on disk. The next step — bunx vite build — then died with Cannot find package '@vitejs/plugin-react' after
    a successful-looking root install. The installer (and eyas start /
    eyas update apply / the Docker image) now bun installs src/web first,
    retries the UI build once if a package is still missing, and skips link:
    deps that are not bun link-ed on this machine (the Saker editor) so a
    public clone still produces a UI.

EYAS v0.8.21-beta — A knock at the door

Choose a tag to compare

@eyssen eyssen released this 02 Sep 17:15

The one-line installer used to guess. Docker on PATH meant Docker, even when
the daemon was down and Bun was missing — and it asked for an admin account,
an AI provider and an agent name that the setup wizard would ask again.

It now knocks first: native or Docker every time, an offer to install (or
start) whatever is missing, and the wizard left to the wizard. GitHub
Sponsors is on the repository, the README and the landing page.

One-line installer

  • Always asks native vs Docker, even when both runtimes are already present.
    Missing git, Bun, or Docker is offered for install (and started, if Docker
    Desktop is installed but the daemon is down) instead of silently picking a
    method and failing at docker compose up.
  • Setup-wizard fields left to the wizard. The installer no longer collects
    admin user/password, AI provider, API key, language, or agent name — those
    belong to first boot in the browser. Directory and HTTP port stay.
  • Banner: original ANSI Shadow EYAS, with the eYssen slant wordmark above it.

Sponsors

  • GitHub Sponsors is wired through the mirror. .github/FUNDING.yml lives
    in this repository so the next orphan snapshot of public main keeps the
    Sponsor button. Tiers, the $1,000/month model-bill goal, and the full list
    are in SPONSORS.md; the README, the landing page and the docs index in
    all six languages point there. Sponsorship is not a support contract.

EYAS v0.8.20-beta — A front door, and its locks

Choose a tag to compare

@eyssen eyssen released this 01 Sep 17:33

EYAS had no public face: the overview page lived in the repository, the
documentation was only reachable from a running instance, and the README still
described a smaller project than the one in the tree.

It has one now — https://eyssen.github.io/eyas/ — and turning the repository's
scanners on for the first time found real defects behind it, which this release
fixes.

A public site

  • The landing page and the docs are published together. / serves the
    product overview, /docs/<lang>/ the 392-page documentation in all six
    languages. One build script assembles both, and CI runs the same script
    rather than a copy of its logic.
  • The landing page speaks six languages, not two. Every string exists per
    language in the page itself, so it stays a single self-contained file. The
    language is chosen before first paint from ?lang=, the last choice, or the
    browser, and English renders without JavaScript.
  • A beta callout and an installation panel replaced a mock terminal line.
    The callout asks for reports and links the issue tracker; the panel carries
    the three real install routes and a link to the getting-started guide that
    follows the language switch.

Security fixes

  • Email header injection in both address formatters. A display name was
    written into a header without removing CR/LF, so a name carrying
    \r\nBcc: … added a recipient. The quote was escaped but the backslash was
    not, so a name ending in one escaped its own closing quote.
  • The CLI MCP bridge secret came from Math.random() plus a timestamp.
    That secret authenticates bridge sessions; it now takes 24 bytes from the
    CSPRNG and encodes no clock.
  • Notification event patterns are globs, but only the dot was escaped
    before the star was expanded, leaving every other regex metacharacter live:
    board.(task).* matched board.task.assigned through a regex group.
  • Generated skill frontmatter escaped quotes but not backslashes, the
    key-injection its own guard was written to prevent.
  • The research HTML stripper missed </script > and </script foo>, kept
    the contents of comments, and kept a tag left unterminated by truncation.
  • Four advisories patched: drizzle-orm (SQL injection via improperly
    escaped identifiers), nodemailer, @anthropic-ai/sdk, and sharp in the docs
    package. Measured against a baseline: the suite fails identically before and
    after, so the upgrades change nothing else.

Around the repository

  • A security policy, a contributing guide and issue templates. Private
    vulnerability reporting is on, and SECURITY.md now names that channel
    instead of leaving reporters with a public issue. CONTRIBUTING.md leads
    with what a contributor cannot guess: this repository is a mirror.
  • The README's figures are measured, not estimated. 57 modules, 228 skills,
    7,200+ tests across 747 files, twelve provider submodules including the CLI
    engines that need no API key, and the six setup-wizard steps the code
    actually registers.

EYAS v0.8.19-beta — Related prior work

Choose a tag to compare

@eyssen eyssen released this 31 Aug 19:09

[0.8.19-beta] - 2026-08-31 — Related prior work

A new chat used to know who you are and how you work. It did not know that
this task is the follow-up to one you already finished. That knowledge lived
in a provider's own memory — and vanished when the provider changed.

EYAS now searches its own store on every turn: past user and assistant
messages, plus the vault, plus episodic notes. A small related-work block
lands in the prompt without the model having to call a tool.

What the last job was

  • Past conversation text is searchable. User and assistant messages get
    an FTS index (diacritics folded, bodies clipped). Deleted threads stay out.
    Backfill is chunked so start does not wait on history.
  • search_memory includes those messages by default. Hits are labelled
    conversation. The current thread is excluded. Other projects stay out
    unless scope=all. HTTP search stays unfiltered.
  • A related-work block is injected on every turn. The current message is
    the query. Vault, episodic, and conversation hits are one-liners. Follow-up
    is still search_memory for a body. Resume after a skill proposal uses the
    stored user message, not an empty body.

What stays out of the block

  • An echo of this turn is not prior work. A sibling conversation that
    only restates the question is dropped so the actual earlier decision can
    rank.
  • Weak vault glue is not a hit. Notes that only share short words
    (durable, setup, from) stay out. Codes and distinctive names
    (IAP, Cloudflare, 1010) still match.
  • Conversation hits keep two reserved slots. A full vault cannot push
    the last related thread off the block.

Handbook: Memory and Tools — six languages.

EYAS v0.8.18-beta — A desk of its own

Choose a tag to compare

@eyssen eyssen released this 31 Aug 10:23

Memory fills itself. Hands can make a still, a clip, a form. What was still
missing is the desk: a place the work belongs so a new chat does not dump onto
general-general, sibling projects share family facts without seeing each
other's notes, and a long tool-using session can stay in the web UI instead of
fleeing back to a TUI.

The desk is a project. A type names the family; a project is one instance of
that family. Folders, connections, memory, and wiki pages inherit from type to
project to conversation. EYAS is a general product — the paths, the tickets,
the clients live on the instance.

The project is the room

  • New conversations pick a project grouped by type. Domain work no longer
    silently lands on general-general. New projects inherit type sources and
    directories when omitted; an empty project prompt inherits the type brief
    instead of copying it. Instance projects are not seeded.
  • A domain type ships with a generic operating brief. Indexer, local vs
    remote writes, domain notes vs project notes. The type is a behaviour, not
    a tenant.
  • The project form prompt actually reaches the model. The form wrote DB
    prompts; the assembler read AGENTS.md; the two never met. The loader now
    takes a non-empty DB prompt first (file fallback), applies + / empty /
    override, and a form save materializes AGENTS.md as a derived dump.
  • Tags stay a board filter. They render as one tags: line in the prompt
    suffix so a swap does not change the project-context cache prefix. Category
    names are documented, not seeded.

Memory that knows which desk it is on

  • Type-level domain notes rank with the active project. A kind=domain
    note is for the conversation's project type, so sibling projects share
    family facts without seeing each other's project notes. general-general
    has no type notes. Capture stays in the EYAS vault.
  • search_memory defaults to the current project and type. Other
    projects stay out unless the model passes scope=all. HTTP vault search
    stays unfiltered.
  • Named working directories pin from type to conversation. Types and
    projects store optional named folders (name + absolute path). An empty
    project list falls back to the type. New conversations pin that list the
    way they pin search sources; file tools stay inside the pinned roots, not
    the EYAS checkout. The conversation fields bar picks the primary workspace.
  • Catalog connections pin on the project. Ticket tools use
    ticketConnectionId; other tools use defaultConnectionId. An explicit
    connectionId wins. Missing project connections still fall back to the
    global secrets.

The wiki writes the project's own pages

  • Closed board cards write ticket-<id> on the project's wiki.
    Team-session findings and decisions write decision-<id> there instead of
    the vault when the conversation has a project. Human saves take ownership.
    general-general gets no page.
  • Wiki writes stay off until a project opts in to closed tickets and/or
    team decisions. Ticket pages default to title-only instead of the
    transcript.

A conversation that can stay in the web UI

  • Long tool-using chats keep a trace. What is running, short args, a
    file-edit diff. Stop aborts the server-side run. Plan first parks a written
    plan for approval before tools run.
  • git status and git diff skip approval. CLI providers sent those
    read-only commands through Bash/run_command, which is red on every call.
    When the argv matches the dedicated tools, the gate allows them as green.
    Arbitrary shell, write-git, and metacharacters stay refused.

Skills, Telegram, and a copy of what you already wrote

  • Extra skill and persona roots import without host Claude config.
    Instance overlay lists markdown directories. Imported files win on id
    collision and appear on the Agents page. Isolation stays on:
    settingSources stay empty; host MEMORY.md is not loaded.
  • Telegram /new and /start start a fresh thread. Paired DMs already
    created one conversation per sender. The slash command drops that mapping
    so the next message does not go to the model. When a yellow or red tool
    waits, the paired chat gets an Approve/Deny ping. Raw tool args stay off
    the ping.
  • Data port copies, it does not mount. Scan → review → import writes
    markdown into the EYAS vault. The source path is not read again.
    Undeclared notes get kind: reference, never user. MEMORY.md indexes
    and claude-sessions transcripts stay out even if everything is selected.
    A home scan stays in assistant folders and Documents — GitHub and other
    source trees are not walked. Grok memory files that symlink into
    ai-memory count as that vault; they are not imported twice. Classify and
    transform prompts assume the user pointed at the wrong (too wide) folder.

Ops is Kubernetes, not a cloud overlay

  • The ops module and Helm chart are general Kubernetes. Cloud-provider
    values stay on the instance; bundled OCI skills are unchanged. The OCI OKE
    overlay is gone.

The product is not a tenant

  • EYAS is a general product; this operator is one tenant. Capture prompt
    and tests no longer name shop clients, modules, or tickets. Instance data
    stays on the machine.

Handbook: conversations (working folders, tool trace, plan first), projects,
skills import roots, Telegram /new + Approve/Deny, git remap, and Data
port — six languages.

Known issues

  • Plan first still wants a live trial on a long product conversation. The
    four surfaces (trace, diff, stop, plan) are in; a real long run on this
    machine has not been the gate.

EYAS v0.8.17-beta — Hands that make things

Choose a tag to compare

@eyssen eyssen released this 29 Aug 21:14

An agent that can remember still cannot show you the thing. This wave is the
hands: generate a still or a clip through a vendor you already pay, render a
title card on this machine, cut footage from a transcript, and fill a form in
a browser that is not the one you use every day.

Media is SaaS prompt-to-pixel. Studio is local production. The browser is
EYAS's own Chromium, plus optional sidecars when the work needs the Chrome
you already logged into. Recordly records the screen; it is AGPL, so it is a
catalogue card, never a bundled engine.

Every lane fail-closes with a remedy. Missing Node, missing FFmpeg, missing
CLI, missing Chromium: the tool says so. None of them silently disable the
sandbox. None of them vendor a third-party LLM. The model stays EYAS's.

Media is SaaS, and none of it is default

  • Agents generate, upscale, and wait through five media_* tools.
    media_catalog, media_generate, media_wait, media_balance,
    media_history. The vendor is a routing choice, not a tool dump.
  • Magnific, Higgsfield, and fal are optional backends. None is default;
    several can run at once. Zero connected providers is an empty, fail-closed
    state — never mock pixels. Magnific and Higgsfield sign in with OAuth; fal
    takes an API key. The Media page and the handbook compare them on the
    criteria that actually matter here: strength, sign-in, credits, and file
    lifetime.
  • Completed files land in Documents and on the producing turn. Vendor CDN
    URLs expire — Higgsfield's in about seven days — so ingest copies the bytes
    locally (up to 200 MB, no JPEG recompress) rather than leaving a link that
    will 404.
  • Raw vendor MCP tools stay off. Turning them on dumps mcp_magnific_* /
    mcp_higgsfield_* / mcp_fal_* onto the agent and skips ingest. Leave them
    off unless you are debugging.
  • Routing is per kind, with an optional budget. Default / fallback cover
    an outage; "also run on" fans the same prompt out and doubles credits.
    Daily and monthly caps fail before the vendor is called.

Studio is local production

  • Studio is a new category (Content → Studio, /studio), not Media.
    Local engines live here; Media stays hosted prompt-to-pixel.
  • First engine: Hyperframes. The agent authors HTML compositions and
    renders deterministic MP4s through six hyperframes_* tools. Node.js 22+,
    FFmpeg, and the Hyperframes CLI are required; missing any of them fail
    closed with a remedy. Hyperframes downloads its own chrome-headless-shell
    — never EYAS_CHROMIUM_PATH, never --no-sandbox. Output lands in
    Documents and on the producing turn. The CLI is Apache-2.0 and is not
    vendored.
  • Second engine: Video Use. Transcript-first footage cuts (videouse_*).
    EYAS reimplements the open-source hard rules in TypeScript (MIT) rather
    than vendoring librosa or Manim. FFmpeg on this machine; ElevenLabs Scribe
    is optional for transcription. Confirm a cut strategy before writing
    ranges. Overlays can be Hyperframes renders. Not Media, not Hyperframes.

Recordly is a companion, not a product

  • Recordly is an AGPL desktop screen recorder. It is not a Studio engine
    and it is not bundled. Catalogue card: Extensions → Third-Party
    (recordly). Manual install only (GitHub / Setup guide);
    POST /extensions/recordly/install is refused. Export MP4/GIF in Recordly,
    then attach in Documents. Skill: config/skills/integrations/recordly.md.

A browser of its own

  • Headless browser_* tools share the design-print Chromium. Numbered
    interactive indexes from browser_snapshot (click/fill by index; CSS is
    the fallback). Indexes and snapshotId die on navigation — snapshot
    again. Same SSRF, same 5-minute process.
  • The session is a real browser, not a one-shot page. Tabs, back, wait,
    hover, select, dialog, file upload, page evaluate, download → Documents,
    Playwright storageState, and an EYAS-owned userDataDir
    (data/browser/profile / EYAS_BROWSER_USER_DATA_DIR). The daily Chrome
    profile is rejected first (Chrome 136+ blocks Default-profile CDP).
  • Action cache without Stagehand. A successful browser_click /
    browser_fill with intent stores a durable CSS/role locator in vault
    JSON (projects/<id>/ or procedural/browser-action-cache.json).
    browser_replay reuses it on the same origin without an LLM or a snapshot
    index. Fill values and TOTP seeds are never cached.
  • browser_totp (yellow) reads the seed from Secrets or macOS Keychain
    and returns only the 6-digit code for browser_fill. The seed never
    leaves that call.
  • Snapshot and locator scripts run as IIFEs. Playwright evaluate of a
    string does not call () => sources, so indexes and cached locators would
    otherwise stay empty. That was a live miss, not a design choice.

Sidecars for the Chrome you already logged into

  • Agent Browser is the recommended sidecar (Vercel, Apache-2.0).
    EYAS_AGENT_BROWSER_BIN → PATH, fail-closed doctor
    (doctor --offline --quick --json). Tools agent_browser_status /
    agent_browser_run (argv or batch JSON, @e1 refs). MCP catalog
    agent-browser mcp --tools core,statemcp_agent_browser_*. EYAS-owned
    --profile (data/browser/agent-browser/profile). Daily Chrome /
    --profile Default / --auto-connect / chat / --tools all refused.
    Rust is not vendored. AI_GATEWAY_* is stripped on spawn. The LLM stays
    the EYAS model module.
  • Python Browser Use remains as a legacy sidecar. Extra module, MIT CLI
    wrapper, telemetry off, Cloud API key stripped unless turned on.
    browser_use_status / browser_use_exec. UI /browser-use. Prefer Agent
    Browser when that card is Ready.
  • Playwright MCP is a Connections catalog row (playwright-mcp) plus
    MCP catalog sidecar (npx @playwright/mcp@latest --isolated). Agent tools
    arrive through the existing MCP bridge (mcp_playwright_*). Doctor is
    fail-closed (Node 18+, npx). Telemetry off. --no-sandbox is stripped and
    refused. The Python browser-use MCP is rejected (it wants an LLM key and
    retry_with_browser_use_agent). Live tab: Playwright MCP Bridge extension
    (--extension).
  • Chrome DevTools MCP (Google, Apache-2.0) is a separate coding/debug
    lane — Connections type chrome-devtools-mcp plus MCP catalog
    (npx chrome-devtools-mcp@latest --isolated). Console, network,
    Lighthouse, WebMCP. Not form-filling (browser_* stays the form
    lane). Tools arrive as mcp_chrome-devtools_*. WebMCP
    (list_webmcp_tools / execute_webmcp_tool) only if the sidecar
    advertises them. --autoConnect and the daily Chrome profile refused.
    --no-sandbox stripped. Doctor fail-closed.

MCP that can reach a hosted server

  • The MCP client speaks Streamable HTTP and OAuth. Hosted creative
    servers connect without a custom adapter per vendor. That is what made
    Magnific and Higgsfield possible as Media backends rather than one-off
    integrations.

The handbook

  • The Starlight docs are rewritten around a first-hour path, with a
    purpose opening on every chapter, the missing admin surfaces filled in,
    and in-app ? help wired on every remaining product page. Six languages.

A skill proposal can turn the skill off

  • The third button is global. "Not this time" still only covers this
    conversation. "Turn it off" declines here and disables the skill, so it
    will not match again until someone turns it back on in Skills. The turn
    then resumes the same way as a decline. Only owner and admin see the
    button — a user who can talk but cannot manage skills still has yes and
    no.

Fixed along the way

  • The kanban context stripe used the conversation's lifetime token total
    against a hardcoded 128k window.
    Opening the same Grok card painted
    green (composed size / 500k) while the board painted red (cumulative /
    128k). Occupancy inputs now come from one function
    (loadConversationContext): latest composition estimated_tokens over
    the model's real window. The board, the conversation GET, and the
    end-of-turn frame all attach those fields; both stripes only paint them.
    A card with no composition stays blank rather than inventing a reading
    from tokensUsed.
  • The open chat overflowed the viewport by about a centimetre. The page
    sized itself with 100vh minus the top bar and ignored the status bar
    plus leftover main padding. New messages then called scrollIntoView,
    which scrolled every overflow ancestor and hid the header. The pane now
    fills the chrome remainder, and only the message list scrolls.
  • The template picker is opaque. It already sits above the page (the
    header is a stacking context), but it was still a glass-card — 3%
    white in dark mode — so the page bled through the names. It now uses
    the same solid popover surface as the notification panel and the
    user menu.

Known issues

  • The skill matcher still scores badly. The third button means a bad
    match now costs a click and then stops being offered, but the scoring
    itself is untouched — google-drive-integration can still light up for
    "what time is it".
  • Action-cache locators are CSS/role, not visual. A redesigned page on
    the same origin will miss. That is the Stagehand idea without the
    library, and a restyle is a cache miss rather than a silent click on
    the wrong control.
  • No sidecar binary is in the tree. Agent Browser, Playwright MCP,
    Chrome DevTools MCP, Hyperframes CLI, FFmpeg, and the Chromium used by
    browser_* are resolved or they say they are missing. A VPS without
    them is a working EYAS that cannot click or render until they are
    installed.

EYAS v0.8.16-beta — A memory of its own

Choose a tag to compare

@eyssen eyssen released this 28 Aug 14:03

What EYAS knows now comes from what EYAS remembers. The vault writes itself: a
durable fact stated in any conversation — on any model — becomes a note without
anyone asking, and the same note is what every later conversation reads back.
Closing that loop meant winning an argument with the host machine: conversations
on the Claude Code CLI no longer read the owner’s own Claude config and memory,
because an assistant that can see a second memory will happily report a fact
“already recorded” that its own vault has never held.

Every fix here was found the same way: a live test, a measurement table, and a
root cause chased until it reproduced deterministically. The capture run ledger
(memory_capture_runs) is why each diagnosis took minutes instead of days.

Memory that fills itself — and knows where it came from

  • A durable fact learned in a conversation is written to the vault without
    anyone asking.
    Capture runs on every conversation, globally, on by default;
    memory.capture.enabled in config/default.yaml switches it off. A small
    model call attaches to a qualifying turn AFTER the reply has been delivered —
    never in its critical path — and a capture that fails is a missing note, never
    a failed conversation.
  • The extractor reaches a model that can answer it, whatever the instance
    runs.
    Capture assumes nothing about what is installed — most instances are a
    VPS or a pod with no room for a local model, and many have nothing but a host
    CLI. Resolution is a ladder over what is actually enabled: the heartbeat
    tier, but only when this instance really has the provider that tier names and
    it is not a CLI; otherwise the first enabled, registered provider that is not a
    host CLI whose model can be named; otherwise no pin at all, letting the gateway
    fall back the way it does for any unpinned request — anthropic when registered,
    else the first registered provider, a CLI included — because a capture that is
    attempted is measured and one that is skipped is invisible. The rung is
    logged. The routing tier is configuration, so it can name a provider this box
    does not have — and it did: the pin was silently dropped, and a CLI provider's
    complete(), which runs a full agent turn, answered the extraction prompt in
    prose. One unparsable row per qualifying turn, and never a note.
    Three repairs meet the CLI there as well: the parser lifts the first balanced
    {…} object out of surrounding chatter (string-aware, so a brace inside a
    value does not close it), the prompt says the reply is the object and nothing
    else — no commentary, no fence, no tool calls — and the unusable-output
    warning now carries the reply's length and its first 200 characters, so the
    next diagnosis is not blind.
  • The extraction runs in an isolated context, so a CLI's own loaded memory
    cannot pre-empt EYAS's.
    A request can now ask to be isolated — no
    filesystem settings, no CLI-native memory or config, no bridged tools, a
    single turn — and Claude Code honours it whatever its loadClaudeMd setting
    says. Without it the extraction call loaded the owner's ~/.claude memory,
    which another tool had already written the fact into: the model read it there,
    reported it known, and EYAS's vault — the one place it was NOT recorded —
    stayed empty. No prompt rule wins against a whole loaded memory system. The
    ladder now prefers a CLI that advertises the capability over one that does
    not, choosing on the CAPABILITY and never on a provider name; grok CLI's
    protocol offers no such switch, so it says so rather than pretending.
  • The extractor believes the notes on file, not the assistant's account of
    them.
    A retest caught it returning a healthy-empty batch on a fact-dense
    exchange: the reply had said "I've already saved that to memory" — it had not,
    the CLI narrated a tool call that never ran — and the extractor honoured the
    do-not-restate rule against that claim while its own EXISTING NOTES section
    was empty. Coverage is now judged ONLY against EXISTING NOTES, and the prompt
    says in as many words that an assistant's statement about saving is narration,
    not evidence. What a model concludes cannot be asserted in a test; that the
    instruction ships is pinned by one.
  • Every capture run records which model produced it. memory_capture_runs
    gains a provider column holding provider/model — NULL when no model was
    called, because a gate skip spends nothing. An instance with several providers
    could already count its unparsable runs but could not say which model was
    failing to answer in JSON, and answering that took a live retest once already.
  • Memory is EYAS's own, in both directions. The mandatory memory rule named
    no tool and only one direction ("update memory when you learn something new"),
    which a CLI-backed agent reads as its own machine-global convention. It now
    names search_memory for recall and save_memory for recording, states that
    EYAS's memory is the only memory, and forbids writing to a machine-global
    memory directory, an ai-memory or Obsidian vault, ~/.claude or ~/.grok.
    Because a rule is guidance, the deterministic gate denies the same paths to
    every file-writing tool, matching the path fields of a call and never its
    content. Read, Grep and Glob stay open — the data-port importer exists
    to carry exactly those notes into EYAS — but the shell is blocked in both
    directions, because cat is one character from >> and no reading of a
    command string proves which one it is. The denied set is narrow on purpose:
    an ai-memory directory, a home-anchored ~/.claude or ~/.grok, and a
    memory/ directory under either. A workspace's own .claude/settings.json
    and .claude/agents/* pass, since that is project config, not memory.
    MEMORY.md is deliberately not on the list: the gate is handed a path, not a
    workspace root, and cannot tell the owner's global index from a repository's
    own docs/MEMORY.md.
  • The gate is structural, not lexical. One length check, minUserChars
    (default 40), counted in Unicode code points so an accented message gates
    identically to an ASCII one of the same length. No keyword list in any
    language: this product ships in six, and the repository has already paid twice
    for that class of bug — JavaScript's \b is ASCII-only, so \bűrlap never
    matched "Űrlapelemek", and Hungarian lengthens the stem vowel, so "minta" is
    not a prefix of "minták". Deciding what a sentence MEANS is the model's half
    of the design.
  • The runaway guard counts model spend, not turns. maxPerConversation
    (default 20) is consumed by a successful extraction, an unparsable reply and
    an errored call — never by a too-short skip. Counting skips meant twenty short
    acknowledgements ("ok", "mehet") exhausted the budget without a single model
    call, and the next fact-rich turn was refused. Every outcome still writes its
    row; only what the budget is spent on changed.
  • 0–2 candidate notes against a strict schema. user (who the owner is),
    feedback (how to work — invalid unless it carries both a Why and a How to
    apply), project (a durable fact about the conversation's project) and
    reference. When the conversation has no real project, a project candidate
    is REJECTED by the schema rather than hidden from the model — and because the
    refinement runs per note inside one array parse, a single stray project
    candidate fails the whole batch, which is then dropped and recorded as
    unparsable. {"notes":[]} is the common and correct answer, and the prompt
    says so.
  • A repeated fact reinforces one note instead of spawning a second.
    Deduplication is word-set overlap against the existing summary rather than
    string equality, because a reinforcement rephrases ("Answers in Hungarian" →
    "Answers in Hungarian, always"); a match appends a dated bullet under
    ## History and never overwrites what was there.
  • Sanitised before it touches disk, not when it is read. The privacy module
    runs over the summary and the body before the vault write, because a read-time
    redaction would leave the raw text in the file and in the FTS index built from
    it.
  • A project's facts rank first inside that project and are invisible outside
    it.
    The always-on index ranks global user and feedback first, then the
    ACTIVE project's project notes, then reference; another project's notes
    never appear at all. Project notes live in projects/<project-id>/ with a
    project frontmatter field frozen at capture, so re-scoping a note is a
    deliberate act rather than a side effect of the next update.
  • The seed catch-all project is not a project identity. Every conversation
    defaults into general-general, so treating it as a real project would file
    the owner's general facts under it and hide them everywhere else.
    The rule lives in one FUNCTION, effectiveProjectId(), and every entry point
    calls it — capture, both recall paths, and the memory tools — so the write
    half and the read half cannot disagree about what counts as a project.
  • Every note records where it came from. memory_note_links names the
    conversation that wrote a note or later reinforced it, in the same multi-owner
    shape as design_links and document_links, and episodic memories now carry
    conversation_id and project_id.
  • Every outcome that reached the gate writes a memory_capture_runs row
    skips with their reason, extractions with the kinds they wrote. Two silences
    are deliberate: capture switched off writes nothing at all, and a background
    run with no assistant text to read never reaches the gate, because a skip row
    per autonomous run would only inflate the diagnostics it exists to keep
    honest. One silence is a known gap rather than a choice: a God Mode turn
    returns its own stream before the post-turn block and so captures nothing —
    no note, no row....
Read more

EYAS v0.8.15-beta — Designs your agents follow

Choose a tag to compare

@eyssen eyssen released this 27 Aug 11:06

A design stops being a picture you keep somewhere else and becomes something the
work follows. Multi-artboard canvases in the Claude Design format, rendered by
EYAS's own MIT runtime: create one, import one, edit it by hand or on the canvas
or by asking, version every change through a single validator, attach it to a
conversation or a project, and export it to PNG and PDF.

Getting an agent to actually use one took longer than building it, and that is
most of what follows. The tool inventory was clipped to 15% of itself. A matched
skill emptied the tool list. Nothing that was ever written to memory could reach
a prompt, and nothing wrote to memory either. Each was invisible on its own, and
together they were why a design sat attached to a conversation and changed
nothing.

Design (F2)

  • A "Design" menu item. Multi-artboard canvases on a pan-and-zoom surface, in
    the Claude Design container format: <Name>.dc.html artboards, a canvas.json
    layout manifest with pages and sticky notes, and images stored as bare base64
    under their filename. A canvas exported here re-seeds there, and one published
    there imports and renders here.
  • EYAS's own runtime. The hosting platform's editor is a ~2.4 MB precompiled
    payload under a licence this repository cannot redistribute, so the Design
    Components dialect is implemented from scratch as MIT code: dotted-path holes,
    <sc-for>, <sc-if>, <dc-import>, JSX-camelCase event binding, and real
    execution of the artboard's class Component extends DCLogic — so clickable
    prototypes, variant switches and selection state work.
  • The isolation that makes executing AI-authored JavaScript acceptable: a
    srcdoc iframe with sandbox="allow-scripts" and never allow-same-origin, a
    CSP inside the srcdoc with connect-src 'none', and Google Fonts as the only
    external origin. No route serves an artboard as a document — the render endpoint
    returns the srcdoc and the sandbox value in one JSON payload so they cannot
    drift apart. The runtime moves <helmet> content into <head> but drops any
    <script> there.
  • A validator gate on every write. Hand edit, import or AI result, all of it
    is checked before it can become a version: an artboard with no <x-dc> root, a
    layout entry naming a file that is not there, an image reference with nothing
    behind it, a case-insensitive artboard-stem collision, a launch pointing at
    nothing, a stray top-level key in canvas.json, and the }} ? ternary inside a
    style attribute that the format drops silently. A rejected edit leaves the
    previous version byte-identical.
  • One AI pipeline, not one per vendor. The same prompt and the same gate
    whatever the provider; only the executor tier varies — whole-canvas rewrite for
    small canvases, per-artboard iteration for large ones, both on plain text
    completion so a local model works too. A failed attempt is retried once with the
    validator's own output as the feedback.
  • Agents get design_list, design_read, design_write and design_create,
    all category: 'custom' so they survive the MCP bridge and exist on the CLI
    providers. A design linked to a conversation travels with every turn as a
    design-context section; a large canvas is summarised and the agent fetches what
    it needs.
  • Import from a published canvas page, export as raw files, as a portable canvas
    document, or as a standalone HTML page that opens and prints anywhere.

WYSIWYG (F4)

  • Click an element, change it in a panel, and it lands in the source.
    Typography, colour, box, border, radius and layout, including grid tracks that
    round-trip through repeat(N, minmax(0, 1fr)). Text is editable in place
    unless it is bound to a {{hole}}, which the panel says rather than silently
    overwriting the binding.
  • The design that this forced. The artboard iframe has no
    allow-same-origin, so the app cannot reach its DOM. Rather than parse and
    mutate the template in the app — which would need a server-side DOM and a
    rendered-node-to-source mapping — the runtime owns the mutation: it stamps
    every template element with a stable index at parse time, applies the edit to
    its own copy, re-serialises, and posts the finished template back. The app
    splices it into the .dc.html file with the head marker, helmet and logic
    script preserved byte-for-byte.
  • Style edits keep {{holes}} in declarations they did not touch. The patch
    works on the style attribute as text, declaration by declaration; a DOM style
    API would have destroyed the binding silently.
  • The splice refuses anything that does not read back as what was written.
    Checking that the result merely parses is too weak: a </x-dc> inside a
    template closes the element early, and the file still parses — into a
    truncated artboard.
  • Messages are attributed to the artboard's own frame before they are acted
    on, and validated against a strict shape. They come from an opaque origin and
    are exactly as untrusted as the artboard.
  • Tweak chips from data-props re-render live; pinning one writes it back as
    the artboard's declared default.
  • Undo/redo per artboard with Cmd/Ctrl+Z, and one version per explicit save
    rather than one per keystroke.
  • The runtime defaults to interact, not edit: the canvas shows working
    prototypes, and an artboard marked is_interactive never enters edit mode.

Print, PDF and PNG (F5)

  • A design canvas exports as PNG and PDF. One artboard at 1× or 2×, one
    artboard as a PDF at its own natural size, or the whole canvas as a single
    multi-page PDF. print: 'fixed' artboards come out as one page at exactly
    their frame — a CSS pixel is 1/96 inch, so the size passes through without a
    conversion; print: 'flow' artboards paginate onto A4 or Letter.
  • The browser fact that shaped it: Chromium will not paginate inside an
    iframe.
    It lays a frame out as a fixed box and clips the overflow, so a
    flowing artboard printed in the preview's sandboxed iframe would come out as
    one truncated page. Every artboard is therefore rendered as its own top-level
    document, and a canvas PDF is those PDFs concatenated with pdf-lib. That is
    the better answer anyway: each page keeps its natural size, a flowing report
    still paginates, and one artboard's <helmet> CSS cannot leak into the next.
  • Losing the sandbox attribute meant replacing it with three things. Every
    print page opens in a throwaway browser context with no cookies and an opaque
    origin; every request is aborted in the browser process except the two Google
    Fonts origins the format admits; and the page carries the same ARTBOARD_CSP
    as the preview, imported rather than re-typed so the two cannot drift.
  • A broken artboard is refused, not exported blank. Both failure layers are
    checked — the mount throwing, and the runtime's own marker when a component
    constructor or renderVals() throws. A PDF whose only content is
    "renderVals() threw: …" is worse than an error message.
  • The browser is optional and says so. playwright-core is a real
    dependency (Apache-2.0, no postinstall, no runtime dependencies of its own);
    the ~150 MB browser binary is not. It is resolved from EYAS_CHROMIUM_PATH,
    then Playwright's own registry, then known system paths, and when there is
    none /api/v1/designs/print-status answers available: false with the remedy
    and the UI disables the buttons. The Docker image installs Chromium and the
    fonts a headless browser needs; deleting that layer costs ~350 MB and switches
    these two features off cleanly.
  • The Chromium sandbox is never disabled automatically. A sandbox failure
    does not fall back to --no-sandbox: the renderer is the process that
    executes AI-authored artboard JavaScript, and turning a deployment problem
    into a silent security downgrade there is not a trade-off worth making
    quietly. It takes an explicit EYAS_CHROMIUM_NO_SANDBOX=1, and the error
    message says so.
  • playwright is gone as a shimmed optional module. The browser tools and
    the print pipeline now share one resolver, so there is a single place that
    knows how to find a Chromium, and the SSRF predicates moved to
    shared/net-guard where the headless browser can apply them per request.
  • New dependencies: playwright-core (Apache-2.0) and pdf-lib (MIT). The
    latter is not in the design spec's dependency list — it was added because
    concatenating per-artboard PDFs is what makes a mixed canvas correct instead
    of compromised onto one uniform paper.

Canvas usability, and one field taken back out

  • The canvas takes a scroll wheel. Plain scroll pans, Shift scrolls
    sideways, Ctrl/⌘ + scroll zooms — anchored on the pointer, so the thing under
    the cursor stays under the cursor. The listener is attached natively with
    passive: false, because React routes onWheel through a passive root
    listener where preventDefault() is ignored and Ctrl+wheel zooms the browser
    instead.
  • An artboard can be opened on its own. A control on its title row (or a
    double-click on the title) fits it to the viewport; Esc returns to the
    previous view. This is what finally makes artboardEntry.expand do something:
    fit shrinks the whole artboard to the viewport, fill widens the frame to
    the viewport at natural scale and lets it scroll.
  • "Fit" now fits. It measures the page's actual bounding box — artboards and
    annotations — instead of resetting to a hardcoded 60% at 40,40.
  • Nothing was put over the frames. An overlay would make the entire surface
    pannable, but it would also silence every is_interactive prototype until you
    clicked into it. Dragging the background works, so opening an artboard is an
    explicit control rather than a gesture over a frame.
  • A design can be renamed in place from its header.
  • designs.status is gone. It rendered a badge in the list and ...
Read more

EYAS v0.8.14-beta — Shape your own landing page

Choose a tag to compare

@eyssen eyssen released this 26 Aug 08:01

The landing page stops being something you're handed and becomes something you shape. The fixed
dashboard is gone; a nine-tile grid takes its place, and the extension point it's built on had
been sitting declared and unused since before this feature existed.

Home

  • Widget grid at /: drag to move, drag a corner to resize, remove a tile, or open a drawer
    and add one — including tiles from disabled modules, shown dimmed so you can see what could be
    there. Layout is per user and per breakpoint (lg/md/sm arrange independently) and saves
    itself ~800ms after you stop dragging.
  • Factory nine: Pulse, Attention, Running agents, Schedule, Conversations, Board, Briefing,
    Cost, System. No stored layout means the factory layout applies — so a later release that adds
    a tile reaches everyone who never customised, automatically. A customised user is instead
    offered any newly-added factory widgets ("Add" / "No thanks"), never handed them silently.
  • Tiles fail alone. A per-tile error boundary means a broken module shows "Unavailable" on
    its own tile; the other eight keep working.
  • Disabled-module tiles survive. A tile from a disabled module drops out of the rendered grid
    but keeps its stored position and config, and returns intact when the module is re-enabled.
  • The widget extension point is alive. FrontendManifest.widgets / WidgetRegistration
    (src/core/types.ts) were declared and typed but nothing populated or read them. The new
    home module now collects them from the module loader and serves them at
    GET /api/v1/home/widgets; a contract test forbids a manifest-declared widget with no frontend
    component, or the reverse.
  • Setup requests collapse: SetupRecommendationsCard fired 10 requests on every open
    (providers, projects, prompts, agents, search sources, backups, ingress, autonomy, vault,
    communication). It now calls one server-cached aggregate, GET /api/v1/home/setup-status.
  • Dead code removed: AutonomyNudgeCard — never imported, never rendered — is deleted, along
    with the fixed dashboard page it used to live on.
  • Handbook: daily/home documents the grid, edit mode, add/remove/resize, restoring the factory
    layout, and the new-widget offer, in en, hu, de, es, fr and tlh.

Security

  • The home endpoints were never authenticated. home created its routes in onRegister, which
    runs for every module before any module's onStart — where auth mounts its middleware. Hono
    composes middleware in registration order, so nothing auth registered could ever apply: every
    /api/v1/home/* request failed with 401 and the UI bounced the user straight back to the login
    screen. Routes now mount in onStart and home declares auth as a dependency, which forces the
    order through the loader's topological sort. CSRF pairing added for the mutating layout routes.
  • A contract test now asserts what a route test structurally cannot. Every route test in this
    repo installs its own c.set('userId', …), i.e. simulates a world where auth already ran — which
    is why thousands of green tests never saw the above. api-auth-coverage.contract.test.ts runs the
    real dependency resolver over the real registration order and fails if a module that mounts routes
    lands before auth, or if an /api/v1 segment is neither covered nor on a named public list.

Fixed

  • Each breakpoint keeps its own arrangement. The grid loaded the layout once at lg and never
    reloaded, so crossing a width threshold made the library derive an md layout from the lg one
    and save it — creating a stored row for someone who never customised (which stops future factory
    widgets reaching them) and flattening a deliberately arranged desktop layout on the way back.
  • Tiles stay inside their tiles. Content larger than its cell escaped the frame and painted over
    neighbouring panels; it is now contained, with a visible scroll affordance where it scrolls. Pulse
    stays readable at its minimum height instead of clipping its own figures.
  • A failed fetch no longer reads as good news. Attention, Conversations, Briefing and Board
    rendered a backend failure as a successful empty state — a dead approvals endpoint said "Nothing
    needs your attention". All nine tiles now report an unavailable source as unavailable.

Known issues

  • The Privacy page bounces to login, for the same reason the home page did: privacy registers
    before auth, so /api/v1/privacy/* never passes through the auth middleware. Its handlers use
    requirePermission, so the endpoints fail closed rather than being exposed — but the page is
    unusable. Not fixed here: privacy's registration position is load-bearing for model wrapping.
  • 17 route segments with mutating endpoints have no CSRF pairing (a2a, artifacts, client-wiki,
    connections, costops, data-port, federation, ideas, intel, internal, ops, privacy, prompt-coach,
    skill-generation, system, team-sessions, voice). Pre-existing; frozen in a self-checking baseline
    so the list cannot grow unnoticed.
  • Disabling auth would start every module unprotected. startAll never checks that a live
    module's declared dependencies are enabled, and EyasModule.required is declared but read nowhere.
  • Visual layout was verified by a human on one screen size only; jsdom performs no layout, so no
    automated test covers rendering, drag-and-drop or resize.