Releases: eyssen/eyas
Release list
EYAS v0.8.23-beta — Nothing said is lost
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_rawrow 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 insideaddMessageitself, 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'sLlmResponseappend and joined
to its conversation throughagent_sessions. - Trust is assigned at capture and never inherited from context. Your own
messages areowner, model-authored text isderived, 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 passeschunkTokens(8000
estimated tokens), a once-a-minute sweep finds it idle foridleFlushMinutes
(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 fromkey: valuelines (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 undermemory/v2imports 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 avalid_untiland aninvalidated_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 = NULLandmodel_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 asExecute the following command in the pod: ….
Configuration
Seven new keys, all under memory:
memory.l0.enabled(defaulttrue) — the master switch for raw capture.memory.l0.extractInLegacy(defaulttrue) — derive facts and gists even
thoughengineis stilllegacy. Set it tofalseto keep the raw text and
derive nothing.memory.engine(defaultlegacy) — selects the read/write engine. Today
it gates only extraction; it does not switch retrieval yet.memory.l0.chunkTokens(8000) andmemory.l0.idleFlushMinutes(30) —
the two size and time flush triggers.memory.l0.captureToolResults(defaultfalse) — 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_commandstdout,
read_filecontents and a livebrowser_totpone-time code all land in the
raw layer as plain text. Nothing redacts them and nothing encrypts them at
rest —dek_idis 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 doctorreports 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-wasm0.0.27 (MIT), used only when
neither runtime provides zstd natively. - Every EYAS database connection now runs
PRAGMA synchronous = NORMAL
instead of SQLite's defaultFULL. 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 noeyas memorycommand in this release: the raw
rows, facts and gists are written and then wait. Recall, embeddings and the
context assembler are the next wave. Settingmemory.engine: v2today 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
[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/webhas
its ownpackage.jsonand is not a bun workspace, so rootbun install
never put Vite or@vitejs/plugin-reacton disk. The next step —bunx vite build— then died withCannot find package '@vitejs/plugin-react'after
a successful-looking root install. The installer (andeyas start/
eyas update apply/ the Docker image) nowbun installssrc/webfirst,
retries the UI build once if a package is still missing, and skipslink:
deps that are notbun 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
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 atdocker 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.ymllives
in this repository so the next orphan snapshot of publicmainkeeps the
Sponsor button. Tiers, the $1,000/month model-bill goal, and the full list
are inSPONSORS.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
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).*matchedboard.task.assignedthrough 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, andSECURITY.mdnow names that channel
instead of leaving reporters with a public issue.CONTRIBUTING.mdleads
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
[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_memoryincludes those messages by default. Hits are labelled
conversation. The current thread is excluded. Other projects stay out
unlessscope=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 stillsearch_memoryfor 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
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 ongeneral-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 readAGENTS.md; the two never met. The loader now
takes a non-empty DB prompt first (file fallback), applies+/ empty /
override, and a form save materializesAGENTS.mdas 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_memorydefaults to the current project and type. Other
projects stay out unless the model passesscope=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 usedefaultConnectionId. An explicit
connectionIdwins. 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 writedecision-<id>there instead of
the vault when the conversation has a project. Human saves take ownership.
general-generalgets 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 statusandgit diffskip 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:
settingSourcesstay empty; hostMEMORY.mdis not loaded. - Telegram
/newand/startstart 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 getkind: reference, neveruser.MEMORY.mdindexes
andclaude-sessionstranscripts stay out even if everything is selected.
A home scan stays in assistant folders and Documents —GitHuband other
source trees are not walked. Grok memory files that symlink into
ai-memorycount 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
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 sixhyperframes_*tools. Node.js 22+,
FFmpeg, and the Hyperframes CLI are required; missing any of them fail
closed with a remedy. Hyperframes downloads its ownchrome-headless-shell
— neverEYAS_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/installis 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 frombrowser_snapshot(click/fill by index; CSS is
the fallback). Indexes andsnapshotIddie 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, pageevaluate, download → Documents,
PlaywrightstorageState, and an EYAS-owneduserDataDir
(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_fillwithintentstores a durable CSS/role locator in vault
JSON (projects/<id>/orprocedural/browser-action-cache.json).
browser_replayreuses 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 forbrowser_fill. The seed never
leaves that call.- Snapshot and locator scripts run as IIFEs. Playwright
evaluateof 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). Toolsagent_browser_status/
agent_browser_run(argv or batch JSON,@e1refs). MCP catalog
agent-browser mcp --tools core,state→mcp_agent_browser_*. EYAS-owned
--profile(data/browser/agent-browser/profile). Daily Chrome /
--profile Default/--auto-connect/chat/--tools allrefused.
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-sandboxis 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 typechrome-devtools-mcpplus MCP catalog
(npx chrome-devtools-mcp@latest --isolated). Console, network,
Lighthouse, WebMCP. Not form-filling (browser_*stays the form
lane). Tools arrive asmcp_chrome-devtools_*. WebMCP
(list_webmcp_tools/execute_webmcp_tool) only if the sidecar
advertises them.--autoConnectand the daily Chrome profile refused.
--no-sandboxstripped. 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 compositionestimated_tokensover
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
fromtokensUsed. - The open chat overflowed the viewport by about a centimetre. The page
sized itself with100vhminus the top bar and ignored the status bar
plus leftover main padding. New messages then calledscrollIntoView,
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 aglass-card— 3%
white in dark mode — so the page bled through the names. It now uses
the same solidpopoversurface 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-integrationcan 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
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.enabledinconfig/default.yamlswitches 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: theheartbeat
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. Oneunparsablerow 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 beisolated— no
filesystem settings, no CLI-native memory or config, no bridged tools, a
single turn — and Claude Code honours it whatever itsloadClaudeMdsetting
says. Without it the extraction call loaded the owner's~/.claudememory,
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 aprovidercolumn holdingprovider/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
namessearch_memoryfor recall andsave_memoryfor recording, states that
EYAS's memory is the only memory, and forbids writing to a machine-global
memory directory, anai-memoryor Obsidian vault,~/.claudeor~/.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,GrepandGlobstay open — the data-port importer exists
to carry exactly those notes into EYAS — but the shell is blocked in both
directions, becausecatis one character from>>and no reading of a
command string proves which one it is. The denied set is narrow on purpose:
anai-memorydirectory, a home-anchored~/.claudeor~/.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.mdis 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
owndocs/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\bis ASCII-only, so\bűrlapnever
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, aprojectcandidate
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
## Historyand 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 globaluserandfeedbackfirst, then the
ACTIVE project'sprojectnotes, thenreference; another project's notes
never appear at all. Project notes live inprojects/<project-id>/with a
projectfrontmatter 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 intogeneral-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_linksnames the
conversation that wrote a note or later reinforced it, in the same multi-owner
shape asdesign_linksanddocument_links, and episodic memories now carry
conversation_idandproject_id. - Every outcome that reached the gate writes a
memory_capture_runsrow —
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....
EYAS v0.8.15-beta — Designs your agents follow
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.htmlartboards, acanvas.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'sclass Component extends DCLogic— so clickable
prototypes, variant switches and selection state work. - The isolation that makes executing AI-authored JavaScript acceptable: a
srcdociframe withsandbox="allow-scripts"and neverallow-same-origin, a
CSP inside the srcdoc withconnect-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, alaunchpointing at
nothing, a stray top-level key incanvas.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_writeanddesign_create,
allcategory: '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-contextsection; 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 throughrepeat(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.htmlfile 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-propsre-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 markedis_interactivenever 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 withpdf-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 sameARTBOARD_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 orrenderVals()throws. A PDF whose only content is
"renderVals() threw: …" is worse than an error message. - The browser is optional and says so.
playwright-coreis a real
dependency (Apache-2.0, no postinstall, no runtime dependencies of its own);
the ~150 MB browser binary is not. It is resolved fromEYAS_CHROMIUM_PATH,
then Playwright's own registry, then known system paths, and when there is
none/api/v1/designs/print-statusanswersavailable: falsewith 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 explicitEYAS_CHROMIUM_NO_SANDBOX=1, and the error
message says so. playwrightis 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-guardwhere the headless browser can apply them per request.- New dependencies:
playwright-core(Apache-2.0) andpdf-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 routesonWheelthrough a passive root
listener wherepreventDefault()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 makesartboardEntry.expanddo something:
fitshrinks the whole artboard to the viewport,fillwidens 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 everyis_interactiveprototype 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.statusis gone. It rendered a badge in the list and ...
EYAS v0.8.14-beta — Shape your own landing page
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/smarrange 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
homemodule 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:
SetupRecommendationsCardfired 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/homedocuments 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.
homecreated its routes inonRegister, which
runs for every module before any module'sonStart— 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 inonStartandhomedeclaresauthas 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 ownc.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.tsruns the
real dependency resolver over the real registration order and fails if a module that mounts routes
lands beforeauth, or if an/api/v1segment is neither covered nor on a named public list.
Fixed
- Each breakpoint keeps its own arrangement. The grid loaded the layout once at
lgand never
reloaded, so crossing a width threshold made the library derive anmdlayout from thelgone
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:
privacyregisters
beforeauth, 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
authwould start every module unprotected.startAllnever checks that a live
module's declared dependencies are enabled, andEyasModule.requiredis 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.